Compare commits

...
14 Commits
Author SHA1 Message Date
David Stadelmann 19a48951df feat: add s-shape wire style
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.

Supports anchor-based placement via the `s-at-*` family of parameters
(s-at-1, s-at-mid, s-at-2) analogous to `zigzag-at` and `u-at`.
2026-10-22 00:32:08 +02:00
David Stadelmann 1f247c2efc feat: add u-shape wire-style
Add the "u-shape" wire style, which connects two ports facing the same
cardinal side with a U-shaped bend. `u-side` selects the side and is
auto-detected from the port anchors with `detect-port-side` when set to
`auto`. `u-depth` sets the distance of the bend from the outermost
endpoint.

Add `u-at` for anchor-based placement of the crossbar, analogous to
`zigzag-at`. It overrides `u-depth`, which defaults to `auto` so the two
are mutually exclusive.

Add the `u-shape-wire` and `u-at` examples, extend the `wires` example
and add the `gallery/test_ushape` gallery entry.
2026-10-22 00:32:04 +02:00
David Stadelmann beda61b9f4 feat: auto-detect zigzag-dir from port anchors
When `zigzag-dir` is `auto` (the new default), deduce it from the
endpoint port anchors using `detect-port-side`. East/west ports yield
"vertical" and north/south ports yield "horizontal". If the direction
cannot be deduced (no port anchor on either endpoint, or conflicting
directions) it falls back to "vertical", the previous default.
2026-10-22 00:32:01 +02:00
David Stadelmann 2c6e5f44df feat: add anchor-based zigzag bend placement via zigzag-at
Add the `zigzag-at` parameter to place the middle bend of a zigzag wire
on the axis of a referenced anchor. It overrides `zigzag-ratio`, which
now defaults to `auto` (resolving to 50%) so the two are mutually
exclusive. Accepted forms are an anchor name, any CeTZ coordinate, or
the scalar offset `(rel: <length>, to: <coord>)`.

Add the generic helpers this relies on and that other wire styles reuse:
- `normalize-scalar-offset(at, axis)` turns a scalar offset into an x or
  y offset; `normalize-zigzag-at` is a wrapper around it.
- `detect-port-side(anchor, ctx)` deduces the cardinal side a port faces
  by comparing it with its element's `.center` anchor.

Add the `zigzag-at` example.
2026-10-22 00:31:52 +02:00
David Stadelmann df4c956a0b chore: update minimum compiler version to 0.15.0 2026-10-21 23:39:59 +02:00
David Stadelmann ac087087c5 deps: update tidy to 0.4.3 2026-10-21 23:39:59 +02:00
David Stadelmann 77eb4fa946 deps: update CeTZ 0.3.4 to 0.5.2 with coordinate fixes
CeTZ 0.5.2 requires 3D coordinate vectors (x, y, z) for raw
coordinate tuples passed to coordinate.resolve(). Update internal
wire computations (dodge, guided) to use 3-element tuples, fix
.last() to .at(1) for y-component access on resolved coordinates,
and add a normalize-coord utility that automatically pads user-
provided 2D points (x, y) to (x, y, 0) so users do not need to
specify the z dimension.
2026-10-21 23:39:59 +02:00
HEL c9aa81af2e Merge pull request 'Release v0.2.1' (#25) from dev into main
Reviewed-on: HEL/circuiteria#25
2026-07-03 23:37:38 +00:00
HEL 6871053810 Merge pull request 'Release v0.2.1' (#24) from release/0.2.1 into dev
Reviewed-on: HEL/circuiteria#24
2026-07-03 23:36:46 +00:00
HEL 73b7348eed chore: remove duplicate alt text 2026-07-04 01:35:35 +02:00
HEL b71e822bb8 chore: use preview import paths in examples 2026-06-30 21:40:09 +02:00
HEL 4c1025b1eb chore: remove "Typst" from description 2026-06-30 21:31:55 +02:00
HEL b35c04a7d9 chore: add alt text on README images 2026-06-30 21:29:01 +02:00
HEL 0612e9b6ec chore: bump to 0.2.1 2026-06-30 21:13:41 +02:00
28 changed files with 868 additions and 64 deletions

No files matched your search

+7 -7
View File
@@ -3,7 +3,7 @@
Circuiteria is a [Typst](https://typst.app) package for drawing block circuit diagrams using the [CeTZ](https://typst.app/universe/package/cetz) package.
<p align="center">
<img src="./gallery/platypus.png" alt="Perry the platypus">
<img src="./gallery/platypus.png" alt="A platypus / Perry the Platypus meme made with colored rounded rectangles">
</p>
## Examples
@@ -11,17 +11,17 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
<tr>
<td colspan="2">
<a href="./gallery/test.typ">
<img src="./gallery/test.png" width="500px">
<img src="./gallery/test.png" width="500px" alt="Block diagram of a multi-cycle RISC-V FPGA processor">
</a>
</td>
</tr>
<tr>
<td colspan="2">A bit of eveything</td>
<td colspan="2">A bit of everything</td>
</tr>
<tr>
<td colspan="2">
<a href="./gallery/test5.typ">
<img src="./gallery/test5.png" width="500px">
<img src="./gallery/test5.png" width="500px" alt="Block diagram of a single-cycle RISC-V processor">
</a>
</td>
</tr>
@@ -31,12 +31,12 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
<tr>
<td>
<a href="./gallery/test4.typ">
<img src="./gallery/test4.png" width="250px">
<img src="./gallery/test4.png" width="250px" alt="Top-level block diagram of a single-cycle RISC-V processor">
</a>
</td>
<td>
<a href="./gallery/test6.typ">
<img src="./gallery/test6.png" width="250px">
<img src="./gallery/test6.png" width="250px" alt="Block diagram of a simple single-cycle RISC-V ALU">
</a>
</td>
</tr>
@@ -56,7 +56,7 @@ For more information, see the [manual](manual.pdf)
To use this package, simply import [circuiteria](https://typst.app/universe/package/circuiteria) and call the `circuit` function:
```typ
#import "@preview/circuiteria:0.2.0"
#import "@preview/circuiteria:0.2.1"
#circuiteria.circuit({
import circuiteria: *
...
+206 -2
View File
@@ -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,213 @@ 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 s-shape-wire = example(
```
// `s-shape` connects endpoints on opposite sides
// via 5 segments joined at 4 corners (u1-start,
// u1-end, u2-start, u2-end). Auto-detects axis
// from port anchors.
element.block(x: 0, y: 0, w: 1, h: 1, id: "A",
fill: util.colors.orange,
ports: (west: ((id: "out"),)))
element.block(x: 2.5, y: -1.75, w: 1, h: 1, id: "B",
fill: util.colors.orange,
ports: (east: ((id: "in"),)))
wire.wire("s1", ("A-port-out", "B-port-in"),
style: "s-shape")
// Anchor-based override of the crossbar position.
element.block(x: 5.5, y: 0, w: 1, h: 1, id: "C",
fill: util.colors.yellow,
ports: (west: ((id: "out"),)))
element.block(x: 8.5, y: -1.75, w: 1, h: 1, id: "D",
fill: util.colors.yellow,
ports: (east: ((id: "in"),)))
element.block(x: 7, y: -0.7, w: 0.5, h: 0.5, id: "ref",
fill: util.colors.pink)
wire.wire("s2", ("C-port-out", "D-port-in"),
style: "s-shape",
s-at-mid: "ref.south")
// C-shape degeneration: crossbar outside the
// endpoint span (s-ratio-mid > 100%).
element.block(x: 11, y: 0, w: 1, h: 1, id: "E",
fill: util.colors.green,
ports: (west: ((id: "out"),)))
element.block(x: 14, y: -0.75, w: 1, h: 1, id: "F",
fill: util.colors.green,
ports: (east: ((id: "in"),)))
wire.wire("s3", ("E-port-out", "F-port-in"),
style: "s-shape",
s-ratio-mid: 200%)
```,
vertical: true,
)
#let s-shape-wire-vertical = example(
```
// `s-shape` on the vertical axis (north <-> south):
// seg1 and seg5 are vertical, the crossbar is
// vertical, and the two horizontals connect them.
element.block(x: 0, y: 0, w: 1, h: 1, id: "A",
fill: util.colors.orange,
ports: (north: ((id: "out"),)))
element.block(x: 2, y: -3, w: 1, h: 1, id: "B",
fill: util.colors.orange,
ports: (south: ((id: "in"),)))
wire.wire("s1", ("A-port-out", "B-port-in"),
style: "s-shape")
// Anchor-based override of the vertical crossbar.
element.block(x: 5, y: 0, w: 1, h: 1, id: "C",
fill: util.colors.yellow,
ports: (north: ((id: "out"),)))
element.block(x: 7, y: -3, w: 1, h: 1, id: "D",
fill: util.colors.yellow,
ports: (south: ((id: "in"),)))
element.block(x: 6.25, y: -1.5, w: 0.5, h: 0.5, id: "vref",
fill: util.colors.pink)
wire.wire("s2", ("C-port-out", "D-port-in"),
style: "s-shape",
s-at-mid: "vref.east")
```,
vertical: true,
)
#let s-shape-wire-loopback = example(
```
// `s-shape` looping back on a single block that
// has a west output and an east input. The wire
// exits west, wraps around below the block via a
// C-shape, and enters from the east.
//
// Since both endpoints share the same y, the
// crossbar's position is set by `s-ratio-mid`
// as an absolute distance below the endpoint y.
element.block(x: 2, y: 0, w: 1.5, h: 1.5, id: "A",
fill: util.colors.orange,
ports: (
west: ((id: "out"),),
east: ((id: "in"),),
))
wire.wire("s1", ("A-port-out", "A-port-in"),
style: "s-shape",
s-depth-1: 1.5, s-depth-2: 1.5,
s-ratio-mid: 150%)
// Negative `s-ratio-mid` routes the crossbar the
// other way (above the block).
element.block(x: 7, y: 0, w: 1.5, h: 1.5, id: "B",
fill: util.colors.yellow,
ports: (
west: ((id: "out"),),
east: ((id: "in"),),
))
wire.wire("s2", ("B-port-out", "B-port-in"),
style: "s-shape",
s-depth-1: 1.5, s-depth-2: 1.5,
s-ratio-mid: -150%)
```,
vertical: true,
)
#let stub = example(```
draw.circle((0, 0), radius: .1, name: "p")
wire.stub("p", "north", name: "north", length: 1)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
#import "../src/lib.typ": *
#import "@preview/circuiteria:0.2.1": *
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 53 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
#import "../src/lib.typ": circuit, element, util, wire
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 53 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
#import "../src/lib.typ": circuit, element, util, wire
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
#import "../src/lib.typ": circuit, element, util, wire
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 67 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw
#import "../src/lib.typ": *
#import "@preview/circuiteria:0.2.1": *
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 93 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw
#import "../src/lib.typ": *
#import "@preview/circuiteria:0.2.1": *
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw
#import "../src/lib.typ": *
#import "@preview/circuiteria:0.2.1": *
#set page(width: auto, height: auto, margin: .5cm)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
#import "../src/lib.typ": circuit, element, util, wire
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

+53
View File
@@ -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")
})
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -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"
@@ -105,7 +105,7 @@ This package provides a way to make beautiful block circuit diagrams using the C
Simply import Circuiteria and call the `circuit` function:
#pad(left: 1em)[```typ
#import "@preview/circuiteria:0.2.0"
#import "@preview/circuiteria:0.2.1"
#circuiteria.circuit({
import circuiteria: *
...
+1 -1
View File
@@ -1 +1 @@
#import "@preview/cetz:0.3.4": *
#import "@preview/cetz:0.5.2": *
+1 -1
View File
@@ -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
///
+1 -1
View File
@@ -1,4 +1,4 @@
#let version = version(0, 2, 0)
#let version = version(0, 2, 1)
#let name = "circuiteria"
#import "circuit.typ": circuit
+41
View File
@@ -74,3 +74,44 @@
"center", "north", "east", "west", "south",
"north-east", "north-west", "south-east", "south-west"
)
/// 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
}
+545 -39
View File
@@ -1,9 +1,27 @@
#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
///
/// 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
@@ -24,29 +42,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: <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 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 +152,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 +170,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 +193,315 @@
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)
}
// 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 = detect-port-side(start, ctx)
let side-end = detect-port-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()
@@ -147,18 +540,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 +597,26 @@
/// - 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 `<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"`. 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: <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,
@@ -227,11 +632,23 @@
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,
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 + "'")
@@ -241,6 +658,55 @@
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}
@@ -256,7 +722,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 +759,30 @@
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)
let mark = (fill: color)
if directed {
mark = (end: ">", fill: color)
}
+3 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "circuiteria"
version = "0.2.0"
compiler = "0.13.1"
version = "0.2.1"
compiler = "0.15.0"
repository = "https://git.kb28.ch/HEL/circuiteria"
entrypoint = "src/lib.typ"
authors = [
@@ -9,6 +9,6 @@ authors = [
]
categories = [ "visualization" ]
license = "Apache-2.0"
description = "Drawing block circuits with Typst made easy, using CeTZ"
description = "Drawing block circuits made easy, using CeTZ"
keywords = [ "circuit", "block", "draw" ]
exclude = [ "gallery", "justfile", "doc" ]