forked from HEL/circuiteria
Add an optional pentagon shape for block and group ports, configured via the element-level `port-shape`, `port-kind` and `port-size` parameters. Individual ports can override them with `shape`, `kind`, `size`, `stroke` and `fill`. Clock ports with a pentagon shape drop the clock triangle and default to a gray fill. Wires attach to the outer end of the pentagon by default. With the new `shell` parameter on `element.elmt` they attach to the inner tip instead. Each port is exposed as a CeTZ group with a `default` anchor plus the cardinal sub-anchors `.east`, `.west`, `.north` and `.south` on the pentagon's bounding box, so wires can attach to any face. For non-pentagon ports the sub-anchors collapse to the default attach point, and the bare `<elmt-id>-port-<port-id>` anchor resolves as before. Add the `port-shape` and `port-shape-attach` examples and the `gallery/test_port_shape` gallery entry.
206 lines
6.1 KiB
Typst
206 lines
6.1 KiB
Typst
/// 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
|
|
}
|