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 57 additions and 268 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: *
...
-39
View File
@@ -186,45 +186,6 @@ wire.wire("w2", ("b2-port-out", "b3-port-in2"),
style: "zigzag")
```)
#let group-ports = example(```
// outside-port-names: true (default) — labels outside the border
element.group(
id: "g1", name: "outside (default)",
stroke: (dash: "dashed"),
fill: util.colors.blue.lighten(60%),
padding: 2em,
outside-port-names: true,
ports: (
west: ((id: "in", name: "D"),),
east: ((id: "out", name: "Q"),),
),
{ element.block(id: "b1", w: 2, h: 1.5, x: 0, y: 0,
fill: util.colors.green,
ports: (west: ((id: "d", name: "d"),),
east: ((id: "q", name: "q"),))) }
)
wire.wire("wG1in", ("g1-port-in", "b1-port-d"), directed: true)
wire.wire("wG1out", ("b1-port-q", "g1-port-out"), directed: true)
// outside-port-names: false — labels inside the border (block style)
element.group(
id: "g2", name: "inside",
stroke: (dash: "dashed"),
fill: util.colors.orange.lighten(60%),
padding: 2em,
outside-port-names: false,
ports: (
west: ((id: "in", name: "D"),),
east: ((id: "out", name: "Q"),),
),
{ element.block(id: "b2", w: 2, h: 1.5, x: 6, y: 0,
fill: util.colors.green,
ports: (west: ((id: "d"),), east: ((id: "q"),))) }
)
wire.stub("g2-port-in", "west")
wire.stub("g2-port-out", "east")
```, vertical: true)
#let intersection = example(```
wire.wire("w1", ((0, 0), (1, 1)), style: "zigzag")
wire.wire("w2", ((0, 0), (1, -.5)),
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: 23 KiB

-84
View File
@@ -1,84 +0,0 @@
// Demonstrates ports on a group element.
// outside-port-names: true (default) — labels outside the group border
// outside-port-names: false — labels inside (block-style)
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// --- Group with port labels OUTSIDE (default) ---
element.group(
id: "cpu",
name: "CPU (labels outside)",
name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.yellow.lighten(60%),
padding: 1em,
outside-port-names: true,
ports: (
west: (
(id: "in1", name: "D1"),
(id: "in2", name: "D2"),
),
east: ((id: "out", name: "Q"),),
south: (
(id: "rst", name: "RST", vertical: true),
(id: "done", name: "DONE", vertical: true),
),
),
{
element.block(
id: "reg2",
w: 2.5,
h: 3,
x: 0,
y: 0,
fill: util.colors.green,
ports: (
west: ((id: "d1"), (id: "d2")),
east: ((id: "q"),),
north: ((id: "clk", clock: true),),
),
)
},
)
// --- Group with port labels INSIDE (outside-port-names: false) ---
element.group(
id: "cpu2",
name: "CPU (labels inside)",
name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.yellow.lighten(60%),
padding: 4em,
outside-port-names: false,
ports: (
west: (
(id: "in1", name: "D1"),
(id: "in2", name: "D2"),
),
east: ((id: "out", name: "Q"),),
south: (
(id: "rst", name: "RST", vertical: true),
(id: "done", name: "DONE", vertical: true),
),
),
{
element.block(
id: "reg2",
w: 2.5,
h: 3,
x: 10,
y: 0,
fill: util.colors.green,
ports: (
west: ((id: "d1"), (id: "d2")),
east: ((id: "q"),),
north: ((id: "clk", clock: true),),
),
)
},
)
wire.stub("cpu2-port-in1", "west", name: "A")
wire.stub("cpu2-port-in2", "west", name: "B")
wire.stub("cpu2-port-out", "east", name: "Q")
})
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)
+3 -32
View File
@@ -1,15 +1,9 @@
#import "/src/cetz.typ": draw, coordinate
#import "../util.typ"
#import "ports.typ": add-ports
/// Draws a group of elements
///
/// #examples.group
///
/// Ports can also be defined on a group, just like on a block. By default
/// port labels are drawn *outside* the group border so they do not overlap
/// child elements. Set `outside-port-names: false` to draw them inside.
/// #examples.group-ports
/// - body (elements, function): Elements to group
/// - id (str): see #doc-ref("element.elmt")
/// - name (str): The group's name
@@ -22,10 +16,6 @@
/// - array: either (`<all>`,), (`<vertical>`, `<horizontal>`) or (`<top>`, `<right>`, `<bottom>`, `<left>`)
/// - dictionary: valid keys are "top", "right", "bottom" and "left"
/// - radius (number): The corner radius
/// - ports (dictionary): see #doc-ref("element.elmt")
/// - ports-margins (dictionary): see #doc-ref("element.elmt")
/// - outside-port-names (bool): If `true` (default), port name labels are drawn outside the group border so they do not overlap child elements. Set to `false` to draw labels inside, matching the block behaviour
/// - debug (dictionary): see #doc-ref("element.elmt")
#let group(
body,
id: "",
@@ -34,13 +24,7 @@
fill: none,
stroke: black + 1pt,
padding: 0.5em,
radius: 0.5em,
ports: (:),
ports-margins: (:),
outside-port-names: true,
debug: (
ports: false
)
radius: 0.5em
) = {
let min-x = none
let max-x = none
@@ -77,26 +61,13 @@
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,
fill: fill
)
add-ports(
id,
id + ".north-west",
id + ".north-east",
id + ".south-east",
id + ".south-west",
ports,
ports-margins,
debug: debug.ports,
outside-name: outside-port-names,
)
if name != none {
draw.content(
id + "." + name-anchor,
+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",
+9 -24
View File
@@ -1,12 +1,11 @@
#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor, opposite-anchor
#import "../util.typ": rotate-anchor
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false,
outside-name: false,
debug: false
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
@@ -18,31 +17,19 @@
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)
// TODO: use context or vectors to have the height relative to the width
draw.line(prev, pos1, next)
}
// When outside-name is true (e.g. for groups) the label is placed on the
// exterior of the element border so it does not overlap child elements.
// Horizontal labels: anchor = opposite of the side (e.g. east port → west-anchored text, outside).
// Vertical labels: same opposite side but rotated 90°, keeping text readable.
let name-anchor = if outside-name {
if name-rotate { rotate-anchor(opposite-anchor(side)) }
else { opposite-anchor(side) }
} else {
if name-rotate { rotate-anchor(side) } else { side }
}
draw.content(
pos,
anchor: name-anchor,
anchor: if name-rotate {rotate-anchor(side)} else {side},
padding: 2pt,
angle: if name-rotate {90deg} else {0deg},
name
@@ -73,8 +60,7 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: false,
outside-name: false,
debug: false
) = {
let sides = (
"north": (tl, tr),
@@ -123,8 +109,7 @@
pos,
prev: pos-prev,
next: pos-next,
debug: debug,
outside-name: outside-name,
debug: debug
)
}
}
+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
}
)
+11 -14
View File
@@ -1,5 +1,5 @@
#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
@@ -157,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 {
@@ -240,9 +240,6 @@
if pts.len() != 2 {
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
}
// Normalize user-provided coordinates: (x, y) -> (x, y, 0)
let pts = pts.map(normalize-coord)
let stroke = (
paint: color,
@@ -368,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" ]