Compare commits

..
Author SHA1 Message Date
David Stadelmann faa0c67bcb feat: allow element alignment based on ports for x analogue to y
Accept `(from: <anchor>, to: <port-id>)` for the `x` parameter of
elements, aligning the element's port with the anchor, analogous to the
existing form for `y`. Add a `ports-x` parameter for elements with
`auto-ports: false` and a `gallery/test_xalign` gallery entry.
2026-10-22 00:32:37 +02:00
David Stadelmann add0e2929b feat: add relative y coordinates 2026-10-22 00:28:15 +02:00
14 changed files with 7036 additions and 7195 deletions

No files matched your search

-42
View File
@@ -186,48 +186,6 @@ wire.wire("w2", ("b2-port-out", "b3-port-in2"),
style: "zigzag")
```)
#let group-ports = example(```
// outside-port-names: true (default) — labels outside the border
element.group(
id: "g1", name: "outside (default)",
stroke: (dash: "dashed"),
fill: util.colors.blue.lighten(60%),
padding: 2em,
outside-port-names: true,
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 intersection = example(```
wire.wire("w1", ((0, 0), (1, 1)), style: "zigzag")
wire.wire("w2", ((0, 0), (1, -.5)),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

-86
View File
@@ -1,86 +0,0 @@
// Demonstrates ports on a group element.
// outside-port-names: true (default) — labels outside the group border
// outside-port-names: false — labels inside (block-style)
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// --- Group with port labels 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")
})
+30
View File
@@ -0,0 +1,30 @@
// Demonstrates the `(from: <anchor>, to: <port-id>)` port-alignment shorthand
// for the `x` parameter (analogous to the existing shorthand for `y`).
#import "/src/lib.typ": circuit, element, util, wire
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
// Anchor block: absolute placement, with a single "addr" port on its
// north side (auto-centred horizontally by circuiteria).
element.block(
x: 0, y: 0, w: 2, h: 1.5,
id: "vcemem",
fill: util.colors.orange,
ports: (north: ((id: "addr"),), east: ((id: "out"),))
)
// This block's "addr" port (south side) is x-aligned with vcemem's "addr"
// port (north side), even though B is wider than vcemem and not
// horizontally aligned by default.
element.block(
x: (from: "vcemem-port-addr", to: "addr"),
y: (rel: 2, to: "vcemem.north"),
w: 4.5, h: 1,
id: "fsm",
fill: util.colors.yellow,
ports: (south: ((id: "addr"),), east: ((id: "out"),))
)
wire.wire("wAddr", ("vcemem-port-addr", "fsm-port-addr"), style: "zigzag")
})
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")
})
+6878 -6987
View File
File diff suppressed because it is too large. Load diff
-2
View File
@@ -27,7 +27,6 @@
name-anchor: "center",
ports: (),
ports-margins: (),
port-name-padding: 2pt,
fill: none,
radius: 0.5em,
stroke: black + 1pt,
@@ -45,7 +44,6 @@
name-anchor: name-anchor,
ports: ports,
ports-margins: ports-margins,
port-name-padding: port-name-padding,
fill: fill,
stroke: stroke,
id: id,
+45 -11
View File
@@ -21,10 +21,14 @@
/// - draw-shape (function): Draw function
/// - x (number, dictionary): The x position (bottom-left corner).
///
/// 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
/// If it is a dictionary, it can take one of two forms:
/// - `(rel: number, to: str)`: `rel` is the x offset and `to` the base anchor
/// - `(from: str, to: str)`: `from` is the base anchor and `to` is the id of the port to align with the anchor (analogous to `y`)
/// - 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
@@ -35,11 +39,11 @@
/// - `clock` (`bool`): Whether it is a clock port (triangle symbol)
/// - `vertical` (`bool`): Whether the name should be drawn vertically
/// - 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`)
/// - 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-x (dictionary): Dictionary of the ports x offsets (used with `auto-ports: false`)
/// - ports-y (dictionary): Dictionary of the ports y offsets (used with `auto-ports: false`)
/// - debug (dictionary): Dictionary of debug options.
///
@@ -55,11 +59,11 @@
name-anchor: "center",
ports: (:),
ports-margins: (:),
port-name-padding: 2pt,
fill: none,
stroke: black + 1pt,
id: "",
auto-ports: true,
ports-x: (:),
ports-y: (:),
debug: (
ports: false
@@ -75,14 +79,45 @@
if w == none { panic("Parameter w must be set") }
if h == none { panic("Parameter h must be set") }
if (type(x) == dictionary) {
let offset = x.rel
if (type(x) == dictionary and "rel" in x) {
x = util.resolve-rel-coord(ctx, x, 0)
} else if (type(x) == dictionary) {
let from = x.from
let to = x.to
let (ctx, to-pos) = coordinate.resolve(ctx, (rel: (offset, 0), to: to))
x = to-pos.at(0)
let (to-side, i) = find-port(ports, to)
let margins = (0%, 0%)
if to-side in ports-margins {
margins = ports-margins.at(to-side)
}
if (type(y) == dictionary) {
let dx
let left-margin
if to-side in ("north", "south") {
let used-pct = 100% - margins.at(0) - margins.at(1)
let used-width = width * used-pct / 100%
left-margin = width * margins.at(0) / 100%
dx = used-width * (i + 1) / (ports.at(to-side).len() + 1)
if not auto-ports {
left-margin = 0
dx = ports-x.at(to)(width)
}
} else if to-side == "west" {
dx = 0
left-margin = 0
} else if to-side == "east" {
dx = width
left-margin = 0
}
let (ctx, from-pos) = coordinate.resolve(ctx, from)
x = from-pos.at(0) + dx - width + left-margin
}
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)
@@ -141,8 +176,7 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: debug.ports,
name-padding: port-name-padding,
debug: debug.ports
)
}
})
+1 -33
View File
@@ -1,15 +1,9 @@
#import "/src/cetz.typ": draw, coordinate
#import "../util.typ"
#import "ports.typ": add-ports
/// Draws a group of elements
///
/// #examples.group
///
/// Ports can also be defined on a group, just like on a block. By default
/// port labels are drawn *outside* the group border so they do not overlap
/// child elements. Set `outside-port-names: false` to draw them inside.
/// #examples.group-ports
/// - body (elements, function): Elements to group
/// - id (str): see #doc-ref("element.elmt")
/// - name (str): The group's name
@@ -22,11 +16,6 @@
/// - array: either (`<all>`,), (`<vertical>`, `<horizontal>`) or (`<top>`, `<right>`, `<bottom>`, `<left>`)
/// - dictionary: valid keys are "top", "right", "bottom" and "left"
/// - radius (number): The corner radius
/// - ports (dictionary): see #doc-ref("element.elmt")
/// - ports-margins (dictionary): see #doc-ref("element.elmt")
/// - outside-port-names (bool): If `true` (default), port name labels are drawn outside the group border so they do not overlap child elements. Set to `false` to draw labels inside, matching the block behaviour
/// - 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)
/// - debug (dictionary): see #doc-ref("element.elmt")
#let group(
body,
id: "",
@@ -35,14 +24,7 @@
fill: none,
stroke: black + 1pt,
padding: 0.5em,
radius: 0.5em,
ports: (:),
ports-margins: (:),
outside-port-names: true,
port-name-padding: 2pt,
debug: (
ports: false
)
radius: 0.5em
) = {
let min-x = none
let max-x = none
@@ -86,20 +68,6 @@
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,
)
if name != none {
draw.content(
id + "." + name-anchor,
+5 -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
+5 -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"
@@ -60,13 +61,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
+6 -24
View File
@@ -1,13 +1,11 @@
#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor, opposite-anchor
#import "../util.typ": rotate-anchor
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false,
outside-name: false,
name-padding: 2pt,
debug: false
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
@@ -29,22 +27,10 @@
// TODO: use context or vectors to have the height relative to the width
draw.line(prev, pos1, next)
}
// When outside-name is true (e.g. for groups) the label is placed on the
// exterior of the element border so it does not overlap child elements.
// Horizontal labels: anchor = opposite of the side (e.g. east port → west-anchored text, outside).
// Vertical labels: same opposite side but rotated 90°, keeping text readable.
let name-anchor = if outside-name {
if name-rotate { rotate-anchor(opposite-anchor(side)) }
else { opposite-anchor(side) }
} else {
if name-rotate { rotate-anchor(side) } else { side }
}
draw.content(
pos,
anchor: name-anchor,
padding: name-padding,
anchor: if name-rotate {rotate-anchor(side)} else {side},
padding: 2pt,
angle: if name-rotate {90deg} else {0deg},
name
)
@@ -74,9 +60,7 @@
tl, tr, br, bl,
ports,
ports-margins,
debug: false,
outside-name: false,
name-padding: 2pt,
debug: false
) = {
let sides = (
"north": (tl, tr),
@@ -125,9 +109,7 @@
pos,
prev: pos-prev,
next: pos-next,
debug: debug,
outside-name: outside-name,
name-padding: name-padding,
debug: debug
)
}
}
+20
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)[