Compare commits

...
Author SHA1 Message Date
David Stadelmann 72eab1620c feat: Add anchor-based zigzag bend placement via zigzag-at
Introduce a new `zigzag-at` parameter on `wire.wire` as an alternative
to `zigzag-ratio` for positioning the middle bend segment of a
`style: "zigzag"` wire. Instead of a fraction of the endpoint span,
the bend is placed on the axis of a referenced anchor via CeTZ's
perpendicular operator (`|-`):

  - zigzag-dir "vertical":   segment sits at anchor.x
  - zigzag-dir "horizontal": segment sits at anchor.y

Accepted forms:
  - str                         — raw CeTZ anchor name (e.g. port anchor
                                  "elmt-port-id" or "elmt.center")
  - array / native CeTZ dict    — used as-is
  - (rel: <length>, to: <coord>) with scalar rel — ergonomic axial
    offset, rewritten to (rel: (dx, 0), to: ...) for vertical dir or
    (rel: (0, dy), to: ...) for horizontal dir

`zigzag-ratio` default flipped from `50%` to `auto` (still resolves
to 50%) to allow detecting an explicit user override. Panics on
misuse:
  - `zigzag-at` set together with an explicit `zigzag-ratio`
  - `zigzag-at` set with a non-zigzag `style`

Add `examples.zigzag-at` demonstrating both the string-anchor and the
ergonomic offset forms.
2026-09-14 17:47:09 +02:00
David Stadelmann fff5b2ecd5 feat: Support cross-element port alignment via align-with
Extend the port `align-with` field to accept references to ports of
other elements, in addition to the existing local-port form:

  - "<local-id>"                    → same-element port (unchanged)
  - (element: "<id>", port: "<id>") → cross-element reference
  - "<raw-anchor>"                  → arbitrary CeTZ anchor name

Cross-element alignment is implemented via CeTZ perpendicular
projection (`|-`) so the port sits on this element's side line while
sharing the remote port's y (east/west) or x (north/south) coordinate.
Local resolution still validates matching axis and detects cycles;
external refs skip axis validation (remote side unknown at eval time)
and require the target element to be drawn first.

Also enrich port-related panic messages with the element id and port
context to make offending figures easier to locate, and add a manual
example (`examples.align-with-ports`) demonstrating both local and
cross-element alignment.
2026-09-14 17:12:57 +02:00
David Stadelmann f7cb457202 fix: Make ports always appear on the highest layer 2026-09-14 15:18:23 +02:00
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
6 changed files with 9020 additions and 7958 deletions

No files matched your search

+69
View File
@@ -23,6 +23,31 @@ wire.stub("block-port-in2", "west")
wire.stub("block-port-out", "east")
```)
#let align-with-ports = example(```
// `align-with` reuses another port's coord
// (E/W: y, N/S: x). Forms:
// 1. "id" — local
// 2. (element:"e", port:"p") — cross-elmt
// 3. "raw-anchor" — CeTZ anchor
element.block(x: 0, y: 0, w: 2, h: 2, id: "b1",
ports: (
west: ((id: "a", name: "A", y: 30%),),
east: ((id: "oa", name: "A'",
"align-with": "a"),),
))
element.block(x: 4, y: -.3, w: 2, h: 2.6, id: "b2",
ports: (
west: ((id: "ia", name: "A",
"align-with":
(element: "b1", port: "oa")),),
east: ((id: "o", name: "C"),),
))
wire.stub("b1-port-a", "west")
wire.wire("w", ("b1-port-oa", "b2-port-ia"),
style: "direct")
wire.stub("b2-port-o", "east")
```, vertical: true)
#let extender = example(```
element.extender(
x: 0, y: 0, w: 3, h: 1,
@@ -351,6 +376,29 @@ 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)),
@@ -399,3 +447,24 @@ element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB",
wire.wire("uSS", ("sA-port-out", "sB-port-out"),
style: "u-shape", u-side: "south", u-depth: 0.5)
```, vertical: true)
#let zigzag-at = example(```
// `zigzag-at` places the middle bend of a zigzag wire
// on the axis of a referenced anchor, overriding
// `zigzag-ratio`.
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "src",
ports: (east: ((id: "o", name: "O"),),))
element.block(x: 3, y: -1.2, w: 1, h: 1, id: "ref",
fill: util.colors.pink,
ports: (west: ((id: "p", name: "R"),),))
element.block(x: 5.5, y: -2, w: 1.5, h: 1.5, id: "dst",
ports: (west: ((id: "i", name: "I"),),))
// String form: anchor name of a port.
wire.wire("w1", ("src-port-o", "dst-port-i"),
style: "zigzag",
zigzag-at: "ref-port-p")
// Ergonomic offset: shift 8pt right of ref.east.
wire.wire("w2", ("src-port-o", "dst-port-i"),
style: "zigzag",
zigzag-at: (rel: 8pt, to: "ref.east"))
```, vertical: true)
+8604 -7894
View File
File diff suppressed because it is too large. Load diff
+10 -1
View File
@@ -25,7 +25,13 @@
/// #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,
@@ -35,7 +41,7 @@
ports: (),
ports-margins: (),
port-name-padding: 2pt,
port-names-outside: false,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
@@ -43,11 +49,13 @@
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,
@@ -64,5 +72,6 @@
fill: fill,
stroke: stroke,
id: id,
shell: shell,
debug: debug
)
+17 -3
View File
@@ -36,9 +36,12 @@
/// - `name` (`str`): Optional name displayed *in* the block
/// - `clock` (`bool`): Whether it is a clock port (triangle symbol)
/// - `vertical` (`bool`): Whether the name should be drawn vertically
/// - `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.
/// - `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.
/// - 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): If true, port name labels are drawn on the outside of the block instead of the inside (default `false`)
/// - 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
@@ -47,6 +50,7 @@
/// - 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:
@@ -62,7 +66,7 @@
ports: (:),
ports-margins: (:),
port-name-padding: 2pt,
port-names-outside: false,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
@@ -71,6 +75,8 @@
id: "",
auto-ports: true,
ports-y: (:),
shell: false,
body: none,
debug: (
ports: false
)
@@ -145,19 +151,27 @@
}
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,
width: width,
height: height,
debug: debug.ports,
name-padding: port-name-padding,
outside-name: port-names-outside,
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
}
})
+227 -38
View File
@@ -13,6 +13,7 @@
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)
@@ -38,12 +39,14 @@
if shape == "pentagon" {
let sh = port-shape-vertices(side, kind, size)
let v = sh.vertices.map(off => (rel: off, to: pos))
draw.line(
// Draw pentagon on a higher layer so it always renders on top of wires
// and neighbouring elements.
draw.on-layer(1, 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 {
@@ -80,6 +83,10 @@
// (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(
label-pos,
anchor: name-anchor,
@@ -91,7 +98,7 @@
if debug {
draw.circle(
outer-pos,
anchor-pos,
name: id,
radius: .1,
stroke: none,
@@ -100,7 +107,7 @@
} else {
draw.hide(draw.circle(
outer-pos,
anchor-pos,
radius: 0,
stroke: none,
name: id
@@ -108,11 +115,126 @@
}
}
// Normalize an `align-with` value to either:
// - `(local: <id>)` → reference to another port of the *same* element
// - `(external: <anchor-name>)` → CeTZ anchor name of a port in another element
// Accepted input forms:
// - `str` matching a local port id → local
// - `str` not matching any local port id → external, used as-is as CeTZ anchor
// - `(element: <str>, port: <str>)` → external, anchor = `<element>-port-<port>`
#let classify-align-with(raw-val, placements) = {
if type(raw-val) == dictionary {
if "element" in raw-val and "port" in raw-val {
return (external: raw-val.element + "-port-" + raw-val.port)
}
panic("`align-with` dictionary must contain `element` and `port` keys, got: " + repr(raw-val))
}
if type(raw-val) != str {
panic("`align-with` must be a string or `(element:, port:)` dictionary, got: " + repr(raw-val))
}
if raw-val in placements {
return (local: raw-val)
}
return (external: raw-val)
}
// Resolve a single port's placement, following `align-with` references
// transitively and detecting cycles.
//
// Returns `(placements, result)` where `result` is either:
// - `(pct: <ratio>)` → percentage along the side (local placement)
// - `(external: <anchor-name>)` → align with the given remote CeTZ anchor
#let resolve-placement(placements, id, visiting, elmt-id: "") = {
let where = if elmt-id != "" { " (element '" + elmt-id + "')" } else { "" }
let p = placements.at(id)
if p.at("resolved", default: false) {
if "external-ref" in p {
return (placements, (external: p.external-ref))
}
return (placements, (pct: p.pct))
}
if id in visiting {
panic("align-with cycle involving port '" + id + "'" + where + " (path: "
+ visiting.join(" -> ") + " -> " + id + ")")
}
let raw = p.raw
let side = p.side
let is-ew = side in ("east", "west")
let has-align = "align-with" in raw
let has-x = "x" in raw
let has-y = "y" in raw
let has-num = has-x or has-y
if has-align and has-num {
panic("Port '" + id + "'" + where + ": cannot combine `align-with` with `x`/`y` override")
}
if is-ew and has-x {
panic("Port '" + id + "'" + where + " on side '" + side
+ "': use `y` for placement override on east/west sides, not `x`")
}
if (not is-ew) and has-y {
panic("Port '" + id + "'" + where + " on side '" + side
+ "': use `x` for placement override on north/south sides, not `y`")
}
let result = none
if has-num {
let val = if is-ew { raw.y } else { raw.x }
let dim = if is-ew { p.height } else { p.width }
if type(val) == function {
val = val(dim)
}
let pct
if type(val) == ratio {
pct = val
} else {
// Numeric length: measured from `pt0` along the side.
pct = (val / dim) * 100%
}
result = (pct: pct)
} else if has-align {
let classified = classify-align-with(raw.at("align-with"), placements)
if "external" in classified {
// Cross-element / raw anchor reference: axis cannot be validated here
// (remote side unknown at eval time). Propagate as external.
result = (external: classified.external)
} else {
let ref-id = classified.local
let ref-side = placements.at(ref-id).side
let ref-ew = ref-side in ("east", "west")
if is-ew != ref-ew {
panic("Port '" + id + "'" + where + " on '" + side + "' cannot `align-with` port '"
+ ref-id + "' on '" + ref-side + "': incompatible axis")
}
let new-visiting = visiting + (id,)
let res = resolve-placement(placements, ref-id, new-visiting, elmt-id: elmt-id)
placements = res.at(0)
result = res.at(1)
}
} else {
result = (pct: p.pct-auto)
}
if "external" in result {
p.insert("external-ref", result.external)
p.insert("pct", none)
} else {
p.insert("pct", result.pct)
}
p.insert("resolved", true)
placements.insert(id, p)
return (placements, result)
}
#let add-ports(
elmt-id,
tl, tr, br, bl,
ports,
ports-margins,
width: 0,
height: 0,
debug: false,
outside-name: false,
name-padding: 2pt,
@@ -121,6 +243,7 @@
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let sides = (
"north": (tl, tr),
@@ -133,51 +256,117 @@
return
}
// ---- Phase 1: build placement table with per-port baseline pct. ----
let placements = (:)
// Preserve iteration order for phase 3.
let order = ()
// Per-side (pt0, pt1) after margin adjustment, and per-side spacing.
let side-info = (:)
for (side, props) in sides {
let side-ports = ports.at(side, default: ())
let pt0 = props.at(0)
let pt1 = props.at(1)
if side in ports-margins {
let margins = ports-margins.at(side)
let a = (pt0, margins.at(0), pt1)
let b = (pt0, 100% - margins.at(1), pt1)
pt0 = a
pt1 = b
}
let space = 100% / (side-ports.len() + 1)
side-info.insert(side, (pt0: pt0, pt1: pt1, space: space))
for (i, port) in side-ports.enumerate() {
let pct = (i + 1) * space
let pt0 = props.at(0)
let pt1 = props.at(1)
if side in ports-margins {
let (a, b) = (pt0, pt1)
let margins = ports-margins.at(side)
a = (pt0, margins.at(0), pt1)
b = (pt0, 100% - margins.at(1), pt1)
pt0 = a
pt1 = b
let pid = port.at("id")
if pid in placements {
panic("Duplicate port id '" + pid + "' in element '" + elmt-id
+ "' on side '" + side + "' (existing side: '"
+ placements.at(pid).side + "', new port: " + repr(port) + ")")
}
let pos = (pt0, pct, pt1)
let pct-prev = (i + 0.5) * space
let pct-next = (i + 1.5) * space
let pos-prev = (pt0, pct-prev, pt1)
let pos-next = (pt0, pct-next, pt1)
let pct-auto = (i + 1) * space
placements.insert(pid, (
side: side,
raw: port,
index: i,
pct-auto: pct-auto,
width: width,
height: height,
resolved: false,
))
order.push(pid)
}
}
// ---- Phase 2: resolve overrides & align-with references. ----
for pid in order {
let res = resolve-placement(placements, pid, (), elmt-id: elmt-id)
placements = res.at(0)
}
// ---- Phase 3: draw. ----
for pid in order {
let p = placements.at(pid)
let side = p.side
let info = side-info.at(side)
let pt0 = info.pt0
let pt1 = info.pt1
let space = info.space
let port = p.raw
let is-ew = side in ("east", "west")
let pos
let pos-prev
let pos-next
if "external-ref" in p {
// Cross-element alignment: build pos via CeTZ perpendicular so this
// port shares one coordinate with the remote anchor and stays on this
// element's side line for the other coordinate.
// east/west: same y as remote, x from this side (pt0.x == pt1.x)
// north/south: same x as remote, y from this side (pt0.y == pt1.y)
let ref = p.external-ref
if is-ew {
pos = (pt0, "|-", ref)
// Fallback prev/next: just the pos itself; clock ports do not
// combine meaningfully with cross-element align.
pos-prev = pos
pos-next = pos
} else {
pos = (ref, "|-", pt0)
pos-prev = pos
pos-next = pos
}
} else {
let pct = p.pct
pos = (pt0, pct, pt1)
pos-prev = (pt0, pct - 0.5 * space, pt1)
pos-next = (pt0, pct + 0.5 * space, pt1)
if port.at("small", default: false) {
pos-prev = (pos, 4pt, pt0)
pos-next = (pos, 4pt, pt1)
}
add-port(
elmt-id,
side,
port,
pos,
prev: pos-prev,
next: pos-next,
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,
)
}
add-port(
elmt-id,
side,
port,
pos,
prev: pos-prev,
next: pos-next,
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,
)
}
}
+93 -22
View File
@@ -27,29 +27,80 @@
return (pts, anchors)
}
#let get-zigzag-wire(pts, ratio, dir) = {
// Normalize a `zigzag-at` value.
//
// Accepted forms:
// - `none` → unset; ratio-based placement is used.
// - `str` / tuple / native CeTZ dict → passed through as a CeTZ coordinate.
// - `(rel: <length>, to: <coord>)` with a *scalar* `rel`
// → ergonomic axial offset, rewritten
// to `(rel: (rel, 0), to: to)` for
// `dir == "vertical"` or
// `(rel: (0, rel), to: to)` for
// `dir == "horizontal"`. If `rel` is
// already an array/tuple, the value
// is passed through unchanged.
#let normalize-zigzag-at(at, dir) = {
if at == none { return none }
if type(at) == dictionary and "rel" in at and "to" in at and type(at.rel) != array {
let r = at.rel
if dir == "vertical" {
return (rel: (r, 0), to: at.to)
} else {
return (rel: (0, r), to: at.to)
}
}
return at
}
#let get-zigzag-wire(pts, ratio, dir, at: none) = {
let start = pts.first()
let end = pts.last()
let mid = if dir == "vertical" {
(start, ratio, (horizontal: end, vertical: ()))
} else {
(start, ratio, (horizontal: (), vertical: end))
}
let points = if dir == "vertical" {
(
start,
(horizontal: mid, vertical: ()),
(horizontal: (), vertical: end),
end
)
let points
if at == none {
// Ratio-based placement (default).
let mid = if dir == "vertical" {
(start, ratio, (horizontal: end, vertical: ()))
} else {
(start, ratio, (horizontal: (), vertical: end))
}
points = if dir == "vertical" {
(
start,
(horizontal: mid, vertical: ()),
(horizontal: (), vertical: end),
end
)
} else {
(
start,
(horizontal: (), vertical: mid),
(horizontal: end, vertical: ()),
end
)
}
} else {
(
start,
(horizontal: (), vertical: mid),
(horizontal: end, vertical: ()),
end
)
// Anchor-based placement: the middle bend segment sits on the axis
// extracted from `at` via CeTZ's perpendicular operator `|-`:
// - vertical: middle segment at x = at.x
// - horizontal: middle segment at y = at.y
points = if dir == "vertical" {
(
start,
(at, "|-", start),
(at, "|-", end),
end
)
} else {
(
start,
(start, "|-", at),
(end, "|-", at),
end
)
}
}
let anchors = (
"start": start,
@@ -301,8 +352,9 @@
/// - reverse (bool): If true, the start and end points will be swapped (useful in cases where the start point depends on the end point, for example with perpendiculars)
/// - directed (bool): If true, the wire will be directed, meaning an arrow will be drawn at the endpoint
/// - rotate-name (bool): If true, the name will be rotated according to the wire's slope
/// - zigzag-ratio (ratio): Position of the zigzag vertical relative to the horizontal span (only with style "zigzag")
/// - zigzag-ratio (auto, ratio): Position of the zigzag middle bend segment relative to the horizontal (or vertical) span, as a fraction between the two endpoints (only with style "zigzag"). Defaults to `auto`, which resolves to `50%`. Panics if combined with an explicitly set `zigzag-at`.
/// - zigzag-dir (str): The zigzag's direction. As either "vertical" or "horizontal" (only with dstyle "zigzag")
/// - zigzag-at (none, str, array, dictionary): Optional. Anchor-based placement of the zigzag middle bend segment (only with style "zigzag"). When set, overrides `zigzag-ratio` (which must be left at its default `auto`). The middle segment sits on the axis extracted from the given coordinate: `x` for `zigzag-dir: "vertical"`, `y` for `"horizontal"`. Accepted forms: a raw CeTZ anchor name string (e.g. `"elmt.center"` or `"elmt-port-a"`); any native CeTZ coordinate (tuple, dict such as `(rel: (dx, dy), to: anchor)`); or the ergonomic scalar-offset form `(rel: <length>, to: <coord>)`, which is rewritten to a `(dx, 0)` offset for `"vertical"` and `(0, dy)` for `"horizontal"`. See #examples.zigzag-at
/// - dodge-y (number): Y position to dodge the wire to (only with style "dodge")
/// - dodge-sides (array): The start and end sides (going out of the connected element) of the wire (only with style "dodge")
/// - dodge-margins (array): The start and end margins (i.e. space before dodging) of the wire (only with style "dodge")
@@ -326,8 +378,9 @@
guided-sides: ("east", "west"),
directed: false,
rotate-name: true,
zigzag-ratio: 50%,
zigzag-ratio: auto,
zigzag-dir: "vertical",
zigzag-at: none,
dodge-y: 0,
dodge-sides: ("east", "west"),
dodge-margins: (5%, 5%),
@@ -342,6 +395,19 @@
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
}
// `zigzag-at` validation.
if zigzag-at != none {
if style != "zigzag" {
panic("wire '" + id + "': `zigzag-at` requires style: \"zigzag\" (got \""
+ style + "\")")
}
if zigzag-ratio != auto {
panic("wire '" + id
+ "': cannot combine `zigzag-at` with an explicit `zigzag-ratio`")
}
}
let zigzag-ratio-effective = if zigzag-ratio == auto { 50% } else { zigzag-ratio }
// Normalize user-provided coordinates: (x, y) -> (x, y, 0)
let pts = pts.map(normalize-coord)
@@ -360,7 +426,12 @@
(points, anchors) = get-direct-wire(pts)
} else if style == "zigzag" {
(points, anchors) = get-zigzag-wire(pts, zigzag-ratio, zigzag-dir)
(points, anchors) = get-zigzag-wire(
pts,
zigzag-ratio-effective,
zigzag-dir,
at: normalize-zigzag-at(zigzag-at, zigzag-dir),
)
} else if style == "dodge" {
(points, anchors) = get-dodge-wire(