Compare commits

..
Author SHA1 Message Date
David Stadelmann fff5b2ecd5 feat: Support cross-element port alignment via align-with
Extend the port `align-with` field to accept references to ports of
other elements, in addition to the existing local-port form:

  - "<local-id>"                    → same-element port (unchanged)
  - (element: "<id>", port: "<id>") → cross-element reference
  - "<raw-anchor>"                  → arbitrary CeTZ anchor name

Cross-element alignment is implemented via CeTZ perpendicular
projection (`|-`) so the port sits on this element's side line while
sharing the remote port's y (east/west) or x (north/south) coordinate.
Local resolution still validates matching axis and detects cycles;
external refs skip axis validation (remote side unknown at eval time)
and require the target element to be drawn first.

Also enrich port-related panic messages with the element id and port
context to make offending figures easier to locate, and add a manual
example (`examples.align-with-ports`) demonstrating both local and
cross-element alignment.
2026-09-14 17:12:57 +02:00
David Stadelmann f7cb457202 fix: Make ports always appear on the highest layer 2026-09-14 15:18:23 +02:00
David Stadelmann fd761487ff feat: Add parameter shell to block (alternative to group)
Defining a block and placing elements into that block has the benefit
that you can use the ports to align the elements within the block to the
ports of that outer ("shell") block
2026-09-14 11:45:55 +02:00
David Stadelmann b8a8d38ed2 feat: Allow port names to be placed outside of a block 2026-09-14 11:42:37 +02:00
David Stadelmann 6070ce7fe3 feat: Add wire.breakout element for bus fan-out diagrams
Introduces wire.breakout: a 45° diagonal line of configurable length
(default 1em) that starts at a given (x, y) and exposes a port anchor
<id>-port-<id> at its end point so wires can attach to it as if it were
a block port. A synthesized <id>.center anchor is placed opposite the
configured port-side so wire.wire with style "u-shape" can auto-detect
the port's cardinal side.
- x/y accept number, (rel:, to:) and (from:) forms; (from:) sets the
  start point to the referenced anchor's coordinate.
- direction: one of north-east/north-west/south-east/south-west.
- Optional label positioned via name-pos (NE/NW/SE/SW); when name-pos is
  opposite to direction, the label falls back to the start point so it
  never covers the diagonal line. Label padding matches wire.wire (3pt).
Also adds three examples (breakout, breakout-label, breakout-wire) in
doc/examples.typ referenced from the docstring so tidy auto-renders
them in the manual, and a gallery entry (gallery/test_breakout.typ)
demonstrating orthogonal bus fan-out with breakouts in all four
directions.
2026-08-26 09:45:24 +02:00
David Stadelmann a51e8bb93b feature: Add port shape 2026-08-17 15:00:02 +02:00
David Stadelmann 563bb7b1d2 feat: Add port-name-padding parameter
Replaces the hard-coded 2pt padding used when placing port name labels.
Configurable per element via port-name-padding on elmt (forwarded by
block and group), defaulting to 2pt to preserve existing layout.
2026-08-17 14:13:06 +02:00
David Stadelmann 0d1478c765 feat: Add ports on group elements
Groups now accept ports/ports-margins like blocks. Port name labels
default to outside the group border (outside-port-names: true) so they
don't overlap child elements; set to false for block-style inside
labels.
2026-08-17 14:12:50 +02:00
David Stadelmann 002db0e6f5 feat: Add u-shape wire-style 2026-08-17 11:59:21 +02:00
David Stadelmann 0fb4929aca feat: Add relative y coordinates 2026-08-17 11:17:05 +02:00
David Stadelmann 23213fabca chore: update minimum compiler version to 0.15.0 2026-07-28 14:48:32 +02:00
David Stadelmann 320318a25b deps: update tidy to 0.4.3 2026-07-28 14:45:49 +02:00
David Stadelmann 9bcd91131c deps: update CeTZ 0.3.4 to 0.5.2 with coordinate fixes 2026-07-28 14:43:25 +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
HEL 42d6915b15 Merge pull request 'fix: handle y port alignment with gates' (#22) from fix/15-gate-y-port-alignment into dev
Reviewed-on: HEL/circuiteria#22
2026-06-30 19:08:48 +00:00
HEL b646d24930 fix: handle y port alignment with gates 2026-06-30 21:07:52 +02:00
HEL 1533b5ccd4 Merge pull request 'Update manual and gallery' (#21) from feat/update-manual-and-gallery into dev
Reviewed-on: HEL/circuiteria#21
2026-06-30 17:18:26 +00:00
HEL 2633b35f39 chore: remove gallery PDFs 2026-06-30 19:16:17 +02:00
HEL 415029e0c3 chore: update gallery examples 2026-06-30 19:15:26 +02:00
HEL 091ec8c08a chore: update manual 2026-06-30 19:13:19 +02:00
HEL ffd3356ab2 docs: add radius parameter for block 2026-06-30 19:02:08 +02:00
HEL 02e59c5d7a Merge pull request 'Update CeTZ' (#20) from feat/18-update-cetz into dev
Reviewed-on: HEL/circuiteria#20
2026-06-30 16:54:30 +00:00
HEL 96dd9b0543 chore: bump compiler version to 0.13.1 2026-06-30 18:52:01 +02:00
HEL da9c2b568b chore: update cetz to 0.3.4 2026-06-30 18:50:22 +02:00
HEL 459892bb13 refactor: centralize cetz import 2026-06-30 18:48:34 +02:00
HEL 255eec7f35 Merge pull request 'chore: expose package name' (#19) from feat/17-expose-name into dev
Reviewed-on: HEL/circuiteria#19
2026-06-30 16:34:28 +00:00
HEL 536f5a075d chore: expose package name 2026-06-30 18:31:51 +02:00
HEL d73215c889 Merge pull request 'feature: added guided wire style' (#14) from Shmerlard/circuiteria:dev into dev
Reviewed-on: HEL/circuiteria#14
Reviewed-by: Louis Heredero <louis@herdac.ch>
2025-08-24 11:01:02 +00:00
Shmerlard 87643983ec added guided wire 2025-08-17 22:05:36 +03:00
HEL 94fef0a245 Merge pull request 'Add radius parameter to block' (#12) from Shmerlard/circuiteria:main into dev
Reviewed-on: HEL/circuiteria#12
Reviewed-by: Louis Heredero <louis@herdac.ch>
2025-08-17 12:36:04 +00:00
Shmerlard 1a0a659ace no font is set in circuit 2025-08-17 15:31:09 +03:00
Shmerlard f2ce91ec39 small fix for radius 2025-08-17 14:33:31 +03:00
Shmerlard 043bb339fe font arguement, radius arguement
now in `block.typ` the radius is optional
and the font can be set in the canvas
2025-08-17 14:00:21 +03:00
HEL 693676d61a Merge pull request 'feature: add IEC gates' (#11) from bono/circuiteria:add_iec_gates into dev
Reviewed-on: HEL/circuiteria#11
Reviewed-by: Louis Heredero <louis@herdac.ch>
2025-06-29 13:19:56 +00:00
bono 94d5d6b854 add examples and add iec gates to manual 2025-06-28 13:56:22 -03:00
bono 71f128f6c9 fix typo in xnor gate 2025-06-28 13:55:14 -03:00
bono ea8277ee5b rename iec gates to have consistent naming 2025-06-28 12:28:28 -03:00
bono c5e4f8039a improve iec-gate 2025-06-28 12:10:00 -03:00
bono 8c91ccdd54 improve default-draw-shape by drawing a rectangle 2025-06-28 11:40:36 -03:00
bono 9966656e8b add examples 2025-06-22 14:49:11 -03:00
bono 3ccb79c6c2 add iec gates 2025-06-22 14:46:52 -03:00
60 changed files with 2507 additions and 967 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: *
...
+301 -2
View File
@@ -23,6 +23,31 @@ wire.stub("block-port-in2", "west")
wire.stub("block-port-out", "east")
```)
#let align-with-ports = example(```
// `align-with` reuses another port's coord
// (E/W: y, N/S: x). Forms:
// 1. "id" — local
// 2. (element:"e", port:"p") — cross-elmt
// 3. "raw-anchor" — CeTZ anchor
element.block(x: 0, y: 0, w: 2, h: 2, id: "b1",
ports: (
west: ((id: "a", name: "A", y: 30%),),
east: ((id: "oa", name: "A'",
"align-with": "a"),),
))
element.block(x: 4, y: -.3, w: 2, h: 2.6, id: "b2",
ports: (
west: ((id: "ia", name: "A",
"align-with":
(element: "b1", port: "oa")),),
east: ((id: "o", name: "C"),),
))
wire.stub("b1-port-a", "west")
wire.wire("w", ("b1-port-oa", "b2-port-ia"),
style: "direct")
wire.stub("b2-port-o", "east")
```, vertical: true)
#let extender = example(```
element.extender(
x: 0, y: 0, w: 3, h: 1,
@@ -56,7 +81,7 @@ for i in range(3) {
```)
#let wires = example(```
for i in range(3) {
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)))
@@ -65,6 +90,12 @@ wire.wire("w1", ("p0", "p1"), style: "direct")
wire.wire("w2", ("p2", "p3"), style: "zigzag")
wire.wire("w3", ("p4", "p5"), style: "dodge",
dodge-y: -0.5, dodge-margins: (0.5, 0.5))
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 stub = example(```
@@ -75,6 +106,81 @@ wire.stub("p", "south", name: "south", length: 15pt)
wire.stub("p", "west", name: "west", length: 3em)
```)
#let breakout = example(```
// 16 breakouts: 4 directions × 4 label positions, laid out
// two per row so the labels have room to breathe.
let dirs = ("north-east", "north-west", "south-east", "south-west")
let poss = ("north-east", "north-west", "south-east", "south-west")
let short = (
"north-east": "NE", "north-west": "NW",
"south-east": "SE", "south-west": "SW",
)
let cols = 2
let dx = 5
let dy = 2
for (i, dir) in dirs.enumerate() {
for (j, pos) in poss.enumerate() {
let n = i * poss.len() + j
let r = calc.floor(n / cols)
let c = calc.rem(n, cols)
draw.circle((c * dx, -r * dy), radius: .05)
wire.breakout("b" + str(n),
x: c * dx, y: -r * dy,
direction: dir,
port-side: if dir.ends-with("east") { "west" } else { "east" },
name: short.at(dir) + "/" + short.at(pos),
name-pos: pos)
}
}
```)
#let breakout-label = example(```
// name-pos matches direction → label sits at the end point.
wire.breakout("b1", x: 0, y: 0, direction: "north-east",
port-side: "east",
name: "at end", name-pos: "north-east")
// name-pos opposite to direction → label would cover the line, so
// it falls back to the start point.
wire.breakout("b2", x: 4, y: 0, direction: "north-east",
port-side: "east",
name: "at start", name-pos: "south-west")
```, vertical: true)
#let breakout-wire = example(```
// Bus fan-out: a horizontal wire from the left block to the lower
// right block carries the full 10-bit bus. A breakout in the middle
// of that wire taps bits [9:5] and routes them to the upper right
// block via an L-shaped zigzag (up, then right).
element.block(x: 0, y: 0, w: 2, h: 2, id: "src",
fill: util.colors.blue,
ports: (east: ((id: "out"),)))
element.block(x: 6, y: 0, w: 2, h: 2, id: "dstL",
fill: util.colors.green,
ports: (west: ((id: "in"),)))
element.block(x: 6, y: 4, w: 2, h: 2, id: "dstU",
fill: util.colors.orange,
ports: (west: ((id: "in"),)))
// Full bus wire, labelled "[9:0]" on the left.
wire.wire("wbus", ("src-port-out", "dstL-port-in"),
name: "[9:0]", name-pos: "start")
// Breakout tapped from the midpoint of the bus.
wire.breakout("tap",
x: (rel: 2, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-east",
port-side: "west",
name: "[9:5]", name-pos: "north-east")
// L-shaped wire from the breakout up and then right to the upper
// block's input.
wire.wire("wtap", ("tap-port-tap", "dstU-port-in"),
style: "zigzag", zigzag-dir: "horizontal",
zigzag-ratio: 100%)
```)
#let gate-and = example(```
gates.gate-and(x: 0, y: 0, w: 1.5, h: 1.5)
gates.gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
@@ -115,6 +221,46 @@ gates.gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5)
gates.gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-and = example(```
gates.iec-gate-and(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-and(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-nand = example(```
gates.iec-gate-nand(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-nand(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-or = example(```
gates.iec-gate-or(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-or(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-nor = example(```
gates.iec-gate-nor(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-nor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-xor = example(```
gates.iec-gate-xor(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-xor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-xnor = example(```
gates.iec-gate-xnor(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-xnor(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-buf = example(```
gates.iec-gate-buf(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-buf(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let iec-gate-not = example(```
gates.iec-gate-not(x: 0, y: 0, w: 1.5, h: 1.5)
gates.iec-gate-not(x: 3, y: 0, w: 1.5, h: 1.5, inverted: "all")
```, vertical: true)
#let group = example(```
element.group(
id: "g1", name: "Group 1", stroke: (dash: "dashed"),
@@ -142,9 +288,162 @@ 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,
port-name-padding: 4pt,
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)
// with extra padding via port-name-padding
element.group(
id: "g2", name: "inside",
stroke: (dash: "dashed"),
fill: util.colors.orange.lighten(60%),
padding: 2em,
outside-port-names: false,
port-name-padding: 0.4em,
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 port-names-outside = example(```
element.block(
x: 0, y: 0, w: 2, h: 2, id: "block",
port-names-outside: true,
port-name-padding: 4pt,
ports: (
north: ((id: "clk", name: "CLK", clock: true),),
west: ((id: "in1", name: "A"),
(id: "in2", name: "B")),
east: ((id: "out", name: "Y"),)
)
)
```)
#let port-shape = example(```
// Block with pentagon-shaped ports.
// - port-shape / port-kind / port-size are element-level defaults.
// - Any port field (shape, kind, size, stroke, fill) overrides per port.
// - kind: "in" → tip points into the block (default)
// kind: "out" → tip points away from the block
// - clock ports with shape: "pentagon" drop their triangle and default
// to a gray fill.
element.block(
id: "b", w: 3, h: 3, x: 0, y: 0,
fill: util.colors.yellow.lighten(60%),
port-shape: "pentagon",
port-size: 1em,
ports: (
// west: default input
west: ((id: "in", name: "D"),),
// east: output — tip points away
east: ((id: "out", name: "Q", kind: "out"),),
// north: clock port — pentagon replaces the triangle, gray fill
north: ((id: "clk", name: "CLK", clock: true),),
// south: styled output with per-port fill and stroke
south: ((id: "rdy", name: "RDY", kind: "out",
fill: util.colors.blue, stroke: red + 1.5pt),),
),
)
// Wires connect at the outer end of each pentagon.
wire.stub("b-port-in", "west")
wire.stub("b-port-out", "east")
wire.stub("b-port-clk", "north")
wire.stub("b-port-rdy", "south")
```)
#let shell = example(```
// A shell block acts as a container: port name labels are drawn
// outside by default, and wires attach at the *inside* tip of
// pentagon-shaped ports so child elements can sit within.
element.block(
id: "sh", w: 5, h: 3, x: 0, y: 0,
shell: true,
port-shape: "pentagon",
fill: util.colors.blue.lighten(80%),
ports: (
west: ((id: "in", name: "IN"),),
east: ((id: "out", name: "OUT", kind: "out"),),
),
{
element.block(id: "inner", w: 2, h: 1.5, x: 1.5, y: 0.75,
fill: util.colors.yellow,
ports: (west: ((id: "a"),), east: ((id: "b"),)))
}
)
wire.wire("w-in", ("sh-port-in", "inner-port-a"))
wire.wire("w-out", ("inner-port-b", "sh-port-out"))
```)
#let intersection = example(```
wire.wire("w1", ((0, 0), (1, 1)), style: "zigzag")
wire.wire("w2", ((0, 0), (1, -.5)),
style: "zigzag", zigzag-ratio: 80%)
wire.intersection("w1.zig")
```)
```)
#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)
Binary file not shown.
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)
Binary file not shown.
-286
View File
@@ -1,286 +0,0 @@
#import "../src/lib.typ": *
#import "/src/cetz.typ": draw
#set text(font: "Source Sans 3")
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
element.block(
size: (1.5, 2.2),
id: "PCBuf",
fill: util.colors.orange,
ports: (
west: "PCNext",
north: (id: "CLK", clock: true),
east: "PC",
south: (("EN", "EN"),)
)
)
wire.stub("PCBuf.CLK", name: "CLK")
wire.stub("PCBuf.EN", name: "PCWrite")
element.multiplexer(
pos: (
3, (align: "in0", with: "PCBuf.PC")
),
size: (1, 2),
id: "AdrSrc-MP",
fill: util.colors.orange,
entries: 2
)
wire.wire(
"PCBuf.PC",
"AdrSrc-MP.in0",
id: "wPCBuf-InstDataMgr",
name: "PC",
bus: true
)
wire.stub("AdrSrc-MP.north", side: "north", name: "AdrSrc")
element.block(
pos: (
6, (align: "A", with: "AdrSrc-MP.out")
),
size: (3, 4),
id: "InstDataMgr",
fill: util.colors.yellow,
ports: (
west: (
("A", "A"),
("WD", "WD")
),
north: (
(id: "CLK", clock: true),
(id: "WE", name: "WE", vertical: true),
(id: "IRWrite", name: "IRWrite", vertical: true)
),
east: (
("Instr", "Instr."),
("RD", "RD")
)
),
ports-margins: (
west: (30%, 0%),
east: (40%, 0%)
)
)
wire.wire(
"AdrSrc-MP.out",
"InstDataMgr.A",
id: "wAdrSrcMP-InstDataMgr",
name: (none, "Adr"),
bus: true
)
wire.stub("InstDataMgr.CLK", name: "CLK")
wire.stub("InstDataMgr.WE")
wire.stub("InstDataMgr.IRWrite")
wire.stub("InstDataMgr.WD")
element.block(
pos: (
15, (align: "WD3", with: "InstDataMgr.RD")
),
size: (3, 4),
id: "RegFile",
fill: util.colors.pink,
ports: (
west: (
("A1", "A1"),
("A2", "A2"),
("A3", "A3"),
("WD3", "WD3"),
),
north: (
(id: "CLK", clock: true),
(id: "WE3", name: "WE3", vertical: true)
),
east: (
("RD1", "RD1"),
("RD2", "RD2"),
)
),
ports-margins: (
east: (20%, 20%)
)
)
wire.stub("RegFile.CLK", name: "CLK")
wire.stub("RegFile.WE3", name: "Regwrite", name-offset: 0.6)
wire.stub("RegFile.A2")
wire.stub("RegFile.RD2")
element.extender(
pos: (15, -3.5),
size: (3, 1),
id: "Extender",
fill: util.colors.green
)
wire.wire(
"Extender.north",
(18, -2),
id: "wExtender-ImmSrc",
style: "zigzag",
zigzag-ratio: 0%,
name: (none, "ImmSrc"),
bus: true
)
let mid = ("InstDataMgr.east", 50%, "RegFile.west")
wire.wire(
"InstDataMgr.Instr",
(vertical: (), horizontal: mid),
id: "wInstDataMgr-Bus",
name: ("Instr", none),
bus: true
)
wire.wire(
(v => (v.at(0), -3.5), mid),
(horizontal: (), vertical: (0, 3.5)),
id: "wBus",
bus: true
)
wire.wire(
"RegFile.A1",
(horizontal: mid, vertical: ()),
id: "wBus-RegFile-A1",
name: (none, "RS1"),
slice: (19, 15),
reverse: true,
bus: true
)
wire.wire(
"RegFile.A3",
(horizontal: mid, vertical: ()),
id: "wBus-RegFile-A3",
name: (none, "RD"),
slice: (11, 7),
reverse: true,
bus: true
)
wire.wire(
"Extender.in",
(horizontal: mid, vertical: ()),
id: "wBus-Extender",
slice: (31, 7),
reverse: true,
bus: true
)
element.alu(
pos: (
22, (align: "in1", with: "RegFile.RD1")
),
size: (1, 2),
id: "ALU",
fill: util.colors.purple
)
wire.wire(
"RegFile.RD1",
"ALU.in1",
id: "wRegFile-ALU",
name: ("A", "SrcA"),
bus: true
)
wire.stub("ALU.north", side: "north")
element.block(
pos: (
26, (align: "in", with: "ALU.out")
),
size: (1.5, 2),
id: "OutBuf",
fill: util.colors.orange,
ports: (
west: "in",
north: (id: "CLK", clock: true),
east: "out"
)
)
wire.stub("OutBuf.CLK", name: "CLK")
wire.wire(
"ALU.out",
"OutBuf.in",
id: "wALU-OutBuf",
name: "ALUResult",
bus: true
)
element.multiplexer(
pos: (
30, (align: "in0", with: "OutBuf.out")
),
size: (1, 2.5),
id: "Res-MP",
fill: util.colors.orange,
entries: 3
)
wire.stub("Res-MP.north", side: "north", name: "ResultSrc")
wire.stub("Res-MP.in2")
wire.wire(
"OutBuf.out",
"Res-MP.in0",
id: "wOutBuf-ResMP",
name: "ALUOut",
bus: true
)
wire.wire(
"Extender.out",
"ALU.in2",
id: "wExt-ALU",
name: ("ImmExt", "SrcB"),
bus: true,
style: "zigzag",
zigzag-ratio: 60%
)
wire.wire(
"InstDataMgr.RD",
"Res-MP.in1",
id: "wInstDataMgr-ResMP",
style: "dodge",
dodge-y: -4,
dodge-sides: ("east", "west"),
name: ("Data", none),
bus: true
)
wire.wire(
"Res-MP.out",
"AdrSrc-MP.in1",
id: "wResMP-AdrSrc",
style: "dodge",
dodge-y: -5,
dodge-sides: ("east", "west"),
dodge-margins: (0.5, 1),
bus: true
)
wire.wire(
"Res-MP.out",
"RegFile.WD3",
id: "wResMP-RegFile",
style: "dodge",
dodge-y: -5,
dodge-sides: ("east", "west"),
dodge-margins: (0.5, 1),
bus: true
)
wire.wire(
"Res-MP.out",
"PCBuf.PCNext",
id: "wResMP-PCBuf",
style: "dodge",
dodge-y: -5,
dodge-sides: ("east", "west"),
dodge-margins: (0.5, 1.5),
name: (none, "PCNext"),
bus: true
)
wire.intersection("wResMP-RegFile.dodge-end", radius: .2)
wire.intersection("wResMP-AdrSrc.dodge-end", radius: .2)
})
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+1 -2
View File
@@ -1,5 +1,4 @@
#import "/src/cetz.typ": draw
#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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 23 KiB

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

After

Width:  |  Height:  |  Size: 5.8 KiB

+97
View File
@@ -0,0 +1,97 @@
#import "@preview/circuiteria:0.2.1": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
element.iec-gate-buf(
x: 0,
y: 0,
w: 2,
h: 2,
id: "iec-buf",
inputs: 1,
)
wire.stub("iec-buf-port-in0", "west")
element.iec-gate-not(
x: 3,
y: 0,
w: 2,
h: 2,
id: "iec-not",
inputs: 1,
)
wire.stub("iec-not-port-in0", "west")
element.iec-gate-and(
id: "iec-and",
x: 0,
y: -3,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-and-port-in" + str(i), "west")
}
element.iec-gate-nand(
id: "iec-nand",
x: 3,
y: -3,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-nand-port-in" + str(i), "west")
}
element.iec-gate-or(
id: "iec-or",
x: 0,
y: -6,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-or-port-in" + str(i), "west")
}
element.iec-gate-nor(
id: "iec-nor",
x: 3,
y: -6,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-nor-port-in" + str(i), "west")
}
element.iec-gate-xor(
id: "iec-xor",
x: 0,
y: -9,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-xor-port-in" + str(i), "west")
}
element.iec-gate-xnor(
id: "iec-nxor",
x: 3,
y: -9,
w: 2,
h: 2,
inputs: 2,
)
for i in range(2) {
wire.stub("iec-nxor-port-in" + str(i), "west")
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+117
View File
@@ -0,0 +1,117 @@
// Demonstrates the wire.breakout element used to split off slices of a
// bus that connects blocks. The main bus wires are always laid out
// orthogonally; breakouts diagonally tap intermediate points on those
// wires and route the sub-bus to another block via an L-shaped zigzag.
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// ---- Source block on the left, main sink on the right ----
element.block(x: 0, y: 0, w: 2, h: 2, id: "src", fill: util.colors.blue, ports: (
east: ((id: "out"),),
))
element.block(x: 15, y: 0, w: 2, h: 2, id: "dstMain", fill: util.colors.green, ports: (
west: ((id: "in"),),
))
// Full bus from src to dstMain, labelled [15:0] at the start.
wire.wire("wbus", ("src-port-out", "dstMain-port-in"), name: "[15:0]", name-pos: "start")
// ---- Upper-left sink: taps bits [11:8] via NW breakout ----
element.block(x: 0, y: 4, w: 2, h: 2, id: "dstNW", fill: util.colors.yellow, ports: (
east: ((id: "in"),),
))
wire.breakout(
"tapNE",
x: (rel: 3, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-east",
port-side: "west",
name: "[15:12]",
name-pos: "north-east",
)
// L-shape: up first, then left into dstNW.
wire.wire(
"wNW",
("tapNE-port-tapNE", "dstNW-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Upper-right sink: taps bits [15:12] via NE breakout ----
element.block(x: 15, y: 4, w: 2, h: 2, id: "dstNE", fill: util.colors.orange, ports: (
west: ((id: "in"),),
))
wire.breakout(
"tapNW",
x: (rel: 7, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "north-west",
port-side: "east",
name: "[11:8]",
name-pos: "north-east",
)
// L-shape: up first, then right into dstNE.
wire.wire(
"wNE",
("tapNW-port-tapNW", "dstNE-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Lower-left sink: taps bits [3:0] via SW breakout ----
element.block(x: 0, y: -4, w: 2, h: 2, id: "dstSW", fill: util.colors.purple, ports: (
east: ((id: "in"),),
))
wire.breakout(
"tapSE",
x: (rel: 5, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "south-east",
port-side: "west",
name: "[7:4]",
name-pos: "south-east",
)
// L-shape: down first, then left into dstSW.
wire.wire(
"wSW",
("tapSE-port-tapSE", "dstSW-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
// ---- Lower-right sink: taps bits [7:4] via SE breakout ----
element.block(x: 15, y: -4, w: 2, h: 2, id: "dstSE", fill: util.colors.pink, ports: (
west: ((id: "in"),),
))
wire.breakout(
"tapSW",
x: (rel: 11, to: "src-port-out"),
y: (from: "src-port-out"),
direction: "south-west",
port-side: "east",
name: "[3:0]",
name-pos: "south-west",
)
// L-shape: down first, then right into dstSE.
wire.wire(
"wSE",
("tapSW-port-tapSW", "dstSE-port-in"),
style: "zigzag",
zigzag-dir: "horizontal",
zigzag-ratio: 100%,
)
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+86
View File
@@ -0,0 +1,86 @@
// 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 INSIDE (outside-port-names: false) ---
element.group(
id: "cpu",
name: "CPU (labels outside)",
name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.yellow.lighten(60%),
padding: 1em,
port-name-padding: .5em,
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,
port-name-padding: 0.5em,
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")
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+117
View File
@@ -0,0 +1,117 @@
// Demonstrates pentagon-shaped ports.
//
// - Element-level defaults: `port-shape`, `port-kind`, `port-size`.
// - Per-port overrides: `shape`, `kind`, `size`, `stroke`, `fill`.
// - Wire endpoints connect at the outer end of each pentagon.
// - Clock ports with pentagon shape drop the clock triangle and default
// to a gray fill.
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// -----------------------------------------------------------------
// 1) All four sides × input/output
// -----------------------------------------------------------------
element.block(
id: "grid", w: 4, h: 4, x: 0, y: 0,
name: "sides × in/out", name-anchor: "center",
fill: util.colors.yellow.lighten(60%),
port-shape: "pentagon",
ports: (
west: (
(id: "wi", name: "W-in"),
(id: "wo", name: "W-out", kind: "out"),
),
east: (
(id: "ei", name: "E-in"),
(id: "eo", name: "E-out", kind: "out"),
),
north: (
(id: "ni", name: "N-in", vertical: true),
(id: "no", name: "N-out", kind: "out", vertical: true),
),
south: (
(id: "si", name: "S-in", vertical: true),
(id: "so", name: "S-out", kind: "out", vertical: true),
),
),
)
// -----------------------------------------------------------------
// 2) Per-port styling: fill and stroke overrides + size override
// -----------------------------------------------------------------
element.block(
id: "styled", w: 3, h: 3, x: 10, y: 0,
name: "per-port styling", name-anchor: "center",
fill: util.colors.green.lighten(40%),
port-shape: "pentagon",
ports: (
west: ((id: "a", name: "A", fill: util.colors.blue),),
east: ((id: "b", name: "B", kind: "out",
stroke: red + 1.5pt, fill: util.colors.pink),),
north: ((id: "c", name: "C", size: 1.5em),),
south: ((id: "d", name: "D", kind: "out",
fill: util.colors.orange),),
),
)
// -----------------------------------------------------------------
// 3) Clock port with pentagon → gray fill, no triangle
// -----------------------------------------------------------------
element.block(
id: "clk", w: 3, h: 2.5, x: 16, y: 0,
name: "clock + pentagon", name-anchor: "center",
fill: util.colors.orange.lighten(60%),
port-shape: "pentagon",
ports: (
west: ((id: "d", name: "D"),),
east: ((id: "q", name: "Q", kind: "out"),),
north: ((id: "clk", name: "CLK", clock: true),),
),
)
// -----------------------------------------------------------------
// 4) Two pentagon-ported blocks connected by a wire
// (wire terminates at outer edge of each pentagon)
// -----------------------------------------------------------------
element.block(
id: "src", w: 2, h: 1.5, x: 0, y: -5,
name: "src", name-anchor: "center",
fill: util.colors.pink,
port-shape: "pentagon",
ports: (east: ((id: "q", name: "Q", kind: "out"),)),
)
element.block(
id: "dst", w: 2, h: 1.5, x: 6, y: -5,
name: "dst", name-anchor: "center",
fill: util.colors.blue,
port-shape: "pentagon",
ports: (west: ((id: "d", name: "D"),)),
)
wire.wire("w-sd", ("src-port-q", "dst-port-d"))
// -----------------------------------------------------------------
// 5) Group with pentagon ports enclosing a child block
// -----------------------------------------------------------------
element.group(
id: "g", name: "group w/ pentagon ports", name-anchor: "north",
stroke: (dash: "dashed"),
fill: util.colors.purple.lighten(70%),
padding: 2em,
port-shape: "pentagon",
ports: (
west: ((id: "in", name: "IN"),),
east: ((id: "out", name: "OUT", kind: "out"),),
),
{
element.block(
id: "inner", w: 2, h: 1.5, x: 12, y: -5,
fill: util.colors.green,
ports: (west: ((id: "d"),), east: ((id: "q"),)),
)
},
)
wire.wire("wgi", ("g-port-in", "inner-port-d"))
wire.wire("wgo", ("inner-port-q", "g-port-out"))
})
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")
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+46
View File
@@ -0,0 +1,46 @@
// Demonstrates the `(rel: <number>, to: <anchor>)` shorthand for the `y`
// parameter (analogous to the existing shorthand for `x`).
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// Anchor block: absolute placement.
element.block(
x: 0, y: 0, w: 2, h: 1.5,
id: "A",
fill: util.colors.orange,
ports: (east: ((id: "out"),))
)
// X relative, Y absolute (existing feature).
element.block(
x: (rel: 1, to: "A.east"), y: 0, w: 2, h: 1.5,
id: "B",
fill: util.colors.yellow,
ports: (west: ((id: "in"),), east: ((id: "out"),))
)
// Both X and Y relative to another element's anchor (new feature).
element.block(
x: (rel: 1, to: "B.east"),
y: (rel: 2, to: "B.south-west"),
w: 2, h: 1.5,
id: "C",
fill: util.colors.green,
ports: (west: ((id: "in"),))
)
// Y relative only, X absolute.
element.block(
x: 0,
y: (rel: -2.5, to: "A.south-west"),
w: 2, h: 1.5,
id: "D",
fill: util.colors.blue,
ports: (east: ((id: "out"),))
)
wire.wire("wAB", ("A-port-out", "B-port-in"))
wire.wire("wBC", ("B-port-out", "C-port-in"), style: "zigzag")
})
BIN
View File
Binary file not shown.
+8 -3
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: *
...
@@ -195,7 +195,12 @@ If you have installed Circuiteria directly in your project, import #link("src/li
read("src/elements/logic/and.typ") + "\n" +
read("src/elements/logic/buf.typ") + "\n" +
read("src/elements/logic/or.typ") + "\n" +
read("src/elements/logic/xor.typ"),
read("src/elements/logic/xor.typ") + "\n" +
read("src/elements/logic/iec_gate.typ") + "\n" +
read("src/elements/logic/iec_and.typ") + "\n" +
read("src/elements/logic/iec_buf.typ") + "\n" +
read("src/elements/logic/iec_or.typ") + "\n" +
read("src/elements/logic/iec_xor.typ"),
name: "gates",
old-syntax: true,
scope: (
+1 -1
View File
@@ -1 +1 @@
#import "@preview/cetz:0.3.4": *
#import "@preview/cetz:0.5.2": *
+3 -50
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
///
@@ -9,52 +9,5 @@
/// - length (length, ratio): Optional base unit
/// -> none
#let circuit(body, length: 2em) = {
let next-id = 0
let elements = (:)
let ids = ()
for e in body {
if type(e) == dictionary and "id" in e {
ids.push(e.id)
}
}
for element in body {
let internal = type(element) == dictionary and "id" in element
let eid = if internal {element.id} else {auto}
if eid == auto {
while str(next-id) in ids {
next-id += 1
}
eid = str(next-id)
ids.push(eid)
if internal {
element.id = eid
}
next-id += 1
}
if eid in elements {
panic("An element with the id '" + eid + "' already exists. Please use a different id")
}
elements.insert(eid, element)
}
for element in elements.values() {
if type(element) == dictionary and "pre-process" in element {
elements = (element.pre-process)(elements, element)
assert(
type(elements) == dictionary,
message: "The `pre-process` method of element '" + element.id + "' did not return the elements dictionary"
)
}
}
canvas(length: length, {
for element in elements.values() {
if type(element) == dictionary and "draw" in element {
(element.draw)(element)
} else {
(element,)
}
}
})
}
canvas(length: length, body)
}
+5
View File
@@ -11,5 +11,10 @@
#import "elements/logic/or.typ": gate-or, gate-nor
#import "elements/logic/xor.typ": gate-xor, gate-xnor
#import "elements/logic/buf.typ": gate-buf, gate-not
#import "elements/logic/iec_gate.typ": iec-gate
#import "elements/logic/iec_and.typ": iec-gate-and, iec-gate-nand
#import "elements/logic/iec_buf.typ": iec-gate-buf, iec-gate-not
#import "elements/logic/iec_or.typ": iec-gate-or, iec-gate-nor
#import "elements/logic/iec_xor.typ": iec-gate-xor, iec-gate-xnor
#import "elements/group.typ": group
+59 -37
View File
@@ -1,40 +1,45 @@
#import "/src/cetz.typ": draw
#import "element.typ"
#import "ports.typ": add-port
#import "../util.typ"
#let draw-shape(elmt, bounds) = {
let tl = bounds.tl
let tr = bounds.tr
let bl = bounds.bl
let br = bounds.br
#let draw-shape(id, tl, tr, br, bl, fill, stroke) = {
let p0 = tl
let p1 = util.lerp(tr, 10%, br)
let p2 = util.lerp(tr, 90%, br)
let p1 = (tr, 10%, br)
let p2 = (tr, 90%, br)
let p3 = bl
let p4 = util.lerp(tl, 55%, bl)
let p5 = util.lerp(tl, 50%, br)
let p6 = util.lerp(tl, 45%, bl)
let bounds2 = element.complete-bounds(elmt, (
tl: p0,
tr: p1,
br: p2,
bl: p3
))
let p4 = (tl, 55%, bl)
let p5 = (tl, 50%, br)
let p6 = (tl, 45%, bl)
let f = {
draw.line(
p0, p1, p2, p3, p4, p5, p6,
fill: elmt.fill,
stroke: elmt.stroke,
close: true
let f1 = draw.group(name: id, {
draw.merge-path(
inset: 0.5em,
fill: fill,
stroke: stroke,
close: true,
draw.line(p0, p1, p2, p3, p4, p5, p6)
)
draw.anchor("north", (p0, 50%, p1))
draw.anchor("south", (p2, 50%, p3))
draw.anchor("west", (p0, 50%, p3))
draw.anchor("east", (p1, 50%, p2))
draw.anchor("north-west", p0)
draw.anchor("north-east", p1)
draw.anchor("south-east", p2)
draw.anchor("south-west", p3)
draw.anchor("name", (p5, 50%, (p1, 50%, p2)))
})
let f2 = add-port(id, "west", (id: "in1"), (p0, 50%, p6))
let f3 = add-port(id, "west", (id: "in2"), (p3, 50%, p4))
let f4 = add-port(id, "east", (id: "out"), (p1, 50%, p2))
let f = {
f1; f2; f3; f4
}
return (f, bounds2)
return (f, tl, tr, br, bl)
}
/// Draws an ALU with two inputs
@@ -42,7 +47,18 @@
/// #examples.alu
/// For parameters description, see #doc-ref("element.elmt")
#let alu(
..args
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
fill: none,
stroke: black + 1pt,
id: "",
debug: (
ports: false
)
) = {
let ports = (
west: (
@@ -54,18 +70,24 @@
)
)
return element.elmt(
element.elmt(
draw-shape: draw-shape,
x: x,
y: y,
w: w,
h: h,
name: name,
name-anchor: name-anchor,
ports: ports,
ports-pos: (
west: (
in1: l => l * 0.225,
in2: l => l * 0.775,
),
east: (
out: l => l * 0.5
)
fill: fill,
stroke: stroke,
id: id,
auto-ports: false,
ports-y: (
in1: (h) => {h * 0.225},
in2: (h) => {h * 0.775},
out: (h) => {h * 0.5}
),
..args
debug: debug
)
}
+64 -12
View File
@@ -1,25 +1,77 @@
#import "/src/cetz.typ": draw
#import "element.typ"
#let draw-shape(elmt, bounds) = {
#let draw-shape(id, tl, tr, br, bl, fill, stroke, radius: 0.5em) = {
let f = draw.rect(
radius: 0.5em,
radius: radius,
inset: 0.5em,
fill: elmt.fill,
stroke: elmt.stroke,
bounds.bl, bounds.tr
fill: fill,
stroke: stroke,
name: id,
bl, tr
)
return (f, bounds)
return (f, tl, tr, br, bl)
}
/// Draws a block element
///
/// #examples.block
/// For parameters description, see #doc-ref("element.elmt")
///
/// Ports can be given a pentagon shape (with configurable direction, size,
/// stroke and fill) via `port-shape` and per-port overrides:
/// #examples.port-shape
///
/// Port name labels can be placed outside the block via `port-names-outside`:
/// #examples.port-names-outside
/// For other parameters description, see #doc-ref("element.elmt")
/// - radius (number, length, ratio, dictionary): The corner radius of the block. See CeTZ documentation for more information
/// - shell (bool): If true, the block behaves like a shell/container (analogous to a group): port name labels default to being drawn outside the block, and wires attach to the inside tip of pentagon-shaped ports. See #doc-ref("element.elmt")
///
/// Use `shell: true` to wrap child elements inside a bordered container without
/// having ports overlap the interior:
/// #examples.shell
#let block(
..args
..args,
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
ports: (),
ports-margins: (),
port-name-padding: 2pt,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
fill: none,
radius: 0.5em,
stroke: black + 1pt,
id: "",
shell: false,
debug: (
ports: false
)
) = element.elmt(
cls: "block",
draw-shape: draw-shape,
..args
)
draw-shape: draw-shape.with(radius: radius),
body: if args.pos().len() > 0 { args.pos().first() } else { none },
x: x,
y: y,
w: w,
h: h,
name: name,
name-anchor: name-anchor,
ports: ports,
ports-margins: ports-margins,
port-name-padding: port-name-padding,
port-names-outside: port-names-outside,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
fill: fill,
stroke: stroke,
id: id,
shell: shell,
debug: debug
)
+121 -268
View File
@@ -1,5 +1,5 @@
#import "/src/cetz.typ": draw, coordinate, matrix, vector
#import "ports.typ": add-ports, add-port, get-port-pos, get-port-idx
#import "/src/cetz.typ": draw, coordinate
#import "ports.typ": add-ports, add-port
#import "../util.typ"
#let find-port(ports, id) = {
@@ -10,234 +10,13 @@
}
}
}
panic("Could not find port with id '" + str(id) + "' in ports " + repr(ports))
panic("Could not find port with id " + str(id))
}
#let local-to-global(origin, u, v, points) = {
return points-real = points.map(p => {
let (pu, pv) = p
return vector.add(
origin,
vector.add(
vector.scale(u, pu),
vector.scale(v, pv)
)
)
})
#let default-draw-shape(id, tl, tr, br, bl, fill, stroke) = {
return ({}, tl, tr, br, bl)
}
#let default-draw-shape(elmt, bounds) = {
return ({}, bounds)
}
#let default-pre-process(elements, element) = {
return elements
}
#let resolve-offset(ctx, offset, from, axis) = {
let (ctx, pos) = coordinate.resolve(
ctx,
(rel: offset, to: from)
)
return pos.at(axis)
}
#let resolve-align(ctx, elmt, bounds, align, with, axis) = {
let (align-side, i) = find-port(elmt.ports, align)
let margins = (0%, 0%)
if align-side in elmt.ports-margins {
margins = elmt.ports-margins.at(align-side)
}
let parallel-sides = (
("north", "south"),
("west", "east")
).at(axis)
let ortho-sides = (
("west", "east"),
("north", "south")
).at(axis)
let dl
let start-margin
let len = elmt.size.at(axis)
if align-side in parallel-sides {
let used-pct = 100% - margins.at(0) - margins.at(1)
let used-len = len * used-pct / 100%
start-margin = len * margins.at(0) / 100%
//dl = used-len * (i + 1) / (elmt.ports.at(align-side).len() + 1)
dl = get-port-pos(elmt, bounds, align-side, align, get-port-idx(elmt, align, side: align-side))
/*if not elmt.auto-ports {
start-margin = 0
dl = elmt.ports-pos.at(align)(len)
}*/
} else if align-side == ortho-sides.first() {
dl = 0
start-margin = 0
} else {
dl = len
start-margin = 0
}
if axis == 1 {
dl = len - dl
}
let (ctx, with-pos) = coordinate.resolve(ctx, with)
return with-pos.at(axis) - dl + start-margin
}
#let resolve-coordinate(ctx, elmt, bounds, coord, axis) = {
if type(coord) == dictionary {
let offset = coord.at("offset", default: none)
let from = coord.at("from", default: none)
let align = coord.at("align", default: none)
let with = coord.at("with", default: none)
if none not in (offset, from) {
if type(offset) != array {
let a = (0, 0)
a.at(axis) = offset
offset = a
}
return resolve-offset(ctx, offset, from, axis)
} else if none not in (align, with) {
return resolve-align(ctx, elmt, bounds, align, with, axis)
} else {
panic("Dictionnary must either provide both 'offset' and 'from', or 'align' and 'with'")
}
}
if type(coord) not in (int, float, length) {
panic("Invalid " + "xy".at(axis) + " coordinate: " + repr(coord))
}
return coord
}
#let complete-bounds(elmt, bounds) = {
let b = bounds
bounds += (
center: (
(b.br.at(0) + b.tl.at(0))/2,
(b.br.at(1) + b.tl.at(1))/2
),
b: (
(b.br.at(0) + b.bl.at(0))/2,
(b.br.at(1) + b.bl.at(1))/2
),
t: (
(b.tr.at(0) + b.tl.at(0))/2,
(b.tr.at(1) + b.tl.at(1))/2
),
l: (
(b.bl.at(0) + b.tl.at(0))/2,
(b.bl.at(1) + b.tl.at(1))/2
),
r: (
(b.br.at(0) + b.tr.at(0))/2,
(b.br.at(1) + b.tr.at(1))/2
),
sides: (
north: (bounds.tl, bounds.tr),
south: (bounds.bl, bounds.br),
west: (bounds.tl, bounds.bl),
east: (bounds.tr, bounds.br),
),
lengths: (
north: (bounds.tr.at(0) - bounds.tl.at(0)),
south: (bounds.br.at(0) - bounds.bl.at(0)),
west: (bounds.tl.at(1) - bounds.bl.at(1)),
east: (bounds.tr.at(1) - bounds.br.at(1)),
),
ports: (:)
)
for (side, props) in bounds.sides.pairs() {
let props2 = props
if side in elmt.ports-margins {
let (pt0, pt1) = props
let margins = elmt.ports-margins.at(side)
let a = util.lerp(pt0, margins.at(0), pt1)
let b = util.lerp(pt0, 100% - margins.at(1), pt1)
props2 = (a, b)
}
bounds.ports.insert(side, props2)
}
return bounds
}
#let make-bounds(elmt, x, y, w, h) = {
let w2 = w / 2
let h2 = h / 2
let bounds = (
bl: (x, y),
tl: (x, y + h),
tr: (x + w, y + h),
br: (x + w, y),
)
return complete-bounds(elmt, bounds)
}
#let render(draw-shape, elmt) = draw.group(name: elmt.id, ctx => {
let width = elmt.size.first()
let height = elmt.size.last()
let x = elmt.pos.first()
let y = elmt.pos.last()
let bounds = make-bounds(elmt, 0, 0, width, height)
x = resolve-coordinate(ctx, elmt, bounds, x, 0)
y = resolve-coordinate(ctx, elmt, bounds, y, 1)
bounds = make-bounds(elmt, x, y, width, height)
// Workaround because CeTZ needs to have all draw functions in the body
let func = {}
let res = draw-shape(elmt, bounds)
assert(
type(res) == array and res.len() == 2,
message: "The drawing function of element '" + elmt.id + "' did not return a function and new bounds"
)
(func, bounds) = res
if type(func) == function {
func = (func,)
}
assert(
type(bounds) == dictionary,
message: "The drawing function of element '" + elmt.id + "' did not return the correct bounds dictionary"
)
func
draw.anchor("north", bounds.t)
draw.anchor("south", bounds.b)
draw.anchor("west", bounds.l)
draw.anchor("east", bounds.r)
draw.anchor("north-west", bounds.tl)
draw.anchor("north-east", bounds.tr)
draw.anchor("south-east", bounds.br)
draw.anchor("south-west", bounds.bl)
if elmt.name != none {
draw.content(
(name: elmt.id, anchor: elmt.name-anchor),
anchor: if elmt.name-anchor in util.valid-anchors {elmt.name-anchor} else {"center"},
padding: 0.5em,
align(center)[*#elmt.name*]
)
}
add-ports(elmt, bounds)
if elmt.debug.bounds {
draw.line(
bounds.tl, bounds.tr, bounds.br, bounds.bl,
stroke: red,
close: true
)
}
})
/// Draws an element
/// - draw-shape (function): Draw function
/// - x (number, dictionary): The x position (bottom-left corner).
@@ -245,7 +24,9 @@
/// If it is a dictionary, it should be in the format `(rel: number, to: str)`, where `rel` is the offset and `to` the base anchor
/// - y (number, dictionary): The y position (bottom-left corner).
///
/// If it is a dictionary, it should be in the format `(from: str, to: str)`, where `from` is the base anchor and `to` is the id of the port to align with the anchor
/// If it is a dictionary, it can take one of two forms:
/// - `(rel: number, to: str)`: `rel` is the y offset and `to` the base anchor (analogous to `x`)
/// - `(from: str, to: str)`: `from` is the base anchor and `to` is the id of the port to align with the anchor
/// - w (number): Width of the element
/// - h (number): Height of the element
/// - name (none, str): Optional name of the block
@@ -255,70 +36,142 @@
/// - `name` (`str`): Optional name displayed *in* the block
/// - `clock` (`bool`): Whether it is a clock port (triangle symbol)
/// - `vertical` (`bool`): Whether the name should be drawn vertically
/// - `align-with` (`str`, `dictionary`): Optional. Reuse another port's placement coordinate (east/west: y, north/south: x). Accepted forms: `"<local-id>"` (same-element port; axis must match, transitively resolved, cycles panic), `(element: "<id>", port: "<id>")` (cross-element; axis not validated; target element must be drawn first), or `"<raw-anchor>"` (any unrecognised string is used as a CeTZ anchor name). See #examples.align-with-ports
/// - `x` (length, ratio, function): Optional. North/south ports only. Explicit x-offset override measured from the left corner of the side. A `ratio` is used as a percentage along the side; a length/number is used as an absolute offset; a function is called with the element width and must return a length/number. Panics if used together with `align-with` or on an east/west port.
/// - `y` (length, ratio, function): Optional. East/west ports only. Analogous to `x`, measured from the top corner of the side and called with the element height when a function is passed.
/// - ports-margins (dictionary): Dictionary of ports margins (used with automatic port placement). They keys are cardinal directions ("north", "east", "south", "west"). The values are tuples of (`<start>`, `<end>`) margins (numbers)
/// - port-name-padding (number, length): Padding between the port name label and the port anchor point (default `2pt`)
/// - port-names-outside (bool, auto): If true, port name labels are drawn on the outside of the block instead of the inside. When `auto` (default), follows the `shell` setting (outside if `shell: true`, inside otherwise)
/// - port-shape (none, str): Default port shape for all ports of this element. Set to `"pentagon"` to draw pentagon-shaped ports. Overridable per port via the port's `shape` field. See #examples.port-shape
/// - port-kind (str): Default direction of ports (`"in"` or `"out"`). Overridable per port via the port's `kind` field
/// - port-size (length): Default unit size of pentagon-shaped ports (default `1em`). Overridable per port via the port's `size` field
/// - fill (none, color): Fill color
/// - stroke (stroke): Border stroke
/// - id (str): The block id (for future reference)
/// - auto-ports (bool): Whether to use auto port placements or not. If false, `draw-shape` is responsible for adding the appropiate ports
/// - ports-y (dictionary): Dictionary of the ports y offsets (used with `auto-ports: false`)
/// - shell (bool): If true, the block behaves like a shell/container (analogous to a group): port name labels default to being drawn *outside* the block border, and wires attach to the *inside* tip of pentagon-shaped ports instead of the outside base. Useful for wrapping child elements without having ports overlap them (default `false`)
/// - debug (dictionary): Dictionary of debug options.
///
/// Supported fields include:
/// - `ports`: if true, shows dots on all ports of the element
#let elmt(
cls: "element",
draw-shape: default-draw-shape,
pre-process: default-pre-process,
pos: (0, 0),
size: (1, 1),
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
ports: (:),
ports-margins: (:),
port-name-padding: 2pt,
port-names-outside: auto,
port-shape: none,
port-kind: "in",
port-size: 1em,
fill: none,
stroke: black + 1pt,
id: auto,
ports-pos: auto,
id: "",
auto-ports: true,
ports-y: (:),
shell: false,
body: none,
debug: (
bounds: false,
ports: false
),
extra: (:)
) = {
for (key, side-ports) in ports.pairs() {
if type(side-ports) == str {
side-ports = ((id: side-ports),)
} else if type(side-ports) == dictionary {
side-ports = (side-ports,)
)
) = draw.get-ctx(ctx => {
let width = w
let height = h
let x = x
let y = y
if x == none { panic("Parameter x must be set") }
if y == none { panic("Parameter y must be set") }
if w == none { panic("Parameter w must be set") }
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
let (to-side, i) = find-port(ports, to)
let margins = (0%, 0%)
if to-side in ports-margins {
margins = ports-margins.at(to-side)
}
for (i, port) in side-ports.enumerate() {
if type(port) == array {
side-ports.at(i) = (
id: port.at(0, default: ""),
name: port.at(1, default: "")
)
} else if type(port) == str {
side-ports.at(i) = (id: port)
let dy
let top-margin
if to-side in ("east", "west") {
let used-pct = 100% - margins.at(0) - margins.at(1)
let used-height = height * used-pct / 100%
top-margin = height * margins.at(0) / 100%
dy = used-height * (i + 1) / (ports.at(to-side).len() + 1)
if not auto-ports {
top-margin = 0
dy = ports-y.at(to)(height)
}
} else if to-side == "north" {
dy = 0
top-margin = 0
} else if to-side == "south" {
dy = height
top-margin = 0
}
ports.at(key) = side-ports
let (ctx, from-pos) = coordinate.resolve(ctx, from)
y = from-pos.at(1) + dy - height + top-margin
}
return ((
cls: cls,
id: id,
draw: render.with(draw-shape),
pre-process: pre-process,
pos: pos,
size: size,
name: name,
name-anchor: name-anchor,
ports: ports,
ports-margins: ports-margins,
fill: fill,
stroke: stroke,
ports-pos: ports-pos,
debug: debug
) + extra,)
}
let tl = (x, y + height)
let tr = (x + width, y + height)
let br = (x + width, y)
let bl = (x, y)
// Workaround because CeTZ needs to have all draw functions in the body
let func = {}
(func, tl, tr, br, bl) = draw-shape(id, tl, tr, br, bl, fill, stroke)
func
if (name != none) {
draw.content(
(name: id, anchor: name-anchor),
anchor: if name-anchor in util.valid-anchors {name-anchor} else {"center"},
padding: 0.5em,
align(center)[*#name*]
)
}
if auto-ports {
let outside-name = if port-names-outside == auto { shell } else { port-names-outside }
add-ports(
id,
tl, tr, br, bl,
ports,
ports-margins,
width: width,
height: height,
debug: debug.ports,
name-padding: port-name-padding,
outside-name: outside-name,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: stroke,
elmt-fill: fill,
wire-inside: shell,
)
}
if body != none {
body
}
})
+75 -33
View File
@@ -1,25 +1,41 @@
#import "/src/cetz.typ": draw
#import "element.typ"
#import "ports.typ": add-port
#import "../util.typ"
#let draw-shape(elmt, bounds) = {
let ratio = elmt.l-ratio / 100%
let tl2 = util.lerp(bounds.bl, elmt.l-ratio, bounds.tl)
let bounds2 = element.complete-bounds(elmt, (
tl: tl2,
tr: bounds.tr,
br: bounds.br,
bl: bounds.bl,
))
#let draw-shape(id, tl, tr, br, bl, fill, stroke, h-ratio: 75%, align-out: true) = {
let (x, y) = bl
let (width, height) = (tr.at(0) - x, tr.at(1) - y)
let f = draw.line(
bounds2.tl, bounds2.tr, bounds2.br, bounds2.bl,
fill: elmt.fill,
stroke: elmt.stroke,
close: true
)
return (f, bounds2)
let ratio = h-ratio / 100%
tl = (x, y + height * ratio)
let tr2 = (x + width, y + height * ratio)
let br = (x + width, y)
if align-out {
(tr, tr2) = (tr2, tr)
} else {
(tr, tr2) = (tr, tr)
}
let f = draw.group(name: id, {
draw.merge-path(
inset: 0.5em,
fill: fill,
stroke: stroke,
close: true,
draw.line(tl, tr2, br, bl)
)
draw.anchor("north", (tl, 50%, tr2))
draw.anchor("south", (bl, 50%, br))
draw.anchor("west", (tl, 50%, bl))
draw.anchor("east", (tr2, 50%, br))
draw.anchor("north-west", tl)
draw.anchor("north-east", tr2)
draw.anchor("south-east", br)
draw.anchor("south-west", bl)
})
return (f, tl, tr, br, bl)
}
/// Draws a bit extender
@@ -29,28 +45,54 @@
/// - h-ratio (ratio): The height ratio of the left side relative to the full height
/// - align-out (bool): If true, the output and input ports are aligned, otherwise, the output port is centered on the right side
#let extender(
l-ratio: 75%,
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
fill: none,
stroke: black + 1pt,
id: "",
h-ratio: 75%,
align-out: true,
..args
debug: (
ports: false
)
) = {
let ports = (
west: ((id: "in"),),
east: ((id: "out"),)
west: (
(id: "in"),
),
east: (
(id: "out"),
)
)
let out-pct = if align-out {l-ratio / 2} else {50%}
let ports-pos = (
"east": (l, i) => {l - l * (out-pct / 100%)}
let out-pct = if align-out {h-ratio / 2} else {50%}
let ports-y = (
"in": (h) => {h - h * (h-ratio / 200%)},
"out": (h) => {h - h * (out-pct / 100%)}
)
return element.elmt(
cls: "extender",
draw-shape: draw-shape,
element.elmt(
draw-shape: draw-shape.with(h-ratio: h-ratio, align-out: align-out),
x: x,
y: y,
w: w,
h: h,
name: name,
name-anchor: name-anchor,
ports: ports,
ports-pos: ports-pos,
extra: (
l-ratio: l-ratio,
align-out: align-out
),
..args
auto-ports: false,
ports-y: ports-y,
fill: fill,
stroke: stroke,
id: id,
debug: debug
)
let in-pos = (rel: (0, h * (h-ratio / 200%)), 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)
}
+44 -1
View File
@@ -1,9 +1,15 @@
#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
@@ -16,6 +22,14 @@
/// - 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
/// - port-name-padding (number, length): Padding between the port name label and the group border (default `2pt`). Accepts anything CeTZ accepts as `padding` (number or length)
/// - port-shape (none, str): see #doc-ref("element.elmt")
/// - port-kind (str): see #doc-ref("element.elmt")
/// - port-size (length): see #doc-ref("element.elmt")
/// - debug (dictionary): see #doc-ref("element.elmt")
#let group(
body,
id: "",
@@ -24,7 +38,17 @@
fill: none,
stroke: black + 1pt,
padding: 0.5em,
radius: 0.5em
radius: 0.5em,
ports: (:),
ports-margins: (:),
outside-port-names: true,
port-name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
debug: (
ports: false
)
) = {
let min-x = none
let max-x = none
@@ -68,6 +92,25 @@
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,
name-padding: port-name-padding,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: stroke,
elmt-fill: fill,
)
if name != none {
draw.content(
id + "." + name-anchor,
+11 -5
View File
@@ -1,4 +1,5 @@
#import "/src/cetz.typ": draw, coordinate
#import "/src/util.typ"
#import "../ports.typ": add-ports, add-port
#import "../element.typ"
@@ -49,13 +50,12 @@
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
let offset = x.rel
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
x = to-pos.at(0)
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary) {
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
@@ -63,6 +63,12 @@
if to == "out" {
dy = height / 2
} else {
let all-ports = range(inputs).map(i => "in" + str(i))
let i = all-ports.position(p => p == to)
assert(
i != none,
message: "Unknown port '" + y.to + "' must be one of " + repr(all-ports)
)
dy = height * (i + 0.5) / inputs
}
+70
View File
@@ -0,0 +1,70 @@
#import "/src/cetz.typ": draw
// #import "iec_gate.typ" as iec-gate
#import "iec_gate.typ" as iec-gate
/// Draws an IEC-AND gate. This function is also available as `element.iec-gate-and()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-and
#let iec-gate-and(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
),
) = {
iec-gate.iec-gate(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: inverted,
debug: debug,
symbol: $amp$,
)
}
/// Draws an IEC-NAND gate. This function is also available as `element.iec-gate-nand()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-nand
#let iec-gate-nand(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
),
) = {
iec-gate-and(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: if inverted != "all" {inverted + ("out",)} else {inverted},
debug: debug,
)
}
+68
View File
@@ -0,0 +1,68 @@
#import "/src/cetz.typ": draw
#import "iec_gate.typ" as iec-gate
/// Draws an IEC buffer gate. This function is also available as `element.iec-gate-buf()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-buf
#let iec-gate-buf(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false,
),
) = {
iec-gate.iec-gate(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: inverted,
debug: debug,
symbol: "1",
)
}
/// Draws an IEC NOT gate. This function is also available as `element.iec-gate-not()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-not
#let iec-gate-not(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false,
),
) = {
iec-gate-buf(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: if inverted != "all" { inverted + ("out",) } else { inverted },
debug: debug,
)
}
+125
View File
@@ -0,0 +1,125 @@
#import "/src/cetz.typ": draw, coordinate
#import "/src/util.typ"
#import "../ports.typ": add-ports, add-port
#import "../element.typ"
#let default-draw-shape(id, tl, tr, br, bl, fill, stroke, symbol) = {
let shapes = draw.rect(
inset: 0.5em,
fill: fill,
stroke: stroke,
name: id,
bl, tr
)
shapes += draw.content(
id + ".center",
[*$ symbol $*]
)
return (shapes, tl, tr, br, bl)
}
/// Draws a logic gate. This function is also available as `element.iec-gate()`
///
/// - draw-shape (function): see #doc-ref("element.elmt")
/// - x (number, dictionary): see #doc-ref("element.elmt")
/// - y (number, dictionary): see #doc-ref("element.elmt")
/// - w (number): see #doc-ref("element.elmt")
/// - h (number): see #doc-ref("element.elmt")
/// - inputs (int): The number of inputs
/// - fill (none, color): see #doc-ref("element.elmt")
/// - stroke (stroke): see #doc-ref("element.elmt")
/// - id (str): see #doc-ref("element.elmt")
/// - inverted (str, array): Either "all" or an array of port ids to display as inverted
/// - inverted-radius (number): The radius of inverted ports dot
/// - debug (dictionary): see #doc-ref("element.elmt")
/// - symbol (str): The symbol to display at the center of the gate
#let iec-gate(
draw-shape: default-draw-shape,
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
inverted-radius: 0.1,
debug: (
ports: false
),
symbol: "",
) = draw.get-ctx(ctx => {
let width = w
let height = h
let x = x
let y = y
if x == none { panic("Parameter x must be set") }
if y == none { panic("Parameter y must be set") }
if w == none { panic("Parameter w must be set") }
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
x = util.resolve-rel-coord(ctx, x, 0)
}
if (type(y) == dictionary and "rel" in y) {
y = util.resolve-rel-coord(ctx, y, 1)
} else if (type(y) == dictionary) {
let from = y.from
let to = y.to
let dy
if to == "out" {
dy = height / 2
} else {
dy = height * (i + 0.5) / inputs
}
let (ctx, from-pos) = coordinate.resolve(ctx, from)
y = from-pos.at(1) + dy - height
}
let tl = (x, y + height)
let tr = (x + width, y + height)
let br = (x + width, y)
let bl = (x, y)
// Workaround because CeTZ needs to have all draw functions in the body
let func = {}
(func, tl, tr, br, bl) = draw-shape(id, tl, tr, br, bl, fill, stroke, symbol)
func
let space = 100% / inputs
for i in range(inputs) {
let pct = (i + 0.5) * space
let port-pos = (tl, pct, bl)
let port-name = "in" + str(i)
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)
}
add-port(
id, "west",
(id: port-name), port-pos,
debug: debug.ports
)
}
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)
}
add-port(
id, "east",
(id: "out"), out-pos,
debug: debug.ports
)
})
+67
View File
@@ -0,0 +1,67 @@
#import "/src/cetz.typ": draw
#import "iec_gate.typ" as iec-gate
/// Draws an IEC-OR gate. This function is also available as `element.iec-gate-or()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-or
#let iec-gate-or(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
)
) = {
iec-gate.iec-gate(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: inverted,
debug: debug,
symbol: $>= 1$,
)
}
/// Draws an IEC-NOR gate. This function is also available as `element.iec-gate-nor()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-nor
#let iec-gate-nor(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
)
) = {
iec-gate-or(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: if inverted != "all" {inverted + ("out",)} else {inverted},
debug: debug
)
}
+67
View File
@@ -0,0 +1,67 @@
#import "/src/cetz.typ": draw
#import "iec_gate.typ" as iec-gate
/// Draws an IEC-XOR gate. This function is also available as `element.iec-gate-xor()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-xor
#let iec-gate-xor(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
)
) = {
iec-gate.iec-gate(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: inverted,
debug: debug,
symbol: $= 1$,
)
}
/// Draws an IEC-XNOR gate. This function is also available as `element.iec-gate-xnor()`
///
/// For parameters, see #doc-ref("gates.iec-gate")
/// #examples.iec-gate-xnor
#let iec-gate-xnor(
x: none,
y: none,
w: none,
h: none,
inputs: 2,
fill: none,
stroke: black + 1pt,
id: "",
inverted: (),
debug: (
ports: false
)
) = {
iec-gate-xor(
x: x,
y: y,
w: w,
h: h,
inputs: inputs,
fill: fill,
stroke: stroke,
id: id,
inverted: if inverted != "all" {inverted + ("out",)} else {inverted},
debug: debug
)
}
+59 -34
View File
@@ -3,24 +3,29 @@
#import "element.typ"
#import "ports.typ": add-port
#let draw-shape(elmt, bounds) = {
let margin = (100% - elmt.l-ratio) / 2
let tr2 = util.lerp(bounds.tr, margin, bounds.br)
let br2 = util.lerp(bounds.br, margin, bounds.tr)
let bounds2 = element.complete-bounds(elmt, (
tl: bounds.tl,
bl: bounds.bl,
tr: tr2,
br: br2,
))
let f = draw.line(
bounds2.tl, bounds2.tr, bounds2.br, bounds2.bl,
close: true,
fill: elmt.fill,
stroke: elmt.stroke
)
#let draw-shape(id, tl, tr, br, bl, fill, stroke, h-ratio: 60%) = {
let margin = (100% - h-ratio) / 2
let tr2 = (tr, margin, br)
let br2 = (br, margin, tr)
let f = draw.group(name: id, {
draw.merge-path(
inset: 0.5em,
fill: fill,
stroke: stroke,
close: true,
draw.line(tl, tr2, br2, bl)
)
draw.anchor("north", (tl, 50%, tr2))
draw.anchor("south", (bl, 50%, br2))
draw.anchor("west", (tl, 50%, bl))
draw.anchor("east", (tr2, 50%, br2))
draw.anchor("north-west", tl)
draw.anchor("north-east", tr2)
draw.anchor("south-east", br2)
draw.anchor("south-west", bl)
})
return (f, bounds2)
return (f, tl, tr, br, bl)
}
/// Draws a multiplexer
@@ -30,44 +35,64 @@
/// - entries (int, array): If it is an integer, it defines the number of input ports (automatically named with their binary index). If it is an array of strings, it defines the name of each input.
/// - h-ratio (ratio): The height ratio of the right side relative to the full height
#let multiplexer(
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
entries: 2,
l-ratio: 60%,
..args
h-ratio: 60%,
fill: none,
stroke: black + 1pt,
id: "",
debug: (
ports: false
)
) = {
let in-ports = ()
let ports-pos = (
"east": auto,
let ports = ()
let ports-y = (
out: (h) => {h * 0.5}
)
if (type(entries) == int) {
let nbits = calc.ceil(calc.log(entries, base: 2))
for i in range(entries) {
let bits = util.lpad(str(i, base: 2), nbits)
in-ports.push((id: "in" + str(i), name: bits))
ports.push((id: "in" + str(i), name: bits))
}
} else {
for (i, port) in entries.enumerate() {
in-ports.push((id: "in" + str(i), name: port))
ports.push((id: "in" + str(i), name: port))
}
}
let n = in-ports.len()
ports-pos.insert("west", (l, i) => {l * (i + 0.5) / n})
let space = 100% / ports.len()
let l = ports.len()
for (i, port) in ports.enumerate() {
ports-y.insert(port.id, (h) => {h * (i + 0.5) / l})
}
element.elmt(
cls: "multiplexer",
draw-shape: draw-shape,
ports: (west: in-ports, east: ((id: "out"),)),
ports-pos: ports-pos,
extra: (l-ratio: l-ratio),
..args
draw-shape: draw-shape.with(h-ratio: h-ratio),
x: x,
y: y,
w: w,
h: h,
name: name,
name-anchor: name-anchor,
ports: (west: ports, east: ((id: "out"),)),
fill: fill,
stroke: stroke,
id: id,
ports-y: ports-y,
auto-ports: false,
debug: debug
)
/*
for (i, port) in ports.enumerate() {
let pct = (i + 0.5) * space
add-port(id, "west", port, (id+".north-west", pct, id+".south-west"))
}
add-port(id, "east", (id: "out"), (id+".north-east", 50%, id+".south-east"))
*/
}
+316 -123
View File
@@ -1,113 +1,105 @@
#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor
#let get-port-side(elmt, port) = {
for (side, ports) in elmt.ports {
for p in ports {
if p.id == port {
return side
}
}
}
panic(
"Unknown port '" + port + "' on element '" + elmt.id + "', could not automatically determine side"
)
}
#let get-port-idx(elmt, port, side: auto) = {
if side == auto {
side = get-port-side(elmt, port)
}
assert(
side in elmt.ports,
message: "No ports on side '" + side + "' of element '" + elmt.id + "'"
)
let i = elmt.ports.at(side).position(p => p.id == port)
assert(
i != none,
message: "Could not find port '" + port + "' on side '" + side + "' of element '" + elmt.id + "'"
)
return i
}
#let get-port-pos(elmt, bounds, side, port, port-i) = {
let (pt0, pt1) = bounds.ports.at(side)
let side-len = if side in ("north", "south") {
pt1.at(0) - pt0.at(0)
} else {
pt0.at(1) - pt1.at(1)
}
let offset = if (
elmt.ports-pos == auto or
elmt.ports-pos.at(side, default: auto) == auto
) {
let space = 100% / (elmt.ports.at(side, default: ()).len() + 1)
(port-i + 1) * space
} else {
assert(
side in elmt.ports-pos,
message: "Could not reliably compute port position (missing side)"
)
let side-pos = elmt.ports-pos.at(side)
if type(side-pos) == function {
(side-pos)(side-len, port-i)
} else if type(side-pos) == array {
(side-pos.at(i))(side-len)
} else if type(side-pos) == dictionary {
assert(
port in side-pos,
message: "Could not reliably compute port position (missing port)"
)
(side-pos.at(port))(side-len)
} else {
panic("Could not reliably compute port position (invalid type)")
}
}
if type(offset) == ratio {
offset = offset * side-len / 100%
}
return offset
}
#import "../util.typ": rotate-anchor, opposite-anchor, port-shape-vertices
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
let is-clock = port.at("clock", default: false)
if (port.at("clock", default: false)) {
// Per-port overrides fall back to element-level defaults.
let shape = port.at("shape", default: port-shape)
let kind = port.at("kind", default: port-kind)
let size = port.at("size", default: port-size)
let shape-stroke = port.at("stroke", default: elmt-stroke)
// Pentagon fill defaults to the element's fill (so the pentagon covers the
// block-border segment it crosses). Clock ports default to gray. Per-port
// `fill` overrides both.
let shape-fill = port.at("fill",
default: if is-clock and shape == "pentagon" { gray } else { elmt-fill })
// When shape is enabled, wire-connection anchor moves outside the block, and
// inside-name labels reference the inner tip/base; otherwise both collapse to
// the boundary point (unchanged behaviour).
let outer-pos = pos
let inner-pos = pos
if shape == "pentagon" {
let sh = port-shape-vertices(side, kind, size)
let v = sh.vertices.map(off => (rel: off, to: pos))
// Draw pentagon on a higher layer so it always renders on top of wires
// and neighbouring elements.
draw.on-layer(1, draw.line(
v.at(0), v.at(1), v.at(2), v.at(3), v.at(4),
close: true,
stroke: shape-stroke,
fill: shape-fill,
))
outer-pos = (rel: sh.outer, to: pos)
inner-pos = (rel: sh.inner, to: pos)
} else if is-clock {
if prev == none or next == none {
panic("Clock port must have previous and next positions")
}
let size = if port.at("small", default: false) {8pt} else {1em}
let csize = 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, -csize) }
else if (side == "east") { offset = (-csize, 0) }
else if (side == "south") { offset = ( 0, csize) }
else if (side == "west") { offset = ( csize, 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 }
}
// Label sits at the outer edge of the shape for outside-name, inner edge for
// inside-name. `name-padding` is thus naturally measured from the shape edge
// (or from the boundary when shape is disabled).
let label-pos = if outside-name { outer-pos } else { inner-pos }
// For "shell" containers, wires must attach at the inside of the pentagon
// (the tip that points inward), not at the exterior base.
let anchor-pos = if wire-inside and shape == "pentagon" { inner-pos } else { outer-pos }
draw.content(
pos,
anchor: if name-rotate {rotate-anchor(side)} else {side},
padding: 2pt,
label-pos,
anchor: name-anchor,
padding: name-padding,
angle: if name-rotate {90deg} else {0deg},
name
)
let id = elmt-id + "-port-" + port.at("id")
if debug {
draw.circle(
pos,
anchor-pos,
name: id,
radius: .1,
stroke: none,
fill: red
@@ -115,65 +107,266 @@
} else {
draw.hide(draw.circle(
pos,
anchor-pos,
radius: 0,
stroke: none
stroke: none,
name: id
))
}
draw.anchor(port.id, pos)
}
// Normalize an `align-with` value to either:
// - `(local: <id>)` → reference to another port of the *same* element
// - `(external: <anchor-name>)` → CeTZ anchor name of a port in another element
// Accepted input forms:
// - `str` matching a local port id → local
// - `str` not matching any local port id → external, used as-is as CeTZ anchor
// - `(element: <str>, port: <str>)` → external, anchor = `<element>-port-<port>`
#let classify-align-with(raw-val, placements) = {
if type(raw-val) == dictionary {
if "element" in raw-val and "port" in raw-val {
return (external: raw-val.element + "-port-" + raw-val.port)
}
panic("`align-with` dictionary must contain `element` and `port` keys, got: " + repr(raw-val))
}
if type(raw-val) != str {
panic("`align-with` must be a string or `(element:, port:)` dictionary, got: " + repr(raw-val))
}
if raw-val in placements {
return (local: raw-val)
}
return (external: raw-val)
}
// Resolve a single port's placement, following `align-with` references
// transitively and detecting cycles.
//
// Returns `(placements, result)` where `result` is either:
// - `(pct: <ratio>)` → percentage along the side (local placement)
// - `(external: <anchor-name>)` → align with the given remote CeTZ anchor
#let resolve-placement(placements, id, visiting, elmt-id: "") = {
let where = if elmt-id != "" { " (element '" + elmt-id + "')" } else { "" }
let p = placements.at(id)
if p.at("resolved", default: false) {
if "external-ref" in p {
return (placements, (external: p.external-ref))
}
return (placements, (pct: p.pct))
}
if id in visiting {
panic("align-with cycle involving port '" + id + "'" + where + " (path: "
+ visiting.join(" -> ") + " -> " + id + ")")
}
let raw = p.raw
let side = p.side
let is-ew = side in ("east", "west")
let has-align = "align-with" in raw
let has-x = "x" in raw
let has-y = "y" in raw
let has-num = has-x or has-y
if has-align and has-num {
panic("Port '" + id + "'" + where + ": cannot combine `align-with` with `x`/`y` override")
}
if is-ew and has-x {
panic("Port '" + id + "'" + where + " on side '" + side
+ "': use `y` for placement override on east/west sides, not `x`")
}
if (not is-ew) and has-y {
panic("Port '" + id + "'" + where + " on side '" + side
+ "': use `x` for placement override on north/south sides, not `y`")
}
let result = none
if has-num {
let val = if is-ew { raw.y } else { raw.x }
let dim = if is-ew { p.height } else { p.width }
if type(val) == function {
val = val(dim)
}
let pct
if type(val) == ratio {
pct = val
} else {
// Numeric length: measured from `pt0` along the side.
pct = (val / dim) * 100%
}
result = (pct: pct)
} else if has-align {
let classified = classify-align-with(raw.at("align-with"), placements)
if "external" in classified {
// Cross-element / raw anchor reference: axis cannot be validated here
// (remote side unknown at eval time). Propagate as external.
result = (external: classified.external)
} else {
let ref-id = classified.local
let ref-side = placements.at(ref-id).side
let ref-ew = ref-side in ("east", "west")
if is-ew != ref-ew {
panic("Port '" + id + "'" + where + " on '" + side + "' cannot `align-with` port '"
+ ref-id + "' on '" + ref-side + "': incompatible axis")
}
let new-visiting = visiting + (id,)
let res = resolve-placement(placements, ref-id, new-visiting, elmt-id: elmt-id)
placements = res.at(0)
result = res.at(1)
}
} else {
result = (pct: p.pct-auto)
}
if "external" in result {
p.insert("external-ref", result.external)
p.insert("pct", none)
} else {
p.insert("pct", result.pct)
}
p.insert("resolved", true)
placements.insert(id, p)
return (placements, result)
}
#let add-ports(
elmt,
bounds
elmt-id,
tl, tr, br, bl,
ports,
ports-margins,
width: 0,
height: 0,
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let debug = elmt.debug.at("ports", default: false)
let sides = (
"north": (tl, tr),
"east": (tr, br),
"south": (bl, br),
"west": (tl, bl)
)
if type(elmt.ports) != dictionary {
if type(ports) != dictionary {
return
}
for (side, props) in bounds.ports {
let side-ports = elmt.ports.at(side, default: ())
let space = 100% / (side-ports.len() + 1)
let (pt0, pt1) = props
let side-len = if side in ("north", "south") {
pt1.at(0) - pt0.at(0)
} else {
pt0.at(1) - pt1.at(1)
}
for (i, port) in side-ports.enumerate() {
let offset = get-port-pos(elmt, bounds, side, port.id, i)
// ---- Phase 1: build placement table with per-port baseline pct. ----
let placements = (:)
// Preserve iteration order for phase 3.
let order = ()
// Per-side (pt0, pt1) after margin adjustment, and per-side spacing.
let side-info = (:)
let pos = (pt0, offset, pt1)
let offset-prev = if type(offset) == ratio {
offset - space / 2
} else {
offset - space * side-len / 200%
for (side, props) in sides {
let side-ports = ports.at(side, default: ())
let pt0 = props.at(0)
let pt1 = props.at(1)
if side in ports-margins {
let margins = ports-margins.at(side)
let a = (pt0, margins.at(0), pt1)
let b = (pt0, 100% - margins.at(1), pt1)
pt0 = a
pt1 = b
}
let space = 100% / (side-ports.len() + 1)
side-info.insert(side, (pt0: pt0, pt1: pt1, space: space))
for (i, port) in side-ports.enumerate() {
let pid = port.at("id")
if pid in placements {
panic("Duplicate port id '" + pid + "' in element '" + elmt-id
+ "' on side '" + side + "' (existing side: '"
+ placements.at(pid).side + "', new port: " + repr(port) + ")")
}
let offset-next = if type(offset) == ratio {
offset + space / 2
let pct-auto = (i + 1) * space
placements.insert(pid, (
side: side,
raw: port,
index: i,
pct-auto: pct-auto,
width: width,
height: height,
resolved: false,
))
order.push(pid)
}
}
// ---- Phase 2: resolve overrides & align-with references. ----
for pid in order {
let res = resolve-placement(placements, pid, (), elmt-id: elmt-id)
placements = res.at(0)
}
// ---- Phase 3: draw. ----
for pid in order {
let p = placements.at(pid)
let side = p.side
let info = side-info.at(side)
let pt0 = info.pt0
let pt1 = info.pt1
let space = info.space
let port = p.raw
let is-ew = side in ("east", "west")
let pos
let pos-prev
let pos-next
if "external-ref" in p {
// Cross-element alignment: build pos via CeTZ perpendicular so this
// port shares one coordinate with the remote anchor and stays on this
// element's side line for the other coordinate.
// east/west: same y as remote, x from this side (pt0.x == pt1.x)
// north/south: same x as remote, y from this side (pt0.y == pt1.y)
let ref = p.external-ref
if is-ew {
pos = (pt0, "|-", ref)
// Fallback prev/next: just the pos itself; clock ports do not
// combine meaningfully with cross-element align.
pos-prev = pos
pos-next = pos
} else {
offset + space * side-len / 200%
pos = (ref, "|-", pt0)
pos-prev = pos
pos-next = pos
}
let pos-prev = (pt0, offset-prev, pt1)
let pos-next = (pt0, offset-next, pt1)
} else {
let pct = p.pct
pos = (pt0, pct, pt1)
pos-prev = (pt0, pct - 0.5 * space, pt1)
pos-next = (pt0, pct + 0.5 * space, pt1)
if port.at("small", default: false) {
pos-prev = (pos, 4pt, pt0)
pos-next = (pos, 4pt, pt1)
}
add-port(
elmt.id,
side,
port,
pos,
prev: pos-prev,
next: pos-next,
debug: debug
)
}
add-port(
elmt-id,
side,
port,
pos,
prev: pos-prev,
next: pos-next,
debug: debug,
outside-name: outside-name,
name-padding: name-padding,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: elmt-stroke,
elmt-fill: elmt-fill,
wire-inside: wire-inside,
)
}
}
}
+6 -1
View File
@@ -2,4 +2,9 @@
#import "elements/logic/and.typ": gate-and, gate-nand
#import "elements/logic/or.typ": gate-or, gate-nor
#import "elements/logic/xor.typ": gate-xor, gate-xnor
#import "elements/logic/buf.typ": gate-buf, gate-not
#import "elements/logic/buf.typ": gate-buf, gate-not
#import "elements/logic/iec_gate.typ": iec-gate
#import "elements/logic/iec_and.typ": iec-gate-and, iec-gate-nand
#import "elements/logic/iec_or.typ": iec-gate-or, iec-gate-nor
#import "elements/logic/iec_buf.typ": iec-gate-buf, iec-gate-not
#import "elements/logic/iec_xor.typ": iec-gate-xor, iec-gate-xnor
+2 -1
View File
@@ -1,4 +1,5 @@
#let version = version(0, 2, 0)
#let version = version(0, 2, 1)
#let name = "circuiteria"
#import "circuit.typ": circuit
#import "element.typ"
+108 -5
View File
@@ -1,3 +1,23 @@
#import "cetz.typ": coordinate
/// Resolves a relative coordinate shorthand of the form `(rel: <number>, to: <anchor>)`
/// to a single scalar along the requested axis.
///
/// Used to implement the shared `(rel:, to:)` shorthand accepted by the `x` and
/// `y` parameters of `elmt`, `gate` and `iec-gate`.
///
/// - ctx (dictionary): The current CeTZ context
/// - val (dictionary): A dictionary with fields `rel` (number) and `to` (anchor)
/// - axis (int): `0` for x, `1` for y
/// -> number
#let resolve-rel-coord(ctx, val, axis) = {
let offset = val.rel
let to = val.to
let delta = if axis == 0 { (offset, 0) } else { (0, offset) }
let (_, to-pos) = coordinate.resolve(ctx, (rel: delta, to: to))
return to-pos.at(axis)
}
/// Predefined color palette
/// #let w = 4
/// #box(width: 100%, align(center)[
@@ -70,14 +90,97 @@
).at(anchor)
}
/// Returns the pentagon port shape vertices and the outer/inner anchor
/// offsets for the given side and direction.
///
/// The pentagon straddles the block boundary: 1 unit outside, 1 unit inside;
/// height ±0.5 units. For `kind: "in"` the tip points into the block; for
/// `kind: "out"` it points away.
///
/// - side (str): One of `"north"`, `"east"`, `"south"`, `"west"`
/// - kind (str): `"in"` or `"out"`
/// - u (length,number): The pentagon unit size
/// -> dictionary: `(vertices: (..5 pairs..), outer: (dx, dy), inner: (dx, dy))`
#let port-shape-vertices(side, kind, u) = {
// Base case: west port, input (tip east / into block).
// Vertices as (dx, dy) offsets relative to the boundary anchor.
let base-vertices = (
(-u, -u/2),
(-u, +u/2),
(u/2, +u/2),
(u, 0), // tip east (inner side)
(u/2, -u/2),
)
// For output, mirror horizontally so the tip points west (outside).
if kind == "out" {
base-vertices = base-vertices.map(p => (-p.at(0), p.at(1)))
}
// Outer (wire-connection) and inner (inside-label) anchors for a west port.
// Outer is always 1u outside the boundary along the port normal; inner is 1u
// inside. Independent of `kind` (only the pentagon fill/tip direction change).
let base-outer = (-u, 0)
let base-inner = (u, 0)
// Rotation from west's frame to target side:
// west: identity (x, y)
// north: (x, y) → (y, -x)
// east: (x, y) → (-x, -y)
// south: (x, y) → (-y, x)
let rot = if side == "west" { p => p }
else if side == "north" { p => (p.at(1), -p.at(0)) }
else if side == "east" { p => (-p.at(0), -p.at(1)) }
else if side == "south" { p => (-p.at(1), p.at(0)) }
else { panic("Unknown side: " + side) }
return (
vertices: base-vertices.map(rot),
outer: rot(base-outer),
inner: rot(base-inner),
)
}
#let valid-anchors = (
"center", "north", "east", "west", "south",
"north-east", "north-west", "south-east", "south-west"
)
#let lerp(pt0, ratio, pt1) = {
return (
(pt1.at(0) - pt0.at(0)) * ratio / 100% + pt0.at(0),
(pt1.at(1) - pt0.at(1)) * ratio / 100% + pt0.at(1)
)
/// 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
}
+393 -86
View File
@@ -1,11 +1,12 @@
#import "/src/cetz.typ": draw, coordinate
#import "util.typ": opposite-anchor
#import "elements/ports.typ": get-port-side
#import "elements/element.typ"
#import "util.typ": opposite-anchor, normalize-coord, resolve-rel-coord
/// List of valid wire styles
/// #examples.wires
#let wire-styles = ("direct", "zigzag", "dodge")
///
/// The `"u-shape"` style connects two ports that face the same cardinal side.
/// #examples.u-shape-wire
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape")
#let signal-width = 1pt
#let bus-width = 1.5pt
@@ -70,8 +71,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
@@ -88,13 +89,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
@@ -111,6 +112,182 @@
return (points, anchors)
}
#let _u-valid-sides = ("east", "west", "north", "south")
// Try to deduce the cardinal side a port anchor faces by comparing its
// position to the center of the element it belongs to. Returns none if the
// anchor is not a port-style anchor (i.e. does not contain "-port-") or if
// resolution fails.
#let _u-detect-side(anchor, ctx) = {
if type(anchor) != str { return none }
let sep = "-port-"
if not anchor.contains(sep) { return none }
let elmt-id = anchor.split(sep).first()
let port-pos = none
let center-pos = none
// coordinate.resolve panics on unknown anchors; there is no try/catch in
// Typst, so we assume port anchors created by add-port always come with a
// matching group of the same name. If not, the user must set u-side
// explicitly.
let (_, p1) = coordinate.resolve(ctx, anchor)
let (_, p2) = coordinate.resolve(ctx, elmt-id + ".center")
port-pos = p1
center-pos = p2
let dx = port-pos.at(0) - center-pos.at(0)
let dy = port-pos.at(1) - center-pos.at(1)
if calc.abs(dx) >= calc.abs(dy) {
return if dx >= 0 { "east" } else { "west" }
} else {
return if dy >= 0 { "north" } else { "south" }
}
}
#let get-u-shape-wire(pts, u-depth, u-side, ctx) = {
let start = pts.first()
let end = pts.last()
// Resolve the (auto|explicit) side
let side = u-side
if side == auto {
let side-start = _u-detect-side(start, ctx)
let side-end = _u-detect-side(end, ctx)
if side-start != none and side-end != none and side-start != side-end {
panic(
"u-shape: start port faces '" + side-start
+ "' but end port faces '" + side-end
+ "'; both must face the same side, or set u-side explicitly"
)
}
if side-start != none { side = side-start }
else if side-end != none { side = side-end }
else {
panic(
"u-shape: could not auto-detect u-side (endpoints are not port anchors); "
+ "please set u-side explicitly to one of " + repr(_u-valid-sides)
)
}
}
if not side in _u-valid-sides {
panic("u-shape: invalid u-side '" + repr(side) + "'; must be one of " + repr(_u-valid-sides))
}
let (ctx, s) = coordinate.resolve(ctx, start)
let (ctx, e) = coordinate.resolve(ctx, end)
let (sx, sy, ..) = s
let (ex, ey, ..) = e
let p1
let p2
if side == "east" {
let bx = calc.max(sx, ex) + u-depth
p1 = (bx, sy, 0)
p2 = (bx, ey, 0)
} else if side == "west" {
let bx = calc.min(sx, ex) - u-depth
p1 = (bx, sy, 0)
p2 = (bx, ey, 0)
} else if side == "north" {
let by = calc.max(sy, ey) + u-depth
p1 = (sx, by, 0)
p2 = (ex, by, 0)
} else if side == "south" {
let by = calc.min(sy, ey) - u-depth
p1 = (sx, by, 0)
p2 = (ex, by, 0)
}
let points = (start, p1, p2, end)
let anchors = (
"start": start,
"u-start": p1,
"u-end": p2,
"end": end
)
return (points, anchors)
}
#let get-guided-wire(pts, margins, sides, center-guides, ctx) = {
let start = pts.first()
let end = pts.last()
let (margin-start, margin-end) = margins
let (side-start, side-end) = sides
let (center_horizontal, center_vertical) = center-guides
let (ctx, p0) = coordinate.resolve(ctx, start)
let (ctx, p6) = coordinate.resolve(ctx, end)
p0 = (x: p0.first(), y: p0.at(1))
p6 = (x: p6.first(), y: p6.at(1))
let box_width = calc.abs(p6.x - p0.x)
let box_height = calc.abs(p6.y - p0.y)
// finding correct dx and dy
let dx1 = box_width * margin-start / 100%
if side-start == "west" {
dx1 *= -1
} else if side-start == "north" or side-start == "south" { dx1 = 0}
let dx2 = box_width * margin-end / 100%
if side-end == "west" {
dx2 *= -1
} else if side-end == "north" or side-end == "south" { dx2 = 0}
let dy1 = box_height * margin-start / 100%
if side-start == "south" {
dy1 *= -1
} else if side-start == "west" or side-start == "east" { dy1 = 0}
let dy2 = box_height * margin-end / 100%
if side-end == "south" {
dy2 *= -1
} else if side-end == "west" or side-end == "east" { dy2 = 0}
// points that are closest to the edge points
let p1 = (p0.x + dx1, p0.y + dy1, 0)
let p5 = (p6.x + dx2, p6.y + dy2, 0)
// middle point
let center_x = p0.x + box_width * center_horizontal / 100%
let center_y = p0.y + box_height * center_vertical / 100%
let p3 = (center_x, center_y, 0)
// setting up the points for that touch the guides
let p2 = (0, 0, 0)
let p4 = (0, 0, 0)
if side-start in ("north", "south") {
p2 = (horizontal: p3, vertical: p1)
} else {
p2 = (horizontal: p1, vertical: p3)
}
if side-end in ("north", "south") {
p4 = (horizontal: p3, vertical: p5)
} else if side-end in ("east", "west") {
p4 = (horizontal: p5, vertical: p3)
}
// returning
let points = (
start,
p1,
p2,
p3,
p4,
p5,
end
)
let anchors = (
"start": start,
"start2": points.at(1),
"guide-start": points.at(2),
"center": points.at(3),
"guide-end": points.at(4),
"end2": points.at(5),
"end": end
)
return (points, anchors)
}
/// Draws a wire between two points
/// - id (str): The wire's id, for future reference (anchors)
/// - pts (array): The two points (as CeTZ compatible coordinates, i.e. XY, relative positions, ids, etc.)
@@ -129,17 +306,24 @@
/// - dodge-y (number): Y position to dodge the wire to (only with style "dodge")
/// - dodge-sides (array): The start and end sides (going out of the connected element) of the wire (only with style "dodge")
/// - dodge-margins (array): The start and end margins (i.e. space before dodging) of the wire (only with style "dodge")
/// - guided-center (array): the horizontal and vertical guides of the center guides (only with style "guided")
/// - guided-margins (array): the start and end of guided margins of the wire (only with style "guided")
/// - guided-sides (array): the side of start and end array (must be either "north", "south", "west", "east") (only work with style "guided")
/// - u-side (auto, str): The cardinal side both endpoints face (one of "east", "west", "north", "south"). If `auto`, it is deduced from the endpoints when they are port anchors of the form `<elmt-id>-port-<port-id>` by comparing each port's position with its element's `.center` anchor. Auto-detection panics if the two endpoints resolve to different sides, or if neither endpoint is a port anchor (only with style "u-shape")
/// - u-depth (number): Distance between the outermost endpoint (in the `u-side` direction) and the U-bend line (only with style "u-shape")
#let wire(
pt0,
pt1,
id: none,
id, pts,
bus: false,
name: none,
name-pos: "middle",
slice: none,
color: black,
dashed: false,
style: "direct",
reverse: false,
guided-center: (50%, 50%),
guided-margins: (5%, 5%),
guided-sides: ("east", "west"),
directed: false,
rotate-name: true,
zigzag-ratio: 50%,
@@ -147,13 +331,19 @@
dodge-y: 0,
dodge-sides: ("east", "west"),
dodge-margins: (5%, 5%),
..args
u-side: auto,
u-depth: 0.5,
) = draw.get-ctx(ctx => {
if not style in wire-styles {
panic("Invalid wire style '" + style + "'")
}
let pts = (pt0, pt1)
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,
@@ -180,6 +370,21 @@
dodge-sides,
ctx
)
} else if style == "guided" {
(points, anchors) = get-guided-wire(
pts,
guided-margins,
guided-sides,
guided-center,
ctx
)
} else if style == "u-shape" {
(points, anchors) = get-u-shape-wire(
pts,
u-depth,
u-side,
ctx
)
}
let mark = (fill: color)
@@ -218,7 +423,7 @@
let names = ()
if type(name) == str {
names = ((name, "middle"),)
names = ((name, name-pos),)
} else if type(name) == array {
names = (
@@ -228,9 +433,6 @@
}
for (name, pos) in names {
if name == none {
continue
}
let point
let anchor
@@ -272,73 +474,178 @@
/// - vertical (bool): Whether the name should be displayed vertically
/// - length (number): The length of the stub
/// - name-offset (number): The name offset, perpendicular to the stub
#let stub(anchor, side: auto, name: none, vertical: false, length: 1em, name-offset: 0) = {
if "." not in anchor {
panic("`anchor` must be a valid anchor of an element")
}
let parts = anchor.split(".")
let port-id = parts.last()
let port-elmt-id = parts.slice(0, -1).join(".")
#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0) = {
let end-offset = (
north: (0, length),
east: (length, 0),
south: (0, -length),
west: (-length, 0)
).at(side)
let name-offset = (
north: (name-offset, length),
east: (length, name-offset),
south: (name-offset, -length),
west: (-length, name-offset)
).at(side)
let pre-process = (elements, elmt) => {
let eid = elmt.id
let side = side
if side == auto {
if port-elmt-id not in elements {
panic("Unknown element " + port-elmt-id)
}
let port-elmt = elements.at(port-elmt-id)
side = get-port-side(port-elmt, port-id)
}
elements.at(eid).insert("side", side)
return elements
}
let draw-func(elmt, bounds) = {
let side = elmt.side
let end-offset = (
north: (0, length),
east: (length, 0),
south: (0, -length),
west: (-length, 0)
).at(side)
let name-offset = (
north: (name-offset, length),
east: (length, name-offset),
south: (name-offset, -length),
west: (-length, name-offset)
).at(side)
let shapes = ()
shapes += draw.line(
anchor,
(rel: end-offset, to: anchor)
)
if name != none {
let text-anchor = if vertical {
(
"north": "west",
"south": "east",
"west": "south",
"east": "north"
).at(side)
} else { opposite-anchor(side) }
shapes += draw.content(
anchor: text-anchor,
padding: 0.2em,
angle: if vertical {90deg} else {0deg},
(rel: name-offset, to: anchor),
name
)
}
return (shapes, bounds)
}
return element.elmt(
draw-shape: draw-func,
pre-process: pre-process
draw.line(
port-id,
(rel: end-offset, to: port-id)
)
}
if name != none {
let text-anchor = if vertical {
(
"north": "west",
"south": "east",
"west": "south",
"east": "north"
).at(side)
} else { opposite-anchor(side) }
draw.content(
anchor: text-anchor,
padding: 0.2em,
angle: if vertical {90deg} else {0deg},
(rel: name-offset, to: port-id),
name
)
}
}
/// Draws a wire breakout: a 45° diagonal line starting at (x, y) and
/// ending 1 unit away in one of the four diagonal directions. The end
/// point exposes a port anchor named `<id>-port-<id>` so wires can attach
/// to it as if it were a block port. A synthesized `<id>.center` anchor
/// is placed opposite `port-side` so #doc-ref("wire.wire") with
/// `style: "u-shape"` can auto-detect the port side.
///
/// #examples.breakout
///
/// Label positions are anchored so that the label sits neatly next to the
/// end point of the breakout. When the label direction matches the
/// breakout direction (which would overlap the line) the label is
/// automatically placed at the start point instead so the text does not
/// cover the line.
///
/// #examples.breakout-label
///
/// The port anchor makes it easy to fan out a bus:
///
/// #examples.breakout-wire
///
/// - id (str): The breakout id (also used as the port id)
/// - x (number, dictionary): The starting x position (same forms as `element.block`)
/// - y (number, dictionary): The starting y position (same forms as `element.block`)
/// - direction (str): One of `"north-east"`, `"north-west"`, `"south-east"`, `"south-west"`
/// - length (length, number): The diagonal length (default `1em`, matches port size)
/// - port-side (str): Cardinal side used for wire-attach behavior (`"north"`, `"east"`, `"south"`, `"west"`)
/// - stroke (stroke): Line stroke (paint + thickness)
/// - name (none, str): Optional label displayed near the breakout
/// - name-pos (none, str): Label position, one of `"north-west"`, `"north-east"`, `"south-west"`, `"south-east"`
/// - debug (bool): If true, shows a red dot at the port anchor
#let breakout(
id,
x: none, y: none,
direction: "north-east",
length: 1em,
port-side: "east",
stroke: black + 1pt,
name: none,
name-pos: none,
debug: false,
) = draw.get-ctx(ctx => {
if x == none { panic("breakout: parameter x must be set") }
if y == none { panic("breakout: parameter y must be set") }
let valid-directions = ("north-east", "north-west", "south-east", "south-west")
if not direction in valid-directions {
panic("breakout: invalid direction '" + repr(direction)
+ "'; must be one of " + repr(valid-directions))
}
let valid-sides = ("north", "east", "south", "west")
if not port-side in valid-sides {
panic("breakout: invalid port-side '" + repr(port-side)
+ "'; must be one of " + repr(valid-sides))
}
// Diagonal component: length / sqrt(2). Resolve to a canvas-space
// number so we can do arithmetic with anchor coordinates.
let d-raw = length / calc.sqrt(2)
let (_, d-vec) = coordinate.resolve(ctx, (d-raw, 0, 0))
let d = d-vec.at(0)
// Direction sign vector for (dx, dy).
let sx = if direction.ends-with("east") { 1 } else { -1 }
let sy = if direction.starts-with("north") { 1 } else { -1 }
// Resolve x.
let sx0 = x
if type(x) == dictionary {
if "rel" in x {
sx0 = resolve-rel-coord(ctx, x, 0)
} else if "from" in x {
let (_, from-pos) = coordinate.resolve(ctx, x.from)
sx0 = from-pos.at(0)
} else {
panic("breakout: unsupported x dictionary: " + repr(x))
}
}
// Resolve y.
let sy0 = y
if type(y) == dictionary {
if "rel" in y {
sy0 = resolve-rel-coord(ctx, y, 1)
} else if "from" in y {
let (_, from-pos) = coordinate.resolve(ctx, y.from)
sy0 = from-pos.at(1)
} else {
panic("breakout: unsupported y dictionary: " + repr(y))
}
}
let start = (sx0, sy0)
let end = (rel: (sx * d, sy * d), to: start)
// Offset used for the synthesized center anchor: opposite of port-side.
let center-offset = (
"north": (0, -1em),
"east": (-1em, 0),
"south": (0, 1em),
"west": (1em, 0),
).at(port-side)
let port-anchor-name = id + "-port-" + id
draw.group(name: id, {
draw.line(start, end, stroke: stroke)
draw.anchor("start", start)
draw.anchor("end", end)
draw.anchor("center", (rel: center-offset, to: end))
if name != none and name-pos != none {
let label-anchor = (
"north-west": "south-east",
"north-east": "south-west",
"south-west": "north-east",
"south-east": "north-west",
).at(name-pos)
// Overlap case: label direction is opposite to breakout direction,
// so the label at the end point would extend back over the line.
// Anchor it at the start point instead.
let label-pos = if name-pos == opposite-anchor(direction) { start } else { end }
draw.content(label-pos, anchor: label-anchor, padding: 3pt, name)
}
})
// Port anchor emitted at top level so it can be referenced by its
// bare name (mirrors add-port's approach in ports.typ).
if debug {
draw.circle(end, name: port-anchor-name,
radius: .1, stroke: none, fill: red)
} else {
draw.hide(draw.circle(end, name: port-anchor-name,
radius: 0, stroke: none))
}
})
+3 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "circuiteria"
version = "0.2.0"
compiler = "0.13.0"
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" ]