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

172 lines
6.2 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 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 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)
/// - 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(
draw-shape: default-draw-shape,
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: true,
ports-y: (:),
shell: false,
body: none,
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) {
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)
}
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 {
let outside-name = if port-names-outside == auto { shell } else { port-names-outside }
add-ports(
id,
tl, tr, br, bl,
ports,
ports-margins,
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
}
})