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:

PartTokens
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).

OpMarkers / 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 mirrora +-< b is one-to-many, a >o-o< b zero-or-many both ways. The six relations, shown end-side:

OpRelation
-+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, crowSPEC 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
a b hi

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 [ ]:

PropertyNotes
alongA 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
catroot node cat
kitchen.counter.bowlexactly that path
kitchen.counter.bowl:leftthe 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.

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"
Inlet Outlet Garden Inlet Outlet Kitchen carries flows flows

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 layoutlayout 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.