forked from HEL/circuiteria
186 lines
5.5 KiB
Typst
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
|
|
} |