circuiteria/src/wire.typ

295 lines
7.9 KiB
Plaintext

#import "@preview/cetz:0.2.2": draw, coordinate
#import "util.typ": opposite-anchor
/// List of valid wire styles
/// #examples.wires
#let wire-styles = ("direct", "zigzag", "dodge")
#let signal-width = 1pt
#let bus-width = 1.5pt
#let intersection(pt, radius: .2, fill: black) = {
draw.circle(pt, radius: radius, stroke: none, fill: fill)
}
#let get-direct-wire(pts) = {
let anchors = (
"start": pts.first(),
"end": pts.last()
)
return (pts, anchors)
}
#let get-zigzag-wire(pts, ratio, dir) = {
let start = pts.first()
let end = pts.last()
let mid = (start, ratio, end)
let points = if dir == "vertical" {
(
start,
(horizontal: mid, vertical: ()),
(horizontal: (), vertical: end),
end
)
} else {
(
start,
(horizontal: (), vertical: mid),
(horizontal: end, vertical: ()),
end
)
}
let anchors = (
"start": start,
"zig": points.at(1),
"zag": points.at(2),
"end": end
)
return (points, anchors)
}
#let get-dodge-wire(pts, dodge-y, margins, sides, ctx) = {
let start = pts.first()
let end = pts.last()
let (margin-start, margin-end) = margins
let (side-start, side-end) = sides
let p1 = (start, margin-start, end)
let p2 = (end, margin-end, start)
let (ctx, p0) = coordinate.resolve(ctx, start)
let (ctx, p3) = coordinate.resolve(ctx, end)
p0 = (x: p0.first(), y: p0.last())
p3 = (x: p3.first(), y: p3.last())
let dx1 = margin-start
let dx2 = margin-end
if type(margin-start) == ratio {
dx1 = calc.abs(p3.x - p0.x) * margin-start / 100%
}
if type(margin-end) == ratio {
dx2 = calc.abs(p3.x - p0.x) * margin-end / 100%
}
if side-start == "west" {
dx1 *= -1
}
if side-end == "east" {
dx2 *= -1
}
p1 = (p0.x + dx1, p0.y)
p2 = (p3.x - dx2, p0.y)
let points = (
start,
(horizontal: p1, vertical: ()),
(horizontal: (), vertical: (0, dodge-y)),
(horizontal: p2, vertical: ()),
(horizontal: (), vertical: end),
end
)
let anchors = (
"start": start,
"start2": points.at(1),
"dodge-start": points.at(2),
"dodge-end": points.at(3),
"end2": points.at(4),
"end": end
)
return (points, anchors)
}
/// Draws a wire between two points
/// - id (str): The wire's id, for future reference (anchors)
/// - pts (array): The two points (as CeTZ compatible coordinates, i.e. XY, relative positions, ids, etc.)
/// - bus (bool): Whether the wire is a bus (multiple bits) or a simple signal (single bit)
/// - name (none, str, array): Optional name of the wire. If it is an array, the first name will be put at the start of the wire, and the second at the end
/// - name-pos (str): Position of the name. One of: "middle", "start" or "end"
/// - slice (none, array): Optional bits slice (start and end bit indices). If set, it will be displayed at the start of the wire
/// - color (color): The stroke color
/// - dashed (bool): Whether the stroke is dashed or not
/// - style (str): The wire's style (see #doc-ref("wire.wire-styles", var: true) for possible values)
/// - reverse (bool): If true, the start and end points will be swapped (useful in cases where the start point depends on the end point, for example with perpendiculars)
/// - directed (bool): If true, the wire will be directed, meaning an arrow will be drawn at the endpoint
/// - rotate-name (bool): If true, the name will be rotated according to the wire's slope
/// - zigzag-ratio (ratio): Position of the zigzag vertical relative to the horizontal span (only with style "zigzag")
/// - zigzag-dir (str): The zigzag's direction. As either "vertical" or "horizontal" (only with dstyle "zigzag")
/// - dodge-y (number): Y position to dodge the wire to (only with style "dodge")
/// - dodge-sides (array): The start and end sides (going out of the connected element) of the wire (only with style "dodge")
/// - dodge-margins (array): The start and end margins (i.e. space before dodging) of the wire (only with style "dodge")
#let wire(
id, pts,
bus: false,
name: none,
name-pos: "middle",
slice: none,
color: black,
dashed: false,
style: "direct",
reverse: false,
directed: false,
rotate-name: true,
zigzag-ratio: 50%,
zigzag-dir: "vertical",
dodge-y: 0,
dodge-sides: ("east", "west"),
dodge-margins: (5%, 5%)
) = draw.get-ctx(ctx => {
if not style in wire-styles {
panic("Invalid wire style '" + style + "'")
}
if pts.len() != 2 {
panic("Wrong number of points (got " + str(pts.len()) + " instead of 2)")
}
let stroke = (
paint: color,
thickness: if bus {bus-width} else {signal-width}
)
if dashed {
stroke.insert("dash", "dashed")
}
let points = ()
let anchors = ()
if style == "direct" {
(points, anchors) = get-direct-wire(pts)
} else if style == "zigzag" {
(points, anchors) = get-zigzag-wire(pts, zigzag-ratio, zigzag-dir)
} else if style == "dodge" {
(points, anchors) = get-dodge-wire(
pts,
dodge-y,
dodge-margins,
dodge-sides,
ctx
)
}
let mark = (fill: color)
if directed {
mark = (end: ">", fill: color)
}
draw.group(name: id, {
draw.line(..points, stroke: stroke, mark: mark)
for (anchor-name, anchor-pos) in anchors {
draw.anchor(anchor-name, anchor-pos)
}
})
let first-pt = id + ".start"
let last-pt = id + ".end"
let first-pos = points.first()
let second-pos = points.at(1)
if reverse {
(first-pt, last-pt) = (last-pt, first-pt)
(first-pos, second-pos) = (second-pos, first-pos)
}
let angle = 0deg
if rotate-name {
(ctx, first-pos) = coordinate.resolve(ctx, first-pos)
(ctx, second-pos) = coordinate.resolve(ctx, second-pos)
let (x1, y1, _) = first-pos
let (x2, y2, _) = second-pos
angle = calc.atan2(x2 - x1, y2 - y1)
}
if name != none {
let names = ()
if type(name) == str {
names = ((name, name-pos),)
} else if type(name) == array {
names = (
(name.at(0), "start"),
(name.at(1), "end")
)
}
for (name, pos) in names {
let point
let anchor
if pos == "middle" {
point = (first-pt, 50%, last-pt)
anchor = "south"
} else if pos == "start" {
point = first-pt
anchor = "south-west"
} else if pos == "end" {
point = last-pt
anchor = "south-east"
}
draw.content(point, anchor: anchor, padding: 3pt, angle: angle, name)
}
}
if slice != none {
let (start, end) = slice
let slice-txt = "[" + str(start) + ":" + str(end) + "]"
draw.content(
first-pt,
anchor: "south-west",
padding: 3pt,
text(slice-txt, size: 0.75em)
)
}
})
/// Draws a wire stub (useful for unlinked ports)
///
/// #examples.stub
/// - port-id (str): The port anchor
/// - side (str): The side on which the port is (one of "north", "east", "south", "west")
/// - name (none, str): Optional name displayed at the end of the stub
/// - vertical (bool): Whether the name should be displayed vertically
/// - length (number): The length of the stub
/// - name-offset (number): The name offset, perpendicular to the stub
#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0) = {
let end-offset = (
north: (0, length),
east: (length, 0),
south: (0, -length),
west: (-length, 0)
).at(side)
let name-offset = (
north: (name-offset, length),
east: (length, name-offset),
south: (name-offset, -length),
west: (-length, name-offset)
).at(side)
draw.line(
port-id,
(rel: end-offset, to: port-id)
)
if name != none {
let text-anchor = if vertical {
(
"north": "west",
"south": "east",
"west": "south",
"east": "north"
).at(side)
} else { opposite-anchor(side) }
draw.content(
anchor: text-anchor,
padding: 0.2em,
angle: if vertical {90deg} else {0deg},
(rel: name-offset, to: port-id),
name
)
}
}