Compare commits

..
Author SHA1 Message Date
David Stadelmann 3076e44207 chore: update minimum compiler version to 0.15.0 2026-07-01 10:39:48 +02:00
David Stadelmann 473dca962f deps: update tidy to 0.4.3 2026-07-01 10:39:18 +02:00
David Stadelmann 9edc60516e 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
coordinate.resolve() and rel-coordinate specs. Update all 2-element
tuples to 3-element tuples and fix .last() to .at(1) for y-component
access on resolved 3D coordinates.
2026-07-01 10:38:39 +02:00
32 changed files with 82 additions and 512 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="A platypus / Perry the Platypus meme made with colored rounded rectangles">
<img src="./gallery/platypus.png" alt="Perry the platypus">
</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" alt="Block diagram of a multi-cycle RISC-V FPGA processor">
<img src="./gallery/test.png" width="500px">
</a>
</td>
</tr>
<tr>
<td colspan="2">A bit of everything</td>
<td colspan="2">A bit of eveything</td>
</tr>
<tr>
<td colspan="2">
<a href="./gallery/test5.typ">
<img src="./gallery/test5.png" width="500px" alt="Block diagram of a single-cycle RISC-V processor">
<img src="./gallery/test5.png" width="500px">
</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" alt="Top-level block diagram of a single-cycle RISC-V processor">
<img src="./gallery/test4.png" width="250px">
</a>
</td>
<td>
<a href="./gallery/test6.typ">
<img src="./gallery/test6.png" width="250px" alt="Block diagram of a simple single-cycle RISC-V ALU">
<img src="./gallery/test6.png" width="250px">
</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.1"
#import "@preview/circuiteria:0.2.0"
#circuiteria.circuit({
import circuiteria: *
...
+2 -96
View File
@@ -56,7 +56,7 @@ for i in range(3) {
```)
#let wires = example(```
for i in range(5) {
for i in range(4) {
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,103 +68,9 @@ 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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 45 KiB

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

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 160 KiB

+4 -4
View File
@@ -1,4 +1,4 @@
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#import "../src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
@@ -117,7 +117,7 @@
wire.wire(
"wExtender-ImmSrc", (
"Extender.north",
(18, -2)
(18, -2, 0)
),
style: "zigzag",
zigzag-ratio: 0%,
@@ -138,8 +138,8 @@
)
wire.wire(
"wBus", (
(v => (v.at(0), -3.5), mid),
(horizontal: (), vertical: (0, 3.5)),
(v => (v.at(0), -3.5, 0), mid),
(horizontal: (), vertical: (0, 3.5, 0)),
),
bus: true
)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 160 KiB

+4 -4
View File
@@ -1,4 +1,4 @@
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#import "../src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
@@ -124,7 +124,7 @@
wire.wire(
"wExtender-ImmSrc", (
"Extender.north",
(18, -2)
(18, -2, 0)
),
style: "zigzag",
zigzag-ratio: 0%,
@@ -145,8 +145,8 @@
)
wire.wire(
"wBus", (
(v => (v.at(0), -3.5), mid),
(horizontal: (), vertical: (0, 3.5)),
(v => (v.at(0), -3.5, 0), mid),
(horizontal: (), vertical: (0, 3.5, 0)),
),
bus: true
)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 66 KiB

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

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 195 KiB

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

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 274 KiB

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

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 76 KiB

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

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 40 KiB

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

Before

Width:  |  Height:  |  Size: 9.3 KiB

-53
View File
@@ -1,53 +0,0 @@
// 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.
+1 -1
View File
@@ -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.1"
#import "@preview/circuiteria:0.2.0"
#circuiteria.circuit({
import circuiteria: *
...
+1 -1
View File
@@ -76,7 +76,7 @@
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0, 0), to: to))
x = to-pos.at(0)
}
+1 -1
View File
@@ -91,7 +91,7 @@
debug: debug
)
let in-pos = (rel: (0, h * (h-ratio / 200%)), to: id+".south-west")
let in-pos = (rel: (0, h * (h-ratio / 200%), 0), to: id+".south-west")
let out-pos = (id+".south-east", out-pct, id+".north-east")
add-port(id, "west", ports.west.first(), in-pos)
add-port(id, "east", ports.east.first(), out-pos)
+2 -2
View File
@@ -61,8 +61,8 @@
draw.hide(draw.group(name: id+"-inner", body))
draw.rect(
(rel: (-pad-left, -pad-bottom), to: id+"-inner.south-west"),
(rel: (pad-right, pad-top), to: id+"-inner.north-east"),
(rel: (-pad-left, -pad-bottom, 0), to: id+"-inner.south-west"),
(rel: (pad-right, pad-top, 0), to: id+"-inner.north-east"),
name: id,
radius: radius,
stroke: stroke,
+3 -3
View File
@@ -51,7 +51,7 @@
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0, 0), to: to))
x = to-pos.at(0)
}
@@ -101,7 +101,7 @@
let port-pos = int-name + ".0"
if inverted == "all" or port-name in inverted {
draw.circle(port-pos, radius: inverted-radius, anchor: "east", stroke: stroke)
port-pos = (rel: (-2 * inverted-radius, 0), to: port-pos)
port-pos = (rel: (-2 * inverted-radius, 0, 0), to: port-pos)
}
add-port(
id, "west",
@@ -113,7 +113,7 @@
let out-pos = id + ".east"
if inverted == "all" or "out" in inverted {
draw.circle(out-pos, radius: inverted-radius, anchor: "west", stroke: stroke)
out-pos = (rel: (2 * inverted-radius, 0), to: out-pos)
out-pos = (rel: (2 * inverted-radius, 0, 0), to: out-pos)
}
add-port(
id, "east",
+3 -3
View File
@@ -62,7 +62,7 @@
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0, 0), to: to))
x = to-pos.at(0)
}
@@ -103,7 +103,7 @@
anchor: "east",
stroke: stroke
)
port-pos = (rel: (-2 * inverted-radius, 0), to: port-pos)
port-pos = (rel: (-2 * inverted-radius, 0, 0), to: port-pos)
}
add-port(
id, "west",
@@ -115,7 +115,7 @@
let out-pos = id + ".east"
if inverted == "all" or "out" in inverted {
draw.circle(out-pos, radius: inverted-radius, anchor: "west", stroke: stroke)
out-pos = (rel: (2 * inverted-radius, 0), to: out-pos)
out-pos = (rel: (2 * inverted-radius, 0, 0), to: out-pos)
}
add-port(
id, "east",
+4 -4
View File
@@ -17,10 +17,10 @@
let size = if port.at("small", default: false) {8pt} else {1em}
let offset
if (side == "north") { offset = ( 0, -size) }
else if (side == "east") { offset = (-size, 0) }
else if (side == "south") { offset = ( 0, size) }
else if (side == "west") { offset = ( size, 0) }
if (side == "north") { offset = ( 0, -size, 0) }
else if (side == "east") { offset = (-size, 0, 0) }
else if (side == "south") { offset = ( 0, size, 0) }
else if (side == "west") { offset = ( size, 0, 0) }
let pos1 = (rel: offset, to: pos)
+1 -1
View File
@@ -1,4 +1,4 @@
#let version = version(0, 2, 1)
#let version = version(0, 2, 0)
#let name = "circuiteria"
#import "circuit.typ": circuit
+1 -42
View File
@@ -73,45 +73,4 @@
#let valid-anchors = (
"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
}
)
+40 -282
View File
@@ -1,16 +1,9 @@
#import "/src/cetz.typ": draw, coordinate
#import "util.typ": opposite-anchor, normalize-coord
#import "util.typ": opposite-anchor
/// List of valid wire styles
/// #examples.wires
///
/// The `"u-shape"` style connects two ports that face the same cardinal side.
/// #examples.u-shape-wire
///
/// The `"u-shape"` crossbar can also be aligned to an anchor via `u-at`
/// (analogous to `zigzag-at`).
/// #examples.u-at
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape")
#let wire-styles = ("direct", "zigzag", "dodge", "guided")
#let signal-width = 1pt
#let bus-width = 1.5pt
@@ -31,95 +24,29 @@
return (pts, anchors)
}
// Normalize an anchor-based placement value used by `zigzag-at`, `u-at`, and
// the `s-at-*` family.
//
// `axis` selects which cartesian component the offset applies to:
// - `"x"` → offset in x (used when the placement fixes an x coordinate, e.g.
// a vertical bend line for horizontal wire flow).
// - `"y"` → offset in y (used when the placement fixes a y coordinate).
//
// Accepted forms:
// - `none` → unset; caller falls back to
// depth/ratio-based placement.
// - `str` / tuple / native CeTZ dict → passed through as a CeTZ coordinate.
// - `(rel: <length>, to: <coord>)` with a *scalar* `rel`
// → ergonomic axial offset, rewritten
// to `(rel: (rel, 0), to: to)` for
// `axis == "x"` or
// `(rel: (0, rel), to: to)` for
// `axis == "y"`. If `rel` is already
// an array/tuple, the value is
// passed through unchanged.
#let normalize-scalar-offset(at, axis) = {
if at == none { return none }
if type(at) == dictionary and "rel" in at and "to" in at and type(at.rel) != array {
let r = at.rel
if axis == "x" {
return (rel: (r, 0), to: at.to)
} else {
return (rel: (0, r), to: at.to)
}
}
return at
}
// Backwards-compatible wrapper: translates the old `dir` vocabulary
// (`"vertical"` / `"horizontal"`) to the `axis` vocabulary used by
// `normalize-scalar-offset`.
#let normalize-zigzag-at(at, dir) = {
let axis = if dir == "vertical" { "x" } else { "y" }
return normalize-scalar-offset(at, axis)
}
#let get-zigzag-wire(pts, ratio, dir, at: none) = {
#let get-zigzag-wire(pts, ratio, dir) = {
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
)
}
let mid = if dir == "vertical" {
(start, ratio, (horizontal: end, vertical: ()))
} 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
)
}
(start, ratio, (horizontal: (), vertical: end))
}
let points = if dir == "vertical" {
(
start,
(horizontal: mid, vertical: ()),
(horizontal: (), vertical: end),
end
)
} else {
(
start,
(horizontal: (), vertical: mid),
(horizontal: end, vertical: ()),
end
)
}
let anchors = (
"start": start,
@@ -182,111 +109,6 @@
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()
@@ -335,8 +157,8 @@
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)
let p2 = (0,0,0)
let p4 = (0,0,0)
if side-start in ("north", "south") {
p2 = (horizontal: p3, vertical: p1)
} else {
@@ -382,18 +204,14 @@
/// - reverse (bool): If true, the start and end points will be swapped (useful in cases where the start point depends on the end point, for example with perpendiculars)
/// - directed (bool): If true, the wire will be directed, meaning an arrow will be drawn at the endpoint
/// - rotate-name (bool): If true, the name will be rotated according to the wire's slope
/// - zigzag-ratio (auto, ratio): Position of the zigzag middle bend segment relative to the horizontal (or vertical) span, as a fraction between the two endpoints (only with style "zigzag"). Defaults to `auto`, which resolves to `50%`. Panics if combined with an explicitly set `zigzag-at`.
/// - zigzag-dir (auto, str): The zigzag's direction, as either "vertical" or "horizontal" (only with style "zigzag"). If `auto`, it is deduced from the endpoints when they are port anchors of the form `<elmt-id>-port-<port-id>` by comparing each port's position with its element's `.center` anchor: east/west-facing ports yield `"vertical"` (wire exits horizontally) and north/south-facing ports yield `"horizontal"`. 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
/// - 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")
/// - 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.
#let wire(
id, pts,
bus: false,
@@ -409,15 +227,11 @@
guided-sides: ("east", "west"),
directed: false,
rotate-name: true,
zigzag-ratio: auto,
zigzag-dir: auto,
zigzag-at: none,
zigzag-ratio: 50%,
zigzag-dir: "vertical",
dodge-y: 0,
dodge-sides: ("east", "west"),
dodge-margins: (5%, 5%),
u-side: auto,
u-depth: auto,
u-at: none,
dodge-margins: (5%, 5%)
) = draw.get-ctx(ctx => {
if not style in wire-styles {
panic("Invalid wire style '" + style + "'")
@@ -426,35 +240,6 @@
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,
@@ -471,26 +256,7 @@
(points, anchors) = get-direct-wire(pts)
} else if style == "zigzag" {
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),
)
(points, anchors) = get-zigzag-wire(pts, zigzag-ratio, zigzag-dir)
} else if style == "dodge" {
(points, anchors) = get-dodge-wire(
@@ -508,17 +274,9 @@
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)
}
@@ -607,17 +365,17 @@
/// - 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)
north: (0, length, 0),
east: (length, 0, 0),
south: (0, -length, 0),
west: (-length, 0, 0)
).at(side)
let name-offset = (
north: (name-offset, length),
east: (length, name-offset),
south: (name-offset, -length),
west: (-length, name-offset)
north: (name-offset, length, 0),
east: (length, name-offset, 0),
south: (name-offset, -length, 0),
west: (-length, name-offset, 0)
).at(side)
draw.line(
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "circuiteria"
version = "0.2.1"
version = "0.2.0"
compiler = "0.15.0"
repository = "https://git.kb28.ch/HEL/circuiteria"
entrypoint = "src/lib.typ"
@@ -9,6 +9,6 @@ authors = [
]
categories = [ "visualization" ]
license = "Apache-2.0"
description = "Drawing block circuits made easy, using CeTZ"
description = "Drawing block circuits with Typst made easy, using CeTZ"
keywords = [ "circuit", "block", "draw" ]
exclude = [ "gallery", "justfile", "doc" ]