Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19a48951df | ||
|
|
1f247c2efc | ||
|
|
beda61b9f4 | ||
|
|
2c6e5f44df | ||
|
|
df4c956a0b | ||
|
|
ac087087c5 | ||
|
|
77eb4fa946 | ||
|
|
c9aa81af2e | ||
|
|
6871053810 | ||
|
|
73b7348eed
|
||
|
|
b71e822bb8
|
||
|
|
4c1025b1eb
|
||
|
|
b35c04a7d9
|
||
|
|
0612e9b6ec
|
No files matched your search
@@ -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.
|
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">
|
<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>
|
</p>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -11,17 +11,17 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a href="./gallery/test.typ">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">A bit of eveything</td>
|
<td colspan="2">A bit of everything</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a href="./gallery/test5.typ">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -31,12 +31,12 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="./gallery/test4.typ">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="./gallery/test6.typ">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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:
|
To use this package, simply import [circuiteria](https://typst.app/universe/package/circuiteria) and call the `circuit` function:
|
||||||
```typ
|
```typ
|
||||||
#import "@preview/circuiteria:0.2.0"
|
#import "@preview/circuiteria:0.2.1"
|
||||||
#circuiteria.circuit({
|
#circuiteria.circuit({
|
||||||
import circuiteria: *
|
import circuiteria: *
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ for i in range(3) {
|
|||||||
```)
|
```)
|
||||||
|
|
||||||
#let wires = example(```
|
#let wires = example(```
|
||||||
for i in range(4) {
|
for i in range(5) {
|
||||||
draw.circle((i * 3, 0), radius: .1, name: "p" + str(i * 2))
|
draw.circle((i * 3, 0), radius: .1, name: "p" + str(i * 2))
|
||||||
draw.circle((i * 3 + 2, 1), radius: .1, name: "p" + str(i * 2 + 1))
|
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)))
|
draw.content((i * 3 + 1, -1), raw(wire.wire-styles.at(i)))
|
||||||
@@ -68,9 +68,213 @@ wire.wire("w3", ("p4", "p5"), style: "dodge",
|
|||||||
wire.wire("w4", ("p6","p7"), style: "guided",
|
wire.wire("w4", ("p6","p7"), style: "guided",
|
||||||
guided-center:(20%, 40%), guided-margins: (90%,87%),
|
guided-center:(20%, 40%), guided-margins: (90%,87%),
|
||||||
guided-sides: ("north","south"))
|
guided-sides: ("north","south"))
|
||||||
|
wire.wire("w5", ("p8", "p9"), style: "u-shape",
|
||||||
|
u-side: "east", u-depth: 0.5)
|
||||||
```, vertical: true)
|
```, vertical: true)
|
||||||
|
|
||||||
|
#let zigzag-at = example(
|
||||||
|
```
|
||||||
|
// `zigzag-at` places the middle bend of a zigzag wire
|
||||||
|
// on the axis of a referenced anchor, overriding
|
||||||
|
// `zigzag-ratio`.
|
||||||
|
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "src",
|
||||||
|
ports: (east: ((id: "o", name: "O"),),))
|
||||||
|
element.block(x: 3, y: -1.2, w: 1, h: 1, id: "ref",
|
||||||
|
fill: util.colors.pink,
|
||||||
|
ports: (west: ((id: "p", name: "R"),),))
|
||||||
|
element.block(x: 5.5, y: -2, w: 1.5, h: 1.5, id: "dst",
|
||||||
|
ports: (west: ((id: "i", name: "I"),),))
|
||||||
|
// String form: anchor name of a port.
|
||||||
|
wire.wire("w1", ("src-port-o", "dst-port-i"),
|
||||||
|
style: "zigzag",
|
||||||
|
zigzag-at: "ref-port-p")
|
||||||
|
// Ergonomic offset: shift 8pt right of ref.east.
|
||||||
|
wire.wire("w2", ("src-port-o", "dst-port-i"),
|
||||||
|
style: "zigzag",
|
||||||
|
zigzag-at: (rel: 8pt, to: "ref.east"))
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let u-shape-wire = example(
|
||||||
|
```
|
||||||
|
// east <-> east (u-side auto-detected)
|
||||||
|
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (east: ((id: "out"),)))
|
||||||
|
element.block(x: 0.5, y: -2.5, w: 1.5, h: 1.5, id: "eB",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (east: ((id: "out"),)))
|
||||||
|
wire.wire("uEE", ("eA-port-out", "eB-port-out"),
|
||||||
|
style: "u-shape", u-depth: 0.5)
|
||||||
|
|
||||||
|
// west <-> west (auto-detected)
|
||||||
|
element.block(x: 6, y: 0, w: 1.5, h: 1.5, id: "wA",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (west: ((id: "in"),)))
|
||||||
|
element.block(x: 6.5, y: -2.5, w: 1.5, h: 1.5, id: "wB",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (west: ((id: "in"),)))
|
||||||
|
wire.wire("uWW", ("wA-port-in", "wB-port-in"),
|
||||||
|
style: "u-shape", u-depth: 0.5)
|
||||||
|
|
||||||
|
// north <-> north (auto-detected)
|
||||||
|
element.block(x: 10, y: -3, w: 1.5, h: 1.5, id: "nA",
|
||||||
|
fill: util.colors.green,
|
||||||
|
ports: (north: ((id: "out"),)))
|
||||||
|
element.block(x: 12.5, y: -2.5, w: 1.5, h: 1.5, id: "nB",
|
||||||
|
fill: util.colors.green,
|
||||||
|
ports: (north: ((id: "out"),)))
|
||||||
|
wire.wire("uNN", ("nA-port-out", "nB-port-out"),
|
||||||
|
style: "u-shape", u-depth: 0.5)
|
||||||
|
|
||||||
|
// south <-> south with explicit u-side override
|
||||||
|
element.block(x: 16, y: -3, w: 1.5, h: 1.5, id: "sA",
|
||||||
|
fill: util.colors.pink,
|
||||||
|
ports: (south: ((id: "out"),)))
|
||||||
|
element.block(x: 18.5, y: -3.5, w: 1.5, h: 1.5, id: "sB",
|
||||||
|
fill: util.colors.pink,
|
||||||
|
ports: (south: ((id: "out"),)))
|
||||||
|
wire.wire("uSS", ("sA-port-out", "sB-port-out"),
|
||||||
|
style: "u-shape", u-side: "south", u-depth: 0.5)
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let u-at = example(
|
||||||
|
```
|
||||||
|
// `u-at` places the U-bend crossbar on the axis
|
||||||
|
// of a referenced anchor, overriding `u-depth`.
|
||||||
|
element.block(x: 0, y: 0, w: 1.5, h: 1.5, id: "eA",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (east: ((id: "o1"), (id: "o2"))))
|
||||||
|
element.block(x: 0, y: -3.5, w: 1.5, h: 1.5, id: "eB",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (east: ((id: "i1"), (id: "i2"))))
|
||||||
|
element.block(x: 4, y: -2.25, w: 0.6, h: 0.6, id: "ref",
|
||||||
|
fill: util.colors.pink)
|
||||||
|
// Align one U crossbar with ref.east.
|
||||||
|
wire.wire("u1", ("eA-port-o1", "eB-port-i1"),
|
||||||
|
style: "u-shape",
|
||||||
|
u-at: "ref.east")
|
||||||
|
// Ergonomic scalar offset: 1 right of ref.east.
|
||||||
|
wire.wire("u2", ("eA-port-o2", "eB-port-i2"),
|
||||||
|
style: "u-shape", color: util.colors.blue,
|
||||||
|
u-at: (rel: 1, to: "ref.east"))
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let s-shape-wire = example(
|
||||||
|
```
|
||||||
|
// `s-shape` connects endpoints on opposite sides
|
||||||
|
// via 5 segments joined at 4 corners (u1-start,
|
||||||
|
// u1-end, u2-start, u2-end). Auto-detects axis
|
||||||
|
// from port anchors.
|
||||||
|
element.block(x: 0, y: 0, w: 1, h: 1, id: "A",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (west: ((id: "out"),)))
|
||||||
|
element.block(x: 2.5, y: -1.75, w: 1, h: 1, id: "B",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (east: ((id: "in"),)))
|
||||||
|
wire.wire("s1", ("A-port-out", "B-port-in"),
|
||||||
|
style: "s-shape")
|
||||||
|
|
||||||
|
// Anchor-based override of the crossbar position.
|
||||||
|
element.block(x: 5.5, y: 0, w: 1, h: 1, id: "C",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (west: ((id: "out"),)))
|
||||||
|
element.block(x: 8.5, y: -1.75, w: 1, h: 1, id: "D",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (east: ((id: "in"),)))
|
||||||
|
element.block(x: 7, y: -0.7, w: 0.5, h: 0.5, id: "ref",
|
||||||
|
fill: util.colors.pink)
|
||||||
|
wire.wire("s2", ("C-port-out", "D-port-in"),
|
||||||
|
style: "s-shape",
|
||||||
|
s-at-mid: "ref.south")
|
||||||
|
|
||||||
|
// C-shape degeneration: crossbar outside the
|
||||||
|
// endpoint span (s-ratio-mid > 100%).
|
||||||
|
element.block(x: 11, y: 0, w: 1, h: 1, id: "E",
|
||||||
|
fill: util.colors.green,
|
||||||
|
ports: (west: ((id: "out"),)))
|
||||||
|
element.block(x: 14, y: -0.75, w: 1, h: 1, id: "F",
|
||||||
|
fill: util.colors.green,
|
||||||
|
ports: (east: ((id: "in"),)))
|
||||||
|
wire.wire("s3", ("E-port-out", "F-port-in"),
|
||||||
|
style: "s-shape",
|
||||||
|
s-ratio-mid: 200%)
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let s-shape-wire-vertical = example(
|
||||||
|
```
|
||||||
|
// `s-shape` on the vertical axis (north <-> south):
|
||||||
|
// seg1 and seg5 are vertical, the crossbar is
|
||||||
|
// vertical, and the two horizontals connect them.
|
||||||
|
element.block(x: 0, y: 0, w: 1, h: 1, id: "A",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (north: ((id: "out"),)))
|
||||||
|
element.block(x: 2, y: -3, w: 1, h: 1, id: "B",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (south: ((id: "in"),)))
|
||||||
|
wire.wire("s1", ("A-port-out", "B-port-in"),
|
||||||
|
style: "s-shape")
|
||||||
|
|
||||||
|
// Anchor-based override of the vertical crossbar.
|
||||||
|
element.block(x: 5, y: 0, w: 1, h: 1, id: "C",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (north: ((id: "out"),)))
|
||||||
|
element.block(x: 7, y: -3, w: 1, h: 1, id: "D",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (south: ((id: "in"),)))
|
||||||
|
element.block(x: 6.25, y: -1.5, w: 0.5, h: 0.5, id: "vref",
|
||||||
|
fill: util.colors.pink)
|
||||||
|
wire.wire("s2", ("C-port-out", "D-port-in"),
|
||||||
|
style: "s-shape",
|
||||||
|
s-at-mid: "vref.east")
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let s-shape-wire-loopback = example(
|
||||||
|
```
|
||||||
|
// `s-shape` looping back on a single block that
|
||||||
|
// has a west output and an east input. The wire
|
||||||
|
// exits west, wraps around below the block via a
|
||||||
|
// C-shape, and enters from the east.
|
||||||
|
//
|
||||||
|
// Since both endpoints share the same y, the
|
||||||
|
// crossbar's position is set by `s-ratio-mid`
|
||||||
|
// as an absolute distance below the endpoint y.
|
||||||
|
element.block(x: 2, y: 0, w: 1.5, h: 1.5, id: "A",
|
||||||
|
fill: util.colors.orange,
|
||||||
|
ports: (
|
||||||
|
west: ((id: "out"),),
|
||||||
|
east: ((id: "in"),),
|
||||||
|
))
|
||||||
|
wire.wire("s1", ("A-port-out", "A-port-in"),
|
||||||
|
style: "s-shape",
|
||||||
|
s-depth-1: 1.5, s-depth-2: 1.5,
|
||||||
|
s-ratio-mid: 150%)
|
||||||
|
|
||||||
|
// Negative `s-ratio-mid` routes the crossbar the
|
||||||
|
// other way (above the block).
|
||||||
|
element.block(x: 7, y: 0, w: 1.5, h: 1.5, id: "B",
|
||||||
|
fill: util.colors.yellow,
|
||||||
|
ports: (
|
||||||
|
west: ((id: "out"),),
|
||||||
|
east: ((id: "in"),),
|
||||||
|
))
|
||||||
|
wire.wire("s2", ("B-port-out", "B-port-in"),
|
||||||
|
style: "s-shape",
|
||||||
|
s-depth-1: 1.5, s-depth-2: 1.5,
|
||||||
|
s-ratio-mid: -150%)
|
||||||
|
```,
|
||||||
|
vertical: true,
|
||||||
|
)
|
||||||
|
|
||||||
#let stub = example(```
|
#let stub = example(```
|
||||||
draw.circle((0, 0), radius: .1, name: "p")
|
draw.circle((0, 0), radius: .1, name: "p")
|
||||||
wire.stub("p", "north", name: "north", length: 1)
|
wire.stub("p", "north", name: "north", length: 1)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,4 +1,4 @@
|
|||||||
#import "../src/lib.typ": *
|
#import "@preview/circuiteria:0.2.1": *
|
||||||
|
|
||||||
#set page(width: auto, height: auto, margin: .5cm)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 53 KiB |
@@ -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)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 53 KiB |
@@ -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)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 23 KiB |
@@ -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)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 67 KiB |
@@ -1,5 +1,5 @@
|
|||||||
#import "/src/cetz.typ": draw
|
#import "/src/cetz.typ": draw
|
||||||
#import "../src/lib.typ": *
|
#import "@preview/circuiteria:0.2.1": *
|
||||||
|
|
||||||
#set page(width: auto, height: auto, margin: .5cm)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 93 KiB |
@@ -1,5 +1,5 @@
|
|||||||
#import "/src/cetz.typ": draw
|
#import "/src/cetz.typ": draw
|
||||||
#import "../src/lib.typ": *
|
#import "@preview/circuiteria:0.2.1": *
|
||||||
|
|
||||||
#set page(width: auto, height: auto, margin: .5cm)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,5 +1,5 @@
|
|||||||
#import "/src/cetz.typ": draw
|
#import "/src/cetz.typ": draw
|
||||||
#import "../src/lib.typ": *
|
#import "@preview/circuiteria:0.2.1": *
|
||||||
|
|
||||||
#set page(width: auto, height: auto, margin: .5cm)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -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)
|
#set page(width: auto, height: auto, margin: .5cm)
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -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")
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#import "@preview/tidy:0.4.1"
|
#import "@preview/tidy:0.4.3"
|
||||||
#import "/src/cetz.typ": draw, canvas
|
#import "/src/cetz.typ": draw, canvas
|
||||||
#import "src/lib.typ"
|
#import "src/lib.typ"
|
||||||
#import "doc/examples.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:
|
Simply import Circuiteria and call the `circuit` function:
|
||||||
#pad(left: 1em)[```typ
|
#pad(left: 1em)[```typ
|
||||||
#import "@preview/circuiteria:0.2.0"
|
#import "@preview/circuiteria:0.2.1"
|
||||||
#circuiteria.circuit({
|
#circuiteria.circuit({
|
||||||
import circuiteria: *
|
import circuiteria: *
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#import "@preview/cetz:0.3.4": *
|
#import "@preview/cetz:0.5.2": *
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#import "/src/cetz.typ": canvas
|
#import "/src/cetz.typ": canvas
|
||||||
#import "@preview/tidy:0.3.0"
|
#import "@preview/tidy:0.4.3"
|
||||||
|
|
||||||
/// Draws a block circuit diagram
|
/// Draws a block circuit diagram
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#let version = version(0, 2, 0)
|
#let version = version(0, 2, 1)
|
||||||
#let name = "circuiteria"
|
#let name = "circuiteria"
|
||||||
|
|
||||||
#import "circuit.typ": circuit
|
#import "circuit.typ": circuit
|
||||||
|
|||||||
@@ -74,3 +74,44 @@
|
|||||||
"center", "north", "east", "west", "south",
|
"center", "north", "east", "west", "south",
|
||||||
"north-east", "north-west", "south-east", "south-west"
|
"north-east", "north-west", "south-east", "south-west"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/// Normalizes a coordinate so that 2D points (x, y) are padded
|
||||||
|
/// to (x, y, 0) as required by CeTZ's coordinate resolver.
|
||||||
|
/// Also handles function-based and dictionary-based coordinates
|
||||||
|
/// recursively.
|
||||||
|
///
|
||||||
|
/// - coord (any): A CeTZ-compatible coordinate
|
||||||
|
/// -> any
|
||||||
|
#let normalize-coord(coord) = {
|
||||||
|
if type(coord) == array {
|
||||||
|
if coord.len() == 2 {
|
||||||
|
let (a, b) = coord
|
||||||
|
// Raw 2D point: (x, y) -> (x, y, 0)
|
||||||
|
if type(a) in (int, float) and type(b) in (int, float) {
|
||||||
|
return (a, b, 0)
|
||||||
|
}
|
||||||
|
// Function-based coordinate: (fn, point)
|
||||||
|
if type(a) == function {
|
||||||
|
let orig = a
|
||||||
|
return (v => {
|
||||||
|
let result = orig(v)
|
||||||
|
if type(result) == array and result.len() == 2 {
|
||||||
|
let (rx, ry) = result
|
||||||
|
if type(rx) in (int, float) and type(ry) in (int, float) {
|
||||||
|
return (rx, ry, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if type(coord) == dictionary {
|
||||||
|
let result = (:)
|
||||||
|
for (key, value) in coord {
|
||||||
|
result.insert(key, normalize-coord(value))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return coord
|
||||||
|
}
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
#import "/src/cetz.typ": draw, coordinate
|
#import "/src/cetz.typ": draw, coordinate
|
||||||
#import "util.typ": opposite-anchor
|
#import "util.typ": opposite-anchor, normalize-coord
|
||||||
|
|
||||||
/// List of valid wire styles
|
/// List of valid wire styles
|
||||||
/// #examples.wires
|
/// #examples.wires
|
||||||
#let wire-styles = ("direct", "zigzag", "dodge", "guided")
|
///
|
||||||
|
/// The `"u-shape"` style connects two ports that face the same cardinal side.
|
||||||
|
/// #examples.u-shape-wire
|
||||||
|
///
|
||||||
|
/// The `"u-shape"` crossbar can also be aligned to an anchor via `u-at`
|
||||||
|
/// (analogous to `zigzag-at`).
|
||||||
|
/// #examples.u-at
|
||||||
|
///
|
||||||
|
/// The `"s-shape"` style connects two ports that face opposite cardinal sides
|
||||||
|
/// (east↔west or north↔south) via 5 segments.
|
||||||
|
/// #examples.s-shape-wire
|
||||||
|
///
|
||||||
|
/// The `"s-shape"` style also supports the vertical axis (north↔south):
|
||||||
|
/// #examples.s-shape-wire-vertical
|
||||||
|
///
|
||||||
|
/// The `"s-shape"` style can also loop a single block's output back to its
|
||||||
|
/// own input by wrapping around above (or below) via a C-shape:
|
||||||
|
/// #examples.s-shape-wire-loopback
|
||||||
|
#let wire-styles = ("direct", "zigzag", "dodge", "guided", "u-shape", "s-shape")
|
||||||
#let signal-width = 1pt
|
#let signal-width = 1pt
|
||||||
#let bus-width = 1.5pt
|
#let bus-width = 1.5pt
|
||||||
|
|
||||||
@@ -24,29 +42,95 @@
|
|||||||
return (pts, anchors)
|
return (pts, anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
#let get-zigzag-wire(pts, ratio, dir) = {
|
// Normalize an anchor-based placement value used by `zigzag-at`, `u-at`, and
|
||||||
|
// the `s-at-*` family.
|
||||||
|
//
|
||||||
|
// `axis` selects which cartesian component the offset applies to:
|
||||||
|
// - `"x"` → offset in x (used when the placement fixes an x coordinate, e.g.
|
||||||
|
// a vertical bend line for horizontal wire flow).
|
||||||
|
// - `"y"` → offset in y (used when the placement fixes a y coordinate).
|
||||||
|
//
|
||||||
|
// Accepted forms:
|
||||||
|
// - `none` → unset; caller falls back to
|
||||||
|
// depth/ratio-based placement.
|
||||||
|
// - `str` / tuple / native CeTZ dict → passed through as a CeTZ coordinate.
|
||||||
|
// - `(rel: <length>, to: <coord>)` with a *scalar* `rel`
|
||||||
|
// → ergonomic axial offset, rewritten
|
||||||
|
// to `(rel: (rel, 0), to: to)` for
|
||||||
|
// `axis == "x"` or
|
||||||
|
// `(rel: (0, rel), to: to)` for
|
||||||
|
// `axis == "y"`. If `rel` is already
|
||||||
|
// an array/tuple, the value is
|
||||||
|
// passed through unchanged.
|
||||||
|
#let normalize-scalar-offset(at, axis) = {
|
||||||
|
if at == none { return none }
|
||||||
|
if type(at) == dictionary and "rel" in at and "to" in at and type(at.rel) != array {
|
||||||
|
let r = at.rel
|
||||||
|
if axis == "x" {
|
||||||
|
return (rel: (r, 0), to: at.to)
|
||||||
|
} else {
|
||||||
|
return (rel: (0, r), to: at.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return at
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backwards-compatible wrapper: translates the old `dir` vocabulary
|
||||||
|
// (`"vertical"` / `"horizontal"`) to the `axis` vocabulary used by
|
||||||
|
// `normalize-scalar-offset`.
|
||||||
|
#let normalize-zigzag-at(at, dir) = {
|
||||||
|
let axis = if dir == "vertical" { "x" } else { "y" }
|
||||||
|
return normalize-scalar-offset(at, axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let get-zigzag-wire(pts, ratio, dir, at: none) = {
|
||||||
let start = pts.first()
|
let start = pts.first()
|
||||||
let end = pts.last()
|
let end = pts.last()
|
||||||
let mid = if dir == "vertical" {
|
|
||||||
(start, ratio, (horizontal: end, vertical: ()))
|
|
||||||
} else {
|
|
||||||
(start, ratio, (horizontal: (), vertical: end))
|
|
||||||
}
|
|
||||||
|
|
||||||
let points = if dir == "vertical" {
|
let points
|
||||||
(
|
if at == none {
|
||||||
start,
|
// Ratio-based placement (default).
|
||||||
(horizontal: mid, vertical: ()),
|
let mid = if dir == "vertical" {
|
||||||
(horizontal: (), vertical: end),
|
(start, ratio, (horizontal: end, vertical: ()))
|
||||||
end
|
} else {
|
||||||
)
|
(start, ratio, (horizontal: (), vertical: end))
|
||||||
|
}
|
||||||
|
|
||||||
|
points = if dir == "vertical" {
|
||||||
|
(
|
||||||
|
start,
|
||||||
|
(horizontal: mid, vertical: ()),
|
||||||
|
(horizontal: (), vertical: end),
|
||||||
|
end
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
start,
|
||||||
|
(horizontal: (), vertical: mid),
|
||||||
|
(horizontal: end, vertical: ()),
|
||||||
|
end
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
(
|
// Anchor-based placement: the middle bend segment sits on the axis
|
||||||
start,
|
// extracted from `at` via CeTZ's perpendicular operator `|-`:
|
||||||
(horizontal: (), vertical: mid),
|
// - vertical: middle segment at x = at.x
|
||||||
(horizontal: end, vertical: ()),
|
// - horizontal: middle segment at y = at.y
|
||||||
end
|
points = if dir == "vertical" {
|
||||||
)
|
(
|
||||||
|
start,
|
||||||
|
(at, "|-", start),
|
||||||
|
(at, "|-", end),
|
||||||
|
end
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
start,
|
||||||
|
(start, "|-", at),
|
||||||
|
(end, "|-", at),
|
||||||
|
end
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let anchors = (
|
let anchors = (
|
||||||
"start": start,
|
"start": start,
|
||||||
@@ -68,8 +152,8 @@
|
|||||||
|
|
||||||
let (ctx, p0) = coordinate.resolve(ctx, start)
|
let (ctx, p0) = coordinate.resolve(ctx, start)
|
||||||
let (ctx, p3) = coordinate.resolve(ctx, end)
|
let (ctx, p3) = coordinate.resolve(ctx, end)
|
||||||
p0 = (x: p0.first(), y: p0.last())
|
p0 = (x: p0.first(), y: p0.at(1))
|
||||||
p3 = (x: p3.first(), y: p3.last())
|
p3 = (x: p3.first(), y: p3.at(1))
|
||||||
|
|
||||||
let dx1 = margin-start
|
let dx1 = margin-start
|
||||||
let dx2 = margin-end
|
let dx2 = margin-end
|
||||||
@@ -86,13 +170,13 @@
|
|||||||
if side-end == "east" {
|
if side-end == "east" {
|
||||||
dx2 *= -1
|
dx2 *= -1
|
||||||
}
|
}
|
||||||
p1 = (p0.x + dx1, p0.y)
|
p1 = (p0.x + dx1, p0.y, 0)
|
||||||
p2 = (p3.x - dx2, p0.y)
|
p2 = (p3.x - dx2, p0.y, 0)
|
||||||
|
|
||||||
let points = (
|
let points = (
|
||||||
start,
|
start,
|
||||||
(horizontal: p1, vertical: ()),
|
(horizontal: p1, vertical: ()),
|
||||||
(horizontal: (), vertical: (0, dodge-y)),
|
(horizontal: (), vertical: (0, dodge-y, 0)),
|
||||||
(horizontal: p2, vertical: ()),
|
(horizontal: p2, vertical: ()),
|
||||||
(horizontal: (), vertical: end),
|
(horizontal: (), vertical: end),
|
||||||
end
|
end
|
||||||
@@ -109,6 +193,315 @@
|
|||||||
return (points, anchors)
|
return (points, anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#let _u-valid-sides = ("east", "west", "north", "south")
|
||||||
|
|
||||||
|
// Try to deduce the cardinal side a port anchor faces by comparing its
|
||||||
|
// position to the center of the element it belongs to. Returns none if the
|
||||||
|
// anchor is not a port-style anchor (i.e. does not contain "-port-") or if
|
||||||
|
// resolution fails.
|
||||||
|
#let detect-port-side(anchor, ctx) = {
|
||||||
|
if type(anchor) != str { return none }
|
||||||
|
let sep = "-port-"
|
||||||
|
if not anchor.contains(sep) { return none }
|
||||||
|
let elmt-id = anchor.split(sep).first()
|
||||||
|
let port-pos = none
|
||||||
|
let center-pos = none
|
||||||
|
// coordinate.resolve panics on unknown anchors; there is no try/catch in
|
||||||
|
// Typst, so we assume port anchors created by add-port always come with a
|
||||||
|
// matching group of the same name. If not, the user must set u-side
|
||||||
|
// explicitly.
|
||||||
|
let (_, p1) = coordinate.resolve(ctx, anchor)
|
||||||
|
let (_, p2) = coordinate.resolve(ctx, elmt-id + ".center")
|
||||||
|
port-pos = p1
|
||||||
|
center-pos = p2
|
||||||
|
let dx = port-pos.at(0) - center-pos.at(0)
|
||||||
|
let dy = port-pos.at(1) - center-pos.at(1)
|
||||||
|
if calc.abs(dx) >= calc.abs(dy) {
|
||||||
|
return if dx >= 0 { "east" } else { "west" }
|
||||||
|
} else {
|
||||||
|
return if dy >= 0 { "north" } else { "south" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let get-u-shape-wire(pts, u-depth, u-side, u-at, ctx) = {
|
||||||
|
let start = pts.first()
|
||||||
|
let end = pts.last()
|
||||||
|
|
||||||
|
// Resolve the (auto|explicit) side
|
||||||
|
let side = u-side
|
||||||
|
if side == auto {
|
||||||
|
let side-start = detect-port-side(start, ctx)
|
||||||
|
let side-end = detect-port-side(end, ctx)
|
||||||
|
if side-start != none and side-end != none and side-start != side-end {
|
||||||
|
panic(
|
||||||
|
"u-shape: start port faces '" + side-start
|
||||||
|
+ "' but end port faces '" + side-end
|
||||||
|
+ "'; both must face the same side, or set u-side explicitly"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if side-start != none { side = side-start }
|
||||||
|
else if side-end != none { side = side-end }
|
||||||
|
else {
|
||||||
|
panic(
|
||||||
|
"u-shape: could not auto-detect u-side (endpoints are not port anchors); "
|
||||||
|
+ "please set u-side explicitly to one of " + repr(_u-valid-sides)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not side in _u-valid-sides {
|
||||||
|
panic("u-shape: invalid u-side '" + repr(side) + "'; must be one of " + repr(_u-valid-sides))
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ctx, s) = coordinate.resolve(ctx, start)
|
||||||
|
let (ctx, e) = coordinate.resolve(ctx, end)
|
||||||
|
let (sx, sy, ..) = s
|
||||||
|
let (ex, ey, ..) = e
|
||||||
|
|
||||||
|
// If `u-at` is set, resolve its coordinate and extract the relevant axis
|
||||||
|
// component; use it as the crossbar position instead of computing from
|
||||||
|
// `u-depth`.
|
||||||
|
let bar-at = none
|
||||||
|
if u-at != none {
|
||||||
|
let axis = if side == "east" or side == "west" { "x" } else { "y" }
|
||||||
|
let at-norm = normalize-scalar-offset(u-at, axis)
|
||||||
|
let (ctx2, r) = coordinate.resolve(ctx, at-norm)
|
||||||
|
bar-at = if axis == "x" { r.first() } else { r.at(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let p1
|
||||||
|
let p2
|
||||||
|
if side == "east" {
|
||||||
|
let bx = if bar-at != none { bar-at } else { calc.max(sx, ex) + u-depth }
|
||||||
|
p1 = (bx, sy, 0)
|
||||||
|
p2 = (bx, ey, 0)
|
||||||
|
} else if side == "west" {
|
||||||
|
let bx = if bar-at != none { bar-at } else { calc.min(sx, ex) - u-depth }
|
||||||
|
p1 = (bx, sy, 0)
|
||||||
|
p2 = (bx, ey, 0)
|
||||||
|
} else if side == "north" {
|
||||||
|
let by = if bar-at != none { bar-at } else { calc.max(sy, ey) + u-depth }
|
||||||
|
p1 = (sx, by, 0)
|
||||||
|
p2 = (ex, by, 0)
|
||||||
|
} else if side == "south" {
|
||||||
|
let by = if bar-at != none { bar-at } else { calc.min(sy, ey) - u-depth }
|
||||||
|
p1 = (sx, by, 0)
|
||||||
|
p2 = (ex, by, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let points = (start, p1, p2, end)
|
||||||
|
let anchors = (
|
||||||
|
"start": start,
|
||||||
|
"u-start": p1,
|
||||||
|
"u-end": p2,
|
||||||
|
"end": end
|
||||||
|
)
|
||||||
|
return (points, anchors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid combinations of endpoint sides for an S-shape wire.
|
||||||
|
// Keyed by (sorted) pair; value is the wire axis.
|
||||||
|
#let _s-axis-from-sides(a, b) = {
|
||||||
|
if a == none or b == none { return none }
|
||||||
|
let pair = (a, b)
|
||||||
|
if ("east" in pair) and ("west" in pair) { return "horizontal" }
|
||||||
|
if ("north" in pair) and ("south" in pair) { return "vertical" }
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
|
||||||
|
// S-shape wire: 5 segments, 4 corners; connects endpoints on opposite sides.
|
||||||
|
//
|
||||||
|
// Horizontal axis (east ↔ west): sequence exits/enters through horizontal
|
||||||
|
// segments; two vertical segments join them via a horizontal crossbar.
|
||||||
|
//
|
||||||
|
// Corner layout (horizontal axis):
|
||||||
|
// start ── c1
|
||||||
|
// │
|
||||||
|
// c2 ─────────── c3
|
||||||
|
// │
|
||||||
|
// c4 ── end
|
||||||
|
//
|
||||||
|
// (or mirrored, depending on which endpoint faces east vs west, and on the
|
||||||
|
// relative y of the endpoints; when y_mid is outside the endpoint span the
|
||||||
|
// shape degenerates into a C-shape.)
|
||||||
|
#let get-s-shape-wire(
|
||||||
|
pts,
|
||||||
|
s-axis,
|
||||||
|
s-depth-1,
|
||||||
|
s-ratio-mid,
|
||||||
|
s-depth-2,
|
||||||
|
s-at-1,
|
||||||
|
s-at-mid,
|
||||||
|
s-at-2,
|
||||||
|
strict,
|
||||||
|
ctx,
|
||||||
|
) = {
|
||||||
|
let start = pts.first()
|
||||||
|
let end = pts.last()
|
||||||
|
|
||||||
|
// Detect endpoint sides
|
||||||
|
let side-start = detect-port-side(start, ctx)
|
||||||
|
let side-end = detect-port-side(end, ctx)
|
||||||
|
|
||||||
|
// Resolve (auto|explicit) axis
|
||||||
|
let axis = s-axis
|
||||||
|
if axis == auto {
|
||||||
|
let detected = _s-axis-from-sides(side-start, side-end)
|
||||||
|
if detected == none {
|
||||||
|
panic(
|
||||||
|
"s-shape: could not auto-detect s-axis; endpoints must be port anchors "
|
||||||
|
+ "facing opposite sides on the same axis (east+west or north+south). "
|
||||||
|
+ "Detected start='" + repr(side-start) + "', end='" + repr(side-end)
|
||||||
|
+ "'. Set s-axis explicitly to \"horizontal\" or \"vertical\"."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
axis = detected
|
||||||
|
}
|
||||||
|
if axis != "horizontal" and axis != "vertical" {
|
||||||
|
panic("s-shape: invalid s-axis '" + repr(axis) + "'; must be \"horizontal\" or \"vertical\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effective depth/ratio (auto → defaults)
|
||||||
|
let depth-1 = if s-depth-1 == auto { 0.5 } else { s-depth-1 }
|
||||||
|
let depth-2 = if s-depth-2 == auto { 0.5 } else { s-depth-2 }
|
||||||
|
let ratio-mid-raw = if s-ratio-mid == auto { 50% } else { s-ratio-mid }
|
||||||
|
let ratio-mid = ratio-mid-raw / 100%
|
||||||
|
|
||||||
|
let (ctx, s) = coordinate.resolve(ctx, start)
|
||||||
|
let (ctx, e) = coordinate.resolve(ctx, end)
|
||||||
|
let (sx, sy, ..) = s
|
||||||
|
let (ex, ey, ..) = e
|
||||||
|
|
||||||
|
// Determine outward directions from each port
|
||||||
|
let outward(side) = {
|
||||||
|
if side == "east" { return (1, 0) }
|
||||||
|
if side == "west" { return (-1, 0) }
|
||||||
|
if side == "north" { return (0, 1) }
|
||||||
|
if side == "south" { return (0, -1) }
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
let out-s = outward(side-start)
|
||||||
|
let out-e = outward(side-end)
|
||||||
|
// Fallback when a port side isn't known (e.g. non-port endpoint): infer from
|
||||||
|
// the axis, assuming the wire flows toward the far endpoint.
|
||||||
|
if out-s == none {
|
||||||
|
if axis == "horizontal" { out-s = (if ex > sx { 1 } else { -1 }, 0) }
|
||||||
|
else { out-s = (0, if ey > sy { 1 } else { -1 }) }
|
||||||
|
}
|
||||||
|
if out-e == none {
|
||||||
|
if axis == "horizontal" { out-e = (if sx > ex { 1 } else { -1 }, 0) }
|
||||||
|
else { out-e = (0, if sy > ey { 1 } else { -1 }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the three placement overrides
|
||||||
|
let resolve-axial(at, ax) = {
|
||||||
|
if at == none { return none }
|
||||||
|
let at-norm = normalize-scalar-offset(at, ax)
|
||||||
|
let (_, r) = coordinate.resolve(ctx, at-norm)
|
||||||
|
return if ax == "x" { r.first() } else { r.at(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let c1x
|
||||||
|
let c1y
|
||||||
|
let c2x
|
||||||
|
let c2y
|
||||||
|
let c3x
|
||||||
|
let c3y
|
||||||
|
let c4x
|
||||||
|
let c4y
|
||||||
|
|
||||||
|
if axis == "horizontal" {
|
||||||
|
// seg1 and seg5 are horizontal; the two verticals sit at x = x1 and x = x2.
|
||||||
|
let x1-at = resolve-axial(s-at-1, "x")
|
||||||
|
let x2-at = resolve-axial(s-at-2, "x")
|
||||||
|
let y-mid-at = resolve-axial(s-at-mid, "y")
|
||||||
|
|
||||||
|
let x1 = if x1-at != none { x1-at } else { sx + out-s.at(0) * depth-1 }
|
||||||
|
let x2 = if x2-at != none { x2-at } else { ex + out-e.at(0) * depth-2 }
|
||||||
|
// When the endpoints share the same y (degenerate span), the ratio-based
|
||||||
|
// formula collapses to sy. Fall back to a unit offset scaled by ratio-mid
|
||||||
|
// so the crossbar can still be pushed away from the endpoint axis
|
||||||
|
// (e.g. for single-block loopbacks). Direction defaults to "down" (-y).
|
||||||
|
let y-mid = if y-mid-at != none {
|
||||||
|
y-mid-at
|
||||||
|
} else if ey == sy {
|
||||||
|
sy - ratio-mid
|
||||||
|
} else {
|
||||||
|
sy + ratio-mid * (ey - sy)
|
||||||
|
}
|
||||||
|
|
||||||
|
c1x = x1; c1y = sy
|
||||||
|
c2x = x1; c2y = y-mid
|
||||||
|
c3x = x2; c3y = y-mid
|
||||||
|
c4x = x2; c4y = ey
|
||||||
|
|
||||||
|
// Sanity checks (strict-only)
|
||||||
|
if strict {
|
||||||
|
// seg1 must extend outward: sign(x1 - sx) should match out-s.x
|
||||||
|
if (x1 - sx) * out-s.at(0) <= 0 {
|
||||||
|
panic("s-shape: seg1 does not extend outward from start port "
|
||||||
|
+ "(x1=" + str(x1) + ", start.x=" + str(sx) + ", outward.x="
|
||||||
|
+ str(out-s.at(0)) + "). Adjust s-depth-1 or s-at-1.")
|
||||||
|
}
|
||||||
|
if (x2 - ex) * out-e.at(0) <= 0 {
|
||||||
|
panic("s-shape: seg5 does not extend outward from end port "
|
||||||
|
+ "(x2=" + str(x2) + ", end.x=" + str(ex) + ", outward.x="
|
||||||
|
+ str(out-e.at(0)) + "). Adjust s-depth-2 or s-at-2.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// vertical axis: seg1 and seg5 are vertical; the two horizontals sit at
|
||||||
|
// y = y1 and y = y2; the middle crossbar is vertical at x = x-mid.
|
||||||
|
let y1-at = resolve-axial(s-at-1, "y")
|
||||||
|
let y2-at = resolve-axial(s-at-2, "y")
|
||||||
|
let x-mid-at = resolve-axial(s-at-mid, "x")
|
||||||
|
|
||||||
|
let y1 = if y1-at != none { y1-at } else { sy + out-s.at(1) * depth-1 }
|
||||||
|
let y2 = if y2-at != none { y2-at } else { ey + out-e.at(1) * depth-2 }
|
||||||
|
// Degenerate-span fallback: see comment in the horizontal branch.
|
||||||
|
let x-mid = if x-mid-at != none {
|
||||||
|
x-mid-at
|
||||||
|
} else if ex == sx {
|
||||||
|
sx + ratio-mid
|
||||||
|
} else {
|
||||||
|
sx + ratio-mid * (ex - sx)
|
||||||
|
}
|
||||||
|
|
||||||
|
c1x = sx; c1y = y1
|
||||||
|
c2x = x-mid; c2y = y1
|
||||||
|
c3x = x-mid; c3y = y2
|
||||||
|
c4x = ex; c4y = y2
|
||||||
|
|
||||||
|
if strict {
|
||||||
|
if (y1 - sy) * out-s.at(1) <= 0 {
|
||||||
|
panic("s-shape: seg1 does not extend outward from start port "
|
||||||
|
+ "(y1=" + str(y1) + ", start.y=" + str(sy) + ", outward.y="
|
||||||
|
+ str(out-s.at(1)) + "). Adjust s-depth-1 or s-at-1.")
|
||||||
|
}
|
||||||
|
if (y2 - ey) * out-e.at(1) <= 0 {
|
||||||
|
panic("s-shape: seg5 does not extend outward from end port "
|
||||||
|
+ "(y2=" + str(y2) + ", end.y=" + str(ey) + ", outward.y="
|
||||||
|
+ str(out-e.at(1)) + "). Adjust s-depth-2 or s-at-2.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let c1 = (c1x, c1y, 0)
|
||||||
|
let c2 = (c2x, c2y, 0)
|
||||||
|
let c3 = (c3x, c3y, 0)
|
||||||
|
let c4 = (c4x, c4y, 0)
|
||||||
|
|
||||||
|
let points = (start, c1, c2, c3, c4, end)
|
||||||
|
let anchors = (
|
||||||
|
"start": start,
|
||||||
|
"u1-start": c1,
|
||||||
|
"u1-end": c2,
|
||||||
|
"u2-start": c3,
|
||||||
|
"u2-end": c4,
|
||||||
|
"end": end,
|
||||||
|
)
|
||||||
|
return (points, anchors)
|
||||||
|
}
|
||||||
|
|
||||||
#let get-guided-wire(pts, margins, sides, center-guides, ctx) = {
|
#let get-guided-wire(pts, margins, sides, center-guides, ctx) = {
|
||||||
let start = pts.first()
|
let start = pts.first()
|
||||||
let end = pts.last()
|
let end = pts.last()
|
||||||
@@ -147,18 +540,18 @@
|
|||||||
|
|
||||||
|
|
||||||
// points that are closest to the edge points
|
// points that are closest to the edge points
|
||||||
let p1 = (p0.x + dx1, p0.y + dy1)
|
let p1 = (p0.x + dx1, p0.y + dy1, 0)
|
||||||
let p5 = (p6.x + dx2, p6.y + dy2)
|
let p5 = (p6.x + dx2, p6.y + dy2, 0)
|
||||||
|
|
||||||
|
|
||||||
// middle point
|
// middle point
|
||||||
let center_x = p0.x + box_width * center_horizontal / 100%
|
let center_x = p0.x + box_width * center_horizontal / 100%
|
||||||
let center_y = p0.y + box_height * center_vertical / 100%
|
let center_y = p0.y + box_height * center_vertical / 100%
|
||||||
let p3 = (center_x, center_y)
|
let p3 = (center_x, center_y, 0)
|
||||||
|
|
||||||
// setting up the points for that touch the guides
|
// setting up the points for that touch the guides
|
||||||
let p2 = (0,0)
|
let p2 = (0, 0, 0)
|
||||||
let p4 = (0,0)
|
let p4 = (0, 0, 0)
|
||||||
if side-start in ("north", "south") {
|
if side-start in ("north", "south") {
|
||||||
p2 = (horizontal: p3, vertical: p1)
|
p2 = (horizontal: p3, vertical: p1)
|
||||||
} else {
|
} else {
|
||||||
@@ -204,14 +597,26 @@
|
|||||||
/// - reverse (bool): If true, the start and end points will be swapped (useful in cases where the start point depends on the end point, for example with perpendiculars)
|
/// - reverse (bool): If true, the start and end points will be swapped (useful in cases where the start point depends on the end point, for example with perpendiculars)
|
||||||
/// - directed (bool): If true, the wire will be directed, meaning an arrow will be drawn at the endpoint
|
/// - directed (bool): If true, the wire will be directed, meaning an arrow will be drawn at the endpoint
|
||||||
/// - rotate-name (bool): If true, the name will be rotated according to the wire's slope
|
/// - rotate-name (bool): If true, the name will be rotated according to the wire's slope
|
||||||
/// - zigzag-ratio (ratio): Position of the zigzag vertical relative to the horizontal span (only with style "zigzag")
|
/// - zigzag-ratio (auto, ratio): Position of the zigzag middle bend segment relative to the horizontal (or vertical) span, as a fraction between the two endpoints (only with style "zigzag"). Defaults to `auto`, which resolves to `50%`. Panics if combined with an explicitly set `zigzag-at`.
|
||||||
/// - zigzag-dir (str): The zigzag's direction. As either "vertical" or "horizontal" (only with dstyle "zigzag")
|
/// - zigzag-dir (auto, str): The zigzag's direction, as either "vertical" or "horizontal" (only with style "zigzag"). If `auto`, it is deduced from the endpoints when they are port anchors of the form `<elmt-id>-port-<port-id>` by comparing each port's position with its element's `.center` anchor: east/west-facing ports yield `"vertical"` (wire exits horizontally) and north/south-facing ports yield `"horizontal"`. If the direction cannot be deduced (no port anchor on either endpoint, or conflicting directions), it falls back to `"vertical"`.
|
||||||
|
/// - zigzag-at (none, str, array, dictionary): Optional. Anchor-based placement of the zigzag middle bend segment (only with style "zigzag"). When set, overrides `zigzag-ratio` (which must be left at its default `auto`). The middle segment sits on the axis extracted from the given coordinate: `x` for `zigzag-dir: "vertical"`, `y` for `"horizontal"`. Accepted forms: a raw CeTZ anchor name string (e.g. `"elmt.center"` or `"elmt-port-a"`); any native CeTZ coordinate (tuple, dict such as `(rel: (dx, dy), to: anchor)`); or the ergonomic scalar-offset form `(rel: <length>, to: <coord>)`, which is rewritten to a `(dx, 0)` offset for `"vertical"` and `(0, dy)` for `"horizontal"`. See #examples.zigzag-at
|
||||||
/// - dodge-y (number): Y position to dodge the wire to (only with style "dodge")
|
/// - dodge-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-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")
|
/// - 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-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-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")
|
/// - guided-sides (array): the side of start and end array (must be either "north", "south", "west", "east") (only work with style "guided")
|
||||||
|
/// - u-side (auto, str): The cardinal side both endpoints face (one of "east", "west", "north", "south"). If `auto`, it is deduced from the endpoints when they are port anchors of the form `<elmt-id>-port-<port-id>` by comparing each port's position with its element's `.center` anchor. Auto-detection panics if the two endpoints resolve to different sides, or if neither endpoint is a port anchor (only with style "u-shape")
|
||||||
|
/// - u-depth (auto, number): Distance between the outermost endpoint (in the `u-side` direction) and the U-bend line (only with style "u-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicitly set `u-at`.
|
||||||
|
/// - u-at (none, str, array, dictionary): Optional. Anchor-based placement of the U-bend crossbar (only with style "u-shape"). When set, overrides `u-depth` (which must be left at its default `auto`). The crossbar sits on the axis extracted from the given coordinate: `x` for `u-side` in `("east", "west")`, `y` for `("north", "south")`. Accepted forms mirror `zigzag-at`: raw CeTZ anchor name string, any native CeTZ coordinate, or the ergonomic scalar-offset form `(rel: <length>, to: <coord>)`, which is rewritten to `(dx, 0)` for east/west and `(0, dy)` for north/south.
|
||||||
|
/// - s-axis (auto, str): The s-shape wire axis, one of "horizontal" or "vertical" (only with style "s-shape"). If `auto`, it is deduced from the endpoint port anchors: opposite east/west ports yield `"horizontal"`, opposite north/south ports yield `"vertical"`. Auto-detection panics on same-side, cross-axis, or non-port endpoints.
|
||||||
|
/// - s-depth-1 (auto, number): Distance seg1 (the first outward segment) extends from the start port (only with style "s-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicit `s-at-1`.
|
||||||
|
/// - s-ratio-mid (auto, ratio): Position of the s-shape crossbar between the endpoints along the axis perpendicular to `s-axis` (only with style "s-shape"). Defaults to `auto`, which resolves to `50%`. Values outside `0%..100%` are allowed and yield a C-shape variant. When the two endpoints share the same perpendicular coordinate (e.g. a single-block loopback where both ports are at the same y), the endpoint span is zero and `s-ratio-mid` is instead interpreted as an absolute offset from that shared coordinate: the crossbar sits `s-ratio-mid` units below the endpoints for horizontal S, or `s-ratio-mid` units right for vertical S (using `1` = ratio unit). Panics if combined with an explicit `s-at-mid`.
|
||||||
|
/// - s-depth-2 (auto, number): Distance seg5 (the last inward segment) extends from the end port (only with style "s-shape"). Defaults to `auto`, which resolves to `0.5`. Panics if combined with an explicit `s-at-2`.
|
||||||
|
/// - s-at-1 (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-depth-1` (only with style "s-shape"). Fixes the x (horizontal S) or y (vertical S) of the first vertical bend line. Accepted forms mirror `zigzag-at`.
|
||||||
|
/// - s-at-mid (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-ratio-mid` (only with style "s-shape"). Fixes the y (horizontal S) or x (vertical S) of the crossbar. Accepted forms mirror `zigzag-at`.
|
||||||
|
/// - s-at-2 (none, str, array, dictionary): Optional. Anchor-based placement overriding `s-depth-2` (only with style "s-shape"). Fixes the x (horizontal S) or y (vertical S) of the second vertical bend line. Accepted forms mirror `zigzag-at`.
|
||||||
|
/// - strict (bool): If true, s-shape performs sanity checks and panics when seg1/seg5 do not extend outward from their port (only with style "s-shape"). Defaults to `false`.
|
||||||
#let wire(
|
#let wire(
|
||||||
id, pts,
|
id, pts,
|
||||||
bus: false,
|
bus: false,
|
||||||
@@ -227,11 +632,23 @@
|
|||||||
guided-sides: ("east", "west"),
|
guided-sides: ("east", "west"),
|
||||||
directed: false,
|
directed: false,
|
||||||
rotate-name: true,
|
rotate-name: true,
|
||||||
zigzag-ratio: 50%,
|
zigzag-ratio: auto,
|
||||||
zigzag-dir: "vertical",
|
zigzag-dir: auto,
|
||||||
|
zigzag-at: none,
|
||||||
dodge-y: 0,
|
dodge-y: 0,
|
||||||
dodge-sides: ("east", "west"),
|
dodge-sides: ("east", "west"),
|
||||||
dodge-margins: (5%, 5%)
|
dodge-margins: (5%, 5%),
|
||||||
|
u-side: auto,
|
||||||
|
u-depth: auto,
|
||||||
|
u-at: none,
|
||||||
|
s-axis: auto,
|
||||||
|
s-depth-1: auto,
|
||||||
|
s-ratio-mid: auto,
|
||||||
|
s-depth-2: auto,
|
||||||
|
s-at-1: none,
|
||||||
|
s-at-mid: none,
|
||||||
|
s-at-2: none,
|
||||||
|
strict: false,
|
||||||
) = draw.get-ctx(ctx => {
|
) = draw.get-ctx(ctx => {
|
||||||
if not style in wire-styles {
|
if not style in wire-styles {
|
||||||
panic("Invalid wire style '" + style + "'")
|
panic("Invalid wire style '" + style + "'")
|
||||||
@@ -241,6 +658,55 @@
|
|||||||
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
|
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `zigzag-at` validation.
|
||||||
|
if zigzag-at != none {
|
||||||
|
if style != "zigzag" {
|
||||||
|
panic("wire '" + id + "': `zigzag-at` requires style: \"zigzag\" (got \""
|
||||||
|
+ style + "\")")
|
||||||
|
}
|
||||||
|
if zigzag-ratio != auto {
|
||||||
|
panic("wire '" + id
|
||||||
|
+ "': cannot combine `zigzag-at` with an explicit `zigzag-ratio`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let zigzag-ratio-effective = if zigzag-ratio == auto { 50% } else { zigzag-ratio }
|
||||||
|
|
||||||
|
// `u-at` validation.
|
||||||
|
if u-at != none {
|
||||||
|
if style != "u-shape" {
|
||||||
|
panic("wire '" + id + "': `u-at` requires style: \"u-shape\" (got \""
|
||||||
|
+ style + "\")")
|
||||||
|
}
|
||||||
|
if u-depth != auto {
|
||||||
|
panic("wire '" + id
|
||||||
|
+ "': cannot combine `u-at` with an explicit `u-depth`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let u-depth-effective = if u-depth == auto { 0.5 } else { u-depth }
|
||||||
|
|
||||||
|
// `s-at-*` validation.
|
||||||
|
let s-params-set = (
|
||||||
|
s-axis != auto or s-depth-1 != auto or s-ratio-mid != auto
|
||||||
|
or s-depth-2 != auto or s-at-1 != none or s-at-mid != none
|
||||||
|
or s-at-2 != none
|
||||||
|
)
|
||||||
|
if s-params-set and style != "s-shape" {
|
||||||
|
panic("wire '" + id + "': `s-*` parameters require style: \"s-shape\" (got \""
|
||||||
|
+ style + "\")")
|
||||||
|
}
|
||||||
|
if s-at-1 != none and s-depth-1 != auto {
|
||||||
|
panic("wire '" + id + "': cannot combine `s-at-1` with an explicit `s-depth-1`")
|
||||||
|
}
|
||||||
|
if s-at-mid != none and s-ratio-mid != auto {
|
||||||
|
panic("wire '" + id + "': cannot combine `s-at-mid` with an explicit `s-ratio-mid`")
|
||||||
|
}
|
||||||
|
if s-at-2 != none and s-depth-2 != auto {
|
||||||
|
panic("wire '" + id + "': cannot combine `s-at-2` with an explicit `s-depth-2`")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize user-provided coordinates: (x, y) -> (x, y, 0)
|
||||||
|
let pts = pts.map(normalize-coord)
|
||||||
|
|
||||||
let stroke = (
|
let stroke = (
|
||||||
paint: color,
|
paint: color,
|
||||||
thickness: if bus {bus-width} else {signal-width}
|
thickness: if bus {bus-width} else {signal-width}
|
||||||
@@ -256,7 +722,26 @@
|
|||||||
(points, anchors) = get-direct-wire(pts)
|
(points, anchors) = get-direct-wire(pts)
|
||||||
|
|
||||||
} else if style == "zigzag" {
|
} else if style == "zigzag" {
|
||||||
(points, anchors) = get-zigzag-wire(pts, zigzag-ratio, zigzag-dir)
|
let zz-dir = zigzag-dir
|
||||||
|
if zz-dir == auto {
|
||||||
|
let side-start = detect-port-side(pts.first(), ctx)
|
||||||
|
let side-end = detect-port-side(pts.last(), ctx)
|
||||||
|
let dir-from-side(s) = if s == none { none } else if s == "east" or s == "west" { "vertical" } else { "horizontal" }
|
||||||
|
let d-start = dir-from-side(side-start)
|
||||||
|
let d-end = dir-from-side(side-end)
|
||||||
|
// Fall back to "vertical" (the previous default) when the direction
|
||||||
|
// cannot be deduced (no port anchors, or conflicting endpoints).
|
||||||
|
if d-start != none and d-end != none and d-start != d-end { zz-dir = "vertical" }
|
||||||
|
else if d-start != none { zz-dir = d-start }
|
||||||
|
else if d-end != none { zz-dir = d-end }
|
||||||
|
else { zz-dir = "vertical" }
|
||||||
|
}
|
||||||
|
(points, anchors) = get-zigzag-wire(
|
||||||
|
pts,
|
||||||
|
zigzag-ratio-effective,
|
||||||
|
zz-dir,
|
||||||
|
at: normalize-zigzag-at(zigzag-at, zz-dir),
|
||||||
|
)
|
||||||
|
|
||||||
} else if style == "dodge" {
|
} else if style == "dodge" {
|
||||||
(points, anchors) = get-dodge-wire(
|
(points, anchors) = get-dodge-wire(
|
||||||
@@ -274,9 +759,30 @@
|
|||||||
guided-center,
|
guided-center,
|
||||||
ctx
|
ctx
|
||||||
)
|
)
|
||||||
}
|
} else if style == "u-shape" {
|
||||||
|
(points, anchors) = get-u-shape-wire(
|
||||||
|
pts,
|
||||||
|
u-depth-effective,
|
||||||
|
u-side,
|
||||||
|
u-at,
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
} else if style == "s-shape" {
|
||||||
|
(points, anchors) = get-s-shape-wire(
|
||||||
|
pts,
|
||||||
|
s-axis,
|
||||||
|
s-depth-1,
|
||||||
|
s-ratio-mid,
|
||||||
|
s-depth-2,
|
||||||
|
s-at-1,
|
||||||
|
s-at-mid,
|
||||||
|
s-at-2,
|
||||||
|
strict,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let mark = (fill: color)
|
let mark = (fill: color)
|
||||||
if directed {
|
if directed {
|
||||||
mark = (end: ">", fill: color)
|
mark = (end: ">", fill: color)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "circuiteria"
|
name = "circuiteria"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
compiler = "0.13.1"
|
compiler = "0.15.0"
|
||||||
repository = "https://git.kb28.ch/HEL/circuiteria"
|
repository = "https://git.kb28.ch/HEL/circuiteria"
|
||||||
entrypoint = "src/lib.typ"
|
entrypoint = "src/lib.typ"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -9,6 +9,6 @@ authors = [
|
|||||||
]
|
]
|
||||||
categories = [ "visualization" ]
|
categories = [ "visualization" ]
|
||||||
license = "Apache-2.0"
|
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" ]
|
keywords = [ "circuit", "block", "draw" ]
|
||||||
exclude = [ "gallery", "justfile", "doc" ]
|
exclude = [ "gallery", "justfile", "doc" ]
|
||||||