--- name: lini description: Use when asked to create, edit, review, or debug a Lini diagram (a .lini file or its SVG) — architecture/box-and-arrow diagrams, flowcharts, mindmaps, org charts, ER schemas, tables, sequence diagrams, bar/line/area/pie/radar/scatter charts, engineering drawings, floor plans, circuit schematics, or pen-drawn artwork written in the Lini language. --- # Lini — writing beautiful diagrams Lini compiles plain text to clean, themeable SVG: composable nodes, a CSS-like cascade, compile-time layout. One core drives every diagram family. This file is self-sufficient for real work; `SPEC.md` (the full language), `ROUTING.md` (wire geometry), and `samples/` (the showroom — one file per feature cluster) go deeper. ## The loop Write → compile → **look at the render** → refine. Never ship a diagram you haven't seen. ```sh lini d.lini -o d.svg # compile; errors are file:line:col with fixes lini --check --strict d.lini # full compile, nothing written; warnings fail — the pre-finish gate lini fmt d.lini # canonical formatting in place (--check: exit 1 if it would change) lini --static d.lini -o s.svg && resvg s.svg d.png # rasterize, then READ d.png ``` `--static` inlines CSS variables and outlines text — required before `resvg` (it can't resolve `var()`). Outlining covers the bundled Latin charset; a run with CJK, Arabic, arrows, or emoji stays `` and warns (`O001`) — check the PNG for it. `--strict` / `--no-warn` bind every form (SVG, `--check`, `--json` diagnostics for tooling). Also: `--watch` (with `-o`), `--format html`, `--embed-font` (browser-only `@font-face`), `--theme NAME|FILE|light/dark`. Exit codes: 0 ok · 1 error (or `fmt --check` would reformat) · 2 I/O · 3 bad CLI. In this repo the binary is `target/release/lini` (`cargo build --release` if missing). `lini desugar d.lini` prints the lowered form when sugar confuses; `lini serve` opens a live playground. ## A complete file A file is **one optional `{ }` stylesheet, then drawn statements** in source order. The stylesheet configures and styles; it draws nothing, and must come first. ``` { // the stylesheet — setup only layout: grid; columns: repeat(2); gap: 30; // scene config (root declarations) --brand: #ff6600; // a themeable colour variable w = 120; scale(n) = (100 * 1.2^n); // bindings — baked numbers / functions |box| { radius: 6; } // a rule: style every box |-| { stroke: --gray-deep; } // a rule: style every link .hot { stroke: --red-deep; } // a class definition |svc::box| { fill: --teal-wash; stroke: --teal-ink; } // a define: new type over a base |room::group| { gap: 40; } [ // a define with a BODY: intrinsic children + links, |box#in| "in"; |box#out| "out" // re-materialised per instance; ids local, in -> out "flows" // reachable from outside as `garden.out` ] } |svc#api| "API" // instances — the canvas |cyl#db| "Postgres" { fill: --rose-wash; } |room#garden| api -> db "queries" .hot // a link with a label and a worn class users -> api // undeclared id → auto-creates |box#users| "users" garden.out -> api // a dot-path into a body ``` Every drawn statement is a node (`|…|`), a text leaf (`"…"`), or a link (bare name + operator). The full node anatomy — **only the bars are required, order fixed** — is `|type#id| "label" .class1.class2 { key: value; } [ children ]`; a link takes the same tail on a different head: `a -> b "label" .cls { } [ ]`. ## Syntax laws that bite - **Declarations end with `;`** and live only inside `{ }`. A value runs to its `;`, so it may span lines. (`;` optional right before `}`.) - **A statement ends at a newline or `;`** — two nodes on one line need the `;`: `|topic| "A"; |topic| "B"`. Bare strings are self-delimiting (`"a" "b"` is two text leaves). A `{ }` or `[ ]` may span lines freely. - **Text is always double-quoted**; escapes `\" \\ \n \t`; leading/trailing spaces are trimmed. A bare word is an identifier (keyword, colour name, id). Single quotes are not strings. String-valued properties (`title`, `hint`, `href`, `src`) need quotes even for one word. - **The comma law**: commas separate repeated list items, spaces separate the components of one item. `data: 9, 15, 24` (three values) · `data: 10 20, 30 40` (two x-y points) · `padding: 5 2 5 5` (one four-part value: `N` · `v h` · `t r b l`) · `gap: 8 0` (row col — flow, grid, sequence alike) · `translate: 10 -4`. - **Math needs parens** — operators appear only inside `(…)`: `padding: (8 * 2);` `width: (w / 2)`. **A call's own parens count**, so an operator inside a call's arguments needs no inner group: `move(-tail - 1, -y)`, `right(w / 2)`, `pattern: grid(1, 3, 0, pitch)`. Calls are bare (`width: scale(3)`), signed numbers are bare (`translate: -35 20`). Inside a group: `+ - * / ^`, comparisons, `a ? b : c`, `pi`/`e`, `1e-6`, locals (`r = 40; 2 * r`), a top-level `,` makes a point, and the math library — `sqrt exp ln log abs sin cos tan min max clamp floor round pow`. Stylesheet bindings (`w = 120;` `wave(a, f) = (u * 320, a * sin(2 * pi * f * u));`) read bare anywhere a value goes — `stroke-width: w`, `move(w, 0)`, `pattern: grid(1, 3, 0, pitch)`. - **Parametric geometry**: a `points:` value may be one expression in the ambient clock `u` (0 → 1), sampled `samples:` times — `|line| { points: (u * 320, 24 * sin(2 * pi * 3 * u)); samples: 64 }`, or `points: wave(18, 2)` from a point-valued binding. Charts bind `x` the same way (below). - **A class is worn, never glued into bars**: `|box| .hot` — not `|box.hot|`. The label comes before classes: `|box#a| "A" .hot`. First class spaced off the head, further ones glued: `.hot.loud`. - **`--name` variables are visual only** (colours, `font-family`). Sizes, gaps, padding, `font-size`, `letter-spacing`, `line-spacing`, `text-transform` bake at compile time — literals or bindings, never variables. - **No coordinate property.** Layout places nodes; to place absolutely use `pin: center; translate: x y` (parent-local coordinates, y grows down) — or `layout: stack` (below) when the whole scene is hand-placed. - **No `text-align`** — a text's lines align by its container's *horizontal* packing knob: `justify` in a row, `align` in a column or grid — so `align` in a card, which stacks. Split intents: wrap text in its own `|block| { justify: start; }`. - **`id.child` paths glue** (no spaces): `kitchen.bowl`. `a:left` forces a link side. Paths resolve exactly in scope — never searched, never auto-created. An **anonymous** container is scope-transparent: its children belong to the enclosing scope and no path names it. - Comments are `// …` only. Identifiers are `[a-zA-Z_][a-zA-Z0-9_-]*`, case-sensitive. Ids may not start with `lini-`. ## Cascade Five tiers, most specific wins, ties → later wins: type defaults/rules `|box| { }` → descendant rules `|table| |box| { }` → class rules `.hot { }` → id rule `#hero { }` → the instance's own `{ }` block. Links walk the same ladder via `|-|` (`#g |-| { }` styles links written in `#g`); `(-) { }` is the dimension subtype and beats `|-| { }` for dimensions only. Values replace wholesale (no per-component merge). Text properties (`font-*`, `color`, spacing, transform) plus `clearance`/`routing`/`format`/`thickness` also **inherit** down the tree, nearest ancestor wins — set `font-size` once on the root and everything scales, captions and link labels included. ## Box model & placement - **Center origin**; source order = paint order (later on top; `layer: N` overrides). - **Auto-size**: box = content + `padding` each side (default 20 on framed boxes, 0 on `|block|`). Explicit `width`/`height` are **floors** — content never clips. Empty auto box = 2×padding (40×40). - **`max-width: N`** wraps text to fit (`text-wrap: nowrap` forbids); the wrapped size is the measured size. - **`pin`** lifts a child out of flow onto a parent anchor: `center`, edges (`top` …), corners (`top left` …). A pinned child is an overlay — paints above, never grows the parent. - **`translate: x y`** nudges any node after placement (layout-neutral); **`rotate: N`** turns about the bbox centre. Both work on text too. - Flow containers: `direction: row | column` (default row — source order flows the way it reads; a closed shape's and a `|topic|`'s children are **card content** and stack instead, so an icon sits over its label), `gap` (default 36; 12 in card content; `gap: row col`), `align` (cross axis) / `justify` (main axis): `start | center | end | stretch | evenly | origin` — no-ops without slack (explicit size or fixed tracks). Inside a drawing/floorplan/stack scope a block that must lay out its own content states `layout: flow` itself (`|room::block| { layout: flow; direction: column; align: center }`). - Grid: `columns` **required** — `columns: 80, auto, repeat(3), repeat(5, 80)`; `rows` optional; children auto-flow, or `cell: col row` / `span: cols rows` (`span: 2` = `2 1`) on any non-text child (bare text can't carry them — wrap it in `|block|`). Empty `""` holds a grid cell. Per-column alignment: `align: start, center, end` (one entry per track). - `gap-fill: colour` paints the gutters (`gap: 1; gap-fill: --stroke` = hairline rules — how `|table|` works). ## Node catalogue Primitives: `|block|` (frameless base rect), `|oval|` (equal sides = circle: `|oval| { width: 40 }`), `|hex|`, `|slant|`, `|cyl|`, `|diamond|`, `|poly|` (`points:`), `|line|` (`points:`, `marker*:`), `|path|` (raw SVG `path:`), `|image|` (`src:` + `width`/`height`; local files embed; `fit: auto | contain | cover | stretch`), `|icon|` (a Phosphor icon), `|sketch|` (`draw:` pen — see Drawing). Text is not a node type — a bare `"…"` is a text leaf; wrap it in `|block|` when it needs an id, border, padding, or pin. Templates (all overridable; extend with `|name::base| { … }`): | Type | What it is | |---|---| | `\|box\|` | the default: rounded framed card (radius 8, padding 20) | | `\|rect\|` | sharp-cornered box | | `\|group\|` | dashed light frame for a captioned region | | `\|caption\|` · `\|footnote\|` · `\|sheet-caption\|` | small muted title pinned above the top-left corner (a group/table **label becomes one**) · at the bottom centre · inside the top-left corner (a schematic scope's label lowers to it) | | `\|badge\|` | small accent pill pinned over the top-right corner | | `\|row\|` / `\|column\|` / `\|grid\|` / `\|stack\|` | frameless layout wrappers | | `\|icon\|` / `\|sign\|` | Phosphor icon: **`symbol: bell` or the label names the glyph — never both** (`\|icon\| "user"` *is* the user glyph, unnamed on the page; a captioned icon is a box wrapping one: `\|box#cdn\| "CDN" [ \|icon\| { symbol: cloud } ]`). `\|sign\|` is the 64px standalone preset (`fit: contain`). **Icons paint with `fill` (body) + `stroke` (line)** — `color:` does nothing. An icon's `[ ]` text rides *on* the symbol as a badge and grows the square: `\|icon\| { symbol: bell } [ "3" ]` | | `\|table\|` | ruled grid; first row auto-becomes the header band; cells via bare strings | | `\|entity\|` | ER card: label = centred title, rows = `"field" "type"` (3 columns for a key gutter) | | `\|note\|` | folded-corner callout card (works in every layout) | | `\|topic\|` / `\|mindmap\|` | tree structure node / the full mindmap preset | | `\|chart\|` / `\|pie\|` / `\|sequence\|` / `\|drawing\|` / `\|floorplan\|` / `\|schematic\|` | layout presets (below) | Shape extras: `multiple: N` (one offset duplicate behind — "several of these"; `N` is the offset, not a count), `shadow: dx dy blur`, `stroke-style: solid|dashed|dotted` (+ drafting `center`/`phantom` on shapes, `wavy` on links only) — **one node, one stroke style**: dashed interior geometry is its own child — `opacity: 0.75`. `href: "url"` makes anything clickable; `hint: "…"` adds a tooltip/accessible ``. A standalone arrow is `|line| { points: 0 0, 50 0; marker-end: arrow; }`. ## Links ``` a -> b // 1 link (labels boxes into existence) a -> b -> c // chain: 2 links, every hop marked a -> b & c // fan-out — shares one trunk at a's side a & b -> c // fan-in |group#g| [ |box#child| "C" ] a:right -> g.child:left "label" // forced sides, path endpoint, label client -> |cyl#db| // capsule endpoint: declare + link in one statement ``` Operators are `[marker][line][marker]`, glued. Lines: `-` solid, `--` dashed, `---` dotted, `~` wavy. End markers: `>` arrow, `<` crow, `*` dot, `<>` diamond (mirrored at the start: `<-`, `<->`, `*-*`…). ER cardinality (crow's-foot), end-side forms: `-+` one · `-<` many · `-o+` zero-or-one · `-+<` one-or-many · `-o<` zero-or-many · `-++` exactly one (mirror at the start: `>o-o<`). The same set by name on `marker:` / `marker-start:` / `marker-end:` (overriding the operator, and the only way on a standalone `|line|`): `arrow dot circle diamond one crow exactly-one zero-or-one one-or-many zero-or-many datum none`. - Style links like nodes: `|-| { stroke: #888; stroke-width: 1.5; }` for all, a worn class per link (`a -> b .loud`), the link's own `{ }` to override. `stroke*` is the wire; `color`/`font-*` the labels. - Labels: one inline (`a -> b "hi"`), several or styled ones in `[ ]`: `a -> b { along: 0.3, 0.7; } [ "near a" "near b" ]`. Labels slide to dodge; they never move the wire. - **Scene config, not link paint**: `clearance: N` (min gap wire↔node; 16 in flow, 10 in a schematic, 18 for dimensions) and `routing: orthogonal | natural | straight` sit on a container's `{ }` and cascade. `orthogonal` (default) = right-angle runs, rounded corners; `natural` = smooth direct curves (the mindmap look — free crossings); `straight` = one trimmed segment. Per-link `routing:` is an error. - A self-loop `a -> a` exits right, hooks over the top. Bodies are sealed: a link inside `[ ]` connects that body's own children; cross-container links go at the lowest scope seeing both ends, via dot-paths. **Where a link is written is its routing world**: a link between two children of one group routes *inside* that group only if written in its `[ ]` — written at root it detours around the group's outside. Keep intra-group wires in the group. - An unroutable link draws as a dashed slanted **stray** and is reported — fix by widening `gap`, shrinking `clearance`, or re-siding; nodes never move. ## Colour & theming Every colour is a `light-dark()` pair — dark mode is automatic; **never hardcode hex where a variable fits**. Role variables (as values, write the short form): `--bg`, `--fill`, `--stroke`, `--stroke-dark` (full drafting black — pen geometry, walls, dimension linework), `--stroke-light` (thin support tone — centrelines, extension lines), `--accent`, `--muted`, `--danger`, `--warn`. `fill: --bg` on the root paints the backdrop (default is none — transparent). **The palette** is the beauty engine. Eleven hues — `red rose orange amber lime green teal sky blue purple gray` (aliases `yellow→amber pink→rose indigo→purple cyan→teal`) — each in five job-named tiers that survive the dark flip: `--teal-wash` (palest — card/section backgrounds) · `--teal-soft` (pastel fill, charts) · `--teal` (everyday pastel) · `--teal-deep` (strong — borders, strokes, wires) · `--teal-ink` (deepest — text). The coloured-node recipe is **wash fill + ink (or deep) stroke + ink text**: `{ fill: --teal-wash; stroke: --teal-ink; color: --teal-ink; }`. `red` is reserved for danger; `rose` is the decorative pink. Literal colours: `#f80`, CSS names, `rgb()`, `hsl(280, 55%, 50%)`, `oklch(0.72, 0.16, 25)` (the palette's own space). Gradients (on `fill`/`stroke`/`gap-fill`): `gradient(--rose, --sky)` auto-135°, `linear-gradient(135, --rose, --sky)`, `radial-gradient(…)`. `hatch(45)` / `hatch(45, 6, --gray-deep)` is the section-line fill texture. Declare your own variables in the stylesheet: `--brand: #ff6600;` then `fill: --brand`. Text: bundled **Google Sans** (default) and **Google Sans Code** (mono, via `font-family: "Google Sans Code"`). `font-weight: normal | medium | semibold | bold` (or 400–700), `font-style: italic`, `text-decoration: underline | line-through`, `text-shadow: dx dy blur colour`, `text-transform: uppercase | lowercase | capitalize`, `letter-spacing` / `line-spacing` (px). Body default 15/medium; captions and link labels derive from the inherited size, so one root `font-size:` scales everything. Themes re-skin at render time — no file edits: `--theme` takes a builtin (`light` · `dark` · `high-contrast` · `blueprint`, white linework on cyanotype blue — the diazo print, for any diagram), a CSS file of `--lini-*` overrides, or a `light/dark` pair. `lini theme NAME` prints a builtin as CSS to start your own. ## Layout engines `layout:` on any container (the root included): `flow` (default) · `grid` · `stack` · `tree` · `sequence` · `chart` · `pie` · `drawing` · `floorplan` · `schematic`. Everything core — cascade, paint, palette, links syntax — works identically inside each. ### Tables & entities ``` |table#basket| { columns: 80, 140, 80; align: start, center, end; } [ "Fruit" "Quantity" "Notes" // first row → header band "Apple" "12" "fresh" "Mango" "3" { color: --red-ink } "ripe" // a styled cell ] |entity#users| "Users" { columns: auto, auto, auto; } [ "PK" "id" "int" "" "email" "varchar" ] users -o< orders // crow's-foot relationship, lands on the card edge ``` Style cells with `|table| |cell| { … }`, the header with `|table| |header| { … }`. A cell needing an id (to wire a field) is written as one: `|cell#uid| "user_id"` (a `|block|` there loses the cell inset). ### Tree & mindmap Structure is `|topic|` nesting inside a `layout: tree` scope — exactly one root topic; non-topic children are a topic's own content (an icon, a badge). `direction: column` (org chart, default) · `row` (outline) · `bilateral` (mindmap split; per-branch `side: left|right` overrides). Branch wires are generated; style a whole arm with `#branchid |-| { }`; size a tier with `.lini-level-2 { font-size: 12; }`. `|mindmap| "Root" [ |topic#ship| "Shipping" [ |topic| "Weekly train" ] … ]` is the preset worth reaching for: bilateral + `routing: natural` + an automatic hue per first-level branch (wash fill, deep stroke and wires, ink text) + depth-ramped sizes + `max-width: 160` wrap. Authored cross-links stay neutral: `a.x:right --- b.y:right "relates" { along: 0.8; }`. ### Sequence `layout: sequence` reads links as time: participants across the top (declared, or auto-created on first use), messages top-to-bottom in source order. `->` call (opens an activation bar) · `-->` return (closes it) · `~>` async · `a -> a` self-message. A participant's own paint colours its lifeline and bars. ``` { layout: sequence; } |box#user| "Customer" { fill: --rose-wash; stroke: --rose-deep; } [ |icon| { symbol: user; fill: none; stroke: --rose-deep; } ] |box#shop| "Storefront" { fill: --sky-wash; stroke: --sky-deep; } |cyl#db| "Orders" { fill: --orange-wash; stroke: --orange-deep; } user -> shop "place order" |loop| "each item" [ shop -> db "reserve stock" db --> shop "in stock" ] |note| "rate-limited" { place: over shop db; } ``` Frames: `|loop|` / `|opt|` / `|alt|` (+ `|else|` separators) hold their messages in `[ ]` but open no scope — messages always wire the sequence's participants. `gap: row col` spaces rows/columns. `place:` modes: `over a` (one lifeline), `over a b` (span), `left a` / `right a` (beside). A **named actor** is a box wrapping an icon (above) — a bare `|icon#user| "user"` spends its label on the symbol name. ### Charts & pie A chart fixes a shared scale from all children, then draws. Default size 360×220 (`width`/`height` set the whole box); radial/pie default 280 square. ``` |chart| "Cycle time (s)" { categories: "15", "30", "50"; } [ |bars| "1.8 kW" { data: 9, 15, 24; } |bars| "2.3 kW" { data: 7, 13, 20; } ] ``` - Series: `|bars|`, `|line|`, `|area|` (`baseline: 40` floats the fill off axis zero), `|dots|` (+ per-node `|bubble| { at: x y; value: N; }`; `|slice| { value: N; }` in a pie). Label = legend entry (auto-shown at ≥ 2). Data — one of `data:` / `fn:`, never both: categorical `data: 9, 15, 24` (must match `categories:` count) · points `data: 0 225, 60 221` · dates `data: "2026-01-01" 18, …` · formula in `x` sampled over the domain (`samples:` count): `fn: (min(100, x * 2))`, a bare call `fn: cure(18)`, or per-band segments `fn: (0.1 + u^2), 5, (2 * u)` — one per `|band|`, each in its local clock `u` (0 → 1). `labels: "a", "b", …` per-datum text needs explicit `data:`. - **Colour is automatic and good**: series walk the palette (interleaved hues, red skipped) in the outlined look — soft fill + deep edge. Only override for meaning: `fill: --sky-soft`, `stroke: --teal`. Per-datum highlight on bars/ dots: `fill: auto, auto, --red, auto`. - `curve: linear (default) | smooth (monotone, never overshoots) | step`; `marker: dot|circle|diamond` puts a mark at every datum; `tooltip: none|hover|auto|always`. - Axes only when you have something to say: `|axis#t| "Speed (mm/s)" { side: bottom; range: 0 133; unit: "%"; scale: log|time; step: 50; }` (`step: month` / `2 week` on a time axis; `ticks:` an explicit list). Bind a series with `axis: t`. `range: 50 1` reverses; `gridlines: none` or a colour. `format: decimal 1 | significant 3 | percent 0 | scientific 2 | engineering 1 | fraction 8 | year|month|day|hour|minute` on a scope, axis, series, or dimension — inherits; presentation only, never measurement. - Annotations in data space: `|band| "Hold" { range: 1.5 4; axis: t; fill: --amber; }` shades a region; a `|mark|` is a reference line (`at: V`), a labelled point (`at: x y`), or label-only (`marker: none`) — style its rule like a wire: `|mark| "SLO 250 ms" { at: 250; axis: ms; stroke: --amber-deep; stroke-style: dashed; color: --amber-ink; }`. - `direction: row` flips bars horizontal; `direction: radial` makes radar (lines close into polygons); `bars: grouped (default) | stacked | overlay` combines bar series. `|pie| { hole: 0.5 }` is a donut. ### Stack (one datum) `layout: stack` arranges nothing: every child's **origin** lands on the container's datum and `translate:` is the only offset. A symmetric primitive's origin is its centre, so shapes stack concentric; a `|sketch|`'s is its **pen origin**, so several sketches keep the frame they were drawn in — which flow throws away. Reach for it for artwork (a logo), a hand-placed figure, or a diagram tuned past what an arranger will do. ``` { layout: stack; unit: mm; density: 10; padding: 25; cap = 0.9; bowl = 2.5; } |sketch#n| { draw: move(0, -3.4) arc(3.4, 3.4, 3.4) down(bowl) arc(cap, 180) up(bowl); mirror: y-axis; } |sketch#dots| { draw: move(11, -9) down(1) arc(cap, -180) up(1) arc(cap, -180) close(); pattern: grid(1, 3, 0, 3); } ``` `|stack|` is the node form. Links go to the **router**, so arrows and labels behave as in a flow. `gap`/`direction`/`align`/`justify` are ignored (a root block refuses them). Nested boxes are unaffected: a `|box|` inside a stack still lays out its own content. **Units:** `unit: px` (the default) is 1 : 1; `unit: mm` plus a root `density:` (px per mm, default 4) draws in millimetres — `density: 10` renders a 24 mm mark 240 px wide. `layout: drawing` is this engine **plus** drafting — mates, dimensions, generated chrome. Same placement, so `|sketch|`, `mirror:` and `pattern:` work identically in both; only a drawing draws a fused mirror's centreline. ### Drawing (engineering) `layout: drawing` places every child's origin on a shared **datum** (no flow); links become dimensions/leaders; **measured values are computed from the geometry** — never type a number a dimension can read. No auto-create. ``` { layout: drawing; } |rect#plate| { width: 120; height: 70; } [ |hole#pin| { width: 10; translate: -35 20; pattern: grid(2, 1, 70, 0); } |hidden#bore| { draw: move(-20, -35) down(70); } // dashed interior geometry ] plate:left (-) plate:right { side: bottom; } // → 120 plate:left (-) plate.pin { side: top; } // → 25 plate.pin (o) // → 2× ⌀10 plate.pin.2 <- "THRU" // leader to the 2nd pattern copy ``` - **Anchors** are `id{.id}[.index][:point]`: `:center` (default), the four sides, the four corners vertical-word-first (`:top-right`), or an authored `:segment`/station. Pattern copies index `plate.pin.2`; `mirror:` copies are not addressable. - Ops: `(-)` linear (binary, chains share a row: `a (-) b (-) c`; a side or edge anchor sets the axis, two **point** anchors — a corner, a hole — read the *aligned* diagonal, so add `project: horizontal | vertical` to read one axis and stack it on a `side:`) · `(o)` round (unary: ⌀ for round features, R for named arcs) · `(<)` angle · leaders `<- "text"` (arrow) / `*- "text"` (dot on a face) / `>- "A"` (datum triangle) — or node-first to a placed annotation: `b1 -* housing:boss` (a `|balloon#b1| "1"`), `plate.pin <- bore` (a `|note#bore|`) · `a:left || b:right { gap: 4 }` mates part faces (moves geometry, draws nothing; negative gap = inserted) and also **seats an annotation on a face** (`finish || plate:top`, `gap:` along the normal, `translate:` the lateral slide). - **The pen** (`|sketch| { draw: … }`, and it works in any layout): calls run left-to-right — `move(x, y)` starts a subpath, `left/right/up/down(n)`, `line(dx, dy)`, `angle(deg, n)`, `curve(c1x, c1y, c2x, c2y, dx, dy)`, `circle(r)`, `fillet(r)` / `chamfer(c)` between two segments, `point()` (a station: records the current point, draws nothing — beside a fillet it is the sharp corner), `close()`. A second `move()` starts a subpath; fill is even-odd, so an inner one reads as a hole. `:name` glued to any call — `close():west`, `point():m1` included — names that segment/station for dimensioning (`body:neck (o)` → ⌀); built-in names (`:left`…) can't be authored; a duplicate errors. Every argument is an expression (bindings read bare). - **Coordinates bite**: the verbs are visual (`up` goes up), but `move`/`line`/ `curve` take raw **y-down** numbers — `move(0, -14)` is 14 *above* the origin. - **Two arcs, and they are not interchangeable.** `arc(dx, dy, r)` is the *minor* arc to a relative point — `r > 0` sweeps clockwise, `r < 0` counter-clockwise, `|r|` ≥ half the chord. `arc(r, deg)` is a **tangent** arc: it continues the current heading and sweeps `deg` (positive = clockwise), updating the heading. **Every run, `line()`, `curve()`, `angle()` or arc leaves a heading; a bare `move()` leaves none** — so open a tangent chain with a run or the two-point form. Bearings are `up = 0`, clockwise (90 right, 180 down). - **`mirror:`** reflects the node's path *and its features* about an axis **through its origin**, then unions the copy — `y-axis` (left↔right), `x-axis` (top↔bottom), a bearing, or a list applied left to right, each reflecting the union so far (`y-axis, x-axis` = 4-fold). An **open** subpath is **fused** (draw half, get the whole — both ends must sit on the axis) and generates the axis `|centerline|` in a drawing; a **closed** one is **duplicated** (draw one ear, get both). The default `auto` reflects iff an ancestor does; `mirror: none` opts a node and its subtree out. - **`pattern:`** (any layout) — `grid(cols, rows, dx, dy)` where **the seed is copy one**, so `grid(1, 3, 0, 20)` gives three, not four; `radial(count, radius)` puts `count` copies *on* the circle about the node's position and draws the `|pitch-circle|`. - `revolve: x-axis` makes a turned part (centerline + shoulder lines auto); `|hole|` punches and centre-marks itself (`thread: 1.25` on it draws the ¾ thread arc). Generated chrome (`|centerline|`, `|pitch-circle|`, `|breakline|`, `|shoulder|`, `|projection|`, `|threadline|`) is styled — or **removed** — by the cascade: `|sketch| |centerline| { stroke: none; fill: none }` takes its space back too. Each is also free to author by hand: `|centerline| { points: 0 -20, 0 20; }`. - Dims: `side:` picks the stacking edge, `tol: 0.1` / `tol: +0.2 -0.05` / `tol: h6` appends tolerance, labels follow (`pin (o) "H7"`) or replace (two-ended) the value; a dimension's `[ ]` carries annotation nodes (`plate:top (-) plate:bottom [ |datum| "C" ]`). **Scale is three settings**: `scale: 2` is the drafting ratio (a 2:1 view that still measures true), `unit:` the physical size of one drawing unit (`mm` default here, also `cm`/`m`/`in`/`px`), and root `density:` the pixels per mm (default 4) — the engine's px-per-unit is their product, never authored. Magnitude is `scale:`'s job: a 5 m beam on A4 is `scale: 0.02`. `density:` lives on the root; `unit:` and `scale:` on the drawing / stack scope (the root itself when it is one). - Sheets: the root stays flow; views are `|drawing|` children of a `|page|`: `|page| { sheet: a4; align: origin } [ |drawing#side| "Title" { scale: 2 } [ … ] side.a (-) side.b |title-block| { title: "…"; drawing-number: "…"; revision: "A"; sheet-number: "1/1"; date: "…"; author: "…"; } [ |image| { src: "logo.svg"; cell: 3 3; width: 12; height: 12 } ] ]` (authored cells seat after the generated fields; dimensions sit in the view or on the page). Multi-view rows share axes with `align: origin`, and an unmarked `-` between anchors in **different views** (`side.screw:head - end.od:top`) is the projection construction line — the one legal cross-view link. - Deep machinery, a line each: `thread: neck 1.25` dresses an ISO thread on a revolved profile — a bare leader on that segment composes `M8×1.25`; `break: -40 40` cuts a long part's boring middle (the view compresses, dims still read the unbroken model). A section / detail is a marker plus a view: `|plane#a| "A" { at: 40 }` or `|magnifier#c| "C" { width: 12 }` on the source, a sibling `|drawing| { of: a }` as the view — its title (`A-A (1:1)`, `C (3:1)`) composes itself. GD&T: `|surface-finish| "Ra 1.6"`, `|feature-control| "position" { tol: 0.05; datums: A B; zone: diameter; material: maximum; modifiers: projected 10 }` (+ `|control|` rows for a composite frame; the ISO 1101 characteristic names validate), `|datum|` — seat on a face with `||` or carry in a dimension's `[ ]`; datum letters come from `>-` leaders (`body:seat >- "A"`). ### Floorplan (architectural) `layout: floorplan` is the drawing engine in an architect's vocabulary — same datum, `scale:`/`unit:`, anchors and dimensions. Build it in four passes: **walls → openings → fixtures → dimensions**. Sizes you type are drawing units; every *built-in* size is true physical mm converted through `unit:`. ``` { layout: floorplan; unit: m; scale: 0.02 } // 1:50 — 80 px per metre |wall#outer| { // draw: is the CENTRELINE draw: move(0, 0) right(7.2):north down(4.8):east left(7.2):south close():west; } [ // openings ride the wall's [ ] |window| { on: north; at: 2.7; width: 1.6 } |door#entry| "D1" { on: south; at: 1.05; width: 0.95; swing: right } // 'south' runs leftward: 'at' counts from its EAST end, and the pen's left // is the outside — 'right' is what opens the door into the flat ] |partition#bathwall| { // ends ON the shell, never across an opening draw: move(4.9, 0) down(2.3):face right(2.3):side } [ |door| { on: side; at: 0.15; width: 0.8 } // opens into the bathroom ] |rect#counter| { width: 0.6; height: 1.4; translate: 6.8 3.2; fill: --bg; stroke: --stroke-dark; stroke-width: 1 } |bed| { rotate: 90; translate: 1.15 1.05 } // head to the west wall |sofa| { symbol: two; rotate: 90; translate: 0.6 3.4 } |dining| { symbol: round; translate: 3.3 3.3 } |appliance| "F" { symbol: fridge; translate: 6.8 2.8 } |bath| { symbol: shower; translate: 6.6 0.6 } |bath| { symbol: toilet; rotate: 90; translate: 5.35 0.5 } // 0° backs WEST "STUDIO 27 m²" { translate: 2.2 2.2 } // room names are plain sheet text // Clear spans, face to face. 'face' runs south, so its 'out' face is the // living side's and its 'in' face the bathroom's — read them the other way // round and each dimension eats the partition. outer:west-in (-) bathwall:face-out { side: top } // → 4.75 — the living space bathwall:face-in (-) outer:east-in { side: top } // → 2.15 — the bathroom outer:west-in (-) outer:east-in { side: top } // → 7 — the shell, clear outer:north-in (-) outer:south-in { side: right } // → 4.6 ``` - **Walls.** `|wall|` is a `|sketch|` whose `draw:` traces the centreline; `thickness:` (200 mm default, inherits nearest-wins; authored per wall in **drawing units** — `thickness: 0.4` under `unit: m`) offsets it into the mitred, solid-filled **poché** outline that takes the paint. `|partition|` is the 100 mm interior define. `fill: --bg; stroke: --stroke-dark` is the hollow double-line look, `fill: hatch(45)` the section convention. Walls bend with `arc()`; `curve()` errors. Draw meeting walls as **separate nodes** — paint order merges them seamlessly. - **Openings.** A `|door|` / `|window|` must sit in its wall's `[ ]`, stationed `on:` a **straight named segment**, `at:` the near jamb's distance from that segment's start (mind the draw direction — a `left(...)` run measures from its east end). They clip the wall and generate their chrome: `hinge: start|end` × `swing: left|right` (left of the pen's travel), `symbol: single | double | sliding` (a slider takes no `hinge:`/`swing:`). `translate:` on one is an error. - **Fixtures.** Place them against something and leave every door its swing: a fixture floating mid-room, a leaf sweeping a tub, or a body crossing a partition is what makes a plan read as noise. `rotate:` turns a piece to its wall — a `toilet` and a `sink` back **west** unturned, a `sofa` backs **north**, a `corner` sofa seats a **north-west** corner; add 90° per quarter-turn clockwise. The families: `|bed|` (queen·king·double·single) · `|sofa|` (three·two·one·corner·stool — `one` is the armchair, `stool` the ⌀350 bar seat) · `|dining|` (six·four·round — sized by its **tabletop**, ⌀1000 for `round`; the pull-back chairs extend the bbox) · `|bath|` (tub·shower·toilet·sink·double-sink — the last is one unit, two basins) · `|appliance|` (stove·fridge·washer·dishwasher) · `|stairs|` (`steps: N` ≥ 2 required; no `symbol:`). `width`/`height` are floors that **stretch** the body. Each fills `--bg`, so furniture masks the floor under it. Label seats: a fixture's hangs **below** the body — leave air there; an `|appliance|`'s centres **inside** it (`"F"` / `"DW"` / `"W/D"`); an opening's sits beside the gap; each turns upright. - **Everything else** is plain geometry: counters, islands, desks and coffee tables are `|rect|`s; a balcony deck, a north arrow or a scale bar is a `|sketch|`; room names and areas are sheet text placed with `translate:` (a two-line name over its area is a `|block| { layout: flow; direction: column }`). A casework `|rect|` takes the core `radius:` for a softened counter — mind that it is **sheet-space pixels**, not drawing units (at 1:50 and the default density, `radius: 4` is 50 mm). - **Dimensions** anchor on the wall's own named runs, which answer three ways: every named run derives its two **face anchors** — `-in` (the enclosed side on a closed run, the left of the pen's travel on an open one) and `-out` — and the bare `:segment` is the **centreline**, where a structural drawing measures. **Dimension inside faces, always**: a room reads its **clear** span and the overall the shell's clear interior — what a listing plan publishes. Never a centreline, and never a span that runs *through* a wall: take the face on the room's own side, so the room clears plus the partitions sum to the overall (`2.65 + 0.1 + 4.05 = 6.8`). Which face that is follows the run's draw direction, so check it — a partition drawn southward has its `-out` face to the west. A name of your own ending `-in`/`-out` on a wall errors. A named **edge**'s extension line springs from the end nearest the dimension line, so it leaves a corner and runs away from the plan. Mind the axis — an edge dimensions **across** itself, so a horizontal span names the two vertical runs. An id'd opening anchors at its **centre**, which makes a location chain (`outer:west-in (-) outer.entry (-) outer:east-in`) — a setting-out drawing's dimension, not a room's, so reach for it only when that is what the sheet is. - **The print look is a theme, never authoring**: render with `--theme blueprint` for white-on-cyanotype; a plan's default stays black-on-white. ### Schematic `layout: schematic` seats parts and lets the router draw square, junction-dotted wires onto pins. 3+-pin parts (and anything with `cell:`) are anchors on tracks; 1–2-pin parts and labels are satellites seated at the pin their wire touches. The sheet is on a grid — `gap` is the part pitch (column and row, default 100), and it is the one lever when a long value overhangs the column beside it, since no part's ink ever moves another part. **A schematic's `columns:` is the wrap count** (one integer, not a track list) and its `cell: col row` is ordinal — empty tracks collapse. `clearance` is 10 (past the `pin-pitch` 20 it errors). No auto-create — unknown bare ids error. ``` { layout: schematic; |vcc::label| { symbol: power } [ "5V" ] } |component#u1| "AMS1117-3.3" [ |pin#vin| { side: left; number: 3; } |space| { span: 2; } // empty rail slots — the datasheet's pin-group gap |pin#en| "EN/~SHDN" { side: left; number: 4; } // label = displayed name (id shows when absent) |pin#gnd| { side: bottom; number: 1; } |pin#vout| { side: right; number: 2; } ] |J#j1| "3V3 OUT" { pins: 4; rotate: 180; } |C#c1| "22u" |label#tach| "TACH" u1.vin - |vcc| // the power-flag capsule, defined above u1.vout - c1 - |gnd| // a chain PASSES THROUGH a 2-pin part: series circuit u1.vout - j1.p3 "3V3" // net name = the wire's label, set beside the trace j1.p1 -> "NSTDBY" // one-ended label wire; the marker sets the tag's shape u1.en - "EN" // a plain name is a RUN of trace, not a stop j1.p2 - tach; tach - j1.p4 // two wires to one declared label merge at its point ``` - **Parts.** Discretes with generated pins: `|R| |C| |L| |D| |LED| |Q| |Y| |F| |FB| |SW| |BT| |V| |I| |M| |BZ| |TP|` (pins `p1 p2`, or `a k`, `b c e` / `g d s` by `symbol:` variant — `zener`, `npn`, `nfet`, `polarized`…); `|opamp|` (pins `out inp inn`, power hidden); `|J|` (`pins: N` — one left-facing column); `|component|` + `|pin|` for anything else; `|gnd|`, `|nc|`, `|junction|` built in. The id is the reference designator (`#R5` reads R5); anonymous parts auto-number (display only — give an id to wire it); `prefix: "IC"` on a define renames the family. `|region::group| { layout: schematic }` makes a captioned sub-sheet — tile several on a `|page|` grid. - **Pose.** `rotate:` is 90°-step: a satellite auto-poses to face its wire, and a forced turn also sets which way its chain grows (`|R| { rotate: 270 }` off a side pin stands the chain **up**); `mirror: x-axis | y-axis` flips a part about its own axis before the turn — on a `|J|`, `rotate: 180` moves pin 1 to the bottom, `mirror: y-axis` faces it right with pin 1 still on top; a transistor's collector swaps sides. Text stays upright, bar a net name, which reads along a vertical trace. - **Wires.** Writing a polarised pin mid-chain sets orientation (`q1.s - d1.k - |gnd|`). A 2-pin part between two placed pins is a **bridge** (`u2.en - r5 - u2.vin`). Naming a discrete's pin reserves it first, so `u6.fb - r16.p1` beside `u6.vout - r15 - r16.p1 - |gnd|` taps *between* the resistors. A name on a pin another statement wires rides that wire as its net label. `:side` on any terminal is an error. - **Labels.** `shape: plain (default, no outline) | left | right | both | round`; `symbol: gnd | earth | chassis | power | nc | antenna` (text beside it; symbol + text = power flag, define it once as above). A plain run's `width: N` lengthens the trace it names, `side:` picks its flank. The classic look (green wires, yellow bodies, beige sheet) is automatic. ## Making it beautiful The defaults are designed — a plain file already reads well. Beauty is mostly restraint plus a few deliberate moves: 1. **Colour by meaning.** One hue per subsystem / branch / state, in the wash + ink recipe. Two or three hues, not seven. Define the pairing once as a define or class and instantiate — never repeat paint per node. 2. **Name regions with `|group|` + its caption label** (`|group#edge| "Edge" { gap: 20; } [ … ]`). Groups organize; boxes state. 3. **Refined over heavy**: keep strokes thin (1.5–2), body text `normal`/`medium`; save bold and strong colour for titles, one hero node, a `|badge|`. One gradient per scene at most (`fill: gradient(--sky, --purple); stroke: none; color: white` on the hero). 4. **Let the engines work.** Don't hand-place what flow/grid/tree can lay out; reach for `pin`+`translate` only for free-form canvases (ER graphs) and overlays, `stack` for artwork. Force `:side` sparingly — reorder declarations first. Charts: accept the palette walk unless colour has meaning. 5. **Meaning in line style**: solid = sync/primary, `-->` dashed = return/ cache/secondary, `~>` wavy = async/event. Encode it as classes (`.async { stroke: --amber-deep; }`) so the legend lives in one place. 6. **Air**: root `padding: 24–30`; group `gap` 20–28; don't shrink the default 36 scene gap without reason. `max-width` on prose-y labels (~160–200). 7. `hint:` on dense nodes, `href:` where a diagram lands in a doc, `|icon|`s for recognition. Set root `fill: --bg` only when a backdrop plate is wanted. ## When the compiler complains Errors carry did-you-mean suggestions — read them, they're usually exact. The ones whose fix is not in the message: | Symptom | Fix | |---|---| | `text content takes no '[ ]'` / `'pin' needs a box` / `'cell' places a grid child` | wrap the string in `\|block\|` | | `'routing' is a scope's strategy` | set `routing:` on the container, not the link | | link endpoint not found | paths never auto-create; declare it or fix the path (the error lists candidates) | | `impossible (a -> b): no legal route: …` | a **stray** — drawn as a slanted dashed line, a warning unless `--strict`. Widen `gap`, drop a forced `:side`, move the link into the group whose children it joins, or lower `clearance` (a schematic already sits at 10) | | `no bundled glyph for '你' …` | `--static` kept that run as `<text>`; fine in a browser, boxes in resvg | | a tangent `arc()` right after `move()` | no heading yet — open with a run or `arc(dx, dy, r)` | Warnings matter too (`--check --strict` before finishing): near-miss ids (`cta` vs `cat`), split label blocks, never-worn classes, strays.