Files
circuiteria/src/util.typ
T

186 lines
5.5 KiB
Typst

#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)[
/// #canvas(length: 2em, {
/// for (i, color) in util.colors.pairs().enumerate() {
/// let x = calc.rem(i, w) * 2
/// let y = calc.floor(i / w) * 2
/// draw.rect((x, -y), (x + 1, -y - 1), fill: color.last(), stroke: none)
/// draw.content((x + 0.5, -y - 1.25), color.first())
/// }
/// })
/// ])
#let colors = (
orange: rgb(245, 180, 147),
yellow: rgb(250, 225, 127),
green: rgb(127, 200, 172),
pink: rgb(236, 127, 178),
purple: rgb(189, 151, 255),
blue: rgb(127, 203, 235)
)
/// Pads a string on the left with 0s to the given length
///
/// #example(`#util.lpad("0100", 8)`, mode: "markup")
///
/// - string (str): The string to pad
/// - len (int): The target length
/// -> str
#let lpad(string, len) = {
let res = "0" * len + string
return res.slice(-len)
}
/// Returns the anchor on the opposite side of the given one
///
/// #example(`#util.opposite-anchor("west")`, mode: "markup")
///
/// - anchor (str): The input anchor
/// -> str
#let opposite-anchor(anchor) = {
return (
north: "south",
east: "west",
south: "north",
west: "east",
north-west: "south-east",
north-east: "south-west",
south-east: "north-west",
south-west: "north-east"
).at(anchor)
}
/// Returns the anchor rotated 90 degrees clockwise relative to the given one
///
/// #example(`#util.rotate-anchor("west")`, mode: "markup")
/// - anchor (str): The anchor to rotate
/// -> str
#let rotate-anchor(anchor) = {
return (
north: "east",
east: "south",
south: "west",
west: "north",
north-west: "north-east",
north-east: "south-east",
south-east: "south-west",
south-west: "north-west"
).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"
)
/// 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
}