///|
let natural_radius : Double = 10.0
///|
let gap : Double = 20.0
///|
let vertical : Double = 12.0
///|
let frame_pad : Double = 14.0
///|
let label_size : Double = 13.0
///|
let small_size : Double = 11.0
///|
let wrap_units : Int32 = 40
///|
fn cut(text : String, start : Int32, end : Int32) -> String {
text.view(start_offset=start, end_offset=end).to_owned()
}
///|
fn text_width(text : String, size : Double, weight : Int32) -> Double {
let metrics = @metrics.FontMetrics::from_defaults(
profile=Monospace,
font_size=size.to_int(),
font_weight=weight,
)
metrics.measure(text).width.to_double()
}
///|
/// Split text into display lines at line breaks and at a fixed number of
/// code points. The joined lines keep every character in order.
fn wrap_lines(text : String) -> Array[String] {
let out = []
for part in text.split("\n") {
let line = part.to_owned()
let mut start = 0
let mut count = 0
let mut i = 0
while i < line.length() {
let c = line.code_unit_at(i).to_int()
let w = if c >= 0xd800 && c <= 0xdbff && i + 1 < line.length() {
2
} else {
1
}
if count == wrap_units {
out.push(cut(line, start, i))
start = i
count = 0
}
i += w
count += 1
}
out.push(cut(line, start, line.length()))
}
out
}
///|
fn make_label(
text : String,
owner : String,
center : Double,
top : Double,
size~ : Double = label_size,
weight~ : Int32 = 400,
) -> Label {
let lines = wrap_lines(text)
let mut width = 0.0
for line in lines {
width = Double::max(width, text_width(line, size, weight))
}
let height = lines.length().to_double() * (size + 4.0)
{
lines: ReadOnlyArray::from_array(lines),
x: center,
size,
weight,
box: { x: center - width / 2.0, y: top, width, height, },
owner,
}
}
///|
fn label_extent(
text : String,
size : Double,
weight : Int32,
) -> (Double, Double) {
let lines = wrap_lines(text)
let mut width = 0.0
for line in lines {
width = Double::max(width, text_width(line, size, weight))
}
(width, lines.length().to_double() * (size + 4.0))
}
///|
priv enum View {
Leaf(Shape, String, Array[String], Array[String])
Seq(ReadOnlyArray[String])
Alt(ReadOnlyArray[String])
Wrap(String, Bool, Bool, String?)
Group(String, String)
Look(String, String)
Excl(String, String)
}
///|
fn anchor_label(kind : @regex.AnchorKind) -> String {
match kind {
LineStart => "^ line start"
LineEnd => "$ line end"
InputStart => "\\A input start"
InputEndBeforeNewline => "\\Z input end"
InputEnd => "\\z input end"
PreviousMatchEnd => "\\G match end"
WordBoundary => "\\b word boundary"
NonWordBoundary => "\\B not word boundary"
}
}
///|
fn set_label(negated : Bool, items : ReadOnlyArray[@regex.SetItem]) -> String {
let out = StringBuilder()
out.write_string(if negated { "none of " } else { "one of " })
for i, item in items {
if i > 0 {
out.write_char(' ')
}
match item {
Char(c) => out.write_string(c)
Range(a, b) => out.write_string(a + "-" + b)
Escape(e) => out.write_string(e)
}
}
out.to_string()
}
///|
priv struct Frag {
w : Double
up : Double
down : Double
}
///|
priv struct Ctx {
topology : @trace.Topology
views : Map[String, View]
frags : Map[String, Frag]
points : Map[String, Point]
terms : Array[TermBox]
frames : Array[Frame]
wires : Map[String, Wire]
limits : Limits
diagnostics : Array[@diagrams.Diagnostic]
mut work : Int32
mut failed : Bool
}
///|
fn Ctx::charge(self : Ctx, units : Int32) -> Bool {
if self.failed {
return false
}
if units > self.limits.work - self.work {
self.failed = true
self.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/layout-limit", "Railroad layout work limit exceeded",
),
)
return false
}
self.work += units
true
}
///|
fn Ctx::wire(
self : Ctx,
owner : String,
role : @trace.ConnectorRole,
points : Array[Point],
label? : Label,
) -> Unit {
guard self.topology.connector_of(owner, role) is Some(connector) else {
self.failed = true
self.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/binding-mismatch", "A layout connector has no topology connector",
),
)
return
}
// Remove repeated points so every segment has a positive length.
let clean : Array[Point] = []
for p in points {
if clean.last() is Some(q) && q.x == p.x && q.y == p.y {
continue
}
clean.push(p)
}
self.wires[connector.id] = {
connector,
points: ReadOnlyArray::from_array(clean),
label,
}
}
///|
fn Ctx::entry_of(self : Ctx, id : String) -> Point {
self.points
.get(self.topology.entry(id).unwrap_or(""))
.unwrap_or({ x: 0.0, y: 0.0, })
}
///|
fn Ctx::exit_of(self : Ctx, id : String) -> Point {
self.points
.get(self.topology.exit(id).unwrap_or(""))
.unwrap_or({ x: 0.0, y: 0.0, })
}
///|
fn Ctx::set_point(
self : Ctx,
id : String,
side : String,
x : Double,
y : Double,
) -> Unit {
self.points[@trace.point_id(id, side)] = { x, y, }
}
///|
fn leaf_extent(
shape : Shape,
text : String,
above : Array[String],
below : Array[String],
) -> Frag {
if shape == Dot {
return { w: 24.0, up: 8.0, down: 8.0, }
}
let (tw, th) = label_extent(
text,
label_size,
if shape == Square {
600
} else {
400
},
)
let mut w = Double::max(tw + 20.0, 28.0)
let mut up = (th + 10.0) / 2.0
let mut down = up
for note in above {
let (nw, nh) = label_extent(note, small_size, 400)
w = Double::max(w, nw + 8.0)
up += nh + 4.0
}
for note in below {
let (nw, nh) = label_extent(note, small_size, 400)
w = Double::max(w, nw + 8.0)
down += nh + 4.0
}
{ w, up, down, }
}
///|
fn Ctx::measure(self : Ctx, id : String, depth : Int32) -> Frag {
let radius = natural_radius.max(self.limits.minimum_approach + 8.0)
if depth > self.limits.depth {
if !self.failed {
self.failed = true
self.diagnostics.push(
@diagrams.Diagnostic::error(
"railroad/layout-limit", "Railroad layout depth limit exceeded",
),
)
}
return { w: 0.0, up: 0.0, down: 0.0, }
}
if !self.charge(1) {
return { w: 0.0, up: 0.0, down: 0.0, }
}
let frag : Frag = match self.views.get(id) {
None => { w: 0.0, up: 0.0, down: 0.0, }
Some(Leaf(shape, text, above, below)) =>
leaf_extent(shape, text, above, below)
Some(Seq(items)) => {
let mut w = 0.0
let mut up = 0.0
let mut down = 0.0
for k, item in items {
let f = self.measure(item, depth + 1)
w += f.w + (if k > 0 { gap } else { 0.0 })
up = Double::max(up, f.up)
down = Double::max(down, f.down)
}
{ w, up, down, }
}
Some(Alt(items)) => {
let mut w = 0.0
let mut total = 0.0
let mut up = 0.0
for k, item in items {
let f = self.measure(item, depth + 1)
w = Double::max(w, f.w)
if k == 0 {
up = f.up
total = f.down
} else {
total += vertical + f.up + f.down
}
}
{ w: w + 4.0 * radius, up, down: total, }
}
Some(Wrap(child, bypass, back, label)) => {
let f = self.measure(child, depth + 1)
let label_h = match label {
Some(text) => label_extent(text, small_size, 400).1 + 2.0
None => 0.0
}
let label_w = match label {
Some(text) => label_extent(text, small_size, 400).0
None => 0.0
}
{
w: Double::max(f.w, label_w) + 4.0 * radius,
up: f.up + (if bypass { vertical } else { 0.0 }),
down: f.down + (if back { vertical } else { 0.0 }) + label_h,
}
}
Some(Group(child, label)) => {
let f = self.measure(child, depth + 1)
let (lw, lh) = label_extent(label, small_size, 400)
{
w: Double::max(f.w, lw) + 2.0 * frame_pad,
up: f.up + frame_pad + lh,
down: f.down + frame_pad,
}
}
Some(Look(child, label)) => {
let f = self.measure(child, depth + 1)
let (lw, _) = label_extent(label, small_size, 400)
{
w: Double::max(f.w, lw) + 2.0 * frame_pad,
up: 10.0,
down: 10.0 + frame_pad + f.up + f.down + frame_pad,
}
}
Some(Excl(left, excluded)) => {
let l = self.measure(left, depth + 1)
let e = self.measure(excluded, depth + 1)
let (lw, lh) = label_extent("except", small_size, 400)
let frame_w = Double::max(e.w, lw) + 2.0 * frame_pad
{
w: Double::max(l.w, frame_w),
up: l.up,
down: l.down + vertical + lh + frame_pad + e.up + e.down + frame_pad,
}
}
}
self.frags[id] = frag
frag
}
///|
fn Ctx::place(self : Ctx, id : String, x : Double, y : Double) -> Unit {
let radius = natural_radius.max(self.limits.minimum_approach + 8.0)
if self.failed || !self.charge(1) {
return
}
let frag = self.frags.get(id).unwrap_or({ w: 0.0, up: 0.0, down: 0.0, })
match self.views.get(id) {
None => ()
Some(Leaf(shape, text, above, below)) => {
guard self.topology.term(id) is Some(term) else { return }
let mut top = y - frag.up
let notes = []
for note in above {
let label = make_label(note, id, x + frag.w / 2.0, top, size=small_size)
notes.push(label)
top += label.box.height + 4.0
}
let (_, th) = if shape == Dot {
(0.0, 16.0)
} else {
label_extent(text, label_size, 0)
}
let box_h = if shape == Dot { 16.0 } else { th + 10.0 }
let rect : Rect = { x, y: y - box_h / 2.0, width: frag.w, height: box_h, }
let label = make_label(
text,
id,
x + frag.w / 2.0,
y - box_h / 2.0 + 5.0,
weight=if shape == Square { 600 } else { 400 },
)
let mut bottom = y + box_h / 2.0 + 4.0
for note in below {
let label = make_label(
note,
id,
x + frag.w / 2.0,
bottom,
size=small_size,
)
notes.push(label)
bottom += label.box.height + 4.0
}
self.points[term.entry] = { x, y, }
self.points[term.exit] = { x: x + frag.w, y, }
self.terms.push({
term,
shape,
rect,
label,
notes: ReadOnlyArray::from_array(notes),
})
}
Some(Seq(items)) => {
let mut cursor = x
for k, item in items {
let f = self.frags[item]
if k > 0 {
cursor += gap
}
self.place(item, cursor, y)
cursor += f.w
}
for k in 1.. {
self.set_point(id, "in", x, y)
self.set_point(id, "out", x + frag.w, y)
let mut baseline = y
for k, item in items {
let f = self.frags[item]
if k > 0 {
let prev = self.frags[items[k - 1]]
baseline += prev.down + vertical + f.up
}
self.place(item, x + 2.0 * radius, baseline)
let a = self.entry_of(item)
let b = self.exit_of(item)
if k == 0 {
self.wire(id, Split(k), [{ x, y, }, a])
self.wire(id, Join(k), [b, { x: x + frag.w, y, }])
} else {
self.wire(id, Split(k), [
{ x, y, },
{ x: x + radius, y, },
{ x: x + radius, y: baseline, },
a,
])
self.wire(id, Join(k), [
b,
{ x: x + frag.w - radius, y: baseline, },
{ x: x + frag.w - radius, y, },
{ x: x + frag.w, y, },
])
}
}
}
Some(Wrap(child, bypass, back, text)) => {
self.set_point(id, "in", x, y)
self.set_point(id, "out", x + frag.w, y)
let f = self.frags[child]
self.place(child, x + 2.0 * radius, y)
let a = self.entry_of(child)
let b = self.exit_of(child)
self.wire(id, Enter, [{ x, y, }, a])
self.wire(id, Leave, [b, { x: x + frag.w, y, }])
if bypass {
let top = y - f.up - vertical
self.wire(id, Bypass, [
{ x, y, },
{ x: x + radius, y, },
{ x: x + radius, y: top, },
{ x: x + frag.w - radius, y: top, },
{ x: x + frag.w - radius, y, },
{ x: x + frag.w, y, },
])
}
if back {
let bottom = y + f.down + vertical
let label = text.map(t => {
make_label(t, id, x + frag.w / 2.0, bottom + 2.0, size=small_size)
})
self.wire(
id,
Return,
[
b,
{ x: x + frag.w - radius, y: b.y, },
{ x: x + frag.w - radius, y: bottom, },
{ x: x + radius, y: bottom, },
{ x: x + radius, y: a.y, },
a,
],
label?,
)
} else if text is Some(t) {
// A count without a loop keeps its label below the term.
let bottom = y + f.down + 2.0
let label = make_label(t, id, x + frag.w / 2.0, bottom, size=small_size)
self.frames.push({
owner: id,
role: Group,
rect: {
x: x + radius,
y: y - f.up - 4.0,
width: frag.w - 2.0 * radius,
height: f.up + f.down + 8.0 + label.box.height,
},
label,
})
}
}
Some(Group(child, text)) => {
self.set_point(id, "in", x, y)
self.set_point(id, "out", x + frag.w, y)
let f = self.frags[child]
let (_, lh) = label_extent(text, small_size, 400)
let top = y - f.up - frame_pad - lh
let label = make_label(
text,
id,
x + frag.w / 2.0,
top + 2.0,
size=small_size,
)
self.frames.push({
owner: id,
role: Group,
rect: { x, y: top, width: frag.w, height: frag.up + frag.down, },
label,
})
self.place(child, x + frame_pad, y)
self.wire(id, Enter, [{ x, y, }, self.entry_of(child)])
self.wire(id, Leave, [self.exit_of(child), { x: x + frag.w, y, }])
}
Some(Look(child, text)) => {
guard self.topology.term(id) is Some(term) else { return }
let f = self.frags[child]
let rect : Rect = {
x,
y: y - frag.up,
width: frag.w,
height: frag.up + frag.down,
}
let label = make_label(
text,
id,
x + frag.w / 2.0,
y - 8.0,
size=small_size,
)
self.points[term.entry] = { x, y, }
self.points[term.exit] = { x: x + frag.w, y, }
self.terms.push({ term, shape: Lookaround, rect, label, notes: [], })
let inner = y + 10.0 + frame_pad + f.up
self.set_point(id, "inner-in", x + 4.0, inner)
self.set_point(id, "inner-out", x + frag.w - 4.0, inner)
self.place(child, x + frame_pad, inner)
self.wire(id, Enter, [{ x: x + 4.0, y: inner, }, self.entry_of(child)])
self.wire(id, Leave, [
self.exit_of(child),
{ x: x + frag.w - 4.0, y: inner, },
])
}
Some(Excl(left, excluded)) => {
let l = self.frags[left]
let e = self.frags[excluded]
self.place(left, x, y)
let (lw, lh) = label_extent("except", small_size, 400)
let frame_w = Double::max(e.w, lw) + 2.0 * frame_pad
let top = y + l.down + vertical
let height = lh + frame_pad + e.up + e.down + frame_pad
let label = make_label(
"except",
id,
x + frame_w / 2.0,
top + 2.0,
size=small_size,
)
self.frames.push({
owner: id,
role: Except,
rect: { x, y: top, width: frame_w, height, },
label,
})
let inner = top + lh + frame_pad + e.up
self.set_point(id, "except-in", x + 4.0, inner)
self.set_point(id, "except-out", x + frame_w - 4.0, inner)
self.place(excluded, x + frame_pad, inner)
self.wire(id, Enter, [{ x: x + 4.0, y: inner, }, self.entry_of(excluded)])
self.wire(id, Leave, [
self.exit_of(excluded),
{ x: x + frame_w - 4.0, y: inner, },
])
}
}
}
///|
fn regex_views(model : @regex.Regex) -> Map[String, View] {
let views : Map[String, View] = Map([])
for node in model.nodes {
let view : View = match node.kind {
Literal(text) => Leaf(Pill, text, [], [])
Class(_) | Backreference(_) => Leaf(Class, node.label(), [], [])
Set(negated, items) => Leaf(Class, set_label(negated, items), [], [])
Anchor(kind) => Leaf(Anchor, anchor_label(kind), [], [])
Sequence(items) => Seq(items)
Choice(items) => Alt(items)
Group(kind, child) =>
Group(
child,
match kind {
Capturing => "group"
NonCapturing => "non-capturing group"
Named(name) => "group " + name
},
)
Repeat(bounds, child) => {
let plain = !bounds.reluctant &&
(
bounds.spelling == "*" ||
bounds.spelling == "+" ||
bounds.spelling == "?"
)
Wrap(
child,
bounds.min == 0,
bounds.max is None || bounds.max.unwrap_or(0) > 1,
if plain {
None
} else {
Some(
bounds.spelling +
(if bounds.reluctant { " reluctant" } else { "" }),
)
},
)
}
Assertion(look, positive, child) =>
Look(
child,
match (look, positive) {
(Ahead, true) => "lookahead ?="
(Ahead, false) => "negative lookahead ?!"
(Behind, true) => "lookbehind ?<="
(Behind, false) => "negative lookbehind ? Map[String, View] {
let views : Map[String, View] = Map([])
for node in grammar.nodes {
let above = node.notes_above.iter().collect()
let below = node.notes_below.iter().collect()
let view : View = match node.kind {
Terminal(text, _) => Leaf(Pill, text, above, below)
Reference(name, _) => Leaf(Square, name, above, below)
Special(text) => Leaf(Special, text, above, below)
Empty => Leaf(Dot, "", above, below)
Sequence(items) => Seq(items)
Choice(items) => Alt(items)
Optional(child) => Wrap(child, true, false, None)
ZeroOrMore(child) => Wrap(child, true, true, None)
OneOrMore(child) => Wrap(child, false, true, None)
Count(count, child) =>
Wrap(
child,
count == "0",
count != "0" && count != "1",
Some(count + "\u{d7}"),
)
Exclusion(left, excluded) => Excl(left, excluded)
}
views[node.id] = view
}
views
}
///|
priv struct Row {
items : Array[String]
mut up : Double
mut down : Double
mut width : Double
}
///|
/// Lay out a regex or grammar railroad. Rules stay in source order; each
/// drawn connector of the topology gets one wire.
pub fn layout(model : Model, limits~ : Limits = Limits::new()) -> Result {
let radius = natural_radius.max(limits.minimum_approach + 8.0)
fn failed(code : String, message : String) -> Result {
{
geometry: None,
diagnostics: [@diagrams.Diagnostic::error(code, message)],
}
}
if limits.nodes <= 0 ||
limits.depth <= 0 ||
limits.work <= 0 ||
limits.width <= 0 ||
limits.output_units <= 0 {
return failed("railroad/layout-limit", "Railroad limits must be positive")
}
let built = match model {
Regex(m) =>
@trace.Topology::regex(
m,
limits=@regex.Limits::new(
nodes=limits.nodes,
depth=limits.depth,
work=limits.work,
),
)
Ebnf(g) =>
@trace.Topology::ebnf(
g,
limits=@ebnf.Limits::new(
nodes=limits.nodes,
depth=limits.depth,
work=limits.work,
),
)
}
guard built.topology is Some(topology) else {
return { geometry: None, diagnostics: built.diagnostics, }
}
let ctx = Ctx::{
topology,
views: match model {
Regex(m) => regex_views(m)
Ebnf(g) => ebnf_views(g)
},
frags: Map([]),
points: Map([]),
terms: [],
frames: [],
wires: Map([]),
limits,
diagnostics: [],
work: 0,
failed: false,
}
let margin = 20.0
let mut y = margin
let mut width = 0.0
let title_text = match model {
Regex(m) => m.title
Ebnf(g) => g.title
}
// Width is fixed after the content is known; place the title afterwards.
if title_text is Some(t) {
let (_, h) = label_extent(t, 16.0, 700)
y += h + 12.0
}
let tracks : Array[TrackBox] = []
let notes : Array[NoteBox] = []
let bodies : Map[String, String?] = Map([])
let rule_notes : Map[String, (String?, String?)] = Map([])
let order : Array[(String, String?)] = []
match model {
Regex(m) => {
bodies[m.id] = Some(m.root)
order.push((m.id, None))
}
Ebnf(g) =>
for item in g.items {
match item {
Rule(rule) => {
bodies[rule.id] = rule.body
rule_notes[rule.id] = (rule.note_above, rule.note_below)
order.push((rule.id, None))
}
Note(id, text) => order.push((id, Some(text)))
}
}
}
let track_index : Map[String, @trace.Track] = Map([])
for track in topology.tracks {
track_index[track.id] = track
}
for entry in order {
if ctx.failed {
break
}
match entry {
(id, Some(text)) => {
let (w, h) = label_extent(text, small_size, 400)
let rect : Rect = { x: margin, y, width: w + 20.0, height: h + 12.0, }
let label = make_label(
text,
id,
margin + rect.width / 2.0,
y + 6.0,
size=small_size,
)
notes.push({ id, rect, label, })
width = Double::max(width, rect.x + rect.width)
y += rect.height + 24.0
}
(id, None) => {
guard track_index.get(id) is Some(track) else { continue }
let track_notes = []
let (above, below) = rule_notes.get(id).unwrap_or((None, None))
if above is Some(text) {
let label = make_label(text, id, margin, y, size=small_size)
let shifted = {
..label,
x: margin + label.box.width / 2.0,
box: { ..label.box, x: margin, },
}
track_notes.push(shifted)
y += label.box.height + 4.0
}
let name = make_label(
if model is Regex(_) {
"pattern"
} else {
track.name
},
id,
margin,
y,
size=14.0,
weight=700,
)
let name = {
..name,
x: margin + name.box.width / 2.0,
box: { ..name.box, x: margin, },
}
y += name.box.height + 6.0
if below is Some(text) {
let label = make_label(text, id, margin, y, size=small_size)
let shifted = {
..label,
x: margin + label.box.width / 2.0,
box: { ..label.box, x: margin, },
}
track_notes.push(shifted)
y += label.box.height + 4.0
}
let x0 = margin + 10.0
match bodies.get(id).unwrap_or(None) {
None => {
let by = y + 10.0
let start : Point = { x: x0, y: by, }
let end : Point = { x: x0 + 2.0 * gap, y: by, }
ctx.points[track.start] = start
ctx.points[track.end] = end
ctx.wire(id, Empty, [start, end])
tracks.push({
track,
label: name,
start,
end,
notes: ReadOnlyArray::from_array(track_notes),
})
width = Double::max(width, end.x + 10.0)
y = by + 10.0 + 24.0
}
Some(body) => {
// Measure every node of this track.
let _ = ctx.measure(body, 0)
// A top-level sequence wider than the limit continues on new rows.
let rows : Array[Row] = []
match ctx.views.get(body) {
Some(Seq(items)) => {
let mut row : Row = {
items: [],
up: 0.0,
down: 0.0,
width: 0.0,
}
for item in items {
let f = ctx.frags[item]
let extra = if row.items.is_empty() { f.w } else { gap + f.w }
if !row.items.is_empty() &&
row.width + extra > limits.width.to_double() {
rows.push(row)
row = { items: [], up: 0.0, down: 0.0, width: 0.0, }
}
row.items.push(item)
row.width += if row.items.length() == 1 {
f.w
} else {
gap + f.w
}
row.up = Double::max(row.up, f.up)
row.down = Double::max(row.down, f.down)
}
rows.push(row)
}
_ => ()
}
let body_x = x0 + gap
let mut by = y +
(if rows.length() > 1 { rows[0].up } else { ctx.frags[body].up })
let start : Point = { x: x0, y: by, }
ctx.points[track.start] = start
let mut last_x = 0.0
let mut bottom = 0.0
let baselines : Array[Double] = []
if rows.length() > 1 {
for r, row in rows {
if r > 0 {
by += rows[r - 1].down + 2.0 * vertical + row.up
}
baselines.push(by)
let mut cursor = body_x
for k, item in row.items {
if k > 0 {
cursor += gap
}
ctx.place(item, cursor, by)
cursor += ctx.frags[item].w
}
last_x = Double::max(last_x, cursor)
bottom = by + row.down
}
let items = match ctx.views.get(body) {
Some(Seq(items)) => items
_ => []
}
let first_of_row : Map[String, Int32] = Map([])
for r, row in rows {
if r > 0 {
first_of_row[row.items[0]] = r
}
}
for k in 1..
Double::max(content_width, label_extent(t, 16.0, 700).0 + 2.0 * margin)
None => content_width
}
let title = title_text.map(t => {
make_label(t, "title", total_width / 2.0, margin, size=16.0, weight=700)
})
// Wires in topology order, so output order is deterministic.
let wires = []
for connector in topology.connectors {
match ctx.wires.get(connector.id) {
Some(w) => wires.push(w)
None =>
return failed(
"railroad/binding-mismatch",
"Connector '\{connector.id}' has no drawn wire",
)
}
}
{
geometry: Some({
topology,
width: total_width.ceil(),
height: (y - 24.0 + margin).ceil(),
title,
tracks: ReadOnlyArray::from_array(tracks),
terms: ReadOnlyArray::from_array(ctx.terms),
frames: ReadOnlyArray::from_array(ctx.frames),
wires: ReadOnlyArray::from_array(wires),
notes: ReadOnlyArray::from_array(notes),
point_index: ctx.points,
}),
diagnostics: ReadOnlyArray::from_array(ctx.diagnostics),
}
}