diff --git a/doc/examples.typ b/doc/examples.typ index a182e59..40da8e5 100644 --- a/doc/examples.typ +++ b/doc/examples.typ @@ -56,7 +56,7 @@ for i in range(3) { ```) #let wires = example(``` -for i in range(4) { +for i in range(5) { draw.circle((i * 3, 0), radius: .1, name: "p" + str(i * 2)) draw.circle((i * 3 + 2, 1), radius: .1, name: "p" + str(i * 2 + 1)) draw.content((i * 3 + 1, -1), raw(wire.wire-styles.at(i))) @@ -68,9 +68,103 @@ wire.wire("w3", ("p4", "p5"), style: "dodge", wire.wire("w4", ("p6","p7"), style: "guided", guided-center:(20%, 40%), guided-margins: (90%,87%), guided-sides: ("north","south")) - +wire.wire("w5", ("p8", "p9"), style: "u-shape", + u-side: "east", u-depth: 0.5) ```, vertical: true) +#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, +) + +#let u-shape-wire = example( + ``` + // east <-> east (u-side auto-detected) + element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA", + fill: util.colors.orange, + ports: (east: ((id: "out"),))) + element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB", + fill: util.colors.orange, + ports: (east: ((id: "out"),))) + wire.wire("uEE", ("eA-port-out", "eB-port-out"), + style: "u-shape", u-depth: 0.5) + + // west <-> west (auto-detected) + element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA", + fill: util.colors.yellow, + ports: (west: ((id: "in"),))) + element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB", + fill: util.colors.yellow, + ports: (west: ((id: "in"),))) + wire.wire("uWW", ("wA-port-in", "wB-port-in"), + style: "u-shape", u-depth: 0.5) + + // north <-> north (auto-detected) + element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA", + fill: util.colors.green, + ports: (north: ((id: "out"),))) + element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB", + fill: util.colors.green, + ports: (north: ((id: "out"),))) + wire.wire("uNN", ("nA-port-out", "nB-port-out"), + style: "u-shape", u-depth: 0.5) + + // south <-> south with explicit u-side override + element.block(x: 16, y: -3, w: 1.5, h: 1.5, id: "sA", + fill: util.colors.pink, + ports: (south: ((id: "out"),))) + element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB", + fill: util.colors.pink, + ports: (south: ((id: "out"),))) + wire.wire("uSS", ("sA-port-out", "sB-port-out"), + style: "u-shape", u-side: "south", u-depth: 0.5) + ```, + vertical: true, +) + +#let u-at = example( + ``` + // `u-at` places the U-bend crossbar on the axis + // of a referenced anchor, overriding `u-depth`. + element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA", + fill: util.colors.orange, + ports: (east: ((id: "o1"), (id: "o2")))) + element.block(x: 0, y: -3.5, w: 1.5, h: 1.5, id: "eB", + fill: util.colors.orange, + ports: (east: ((id: "i1"), (id: "i2")))) + element.block(x: 4, y: -2.25, w: 0.6, h: 0.6, id: "ref", + fill: util.colors.pink) + // Align one U crossbar with ref.east. + wire.wire("u1", ("eA-port-o1", "eB-port-i1"), + style: "u-shape", + u-at: "ref.east") + // Ergonomic scalar offset: 1 right of ref.east. + wire.wire("u2", ("eA-port-o2", "eB-port-i2"), + style: "u-shape", color: util.colors.blue, + u-at: (rel: 1, to: "ref.east")) + ```, + vertical: true, +) + #let stub = example(``` draw.circle((0, 0), radius: .1, name: "p") wire.stub("p", "north", name: "north", length: 1) diff --git a/gallery/platypus.png b/gallery/platypus.png index 7e0a912..cd954d8 100644 Binary files a/gallery/platypus.png and b/gallery/platypus.png differ diff --git a/gallery/test.png b/gallery/test.png index c4e9480..2c5d0c6 100644 Binary files a/gallery/test.png and b/gallery/test.png differ diff --git a/gallery/test2.png b/gallery/test2.png index c4e9480..2c5d0c6 100644 Binary files a/gallery/test2.png and b/gallery/test2.png differ diff --git a/gallery/test3.png b/gallery/test3.png index 8af0ba5..ac6032a 100644 Binary files a/gallery/test3.png and b/gallery/test3.png differ diff --git a/gallery/test4.png b/gallery/test4.png index 4f36ffc..57ae67b 100644 Binary files a/gallery/test4.png and b/gallery/test4.png differ diff --git a/gallery/test5.png b/gallery/test5.png index e23a31f..ecb2eec 100644 Binary files a/gallery/test5.png and b/gallery/test5.png differ diff --git a/gallery/test6.png b/gallery/test6.png index 409663e..eb67604 100644 Binary files a/gallery/test6.png and b/gallery/test6.png differ diff --git a/gallery/test7.png b/gallery/test7.png index cfe3ee4..99d3505 100644 Binary files a/gallery/test7.png and b/gallery/test7.png differ diff --git a/gallery/test_ushape.png b/gallery/test_ushape.png new file mode 100644 index 0000000..4f13e74 Binary files /dev/null and b/gallery/test_ushape.png differ diff --git a/gallery/test_ushape.typ b/gallery/test_ushape.typ new file mode 100644 index 0000000..0d4902e --- /dev/null +++ b/gallery/test_ushape.typ @@ -0,0 +1,53 @@ +// Demonstrates the u-shape wire style. +// The u-side is auto-detected from the port anchors when both endpoints +// belong to elements (deduced from the port position vs the element's +// center anchor). It can also be set explicitly with u-side. +#import "/src/lib.typ": circuit, element, util, wire + +#set page(width: auto, height: auto, margin: .5cm) + +#circuit({ + // ---- east <-> east (auto-detected) ---- + element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA", + fill: util.colors.orange, + ports: (east: ((id: "out"),))) + element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB", + fill: util.colors.orange, + ports: (east: ((id: "out"),))) + wire.wire("uEE", ("eA-port-out", "eB-port-out"), + style: "u-shape", u-depth: 0.6, + name: "E-E") + + // ---- west <-> west (auto-detected) ---- + element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA", + fill: util.colors.yellow, + ports: (west: ((id: "in"),))) + element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB", + fill: util.colors.yellow, + ports: (west: ((id: "in"),))) + wire.wire("uWW", ("wA-port-in", "wB-port-in"), + style: "u-shape", u-depth: 0.6, + name: "W-W") + + // ---- north <-> north (auto-detected) ---- + element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA", + fill: util.colors.green, + ports: (north: ((id: "out"),))) + element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB", + fill: util.colors.green, + ports: (north: ((id: "out"),))) + wire.wire("uNN", ("nA-port-out", "nB-port-out"), + style: "u-shape", u-depth: 0.6, + name: "N-N") + + // ---- south <-> south (explicit u-side override) ---- + element.block(x: 16, y: 0, w: 1.5, h: 1.5, id: "sA", + fill: util.colors.pink, + ports: (south: ((id: "out"),))) + element.block(x: 18.5, y: -0.5, w: 1.5, h: 1.5, id: "sB", + fill: util.colors.pink, + ports: (south: ((id: "out"),))) + wire.wire("uSS", ("sA-port-out", "sB-port-out"), + style: "u-shape", u-side: "south", u-depth: 0.6, + name: "S-S") +}) diff --git a/manual.pdf b/manual.pdf index f4e1052..72a7d77 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index ebe0c88..f1cfa11 100644 --- a/manual.typ +++ b/manual.typ @@ -1,4 +1,4 @@ -#import "@preview/tidy:0.4.1" +#import "@preview/tidy:0.4.3" #import "/src/cetz.typ": draw, canvas #import "src/lib.typ" #import "doc/examples.typ" diff --git a/src/cetz.typ b/src/cetz.typ index 980a619..adb796c 100644 --- a/src/cetz.typ +++ b/src/cetz.typ @@ -1 +1 @@ -#import "@preview/cetz:0.3.4": * \ No newline at end of file +#import "@preview/cetz:0.5.2": * \ No newline at end of file diff --git a/src/circuit.typ b/src/circuit.typ index f89389f..958fa6e 100644 --- a/src/circuit.typ +++ b/src/circuit.typ @@ -1,5 +1,5 @@ #import "/src/cetz.typ": canvas -#import "@preview/tidy:0.3.0" +#import "@preview/tidy:0.4.3" /// Draws a block circuit diagram /// diff --git a/src/util.typ b/src/util.typ index a444434..56af6a6 100644 --- a/src/util.typ +++ b/src/util.typ @@ -73,4 +73,45 @@ #let valid-anchors = ( "center", "north", "east", "west", "south", "north-east", "north-west", "south-east", "south-west" -) \ No newline at end of file +) + +/// Normalizes a coordinate so that 2D points (x, y) are padded +/// to (x, y, 0) as required by CeTZ's coordinate resolver. +/// Also handles function-based and dictionary-based coordinates +/// recursively. +/// +/// - coord (any): A CeTZ-compatible coordinate +/// -> any +#let normalize-coord(coord) = { + if type(coord) == array { + if coord.len() == 2 { + let (a, b) = coord + // Raw 2D point: (x, y) -> (x, y, 0) + if type(a) in (int, float) and type(b) in (int, float) { + return (a, b, 0) + } + // Function-based coordinate: (fn, point) + if type(a) == function { + let orig = a + return (v => { + let result = orig(v) + if type(result) == array and result.len() == 2 { + let (rx, ry) = result + if type(rx) in (int, float) and type(ry) in (int, float) { + return (rx, ry, 0) + } + } + return result + }, b) + } + } + } + if type(coord) == dictionary { + let result = (:) + for (key, value) in coord { + result.insert(key, normalize-coord(value)) + } + return result + } + return coord +} \ No newline at end of file diff --git a/src/wire.typ b/src/wire.typ index cbf7c4b..b22e5a1 100644 --- a/src/wire.typ +++ b/src/wire.typ @@ -1,9 +1,16 @@ #import "/src/cetz.typ": draw, coordinate -#import "util.typ": opposite-anchor +#import "util.typ": opposite-anchor, normalize-coord /// List of valid wire styles /// #examples.wires -#let wire-styles = ("direct", "zigzag", "dodge", "guided") +/// +/// The `"u-shape"` style connects two ports that face the same cardinal side. +/// #examples.u-shape-wire +/// +/// The `"u-shape"` crossbar can also be aligned to an anchor via `u-at` +/// (analogous to `zigzag-at`). +/// #examples.u-at +#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape") #let signal-width = 1pt #let bus-width = 1.5pt @@ -24,29 +31,95 @@ return (pts, anchors) } -#let get-zigzag-wire(pts, ratio, dir) = { +// 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: , to: )` 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 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, @@ -68,8 +141,8 @@ let (ctx, p0) = coordinate.resolve(ctx, start) let (ctx, p3) = coordinate.resolve(ctx, end) - p0 = (x: p0.first(), y: p0.last()) - p3 = (x: p3.first(), y: p3.last()) + p0 = (x: p0.first(), y: p0.at(1)) + p3 = (x: p3.first(), y: p3.at(1)) let dx1 = margin-start let dx2 = margin-end @@ -86,13 +159,13 @@ if side-end == "east" { dx2 *= -1 } - p1 = (p0.x + dx1, p0.y) - p2 = (p3.x - dx2, p0.y) + p1 = (p0.x + dx1, p0.y, 0) + p2 = (p3.x - dx2, p0.y, 0) let points = ( start, (horizontal: p1, vertical: ()), - (horizontal: (), vertical: (0, dodge-y)), + (horizontal: (), vertical: (0, dodge-y, 0)), (horizontal: p2, vertical: ()), (horizontal: (), vertical: end), end @@ -109,6 +182,111 @@ 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 detect-port-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 = detect-port-side(start, ctx) + let side-end = detect-port-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) +} + #let get-guided-wire(pts, margins, sides, center-guides, ctx) = { let start = pts.first() let end = pts.last() @@ -147,18 +325,18 @@ // points that are closest to the edge points - let p1 = (p0.x + dx1, p0.y + dy1) - let p5 = (p6.x + dx2, p6.y + dy2) + let p1 = (p0.x + dx1, p0.y + dy1, 0) + let p5 = (p6.x + dx2, p6.y + dy2, 0) // middle point let center_x = p0.x + box_width * center_horizontal / 100% let center_y = p0.y + box_height * center_vertical / 100% - let p3 = (center_x, center_y) + let p3 = (center_x, center_y, 0) // setting up the points for that touch the guides - let p2 = (0,0) - let p4 = (0,0) + let p2 = (0, 0, 0) + let p4 = (0, 0, 0) if side-start in ("north", "south") { p2 = (horizontal: p3, vertical: p1) } else { @@ -204,14 +382,18 @@ /// - 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-dir (str): The zigzag's direction. As either "vertical" or "horizontal" (only with dstyle "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 (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 `-port-` 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"`. If the direction cannot be deduced (no port anchor on either endpoint, or conflicting directions), it falls back to `"vertical"`. +/// - 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: , to: )`, 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 `-port-` 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: , to: )`, which is rewritten to `(dx, 0)` for east/west and `(0, dy)` for north/south. #let wire( id, pts, bus: false, @@ -227,11 +409,15 @@ guided-sides: ("east", "west"), directed: false, rotate-name: true, - zigzag-ratio: 50%, - zigzag-dir: "vertical", + zigzag-ratio: auto, + zigzag-dir: auto, + zigzag-at: none, dodge-y: 0, dodge-sides: ("east", "west"), - dodge-margins: (5%, 5%) + dodge-margins: (5%, 5%), + u-side: auto, + u-depth: auto, + u-at: none, ) = draw.get-ctx(ctx => { if not style in wire-styles { panic("Invalid wire style '" + style + "'") @@ -240,6 +426,35 @@ 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 } + + // Normalize user-provided coordinates: (x, y) -> (x, y, 0) + let pts = pts.map(normalize-coord) let stroke = ( paint: color, @@ -256,7 +471,26 @@ (points, anchors) = get-direct-wire(pts) } else if style == "zigzag" { - (points, anchors) = get-zigzag-wire(pts, zigzag-ratio, zigzag-dir) + let zz-dir = zigzag-dir + if zz-dir == auto { + let side-start = detect-port-side(pts.first(), ctx) + let side-end = detect-port-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) + // Fall back to "vertical" (the previous default) when the direction + // cannot be deduced (no port anchors, or conflicting endpoints). + if d-start != none and d-end != none and d-start != d-end { zz-dir = "vertical" } + else if d-start != none { zz-dir = d-start } + else if d-end != none { zz-dir = d-end } + else { zz-dir = "vertical" } + } + (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( @@ -274,9 +508,17 @@ guided-center, ctx ) - } + } else if style == "u-shape" { + (points, anchors) = get-u-shape-wire( + pts, + u-depth-effective, + u-side, + u-at, + ctx + ) + } - let mark = (fill: color) + let mark = (fill: color) if directed { mark = (end: ">", fill: color) } diff --git a/typst.toml b/typst.toml index b83700a..fb35880 100644 --- a/typst.toml +++ b/typst.toml @@ -1,7 +1,7 @@ [package] name = "circuiteria" version = "0.2.1" -compiler = "0.13.1" +compiler = "0.15.0" repository = "https://git.kb28.ch/HEL/circuiteria" entrypoint = "src/lib.typ" authors = [