Compare commits

..
11 Commits
Author SHA1 Message Date
David Stadelmann fd761487ff feat: Add parameter shell to block (alternative to group)
Defining a block and placing elements into that block has the benefit
that you can use the ports to align the elements within the block to the
ports of that outer ("shell") block
2026-09-14 11:45:55 +02:00
David Stadelmann b8a8d38ed2 feat: Allow port names to be placed outside of a block 2026-09-14 11:42:37 +02:00
David Stadelmann 6070ce7fe3 feat: Add wire.breakout element for bus fan-out diagrams
Introduces wire.breakout: a 45° diagonal line of configurable length
(default 1em) that starts at a given (x, y) and exposes a port anchor
<id>-port-<id> at its end point so wires can attach to it as if it were
a block port. A synthesized <id>.center anchor is placed opposite the
configured port-side so wire.wire with style "u-shape" can auto-detect
the port's cardinal side.
- x/y accept number, (rel:, to:) and (from:) forms; (from:) sets the
  start point to the referenced anchor's coordinate.
- direction: one of north-east/north-west/south-east/south-west.
- Optional label positioned via name-pos (NE/NW/SE/SW); when name-pos is
  opposite to direction, the label falls back to the start point so it
  never covers the diagonal line. Label padding matches wire.wire (3pt).
Also adds three examples (breakout, breakout-label, breakout-wire) in
doc/examples.typ referenced from the docstring so tidy auto-renders
them in the manual, and a gallery entry (gallery/test_breakout.typ)
demonstrating orthogonal bus fan-out with breakouts in all four
directions.
2026-08-26 09:45:24 +02:00
David Stadelmann a51e8bb93b feature: Add port shape 2026-08-17 15:00:02 +02:00
David Stadelmann 563bb7b1d2 feat: Add port-name-padding parameter
Replaces the hard-coded 2pt padding used when placing port name labels.
Configurable per element via port-name-padding on elmt (forwarded by
block and group), defaulting to 2pt to preserve existing layout.
2026-08-17 14:13:06 +02:00
David Stadelmann 0d1478c765 feat: Add ports on group elements
Groups now accept ports/ports-margins like blocks. Port name labels
default to outside the group border (outside-port-names: true) so they
don't overlap child elements; set to false for block-style inside
labels.
2026-08-17 14:12:50 +02:00
David Stadelmann 002db0e6f5 feat: Add u-shape wire-style 2026-08-17 11:59:21 +02:00
David Stadelmann 0fb4929aca feat: Add relative y coordinates 2026-08-17 11:17:05 +02:00
David Stadelmann 23213fabca chore: update minimum compiler version to 0.15.0 2026-07-28 14:48:32 +02:00
David Stadelmann 320318a25b deps: update tidy to 0.4.3 2026-07-28 14:45:49 +02:00
David Stadelmann 9bcd91131c deps: update CeTZ 0.3.4 to 0.5.2 with coordinate fixes 2026-07-28 14:43:25 +02:00
32 changed files with 1229 additions and 51 deletions

No files matched your search

+231 -1
View File
@@ -56,7 +56,7 @@ for i in range(3) {
```)
#let wires = example(```
for i in range(4) {
for i in range(5) {
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.content((i * 3 + 1, -1), raw(wire.wire-styles.at(i)))
@@ -68,6 +68,8 @@ wire.wire("w3", ("p4", "p5"), style: "dodge",
wire.wire("w4", ("p6","p7"), style: "guided",
guided-center:(20%, 40%), guided-margins: (90%,87%),
guided-sides: ("north","south"))
wire.wire("w5", ("p8", "p9"), style: "u-shape",
u-side: "east", u-depth: 0.5)
```, vertical: true)
@@ -79,6 +81,81 @@ wire.stub("p", "south", name: "south", length: 15pt)
wire.stub("p", "west", name: "west", length: 3em)
```)
#let breakout = example(```
// 16 breakouts: 4 directions × 4 label positions, laid out
// two per row so the labels have room to breathe.
let dirs = ("north-east", "north-west", "south-east", "south-west")
let poss = ("north-east", "north-west", "south-east", "south-west")
let short = (
"north-east": "NE", "north-west": "NW",
"south-east": "SE", "south-west": "SW",
)
let cols = 2
let dx = 5
let dy = 2
for (i, dir) in dirs.enumerate() {
for (j, pos) in poss.enumerate() {
let n = i * poss.len() + j
let r = calc.floor(n / cols)
let c = calc.rem(n, cols)
draw.circle((c * dx, -r * dy), radius: .05)
wire.breakout("b" + str(n),
x: c * dx, y: -r * dy,
direction: dir,
port-side: if dir.ends-with("east") { "west" } else { "east" },
name: short.at(dir) + "/" + short.at(pos),
name-pos: pos)
}
}
```)
#let breakout-label = example(```
// name-pos matches direction → label sits at the end point.
wire.breakout("b1", x: 0, y: 0, direction: "north-east",
port-side: "east",
name: "at end", name-pos: "north-east")
// name-pos opposite to direction → label would cover the line, so
// it falls back to the start point.
wire.breakout("b2", x: 4, y: 0, direction: "north-east",
port-side: "east",
name: "at start", name-pos: "south-west")
```, vertical: true)
#let breakout-wire = example(```
// Bus fan-out: a horizontal wire from the left block to the lower
// right block carries the full 10-bit bus. A breakout in the middle
// of that wire taps bits [9:5] and routes them to the upper right
// block via an L-shaped zigzag (up, then right).
element.block(x: 0, y: 0, w: 2, h: 2, id: "src",
fill: util.colors.blue,
ports: (east: ((id: "out"),)))
element.block(x: 6, y: 0, w: 2, h: 2, id: "dstL",
fill: util.colors.green,
ports: (west: ((id: "in"),)))
element.block(x: 6, y: 4, w: 2, h: 2, id: "dstU",
fill: util.colors.orange,
ports: (west: ((id: "in"),)))
// Full bus wire, labelled "[9:0]" on the left.
wire.wire("wbus", ("src-port-out", "dstL-port-in"),
name: "[9:0]", name-pos: "start")
// Breakout tapped from the midpoint of the bus.
wire.breakout("tap",
x: (rel: 2, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-east",
port-side: "west",
name: "[9:5]", name-pos: "north-east")
// L-shaped wire from the breakout up and then right to the upper
// block's input.
wire.wire("wtap", ("tap-port-tap", "dstU-port-in"),
style: "zigzag", zigzag-dir: "horizontal",
zigzag-ratio: 100%)
```)
#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")
@@ -186,9 +263,162 @@ wire.wire("w2", ("b2-port-out", "b3-port-in2"),
style: "zigzag")
```)
#let group-ports = example(```
// outside-port-names: true (default) — labels outside the border
element.group(
id: "g1", name: "outside (default)",
stroke: (dash: "dashed"),
fill: util.colors.blue.lighten(60%),
padding: 2em,
outside-port-names: true,
port-name-padding: 4pt,
ports: (
west: ((id: "in", name: "D"),),
east: ((id: "out", name: "Q"),),
),
{ element.block(id: "b1", w: 2, h: 1.5, x: 0, y: 0,
fill: util.colors.green,
ports: (west: ((id: "d", name: "d"),),
east: ((id: "q", name: "q"),))) }
)
wire.wire("wG1in", ("g1-port-in", "b1-port-d"), directed: true)
wire.wire("wG1out", ("b1-port-q", "g1-port-out"), directed: true)
// outside-port-names: false — labels inside the border (block style)
// with extra padding via port-name-padding
element.group(
id: "g2", name: "inside",
stroke: (dash: "dashed"),
fill: util.colors.orange.lighten(60%),
padding: 2em,
outside-port-names: false,
port-name-padding: 0.4em,
ports: (
west: ((id: "in", name: "D"),),
east: ((id: "out", name: "Q"),),
),
{ element.block(id: "b2", w: 2, h: 1.5, x: 6, y: 0,
fill: util.colors.green,
ports: (west: ((id: "d"),), east: ((id: "q"),))) }
)
wire.stub("g2-port-in", "west")
wire.stub("g2-port-out", "east")
```, vertical: true)
#let port-names-outside = example(```
element.block(
x: 0, y: 0, w: 2, h: 2, id: "block",
port-names-outside: true,
port-name-padding: 4pt,
ports: (
north: ((id: "clk", name: "CLK", clock: true),),
west: ((id: "in1", name: "A"),
(id: "in2", name: "B")),
east: ((id: "out", name: "Y"),)
)
)
```)
#let port-shape = example(```
// Block with pentagon-shaped ports.
// - port-shape / port-kind / port-size are element-level defaults.
// - Any port field (shape, kind, size, stroke, fill) overrides per port.
// - kind: "in" → tip points into the block (default)
// kind: "out" → tip points away from the block
// - clock ports with shape: "pentagon" drop their triangle and default
// to a gray fill.
element.block(
id: "b", w: 3, h: 3, x: 0, y: 0,
fill: util.colors.yellow.lighten(60%),
port-shape: "pentagon",
port-size: 1em,
ports: (
// west: default input
west: ((id: "in", name: "D"),),
// east: output — tip points away
east: ((id: "out", name: "Q", kind: "out"),),
// north: clock port — pentagon replaces the triangle, gray fill
north: ((id: "clk", name: "CLK", clock: true),),
// south: styled output with per-port fill and stroke
south: ((id: "rdy", name: "RDY", kind: "out",
fill: util.colors.blue, stroke: red + 1.5pt),),
),
)
// Wires connect at the outer end of each pentagon.
wire.stub("b-port-in", "west")
wire.stub("b-port-out", "east")
wire.stub("b-port-clk", "north")
wire.stub("b-port-rdy", "south")
```)
#let shell = example(```
// A shell block acts as a container: port name labels are drawn
// outside by default, and wires attach at the *inside* tip of
// pentagon-shaped ports so child elements can sit within.
element.block(
id: "sh", w: 5, h: 3, x: 0, y: 0,
shell: true,
port-shape: "pentagon",
fill: util.colors.blue.lighten(80%),
ports: (
west: ((id: "in", name: "IN"),),
east: ((id: "out", name: "OUT", kind: "out"),),
),
{
element.block(id: "inner", w: 2, h: 1.5, x: 1.5, y: 0.75,
fill: util.colors.yellow,
ports: (west: ((id: "a"),), east: ((id: "b"),)))
}
)
wire.wire("w-in", ("sh-port-in", "inner-port-a"))
wire.wire("w-out", ("inner-port-b", "sh-port-out"))
```)
#let intersection = example(```
wire.wire("w1", ((0, 0), (1, 1)), style: "zigzag")
wire.wire("w2", ((0, 0), (1, -.5)),
style: "zigzag", zigzag-ratio: 80%)
wire.intersection("w1.zig")
```)
#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",
fill: util.colors.orange,
ports: (east: ((id: "out"),)))
element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB",
fill: util.colors.orange,
ports: (east: ((id: "out"),)))
wire.wire("uEE", ("eA-port-out", "eB-port-out"),
style: "u-shape", u-depth: 0.5)
// west <-> west (auto-detected)
element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA",
fill: util.colors.yellow,
ports: (west: ((id: "in"),)))
element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB",
fill: util.colors.yellow,
ports: (west: ((id: "in"),)))
wire.wire("uWW", ("wA-port-in", "wB-port-in"),
style: "u-shape", u-depth: 0.5)
// north <-> north (auto-detected)
element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA",
fill: util.colors.green,
ports: (north: ((id: "out"),)))
element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB",
fill: util.colors.green,
ports: (north: ((id: "out"),)))
wire.wire("uNN", ("nA-port-out", "nB-port-out"),
style: "u-shape", u-depth: 0.5)
// south <-> south with explicit u-side override
element.block(x: 16, y: -3, w: 1.5, h: 1.5, id: "sA",
fill: util.colors.pink,
ports: (south: ((id: "out"),)))
element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB",
fill: util.colors.pink,
ports: (south: ((id: "out"),)))
wire.wire("uSS", ("sA-port-out", "sB-port-out"),
style: "u-shape", u-side: "south", u-depth: 0.5)
```, vertical: true)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 53 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 53 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 67 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 93 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+117
View File
@@ -0,0 +1,117 @@
// Demonstrates the wire.breakout element used to split off slices of a
// bus that connects blocks. The main bus wires are always laid out
// orthogonally; breakouts diagonally tap intermediate points on those
// wires and route the sub-bus to another block via an L-shaped zigzag.
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// ---- Source block on the left, main sink on the right ----
element.block(x: 0, y: 0, w: 2, h: 2, id: "src", fill: util.colors.blue, ports: (
east: ((id: "out"),),
))
element.block(x: 15, y: 0, w: 2, h: 2, id: "dstMain", fill: util.colors.green, ports: (
west: ((id: "in"),),
))
// Full bus from src to dstMain, labelled [15:0] at the start.
wire.wire("wbus", ("src-port-out", "dstMain-port-in"), name: "[15:0]", name-pos: "start")
// ---- Upper-left sink: taps bits [11:8] via NW breakout ----
element.block(x: 0, y: 4, w: 2, h: 2, id: "dstNW", fill: util.colors.yellow, ports: (
east: ((id: "in"),),
))
wire.breakout(
"tapNE",
x: (rel: 3, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-east",
port-side: "west",
name: "[15:12]",
name-pos: "north-east",
)
// L-shape: up first, then left into dstNW.
wire.wire(
"wNW",
("tapNE-port-tapNE", "dstNW-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Upper-right sink: taps bits [15:12] via NE breakout ----
element.block(x: 15, y: 4, w: 2, h: 2, id: "dstNE", fill: util.colors.orange, ports: (
west: ((id: "in"),),
))
wire.breakout(
"tapNW",
x: (rel: 7, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-west",
port-side: "east",
name: "[11:8]",
name-pos: "north-east",
)
// L-shape: up first, then right into dstNE.
wire.wire(
"wNE",
("tapNW-port-tapNW", "dstNE-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Lower-left sink: taps bits [3:0] via SW breakout ----
element.block(x: 0, y: -4, w: 2, h: 2, id: "dstSW", fill: util.colors.purple, ports: (
east: ((id: "in"),),
))
wire.breakout(
"tapSE",
x: (rel: 5, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "south-east",
port-side: "west",
name: "[7:4]",
name-pos: "south-east",
)
// L-shape: down first, then left into dstSW.
wire.wire(
"wSW",
("tapSE-port-tapSE", "dstSW-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Lower-right sink: taps bits [7:4] via SE breakout ----
element.block(x: 15, y: -4, w: 2, h: 2, id: "dstSE", fill: util.colors.pink, ports: (
west: ((id: "in"),),
))
wire.breakout(
"tapSW",
x: (rel: 11, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "south-west",
port-side: "east",
name: "[3:0]",
name-pos: "south-west",
)
// L-shape: down first, then right into dstSE.
wire.wire(
"wSE",
("tapSW-port-tapSW", "dstSE-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+86
View File
@@ -0,0 +1,86 @@
// Demonstrates ports on a group element.
// outside-port-names: true (default) — labels outside the group border
// outside-port-names: false — labels inside (block-style)
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// --- Group with port labels INSIDE (outside-port-names: false) ---
element.group(
id: "cpu",
name: "CPU (labels outside)",
name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.yellow.lighten(60%),
padding: 1em,
port-name-padding: .5em,
outside-port-names: true,
ports: (
west: (
(id: "in1", name: "D1"),
(id: "in2", name: "D2"),
),
east: ((id: "out", name: "Q"),),
south: (
(id: "rst", name: "RST", vertical: true),
(id: "done", name: "DONE", vertical: true),
),
),
{
element.block(
id: "reg2",
w: 2.5,
h: 3,
x: 0,
y: 0,
fill: util.colors.green,
ports: (
west: ((id: "d1"), (id: "d2")),
east: ((id: "q"),),
north: ((id: "clk", clock: true),),
),
)
},
)
// --- Group with port labels INSIDE (outside-port-names: false) ---
element.group(
id: "cpu2",
name: "CPU (labels inside)",
name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.yellow.lighten(60%),
padding: 4em,
outside-port-names: false,
port-name-padding: 0.5em,
ports: (
west: (
(id: "in1", name: "D1"),
(id: "in2", name: "D2"),
),
east: ((id: "out", name: "Q"),),
south: (
(id: "rst", name: "RST", vertical: true),
(id: "done", name: "DONE", vertical: true),
),
),
{
element.block(
id: "reg2",
w: 2.5,
h: 3,
x: 10,
y: 0,
fill: util.colors.green,
ports: (
west: ((id: "d1"), (id: "d2")),
east: ((id: "q"),),
north: ((id: "clk", clock: true),),
),
)
},
)
wire.stub("cpu2-port-in1", "west", name: "A")
wire.stub("cpu2-port-in2", "west", name: "B")
wire.stub("cpu2-port-out", "east", name: "Q")
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+117
View File
@@ -0,0 +1,117 @@
// Demonstrates pentagon-shaped ports.
//
// - Element-level defaults: `port-shape`, `port-kind`, `port-size`.
// - Per-port overrides: `shape`, `kind`, `size`, `stroke`, `fill`.
// - Wire endpoints connect at the outer end of each pentagon.
// - Clock ports with pentagon shape drop the clock triangle and default
// to a gray fill.
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// -----------------------------------------------------------------
// 1) All four sides × input/output
// -----------------------------------------------------------------
element.block(
id: "grid", w: 4, h: 4, x: 0, y: 0,
name: "sides × in/out", name-anchor: "center",
fill: util.colors.yellow.lighten(60%),
port-shape: "pentagon",
ports: (
west: (
(id: "wi", name: "W-in"),
(id: "wo", name: "W-out", kind: "out"),
),
east: (
(id: "ei", name: "E-in"),
(id: "eo", name: "E-out", kind: "out"),
),
north: (
(id: "ni", name: "N-in", vertical: true),
(id: "no", name: "N-out", kind: "out", vertical: true),
),
south: (
(id: "si", name: "S-in", vertical: true),
(id: "so", name: "S-out", kind: "out", vertical: true),
),
),
)
// -----------------------------------------------------------------
// 2) Per-port styling: fill and stroke overrides + size override
// -----------------------------------------------------------------
element.block(
id: "styled", w: 3, h: 3, x: 10, y: 0,
name: "per-port styling", name-anchor: "center",
fill: util.colors.green.lighten(40%),
port-shape: "pentagon",
ports: (
west: ((id: "a", name: "A", fill: util.colors.blue),),
east: ((id: "b", name: "B", kind: "out",
stroke: red + 1.5pt, fill: util.colors.pink),),
north: ((id: "c", name: "C", size: 1.5em),),
south: ((id: "d", name: "D", kind: "out",
fill: util.colors.orange),),
),
)
// -----------------------------------------------------------------
// 3) Clock port with pentagon → gray fill, no triangle
// -----------------------------------------------------------------
element.block(
id: "clk", w: 3, h: 2.5, x: 16, y: 0,
name: "clock + pentagon", name-anchor: "center",
fill: util.colors.orange.lighten(60%),
port-shape: "pentagon",
ports: (
west: ((id: "d", name: "D"),),
east: ((id: "q", name: "Q", kind: "out"),),
north: ((id: "clk", name: "CLK", clock: true),),
),
)
// -----------------------------------------------------------------
// 4) Two pentagon-ported blocks connected by a wire
// (wire terminates at outer edge of each pentagon)
// -----------------------------------------------------------------
element.block(
id: "src", w: 2, h: 1.5, x: 0, y: -5,
name: "src", name-anchor: "center",
fill: util.colors.pink,
port-shape: "pentagon",
ports: (east: ((id: "q", name: "Q", kind: "out"),)),
)
element.block(
id: "dst", w: 2, h: 1.5, x: 6, y: -5,
name: "dst", name-anchor: "center",
fill: util.colors.blue,
port-shape: "pentagon",
ports: (west: ((id: "d", name: "D"),)),
)
wire.wire("w-sd", ("src-port-q", "dst-port-d"))
// -----------------------------------------------------------------
// 5) Group with pentagon ports enclosing a child block
// -----------------------------------------------------------------
element.group(
id: "g", name: "group w/ pentagon ports", name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.purple.lighten(70%),
padding: 2em,
port-shape: "pentagon",
ports: (
west: ((id: "in", name: "IN"),),
east: ((id: "out", name: "OUT", kind: "out"),),
),
{
element.block(
id: "inner", w: 2, h: 1.5, x: 12, y: -5,
fill: util.colors.green,
ports: (west: ((id: "d"),), east: ((id: "q"),)),
)
},
)
wire.wire("wgi", ("g-port-in", "inner-port-d"))
wire.wire("wgo", ("inner-port-q", "g-port-out"))
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

+53
View File
@@ -0,0 +1,53 @@
// Demonstrates the u-shape wire style.
// The u-side is auto-detected from the port anchors when both endpoints
// belong to elements (deduced from the port position vs the element's
// center anchor). It can also be set explicitly with u-side.
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// ---- east <-> east (auto-detected) ----
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA",
fill: util.colors.orange,
ports: (east: ((id: "out"),)))
element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB",
fill: util.colors.orange,
ports: (east: ((id: "out"),)))
wire.wire("uEE", ("eA-port-out", "eB-port-out"),
style: "u-shape", u-depth: 0.6,
name: "E-E")
// ---- west <-> west (auto-detected) ----
element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA",
fill: util.colors.yellow,
ports: (west: ((id: "in"),)))
element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB",
fill: util.colors.yellow,
ports: (west: ((id: "in"),)))
wire.wire("uWW", ("wA-port-in", "wB-port-in"),
style: "u-shape", u-depth: 0.6,
name: "W-W")
// ---- north <-> north (auto-detected) ----
element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA",
fill: util.colors.green,
ports: (north: ((id: "out"),)))
element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB",
fill: util.colors.green,
ports: (north: ((id: "out"),)))
wire.wire("uNN", ("nA-port-out", "nB-port-out"),
style: "u-shape", u-depth: 0.6,
name: "N-N")
// ---- south <-> south (explicit u-side override) ----
element.block(x: 16, y: 0, w: 1.5, h: 1.5, id: "sA",
fill: util.colors.pink,
ports: (south: ((id: "out"),)))
element.block(x: 18.5, y: -0.5, w: 1.5, h: 1.5, id: "sB",
fill: util.colors.pink,
ports: (south: ((id: "out"),)))
wire.wire("uSS", ("sA-port-out", "sB-port-out"),
style: "u-shape", u-side: "south", u-depth: 0.6,
name: "S-S")
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+46
View File
@@ -0,0 +1,46 @@
// Demonstrates the `(rel: <number>, to: <anchor>)` shorthand for the `y`
// parameter (analogous to the existing shorthand for `x`).
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// Anchor block: absolute placement.
element.block(
x: 0, y: 0, w: 2, h: 1.5,
id: "A",
fill: util.colors.orange,
ports: (east: ((id: "out"),))
)
// X relative, Y absolute (existing feature).
element.block(
x: (rel: 1, to: "A.east"), y: 0, w: 2, h: 1.5,
id: "B",
fill: util.colors.yellow,
ports: (west: ((id: "in"),), east: ((id: "out"),))
)
// Both X and Y relative to another element's anchor (new feature).
element.block(
x: (rel: 1, to: "B.east"),
y: (rel: 2, to: "B.south-west"),
w: 2, h: 1.5,
id: "C",
fill: util.colors.green,
ports: (west: ((id: "in"),))
)
// Y relative only, X absolute.
element.block(
x: 0,
y: (rel: -2.5, to: "A.south-west"),
w: 2, h: 1.5,
id: "D",
fill: util.colors.blue,
ports: (east: ((id: "out"),))
)
wire.wire("wAB", ("A-port-out", "B-port-in"))
wire.wire("wBC", ("B-port-out", "C-port-in"), style: "zigzag")
})
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
#import "@preview/tidy:0.4.1"
#import "@preview/tidy:0.4.3"
#import "/src/cetz.typ": draw, canvas
#import "src/lib.typ"
#import "doc/examples.typ"
+1 -1
View File
@@ -1 +1 @@
#import "@preview/cetz:0.3.4": *
#import "@preview/cetz:0.5.2": *
+1 -1
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": canvas
#import "@preview/tidy:0.3.0"
#import "@preview/tidy:0.4.3"
/// Draws a block circuit diagram
///
+26
View File
@@ -16,9 +16,22 @@
/// Draws a block element
///
/// #examples.block
///
/// Ports can be given a pentagon shape (with configurable direction, size,
/// stroke and fill) via `port-shape` and per-port overrides:
/// #examples.port-shape
///
/// Port name labels can be placed outside the block via `port-names-outside`:
/// #examples.port-names-outside
/// For other parameters description, see #doc-ref("element.elmt")
/// - radius (number, length, ratio, dictionary): The corner radius of the block. See CeTZ documentation for more information
/// - shell (bool): If true, the block behaves like a shell/container (analogous to a group): port name labels default to being drawn outside the block, and wires attach to the inside tip of pentagon-shaped ports. See #doc-ref("element.elmt")
///
/// Use `shell: true` to wrap child elements inside a bordered container without
/// having ports overlap the interior:
/// #examples.shell
#let block(
..args,
x: none,
y: none,
w: none,
@@ -27,15 +40,22 @@
name-anchor: "center",
ports: (),
ports-margins: (),
port-name-padding: 2pt,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
fill: none,
radius: 0.5em,
stroke: black + 1pt,
id: "",
shell: false,
debug: (
ports: false
)
) = element.elmt(
draw-shape: draw-shape.with(radius: radius),
body: if args.pos().len() > 0 { args.pos().first() } else { none },
x: x,
y: y,
w: w,
@@ -44,8 +64,14 @@
name-anchor: name-anchor,
ports: ports,
ports-margins: ports-margins,
port-name-padding: port-name-padding,
port-names-outside: port-names-outside,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
fill: fill,
stroke: stroke,
id: id,
shell: shell,
debug: debug
)
+34 -7
View File
@@ -24,7 +24,9 @@
/// If it is a dictionary, it should be in the format `(rel: number, to: str)`, where `rel` is the offset and `to` the base anchor
/// - y (number, dictionary): The y position (bottom-left corner).
///
/// If it is a dictionary, it should be in the format `(from: str, to: str)`, where `from` is the base anchor and `to` is the id of the port to align with the anchor
/// If it is a dictionary, it can take one of two forms:
/// - `(rel: number, to: str)`: `rel` is the y offset and `to` the base anchor (analogous to `x`)
/// - `(from: str, to: str)`: `from` is the base anchor and `to` is the id of the port to align with the anchor
/// - w (number): Width of the element
/// - h (number): Height of the element
/// - name (none, str): Optional name of the block
@@ -35,11 +37,17 @@
/// - `clock` (`bool`): Whether it is a clock port (triangle symbol)
/// - `vertical` (`bool`): Whether the name should be drawn vertically
/// - 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-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. See #examples.port-shape
/// - 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
/// - fill (none, color): Fill color
/// - stroke (stroke): Border stroke
/// - id (str): The block id (for future reference)
/// - auto-ports (bool): Whether to use auto port placements or not. If false, `draw-shape` is responsible for adding the appropiate ports
/// - ports-y (dictionary): Dictionary of the ports y offsets (used with `auto-ports: false`)
/// - shell (bool): If true, the block behaves like a shell/container (analogous to a group): port name labels default to being drawn *outside* the block border, and wires attach to the *inside* tip of pentagon-shaped ports instead of the outside base. Useful for wrapping child elements without having ports overlap them (default `false`)
/// - debug (dictionary): Dictionary of debug options.
///
/// Supported fields include:
@@ -54,11 +62,18 @@
name-anchor: "center",
ports: (:),
ports-margins: (:),
port-name-padding: 2pt,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
fill: none,
stroke: black + 1pt,
id: "",
auto-ports: true,
ports-y: (:),
shell: false,
body: none,
debug: (
ports: false
)
@@ -74,13 +89,12 @@
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
x = to-pos.at(0)
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary) {
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
let (to-side, i) = find-port(ports, to)
@@ -134,12 +148,25 @@
}
if auto-ports {
let outside-name = if port-names-outside == auto { shell } else { port-names-outside }
add-ports(
id,
tl, tr, br, bl,
ports,
ports-margins,
debug: debug.ports
debug: debug.ports,
name-padding: port-name-padding,
outside-name: outside-name,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: stroke,
elmt-fill: fill,
wire-inside: shell,
)
}
if body != none {
body
}
})
+44 -1
View File
@@ -1,9 +1,15 @@
#import "/src/cetz.typ": draw, coordinate
#import "../util.typ"
#import "ports.typ": add-ports
/// Draws a group of elements
///
/// #examples.group
///
/// Ports can also be defined on a group, just like on a block. By default
/// port labels are drawn *outside* the group border so they do not overlap
/// child elements. Set `outside-port-names: false` to draw them inside.
/// #examples.group-ports
/// - body (elements, function): Elements to group
/// - id (str): see #doc-ref("element.elmt")
/// - name (str): The group's name
@@ -16,6 +22,14 @@
/// - array: either (`<all>`,), (`<vertical>`, `<horizontal>`) or (`<top>`, `<right>`, `<bottom>`, `<left>`)
/// - dictionary: valid keys are "top", "right", "bottom" and "left"
/// - radius (number): The corner radius
/// - ports (dictionary): see #doc-ref("element.elmt")
/// - ports-margins (dictionary): see #doc-ref("element.elmt")
/// - outside-port-names (bool): If `true` (default), port name labels are drawn outside the group border so they do not overlap child elements. Set to `false` to draw labels inside, matching the block behaviour
/// - port-name-padding (number, length): Padding between the port name label and the group border (default `2pt`). Accepts anything CeTZ accepts as `padding` (number or length)
/// - port-shape (none, str): see #doc-ref("element.elmt")
/// - port-kind (str): see #doc-ref("element.elmt")
/// - port-size (length): see #doc-ref("element.elmt")
/// - debug (dictionary): see #doc-ref("element.elmt")
#let group(
body,
id: "",
@@ -24,7 +38,17 @@
fill: none,
stroke: black + 1pt,
padding: 0.5em,
radius: 0.5em
radius: 0.5em,
ports: (:),
ports-margins: (:),
outside-port-names: true,
port-name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
debug: (
ports: false
)
) = {
let min-x = none
let max-x = none
@@ -68,6 +92,25 @@
stroke: stroke,
fill: fill
)
add-ports(
id,
id + ".north-west",
id + ".north-east",
id + ".south-east",
id + ".south-west",
ports,
ports-margins,
debug: debug.ports,
outside-name: outside-port-names,
name-padding: port-name-padding,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: stroke,
elmt-fill: fill,
)
if name != none {
draw.content(
id + "." + name-anchor,
+5 -5
View File
@@ -1,4 +1,5 @@
#import "/src/cetz.typ": draw, coordinate
#import "/src/util.typ"
#import "../ports.typ": add-ports, add-port
#import "../element.typ"
@@ -49,13 +50,12 @@
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
x = to-pos.at(0)
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary) {
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
+5 -5
View File
@@ -1,4 +1,5 @@
#import "/src/cetz.typ": draw, coordinate
#import "/src/util.typ"
#import "../ports.typ": add-ports, add-port
#import "../element.typ"
@@ -60,13 +61,12 @@
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
x = to-pos.at(0)
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary) {
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
+89 -15
View File
@@ -1,36 +1,94 @@
#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor
#import "../util.typ": rotate-anchor, opposite-anchor, port-shape-vertices
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
let is-clock = port.at("clock", default: false)
if (port.at("clock", default: false)) {
// Per-port overrides fall back to element-level defaults.
let shape = port.at("shape", default: port-shape)
let kind = port.at("kind", default: port-kind)
let size = port.at("size", default: port-size)
let shape-stroke = port.at("stroke", default: elmt-stroke)
// 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
// `fill` overrides both.
let shape-fill = port.at("fill",
default: if is-clock and shape == "pentagon" { gray } else { elmt-fill })
// When shape is enabled, wire-connection anchor moves outside the block, and
// inside-name labels reference the inner tip/base; otherwise both collapse to
// the boundary point (unchanged behaviour).
let outer-pos = pos
let inner-pos = pos
if shape == "pentagon" {
let sh = port-shape-vertices(side, kind, size)
let v = sh.vertices.map(off => (rel: off, to: pos))
draw.line(
v.at(0), v.at(1), v.at(2), v.at(3), v.at(4),
close: true,
stroke: shape-stroke,
fill: shape-fill,
)
outer-pos = (rel: sh.outer, to: pos)
inner-pos = (rel: sh.inner, to: pos)
} else if is-clock {
if prev == none or next == none {
panic("Clock port must have previous and next positions")
}
let size = if port.at("small", default: false) {8pt} else {1em}
let csize = if port.at("small", default: false) {8pt} else {1em}
let offset
if (side == "north") { offset = ( 0, -size) }
else if (side == "east") { offset = (-size, 0) }
else if (side == "south") { offset = ( 0, size) }
else if (side == "west") { offset = ( size, 0) }
if (side == "north") { offset = ( 0, -csize) }
else if (side == "east") { offset = (-csize, 0) }
else if (side == "south") { offset = ( 0, csize) }
else if (side == "west") { offset = ( csize, 0) }
let pos1 = (rel: offset, to: pos)
// TODO: use context or vectors to have the height relative to the width
draw.line(prev, pos1, next)
}
// When outside-name is true (e.g. for groups) the label is placed on the
// exterior of the element border so it does not overlap child elements.
// 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.
let name-anchor = if outside-name {
if name-rotate { rotate-anchor(opposite-anchor(side)) }
else { opposite-anchor(side) }
} else {
if name-rotate { rotate-anchor(side) } else { side }
}
// Label sits at the outer edge of the shape for outside-name, inner edge for
// inside-name. `name-padding` is thus naturally measured from the shape edge
// (or from the boundary when shape is disabled).
let label-pos = if outside-name { outer-pos } else { inner-pos }
// For "shell" containers, wires must attach at the inside of the pentagon
// (the tip that points inward), not at the exterior base.
let anchor-pos = if wire-inside and shape == "pentagon" { inner-pos } else { outer-pos }
draw.content(
pos,
anchor: if name-rotate {rotate-anchor(side)} else {side},
padding: 2pt,
label-pos,
anchor: name-anchor,
padding: name-padding,
angle: if name-rotate {90deg} else {0deg},
name
)
@@ -38,7 +96,7 @@
if debug {
draw.circle(
pos,
anchor-pos,
name: id,
radius: .1,
stroke: none,
@@ -47,7 +105,7 @@
} else {
draw.hide(draw.circle(
pos,
anchor-pos,
radius: 0,
stroke: none,
name: id
@@ -60,7 +118,15 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: false
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let sides = (
"north": (tl, tr),
@@ -109,7 +175,15 @@
pos,
prev: pos-prev,
next: pos-next,
debug: debug
debug: debug,
outside-name: outside-name,
name-padding: name-padding,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: elmt-stroke,
elmt-fill: elmt-fill,
wire-inside: wire-inside,
)
}
}
+110
View File
@@ -1,3 +1,23 @@
#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)[
@@ -70,7 +90,97 @@
).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) }
return (
vertices: base-vertices.map(rot),
outer: rot(base-outer),
inner: rot(base-inner),
)
}
#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
}
+262 -13
View File
@@ -1,9 +1,12 @@
#import "/src/cetz.typ": draw, coordinate
#import "util.typ": opposite-anchor
#import "util.typ": opposite-anchor, normalize-coord, resolve-rel-coord
/// List of valid wire styles
/// #examples.wires
#let wire-styles = ("direct", "zigzag", "dodge", "guided")
///
/// The `"u-shape"` style connects two ports that face the same cardinal side.
/// #examples.u-shape-wire
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape")
#let signal-width = 1pt
#let bus-width = 1.5pt
@@ -68,8 +71,8 @@
let (ctx, p0) = coordinate.resolve(ctx, start)
let (ctx, p3) = coordinate.resolve(ctx, end)
p0 = (x: p0.first(), y: p0.last())
p3 = (x: p3.first(), y: p3.last())
p0 = (x: p0.first(), y: p0.at(1))
p3 = (x: p3.first(), y: p3.at(1))
let dx1 = margin-start
let dx2 = margin-end
@@ -86,13 +89,13 @@
if side-end == "east" {
dx2 *= -1
}
p1 = (p0.x + dx1, p0.y)
p2 = (p3.x - dx2, p0.y)
p1 = (p0.x + dx1, p0.y, 0)
p2 = (p3.x - dx2, p0.y, 0)
let points = (
start,
(horizontal: p1, vertical: ()),
(horizontal: (), vertical: (0, dodge-y)),
(horizontal: (), vertical: (0, dodge-y, 0)),
(horizontal: p2, vertical: ()),
(horizontal: (), vertical: end),
end
@@ -109,6 +112,100 @@
return (points, anchors)
}
#let _u-valid-sides = ("east", "west", "north", "south")
// Try to deduce the cardinal side a port anchor faces by comparing its
// position to the center of the element it belongs to. Returns none if the
// anchor is not a port-style anchor (i.e. does not contain "-port-") or if
// resolution fails.
#let _u-detect-side(anchor, ctx) = {
if type(anchor) != str { return none }
let sep = "-port-"
if not anchor.contains(sep) { return none }
let elmt-id = anchor.split(sep).first()
let port-pos = none
let center-pos = none
// coordinate.resolve panics on unknown anchors; there is no try/catch in
// Typst, so we assume port anchors created by add-port always come with a
// matching group of the same name. If not, the user must set u-side
// explicitly.
let (_, p1) = coordinate.resolve(ctx, anchor)
let (_, p2) = coordinate.resolve(ctx, elmt-id + ".center")
port-pos = p1
center-pos = p2
let dx = port-pos.at(0) - center-pos.at(0)
let dy = port-pos.at(1) - center-pos.at(1)
if calc.abs(dx) >= calc.abs(dy) {
return if dx >= 0 { "east" } else { "west" }
} else {
return if dy >= 0 { "north" } else { "south" }
}
}
#let get-u-shape-wire(pts, u-depth, u-side, ctx) = {
let start = pts.first()
let end = pts.last()
// Resolve the (auto|explicit) side
let side = u-side
if side == auto {
let side-start = _u-detect-side(start, ctx)
let side-end = _u-detect-side(end, ctx)
if side-start != none and side-end != none and side-start != side-end {
panic(
"u-shape: start port faces '" + side-start
+ "' but end port faces '" + side-end
+ "'; both must face the same side, or set u-side explicitly"
)
}
if side-start != none { side = side-start }
else if side-end != none { side = side-end }
else {
panic(
"u-shape: could not auto-detect u-side (endpoints are not port anchors); "
+ "please set u-side explicitly to one of " + repr(_u-valid-sides)
)
}
}
if not side in _u-valid-sides {
panic("u-shape: invalid u-side '" + repr(side) + "'; must be one of " + repr(_u-valid-sides))
}
let (ctx, s) = coordinate.resolve(ctx, start)
let (ctx, e) = coordinate.resolve(ctx, end)
let (sx, sy, ..) = s
let (ex, ey, ..) = e
let p1
let p2
if side == "east" {
let bx = calc.max(sx, ex) + u-depth
p1 = (bx, sy, 0)
p2 = (bx, ey, 0)
} else if side == "west" {
let bx = calc.min(sx, ex) - u-depth
p1 = (bx, sy, 0)
p2 = (bx, ey, 0)
} else if side == "north" {
let by = calc.max(sy, ey) + u-depth
p1 = (sx, by, 0)
p2 = (ex, by, 0)
} else if side == "south" {
let by = calc.min(sy, ey) - u-depth
p1 = (sx, by, 0)
p2 = (ex, by, 0)
}
let points = (start, p1, p2, end)
let anchors = (
"start": start,
"u-start": p1,
"u-end": p2,
"end": end
)
return (points, anchors)
}
#let get-guided-wire(pts, margins, sides, center-guides, ctx) = {
let start = pts.first()
let end = pts.last()
@@ -147,18 +244,18 @@
// points that are closest to the edge points
let p1 = (p0.x + dx1, p0.y + dy1)
let p5 = (p6.x + dx2, p6.y + dy2)
let p1 = (p0.x + dx1, p0.y + dy1, 0)
let p5 = (p6.x + dx2, p6.y + dy2, 0)
// middle point
let center_x = p0.x + box_width * center_horizontal / 100%
let center_y = p0.y + box_height * center_vertical / 100%
let p3 = (center_x, center_y)
let p3 = (center_x, center_y, 0)
// setting up the points for that touch the guides
let p2 = (0,0)
let p4 = (0,0)
let p2 = (0, 0, 0)
let p4 = (0, 0, 0)
if side-start in ("north", "south") {
p2 = (horizontal: p3, vertical: p1)
} else {
@@ -212,6 +309,8 @@
/// - guided-center (array): the horizontal and vertical guides of the center guides (only with style "guided")
/// - guided-margins (array): the start and end of guided margins of the wire (only with style "guided")
/// - guided-sides (array): the side of start and end array (must be either "north", "south", "west", "east") (only work with style "guided")
/// - u-side (auto, str): The cardinal side both endpoints face (one of "east", "west", "north", "south"). If `auto`, it is deduced from the endpoints when they are port anchors of the form `<elmt-id>-port-<port-id>` by comparing each port's position with its element's `.center` anchor. Auto-detection panics if the two endpoints resolve to different sides, or if neither endpoint is a port anchor (only with style "u-shape")
/// - u-depth (number): Distance between the outermost endpoint (in the `u-side` direction) and the U-bend line (only with style "u-shape")
#let wire(
id, pts,
bus: false,
@@ -231,7 +330,9 @@
zigzag-dir: "vertical",
dodge-y: 0,
dodge-sides: ("east", "west"),
dodge-margins: (5%, 5%)
dodge-margins: (5%, 5%),
u-side: auto,
u-depth: 0.5,
) = draw.get-ctx(ctx => {
if not style in wire-styles {
panic("Invalid wire style '" + style + "'")
@@ -241,6 +342,9 @@
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
}
// Normalize user-provided coordinates: (x, y) -> (x, y, 0)
let pts = pts.map(normalize-coord)
let stroke = (
paint: color,
thickness: if bus {bus-width} else {signal-width}
@@ -274,6 +378,13 @@
guided-center,
ctx
)
} else if style == "u-shape" {
(points, anchors) = get-u-shape-wire(
pts,
u-depth,
u-side,
ctx
)
}
let mark = (fill: color)
@@ -400,3 +511,141 @@
)
}
}
/// Draws a wire breakout: a 45° diagonal line starting at (x, y) and
/// ending 1 unit away in one of the four diagonal directions. The end
/// point exposes a port anchor named `<id>-port-<id>` so wires can attach
/// to it as if it were a block port. A synthesized `<id>.center` anchor
/// is placed opposite `port-side` so #doc-ref("wire.wire") with
/// `style: "u-shape"` can auto-detect the port side.
///
/// #examples.breakout
///
/// Label positions are anchored so that the label sits neatly next to the
/// end point of the breakout. When the label direction matches the
/// breakout direction (which would overlap the line) the label is
/// automatically placed at the start point instead so the text does not
/// cover the line.
///
/// #examples.breakout-label
///
/// The port anchor makes it easy to fan out a bus:
///
/// #examples.breakout-wire
///
/// - id (str): The breakout id (also used as the port id)
/// - x (number, dictionary): The starting x position (same forms as `element.block`)
/// - y (number, dictionary): The starting y position (same forms as `element.block`)
/// - direction (str): One of `"north-east"`, `"north-west"`, `"south-east"`, `"south-west"`
/// - length (length, number): The diagonal length (default `1em`, matches port size)
/// - port-side (str): Cardinal side used for wire-attach behavior (`"north"`, `"east"`, `"south"`, `"west"`)
/// - stroke (stroke): Line stroke (paint + thickness)
/// - name (none, str): Optional label displayed near the breakout
/// - name-pos (none, str): Label position, one of `"north-west"`, `"north-east"`, `"south-west"`, `"south-east"`
/// - debug (bool): If true, shows a red dot at the port anchor
#let breakout(
id,
x: none, y: none,
direction: "north-east",
length: 1em,
port-side: "east",
stroke: black + 1pt,
name: none,
name-pos: none,
debug: false,
) = draw.get-ctx(ctx => {
if x == none { panic("breakout: parameter x must be set") }
if y == none { panic("breakout: parameter y must be set") }
let valid-directions = ("north-east", "north-west", "south-east", "south-west")
if not direction in valid-directions {
panic("breakout: invalid direction '" + repr(direction)
+ "'; must be one of " + repr(valid-directions))
}
let valid-sides = ("north", "east", "south", "west")
if not port-side in valid-sides {
panic("breakout: invalid port-side '" + repr(port-side)
+ "'; must be one of " + repr(valid-sides))
}
// Diagonal component: length / sqrt(2). Resolve to a canvas-space
// number so we can do arithmetic with anchor coordinates.
let d-raw = length / calc.sqrt(2)
let (_, d-vec) = coordinate.resolve(ctx, (d-raw, 0, 0))
let d = d-vec.at(0)
// Direction sign vector for (dx, dy).
let sx = if direction.ends-with("east") { 1 } else { -1 }
let sy = if direction.starts-with("north") { 1 } else { -1 }
// Resolve x.
let sx0 = x
if type(x) == dictionary {
if "rel" in x {
sx0 = resolve-rel-coord(ctx, x, 0)
} else if "from" in x {
let (_, from-pos) = coordinate.resolve(ctx, x.from)
sx0 = from-pos.at(0)
} else {
panic("breakout: unsupported x dictionary: " + repr(x))
}
}
// Resolve y.
let sy0 = y
if type(y) == dictionary {
if "rel" in y {
sy0 = resolve-rel-coord(ctx, y, 1)
} else if "from" in y {
let (_, from-pos) = coordinate.resolve(ctx, y.from)
sy0 = from-pos.at(1)
} else {
panic("breakout: unsupported y dictionary: " + repr(y))
}
}
let start = (sx0, sy0)
let end = (rel: (sx * d, sy * d), to: start)
// Offset used for the synthesized center anchor: opposite of port-side.
let center-offset = (
"north": (0, -1em),
"east": (-1em, 0),
"south": (0, 1em),
"west": (1em, 0),
).at(port-side)
let port-anchor-name = id + "-port-" + id
draw.group(name: id, {
draw.line(start, end, stroke: stroke)
draw.anchor("start", start)
draw.anchor("end", end)
draw.anchor("center", (rel: center-offset, to: end))
if name != none and name-pos != none {
let label-anchor = (
"north-west": "south-east",
"north-east": "south-west",
"south-west": "north-east",
"south-east": "north-west",
).at(name-pos)
// Overlap case: label direction is opposite to breakout direction,
// so the label at the end point would extend back over the line.
// Anchor it at the start point instead.
let label-pos = if name-pos == opposite-anchor(direction) { start } else { end }
draw.content(label-pos, anchor: label-anchor, padding: 3pt, name)
}
})
// Port anchor emitted at top level so it can be referenced by its
// bare name (mirrors add-port's approach in ports.typ).
if debug {
draw.circle(end, name: port-anchor-name,
radius: .1, stroke: none, fill: red)
} else {
draw.hide(draw.circle(end, name: port-anchor-name,
radius: 0, stroke: none))
}
})
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "circuiteria"
version = "0.2.1"
compiler = "0.13.1"
compiler = "0.15.0"
repository = "https://git.kb28.ch/HEL/circuiteria"
entrypoint = "src/lib.typ"
authors = [