forked from HEL/circuiteria
Document the per-port `shape`, `kind`, `size`, `stroke`, and `fill` overrides in the ports dictionary of `element.elmt`, simplify the `port-shape` parameter description, and update `port-shape-vertices` return signature. Also reformat source and examples for consistency and rebuild manual.pdf.
226 lines
6.8 KiB
Typst
226 lines
6.8 KiB
Typst
#import "cetz.typ": coordinate
|
|
|
|
/// Resolves a relative coordinate shorthand of the form `(rel: <number>, to: <anchor>)`
|
|
/// to a single scalar along the requested axis.
|
|
///
|
|
/// Used to implement the shared `(rel:, to:)` shorthand accepted by the `x` and
|
|
/// `y` parameters of `elmt`, `gate` and `iec-gate`.
|
|
///
|
|
/// - ctx (dictionary): The current CeTZ context
|
|
/// - val (dictionary): A dictionary with fields `rel` (number) and `to` (anchor)
|
|
/// - axis (int): `0` for x, `1` for y
|
|
/// -> number
|
|
#let resolve-rel-coord(ctx, val, axis) = {
|
|
let offset = val.rel
|
|
let to = val.to
|
|
let delta = if axis == 0 { (offset, 0) } else { (0, offset) }
|
|
let (_, to-pos) = coordinate.resolve(ctx, (rel: delta, to: to))
|
|
return to-pos.at(axis)
|
|
}
|
|
|
|
/// Predefined color palette
|
|
/// #let w = 4
|
|
/// #box(width: 100%, align(center)[
|
|
/// #canvas(length: 2em, {
|
|
/// for (i, color) in util.colors.pairs().enumerate() {
|
|
/// let x = calc.rem(i, w) * 2
|
|
/// let y = calc.floor(i / w) * 2
|
|
/// draw.rect((x, -y), (x + 1, -y - 1), fill: color.last(), stroke: none)
|
|
/// draw.content((x + 0.5, -y - 1.25), color.first())
|
|
/// }
|
|
/// })
|
|
/// ])
|
|
#let colors = (
|
|
orange: rgb(245, 180, 147),
|
|
yellow: rgb(250, 225, 127),
|
|
green: rgb(127, 200, 172),
|
|
pink: rgb(236, 127, 178),
|
|
purple: rgb(189, 151, 255),
|
|
blue: rgb(127, 203, 235),
|
|
)
|
|
|
|
/// Pads a string on the left with 0s to the given length
|
|
///
|
|
/// #example(`#util.lpad("0100", 8)`, mode: "markup")
|
|
///
|
|
/// - string (str): The string to pad
|
|
/// - len (int): The target length
|
|
/// -> str
|
|
#let lpad(string, len) = {
|
|
let res = "0" * len + string
|
|
return res.slice(-len)
|
|
}
|
|
|
|
/// Returns the anchor on the opposite side of the given one
|
|
///
|
|
/// #example(`#util.opposite-anchor("west")`, mode: "markup")
|
|
///
|
|
/// - anchor (str): The input anchor
|
|
/// -> str
|
|
#let opposite-anchor(anchor) = {
|
|
return (
|
|
north: "south",
|
|
east: "west",
|
|
south: "north",
|
|
west: "east",
|
|
|
|
north-west: "south-east",
|
|
north-east: "south-west",
|
|
south-east: "north-west",
|
|
south-west: "north-east",
|
|
).at(anchor)
|
|
}
|
|
|
|
/// Returns the anchor rotated 90 degrees clockwise relative to the given one
|
|
///
|
|
/// #example(`#util.rotate-anchor("west")`, mode: "markup")
|
|
/// - anchor (str): The anchor to rotate
|
|
/// -> str
|
|
#let rotate-anchor(anchor) = {
|
|
return (
|
|
north: "east",
|
|
east: "south",
|
|
south: "west",
|
|
west: "north",
|
|
|
|
north-west: "north-east",
|
|
north-east: "south-east",
|
|
south-east: "south-west",
|
|
south-west: "north-west",
|
|
).at(anchor)
|
|
}
|
|
|
|
/// Returns the pentagon port shape vertices and the outer/inner anchor
|
|
/// offsets for the given side and direction.
|
|
///
|
|
/// The pentagon straddles the block boundary: 1 unit outside, 1 unit inside;
|
|
/// height ±0.5 units. For `kind: "in"` the tip points into the block; for
|
|
/// `kind: "out"` it points away.
|
|
///
|
|
/// - side (str): One of `"north"`, `"east"`, `"south"`, `"west"`
|
|
/// - kind (str): `"in"` or `"out"`
|
|
/// - u (length,number): The pentagon unit size
|
|
/// -> dictionary: `(vertices: (..5 pairs..), outer: (dx, dy), inner: (dx, dy))`
|
|
#let port-shape-vertices(side, kind, u) = {
|
|
// Base case: west port, input (tip east / into block).
|
|
// Vertices as (dx, dy) offsets relative to the boundary anchor.
|
|
let base-vertices = (
|
|
(-u, -u / 2),
|
|
(-u, +u / 2),
|
|
(u / 2, +u / 2),
|
|
(u, 0), // tip east (inner side)
|
|
(u / 2, -u / 2),
|
|
)
|
|
// For output, mirror horizontally so the tip points west (outside).
|
|
if kind == "out" {
|
|
base-vertices = base-vertices.map(p => (-p.at(0), p.at(1)))
|
|
}
|
|
// Outer (wire-connection) and inner (inside-label) anchors for a west port.
|
|
// Outer is always 1u outside the boundary along the port normal; inner is 1u
|
|
// inside. Independent of `kind` (only the pentagon fill/tip direction change).
|
|
let base-outer = (-u, 0)
|
|
let base-inner = (u, 0)
|
|
|
|
// Rotation from west's frame to target side:
|
|
// west: identity (x, y)
|
|
// north: (x, y) → (y, -x)
|
|
// east: (x, y) → (-x, -y)
|
|
// south: (x, y) → (-y, x)
|
|
let rot = if side == "west" { p => p } else if side == "north" {
|
|
p => (p.at(1), -p.at(0))
|
|
} else if side == "east" { p => (-p.at(0), -p.at(1)) } else if side == "south" {
|
|
p => (-p.at(1), p.at(0))
|
|
} else { panic("Unknown side: " + side) }
|
|
|
|
// World-cardinal midpoints of the pentagon's bounding box. These are
|
|
// named in *world* coordinates (not rotated with the shape), so `.east`
|
|
// always sits at the world +x extent of the pentagon, `.north` at +y, etc.
|
|
// Consequently the tip's cardinal name follows its world direction:
|
|
// - west side, kind "in" → tip world +x → tip = .east
|
|
// - west side, kind "out" → tip world -x → tip = .west
|
|
// - east side, kind "in" → tip world -x → tip = .west
|
|
// - east side, kind "out" → tip world +x → tip = .east
|
|
// - north side, kind "in" → tip world -y → tip = .south
|
|
// - north side, kind "out" → tip world +y → tip = .north
|
|
// - south side, kind "in" → tip world +y → tip = .north
|
|
// - south side, kind "out" → tip world -y → tip = .south
|
|
//
|
|
// The pentagon bounding box extents depend only on the side's orientation
|
|
// (horizontal for west/east, vertical for north/south), not on `kind`.
|
|
let (hx, hy) = if side == "west" or side == "east" {
|
|
(u, u / 2)
|
|
} else {
|
|
(u / 2, u)
|
|
}
|
|
let world-east = (hx, 0)
|
|
let world-west = (-hx, 0)
|
|
let world-north = (0, hy)
|
|
let world-south = (0, -hy)
|
|
|
|
return (
|
|
vertices: base-vertices.map(rot),
|
|
outer: rot(base-outer),
|
|
inner: rot(base-inner),
|
|
east: world-east,
|
|
west: world-west,
|
|
north: world-north,
|
|
south: world-south,
|
|
)
|
|
}
|
|
|
|
#let valid-anchors = (
|
|
"center",
|
|
"north",
|
|
"east",
|
|
"west",
|
|
"south",
|
|
"north-east",
|
|
"north-west",
|
|
"south-east",
|
|
"south-west",
|
|
)
|
|
|
|
/// Normalizes a coordinate so that 2D points (x, y) are padded
|
|
/// to (x, y, 0) as required by CeTZ's coordinate resolver.
|
|
/// Also handles function-based and dictionary-based coordinates
|
|
/// recursively.
|
|
///
|
|
/// - coord (any): A CeTZ-compatible coordinate
|
|
/// -> any
|
|
#let normalize-coord(coord) = {
|
|
if type(coord) == array {
|
|
if coord.len() == 2 {
|
|
let (a, b) = coord
|
|
// Raw 2D point: (x, y) -> (x, y, 0)
|
|
if type(a) in (int, float) and type(b) in (int, float) {
|
|
return (a, b, 0)
|
|
}
|
|
// Function-based coordinate: (fn, point)
|
|
if type(a) == function {
|
|
let orig = a
|
|
return (
|
|
v => {
|
|
let result = orig(v)
|
|
if type(result) == array and result.len() == 2 {
|
|
let (rx, ry) = result
|
|
if type(rx) in (int, float) and type(ry) in (int, float) {
|
|
return (rx, ry, 0)
|
|
}
|
|
}
|
|
return result
|
|
},
|
|
b,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
if type(coord) == dictionary {
|
|
let result = (:)
|
|
for (key, value) in coord {
|
|
result.insert(key, normalize-coord(value))
|
|
}
|
|
return result
|
|
}
|
|
return coord
|
|
}
|