9. Links
A link connects scene-node ids with an operator (a -> b). Like every node it has a
{ } style and a [ ] of content — its content is its labels (text),
placed along the route by along:. It is never written as a |link| instance; the
operator draws it.
A link is styled like a node: its type is |-| — a line in the identity capsule,
the one selector that matches every link — so stroke is its wire and color /
font-* its labels, the ordinary vocabulary (SPEC 6) with no
parallel family. Only clearance and routing stay scene config
(Styling).
Operators
A link op is [start_marker?][line][end_marker?], no spaces:
| Part | Tokens |
|---|---|
| Line | - solid · -- dashed · --- dotted · ~ wavy |
| Start markers | < arrow · > crow · * dot · <> diamond · +/o ER cardinality (below) |
| End markers | > arrow · < crow · * dot · <> diamond · +/o ER cardinality (below) |
The same marker glyph differs by position (< is arrow at the start, crow at the
end).
| Op | Markers / Line |
|---|---|
-> <- <-> | arrow combinations, solid |
-* *- *-* | dot combinations |
-<> <>-<> | diamond |
-< -+ -o< -+< -o+ -++ >-< | ER cardinality (crow’s-foot, below) |
--> ---> ~> | dashed / dotted / wavy |
- -- --- ~ | no markers (each line style) |
An operator with no marker glyphs leaves both ends bare. Explicit marker: /
marker-start: / marker-end: override the operator (source order wins). The
operator’s line part sets the link’s stroke-style (-- ⇒ dashed, --- ⇒ dotted,
~ ⇒ wavy); an explicit stroke-style: overrides it.
ER cardinality — a crow’s-foot marker, composed. A cardinality marker reads
[min][max]: the min ring o (zero) or bar + (one) hugs the line, the max
bar + (one) or crow (many — < at the end, > at the start) sits outermost. Either
end takes one; the two sides mirror — a +-< b is one-to-many, a >o-o< b zero-or-many
both ways. The six relations, shown end-side:
| Op | Relation |
|---|---|
-+ | one |
-< | many |
-o+ | zero-or-one |
-+< | one-or-many |
-o< | zero-or-many |
-++ | exactly one |
A lone -o (no max) errors; the hollow ring exists only inside the ER
cardinality glyphs and has no standalone endpoint form. The ops are sugar over the marker: set (one,
exactly-one, zero-or-one, one-or-many, zero-or-many, crow —
SPEC 7); marker*: overrides.
Syntax
endpoints op endpoints [op endpoints …] [ "label" ] [ .class… ] [ { style } ] [ [ labels ] ]
The tail is the node tail ("label" .class { style } [ … ]); only the head differs
— endpoints + operators, versus bars — and a link’s [ ] holds only labels (text),
where a node’s holds children (a drawing’s dimensions and leaders alone may also
carry annotation nodes there — SPEC 15.9).
endpoints is one or more endpoints joined by &:
a -> b // 1 link
a -> b -> c // chain: 2 links
a -> b & c // fan-out: a→b, a→c
a & b -> c // fan-in
a & b -> c & d // cartesian: 4 links
a -> b -> c & d // chain + fan
Each hop carries its own wire operator; mixing operator kinds — a wire op with
a measure or mate — in one chain is a parse error. On a chain or fan, the label,
class, and { } apply to every link the statement expands to.
A chain marks every hop. a -> b -> c is exactly a -> b; b -> c — desugar
expands the chain (SPEC 19), so each hop carries the
operator’s full markers and lini desugar shows the two links. A bare first hop
is spelled with the bare line op: a - b -> c. (Fan-out & is not sugar — its
shared trunk is routing geometry, ROUTING.md.) A schematic
scope is the one carve-out (SPEC 16.5).
Styling
The vocabulary is SPEC 6’s, at the ordinary defaults
(SPEC 17): stroke / stroke-width /
stroke-style dress the wire (the style usually set by the operator, above),
color and the font-* family its labels (Labels).
|-| { … } styles every link; a descendant (#g |-|, |table| |-|) or a worn class
scopes it, exactly as |box| / #g |box| / .hot scope a node; a link’s own { }
overrides — the same cascade a node walks (SPEC 4):
{
|-| { stroke: #888; stroke-width: 1.5; font-size: 12 } // every link
#g |-| { stroke: --blue } // links written in #g
.flow { stroke: --teal } // a worn class — nodes or links
clearance: 12; routing: orthogonal // scene config, cascades to links
}
a -> b "hi" .flow { stroke: red; stroke-style: dashed } // one link overrides
clearance and routing are scene config — geometry, not paint — set on a
container’s { }, cascading to that scope’s links, nearest winning; the router then inflates every
node’s keep-out by the maximum clearance any link carries
(ROUTING.md). marker* come from the operator and
override per link.
Labels
A link’s label is text, placed along the route by along: — the link’s track
rule, exactly as columns: is a grid’s. One label trails the head (a -> b "watches"); two or more, or a styled one, ride the [ ]:
| Property | Notes |
|---|---|
along | A list of 0..1 fractions along the whole drawn route, one per label (along: 0.2, 0.5, 0.8). Omitted → auto-distribute across the hops, so one label avoids junctions and several spread out. |
a -> b "watches" // the common case — one label, auto-placed
a -> b "watches" .loud { stroke: red } // + a class and wire colour
a -> b { along: 0.3, 0.7 } [ "near a" "near b" ] // two labels
a -> b [ "watches" { translate: 0 -6 } ] // a styled / nudged label
Each label is an ordinary styleable text leaf; the head label takes no style
(SPEC 3) — a styled label rides the [ ], exactly as a
node’s does. Keep one link’s labels in one [ ] — a head label and a
[ ] of labels on the same link warns (SPEC 21). A label is an obstacle to nothing, and may slide along the link to keep
clear of nodes and other labels; the link never moves for it. A label cuts its
wire: the route is masked open behind the label’s box — the same luminance
knockout a drawing’s |halo| puts where annotation linework crosses geometry
(SPEC 15.7), so the break holds over any
background and in either theme — and it cuts every wire the box reaches, not
only the labelled one. A schematic sheet draws the other convention, standing the
net name beside the trace (SPEC 16.5). Link labels ride the
chrome size (SPEC 6) at font-weight: normal; a link’s text props
cascade to its labels
(|-| { font-size: 14; color: --blue } restyles every link’s labels at once,
absolutely).
Endpoints & scope
endpoint = ( ident | ident_bars ) { "." ident } [ ":" side ]
side = top | bottom | left | right
A path walks with . into children; a final :side forces a side. An
endpoint may open with an identity capsule instead of an id — see
Capsule endpoints below. Every link
resolves in a scope — the scene root for top-level links, the container’s body for
links written inside one. The first segment names a node in the scope, each further
segment a child of the previous. There is no search. A single bare id not in the
scope auto-creates a box there (Implicit nodes); a multi-segment
path that does not resolve is an error, and the error suggests full paths of
same-named nodes —
link endpoint 'kitchen.bowl' not found at scene root; did you mean 'kitchen.counter.bowl'?
| Endpoint (root link) | Resolves to |
|---|---|
cat | root node cat |
kitchen.counter.bowl | exactly that path |
kitchen.counter.bowl:left | the same node, left side forced |
Capsule endpoints
An endpoint position may hold an identity capsule — bars, exactly as a
declaration writes them: |type| or |type#id|. A capsule declares and
links in one statement: desugar hoists it to an ordinary declaration at
the statement’s position in its scope and the link references it
(SPEC 19), so it is the typed form of
declaration-at-first-use (SPEC 3):
cat -> |cyl#db| // declare db (empty, per SPEC 3), link to it
cat -> |cyl#db| "watches" { … } // the tail is the LINK's, as always
a -> |box| -> c // anonymous mid-chain — a minted internal id
a & b -> |gnd| // a fan into ONE instance
Three existing laws govern it, none new:
- A statement’s tail belongs to its head (SPEC 3) — a capsule takes no label, class, style, or children; everything after it is the link’s.
- Identity travels, dress doesn’t (SPEC 1) — the id inside the bars comes along; classes never sat inside bars.
- Declared nodes are empty unless labelled (SPEC 3) — a
define supplies intrinsic content (
|vm::label| { symbol: power } [ "VM" ]).
The capsule composes with the rest of the endpoint’s anatomy (.path,
.index, :side) — though an inline capsule has no authored pins, so a
pin path on one (|component#U9|.p4) is an error; anonymous capsules mint
reserved internal ids (lini-cap-N); an id’d capsule declared twice is the
ordinary duplicate-id error. At statement head, a capsule followed by a link
operator opens a link (SPEC 1); followed by anything else
it is the node declaration it always was. A drawing scope rejects
capsules — a drawing never invents an endpoint (SPEC 15); a
sequence accepts them (a typed participant).
Bodies are sealed: a body link connects nodes of its own subtree only.
Cross-container links are written at the lowest level where both ends are visible —
usually the root. Without a side the router picks edges by geometry; with a :side,
that edge is forced.
An anonymous container opens no scope: it is scope-transparent — its
children belong to its parent’s scope (ids stay unique across it), a dot-path
never names it, and its own [ ] links resolve in the parent’s scope. Name a
container to give its children a dot-path of their own. A sequence frame is
transparent the same way (SPEC 13). Scope-transparency is
about names, not geometry: the router sees the container itself, so links
route inside an anonymous group exactly as inside a named one; its scene
config (clearance:, routing:) cascades onto the links written in it
(ROUTING.md Model step 1); and a layout-owning container
realises the statements written in it whether or not it is named — an
anonymous |drawing| draws its own dimensions, an anonymous |sequence| lays
its own messages on the time axis. The wiring strategy follows the container
that wrote the statement (SPEC 11, seam 2), never the
dot-path its endpoints resolve against.
Internal links in a body
A container’s (or define’s) [ ] may link its own children — children and links read in
source order, so a wire usually trails the boxes it joins but may also sit among them
(a layout: sequence (SPEC 13) relies on this — its frames interleave with its
messages). In a define, ids are local and materialize per instance — the same sealed-body
rule. From outside, the dot-path navigates in:
{
|room::group| {
gap: 10;
} [
|box#inlet| "Inlet"
|box#outlet| "Outlet"
inlet -> outlet "flows"
]
}
|room#garden| "Garden"
|room#kitchen| "Kitchen"
garden.outlet -> kitchen.inlet "carries"
Routing
routing selects the strategy for a scope and cascades like clearance:
orthogonal (the default) routes horizontal/vertical runs through the free space
between nodes, corners rounded; natural fits direct smooth curves —
tangent-normal at both ends, bending gently around what they would hit, free to
cross (ROUTING.md); straight draws each link as one
segment between the bodies, trimmed to their boundaries — it avoids nothing and
reports nothing. routing pairs with
layout — layout places the nodes, routing wires them — so a group can route
its internals one way while the root routes another; which subsystem realises a
scope’s links is the scope’s wiring strategy (SPEC 11).
The full routing contract — clearance, spacing, crossings, fan-out, self-loops —
lives in ROUTING.md, the source of truth for routing.