Files
circuiteria/src/elements/element.typ
T
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

182 lines
5.7 KiB
Typst

#import "/src/cetz.typ": draw, coordinate
#import "ports.typ": add-ports, add-port
#import "../util.typ"
#let find-port(ports, id) = {
for (side, side-ports) in ports {
for (i, port) in side-ports.enumerate() {
if port.id == id {
return (side, i)
}
}
}
panic("Could not find port with id " + str(id))
}
#let default-draw-shape(id, tl, tr, br, bl, fill, stroke) = {
return ({}, tl, tr, br, bl)
}
/// Draws an element
/// - draw-shape (function): Draw function
/// - x (number, dictionary): The x position (bottom-left corner).
///
/// 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 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
/// - name-anchor (str): Anchor for the optional name
/// - ports (dictionary): Dictionary of ports. The keys are cardinal directions ("north", "east", "south" and/or "west"). The values are arrays of ports (dictionaries) with the following fields:
/// - `id` (`str`): (Required) Port id
/// - `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
/// - 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)
/// - 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.
///
/// Supported fields include:
/// - `ports`: if true, shows dots on all ports of the element
#let elmt(
draw-shape: default-draw-shape,
x: none,
y: none,
w: none,
h: none,
name: none,
name-anchor: "center",
ports: (:),
ports-margins: (:),
fill: none,
stroke: black + 1pt,
id: "",
auto-ports: true,
ports-x: (:),
ports-y: (:),
debug: (
ports: false
)
) = 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 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 (to-side, i) = find-port(ports, to)
let margins = (0%, 0%)
if to-side in ports-margins {
margins = ports-margins.at(to-side)
}
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)
let margins = (0%, 0%)
if to-side in ports-margins {
margins = ports-margins.at(to-side)
}
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
}
let (ctx, from-pos) = coordinate.resolve(ctx, from)
y = from-pos.at(1) + dy - height + top-margin
}
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 {
add-ports(
id,
tl, tr, br, bl,
ports,
ports-margins,
debug: debug.ports
)
}
})