docs: document per-port shape overrides and rebuild manual

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.
This commit is contained in:
David Stadelmann committed 2026-09-16 10:07:13 +02:00
1 parent 663614056e
commit a839dbb6d2
5 files changed
+7914 -7800

No files matched your search

+200 -154
View File
@@ -23,30 +23,33 @@ wire.stub("block-port-in2", "west")
wire.stub("block-port-out", "east") wire.stub("block-port-out", "east")
```) ```)
#let align-with-ports = example(``` #let align-with-ports = example(
// `align-with` reuses another port's coord ```
// (E/W: y, N/S: x). Forms: // `align-with` reuses another port's coord
// 1. "id" — local // (E/W: y, N/S: x). Forms:
// 2. (element:"e", port:"p") — cross-elmt // 1. "id" — local
// 3. "raw-anchor" — CeTZ anchor // 2. (element:"e", port:"p") — cross-elmt
element.block(x: 0, y: 0, w: 2, h: 2, id: "b1", // 3. "raw-anchor" — CeTZ anchor
element.block(x: 0, y: 0, w: 2, h: 2, id: "b1",
ports: ( ports: (
west: ((id: "a", name: "A", y: 30%),), west: ((id: "a", name: "A", y: 30%),),
east: ((id: "oa", name: "A'", east: ((id: "oa", name: "A'",
"align-with": "a"),), "align-with": "a"),),
)) ))
element.block(x: 4, y: -.3, w: 2, h: 2.6, id: "b2", element.block(x: 4, y: -.3, w: 2, h: 2.6, id: "b2",
ports: ( ports: (
west: ((id: "ia", name: "A", west: ((id: "ia", name: "A",
"align-with": "align-with":
(element: "b1", port: "oa")),), (element: "b1", port: "oa")),),
east: ((id: "o", name: "C"),), east: ((id: "o", name: "C"),),
)) ))
wire.stub("b1-port-a", "west") wire.stub("b1-port-a", "west")
wire.wire("w", ("b1-port-oa", "b2-port-ia"), wire.wire("w", ("b1-port-oa", "b2-port-ia"),
style: "direct") style: "direct")
wire.stub("b2-port-o", "east") wire.stub("b2-port-o", "east")
```, vertical: true) ```,
vertical: true,
)
#let extender = example(``` #let extender = example(```
element.extender( element.extender(
@@ -80,23 +83,26 @@ for i in range(3) {
} }
```) ```)
#let wires = example(``` #let wires = example(
for i in range(5) { ```
for i in range(5) {
draw.circle((i * 3, 0), radius: .1, name: "p" + str(i * 2)) draw.circle((i * 3, 0), radius: .1, name: "p" + str(i * 2))
draw.circle((i * 3 + 2, 1), radius: .1, name: "p" + str(i * 2 + 1)) draw.circle((i * 3 + 2, 1), radius: .1, name: "p" + str(i * 2 + 1))
draw.content((i * 3 + 1, -1), raw(wire.wire-styles.at(i))) draw.content((i * 3 + 1, -1), raw(wire.wire-styles.at(i)))
} }
wire.wire("w1", ("p0", "p1"), style: "direct") wire.wire("w1", ("p0", "p1"), style: "direct")
wire.wire("w2", ("p2", "p3"), style: "zigzag") wire.wire("w2", ("p2", "p3"), style: "zigzag")
wire.wire("w3", ("p4", "p5"), style: "dodge", wire.wire("w3", ("p4", "p5"), style: "dodge",
dodge-y: -0.5, dodge-margins: (0.5, 0.5)) dodge-y: -0.5, dodge-margins: (0.5, 0.5))
wire.wire("w4", ("p6","p7"), style: "guided", wire.wire("w4", ("p6","p7"), style: "guided",
guided-center:(20%, 40%), guided-margins: (90%,87%), guided-center:(20%, 40%), guided-margins: (90%,87%),
guided-sides: ("north","south")) guided-sides: ("north","south"))
wire.wire("w5", ("p8", "p9"), style: "u-shape", wire.wire("w5", ("p8", "p9"), style: "u-shape",
u-side: "east", u-depth: 0.5) u-side: "east", u-depth: 0.5)
```, vertical: true) ```,
vertical: true,
)
#let stub = example(``` #let stub = example(```
draw.circle((0, 0), radius: .1, name: "p") draw.circle((0, 0), radius: .1, name: "p")
@@ -134,18 +140,21 @@ for (i, dir) in dirs.enumerate() {
} }
```) ```)
#let breakout-label = example(``` #let breakout-label = example(
// name-pos matches direction → label sits at the end point. ```
wire.breakout("b1", x: 0, y: 0, direction: "north-east", // name-pos matches direction → label sits at the end point.
wire.breakout("b1", x: 0, y: 0, direction: "north-east",
port-side: "east", port-side: "east",
name: "at end", name-pos: "north-east") name: "at end", name-pos: "north-east")
// name-pos opposite to direction → label would cover the line, so // name-pos opposite to direction → label would cover the line, so
// it falls back to the start point. // it falls back to the start point.
wire.breakout("b2", x: 4, y: 0, direction: "north-east", wire.breakout("b2", x: 4, y: 0, direction: "north-east",
port-side: "east", port-side: "east",
name: "at start", name-pos: "south-west") name: "at start", name-pos: "south-west")
```, vertical: true) ```,
vertical: true,
)
#let breakout-wire = example(``` #let breakout-wire = example(```
// Bus fan-out: a horizontal wire from the left block to the lower // Bus fan-out: a horizontal wire from the left block to the lower
@@ -181,85 +190,133 @@ wire.wire("wtap", ("tap-port-tap", "dstU-port-in"),
zigzag-ratio: 100%) zigzag-ratio: 100%)
```) ```)
#let gate-and = example(``` #let gate-and = example(
gates.gate-and(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-and(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-nand = example(``` #let gate-nand = example(
gates.gate-nand(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-nand(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-nand(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-nand(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-buf = example(``` #let gate-buf = example(
gates.gate-buf(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-buf(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-buf(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-buf(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-not = example(``` #let gate-not = example(
gates.gate-not(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-not(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-not(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-not(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-or = example(``` #let gate-or = example(
gates.gate-or(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-or(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-or(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-or(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-nor = example(``` #let gate-nor = example(
gates.gate-nor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-nor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-nor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-nor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-xor = example(``` #let gate-xor = example(
gates.gate-xor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-xor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-xor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-xor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let gate-xnor = example(``` #let gate-xnor = example(
gates.gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-and = example(``` #let iec-gate-and = example(
gates.iec-gate-and(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-and(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-nand = example(``` #let iec-gate-nand = example(
gates.iec-gate-nand(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-nand(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-nand(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-nand(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-or = example(``` #let iec-gate-or = example(
gates.iec-gate-or(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-or(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-or(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-or(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-nor = example(``` #let iec-gate-nor = example(
gates.iec-gate-nor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-nor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-nor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-nor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-xor = example(``` #let iec-gate-xor = example(
gates.iec-gate-xor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-xor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-xor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-xor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-xnor = example(``` #let iec-gate-xnor = example(
gates.iec-gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-buf = example(``` #let iec-gate-buf = example(
gates.iec-gate-buf(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-buf(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-buf(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-buf(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let iec-gate-not = example(``` #let iec-gate-not = example(
gates.iec-gate-not(x: 0, y: 0, w: 1.5, h: 1.5) ```
gates.iec-gate-not(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all") gates.iec-gate-not(x: 0, y: 0, w: 1.5, h: 1.5)
```, vertical: true) gates.iec-gate-not(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```,
vertical: true,
)
#let group = example(``` #let group = example(```
element.group( element.group(
@@ -288,9 +345,10 @@ wire.wire("w2", ("b2-port-out", "b3-port-in2"),
style: "zigzag") style: "zigzag")
```) ```)
#let group-ports = example(``` #let group-ports = example(
// outside-port-names: true (default) — labels outside the border ```
element.group( // outside-port-names: true (default) — labels outside the border
element.group(
id: "g1", name: "outside (default)", id: "g1", name: "outside (default)",
stroke: (dash: "dashed"), stroke: (dash: "dashed"),
fill: util.colors.blue.lighten(60%), fill: util.colors.blue.lighten(60%),
@@ -305,13 +363,13 @@ element.group(
fill: util.colors.green, fill: util.colors.green,
ports: (west: ((id: "d", name: "d"),), ports: (west: ((id: "d", name: "d"),),
east: ((id: "q", name: "q"),))) } east: ((id: "q", name: "q"),))) }
) )
wire.wire("wG1in", ("g1-port-in", "b1-port-d"), directed: true) wire.wire("wG1in", ("g1-port-in", "b1-port-d"), directed: true)
wire.wire("wG1out", ("b1-port-q", "g1-port-out"), directed: true) wire.wire("wG1out", ("b1-port-q", "g1-port-out"), directed: true)
// outside-port-names: false — labels inside the border (block style) // outside-port-names: false — labels inside the border (block style)
// with extra padding via port-name-padding // with extra padding via port-name-padding
element.group( element.group(
id: "g2", name: "inside", id: "g2", name: "inside",
stroke: (dash: "dashed"), stroke: (dash: "dashed"),
fill: util.colors.orange.lighten(60%), fill: util.colors.orange.lighten(60%),
@@ -325,10 +383,12 @@ element.group(
{ element.block(id: "b2", w: 2, h: 1.5, x: 6, y: 0, { element.block(id: "b2", w: 2, h: 1.5, x: 6, y: 0,
fill: util.colors.green, fill: util.colors.green,
ports: (west: ((id: "d"),), east: ((id: "q"),))) } ports: (west: ((id: "d"),), east: ((id: "q"),))) }
)
wire.stub("g2-port-in", "west")
wire.stub("g2-port-out", "east")
```,
vertical: true,
) )
wire.stub("g2-port-in", "west")
wire.stub("g2-port-out", "east")
```, vertical: true)
#let port-names-outside = example(``` #let port-names-outside = example(```
element.block( element.block(
@@ -406,85 +466,71 @@ wire.wire("w2", ((0, 0), (1, -.5)),
wire.intersection("w1.zig") wire.intersection("w1.zig")
```) ```)
#let u-shape-wire = example(``` #let u-shape-wire = example(
// east <-> east (u-side auto-detected) ```
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA", // east <-> east (u-side auto-detected)
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA",
fill: util.colors.orange, fill: util.colors.orange,
ports: (east: ((id: "out"),))) ports: (east: ((id: "out"),)))
element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB", element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB",
fill: util.colors.orange, fill: util.colors.orange,
ports: (east: ((id: "out"),))) ports: (east: ((id: "out"),)))
wire.wire("uEE", ("eA-port-out", "eB-port-out"), wire.wire("uEE", ("eA-port-out", "eB-port-out"),
style: "u-shape", u-depth: 0.5) style: "u-shape", u-depth: 0.5)
// west <-> west (auto-detected) // west <-> west (auto-detected)
element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA", element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA",
fill: util.colors.yellow, fill: util.colors.yellow,
ports: (west: ((id: "in"),))) ports: (west: ((id: "in"),)))
element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB", element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB",
fill: util.colors.yellow, fill: util.colors.yellow,
ports: (west: ((id: "in"),))) ports: (west: ((id: "in"),)))
wire.wire("uWW", ("wA-port-in", "wB-port-in"), wire.wire("uWW", ("wA-port-in", "wB-port-in"),
style: "u-shape", u-depth: 0.5) style: "u-shape", u-depth: 0.5)
// north <-> north (auto-detected) // north <-> north (auto-detected)
element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA", element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA",
fill: util.colors.green, fill: util.colors.green,
ports: (north: ((id: "out"),))) ports: (north: ((id: "out"),)))
element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB", element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB",
fill: util.colors.green, fill: util.colors.green,
ports: (north: ((id: "out"),))) ports: (north: ((id: "out"),)))
wire.wire("uNN", ("nA-port-out", "nB-port-out"), wire.wire("uNN", ("nA-port-out", "nB-port-out"),
style: "u-shape", u-depth: 0.5) style: "u-shape", u-depth: 0.5)
// south <-> south with explicit u-side override // south <-> south with explicit u-side override
element.block(x: 16, y: -3, w: 1.5, h: 1.5, id: "sA", element.block(x: 16, y: -3, w: 1.5, h: 1.5, id: "sA",
fill: util.colors.pink, fill: util.colors.pink,
ports: (south: ((id: "out"),))) ports: (south: ((id: "out"),)))
element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB", element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB",
fill: util.colors.pink, fill: util.colors.pink,
ports: (south: ((id: "out"),))) ports: (south: ((id: "out"),)))
wire.wire("uSS", ("sA-port-out", "sB-port-out"), wire.wire("uSS", ("sA-port-out", "sB-port-out"),
style: "u-shape", u-side: "south", u-depth: 0.5) style: "u-shape", u-side: "south", u-depth: 0.5)
```, vertical: true) ```,
vertical: true,
)
#let zigzag-at = example(``` #let zigzag-at = example(
// `zigzag-at` places the middle bend of a zigzag wire ```
// on the axis of a referenced anchor, overriding // `zigzag-at` places the middle bend of a zigzag wire
// `zigzag-ratio`. // on the axis of a referenced anchor, overriding
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "src", // `zigzag-ratio`.
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "src",
ports: (east: ((id: "o", name: "O"),),)) ports: (east: ((id: "o", name: "O"),),))
element.block(x: 3, y: -1.2, w: 1, h: 1, id: "ref", element.block(x: 3, y: -1.2, w: 1, h: 1, id: "ref",
fill: util.colors.pink, fill: util.colors.pink,
ports: (west: ((id: "p", name: "R"),),)) ports: (west: ((id: "p", name: "R"),),))
element.block(x: 5.5, y: -2, w: 1.5, h: 1.5, id: "dst", element.block(x: 5.5, y: -2, w: 1.5, h: 1.5, id: "dst",
ports: (west: ((id: "i", name: "I"),),)) ports: (west: ((id: "i", name: "I"),),))
// String form: anchor name of a port. // String form: anchor name of a port.
wire.wire("w1", ("src-port-o", "dst-port-i"), wire.wire("w1", ("src-port-o", "dst-port-i"),
style: "zigzag", style: "zigzag",
zigzag-at: "ref-port-p") zigzag-at: "ref-port-p")
// Ergonomic offset: shift 8pt right of ref.east. // Ergonomic offset: shift 8pt right of ref.east.
wire.wire("w2", ("src-port-o", "dst-port-i"), wire.wire("w2", ("src-port-o", "dst-port-i"),
style: "zigzag", style: "zigzag",
zigzag-at: (rel: 8pt, to: "ref.east")) zigzag-at: (rel: 8pt, to: "ref.east"))
```, vertical: true) ```,
vertical: true,
#let port-shape-attach = example(``` )
// Pentagon ports expose `.east`, `.west`, `.north`,
// `.south` sub-anchors on the pentagon boundary,
// letting wires attach to any face.
element.block(x: 0, y: 0, w: 2, h: 2, id: "a",
port-shape: "pentagon",
ports: (west: ((id: "p", name: "P"),),))
draw.circle((-2, 0.5), radius: .05, name: "L")
draw.circle(( 3, 1.5), radius: .05, name: "R")
draw.circle((-1, 2.5), radius: .05, name: "T")
// .west = outer flat base
wire.wire("w1", ("L", "a-port-p.west"), style: "direct")
// .east = tip inside the block
wire.wire("w2", ("R", "a-port-p.east"),
style: "direct", color: red)
// .north = top edge midpoint
wire.wire("w3", ("T", "a-port-p.north"),
style: "direct", color: blue)
```, vertical: true)
+7585 -7585
View File
File diff suppressed because it is too large. Load diff
+16 -8
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw, coordinate #import "/src/cetz.typ": coordinate, draw
#import "ports.typ": add-ports, add-port #import "ports.typ": add-port, add-ports
#import "../util.typ" #import "../util.typ"
#let find-port(ports, id) = { #let find-port(ports, id) = {
@@ -39,10 +39,15 @@
/// - `align-with` (`str`, `dictionary`): Optional. Reuse another port's placement coordinate (east/west: y, north/south: x). Accepted forms: `"<local-id>"` (same-element port; axis must match, transitively resolved, cycles panic), `(element: "<id>", port: "<id>")` (cross-element; axis not validated; target element must be drawn first), or `"<raw-anchor>"` (any unrecognised string is used as a CeTZ anchor name). See #examples.align-with-ports /// - `align-with` (`str`, `dictionary`): Optional. Reuse another port's placement coordinate (east/west: y, north/south: x). Accepted forms: `"<local-id>"` (same-element port; axis must match, transitively resolved, cycles panic), `(element: "<id>", port: "<id>")` (cross-element; axis not validated; target element must be drawn first), or `"<raw-anchor>"` (any unrecognised string is used as a CeTZ anchor name). See #examples.align-with-ports
/// - `x` (length, ratio, function): Optional. North/south ports only. Explicit x-offset override measured from the left corner of the side. A `ratio` is used as a percentage along the side; a length/number is used as an absolute offset; a function is called with the element width and must return a length/number. Panics if used together with `align-with` or on an east/west port. /// - `x` (length, ratio, function): Optional. North/south ports only. Explicit x-offset override measured from the left corner of the side. A `ratio` is used as a percentage along the side; a length/number is used as an absolute offset; a function is called with the element width and must return a length/number. Panics if used together with `align-with` or on an east/west port.
/// - `y` (length, ratio, function): Optional. East/west ports only. Analogous to `x`, measured from the top corner of the side and called with the element height when a function is passed. /// - `y` (length, ratio, function): Optional. East/west ports only. Analogous to `x`, measured from the top corner of the side and called with the element height when a function is passed.
/// - `shape` (`none`, `str`): Optional. Per-port override of the element-level `port-shape` (e.g. `"pentagon"`). Lets a single port on a block or group opt into (or out of) a shape independently of the element default. See the `port-shape` parameter below.
/// - `kind` (`str`): Optional. Per-port override of the element-level `port-kind` (`"in"` / `"out"`). Affects pentagon orientation.
/// - `size` (length): Optional. Per-port override of the element-level `port-size` (unit size of pentagon-shaped ports).
/// - `stroke` (stroke): Optional. Per-port override of the pentagon stroke. Defaults to the element stroke.
/// - `fill` (none, color): Optional. Per-port override of the pentagon fill. Defaults to the element fill (or `gray` for clock ports with `shape: "pentagon"`).
/// - ports-margins (dictionary): Dictionary of ports margins (used with automatic port placement). They keys are cardinal directions ("north", "east", "south", "west"). The values are tuples of (`<start>`, `<end>`) margins (numbers) /// - ports-margins (dictionary): Dictionary of ports margins (used with automatic port placement). They keys are cardinal directions ("north", "east", "south", "west"). The values are tuples of (`<start>`, `<end>`) margins (numbers)
/// - port-name-padding (number, length): Padding between the port name label and the port anchor point (default `2pt`) /// - port-name-padding (number, length): Padding between the port name label and the port anchor point (default `2pt`)
/// - port-names-outside (bool, auto): If true, port name labels are drawn on the outside of the block instead of the inside. When `auto` (default), follows the `shell` setting (outside if `shell: true`, inside otherwise) /// - port-names-outside (bool, auto): If true, port name labels are drawn on the outside of the block instead of the inside. When `auto` (default), follows the `shell` setting (outside if `shell: true`, inside otherwise)
/// - port-shape (none, str): Default port shape for all ports of this element. Set to `"pentagon"` to draw pentagon-shaped ports. Overridable per port via the port's `shape` field. Pentagon-shaped ports expose four cardinal sub-anchors `.east`, `.west`, `.north`, `.south` on the pentagon's bounding box (all lying on the pentagon boundary) so wires can attach to different faces (for a north/south-side port the pentagon is rotated, so `.north`/`.south` are the tip/outer base, and `.east`/`.west` are the transverse edge midpoints). For non-pentagon ports these sub-anchors degenerate to the default attach point. See #examples.port-shape and #examples.port-shape-attach /// - port-shape (none, str): Default port shape for all ports of this element. Set to `"pentagon"` to draw pentagon-shaped ports. Overridable per port via the port's `shape` field (see the ports dictionary above); a per-port `shape: none` also removes the shape for just that port. See #examples.port-shape
/// - port-kind (str): Default direction of ports (`"in"` or `"out"`). Overridable per port via the port's `kind` field /// - port-kind (str): Default direction of ports (`"in"` or `"out"`). Overridable per port via the port's `kind` field
/// - port-size (length): Default unit size of pentagon-shaped ports (default `1em`). Overridable per port via the port's `size` field /// - port-size (length): Default unit size of pentagon-shaped ports (default `1em`). Overridable per port via the port's `size` field
/// - fill (none, color): Fill color /// - fill (none, color): Fill color
@@ -78,8 +83,8 @@
shell: false, shell: false,
body: none, body: none,
debug: ( debug: (
ports: false ports: false,
) ),
) = draw.get-ctx(ctx => { ) = draw.get-ctx(ctx => {
let width = w let width = w
let height = h let height = h
@@ -144,9 +149,9 @@
if (name != none) { if (name != none) {
draw.content( draw.content(
(name: id, anchor: name-anchor), (name: id, anchor: name-anchor),
anchor: if name-anchor in util.valid-anchors {name-anchor} else {"center"}, anchor: if name-anchor in util.valid-anchors { name-anchor } else { "center" },
padding: 0.5em, padding: 0.5em,
align(center)[*#name*] align(center)[*#name*],
) )
} }
@@ -154,7 +159,10 @@
let outside-name = if port-names-outside == auto { shell } else { port-names-outside } let outside-name = if port-names-outside == auto { shell } else { port-names-outside }
add-ports( add-ports(
id, id,
tl, tr, br, bl, tl,
tr,
br,
bl,
ports, ports,
ports-margins, ports-margins,
width: width, width: width,
+83 -28
View File
@@ -1,8 +1,11 @@
#import "/src/cetz.typ": draw #import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor, opposite-anchor, port-shape-vertices #import "../util.typ": opposite-anchor, port-shape-vertices, rotate-anchor
#let add-port( #let add-port(
elmt-id, side, port, pos, elmt-id,
side,
port,
pos,
prev: none, prev: none,
next: none, next: none,
debug: false, debug: false,
@@ -27,8 +30,9 @@
// Pentagon fill defaults to the element's fill (so the pentagon covers the // Pentagon fill defaults to the element's fill (so the pentagon covers the
// block-border segment it crosses). Clock ports default to gray. Per-port // block-border segment it crosses). Clock ports default to gray. Per-port
// `fill` overrides both. // `fill` overrides both.
let shape-fill = port.at("fill", let shape-fill = port.at("fill", default: if is-clock and shape == "pentagon" { gray } else {
default: if is-clock and shape == "pentagon" { gray } else { elmt-fill }) elmt-fill
})
// When shape is enabled, wire-connection anchor moves outside the block, and // When shape is enabled, wire-connection anchor moves outside the block, and
// inside-name labels reference the inner tip/base; otherwise both collapse to // inside-name labels reference the inner tip/base; otherwise both collapse to
@@ -42,7 +46,11 @@
// Draw pentagon on a higher layer so it always renders on top of wires // Draw pentagon on a higher layer so it always renders on top of wires
// and neighbouring elements. // and neighbouring elements.
draw.on-layer(1, draw.line( draw.on-layer(1, draw.line(
v.at(0), v.at(1), v.at(2), v.at(3), v.at(4), v.at(0),
v.at(1),
v.at(2),
v.at(3),
v.at(4),
close: true, close: true,
stroke: shape-stroke, stroke: shape-stroke,
fill: shape-fill, fill: shape-fill,
@@ -54,12 +62,13 @@
panic("Clock port must have previous and next positions") panic("Clock port must have previous and next positions")
} }
let csize = if port.at("small", default: false) {8pt} else {1em} let csize = if port.at("small", default: false) { 8pt } else { 1em }
let offset let offset
if (side == "north") { offset = ( 0, -csize) } if (side == "north") { offset = (0, -csize) } else if (side == "east") {
else if (side == "east") { offset = (-csize, 0) } offset = (-csize, 0)
else if (side == "south") { offset = ( 0, csize) } } else if (side == "south") { offset = (0, csize) } else if (side == "west") {
else if (side == "west") { offset = ( csize, 0) } offset = (csize, 0)
}
let pos1 = (rel: offset, to: pos) let pos1 = (rel: offset, to: pos)
@@ -72,8 +81,7 @@
// Horizontal labels: anchor = opposite of the side (e.g. east port → west-anchored text, outside). // Horizontal labels: anchor = opposite of the side (e.g. east port → west-anchored text, outside).
// Vertical labels: same opposite side but rotated 90°, keeping text readable. // Vertical labels: same opposite side but rotated 90°, keeping text readable.
let name-anchor = if outside-name { let name-anchor = if outside-name {
if name-rotate { rotate-anchor(opposite-anchor(side)) } if name-rotate { rotate-anchor(opposite-anchor(side)) } else { opposite-anchor(side) }
else { opposite-anchor(side) }
} else { } else {
if name-rotate { rotate-anchor(side) } else { side } if name-rotate { rotate-anchor(side) } else { side }
} }
@@ -91,8 +99,8 @@
label-pos, label-pos,
anchor: name-anchor, anchor: name-anchor,
padding: name-padding, padding: name-padding,
angle: if name-rotate {90deg} else {0deg}, angle: if name-rotate { 90deg } else { 0deg },
name name,
) )
let id = elmt-id + "-port-" + port.at("id") let id = elmt-id + "-port-" + port.at("id")
@@ -129,7 +137,7 @@
anchor-pos, anchor-pos,
radius: .1, radius: .1,
stroke: none, stroke: none,
fill: red fill: red,
) )
} }
} }
@@ -173,8 +181,17 @@
return (placements, (pct: p.pct)) return (placements, (pct: p.pct))
} }
if id in visiting { if id in visiting {
panic("align-with cycle involving port '" + id + "'" + where + " (path: " panic(
+ visiting.join(" -> ") + " -> " + id + ")") "align-with cycle involving port '"
+ id
+ "'"
+ where
+ " (path: "
+ visiting.join(" -> ")
+ " -> "
+ id
+ ")",
)
} }
let raw = p.raw let raw = p.raw
@@ -190,12 +207,26 @@
panic("Port '" + id + "'" + where + ": cannot combine `align-with` with `x`/`y` override") panic("Port '" + id + "'" + where + ": cannot combine `align-with` with `x`/`y` override")
} }
if is-ew and has-x { if is-ew and has-x {
panic("Port '" + id + "'" + where + " on side '" + side panic(
+ "': use `y` for placement override on east/west sides, not `x`") "Port '"
+ id
+ "'"
+ where
+ " on side '"
+ side
+ "': use `y` for placement override on east/west sides, not `x`",
)
} }
if (not is-ew) and has-y { if (not is-ew) and has-y {
panic("Port '" + id + "'" + where + " on side '" + side panic(
+ "': use `x` for placement override on north/south sides, not `y`") "Port '"
+ id
+ "'"
+ where
+ " on side '"
+ side
+ "': use `x` for placement override on north/south sides, not `y`",
)
} }
let result = none let result = none
@@ -224,8 +255,19 @@
let ref-side = placements.at(ref-id).side let ref-side = placements.at(ref-id).side
let ref-ew = ref-side in ("east", "west") let ref-ew = ref-side in ("east", "west")
if is-ew != ref-ew { if is-ew != ref-ew {
panic("Port '" + id + "'" + where + " on '" + side + "' cannot `align-with` port '" panic(
+ ref-id + "' on '" + ref-side + "': incompatible axis") "Port '"
+ id
+ "'"
+ where
+ " on '"
+ side
+ "' cannot `align-with` port '"
+ ref-id
+ "' on '"
+ ref-side
+ "': incompatible axis",
)
} }
let new-visiting = visiting + (id,) let new-visiting = visiting + (id,)
let res = resolve-placement(placements, ref-id, new-visiting, elmt-id: elmt-id) let res = resolve-placement(placements, ref-id, new-visiting, elmt-id: elmt-id)
@@ -249,7 +291,10 @@
#let add-ports( #let add-ports(
elmt-id, elmt-id,
tl, tr, br, bl, tl,
tr,
br,
bl,
ports, ports,
ports-margins, ports-margins,
width: 0, width: 0,
@@ -268,7 +313,7 @@
"north": (tl, tr), "north": (tl, tr),
"east": (tr, br), "east": (tr, br),
"south": (bl, br), "south": (bl, br),
"west": (tl, bl) "west": (tl, bl),
) )
if type(ports) != dictionary { if type(ports) != dictionary {
@@ -301,9 +346,19 @@
for (i, port) in side-ports.enumerate() { for (i, port) in side-ports.enumerate() {
let pid = port.at("id") let pid = port.at("id")
if pid in placements { if pid in placements {
panic("Duplicate port id '" + pid + "' in element '" + elmt-id panic(
+ "' on side '" + side + "' (existing side: '" "Duplicate port id '"
+ placements.at(pid).side + "', new port: " + repr(port) + ")") + pid
+ "' in element '"
+ elmt-id
+ "' on side '"
+ side
+ "' (existing side: '"
+ placements.at(pid).side
+ "', new port: "
+ repr(port)
+ ")",
)
} }
let pct-auto = (i + 1) * space let pct-auto = (i + 1) * space
placements.insert(pid, ( placements.insert(pid, (
+30 -25
View File
@@ -36,7 +36,7 @@
green: rgb(127, 200, 172), green: rgb(127, 200, 172),
pink: rgb(236, 127, 178), pink: rgb(236, 127, 178),
purple: rgb(189, 151, 255), purple: rgb(189, 151, 255),
blue: rgb(127, 203, 235) blue: rgb(127, 203, 235),
) )
/// Pads a string on the left with 0s to the given length /// Pads a string on the left with 0s to the given length
@@ -67,7 +67,7 @@
north-west: "south-east", north-west: "south-east",
north-east: "south-west", north-east: "south-west",
south-east: "north-west", south-east: "north-west",
south-west: "north-east" south-west: "north-east",
).at(anchor) ).at(anchor)
} }
@@ -86,7 +86,7 @@
north-west: "north-east", north-west: "north-east",
north-east: "south-east", north-east: "south-east",
south-east: "south-west", south-east: "south-west",
south-west: "north-west" south-west: "north-west",
).at(anchor) ).at(anchor)
} }
@@ -97,24 +97,19 @@
/// height ±0.5 units. For `kind: "in"` the tip points into the block; for /// height ±0.5 units. For `kind: "in"` the tip points into the block; for
/// `kind: "out"` it points away. /// `kind: "out"` it points away.
/// ///
/// The returned dictionary also carries the four world-cardinal midpoints
/// of the pentagon's bounding box (`east`, `west`, `north`, `south`) — each
/// lies on the pentagon boundary and is used as an alternate wire-attach
/// anchor.
///
/// - side (str): One of `"north"`, `"east"`, `"south"`, `"west"` /// - side (str): One of `"north"`, `"east"`, `"south"`, `"west"`
/// - kind (str): `"in"` or `"out"` /// - kind (str): `"in"` or `"out"`
/// - u (length,number): The pentagon unit size /// - u (length,number): The pentagon unit size
/// -> dictionary: `(vertices: (..5 pairs..), outer: (dx, dy), inner: (dx, dy), east: (dx, dy), west: (dx, dy), north: (dx, dy), south: (dx, dy))` /// -> dictionary: `(vertices: (..5 pairs..), outer: (dx, dy), inner: (dx, dy))`
#let port-shape-vertices(side, kind, u) = { #let port-shape-vertices(side, kind, u) = {
// Base case: west port, input (tip east / into block). // Base case: west port, input (tip east / into block).
// Vertices as (dx, dy) offsets relative to the boundary anchor. // Vertices as (dx, dy) offsets relative to the boundary anchor.
let base-vertices = ( let base-vertices = (
(-u, -u/2), (-u, -u / 2),
(-u, +u/2), (-u, +u / 2),
(u/2, +u/2), (u / 2, +u / 2),
(u, 0), // tip east (inner side) (u, 0), // tip east (inner side)
(u/2, -u/2), (u / 2, -u / 2),
) )
// For output, mirror horizontally so the tip points west (outside). // For output, mirror horizontally so the tip points west (outside).
if kind == "out" { if kind == "out" {
@@ -131,11 +126,11 @@
// north: (x, y) → (y, -x) // north: (x, y) → (y, -x)
// east: (x, y) → (-x, -y) // east: (x, y) → (-x, -y)
// south: (x, y) → (-y, x) // south: (x, y) → (-y, x)
let rot = if side == "west" { p => p } let rot = if side == "west" { p => p } else if side == "north" {
else if side == "north" { p => (p.at(1), -p.at(0)) } p => (p.at(1), -p.at(0))
else if side == "east" { p => (-p.at(0), -p.at(1)) } } else if side == "east" { p => (-p.at(0), -p.at(1)) } else if side == "south" {
else if side == "south" { p => (-p.at(1), p.at(0)) } p => (-p.at(1), p.at(0))
else { panic("Unknown side: " + side) } } else { panic("Unknown side: " + side) }
// World-cardinal midpoints of the pentagon's bounding box. These are // World-cardinal midpoints of the pentagon's bounding box. These are
// named in *world* coordinates (not rotated with the shape), so `.east` // named in *world* coordinates (not rotated with the shape), so `.east`
@@ -153,11 +148,11 @@
// The pentagon bounding box extents depend only on the side's orientation // The pentagon bounding box extents depend only on the side's orientation
// (horizontal for west/east, vertical for north/south), not on `kind`. // (horizontal for west/east, vertical for north/south), not on `kind`.
let (hx, hy) = if side == "west" or side == "east" { let (hx, hy) = if side == "west" or side == "east" {
(u, u/2) (u, u / 2)
} else { } else {
(u/2, u) (u / 2, u)
} }
let world-east = ( hx, 0) let world-east = (hx, 0)
let world-west = (-hx, 0) let world-west = (-hx, 0)
let world-north = (0, hy) let world-north = (0, hy)
let world-south = (0, -hy) let world-south = (0, -hy)
@@ -174,8 +169,15 @@
} }
#let valid-anchors = ( #let valid-anchors = (
"center", "north", "east", "west", "south", "center",
"north-east", "north-west", "south-east", "south-west" "north",
"east",
"west",
"south",
"north-east",
"north-west",
"south-east",
"south-west",
) )
/// Normalizes a coordinate so that 2D points (x, y) are padded /// Normalizes a coordinate so that 2D points (x, y) are padded
@@ -196,7 +198,8 @@
// Function-based coordinate: (fn, point) // Function-based coordinate: (fn, point)
if type(a) == function { if type(a) == function {
let orig = a let orig = a
return (v => { return (
v => {
let result = orig(v) let result = orig(v)
if type(result) == array and result.len() == 2 { if type(result) == array and result.len() == 2 {
let (rx, ry) = result let (rx, ry) = result
@@ -205,7 +208,9 @@
} }
} }
return result return result
}, b) },
b,
)
} }
} }
} }