lini 0.19.0

Pretty diagrams, charts, and technical drawings from plain text, with fine-grained control. Compiles to clean, themeable SVG.
Documentation
# TODO

## Diagnostics / error reporting

Many misuses are **silently ignored** instead of reported — the value just does
nothing. Build one context-aware pass, keyed by node kind (box / text / wire /
wire-label) → its valid properties; anything else warns (errors under
`--strict`), LSP-formatted (`file:line:col`).

- **Property on the wrong node**`translate`/`pin`/`padding`/`width` on a wire
  do nothing; error and suggest a `|block|` label.
- **Unknown property**, with a "did you mean" hint (`paddding:``padding`).
- **Out-of-range / wrong-shape value**`translate: 0 -10 0`, `pin: middle`  point at the offending token.
- Fold in the existing grid-prop checks (`cell` / `span` / `columns`).

## Animation (idea — later, very light)

Small, native CSS/SVG effects so the browser does the work and it degrades to a
static frame when baked: moving dashes/dots on wires (cf. d2), a gentle wobble, a
colour/shadow pulse, maybe animating a gradient. Live-only; currently a SPEC §19
non-goal to revisit.

## OKLCH colour output (idea)

The palette is generated from OKLCH but emitted as hex for renderer compatibility
(resvg / librsvg / email don't parse `oklch()`). Add an opt-in — a
`--color-space oklch` flag or similar — that emits the `--lini-*` palette and
gradient stops as `oklch(L C H)` for users who target modern browsers only. Hex
stays the default; oklch is the wide-gamut, perceptual path for those who can use
it. `oklch()` *input* already works (`fill: oklch(0.7, 0.14, 200)`).

## Image export — PNG / WebP (idea)

`lini x.lini -o x.png` / `-o x.webp` straight from the CLI (format from the
extension), so people don't have to pipe through an external resvg. Probably behind
a cargo `raster` feature so the default binary stays lean — opt in for raster.

Path (all pure Rust, no C):

- rasterize the (baked) SVG with **`resvg`** → a `tiny-skia` Pixmap. Same renderer we
  already point users at, so output matches it.
- **PNG** is free — `Pixmap::encode_png()` is built in.
- **WebP** via **`image-webp`**, lossless — the right mode for flat diagrams (lossy
  smears edges/text, and lossy is also the variant that needs C/libwebp).

Open, not settled:

- **binary size** — measure, don't guess (`cargo bloat`, or build with/without the
  feature and diff). The weight is `tiny-skia` + the font/shaping crates, not resvg
  itself (its 76 KiB crate page is just source). Could be small — decide from the number.
- **fonts** — resvg needs a font to draw text: bundle the monospace (a few hundred KB,
  byte-reproducible) or use system fonts (no size, not reproducible). Reproducible
  output leans bundle.
- **one mode only** — a raster can't carry `light-dark()`, so it bakes to a single
  theme (light default; `--theme dark` for dark). Auto dark/light is lost in a PNG.
- **resolution** — needs a `--scale 2` / `--width N` knob (today's `resvg --zoom`).
- **leaner alt** — draw straight to a tiny-skia Pixmap from the scene model (skip
  resvg/usvg): lighter, fully reproducible, but reimplements the render backend. Reuse
  the SVG via resvg for a first cut.

Nice-to-have, not urgent. Build later.

## Auto-layout (idea — `layout: auto`)

Opt-in modes where the *layout* places the nodes and connectors stay dumb (a straight
line or one curve), instead of the router doing the hard work. It inverts the default
— here Lini places, you don't — and lives alongside `sequence` / `chart` as another
per-container layout. Names and shape all open to explore.

Sketch (tentative):

- one `layout: auto` (or `layout: dag`), with a second rule picking the flavour, the
  way charts might take a `direction`: `direction: radial | flow | column`  - `radial` → the mindmap fan
  - `flow` → layered DAG / flowchart (Sugiyama)
  - `column` → tidy tree / org chart
- under it, `[ ]` nesting reads as **structure** (tree/graph edges), not box
  containment — a node's own child-layout (box → column) is superseded by the
  auto-layout, you just pick the node's *kind*.
- (a Sugiyama-ish DAG is roughly possible with a grid today, just manual and fiddly;
  auto-layout would do the placement for you.)

Two things here are **not** speculative — already true / already in the model, and the
real foundation (could just as well be noted outside auto-layout):

- **Composability.** Every node is a container, layout is per-container, and wires
  cross containers by dot-path. So a `flow` DAG with a `|chart|` node, next to a
  `radial` mindmap, wired together — one SVG. Heterogeneous diagrams composed and
  interconnected; nothing else does this.
- **Context-aware routing.** Wires already route *inside* a group (root is just a
  container — routing doesn't have to live at the top). The new bit is choosing the
  routing *strategy* from where the wire sits: orthogonal in a flow/manual container,
  a dumb straight/curve inside an auto-layout. So a mindmap can hold a child with
  orthogonally-wired internals, or vice versa — the sky's the limit.

Build later.

## Drawing features — next round (scored 1 easy → 5 hard)

Gap analysis against the ramjet drawings (`screw_tip_spike`, `cooling_ring`,
`screw` — the injection-screw sheet is the bar). The principle throughout: lini
is 2D — a *section is authored* (drawn with the pen + `hatch()`, as the bushing
already is), but everything *about* a section that is bookkeeping — the cutting
plane, the letters, the title, the scale ratio — composes from facts the engine
already has.

### Sections & details

**Graduated to `DRAWING-0.18.md`** (the planned round): the `|cutting-plane|`
chrome, composed `section:` / `detail:` titles, `|detail-circle|` markers, and
the auto `|detail|` view.

### From the ramjet sheets

(Physical-size emission and the `|title-block|` fields graduated to
`DRAWING-0.18.md`, as did `fillet`/`chamfer` against arcs and the
`.lini-dim-text` de-inlining.)


- **Leader label follows the composed thread spec (1).** `bar:m10 <- "LH"` should
  read `M10×1.5 LH` — one-ended labels *follow* the value everywhere else
  (SPEC 15.6); today a label suppresses the auto-compose. *How:* in
  `leaders::callout`, when the anchor is threaded (`thread_spec` hits) and
  `w.texts` is non-empty, prepend the composed spec to the first text instead of
  skipping composition; SPEC 15.3's bare-leader sentence gains the follows
  clause.
- **Boxed datum letter (1).** ISO frames the `>- "A"` letter in a small square;
  ours is bare text. *How:* in `leaders::callout`'s datum arm, wrap the text
  leaf with a `prim::rect` sized `text width + 2×pad` (the `Stacked` text-box
  math in `dims.rs` already measures this), classes `lini-dim-line`; place at
  the landing.
- **Surface-finish symbol (3).** The Ra checkmark flag (`0.8` on a bent leader).
  *How:* SPEC 23's stated direction — glyphs as paths, like icons: a small
  built-in path set (`finish`, `finish-machined`, `finish-any`) drawn by a new
  `prim::glyph` at `marker_size × k`, placed by the callout machinery — a new
  leader head or, simpler, a `|finish| "0.8"` template the engine seats on a
  face like the datum triangle (surface normal from `Anchor::outward`, which
  already exists). Start with the seated-on-face form; the flag-on-leader
  variant after.
- **GD&T feature-control frames (3–4).** The boxed `⌰ 0.02 A` runout frames on
  the screw. *How:* a `|fcf|`-ish template over `|table|` (one row, hairline
  gutters — the table machinery *is* the frame) whose first cell is a glyph
  named by ident (`runout`, `flatness`, `position`, … — the same path-set
  mechanism as surface finish); authored
  `|fcf| { tol: 0.02; datum: a; kind: runout }` or cells. Wire to a feature
  with the ordinary two-ended link or seat like a note. Glyph set is the only
  real work; no new grammar (SPEC 23's promise).
- **Internal thread in section (3).** The screw's `E` detail: an internal
  `M10×1.5 LH` bore in a hatched section — a drawn bore should dress like the
  outside of a stud. *How:* `threads.rs` already finds the run and offsets
  toward the material by `level.signum()`; an inner-subpath segment needs the
  sign flipped — decide the material side from the subpath's winding (the pen
  knows even-odd nesting: a segment on an inner subpath flips). One sign
  decision + the major-vs-minor role swap in what draws thin.
- **Counterbore / countersink (2).** The remaining hole variants. *How:* like
  `thread:` on a hole — properties (`cbore: ⌀ depth`, `csink: ⌀ angle`) whose
  top-view chrome is one/two extra circles (`chrome::fill` arm, concentric at
  the datum), lowered exactly as the ¾ arc; `pattern:` replicates per copy for
  free. Side-view forms stay authored (they're section geometry).
- **Local / data-URI images (2).** `|image|` is external-URL only — a title-block
  logo needs `src: "./logo.svg"` embedded for a self-contained SVG. *How:* at
  render, a relative/`file:` src reads the file (svg → inline `<g>` or nested
  `<svg>`; png/jpg/webp → base64 `data:` href), size from `width`/`height` as
  today; a `--no-embed` escape keeps URLs. CLI knows the input dir for
  resolution; the LSP/server path needs the same root. Determinism: bytes come
  from the file, so snapshots stay stable.
- **The injection-screw sample (3).** Mostly composable *today*: the sectioned
  barrel is a revolved root profile + two `pattern: grid` rows of flight-section
  sketches (bottom row offset half a pitch), hatched; the unsectioned overview's
  helix silhouettes are parametric `points:` curves (`points:` already takes a
  `u` expr). A stress sample to prove it and catch what breaks — likely
  suspects: hatch continuity across pattern copies, dim rows over 500-unit
  spans, the FEED/TRANSITION/METERING band. A `helix()`-ish pen helper only if
  the sample shows the need.
- **Zone-band annotations (1).** FEED / TRANSITION / METERING over 190/110/100 —
  works today (a chain dim + labels); a sample corner, not a feature. Verify in
  the screw sample.

### Deferred-list promotions (SPEC 23 already names these)

- **Repeated-segment counting (2)** — one `:segment` on several corners reading
  `4× R3`, as `pattern:` already prefixes counts. *How:* lift the
  duplicate-segment error into a per-name **list** in the pen
  (`Folded.segments` keyed name → Vec); anchors read the first, `compose`
  prefixes `` when the list is longer — the same seat as the `pattern:`
  count.
- **Fan leaders (3)**`a & b <- "2× R5"`, one note, two tips. *How:* the `&`
  fan already parses for links; resolve currently rejects it one-ended — allow
  it for leader ops, and `leaders::callout` draws one text + landing with a tip
  ray per endpoint (share the elbow, first anchor steers the direction).
- **Per-copy pattern anchors + pitch dims (3)**`bolt.2`, hole-to-hole
  pitches. *How:* the carrier already holds real copy children; give copies
  synthetic ids (`m8.1`, `m8.2``pattern::expand` numbers them) and let the
  endpoint dot-path walk match them (anchors walk `PlacedNode.children` by id
  already); the SPEC 23 "unaddressable" gates come out.
- **Aligned (point-to-point) dims (3)** — today horizontal/vertical only.
  *How:* `dims.rs` gains an `Axis::Along(P)` — the dim line parallel to the
  anchor-to-anchor direction, extension lines perpendicular to it; the row
  packer treats it as its own side-less band (no packing against H/V rows —
  seat at `dim-offset` along the normal, `gap:` steers). The ISO-aligned text
  rotation machinery already handles arbitrary angles (the diametral line
  proves it).
- **Dim-line breaks / halos (3)** — where extension lines cross geometry (the
  spike's ⌀ stack wants it). *How:* render-side halo, not layout: extension /
  dim lines get `paint-order`-style masking — emit each `lini-ext-line` with a
  wider background-colour understroke (`stroke: --bg; stroke-width: 3×`)
  beneath the line, one extra element, no geometry maths; drafting calls this
  the gap convention. Opt-out via cascade.
- **`explode:` (3)** — scale directed mates' separations for exploded views.
  *How:* `mates.rs` seat solves along the normal; an `explode: k` on the
  drawing multiplies each directed mate's resolved offset (including `gap:`)
  by `k` at seat time — one factor threaded through `Ctx`; balloons follow
  because they're placed after seating.

### Core debts the drawings keep hitting

- **The root-drawing router gap (3)** — a wire inside a nested flow scope of a
  *root* `{ layout: drawing }` is silently dropped (a node `|drawing|` routes
  it fine). The anonymous-container half of this family is **fixed**
  (2026-07-07: scope-transparency everywhere — lookups, engine filters, the
  anchor walk, auto-create; SPEC 9 states the model). What remains: the root
  drawing's `layout()` interception skips `routing::route` entirely — it
  should still route the links of nested non-drawing scopes. *How:* in the
  root-drawing arm of `layout()`, partition `program.links` by
  `is_drawing_scope` of each link's scope and hand the rest to the router
  (the router already routes inside containers).
- **`w` / `h` ambient in expressions (3)** — profiles full of `83.63`-style
  derived spans want `right(`l - 2*tip`)` against named constants today; fine,
  but the ambient keeps recurring in real profiles. *How:* still blocked on the
  circularity SPEC 10.7 notes (size needs the expression, the expression wants
  the size); scope it to the **pen only**`w`/`h` bound to the profile's
  *declared-so-far* extent is still circular, so the honest cut is stylesheet
  constants per part (status quo) or `w`/`h` meaning the *parent drawing's*
  content box, which is known. Decide at spec time; don't build first.