figdown 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXPRESSING.md +119 -0
- package/LAYOUT-GUIDE.md +367 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/README.zh-tw.md +185 -0
- package/SHOWCASE.md +421 -0
- package/dist/figdown.js +2239 -0
- package/dist/figdown.mjs +2231 -0
- package/examples/evpn-fabric.svg +54 -0
- package/examples/showcase/arp-resolution.svg +51 -0
- package/examples/showcase/ethernet-frame.svg +37 -0
- package/examples/showcase/l2-forwarding-logic.svg +42 -0
- package/examples/showcase/tcp-handshake.svg +56 -0
- package/examples/showcase/tcp-header.svg +47 -0
- package/examples/showcase/tcp-state-machine.svg +140 -0
- package/package.json +51 -0
- package/skill/figdown/build-svg.js +73 -0
- package/skill/figdown/figdown.html +3290 -0
package/EXPRESSING.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Expressing intent in FigDown — author index
|
|
2
|
+
|
|
3
|
+
> **Non-normative index.** Spec is normative: [spec/syntax-draft.md](spec/syntax-draft.md).
|
|
4
|
+
> Full authoring workflow: [AUTHORING.md](AUTHORING.md) (pick a template first, then come back here).
|
|
5
|
+
> Layout hints: [LAYOUT-GUIDE.md](LAYOUT-GUIDE.md).
|
|
6
|
+
> Build and validate: `node tools/build-svg.js X.fd` — errors are `Line N: message`.
|
|
7
|
+
>
|
|
8
|
+
> 繁體中文版:[EXPRESSING.zh-tw.md](EXPRESSING.zh-tw.md)
|
|
9
|
+
|
|
10
|
+
Purpose: one-line lookup — "I need to show X → use Y." One row per intent. Notes column = a caveat or scope limit, not a tutorial.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Structure & composition
|
|
15
|
+
|
|
16
|
+
| I need to show… | Use | Notes |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| containment — node belongs inside a box | `group g "Label"` + `node n "…" in=g` | one level of nesting only (spec §2.2); for deeper, represent the inner group as a proxy node |
|
|
19
|
+
| set membership / category (color + legend) | `class c "meaning" color=… style=…` + `class=c` on members | legend derives automatically; bare `color=` carries no named meaning |
|
|
20
|
+
| one class used on both nodes and edges | same `class c "meaning" color=…` on each | `color=` **fills** a node box but **strokes** an edge line; `stroke=` outlines a node and is **ignored** on an edge; `style=` applies to both. Pick `color=` when the class must read on edges |
|
|
21
|
+
| hierarchy / tree | directed `edge` chain; `flow down` to orient | the edges carry the tree; group is for spatial containment, not hierarchy |
|
|
22
|
+
| adjacency without a link | `in=` on the same `group`, no `edge` between them | the shared frame communicates co-location |
|
|
23
|
+
| cross-cutting category spanning groups | `class` + `class=` on elements in different groups | one class can mark nodes, edges, and fields across the whole document |
|
|
24
|
+
| system boundary / inside vs outside | `boundary ext "label"` + edges to/from it; internal nodes in a `group` | `boundary` is never drawn as a shape — the edge ends open |
|
|
25
|
+
| ownership zones / domain coloring | `class zone "Owner" color=…` on a `group` or its members | assign to the group itself to color the frame |
|
|
26
|
+
| conditional or optional element (standby link, optional component) | `class cond "Condition text" style=dashed` + `class=cond` on the element | `style=dashed` alone states nothing (R57); write the condition in the class label |
|
|
27
|
+
| one-level nesting limit | `in=` on `node`; `group … in=…` is a line error | for two levels: add a proxy `node` representing the inner group |
|
|
28
|
+
| layer stack or spatial arrangement where order is knowledge | `edge` chain + `flow down`, or single-row `table`; state the order in prose | no spatial-arrangement construct yet — see Known limits OQ-S25 |
|
|
29
|
+
| slot map / position in a spatial grid | state positions in label text + `pin` in render zone | `pin` is geometry only — use a `table` for anything a reader must count or address |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Flow & behaviour
|
|
34
|
+
|
|
35
|
+
| I need to show… | Use | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| ordered steps | directed `edge` chain; order = edge direction | `flow right` or `flow down` sets reading axis |
|
|
38
|
+
| decision + branches | `node q "…" shape=diamond` + `edge q -[yes]-> …` / `edge q -[no]-> …` | `shape=diamond` is geometry only (D7); declare a `class` if the decision role carries meaning (OQ-S18) |
|
|
39
|
+
| loop / retry | directed cycle back to an earlier node; condition in the mid-label | a back-edge crossing is expected and correct |
|
|
40
|
+
| error paths | `class err "Error path" style=dashed` + `class=err` on error edges | separate semantic category from the happy path |
|
|
41
|
+
| state machine / lifecycle | nodes as states + labelled directed edges as transitions | no sequence genre yet; order meaning rides on labels |
|
|
42
|
+
| pipeline stages | `node` per stage + `boundary` at the mouths + `flow right` + `rank` | `boundary` marks where data enters/leaves the figure |
|
|
43
|
+
| event → action | `edge src -[event]-> tgt` | mid-label is the trigger; head-label can name the action |
|
|
44
|
+
| precedence / partial order | a DAG of directed edges | absence of an edge means no stated constraint |
|
|
45
|
+
| fan-out / fan-in | edges from/to a common node | AND-vs-XOR join discipline: declare a `class` (OQ-S18 — no first-class gateway yet) |
|
|
46
|
+
| message exchange between parties | nodes as parties + labelled directed edges; ordinal labels for ordering | strict ordering: number labels consistently (OQ-S18) — see Known limits |
|
|
47
|
+
| a label too wide for its diamond / ellipse / cylinder | nothing — shapes size themselves from their **inscribed** area | do not add `size`; see the note below |
|
|
48
|
+
|
|
49
|
+
> **Delete `size` lines added to make a shape fit its text.** Non-rectangular
|
|
50
|
+
> shapes were once sized by their bounding box, so authors added `size` to stop
|
|
51
|
+
> `diamond` and `ellipse` labels overflowing. That is fixed. `size` is rigid and
|
|
52
|
+
> must be re-tuned whenever a label changes, so removing it is a maintenance
|
|
53
|
+
> win. Verified on production documents and reproducible here: strip every
|
|
54
|
+
> `size` line from this repo's examples that carry them and
|
|
55
|
+
> `node tools/shape-check.js --strict` still reports zero containment failures.
|
|
56
|
+
> The drawing is *not* byte-identical — the canvas shifts — but it stays
|
|
57
|
+
> geometrically correct. Layout-motivated `size` ([LAYOUT-GUIDE.md](LAYOUT-GUIDE.md) rung 3) is a separate question.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Data & format
|
|
62
|
+
|
|
63
|
+
| I need to show… | Use | Notes |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| bit-level register / header | `bitfield id "label" [unit=32] [numbering=lsb0\|msb0]` + `field name width` | default `lsb0` (hardware style); use `msb0` for IETF RFC convention |
|
|
66
|
+
| byte-level frame with no bit ruler | single-row `table`, cell order = wire order | `bitfield` adds a bit ruler — use `table` when bits are not the unit |
|
|
67
|
+
| struct / record layout | `table` with `Offset \| Field \| Type \| Width \| Notes` columns | cell order is authoritative; `colw` for presentation only |
|
|
68
|
+
| memory map as lookup | `table` with address ranges + `cell r,c color=` for highlights | |
|
|
69
|
+
| lookup / translation table | `table` + `cell r,c color=` for highlights | |
|
|
70
|
+
| mapping between two node sets | `edge` + `class` naming the relation | the class label is the relation name; edges draw the pairs |
|
|
71
|
+
| pointer chains between nodes | directed `edge` chain | can't target a table cell — see Known limits OQ-S17 |
|
|
72
|
+
| optional / variable-length fields | `field name width optional` or `field name *` in a `bitfield` | `optional` flag triggers reader branching (R48); `*` fills row remainder |
|
|
73
|
+
| endianness / dual ruler convention | two `bitfield` blocks with different `numbering=` | `lsb0` and `msb0` produce different rulers from the same fields |
|
|
74
|
+
| mode-dependent field meaning | separate labelled `bitfield` blocks + `note=` per field | no first-class variant yet — see Known limits OQ-S9 |
|
|
75
|
+
| nested encapsulation / multi-tier header | `table` with `||` colspan spans in header rows | `^^` for rowspan; cell order = wire order |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quantity, time & relation
|
|
80
|
+
|
|
81
|
+
| I need to show… | Use | Notes |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| proportion of a whole / fill level | `fill N% in=g color=…` on a `group` or `node` | label the node with the current value; `fill` is the band |
|
|
84
|
+
| threshold / watermark | `line "label" in=g at=N%` | `line` requires `in=` to a `group` (not a bare node) |
|
|
85
|
+
| quantity comparison | `table` with numeric columns | `▁▃▅▇` Unicode blocks as sparklines in cells (OQ-S12) |
|
|
86
|
+
| signal values over time | `wave id "label"` + `signal name chars` (one char = one tick) | lane alphabet: `0 1 p n x = . 0-9` |
|
|
87
|
+
| event ordering without exact times | directed edges with ordinal mid-labels (`-[1: SYN]->`) | number labels consistently; no sequence genre yet |
|
|
88
|
+
| visual code / legend | `class` — legend derives automatically from declaration order | each `class` line gives swatch + meaning text |
|
|
89
|
+
| annotation explaining why | `node note "…" style=dashed` + dashed edge to the target | no first-class callout yet — see Known limits OQ-S14 |
|
|
90
|
+
| cross-references within a scene | `edge` + `class` naming the relation | can't reference a table cell or bitfield field — see Known limits OQ-S17 / OQ-S20 |
|
|
91
|
+
| units | state them in the label or column header | no machine-readable unit type; interim: text in the label |
|
|
92
|
+
| trends / rates | `table` with a values column; `▁▃▅▇` for sparklines | charts are out of scope (D11/R46) — keep the raster + prose |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Known limits (v0.1)
|
|
97
|
+
|
|
98
|
+
Each entry: what cannot be expressed today · OQ reference · sanctioned interim workaround.
|
|
99
|
+
|
|
100
|
+
- **same entity in two views** — no way to assert two nodes are the same participant; OQ pending; interim: shared `class` + a note stating the identity.
|
|
101
|
+
- **strict message ordering** — order carried only by label numbering convention; sequence genre candidate (v0.2, OQ-S18); interim: number labels consistently (e.g. `1: SYN`, `2: SYN-ACK`).
|
|
102
|
+
- **first-class callout / annotation** — a callout is structurally a node; OQ-S14; interim: `node note "…" style=dashed` + dashed `edge`.
|
|
103
|
+
- **cell anchors** — an `edge` cannot target a `table` cell or `bitfield` field; OQ-S17; interim: whole-table relation + cell name in the edge label.
|
|
104
|
+
- **cross-block references** — no locator from one typed block to another; OQ-S20; interim: prose note or a linking `edge` between the host nodes.
|
|
105
|
+
- **byte-unit packet construct** — `bitfield` adds an unwanted bit ruler; OQ-S24; interim: single-row `table`.
|
|
106
|
+
- **meaningful arrangement** (layer stacks, floorplans, proportional memory maps) — no declared-arrangement construct; OQ-S25/R57; interim: state the arrangement meaning in prose + read the render zone.
|
|
107
|
+
- **wave timing parameters** (setup/hold spans) — no named span between signal events; OQ-S23; interim: keep the original raster + prose.
|
|
108
|
+
- **mode-dependent field variants** — same bit range, different decode per mode; OQ-S9; interim: separate labelled `bitfield` blocks + `note=`.
|
|
109
|
+
- **multiplicity / machine-readable units** — no count or unit type; pending corpus frequency; interim: state them in label text.
|
|
110
|
+
- **decision/gateway role vocabulary** (AND vs XOR) — `shape=diamond` is geometry only; OQ-S18; interim: declare a `class` naming the join discipline.
|
|
111
|
+
- **conditional presence in scene elements** — `optional` is a `bitfield`-only flag; `node a "A" optional` is a line error; v0.2 candidate; interim: `class cond "condition text" style=dashed` + `class=cond`.
|
|
112
|
+
- **mutually exclusive `bitfield` interpretations — a correctness trap, not a cosmetic one.** `wrap` is presentation-only and never reorders or skips bits, so alternative decodings of the *same* bits stacked with `wrap` are read by a machine as one long contiguous sequence: an 18-bit register drawn as eight alternative encodings computes as 144 bits. There is no union/case construct and nothing warns. Open question: what a variant/discriminator construct should look like (adjacent to OQ-S9). Interim: **one `bitfield` block per alternative**, each labelled with the discriminator value that selects it (`bitfield ctl_a "Control — mode=0 (18 bits)" unit=18`).
|
|
113
|
+
- **aside / non-participating annotation** — no node kind that comments on a figure without joining its graph; a free `node` parses but reads as a participant. Open question: whether an aside is a node kind or a document-level construct. Interim: prose beside the figure, or `node note "…" style=dashed` + a dashed `edge` (which does join the graph).
|
|
114
|
+
- **repeated-subgraph template** — no way to declare a sub-structure once and instantiate it *n* times. Open question: whether templating belongs in the language at all or in the generator above it. Interim: write each instance out.
|
|
115
|
+
- **two-level group nesting** — `group … in=…` is a line error; nesting is one level in v0.1. Open question: whether deeper nesting needs new syntax or only a renderer change. Interim: a proxy `node` for the inner group.
|
|
116
|
+
- **mux / selector shape** — `shape=` is a closed geometric set (`box|rounded|circle|ellipse|cloud|diamond|cylinder`); `shape=mux` is a line error. Open question: whether the mux trapezoid is geometry (admissible under D7) or a domain noun (excluded). Interim: `shape=diamond` or a box, with the selector role in the label or a `class`.
|
|
117
|
+
- **blank / idle lane character in `wave`** — the lane alphabet is closed and has no "no activity" tick (`_` and `z` are line errors). Authors reach for `x`, which draws as an unknown-value hatch — a different claim. Open question: whether idle is a lane value or a `gap`-like marker. Interim: `x` plus a `labels=` entry or note saying it means idle.
|
|
118
|
+
- **charts / plots** — deliberately out of scope (D11/R46); keep the raster and describe in prose or a `table`.
|
|
119
|
+
- **closed-world readings** (anything not drawn is denied) — absence leaves no trace in the syntax; no tool can catch this. When a figure's meaning depends on what is NOT drawn, declare the closed-world reading explicitly and state every prohibition in a table row or note (R59 addendum).
|
package/LAYOUT-GUIDE.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Layout Guide — arranging FigDown figures for human readers
|
|
2
|
+
|
|
3
|
+
> **Non-normative recommendation.** Nothing here is required. A `.fd` with zero
|
|
4
|
+
> layout lines is fully conforming (spec §3: auto-layout is the default). This
|
|
5
|
+
> guide teaches how to **think** about arranging a figure for human readers
|
|
6
|
+
> using the render zone. All layout calls are maintainer recommendations, not
|
|
7
|
+
> mandates.
|
|
8
|
+
>
|
|
9
|
+
> Lessons come from field observation of a downstream authoring pass.
|
|
10
|
+
>
|
|
11
|
+
> 繁體中文版:[LAYOUT-GUIDE.zh-tw.md](LAYOUT-GUIDE.zh-tw.md)
|
|
12
|
+
|
|
13
|
+
## 1. The two-zone mindset
|
|
14
|
+
|
|
15
|
+
FigDown has two zones (spec §3, R43): the **semantic zone** (before `render`)
|
|
16
|
+
holds nodes, edges, groups, and classes — the complete knowledge of what the
|
|
17
|
+
figure means. The **rendering zone** (after `render`) holds only `pin`, `size`,
|
|
18
|
+
`routing`, and `route` — instructions for how that knowledge lands on a canvas.
|
|
19
|
+
|
|
20
|
+
Key invariant (R25, R37): stripping every `pin`/`size`/`routing`/`route` line
|
|
21
|
+
must leave a document that still parses, renders under auto-layout, and
|
|
22
|
+
expresses the identical structure and relationships. No knowledge lives in the
|
|
23
|
+
render zone. If removing a `pin` changes what an AI reader can learn, the
|
|
24
|
+
position was encoding meaning that should live in text instead.
|
|
25
|
+
|
|
26
|
+
Auto-layout is a **default**, not a promise. When the original figure's spatial
|
|
27
|
+
arrangement carries reading order — left-to-right pipeline stages, top-down
|
|
28
|
+
layer stack — reproduce that orientation in the render zone so human readers
|
|
29
|
+
follow the same path.
|
|
30
|
+
|
|
31
|
+
Note: `flow` and `rank` are **semantic** directives (R43); they go before
|
|
32
|
+
`render`. Only `pin`, `size`, `routing`, and `route` are legal after `render`.
|
|
33
|
+
|
|
34
|
+
## 2. The escalation ladder
|
|
35
|
+
|
|
36
|
+
Try each rung, rebuild (`node tools/build-svg.js X.fd`), and **stop as soon
|
|
37
|
+
as the figure reads clearly.** The lowest rung that works is the right choice.
|
|
38
|
+
|
|
39
|
+
| Rung | Directive | When |
|
|
40
|
+
|------|-----------|------|
|
|
41
|
+
| 0 | *(nothing)* | Small graphs or figures where spatial order carries no meaning |
|
|
42
|
+
| 1 | `flow right\|down\|left\|up` | Direction of the figure — one line before `render` |
|
|
43
|
+
| 2 | `rank a b c` | Pull parallel peers onto one row/column; before `render` |
|
|
44
|
+
| 3 | `pin … at=x,y` / `size … w= h=` | Topology/spatial where placement IS the message |
|
|
45
|
+
| 4 | `routing orthogonal` / `route … via=` | Edge geometry last; dense buses or trunk lines |
|
|
46
|
+
|
|
47
|
+
**Rung 1 is where authoring should begin.** A scene document should almost
|
|
48
|
+
always carry a `flow` line. It costs one line and gives the layout engine the
|
|
49
|
+
single most useful piece of intent it can receive. "Write nothing" (rung 0)
|
|
50
|
+
is reasonable only for a figure small enough that direction is obvious — or
|
|
51
|
+
for a figure where spatial order carries no meaning at all. Field evidence
|
|
52
|
+
confirms this is already the norm: in the first independent production corpus,
|
|
53
|
+
all 20 scene documents declare `flow` explicitly (15 `flow down`, 5
|
|
54
|
+
`flow right`); not one relies on the template default.
|
|
55
|
+
|
|
56
|
+
**What the direction expresses.** `flow down` reads as a portrait figure whose
|
|
57
|
+
sequence runs top-to-bottom — the common shape for decision and process
|
|
58
|
+
figures. `flow right` reads as a landscape figure whose sequence runs
|
|
59
|
+
left-to-right — the common shape for pipelines and layered architectures. The
|
|
60
|
+
direction is a reading-order statement to the human, not only a layout knob:
|
|
61
|
+
pick the one that matches how the figure should be read, not merely how it
|
|
62
|
+
should be arranged. `flow` only declares the visual reading axis for
|
|
63
|
+
relationships that already exist; it does not create semantic precedence,
|
|
64
|
+
sequence, or connectivity — process order still comes from `edge` declarations
|
|
65
|
+
and typed-block declaration order. Honest limit: for a mesh or a hub topology with no
|
|
66
|
+
dominant sequence, `flow` sets the primary layering axis but does not by
|
|
67
|
+
itself determine the overall aspect. That gap — controlling aspect and
|
|
68
|
+
wrapping on figures with no dominant sequence — is tracked in the engine
|
|
69
|
+
backlog (long-chain wrapping / aspect control, item 10) and there is no
|
|
70
|
+
aspect keyword today.
|
|
71
|
+
|
|
72
|
+
Higher rungs cost future edit effort (every `pin` is a promise; §5).
|
|
73
|
+
|
|
74
|
+
## 3. Patterns by figure family
|
|
75
|
+
|
|
76
|
+
Each sketch is minimal and verified against `build-svg.js`. Keywords used:
|
|
77
|
+
`flow`, `rank`, `pin`, `routing`, `route` — all from spec §3.
|
|
78
|
+
|
|
79
|
+
### Sequential pipeline — `flow` + `rank`
|
|
80
|
+
|
|
81
|
+
```figdown
|
|
82
|
+
figdown 0.1 block
|
|
83
|
+
title Packet Pipeline
|
|
84
|
+
node rx "RX"
|
|
85
|
+
node par "Parser"
|
|
86
|
+
node pol "Policy"
|
|
87
|
+
node tx "TX"
|
|
88
|
+
edge rx -> par
|
|
89
|
+
edge par -> pol
|
|
90
|
+
edge pol -> tx
|
|
91
|
+
flow right
|
|
92
|
+
rank rx par pol tx
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Wrap long chains: one `rank` per stage row, let auto-layout stack the rows.
|
|
96
|
+
|
|
97
|
+
### Decision flowchart — main path on one axis
|
|
98
|
+
|
|
99
|
+
```figdown
|
|
100
|
+
figdown 0.1 flowchart
|
|
101
|
+
node start "Start" shape=rounded
|
|
102
|
+
node check "Header valid?" shape=diamond
|
|
103
|
+
node proc "Process"
|
|
104
|
+
node drop "Drop" shape=rounded
|
|
105
|
+
node done "Done" shape=rounded
|
|
106
|
+
edge start -> check
|
|
107
|
+
edge check -[yes]-> proc
|
|
108
|
+
edge check -[no]-> drop
|
|
109
|
+
edge proc -> done
|
|
110
|
+
flow down
|
|
111
|
+
rank start check proc done
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`rank` the mainline; side-exit branches fall perpendicular under auto-layout.
|
|
115
|
+
|
|
116
|
+
### Hierarchy / layers — one `rank` per layer
|
|
117
|
+
|
|
118
|
+
```figdown
|
|
119
|
+
figdown 0.1 block
|
|
120
|
+
group app "Application"
|
|
121
|
+
group net "Network"
|
|
122
|
+
group link "Link"
|
|
123
|
+
node http "HTTP" in=app
|
|
124
|
+
node tcp "TCP" in=app
|
|
125
|
+
node ip "IP" in=net
|
|
126
|
+
node eth "Ethernet" in=link
|
|
127
|
+
edge http -> tcp
|
|
128
|
+
edge tcp -> ip
|
|
129
|
+
edge ip -> eth
|
|
130
|
+
flow down
|
|
131
|
+
rank http tcp
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Layer labels go in `group` names or node labels, not only in vertical position.
|
|
135
|
+
|
|
136
|
+
### Topology / spatial — pin the anchors
|
|
137
|
+
|
|
138
|
+
```figdown
|
|
139
|
+
figdown 0.1 topology
|
|
140
|
+
node hub "Core"
|
|
141
|
+
node a "ToR-A"
|
|
142
|
+
node b "ToR-B"
|
|
143
|
+
node c "ToR-C"
|
|
144
|
+
edge hub -> a
|
|
145
|
+
edge hub -> b
|
|
146
|
+
edge hub -> c
|
|
147
|
+
render
|
|
148
|
+
pin hub at=300,200
|
|
149
|
+
pin a at=150,80
|
|
150
|
+
pin b at=450,80
|
|
151
|
+
pin c at=300,360
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Pin hub, corners, or geography-defining nodes; let auto-layout fill the rest.
|
|
155
|
+
|
|
156
|
+
### Dense buses — `routing orthogonal` + `route`
|
|
157
|
+
|
|
158
|
+
```figdown
|
|
159
|
+
figdown 0.1 block
|
|
160
|
+
node cpu "CPU"
|
|
161
|
+
node mem "DRAM"
|
|
162
|
+
node dma "DMA"
|
|
163
|
+
node bus "Bus"
|
|
164
|
+
edge cpu -> bus
|
|
165
|
+
edge mem -> bus
|
|
166
|
+
edge dma -> bus
|
|
167
|
+
flow down
|
|
168
|
+
render
|
|
169
|
+
routing orthogonal
|
|
170
|
+
route cpu -> bus via=200,100;200,200
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 4. Two-level pins (groups as layout modules)
|
|
174
|
+
|
|
175
|
+
Spec §3 (D6): a **pinned group** anchors its local origin in canvas px; a
|
|
176
|
+
**pinned member** is group-local. Moving a group is one edit (`pin g at=…`)
|
|
177
|
+
and never disturbs another group. Use groups as layout modules for figures
|
|
178
|
+
with independent spatial regions.
|
|
179
|
+
|
|
180
|
+
```figdown
|
|
181
|
+
group ingress "Ingress Pipeline"
|
|
182
|
+
node rx "RX" in=ingress
|
|
183
|
+
node par "Parse" in=ingress
|
|
184
|
+
group egress "Egress Pipeline"
|
|
185
|
+
node sch "Sched" in=egress
|
|
186
|
+
node tx "TX" in=egress
|
|
187
|
+
render
|
|
188
|
+
pin ingress at=50,100
|
|
189
|
+
pin rx at=0,0
|
|
190
|
+
pin par at=120,0
|
|
191
|
+
pin egress at=50,250
|
|
192
|
+
pin sch at=0,0
|
|
193
|
+
pin tx at=120,0
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 5. The iteration loop for agents
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
author semantics
|
|
200
|
+
→ build: node tools/build-svg.js X.fd
|
|
201
|
+
→ judge: eyeball + node tools/layout-lint.js X.svg (6 metrics; --max-score gate)
|
|
202
|
+
→ if not clear: add ONE rung (§2) and repeat
|
|
203
|
+
→ stop when it reads well
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Before committing layout lines: `node tools/r25-check.js X.fd` — flags nodes whose only relationship to the rest of the figure is geometric (R25 strip test).
|
|
207
|
+
|
|
208
|
+
**Over-pinning warning (R8).** Every `pin` is a promise future edits must
|
|
209
|
+
keep. Pin only the elements whose position IS the message (the hub of a star,
|
|
210
|
+
the corners of a geographic map). Auto-layout keeps improving; engine-backlog
|
|
211
|
+
items will lift rung-0 quality without manual pins needing updates.
|
|
212
|
+
|
|
213
|
+
## 6. When not to fight the layout
|
|
214
|
+
|
|
215
|
+
If semantics are complete and the figure is legible, ship it. Visual difference
|
|
216
|
+
from the original is not semantic loss (R37): the figure states the same
|
|
217
|
+
relationships regardless of where the hub sits on the canvas. Reserve layout
|
|
218
|
+
effort for figures where **arrangement carries meaning** — where a reader
|
|
219
|
+
draws a different conclusion from a different arrangement.
|
|
220
|
+
|
|
221
|
+
A back-edge crossing in a retry loop (e.g. a `no` branch looping back to an
|
|
222
|
+
earlier step) is structural: no rung of the escalation ladder removes it, and
|
|
223
|
+
the crossing count is the same with or without `flow down`. That is a
|
|
224
|
+
legitimate stopping point — the graph encodes a cycle; the lint score reflects
|
|
225
|
+
it correctly.
|
|
226
|
+
|
|
227
|
+
## 7. Before / after: the same semantics, different render zones
|
|
228
|
+
|
|
229
|
+
The first two entries are pairs, byte-identical in the semantic zone. The third is the counter-case: no pair, because the fix was not in the render zone at all. Score = `cross×2 + thru×3 + novlp×3 + lblcol×2 + coinc×2`.
|
|
230
|
+
|
|
231
|
+
**Lint scores are a smoke alarm, not a judge.** The srl-evpn-irb pair below shows lint getting *worse* (ink/e 111→117) while the figure becomes dramatically more readable — because the metrics do not measure group containment, overlap, or reading order. Always look at the render; the ladder ends when a human can read it. One variant of that example scored better on lint but had lost its column alignment — and the alignment was the peer signal; only looking at the render caught it.
|
|
232
|
+
|
|
233
|
+
### Leaf-spine fabric — `+flow down +rank sp1 sp2 +rank lf1 lf2 lf3`
|
|
234
|
+
|
|
235
|
+
Eight-node VXLAN/EVPN topology. Auto-layout scatters spines and leaves; three semantic directives align them into clear tiers. Lint score is the same; edge length halves (144→77 ink/e).
|
|
236
|
+
|
|
237
|
+
| variant | cross | ink/e | score |
|
|
238
|
+
|---------|-------|-------|-------|
|
|
239
|
+
| auto | 3 | 144 | 6 |
|
|
240
|
+
| `+flow +rank` | 3 | 77 | 6 |
|
|
241
|
+
|
|
242
|
+
[auto .fd](examples/layout-compare/evpn-fabric-auto.fd) · [auto .svg](examples/layout-compare/evpn-fabric-auto.svg) · [tuned .fd](examples/layout-compare/evpn-fabric-tuned.fd) · [tuned .svg](examples/layout-compare/evpn-fabric-tuned.svg)
|
|
243
|
+
|
|
244
|
+
### srl-evpn-irb — two-level pins + groups (20 layout lines)
|
|
245
|
+
|
|
246
|
+
Sixteen-node EVPN-VXLAN IRB figure with three leaf groups. Auto-layout lets group boxes overlap each other and member nodes escape their frames, producing a tall chaotic column (561×907 px). The tuned version pins each group as a layout module (spec §3 D6: a pinned group anchors its local origin in canvas px; members are group-local), giving three clean leaf boxes under the fabric cloud (1228×608 px). Aspect ratio flips from portrait to landscape. Lint ink/e gets *worse* (111→117) while the figure becomes unambiguous — the metrics do not measure group containment or overlap.
|
|
247
|
+
|
|
248
|
+
| variant | cross | novlp | ink/e | score |
|
|
249
|
+
|---------|-------|-------|-------|-------|
|
|
250
|
+
| auto | 0 | 1 | 111 | 3 |
|
|
251
|
+
| `+pin` ×20 | 0 | 1 | 117 | 3 |
|
|
252
|
+
|
|
253
|
+
[auto .fd](examples/layout-compare/srl-evpn-irb-auto.fd) · [auto .svg](examples/layout-compare/srl-evpn-irb-auto.svg) · [tuned .fd](examples/layout-compare/srl-evpn-irb-tuned.fd) · [tuned .svg](examples/layout-compare/srl-evpn-irb-tuned.svg)
|
|
254
|
+
|
|
255
|
+
### vxlan-encap — when layout tuning is the wrong fix (0 layout lines)
|
|
256
|
+
|
|
257
|
+
This one used to be listed here as a third before/after pair, and that was the
|
|
258
|
+
mistake. The packet-header figure was authored as fifteen free `node`s; auto-layout
|
|
259
|
+
stacked them into a 415×1704 px vertical column, so 19 `pin` lines were added to
|
|
260
|
+
lay the fields out left to right in byte order (896×736 px). The render looked
|
|
261
|
+
right — and the document was broken, because **the byte order existed only in the
|
|
262
|
+
`pin` x-coordinates**. Nothing in the semantic zone said that Outer MAC precedes
|
|
263
|
+
Outer IP precedes UDP precedes VXLAN, or where the original L2 frame was inserted.
|
|
264
|
+
The pins were not describing an arrangement; they were the only copy of the
|
|
265
|
+
meaning (R37: meaning must never live in geometry).
|
|
266
|
+
|
|
267
|
+
The fix was not a better rung on the ladder — it was **a different construct**. Each
|
|
268
|
+
frame is now a single-row `table` whose cell order *is* the on-the-wire order, with
|
|
269
|
+
a header-tier span for the nested original frame and `class` marks carrying the
|
|
270
|
+
field roles ([vxlan-encap.fd](examples/vxlan-encap.fd)). The document now needs
|
|
271
|
+
**zero** `pin`/`size`/`flow`/`rank` lines and renders at 1036×650 px.
|
|
272
|
+
|
|
273
|
+
**The diagnostic — run the R25 strip test before you tune.** Delete every
|
|
274
|
+
`flow`/`rank`/`pin`/`size`/`routing`/`route` line and the bare `render`, then re-read
|
|
275
|
+
what is left. If a fact you thought the figure stated is now unrecoverable — order,
|
|
276
|
+
adjacency, containment, grouping — you have a semantics bug, and adding layout lines
|
|
277
|
+
only hides it. Fix the construct: ordered sequences belong in `table`, `bitfield`, or
|
|
278
|
+
explicit `edge` chains; containment belongs in `group` or a header span; categories
|
|
279
|
+
belong in `class`. Reach for the ladder in §2 only once the stripped document already
|
|
280
|
+
says everything the figure says.
|
|
281
|
+
|
|
282
|
+
### Failure mode 4 — the render implies a relation the .fd never states
|
|
283
|
+
|
|
284
|
+
The first three modes park meaning where a reader may ignore it (geometry, presentation,
|
|
285
|
+
absence). This one is the mirror image: the `.fd` is correct but the picture asserts
|
|
286
|
+
something extra to a human. Worked case from a private-VLAN example in this repo: the
|
|
287
|
+
promiscuous port was drawn left with edges fanning out, so it read as upstream parent.
|
|
288
|
+
In a private VLAN it is a peer port; the fix was symmetry — four equal-size peer frames,
|
|
289
|
+
promiscuous port centred, router as `boundary` (the only genuinely external thing).
|
|
290
|
+
|
|
291
|
+
R37 protects the agent, who reads syntax; the human reads geometry. A layout that
|
|
292
|
+
implies false structure misleads exactly the reader the render exists for. No tool
|
|
293
|
+
catches this — the author must look. Peers should look like peers: equal size and
|
|
294
|
+
shared alignment carry peerhood without any syntax.
|
|
295
|
+
|
|
296
|
+
**Fidelity checklist** (§5 loop asks "is it readable?"; these ask "is it honest?"):
|
|
297
|
+
|
|
298
|
+
1. **Did I leave anything out?** Is any meaning riding on coordinates, on a stroke or
|
|
299
|
+
colour with no class, or on an edge I did not draw? (`tools/r25-check.js` catches
|
|
300
|
+
the first; the other two need your eyes.)
|
|
301
|
+
2. **Did I add anything?** Does the arrangement imply a hierarchy, direction, sequence,
|
|
302
|
+
or grouping the semantic zone never states? Left-to-right and top-to-bottom both read
|
|
303
|
+
as precedence — check that you meant it.
|
|
304
|
+
3. **Do the edges land where I meant?** An edge piercing a container frame reads as
|
|
305
|
+
targeting the container; a label crossed by a stroke reads as belonging to it.
|
|
306
|
+
|
|
307
|
+
## 8. Cautions from a production corpus
|
|
308
|
+
|
|
309
|
+
A five-way review rendered and inspected all 78 documents of a production
|
|
310
|
+
corpus; below is what authors had to discover by trial and error, most-hit
|
|
311
|
+
first. Expressing-side findings are in [EXPRESSING.md](EXPRESSING.md).
|
|
312
|
+
|
|
313
|
+
**Group captions must be short.** Group rect and canvas are sized from member
|
|
314
|
+
geometry only, so a caption wider than its members is clipped off the *canvas*
|
|
315
|
+
— not merely outside the box, outside the image. Measured: two 35px members, a
|
|
316
|
+
caption needing 368px, canvas 78px wide, caption cut mid-word. Until the
|
|
317
|
+
renderer accounts for caption width, keep a caption no wider than its widest
|
|
318
|
+
member row and put the detail in prose. Most-worked-around defect in the corpus.
|
|
319
|
+
|
|
320
|
+
**Pick the direction for the figure's shape** (extends §2). A short linear
|
|
321
|
+
pipeline — four to six stages, one terminal branch — reads better as
|
|
322
|
+
`flow right` (measured 672×156 px) than as the tall narrow column `flow down`
|
|
323
|
+
gives it (241×396 px). Reverse failure: a very wide multi-line outcome node
|
|
324
|
+
pushes the canvas past a usable aspect ratio; wrap the label with `\n`
|
|
325
|
+
(860×173 → 284×205 px) or move the detail into a companion `table`.
|
|
326
|
+
|
|
327
|
+
**Long back-edges and bypasses need help.** An edge that skips several ranks
|
|
328
|
+
detours to the canvas margin and picks up a waypoint per rank crossed — in a
|
|
329
|
+
six-stage chain one `s1 -> s6` bypass becomes a ten-point polyline out at the
|
|
330
|
+
margin; on large figures it reads as a many-bend staircase. Add a waypoint:
|
|
331
|
+
|
|
332
|
+
```figdown
|
|
333
|
+
node s1 "Stage 1"
|
|
334
|
+
node s6 "Stage 6"
|
|
335
|
+
edge s1 -[fast path]-> s6
|
|
336
|
+
flow down
|
|
337
|
+
render
|
|
338
|
+
route s1 -> s6 via=200,240
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Three points instead of ten. The `route` reference names endpoints and operator
|
|
342
|
+
only, never the mid-label. If a bypass needs several waypoints, reconsider the
|
|
343
|
+
structure instead.
|
|
344
|
+
|
|
345
|
+
**Dense fan-in and fan-out.** Past about five edges between one node and one
|
|
346
|
+
rank, the bundle renders as parallel runs with stacked labels (seven terminal
|
|
347
|
+
targets measured 1079×156 px). Two options today: an explicit relay node, or a
|
|
348
|
+
class that mutes the bundle so it recedes — `class discard "Discard reasons"
|
|
349
|
+
color=#b8b6b0 style=dashed` on the terminal-bound edges, used to good effect by
|
|
350
|
+
a corpus author. On an edge `color=` is what strokes the line (EXPRESSING.md).
|
|
351
|
+
|
|
352
|
+
**Use `rank` for a lateral bypass, not for the mainline.** Under `flow down` a
|
|
353
|
+
`rank` shares a *row*, so ranking the main chain flattens the figure sideways.
|
|
354
|
+
`rank hit punt` lifts a decision's side exit onto the decision's own row and
|
|
355
|
+
leaves the mainline vertical.
|
|
356
|
+
|
|
357
|
+
**Two independent sub-diagrams side by side** — the `group` + `pin` pattern of
|
|
358
|
+
§4. Two state machines, each a group pinned in canvas px with group-local
|
|
359
|
+
members, render as two clean columns in one figure (verified in the corpus).
|
|
360
|
+
Without it authors split into two figures unnecessarily.
|
|
361
|
+
|
|
362
|
+
**When a figure is too big.** Around twenty nodes with a high crossing count a
|
|
363
|
+
single figure stops being readable — guidance, not a rule. Split it and join
|
|
364
|
+
the halves with `boundary` endpoints naming each other: `boundary cont
|
|
365
|
+
"continues in Figure 2"` / `boundary cont "continued from Figure 1"`. A
|
|
366
|
+
boundary states only that the edge crosses the figure's edge, so neither half
|
|
367
|
+
implies a missing participant.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The FigDown Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|