Files
circuiteria/src/elements/ports.typ
T

193 lines
5.5 KiB
Typst

#import "/src/cetz.typ": draw
#import "../util.typ": rotate-anchor, opposite-anchor, port-shape-vertices
#let add-port(
elmt-id, side, port, pos,
prev: none,
next: none,
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let name = port.at("name", default: "")
let name-rotate = port.at("vertical", default: false)
let is-clock = port.at("clock", default: false)
// Per-port overrides fall back to element-level defaults.
let shape = port.at("shape", default: port-shape)
let kind = port.at("kind", default: port-kind)
let size = port.at("size", default: port-size)
let shape-stroke = port.at("stroke", default: elmt-stroke)
// Pentagon fill defaults to the element's fill (so the pentagon covers the
// block-border segment it crosses). Clock ports default to gray. Per-port
// `fill` overrides both.
let shape-fill = port.at("fill",
default: if is-clock and shape == "pentagon" { gray } else { elmt-fill })
// When shape is enabled, wire-connection anchor moves outside the block, and
// inside-name labels reference the inner tip/base; otherwise both collapse to
// the boundary point (unchanged behaviour).
let outer-pos = pos
let inner-pos = pos
if shape == "pentagon" {
let sh = port-shape-vertices(side, kind, size)
let v = sh.vertices.map(off => (rel: off, to: pos))
// Draw pentagon on a higher layer so it always renders on top of wires
// and neighbouring elements.
draw.on-layer(1, draw.line(
v.at(0), v.at(1), v.at(2), v.at(3), v.at(4),
close: true,
stroke: shape-stroke,
fill: shape-fill,
))
outer-pos = (rel: sh.outer, to: pos)
inner-pos = (rel: sh.inner, to: pos)
} else if is-clock {
if prev == none or next == none {
panic("Clock port must have previous and next positions")
}
let csize = if port.at("small", default: false) {8pt} else {1em}
let offset
if (side == "north") { offset = ( 0, -csize) }
else if (side == "east") { offset = (-csize, 0) }
else if (side == "south") { offset = ( 0, csize) }
else if (side == "west") { offset = ( csize, 0) }
let pos1 = (rel: offset, to: pos)
// 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 }
}
// Label sits at the outer edge of the shape for outside-name, inner edge for
// inside-name. `name-padding` is thus naturally measured from the shape edge
// (or from the boundary when shape is disabled).
let label-pos = if outside-name { outer-pos } else { inner-pos }
// For "shell" containers, wires must attach at the inside of the pentagon
// (the tip that points inward), not at the exterior base.
let anchor-pos = if wire-inside and shape == "pentagon" { inner-pos } else { outer-pos }
draw.content(
label-pos,
anchor: name-anchor,
padding: name-padding,
angle: if name-rotate {90deg} else {0deg},
name
)
let id = elmt-id + "-port-" + port.at("id")
if debug {
draw.circle(
anchor-pos,
name: id,
radius: .1,
stroke: none,
fill: red
)
} else {
draw.hide(draw.circle(
anchor-pos,
radius: 0,
stroke: none,
name: id
))
}
}
#let add-ports(
elmt-id,
tl, tr, br, bl,
ports,
ports-margins,
debug: false,
outside-name: false,
name-padding: 2pt,
port-shape: none,
port-kind: "in",
port-size: 1em,
elmt-stroke: black + 1pt,
elmt-fill: none,
wire-inside: false,
) = {
let sides = (
"north": (tl, tr),
"east": (tr, br),
"south": (bl, br),
"west": (tl, bl)
)
if type(ports) != dictionary {
return
}
for (side, props) in sides {
let side-ports = ports.at(side, default: ())
let space = 100% / (side-ports.len() + 1)
for (i, port) in side-ports.enumerate() {
let pct = (i + 1) * space
let pt0 = props.at(0)
let pt1 = props.at(1)
if side in ports-margins {
let (a, b) = (pt0, pt1)
let margins = ports-margins.at(side)
a = (pt0, margins.at(0), pt1)
b = (pt0, 100% - margins.at(1), pt1)
pt0 = a
pt1 = b
}
let pos = (pt0, pct, pt1)
let pct-prev = (i + 0.5) * space
let pct-next = (i + 1.5) * space
let pos-prev = (pt0, pct-prev, pt1)
let pos-next = (pt0, pct-next, pt1)
if port.at("small", default: false) {
pos-prev = (pos, 4pt, pt0)
pos-next = (pos, 4pt, pt1)
}
add-port(
elmt-id,
side,
port,
pos,
prev: pos-prev,
next: pos-next,
debug: debug,
outside-name: outside-name,
name-padding: name-padding,
port-shape: port-shape,
port-kind: port-kind,
port-size: port-size,
elmt-stroke: elmt-stroke,
elmt-fill: elmt-fill,
wire-inside: wire-inside,
)
}
}
}