forked from HEL/circuiteria
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.
723 lines
23 KiB
Typst
723 lines
23 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
|
|
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-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 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 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, 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()
|
|
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 (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")
|
|
/// - 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,
|
|
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: "vertical",
|
|
zigzag-at: none,
|
|
dodge-y: 0,
|
|
dodge-sides: ("east", "west"),
|
|
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 + "'")
|
|
}
|
|
|
|
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 }
|
|
|
|
// 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" {
|
|
(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(
|
|
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,
|
|
u-side,
|
|
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 {
|
|
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"` 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))
|
|
}
|
|
})
|