Introduces a new "s-shape" wire style connecting two ports on opposite cardinal sides (east<->west or north<->south) via 5 segments joined at 4 corners (u1-start, u1-end, u2-start, u2-end). The axis is auto-detected from port anchors; when both endpoints share the same perpendicular coordinate (single-block loopback) `s-ratio-mid` is reinterpreted as an absolute signed offset so the crossbar can still be placed above or below the endpoints. Also adds `u-at` to the "u-shape" style, an anchor-based crossbar placement analogous to `zigzag-at`. `u-depth` now defaults to `auto` to allow clean mutual-exclusion with `u-at`. Factors the shared placement normalizer into `normalize-scalar-offset(at, axis)`; `normalize-zigzag-at` remains as a thin backwards-compatible wrapper.
1049 lines
37 KiB
Typst
1049 lines
37 KiB
Typst
#import "/src/cetz.typ": draw, coordinate
|
|
#import "util.typ": opposite-anchor, normalize-coord, resolve-rel-coord
|
|
|
|
/// List of valid wire styles
|
|
/// #examples.wires
|
|
///
|
|
/// The `"u-shape"` style connects two ports that face the same cardinal side.
|
|
/// #examples.u-shape-wire
|
|
///
|
|
/// The `"u-shape"` crossbar can also be aligned to an anchor via `u-at`
|
|
/// (analogous to `zigzag-at`).
|
|
/// #examples.u-at
|
|
///
|
|
/// The `"s-shape"` style connects two ports that face opposite cardinal sides
|
|
/// (east↔west or north↔south) via 5 segments.
|
|
/// #examples.s-shape-wire
|
|
///
|
|
/// The `"s-shape"` style also supports the vertical axis (north↔south):
|
|
/// #examples.s-shape-wire-vertical
|
|
///
|
|
/// The `"s-shape"` style can also loop a single block's output back to its
|
|
/// own input by wrapping around above (or below) via a C-shape:
|
|
/// #examples.s-shape-wire-loopback
|
|
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape", "s-shape")
|
|
#let signal-width = 1pt
|
|
#let bus-width = 1.5pt
|
|
|
|
/// Draws a wire intersection at the given anchor
|
|
/// #examples.intersection
|
|
/// - pt (point): A CeTZ compatible point / anchor
|
|
/// - radius (number): The radius of the intersection
|
|
/// - fill (color): The fill color
|
|
#let intersection(pt, radius: .1, fill: black) = {
|
|
draw.circle(pt, radius: radius, stroke: none, fill: fill)
|
|
}
|
|
|
|
#let get-direct-wire(pts) = {
|
|
let anchors = (
|
|
"start": pts.first(),
|
|
"end": pts.last()
|
|
)
|
|
return (pts, anchors)
|
|
}
|
|
|
|
// Normalize an anchor-based placement value used by `zigzag-at`, `u-at`, and
|
|
// the `s-at-*` family.
|
|
//
|
|
// `axis` selects which cartesian component the offset applies to:
|
|
// - `"x"` → offset in x (used when the placement fixes an x coordinate, e.g.
|
|
// a vertical bend line for horizontal wire flow).
|
|
// - `"y"` → offset in y (used when the placement fixes a y coordinate).
|
|
//
|
|
// Accepted forms:
|
|
// - `none` → unset; caller falls back to
|
|
// depth/ratio-based placement.
|
|
// - `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
|
|
// `axis == "x"` or
|
|
// `(rel: (0, rel), to: to)` for
|
|
// `axis == "y"`. If `rel` is already
|
|
// an array/tuple, the value is
|
|
// passed through unchanged.
|
|
#let normalize-scalar-offset(at, axis) = {
|
|
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 axis == "x" {
|
|
return (rel: (r, 0), to: at.to)
|
|
} else {
|
|
return (rel: (0, r), to: at.to)
|
|
}
|
|
}
|
|
return at
|
|
}
|
|
|
|
// Backwards-compatible wrapper: translates the old `dir` vocabulary
|
|
// (`"vertical"` / `"horizontal"`) to the `axis` vocabulary used by
|
|
// `normalize-scalar-offset`.
|
|
#let normalize-zigzag-at(at, dir) = {
|
|
let axis = if dir == "vertical" { "x" } else { "y" }
|
|
return normalize-scalar-offset(at, axis)
|
|
}
|
|
|
|
#let get-zigzag-wire(pts, ratio, dir, at: none) = {
|
|
let start = pts.first()
|
|
let end = pts.last()
|
|
|
|
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 {
|
|
// 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,
|
|
"zig": points.at(1),
|
|
"zag": points.at(2),
|
|
"end": end
|
|
)
|
|
return (points, anchors)
|
|
}
|
|
|
|
#let get-dodge-wire(pts, dodge-y, margins, sides, ctx) = {
|
|
let start = pts.first()
|
|
let end = pts.last()
|
|
let (margin-start, margin-end) = margins
|
|
let (side-start, side-end) = sides
|
|
|
|
let p1 = (start, margin-start, end)
|
|
let p2 = (end, margin-end, start)
|
|
|
|
let (ctx, p0) = coordinate.resolve(ctx, start)
|
|
let (ctx, p3) = coordinate.resolve(ctx, end)
|
|
p0 = (x: p0.first(), y: p0.at(1))
|
|
p3 = (x: p3.first(), y: p3.at(1))
|
|
|
|
let dx1 = margin-start
|
|
let dx2 = margin-end
|
|
|
|
if type(margin-start) == ratio {
|
|
dx1 = calc.abs(p3.x - p0.x) * margin-start / 100%
|
|
}
|
|
if type(margin-end) == ratio {
|
|
dx2 = calc.abs(p3.x - p0.x) * margin-end / 100%
|
|
}
|
|
if side-start == "west" {
|
|
dx1 *= -1
|
|
}
|
|
if side-end == "east" {
|
|
dx2 *= -1
|
|
}
|
|
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, 0)),
|
|
(horizontal: p2, vertical: ()),
|
|
(horizontal: (), vertical: end),
|
|
end
|
|
)
|
|
let anchors = (
|
|
"start": start,
|
|
"start2": points.at(1),
|
|
"dodge-start": points.at(2),
|
|
"dodge-end": points.at(3),
|
|
"end2": points.at(4),
|
|
"end": end
|
|
)
|
|
|
|
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, u-at, 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
|
|
|
|
// If `u-at` is set, resolve its coordinate and extract the relevant axis
|
|
// component; use it as the crossbar position instead of computing from
|
|
// `u-depth`.
|
|
let bar-at = none
|
|
if u-at != none {
|
|
let axis = if side == "east" or side == "west" { "x" } else { "y" }
|
|
let at-norm = normalize-scalar-offset(u-at, axis)
|
|
let (ctx2, r) = coordinate.resolve(ctx, at-norm)
|
|
bar-at = if axis == "x" { r.first() } else { r.at(1) }
|
|
}
|
|
|
|
let p1
|
|
let p2
|
|
if side == "east" {
|
|
let bx = if bar-at != none { bar-at } else { calc.max(sx, ex) + u-depth }
|
|
p1 = (bx, sy, 0)
|
|
p2 = (bx, ey, 0)
|
|
} else if side == "west" {
|
|
let bx = if bar-at != none { bar-at } else { calc.min(sx, ex) - u-depth }
|
|
p1 = (bx, sy, 0)
|
|
p2 = (bx, ey, 0)
|
|
} else if side == "north" {
|
|
let by = if bar-at != none { bar-at } else { calc.max(sy, ey) + u-depth }
|
|
p1 = (sx, by, 0)
|
|
p2 = (ex, by, 0)
|
|
} else if side == "south" {
|
|
let by = if bar-at != none { bar-at } else { 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)
|
|
}
|
|
|
|
// Valid combinations of endpoint sides for an S-shape wire.
|
|
// Keyed by (sorted) pair; value is the wire axis.
|
|
#let _s-axis-from-sides(a, b) = {
|
|
if a == none or b == none { return none }
|
|
let pair = (a, b)
|
|
if ("east" in pair) and ("west" in pair) { return "horizontal" }
|
|
if ("north" in pair) and ("south" in pair) { return "vertical" }
|
|
return none
|
|
}
|
|
|
|
// S-shape wire: 5 segments, 4 corners; connects endpoints on opposite sides.
|
|
//
|
|
// Horizontal axis (east ↔ west): sequence exits/enters through horizontal
|
|
// segments; two vertical segments join them via a horizontal crossbar.
|
|
//
|
|
// Corner layout (horizontal axis):
|
|
// start ── c1
|
|
// │
|
|
// c2 ─────────── c3
|
|
// │
|
|
// c4 ── end
|
|
//
|
|
// (or mirrored, depending on which endpoint faces east vs west, and on the
|
|
// relative y of the endpoints; when y_mid is outside the endpoint span the
|
|
// shape degenerates into a C-shape.)
|
|
#let get-s-shape-wire(
|
|
pts,
|
|
s-axis,
|
|
s-depth-1,
|
|
s-ratio-mid,
|
|
s-depth-2,
|
|
s-at-1,
|
|
s-at-mid,
|
|
s-at-2,
|
|
strict,
|
|
ctx,
|
|
) = {
|
|
let start = pts.first()
|
|
let end = pts.last()
|
|
|
|
// Detect endpoint sides
|
|
let side-start = _u-detect-side(start, ctx)
|
|
let side-end = _u-detect-side(end, ctx)
|
|
|
|
// Resolve (auto|explicit) axis
|
|
let axis = s-axis
|
|
if axis == auto {
|
|
let detected = _s-axis-from-sides(side-start, side-end)
|
|
if detected == none {
|
|
panic(
|
|
"s-shape: could not auto-detect s-axis; endpoints must be port anchors "
|
|
+ "facing opposite sides on the same axis (east+west or north+south). "
|
|
+ "Detected start='" + repr(side-start) + "', end='" + repr(side-end)
|
|
+ "'. Set s-axis explicitly to \"horizontal\" or \"vertical\"."
|
|
)
|
|
}
|
|
axis = detected
|
|
}
|
|
if axis != "horizontal" and axis != "vertical" {
|
|
panic("s-shape: invalid s-axis '" + repr(axis) + "'; must be \"horizontal\" or \"vertical\"")
|
|
}
|
|
|
|
// Effective depth/ratio (auto → defaults)
|
|
let depth-1 = if s-depth-1 == auto { 0.5 } else { s-depth-1 }
|
|
let depth-2 = if s-depth-2 == auto { 0.5 } else { s-depth-2 }
|
|
let ratio-mid-raw = if s-ratio-mid == auto { 50% } else { s-ratio-mid }
|
|
let ratio-mid = ratio-mid-raw / 100%
|
|
|
|
let (ctx, s) = coordinate.resolve(ctx, start)
|
|
let (ctx, e) = coordinate.resolve(ctx, end)
|
|
let (sx, sy, ..) = s
|
|
let (ex, ey, ..) = e
|
|
|
|
// Determine outward directions from each port
|
|
let outward(side) = {
|
|
if side == "east" { return (1, 0) }
|
|
if side == "west" { return (-1, 0) }
|
|
if side == "north" { return (0, 1) }
|
|
if side == "south" { return (0, -1) }
|
|
return none
|
|
}
|
|
let out-s = outward(side-start)
|
|
let out-e = outward(side-end)
|
|
// Fallback when a port side isn't known (e.g. non-port endpoint): infer from
|
|
// the axis, assuming the wire flows toward the far endpoint.
|
|
if out-s == none {
|
|
if axis == "horizontal" { out-s = (if ex > sx { 1 } else { -1 }, 0) }
|
|
else { out-s = (0, if ey > sy { 1 } else { -1 }) }
|
|
}
|
|
if out-e == none {
|
|
if axis == "horizontal" { out-e = (if sx > ex { 1 } else { -1 }, 0) }
|
|
else { out-e = (0, if sy > ey { 1 } else { -1 }) }
|
|
}
|
|
|
|
// Resolve the three placement overrides
|
|
let resolve-axial(at, ax) = {
|
|
if at == none { return none }
|
|
let at-norm = normalize-scalar-offset(at, ax)
|
|
let (_, r) = coordinate.resolve(ctx, at-norm)
|
|
return if ax == "x" { r.first() } else { r.at(1) }
|
|
}
|
|
|
|
let c1x
|
|
let c1y
|
|
let c2x
|
|
let c2y
|
|
let c3x
|
|
let c3y
|
|
let c4x
|
|
let c4y
|
|
|
|
if axis == "horizontal" {
|
|
// seg1 and seg5 are horizontal; the two verticals sit at x = x1 and x = x2.
|
|
let x1-at = resolve-axial(s-at-1, "x")
|
|
let x2-at = resolve-axial(s-at-2, "x")
|
|
let y-mid-at = resolve-axial(s-at-mid, "y")
|
|
|
|
let x1 = if x1-at != none { x1-at } else { sx + out-s.at(0) * depth-1 }
|
|
let x2 = if x2-at != none { x2-at } else { ex + out-e.at(0) * depth-2 }
|
|
// When the endpoints share the same y (degenerate span), the ratio-based
|
|
// formula collapses to sy. Fall back to a unit offset scaled by ratio-mid
|
|
// so the crossbar can still be pushed away from the endpoint axis
|
|
// (e.g. for single-block loopbacks). Direction defaults to "down" (-y).
|
|
let y-mid = if y-mid-at != none {
|
|
y-mid-at
|
|
} else if ey == sy {
|
|
sy - ratio-mid
|
|
} else {
|
|
sy + ratio-mid * (ey - sy)
|
|
}
|
|
|
|
c1x = x1; c1y = sy
|
|
c2x = x1; c2y = y-mid
|
|
c3x = x2; c3y = y-mid
|
|
c4x = x2; c4y = ey
|
|
|
|
// Sanity checks (strict-only)
|
|
if strict {
|
|
// seg1 must extend outward: sign(x1 - sx) should match out-s.x
|
|
if (x1 - sx) * out-s.at(0) <= 0 {
|
|
panic("s-shape: seg1 does not extend outward from start port "
|
|
+ "(x1=" + str(x1) + ", start.x=" + str(sx) + ", outward.x="
|
|
+ str(out-s.at(0)) + "). Adjust s-depth-1 or s-at-1.")
|
|
}
|
|
if (x2 - ex) * out-e.at(0) <= 0 {
|
|
panic("s-shape: seg5 does not extend outward from end port "
|
|
+ "(x2=" + str(x2) + ", end.x=" + str(ex) + ", outward.x="
|
|
+ str(out-e.at(0)) + "). Adjust s-depth-2 or s-at-2.")
|
|
}
|
|
}
|
|
} else {
|
|
// vertical axis: seg1 and seg5 are vertical; the two horizontals sit at
|
|
// y = y1 and y = y2; the middle crossbar is vertical at x = x-mid.
|
|
let y1-at = resolve-axial(s-at-1, "y")
|
|
let y2-at = resolve-axial(s-at-2, "y")
|
|
let x-mid-at = resolve-axial(s-at-mid, "x")
|
|
|
|
let y1 = if y1-at != none { y1-at } else { sy + out-s.at(1) * depth-1 }
|
|
let y2 = if y2-at != none { y2-at } else { ey + out-e.at(1) * depth-2 }
|
|
// Degenerate-span fallback: see comment in the horizontal branch.
|
|
let x-mid = if x-mid-at != none {
|
|
x-mid-at
|
|
} else if ex == sx {
|
|
sx + ratio-mid
|
|
} else {
|
|
sx + ratio-mid * (ex - sx)
|
|
}
|
|
|
|
c1x = sx; c1y = y1
|
|
c2x = x-mid; c2y = y1
|
|
c3x = x-mid; c3y = y2
|
|
c4x = ex; c4y = y2
|
|
|
|
if strict {
|
|
if (y1 - sy) * out-s.at(1) <= 0 {
|
|
panic("s-shape: seg1 does not extend outward from start port "
|
|
+ "(y1=" + str(y1) + ", start.y=" + str(sy) + ", outward.y="
|
|
+ str(out-s.at(1)) + "). Adjust s-depth-1 or s-at-1.")
|
|
}
|
|
if (y2 - ey) * out-e.at(1) <= 0 {
|
|
panic("s-shape: seg5 does not extend outward from end port "
|
|
+ "(y2=" + str(y2) + ", end.y=" + str(ey) + ", outward.y="
|
|
+ str(out-e.at(1)) + "). Adjust s-depth-2 or s-at-2.")
|
|
}
|
|
}
|
|
}
|
|
|
|
let c1 = (c1x, c1y, 0)
|
|
let c2 = (c2x, c2y, 0)
|
|
let c3 = (c3x, c3y, 0)
|
|
let c4 = (c4x, c4y, 0)
|
|
|
|
let points = (start, c1, c2, c3, c4, end)
|
|
let anchors = (
|
|
"start": start,
|
|
"u1-start": c1,
|
|
"u1-end": c2,
|
|
"u2-start": c3,
|
|
"u2-end": c4,
|
|
"end": end,
|
|
)
|
|
return (points, anchors)
|
|
}
|
|
|
|
#let get-guided-wire(pts, margins, sides, center-guides, ctx) = {
|
|
let start = pts.first()
|
|
let end = pts.last()
|
|
let (margin-start, margin-end) = margins
|
|
let (side-start, side-end) = sides
|
|
let (center_horizontal, center_vertical) = center-guides
|
|
|
|
let (ctx, p0) = coordinate.resolve(ctx, start)
|
|
let (ctx, p6) = coordinate.resolve(ctx, end)
|
|
p0 = (x: p0.first(), y: p0.at(1))
|
|
p6 = (x: p6.first(), y: p6.at(1))
|
|
|
|
let box_width = calc.abs(p6.x - p0.x)
|
|
let box_height = calc.abs(p6.y - p0.y)
|
|
|
|
// finding correct dx and dy
|
|
let dx1 = box_width * margin-start / 100%
|
|
if side-start == "west" {
|
|
dx1 *= -1
|
|
} else if side-start == "north" or side-start == "south" { dx1 = 0}
|
|
|
|
let dx2 = box_width * margin-end / 100%
|
|
if side-end == "west" {
|
|
dx2 *= -1
|
|
} else if side-end == "north" or side-end == "south" { dx2 = 0}
|
|
|
|
let dy1 = box_height * margin-start / 100%
|
|
if side-start == "south" {
|
|
dy1 *= -1
|
|
} else if side-start == "west" or side-start == "east" { dy1 = 0}
|
|
|
|
let dy2 = box_height * margin-end / 100%
|
|
if side-end == "south" {
|
|
dy2 *= -1
|
|
} else if side-end == "west" or side-end == "east" { dy2 = 0}
|
|
|
|
|
|
// points that are closest to the edge points
|
|
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, 0)
|
|
|
|
// setting up the points for that touch the guides
|
|
let p2 = (0, 0, 0)
|
|
let p4 = (0, 0, 0)
|
|
if side-start in ("north", "south") {
|
|
p2 = (horizontal: p3, vertical: p1)
|
|
} else {
|
|
p2 = (horizontal: p1, vertical: p3)
|
|
}
|
|
if side-end in ("north", "south") {
|
|
p4 = (horizontal: p3, vertical: p5)
|
|
} else if side-end in ("east", "west") {
|
|
p4 = (horizontal: p5, vertical: p3)
|
|
}
|
|
|
|
// returning
|
|
let points = (
|
|
start,
|
|
p1,
|
|
p2,
|
|
p3,
|
|
p4,
|
|
p5,
|
|
end
|
|
)
|
|
let anchors = (
|
|
"start": start,
|
|
"start2": points.at(1),
|
|
"guide-start": points.at(2),
|
|
"center": points.at(3),
|
|
"guide-end": points.at(4),
|
|
"end2": points.at(5),
|
|
"end": end
|
|
)
|
|
return (points, anchors)
|
|
}
|
|
/// Draws a wire between two points
|
|
/// - id (str): The wire's id, for future reference (anchors)
|
|
/// - pts (array): The two points (as CeTZ compatible coordinates, i.e. XY, relative positions, ids, etc.)
|
|
/// - bus (bool): Whether the wire is a bus (multiple bits) or a simple signal (single bit)
|
|
/// - name (none, str, array): Optional name of the wire. If it is an array, the first name will be put at the start of the wire, and the second at the end
|
|
/// - name-pos (str): Position of the name. One of: "middle", "start" or "end"
|
|
/// - slice (none, array): Optional bits slice (start and end bit indices). If set, it will be displayed at the start of the wire
|
|
/// - color (color): The stroke color
|
|
/// - dashed (bool): Whether the stroke is dashed or not
|
|
/// - style (str): The wire's style (see #doc-ref("wire.wire-styles", var: true) for possible values)
|
|
/// - 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 (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 (auto, str): The zigzag's direction, as either "vertical" or "horizontal" (only with style "zigzag"). 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: east/west-facing ports yield `"vertical"` (wire exits horizontally) and north/south-facing ports yield `"horizontal"`. Auto-detection panics if the two endpoints resolve to conflicting directions, or if neither endpoint is a port anchor.
|
|
/// - 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")
|
|
/// - 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 (auto, number): Distance between the outermost endpoint (in the `u-side` direction) and the U-bend line (only with style "u-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicitly set `u-at`.
|
|
/// - u-at (none, str, array, dictionary): Optional. Anchor-based placement of the U-bend crossbar (only with style "u-shape"). When set, overrides `u-depth` (which must be left at its default `auto`). The crossbar sits on the axis extracted from the given coordinate: `x` for `u-side` in `("east", "west")`, `y` for `("north", "south")`. Accepted forms mirror `zigzag-at`: raw CeTZ anchor name string, any native CeTZ coordinate, or the ergonomic scalar-offset form `(rel: <length>, to: <coord>)`, which is rewritten to `(dx, 0)` for east/west and `(0, dy)` for north/south.
|
|
/// - s-axis (auto, str): The s-shape wire axis, one of "horizontal" or "vertical" (only with style "s-shape"). If `auto`, it is deduced from the endpoint port anchors: opposite east/west ports yield `"horizontal"`, opposite north/south ports yield `"vertical"`. Auto-detection panics on same-side, cross-axis, or non-port endpoints.
|
|
/// - s-depth-1 (auto, number): Distance seg1 (the first outward segment) extends from the start port (only with style "s-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicit `s-at-1`.
|
|
/// - s-ratio-mid (auto, ratio): Position of the s-shape crossbar between the endpoints along the axis perpendicular to `s-axis` (only with style "s-shape"). Defaults to `auto`, which resolves to `50%`. Values outside `0%..100%` are allowed and yield a C-shape variant. When the two endpoints share the same perpendicular coordinate (e.g. a single-block loopback where both ports are at the same y), the endpoint span is zero and `s-ratio-mid` is instead interpreted as an absolute offset from that shared coordinate: the crossbar sits `s-ratio-mid` units below the endpoints for horizontal S, or `s-ratio-mid` units right for vertical S (using `1` = ratio unit). Panics if combined with an explicit `s-at-mid`.
|
|
/// - s-depth-2 (auto, number): Distance seg5 (the last inward segment) extends from the end port (only with style "s-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicit `s-at-2`.
|
|
/// - s-at-1 (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-depth-1` (only with style "s-shape"). Fixes the x (horizontal S) or y (vertical S) of the first vertical bend line. Accepted forms mirror `zigzag-at`.
|
|
/// - s-at-mid (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-ratio-mid` (only with style "s-shape"). Fixes the y (horizontal S) or x (vertical S) of the crossbar. Accepted forms mirror `zigzag-at`.
|
|
/// - s-at-2 (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-depth-2` (only with style "s-shape"). Fixes the x (horizontal S) or y (vertical S) of the second vertical bend line. Accepted forms mirror `zigzag-at`.
|
|
/// - strict (bool): If true, s-shape performs sanity checks and panics when seg1/seg5 do not extend outward from their port (only with style "s-shape"). Defaults to `false`.
|
|
#let wire(
|
|
id, pts,
|
|
bus: false,
|
|
name: none,
|
|
name-pos: "middle",
|
|
slice: none,
|
|
color: black,
|
|
dashed: false,
|
|
style: "direct",
|
|
reverse: false,
|
|
guided-center: (50%, 50%),
|
|
guided-margins: (5%, 5%),
|
|
guided-sides: ("east", "west"),
|
|
directed: false,
|
|
rotate-name: true,
|
|
zigzag-ratio: auto,
|
|
zigzag-dir: auto,
|
|
zigzag-at: none,
|
|
dodge-y: 0,
|
|
dodge-sides: ("east", "west"),
|
|
dodge-margins: (5%, 5%),
|
|
u-side: auto,
|
|
u-depth: auto,
|
|
u-at: none,
|
|
s-axis: auto,
|
|
s-depth-1: auto,
|
|
s-ratio-mid: auto,
|
|
s-depth-2: auto,
|
|
s-at-1: none,
|
|
s-at-mid: none,
|
|
s-at-2: none,
|
|
strict: false,
|
|
) = draw.get-ctx(ctx => {
|
|
if not style in wire-styles {
|
|
panic("Invalid wire style '" + style + "'")
|
|
}
|
|
|
|
if pts.len() != 2 {
|
|
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 }
|
|
|
|
// `u-at` validation.
|
|
if u-at != none {
|
|
if style != "u-shape" {
|
|
panic("wire '" + id + "': `u-at` requires style: \"u-shape\" (got \""
|
|
+ style + "\")")
|
|
}
|
|
if u-depth != auto {
|
|
panic("wire '" + id
|
|
+ "': cannot combine `u-at` with an explicit `u-depth`")
|
|
}
|
|
}
|
|
let u-depth-effective = if u-depth == auto { 0.5 } else { u-depth }
|
|
|
|
// `s-at-*` validation.
|
|
let s-params-set = (
|
|
s-axis != auto or s-depth-1 != auto or s-ratio-mid != auto
|
|
or s-depth-2 != auto or s-at-1 != none or s-at-mid != none
|
|
or s-at-2 != none
|
|
)
|
|
if s-params-set and style != "s-shape" {
|
|
panic("wire '" + id + "': `s-*` parameters require style: \"s-shape\" (got \""
|
|
+ style + "\")")
|
|
}
|
|
if s-at-1 != none and s-depth-1 != auto {
|
|
panic("wire '" + id + "': cannot combine `s-at-1` with an explicit `s-depth-1`")
|
|
}
|
|
if s-at-mid != none and s-ratio-mid != auto {
|
|
panic("wire '" + id + "': cannot combine `s-at-mid` with an explicit `s-ratio-mid`")
|
|
}
|
|
if s-at-2 != none and s-depth-2 != auto {
|
|
panic("wire '" + id + "': cannot combine `s-at-2` with an explicit `s-depth-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}
|
|
)
|
|
if dashed {
|
|
stroke.insert("dash", "dashed")
|
|
}
|
|
|
|
let points = ()
|
|
let anchors = ()
|
|
|
|
if style == "direct" {
|
|
(points, anchors) = get-direct-wire(pts)
|
|
|
|
} else if style == "zigzag" {
|
|
let zz-dir = zigzag-dir
|
|
if zz-dir == auto {
|
|
let side-start = _u-detect-side(pts.first(), ctx)
|
|
let side-end = _u-detect-side(pts.last(), ctx)
|
|
let dir-from-side(s) = if s == none { none } else if s == "east" or s == "west" { "vertical" } else { "horizontal" }
|
|
let d-start = dir-from-side(side-start)
|
|
let d-end = dir-from-side(side-end)
|
|
if d-start != none and d-end != none and d-start != d-end {
|
|
panic("zigzag: start port faces '" + side-start
|
|
+ "' but end port faces '" + side-end
|
|
+ "'; cannot auto-detect `zigzag-dir`. Set it explicitly.")
|
|
}
|
|
if d-start != none { zz-dir = d-start }
|
|
else if d-end != none { zz-dir = d-end }
|
|
else { panic("zigzag: could not auto-detect `zigzag-dir` (no port anchor on either endpoint). Set it explicitly.") }
|
|
}
|
|
(points, anchors) = get-zigzag-wire(
|
|
pts,
|
|
zigzag-ratio-effective,
|
|
zz-dir,
|
|
at: normalize-zigzag-at(zigzag-at, zz-dir),
|
|
)
|
|
|
|
} else if style == "dodge" {
|
|
(points, anchors) = get-dodge-wire(
|
|
pts,
|
|
dodge-y,
|
|
dodge-margins,
|
|
dodge-sides,
|
|
ctx
|
|
)
|
|
} else if style == "guided" {
|
|
(points, anchors) = get-guided-wire(
|
|
pts,
|
|
guided-margins,
|
|
guided-sides,
|
|
guided-center,
|
|
ctx
|
|
)
|
|
} else if style == "u-shape" {
|
|
(points, anchors) = get-u-shape-wire(
|
|
pts,
|
|
u-depth-effective,
|
|
u-side,
|
|
u-at,
|
|
ctx
|
|
)
|
|
} else if style == "s-shape" {
|
|
(points, anchors) = get-s-shape-wire(
|
|
pts,
|
|
s-axis,
|
|
s-depth-1,
|
|
s-ratio-mid,
|
|
s-depth-2,
|
|
s-at-1,
|
|
s-at-mid,
|
|
s-at-2,
|
|
strict,
|
|
ctx,
|
|
)
|
|
}
|
|
|
|
let mark = (fill: color)
|
|
if directed {
|
|
mark = (end: ">", fill: color)
|
|
}
|
|
draw.group(name: id, {
|
|
draw.line(..points, stroke: stroke, mark: mark)
|
|
for (anchor-name, anchor-pos) in anchors {
|
|
draw.anchor(anchor-name, anchor-pos)
|
|
}
|
|
})
|
|
|
|
let first-pt = id + ".start"
|
|
let last-pt = id + ".end"
|
|
let first-pos = points.first()
|
|
let second-pos = points.at(1)
|
|
if reverse {
|
|
(first-pt, last-pt) = (last-pt, first-pt)
|
|
}
|
|
|
|
let angle = 0deg
|
|
if rotate-name {
|
|
(ctx, first-pos) = coordinate.resolve(ctx, first-pos)
|
|
(ctx, second-pos) = coordinate.resolve(ctx, second-pos)
|
|
|
|
if reverse {
|
|
(first-pos, second-pos) = (second-pos, first-pos)
|
|
}
|
|
let (x1, y1, ..) = first-pos
|
|
let (x2, y2, ..) = second-pos
|
|
angle = calc.atan2(x2 - x1, y2 - y1)
|
|
}
|
|
|
|
if name != none {
|
|
let names = ()
|
|
|
|
if type(name) == str or type(name) == content {
|
|
names = ((name, name-pos),)
|
|
|
|
} else if type(name) == array {
|
|
names = (
|
|
(name.at(0), "start"),
|
|
(name.at(1), "end")
|
|
)
|
|
}
|
|
|
|
for (name, pos) in names {
|
|
let point
|
|
let anchor
|
|
|
|
if pos == "middle" {
|
|
point = (first-pt, 50%, last-pt)
|
|
anchor = "south"
|
|
|
|
} else if pos == "start" {
|
|
point = first-pt
|
|
anchor = "south-west"
|
|
|
|
} else if pos == "end" {
|
|
point = last-pt
|
|
anchor = "south-east"
|
|
}
|
|
|
|
draw.content(point, anchor: anchor, padding: 3pt, angle: angle, name)
|
|
}
|
|
}
|
|
|
|
if slice != none {
|
|
let slice-txt = "[" + slice.map(b => str(b)).join(":") + "]"
|
|
|
|
draw.content(
|
|
first-pt,
|
|
anchor: "south-west",
|
|
padding: 3pt,
|
|
text(slice-txt, size: 0.75em)
|
|
)
|
|
}
|
|
})
|
|
|
|
/// Draws a wire stub (useful for unlinked ports)
|
|
///
|
|
/// #examples.stub
|
|
/// - port-id (str): The port anchor
|
|
/// - side (str): The side on which the port is (one of "north", "east", "south", "west")
|
|
/// - name (none, str): Optional name displayed at the end of the stub
|
|
/// - vertical (bool): Whether the name should be displayed vertically
|
|
/// - length (number): The length of the stub
|
|
/// - name-offset (number): The name offset, perpendicular to the stub
|
|
#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0) = {
|
|
let end-offset = (
|
|
north: (0, length),
|
|
east: (length, 0),
|
|
south: (0, -length),
|
|
west: (-length, 0)
|
|
).at(side)
|
|
|
|
let name-offset = (
|
|
north: (name-offset, length),
|
|
east: (length, name-offset),
|
|
south: (name-offset, -length),
|
|
west: (-length, name-offset)
|
|
).at(side)
|
|
|
|
draw.line(
|
|
port-id,
|
|
(rel: end-offset, to: port-id)
|
|
)
|
|
if name != none {
|
|
let text-anchor = if vertical {
|
|
(
|
|
"north": "west",
|
|
"south": "east",
|
|
"west": "south",
|
|
"east": "north"
|
|
).at(side)
|
|
} else { opposite-anchor(side) }
|
|
draw.content(
|
|
anchor: text-anchor,
|
|
padding: 0.2em,
|
|
angle: if vertical {90deg} else {0deg},
|
|
(rel: name-offset, to: port-id),
|
|
name
|
|
)
|
|
}
|
|
}
|
|
|
|
/// 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"` (or `"zigzag"`) 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))
|
|
}
|
|
})
|