1 Commits
Author SHA1 Message Date
purpl3F0x 0c1272e35e Add Verilog parsing and rendering support
- Introduced `verilog.typ` for parsing Verilog/SystemVerilog source code, supporting both ANSI and non-ANSI port styles.
- Implemented functions to analyze modules, infer AXI4, AXI4-Lite, and AXI4-Stream buses, as well as clock and reset signals.
- Added rendering capabilities for Verilog modules, allowing for visual representation of ports and buses.
- Created a new `gallery/verilog.typ` file to facilitate the drawing of Verilog blocks in diagrams.

Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-09-10 12:17:54 +01:00
11 changed files with 832 additions and 4 deletions
+24 -1
View File
@@ -44,6 +44,16 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
<td>Groups</td>
<td>Rotated</td>
</tr>
<tr>
<td colspan="2">
<a href="./gallery/verilog.typ">
<img src="./gallery/verilog.png" width="500px" alt="Block automatically generated from a Verilog module">
</a>
</td>
</tr>
<tr>
<td colspan="2">Generated from a Verilog module</td>
</tr>
</table>
> **Note**\
@@ -61,4 +71,17 @@ To use this package, simply import [circuiteria](https://typst.app/universe/pack
import circuiteria: *
...
})
```
```
### Verilog modules
Blocks can be generated automatically from Verilog/SystemVerilog sources.
AXI4, AXI4-Lite and AXI4-Stream signals are grouped into single bus ports, and
clock/reset signals are detected and placed together:
```typ
#circuiteria.circuit({
import circuiteria: *
verilog.block(read("my_module.v"), x: 0, y: 0)
})
```
The port ids are the signal names, or the bus prefixes for inferred buses
(e.g. `"my_module-port-s_axi"`).
+51
View File
@@ -0,0 +1,51 @@
`timescale 1ns / 1ps
// Simple AXI-Stream to AXI4-Lite bridge used as a demo for the Verilog parser
module axi_demo #(
parameter integer DATA_WIDTH = 32,
parameter integer ADDR_WIDTH = 12
) (
input wire aclk,
input wire aresetn,
// AXI4-Lite slave interface
input wire [ADDR_WIDTH-1:0] s_axil_awaddr,
input wire [2:0] s_axil_awprot,
input wire s_axil_awvalid,
output wire s_axil_awready,
input wire [31:0] s_axil_wdata,
input wire [3:0] s_axil_wstrb,
input wire s_axil_wvalid,
output wire s_axil_wready,
output wire [1:0] s_axil_bresp,
output wire s_axil_bvalid,
input wire s_axil_bready,
input wire [ADDR_WIDTH-1:0] s_axil_araddr,
input wire [2:0] s_axil_arprot,
input wire s_axil_arvalid,
output wire s_axil_arready,
output wire [31:0] s_axil_rdata,
output wire [1:0] s_axil_rresp,
output wire s_axil_rvalid,
input wire s_axil_rready,
// AXI4-Stream sink
input wire [63:0] s_axis_tdata,
input wire [7:0] s_axis_tkeep,
input wire s_axis_tvalid,
output wire s_axis_tready,
input wire s_axis_tlast,
// AXI4-Stream source
output wire [63:0] m_axis_tdata,
output wire [7:0] m_axis_tkeep,
output wire m_axis_tvalid,
input wire m_axis_tready,
output wire m_axis_tlast,
input wire enable,
output wire [3:0] status,
output wire irq
);
endmodule
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+12
View File
@@ -0,0 +1,12 @@
#import "/src/lib.typ" as circuiteria: circuit, util, verilog
#set page(width: auto, height: auto, margin: .5cm)
#circuit({
verilog.block(
read("axi_demo.v"),
x: 0, y: 0,
// fill: util.colors.yellow,
// radius: 0.05em
)
})
BIN
View File
Binary file not shown.
+20 -1
View File
@@ -215,4 +215,23 @@ If you have installed Circuiteria directly in your project, import #link("src/li
)
)
#tidy.show-module(gates-docs, sort-functions: false)
#tidy.show-module(gates-docs, sort-functions: false)
#pagebreak()
#let verilog-docs = tidy.parse-module(
read("src/verilog.typ"),
name: "verilog",
old-syntax: true,
scope: (
element: element,
circuit: circuit,
draw: draw,
wire: wire,
tidy: tidy,
examples: examples,
doc-ref: doc-ref
)
)
#tidy.show-module(verilog-docs, sort-functions: false)
+1
View File
@@ -33,6 +33,7 @@
/// - `id` (`str`): (Required) Port id
/// - `name` (`str`): Optional name displayed *in* the block
/// - `clock` (`bool`): Whether it is a clock port (triangle symbol)
/// - `invert` (`bool`): Whether it is an active-low port (bubble 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)
/// - fill (none, color): Fill color
+15
View File
@@ -27,6 +27,21 @@
// TODO: use context or vectors to have the height relative to the width
draw.line(prev, pos1, next)
}
if (port.at("invert", default: false)) {
let r = 0.1
let offset
if (side == "north") { offset = ( 0, r) }
else if (side == "east") { offset = ( r, 0) }
else if (side == "south") { offset = ( 0, -r) }
else if (side == "west") { offset = ( -r, 0) }
draw.circle(
(rel: offset, to: pos),
radius: r,
fill: white,
stroke: black + 1pt
)
}
draw.content(
pos,
anchor: if name-rotate {rotate-anchor(side)} else {side},
+1
View File
@@ -5,4 +5,5 @@
#import "element.typ"
#import "gates.typ"
#import "util.typ"
#import "verilog.typ"
#import "wire.typ"
+704
View File
@@ -0,0 +1,704 @@
#import "/src/cetz.typ": draw
#import "elements/block.typ": block as block-element
#import "util.typ"
#import "wire.typ"
// ---------------------------------------------------------------------------
// Low level helpers
// ---------------------------------------------------------------------------
#let _ident = "(?:\\\\\\S+|[A-Za-z_][A-Za-z0-9_$]*)"
#let _net-kw = "(?:var|wire|reg|logic|bit|tri|tri0|tri1|triand|trior|wand|wor|uwire|supply0|supply1|signed|unsigned)"
#let _strip-comments(src) = {
let s = src.replace(regex("(?s)/\\*.*?\\*/"), " ")
s = s.replace(regex("//[^\n]*"), " ")
return s
}
// Finds the index of the closing bracket matching the opening one at `start`
#let _find-close(chars, start) = {
let pairs = ("(": ")", "[": "]", "{": "}")
let open = chars.at(start)
let close = pairs.at(open)
let depth = 0
let i = start
while i < chars.len() {
let c = chars.at(i)
if c == open {
depth += 1
} else if c == close {
depth -= 1
if depth == 0 { return i }
}
i += 1
}
return none
}
// Splits a text on commas that are not nested inside brackets
#let _split-commas(text) = {
let items = ()
let depth = 0
let cur = ()
for c in text.clusters() {
if c in ("(", "[", "{") {
depth += 1
} else if c in (")", "]", "}") {
depth -= 1
}
if c == "," and depth <= 0 {
items.push(cur.join())
cur = ()
} else {
cur.push(c)
}
}
if cur.join().trim() != "" { items.push(cur.join()) }
return items.filter(it => it.trim() != "")
}
// Evaluates a very simple integer expression (`4`, `32-1`, `8+2`)
#let _eval-int(expr) = {
if expr == none { return none }
let e = expr.replace(regex("\\s+"), "")
if e.match(regex("^[0-9]+$")) != none { return int(e) }
let m = e.match(regex("^([0-9]+)([+*-])([0-9]+)$"))
if m == none { return none }
let a = int(m.captures.at(0))
let b = int(m.captures.at(2))
let op = m.captures.at(1)
if op == "+" { return a + b }
if op == "-" { return a - b }
return a * b
}
// Extracts the first packed range (`[7:0]`) of a dimension list
#let _first-range(dims) = {
if dims == none { return none }
let m = dims.match(regex("\\[([^\\]]*)\\]"))
if m == none { return none }
return m.captures.at(0).trim()
}
// Computes the width of a range text (`7:0` -> 8), `none` if not constant
#let _range-width(range) = {
if range == none { return 1 }
let m = range.match(regex("^([^:]+):([^:]+)$"))
if m == none { return none }
let hi = _eval-int(m.captures.at(0))
let lo = _eval-int(m.captures.at(1))
if hi == none or lo == none { return none }
return calc.abs(hi - lo) + 1
}
#let _make-port(name, dir, dims) = {
let range = _first-range(dims)
return (
name: name,
dir: dir,
range: range,
width: _range-width(range)
)
}
// ---------------------------------------------------------------------------
// Parsing
// ---------------------------------------------------------------------------
#let _parse-ansi-ports(portlist) = {
let ports = ()
let last-dir = "input"
let last-dims = none
for item in _split-commas(portlist) {
let m = item.match(regex(
"^\\s*(?:(input|output|inout)\\s+)?(?:" + _net-kw + "\\s+)*" +
"((?:\\[[^\\]]*\\]\\s*)*)(" + _ident + ")"
))
if m == none { continue }
let (dir, dims, name) = (m.captures.at(0), m.captures.at(1), m.captures.at(2))
if dir != none {
last-dir = dir
last-dims = dims
} else if dims == none or dims.trim() == "" {
dims = last-dims
}
ports.push(_make-port(name, last-dir, dims))
}
return ports
}
#let _parse-body-ports(portlist, body) = {
let decls = (:)
for m in body.matches(regex(
"(?s)\\b(input|output|inout)\\b((?:\\s+" + _net-kw + ")*)\\s*" +
"((?:\\[[^\\]]*\\]\\s*)*)([^;]*);"
)) {
let dir = m.captures.at(0)
let dims = m.captures.at(2)
for n in _split-commas(m.captures.at(3)) {
let nm = n.match(regex("^\\s*(" + _ident + ")\\s*$"))
if nm == none { continue }
decls.insert(nm.captures.at(0), _make-port(nm.captures.at(0), dir, dims))
}
}
let ports = ()
for item in _split-commas(portlist) {
let m = item.match(regex("(" + _ident + ")\\s*$"))
if m == none { continue }
let name = m.captures.at(0)
if name in decls { ports.push(decls.at(name)) }
}
// Declared but not listed (or unparsable port list): append the rest
for (name, port) in decls {
if ports.find(it => it.name == name) == none { ports.push(port) }
}
return ports
}
#let _parse-module(chunk) = {
let m = chunk.match(regex("^\\s*module\\s+(" + _ident + ")"))
if m == none { return none }
let name = m.captures.at(0)
let chars = chunk.slice(m.end).clusters()
let i = 0
while i < chars.len() and chars.at(i).trim() == "" { i += 1 }
let params = ()
if i < chars.len() and chars.at(i) == "#" {
while i < chars.len() and chars.at(i) != "(" { i += 1 }
let close = _find-close(chars, i)
if close == none { return none }
for p in _split-commas(chars.slice(i + 1, close).join()) {
let pm = p.match(regex("(" + _ident + ")\\s*=\\s*([^,]+)$"))
if pm != none {
params.push((name: pm.captures.at(0), value: pm.captures.at(1).trim()))
}
}
i = close + 1
}
while i < chars.len() and chars.at(i) != "(" and chars.at(i) != ";" { i += 1 }
let portlist = ""
if i < chars.len() and chars.at(i) == "(" {
let close = _find-close(chars, i)
if close == none { return none }
portlist = chars.slice(i + 1, close).join()
i = close + 1
}
let body = if i < chars.len() { chars.slice(i).join() } else { "" }
let ports = if portlist.match(regex("\\b(input|output|inout)\\b")) != none {
_parse-ansi-ports(portlist)
} else {
_parse-body-ports(portlist, body)
}
return (name: name, params: params, ports: ports)
}
/// Parses Verilog/SystemVerilog source code and returns the declared modules.
///
/// Both ANSI (`module m(input clk, ...)`) and non-ANSI
/// (`module m(clk, ...); input clk;`) port styles are supported.
///
/// ```typ
/// #let modules = verilog.parse(read("my_module.v"))
/// ```
///
/// - source (str): The Verilog source code, typically obtained with `read()`
/// -> array
#let parse(source) = {
let src = _strip-comments(source)
let modules = ()
for m in src.matches(regex("\\bmodule\\b")) {
let rest = src.slice(m.start)
let pos = rest.slice(6).position(regex("\\bendmodule\\b"))
let chunk = if pos == none { rest } else { rest.slice(0, 6 + pos) }
let parsed = _parse-module(chunk)
if parsed != none { modules.push(parsed) }
}
return modules
}
/// Returns a single module from Verilog source code.
///
/// - source (str, array, dictionary): Verilog source code, an array of parsed modules or an already parsed module
/// - name (none, str): Name of the wanted module. If `none`, the first module is returned
/// -> dictionary
#let get-module(source, name: none) = {
let modules = if type(source) == str {
parse(source)
} else if type(source) == dictionary {
(source,)
} else {
source
}
if modules.len() == 0 { panic("No Verilog module found") }
if name == none { return modules.first() }
let found = modules.find(it => it.name == name)
if found == none { panic("Could not find Verilog module " + name) }
return found
}
// ---------------------------------------------------------------------------
// Bus / clock / reset inference
// ---------------------------------------------------------------------------
#let axis-suffixes = (
"tvalid", "tready", "tdata", "tlast", "tkeep", "tstrb",
"tuser", "tid", "tdest", "twakeup"
)
#let axi-suffixes = (
"awid", "awaddr", "awlen", "awsize", "awburst", "awlock", "awcache",
"awprot", "awqos", "awregion", "awuser", "awvalid", "awready",
"wid", "wdata", "wstrb", "wlast", "wuser", "wvalid", "wready",
"bid", "bresp", "buser", "bvalid", "bready",
"arid", "araddr", "arlen", "arsize", "arburst", "arlock", "arcache",
"arprot", "arqos", "arregion", "aruser", "arvalid", "arready",
"rid", "rdata", "rresp", "rlast", "ruser", "rvalid", "rready"
)
// Signals that only exist on full AXI4 (used to tell AXI4 from AXI4-Lite)
#let axi-full-only = (
"awid", "awlen", "awsize", "awburst", "awlock", "awcache", "awqos",
"awregion", "wid", "wlast", "bid",
"arid", "arlen", "arsize", "arburst", "arlock", "arcache", "arqos",
"arregion", "rid", "rlast"
)
#let _clock-re = regex("(?i)(^|[^a-z0-9])(a?clk|clock)([0-9]*)($|[^a-z0-9])")
#let _reset-re = regex("(?i)(^|[^a-z0-9])(a?rst|a?reset)(_?n)?([0-9]*)($|[^a-z0-9])")
#let _is-clock(name) = name.match(_clock-re) != none
#let _is-reset(name) = name.match(_reset-re) != none
#let _is-active-low(name) = name.match(regex("(?i)(_n|_b|_l|resetn|rstn)$")) != none
// Splits a signal name into (prefix, suffix) using the given suffix table
#let _match-suffix(name, suffixes) = {
let lower = lower(name)
let best = none
for s in suffixes {
if not lower.ends-with(s) { continue }
if lower.len() == s.len() {
if best == none or s.len() > best.len() { best = s }
continue
}
let sep = lower.at(lower.len() - s.len() - 1)
if sep != "_" { continue }
if best == none or s.len() > best.len() { best = s }
}
if best == none { return none }
let prefix = name.slice(0, name.len() - best.len())
return (prefix: prefix.trim("_", at: end), suffix: best)
}
#let _group-signals(ports) = {
let groups = (:)
let rest = ()
for port in ports {
let kind = none
let m = _match-suffix(port.name, axis-suffixes)
if m != none {
kind = "axi-stream"
} else {
m = _match-suffix(port.name, axi-suffixes)
if m != none { kind = "axi" }
}
if m == none {
rest.push(port)
continue
}
let key = kind + "|" + lower(m.prefix)
let group = groups.at(key, default: (
kind: kind,
prefix: m.prefix,
signals: (:)
))
group.signals.insert(m.suffix, port)
groups.insert(key, group)
}
return (groups: groups, rest: rest)
}
#let _bus-width(group, ..names) = {
for n in names.pos() {
if n in group.signals {
return group.signals.at(n).width
}
}
return none
}
#let _finalize-bus(group) = {
let sig = group.signals
let kind = group.kind
if kind == "axi-stream" {
if "tvalid" not in sig and not ("tdata" in sig and "tready" in sig) {
return none
}
} else {
if sig.len() < 4 { return none }
if "awvalid" not in sig and "arvalid" not in sig { return none }
let lite = sig.keys().find(it => it in axi-full-only) == none
if lite or lower(group.prefix).contains(regex("axil|lite")) {
kind = "axi-lite"
}
}
let ref = if kind == "axi-stream" {
sig.at("tvalid", default: sig.at("tdata", default: none))
} else {
sig.at("awvalid", default: sig.at("arvalid", default: none))
}
let role = if ref != none and ref.dir == "output" { "master" } else { "slave" }
return (
kind: kind,
prefix: group.prefix,
role: role,
dir: if role == "master" { "output" } else { "input" },
data-width: _bus-width(group, "tdata", "wdata", "rdata"),
addr-width: _bus-width(group, "awaddr", "araddr"),
signals: sig,
ports: sig.values()
)
}
/// Analyses the ports of a module and infers AXI4, AXI4-Lite and AXI4-Stream
/// buses as well as clock and reset signals.
///
/// The returned dictionary contains:
/// - `buses`: array of inferred buses. Each bus has the fields `kind`
/// (`"axi"`, `"axi-lite"` or `"axi-stream"`), `prefix`, `role`
/// (`"master"` or `"slave"`), `dir`, `data-width`, `addr-width` and `signals`
/// - `clocks`: array of clock ports
/// - `resets`: array of reset ports (with an additional `active-low` field)
/// - `signals`: array of the remaining ports
///
/// - module (str, array, dictionary): Verilog source code or a parsed module
/// - name (none, str): Name of the wanted module (when several are present)
/// -> dictionary
#let analyze(module, name: none) = {
let module = get-module(module, name: name)
let clocks = ()
let resets = ()
let others = ()
for port in module.ports {
if _is-clock(port.name) {
clocks.push(port)
} else if _is-reset(port.name) {
resets.push(port + (active-low: _is-active-low(port.name)))
} else {
others.push(port)
}
}
let (groups, rest) = _group-signals(others)
let buses = ()
let signals = rest
for (_, group) in groups {
let bus = _finalize-bus(group)
if bus == none {
signals += group.signals.values()
} else {
buses.push(bus)
}
}
buses = buses.sorted(key: it => it.prefix)
return (
name: module.name,
params: module.params,
buses: buses,
clocks: clocks,
resets: resets,
signals: signals,
ports: module.ports
)
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
#let bus-kind-names = (
"axi": "AXI4",
"axi-lite": "AXI4-Lite",
"axi-stream": "AXI4-Stream"
)
#let _bus-label(bus, show-widths: true, show-info: false) = {
let title = if bus.prefix != "" { bus.prefix } else { bus-kind-names.at(bus.kind) }
let info = ()
if show-info {
if bus.prefix != "" { info.push(bus-kind-names.at(bus.kind)) }
if show-widths and bus.data-width != none {
info.push(str(bus.data-width) + "b")
}
}
if info.len() == 0 {
return (label: title, len: title.len())
}
let sub = info.join(" ")
return (
label: box(align(left)[#title \ #text(size: 0.7em, sub)]),
len: calc.max(title.len(), int(sub.len() * 0.7))
)
}
#let _signal-label(port, show-widths: true) = {
let label = if show-widths and port.range != none {
port.name + "[" + port.range + "]"
} else {
port.name
}
return (label: label, len: label.len())
}
// Approximate width (in canvas units) of a label of `len` characters
#let _text-width(len) = 0.28 * len
/// Builds the ports dictionary of a Verilog module, ready to be given to
/// #doc-ref("element.elmt").
///
/// Inputs (and slave buses) are placed on the west side, outputs (and master
/// buses) on the east side, clocks and resets on top of the west side.
///
/// - module (str, array, dictionary): Verilog source code or a parsed module
/// - name (none, str): Name of the wanted module (when several are present)
/// - show-widths (bool): Whether to append the signal widths to their names
/// - show-bus-info (bool): Whether to display the bus kind and data width below the bus name
/// - clock-side (str): Side on which clock ports are placed
/// - reset-side (str): Side on which reset ports are placed
/// -> dictionary
#let module-ports(
module,
name: none,
show-widths: true,
show-bus-info: false,
clock-side: "west",
reset-side: "west"
) = {
let info = if type(module) == dictionary and "buses" in module {
module
} else {
analyze(module, name: name)
}
let ports = (north: (), east: (), south: (), west: ())
for bus in info.buses {
let side = if bus.dir == "output" { "east" } else { "west" }
let label = _bus-label(bus, show-widths: show-widths, show-info: show-bus-info)
ports.at(side).push((
id: bus.prefix,
name: label.label,
label-len: label.len,
bus: true,
kind: bus.kind,
multiline: type(label.label) != str
))
}
for port in info.clocks {
let vertical = clock-side in ("north", "south")
ports.at(clock-side).push((
id: port.name,
// indented so that the label does not collide with the clock triangle
name: if vertical { "" } else { box(inset: (left: 0.7em), port.name) },
label-len: port.name.len() + 2,
clock: true,
small: true
))
}
for port in info.resets {
ports.at(reset-side).push((
id: port.name,
name: port.name,
label-len: port.name.len(),
invert: port.active-low,
vertical: reset-side in ("north", "south")
))
}
for port in info.signals {
let side = if port.dir == "output" { "east" } else { "west" }
let label = _signal-label(port, show-widths: show-widths)
ports.at(side).push((
id: port.name,
name: label.label,
label-len: label.len,
invert: _is-active-low(port.name),
bus: port.width == none or port.width > 1
))
}
return ports
}
/// Draws a block from a Verilog module description.
///
/// AXI4, AXI4-Lite and AXI4-Stream signals are grouped into single bus ports,
/// and clock/reset signals are automatically detected and grouped together.
///
/// ```typ
/// #circuiteria.circuit({
/// circuiteria.verilog.block(read("axi_dma.v"), x: 0, y: 0)
/// })
/// ```
///
/// The port ids are the signal names (or the bus prefixes), so wires can be
/// attached with `"<id>-port-<signal>"`.
///
/// - module (str, array, dictionary): Verilog source code or a parsed module
/// - module-name (none, str): Name of the wanted module (when several are present)
/// - x (number, dictionary): The x position (bottom-left corner)
/// - y (number, dictionary): The y position (bottom-left corner)
/// - w (auto, number): Width of the block. Computed from the port names if `auto`
/// - h (auto, number): Height of the block. Computed from the port count if `auto`
/// - id (auto, str): The block id. Defaults to the module name
/// - name (auto, none, str, content): Name displayed in the block. Defaults to the module name
/// - name-anchor (str): Anchor for the displayed name
/// - show-widths (bool): Whether to append the signal widths to their names
/// - show-bus-info (bool): Whether to display the bus kind and data width below the bus name
/// - stubs (bool): Whether to draw an unnamed wire stub on every port
/// - stub-length (number, length): Length of the automatic stubs
/// - bus-markers (bool): Whether to draw the bus stubs as double lines
/// - clock-side (str): Side on which clock ports are placed
/// - reset-side (str): Side on which reset ports are placed
/// - fill (none, color): Fill color
/// - stroke (stroke): Border stroke
/// - radius (number, length, ratio, dictionary): The corner radius of the block
/// - ports-margins (dictionary): Dictionary of ports margins
/// - debug (dictionary): Dictionary of debug options
#let block(
module,
module-name: none,
x: 0,
y: 0,
w: auto,
h: auto,
id: auto,
name: auto,
name-anchor: "south",
show-widths: true,
show-bus-info: false,
stubs: true,
stub-length: 1em,
bus-markers: true,
clock-side: "west",
reset-side: "west",
fill: none,
stroke: black + 1pt,
radius: 0.5em,
ports-margins: (:),
debug: (ports: false)
) = {
let info = if type(module) == dictionary and "buses" in module {
module
} else {
analyze(module, name: module-name)
}
let ports = module-ports(
info,
show-widths: show-widths,
show-bus-info: show-bus-info,
clock-side: clock-side,
reset-side: reset-side
)
let id = if id == auto { info.name } else { id }
let name = if name == auto { raw(info.name) } else { name }
let max-len(side) = {
let lens = ports.at(side).map(it => it.at("label-len", default: 8))
if lens.len() == 0 { 0 } else { calc.max(..lens) }
}
let sum-len(side) = {
ports.at(side).fold(0, (acc, it) => acc + it.at("label-len", default: 8))
}
// Two-line (bus) labels need more vertical room
let rows-weight(side) = {
ports.at(side).fold(0, (acc, it) => acc + if it.at("multiline", default: false) { 1.6 } else { 1 })
}
let vert-count = calc.max(ports.north.len(), ports.south.len(), 0)
let margins = ports-margins
for side in ("west", "east") {
if side not in margins { margins.insert(side, (8%, 14%)) }
}
let h = if h != auto { h } else {
let rows = calc.max(rows-weight("west"), rows-weight("east"), 1)
calc.max(1.8, rows * 1.05 / 0.72, if vert-count > 0 { 2.2 } else { 0 })
}
let w = if w != auto { w } else {
let inner = _text-width(max-len("west") + max-len("east")) + 0.8
let title = _text-width(info.name.len() * 1.2) + 0.8
let vert-len = calc.max(sum-len("north"), sum-len("south"))
let vert = _text-width(vert-len) + 0.5 * vert-count + 0.4
calc.max(2.5, inner, title, vert)
}
block-element(
x: x,
y: y,
w: w,
h: h,
id: id,
name: name,
name-anchor: name-anchor,
ports: ports,
ports-margins: margins,
fill: fill,
stroke: stroke,
radius: radius,
debug: debug
)
if stubs {
// active-low ports carry a bubble, so their stub starts past it
let bubble = (north: (0, 0.2), east: (0.2, 0), south: (0, -0.2), west: (-0.2, 0))
let ends = (north: (0, stub-length), east: (stub-length, 0), south: (0, -stub-length), west: (-stub-length, 0))
let gap = 0.1
for (side, side-ports) in ports {
for port in side-ports {
let anchor = id + "-port-" + port.id
let is-bus = port.at("bus", default: false)
if bus-markers and "kind" in port {
let perp = if side in ("east", "west") { (0, gap) } else { (gap, 0) }
// starts past the block border so the fill does not cover it
let o = 0.02
let edge = (north: (0, o), east: (o, 0), south: (0, -o), west: (-o, 0)).at(side)
draw.rect(
(rel: (edge.at(0) - perp.at(0), edge.at(1) - perp.at(1)), to: anchor),
(rel: ends.at(side), to: (rel: perp, to: anchor)),
fill: luma(80%),
stroke: none
)
for k in (1, 0, -1) {
let start = (rel: (perp.at(0) * k, perp.at(1) * k), to: anchor)
draw.line(start, (rel: ends.at(side), to: start), stroke: wire.signal-width)
}
} else if port.at("invert", default: false) {
draw.line(
(rel: bubble.at(side), to: anchor),
(rel: ends.at(side), to: anchor),
stroke: if is-bus { wire.bus-width } else { wire.signal-width }
)
} else {
wire.stub(anchor, side, length: stub-length, bus: is-bus)
}
}
}
}
}
+4 -2
View File
@@ -363,7 +363,8 @@
/// - 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) = {
/// - bus (bool): Whether the stub is a bus (thicker line)
#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0, bus: false) = {
let end-offset = (
north: (0, length),
east: (length, 0),
@@ -380,7 +381,8 @@
draw.line(
port-id,
(rel: end-offset, to: port-id)
(rel: end-offset, to: port-id),
stroke: if bus { bus-width } else { signal-width }
)
if name != none {
let text-anchor = if vertical {