Compare commits

..
Author SHA1 Message Date
David Stadelmann 7f69c9cf49 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 the side the port faces can be deduced from
the anchor.
- 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-10-22 00:29:44 +02:00
David Stadelmann add0e2929b feat: add relative y coordinates 2026-10-22 00:28:15 +02:00
16 changed files with 7969 additions and 6909 deletions

No files matched your search

+75 -42
View File
@@ -79,6 +79,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,48 +261,6 @@ 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 intersection = example(```
wire.wire("w1", ((0, 0), (1, 1)), style: "zigzag")
wire.wire("w2", ((0, 0), (1, -.5)),
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.

Before

Width:  |  Height:  |  Size: 23 KiB

-86
View File
@@ -1,86 +0,0 @@
// 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: 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")
})
+7547 -6701
View File
File diff suppressed because it is too large. Load diff
-2
View File
@@ -27,7 +27,6 @@
name-anchor: "center",
ports: (),
ports-margins: (),
port-name-padding: 2pt,
fill: none,
radius: 0.5em,
stroke: black + 1pt,
@@ -45,7 +44,6 @@
name-anchor: name-anchor,
ports: ports,
ports-margins: ports-margins,
port-name-padding: port-name-padding,
fill: fill,
stroke: stroke,
id: id,
+8 -10
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,7 +37,6 @@
/// - `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`)
/// - fill (none, color): Fill color
/// - stroke (stroke): Border stroke
/// - id (str): The block id (for future reference)
@@ -55,7 +56,6 @@
name-anchor: "center",
ports: (:),
ports-margins: (:),
port-name-padding: 2pt,
fill: none,
stroke: black + 1pt,
id: "",
@@ -76,13 +76,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)
@@ -141,8 +140,7 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: debug.ports,
name-padding: port-name-padding,
debug: debug.ports
)
}
})
+1 -33
View File
@@ -1,15 +1,9 @@
#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
@@ -22,11 +16,6 @@
/// - 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)
/// - debug (dictionary): see #doc-ref("element.elmt")
#let group(
body,
id: "",
@@ -35,14 +24,7 @@
fill: none,
stroke: black + 1pt,
padding: 0.5em,
radius: 0.5em,
ports: (:),
ports-margins: (:),
outside-port-names: true,
port-name-padding: 2pt,
debug: (
ports: false
)
radius: 0.5em
) = {
let min-x = none
let max-x = none
@@ -86,20 +68,6 @@
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,
)
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
+6 -24
View File
@@ -1,13 +1,11 @@
#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor, opposite-anchor
#import "../util.typ": rotate-anchor
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false,
outside-name: false,
name-padding: 2pt,
debug: false
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
@@ -29,22 +27,10 @@
// 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 }
}
draw.content(
pos,
anchor: name-anchor,
padding: name-padding,
anchor: if name-rotate {rotate-anchor(side)} else {side},
padding: 2pt,
angle: if name-rotate {90deg} else {0deg},
name
)
@@ -74,9 +60,7 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: false,
outside-name: false,
name-padding: 2pt,
debug: false
) = {
let sides = (
"north": (tl, tr),
@@ -125,9 +109,7 @@
pos,
prev: pos-prev,
next: pos-next,
debug: debug,
outside-name: outside-name,
name-padding: name-padding,
debug: debug
)
}
}
+20
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)[
+139 -1
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw, coordinate
#import "util.typ": opposite-anchor, normalize-coord
#import "util.typ": opposite-anchor, normalize-coord, resolve-rel-coord
/// List of valid wire styles
/// #examples.wires
@@ -403,3 +403,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 the cardinal side the port faces
/// can be deduced by comparing the port with the `.center` anchor.
///
/// #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))
}
})