@openpowershift/logic-diagram-language 0.1.0

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/examples.d.ts +2 -0
  4. package/dist/examples.js +469 -0
  5. package/dist/export-image.d.ts +10 -0
  6. package/dist/export-image.js +47 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.html +13 -0
  9. package/dist/index.js +18 -0
  10. package/dist/parser/ast.d.ts +118 -0
  11. package/dist/parser/ast.js +79 -0
  12. package/dist/parser/index.d.ts +3 -0
  13. package/dist/parser/index.js +2 -0
  14. package/dist/parser/parser.d.ts +2 -0
  15. package/dist/parser/parser.js +635 -0
  16. package/dist/renderer/astar-router.d.ts +16 -0
  17. package/dist/renderer/astar-router.js +532 -0
  18. package/dist/renderer/gates.d.ts +19 -0
  19. package/dist/renderer/gates.js +92 -0
  20. package/dist/renderer/graph.d.ts +31 -0
  21. package/dist/renderer/graph.js +403 -0
  22. package/dist/renderer/layout.d.ts +76 -0
  23. package/dist/renderer/layout.js +3121 -0
  24. package/dist/renderer/math-renderer.d.ts +16 -0
  25. package/dist/renderer/math-renderer.js +119 -0
  26. package/dist/renderer/svg-renderer.d.ts +3 -0
  27. package/dist/renderer/svg-renderer.js +599 -0
  28. package/dist/renderer/wires.d.ts +5 -0
  29. package/dist/renderer/wires.js +12 -0
  30. package/dist/theme/themes.d.ts +58 -0
  31. package/dist/theme/themes.js +104 -0
  32. package/docs/api.adoc +196 -0
  33. package/docs/ldl-for-llms.md +163 -0
  34. package/docs/user-guide.adoc +318 -0
  35. package/package.json +78 -0
  36. package/spec/render.sh +22 -0
  37. package/spec/sections/attributes.adoc +80 -0
  38. package/spec/sections/connections.adoc +39 -0
  39. package/spec/sections/examples.adoc +212 -0
  40. package/spec/sections/expressions.adoc +182 -0
  41. package/spec/sections/file-extension.adoc +5 -0
  42. package/spec/sections/function-blocks.adoc +120 -0
  43. package/spec/sections/grammar.adoc +64 -0
  44. package/spec/sections/hyperlinks.adoc +18 -0
  45. package/spec/sections/introduction.adoc +16 -0
  46. package/spec/sections/layout-rules.adoc +491 -0
  47. package/spec/sections/lexical-conventions.adoc +68 -0
  48. package/spec/sections/objects.adoc +31 -0
  49. package/spec/sections/options.adoc +146 -0
  50. package/spec/sections/ports.adoc +77 -0
  51. package/spec/sections/rendering-contract.adoc +11 -0
  52. package/spec/sections/revision-history.adoc +9 -0
  53. package/spec/sections/styling.adoc +83 -0
  54. package/spec/sections/svg-symbol-specification.adoc +149 -0
  55. package/spec/sections/symbol-definitions.adoc +143 -0
  56. package/spec/sections/templates.adoc +31 -0
  57. package/spec/spec.adoc +49 -0
@@ -0,0 +1,491 @@
1
+ == Layout Rules
2
+
3
+ The renderer MUST follow these layout rules to produce clear, non-overlapping diagrams.
4
+
5
+ [#visual-quality]
6
+ === Visual Quality Objectives
7
+
8
+ A diagram is "visually good" when an engineer can trace every signal at a glance. The
9
+ renderer optimises for the following objectives, in roughly this priority order, and the
10
+ detailed rules in the rest of this section exist to serve them:
11
+
12
+ . **Left-to-right signal flow.** Inputs on the left, outputs on the right, gates in columns
13
+ ordered by their dependency depth. A reader's eye always moves one way.
14
+ . **Connected and unambiguous.** Every port is wired to exactly what it should be; tap-off
15
+ (fan-out) points are marked with a junction dot; crossings without a dot are never
16
+ connections.
17
+ . **Few crossings.** Wires cross as little as possible. Nodes within a column are ordered to
18
+ minimise crossings (see <<row-optimisation>>). Some crossings are topologically
19
+ unavoidable and are accepted.
20
+ . **Straight, low-bend wires.** A wire is a straight line wherever its endpoints align, and
21
+ otherwise a single clean Z (one vertical run, at least `MIN_DOGLEG` tall). Small jogs are
22
+ never produced.
23
+ . **Aligned gates.** A gate is positioned vertically at the centre of the signals it
24
+ connects — both the sources feeding it and the destination it drives — so its wires are
25
+ as short and straight as possible (see <<coordinate-assignment>>). A single-input gate
26
+ (e.g. NOT) is placed in line with its source and sink.
27
+ . **Even spacing, no overlap.** Gate bodies never overlap; ports within a gate keep a
28
+ uniform gap; parallel wires keep clear of one another and of gate bodies.
29
+ . **Symmetry and balance.** Fan-in and fan-out are arranged symmetrically about the gate
30
+ they serve.
31
+ . **Compact but not cramped.** The diagram is as small as it can be without violating the
32
+ spacing rules, giving a readable aspect ratio.
33
+ . **Stable and fast.** Layout is deterministic (the same source always yields the same
34
+ diagram) and fast: well under 0.5 s for medium diagrams and under 1 s for large ones.
35
+ Routing tries cheap deterministic routes (straight line, then a clean Z) before falling
36
+ back to grid-based pathfinding, so cost scales with diagram complexity, not canvas size.
37
+
38
+ === Column Assignment
39
+
40
+ Gates are assigned to columns based on their depth in the expression tree:
41
+
42
+ * **Column 0**: Input ports (leftmost).
43
+ * **Column N**: Output ports (rightmost).
44
+ * **Intermediate columns**: Gates at depth *d* are placed in column *d*.
45
+
46
+ Outputs of a gate at column *c* align horizontally with the inputs of gates at column *c+1*. The renderer MUST NOT introduce horizontal doglegs (jogs) in a wire between a gate output and a downstream gate input unless there is an obstruction requiring it.
47
+
48
+ === Vertical Alignment
49
+
50
+ * Gate inputs MUST be vertically spaced so that each input sits on the same vertical grid row as its upstream source.
51
+ * When multiple inputs connect to the same source, they implicitly fan out. Fan-out wires MUST be spaced apart vertically so they do not overlap.
52
+ * Horizontal wire segments MUST NOT overlap. If two horizontal wires would occupy the same row, the renderer MUST increment the row of one wire to resolve the conflict.
53
+ * A horizontal wire passing above or below a gate body (one it does not connect to) MUST keep a clear vertical gap of at least **2x the wire gap** off the gate's top/bottom edge, so it never looks crammed against the body. The wire's own source/destination gate is exempt (it connects there). The router keeps the larger clearance vertically while keeping the normal (smaller) clearance horizontally, so fan-in channels just left of a gate are unaffected.
54
+ * Adjacent gate bodies in the same column MUST keep a minimum vertical gap (the protected zone, see the pipeline). When a gate is pushed apart to satisfy this, the small offset added to its wires is kept >= `MIN_DOGLEG` so they stay clean Z-routes.
55
+
56
+ [#row-optimisation]
57
+ ==== Row Optimisation (Sugiyama Barycentre Method)
58
+
59
+ Input nodes are initially assigned rows in natural sort order (I1, I2, ..., I10). When `OPTION INPUT_ORDER = AUTO` (the default) the renderer MUST then apply the Sugiyama barycentre crossing-minimisation algorithm to reorder input nodes so that wires between levels cross as little as possible. When `OPTION INPUT_ORDER = DECLARATION` the inputs keep their natural-sorted order and only the gate rows are propagated from it.
60
+
61
+ The algorithm iterates up to 3 times:
62
+
63
+ . For each input node, compute its barycentre: the average row position of all gates that reference it as an input.
64
+ . Sort the input nodes by their barycentre values.
65
+ . Re-assign sequential row indices (0, 1, 2, ...) based on the sorted order.
66
+ . Recompute the row positions of gates at deeper depths using `(minRow + maxRow) / 2` of their inputs.
67
+
68
+ Gate row positions are NOT compressed within depth groups — each gate stays at the barycentre of its inputs, even if this leaves gaps between gates at the same depth. This ensures gates are visually close to their inputs.
69
+
70
+ [#coordinate-assignment]
71
+ ==== Coordinate Assignment (Two-Sided Alignment)
72
+
73
+ Ordering (above) only fixes the *relative* order of nodes in each column; this step fixes
74
+ their actual vertical positions. A gate should sit at the centre of *all* the signals it
75
+ connects — both the sources feeding its inputs and the destination it drives — not just its
76
+ inputs. Aligning to one side only (inputs) is what leaves a gate offset from its consumer
77
+ and forces needless bends (e.g. a NOT pushed up away from the output it feeds).
78
+
79
+ The renderer therefore performs alternating alignment sweeps after the initial barycentre
80
+ ordering:
81
+
82
+ . **Downward sweep** (sources → sinks): position each gate/output at the median Y of the
83
+ nodes feeding it.
84
+ . **Upward sweep** (sinks → sources): nudge each gate toward the median Y of the nodes it
85
+ drives.
86
+
87
+ Each sweep preserves the column ordering and enforces a minimum vertical separation between
88
+ adjacent nodes in a column (so nodes never overlap). A few iterations converge to a stable
89
+ assignment in which most wires are straight and the rest are clean single-bend Z-routes.
90
+ This is a lightweight form of the priority/median coordinate-assignment methods used in
91
+ layered graph drawing, chosen for speed.
92
+
93
+ [#input-reordering]
94
+ ==== Gate Input Port Assignment
95
+
96
+ Within each gate, input ports MUST be assigned to source signals in ascending order of source Y position. The source with the smallest Y connects to the topmost input port, and the source with the largest Y connects to the bottommost input port.
97
+
98
+ === Layout Pipeline
99
+
100
+ The renderer does not produce a single layout; it produces several **candidate** layouts and keeps
101
+ the one that measures best on the real rendered geometry (see <<crossover-minimisation>>). Each
102
+ candidate is a full, independent layout over two orthogonal dimensions — input **ordering**
103
+ (`heuristic` / `crossmin`) and long-edge **lane packing** (loose / tight) — ranked lexicographically
104
+ by *fewest sub-min doglegs, then crossings, then bends, then height*. The loose-heuristic candidate is
105
+ always generated and is always dogleg-free, so the winner can never be worse than it on any of those.
106
+ A final cosmetic symmetry pass (small-gate fan-in doglegs mirrored) is applied to the winner, fully
107
+ validated so it can never add a crossing or crowd.
108
+
109
+ Within a single candidate, all gate position adjustments MUST be completed before wire routing begins,
110
+ so wire obstacle data (gate bounding boxes) reflects the final gate positions. A candidate executes in
111
+ this order:
112
+
113
+ . Row **ordering** (barycentre + contiguity, or the `crossmin` optimiser) — REAL nodes only.
114
+ . Long-edge **lane reservation**: every edge spanning more than one column is decomposed into a chain
115
+ of thin dummy nodes so coordinate assignment reserves a clear vertical lane for it. Under tight
116
+ packing, adjacent dummy lanes pack at `MIN_DOGLEG` rather than the full column channel (a lane next
117
+ to a real gate always keeps the full channel, so gates never shift toward a lane).
118
+ . **Coordinate assignment**: a joint per-column optimiser (weighted isotonic regression / PAVA) aligns
119
+ each node to the port-aware mean of its neighbours on both sides, iterated to convergence. This
120
+ replaces the old greedy median sweeps and settles gate heights, fan-in spreading and single-input
121
+ alignment together. Dummies are then removed.
122
+ . Downstream placement refinements PAVA does not do on its own: gate min-jog placement, dogleg
123
+ cleanup, a protected minimum gap between adjacent gate bodies, and output placement.
124
+ . **Wire routing** (obstacle-aware A* fan-out), then the reshaping passes: channel-track assignment,
125
+ multi-input fan-in channels (<<fan-in-channels>>), obstacle-aware output placement
126
+ (<<output-placement>>) and input un-wrap (<<input-unwrap>>), sub-`MIN_DOGLEG` straightening,
127
+ feedback routing, shared fan-out trunk merge, and the gate-entrance guarantee (<<gate-entrance>>).
128
+ . **Junction detection**, a final uniform vertical re-normalisation (which shifts every node, wire,
129
+ junction AND label together so labels never drift from the gates they annotate), and a
130
+ **blank-band collapse**: any fully-empty horizontal band between two disconnected logic sections
131
+ (no node, label, or wire — including verticals — crosses it, so the sections share no signal) is
132
+ reduced to a fixed section gap, removing wasted vertical space. Being a uniform per-section shift
133
+ over an empty band, it preserves every wire shape and cannot cause an overlap or crossing.
134
+
135
+ Every reshaping pass validates its proposed geometry through the shared wire-separation contract
136
+ (>= `MIN_WIRE_SPACING` between parallel cross-net segments) plus gate clearance, and reverts any move
137
+ that would add a crossing — so no pass can silently undo another's separation or introduce a crossover.
138
+
139
+ === Wire Routing
140
+
141
+ Every wire MUST be drawn as either a straight horizontal line or a single clean
142
+ **Z-route** (`horizontal → vertical → horizontal`). Wires MUST NOT contain *doglegs* — a
143
+ dogleg is a small or needless jog, defined as a vertical run shorter than `MIN_DOGLEG`
144
+ (30px) sitting between two horizontal segments. Doglegs are avoided primarily at the
145
+ *layout* level (see <<minimum-dogleg-height>>): port Y positions are aligned so the
146
+ natural orthogonal route is already straight or a clean Z.
147
+
148
+ Wires between ports follow these rules:
149
+
150
+ . A wire exits a source port horizontally to the right.
151
+ . At a vertical channel between the source column and the destination column, the wire
152
+ turns vertically and travels to align with the destination port's Y.
153
+ . The wire enters the destination port horizontally. For AND/NOT inputs it enters at the
154
+ straight bounding-box left edge; for OR inputs it terminates **on the concave curve**
155
+ (see <<or-curve-ports>>).
156
+ . [[gate-entrance]]**Minimum gate entrance (a guaranteed contract).** When a wire turns into a gate input port,
157
+ its final horizontal run (the entrance) MUST be at least `GATE_ENTRANCE` (20px) long, so the
158
+ vertical turn sits clear of the gate body/curve and the wire visibly enters horizontally rather
159
+ than turning on the gate's edge. Equivalently, a turning wire's vertical channel sits at least
160
+ `GATE_ENTRANCE` to the left of the port's X. This matters most for OR inputs, whose curve-tap X
161
+ lies inside the bounding box: without the rule the channel would hug (or fall on) the box's left
162
+ edge.
163
+ +
164
+ This is not left to chance in the routing/reshaping heuristics (which each satisfy it only as a
165
+ side effect and can leave a hug behind — the channel-track pass skips multi-bend wires, and fan-in
166
+ nesting is all-or-nothing). It is *guaranteed* by a dedicated final pass that, for **every**
167
+ gate-input wire, pulls its final turn back to the gate-most clear track that satisfies the contract
168
+ — first trying to draw a wandered-but-collinear wire straight, then a per-wire pull-back, then a
169
+ joint re-pack of all of a gate's incoming channels. Every move is validated through the wire-
170
+ separation contract, gate clearance, and a no-new-crossing check, so it can never introduce a crowd
171
+ or crossing; a move that cannot be made cleanly is left as routed. The guarantee is asserted for
172
+ every example by the `gate-input wires keep the GATE_ENTRANCE approach` invariant. The single
173
+ documented exception is a wire that can only reach a compliant entrance by adding a crossing (a
174
+ middle input whose straight line is occupied by another net): there the shorter approach is kept,
175
+ because crossing-minimisation outranks entrance — a *placement* over-constraint, not a routing gap.
176
+
177
+ [#feedback-loops]
178
+ === Feedback (Loop-Back) Wires
179
+
180
+ When an output name is used inside its own definition (directly or transitively) — a
181
+ self/cyclic reference such as a seal-in latch `Q = SET OR (Q AND NOT RESET)` — it MUST NOT
182
+ be added as a new input. Instead it loops back: the output's signal feeds back into the gate
183
+ that consumes it. The feedback shares the output's driving signal (it taps the gate that
184
+ drives the output) and is routed by the obstacle-aware pathfinder as a local loop around the
185
+ gates — not a fixed full-width lane. The loop keeps a clear distance (at least 3x the wire
186
+ gap) off the driver output and the consumer input, and it enters the consumer on the side it
187
+ approaches from (over the top → top input port; under the bottom → bottom input port),
188
+ whichever gives fewer crossings. Feedback wires are exempt from the left-to-right flow and
189
+ no-backward-segment rules (they are loops by nature). A forward reference to an
190
+ *already-defined* output is not a feedback edge.
191
+ . When the source and destination Y differ by less than 1px, the wire MUST be a straight
192
+ horizontal line with no vertical segment.
193
+ . Wires MUST NOT backtrack; a vertical segment's X MUST be >= the source port's X + 15px.
194
+ . Every node position, port Y position, and wire vertex MUST align to the 5px grid (the OR
195
+ input-port X, which taps the curve, is the sole exception). Every wire segment MUST be
196
+ exactly horizontal or vertical — no diagonal segments.
197
+
198
+ The renderer uses A* pathfinding on the 5px grid to realise these routes around obstacles:
199
+
200
+ . Gate bodies are impassable obstacles with a 20% buffer zone. Only the source and
201
+ destination gates of a wire are routable through; all other objects block routing.
202
+ . The cost model penalises wire–wire crossings, wrong-side gate entry (entering to the
203
+ right of a gate input port), and direction changes, so paths avoid crossovers, enter
204
+ gates from the left, and have minimal bends (a straight line or a single Z).
205
+ . Previously-routed wires are obstacles. A horizontal segment MUST NOT run along or cross
206
+ a vertical segment of a different source, and vice versa, unless unavoidable.
207
+ . Parallel wire segments MUST be separated into distinct tracks with a minimum spacing of
208
+ `MIN_WIRE_SPACING` between adjacent parallels, so bundles remain individually legible.
209
+ The renderer favours readability (wider channels) over compactness.
210
+
211
+ [#minimum-dogleg-height]
212
+ ==== Minimum Dogleg Height (`MIN_DOGLEG`)
213
+
214
+ When a wire must dogleg (its source Y differs from its target port Y), the vertical segment of the dogleg MUST be at least `MIN_DOGLEG` (30px) tall. Doglegs shorter than `MIN_DOGLEG` are visually ambiguous and MUST be avoided.
215
+
216
+ The renderer enforces this by:
217
+
218
+ . **Position optimisation**: Shifting gates vertically to align ports with their sources (eliminating doglegs entirely) or ensuring at least `MIN_DOGLEG` of vertical distance.
219
+ . **Dogleg enforcement**: After position optimisation, any remaining small dogleg (where `0 < |sourceY - portY| < MIN_DOGLEG`) triggers a gate height expansion for that specific port, placing it at the source Y position (straight-through) or at source Y ± MIN_DOGLEG (minimum-height dogleg).
220
+
221
+ A dogleg of 0px (source Y matches target Y exactly) is a straight-through wire and is always acceptable.
222
+
223
+ [#gate-sizing-principle]
224
+ ==== Gate Sizing Principle
225
+
226
+ A gate's body height is a function of its *port count only* — it grows to give each input port room
227
+ (`MIN_PORT_GAP` between ports plus edge padding), and for **no other reason**. A gate MUST NOT be
228
+ enlarged merely to make far-apart input sources line up with its ports.
229
+
230
+ When a gate's input sources are spread farther apart than its (port-count-sized) body can span, the
231
+ incoming wires dogleg to reach the ports. Such a dogleg MUST still respect <<_minimum_dogleg_height_min_dogleg,`MIN_DOGLEG`>>
232
+ (a clean Z, never a short jog), and the gate is positioned so the wires are straight where the body
233
+ can reach the source and a clean dogleg otherwise.
234
+
235
+ NOTE: *Tension with the no-dogleg goal.* For a fixed-height gate, two sources whose separation falls
236
+ between the body's usable inner span and that span plus `MIN_DOGLEG` cannot *both* be reached by a
237
+ straight or clean-`MIN_DOGLEG` wire — one wire is forced into the sub-`MIN_DOGLEG` band. The only
238
+ ways to make both wires clean are (a) grow the gate (violates this principle), or (b) reposition the
239
+ upstream *inputs* so their outputs align with the compact gate's ports (*gate-aware input placement*).
240
+ The latter is the correct resolution: gate size stays port-count-driven and the inputs, which have
241
+ free vertical slots in the input column, move to align. Until gate-aware input placement lands, the
242
+ renderer expands the gate as a stopgap (see <<gate-height-adjustment>>) — a known deviation from this
243
+ principle, tracked in `IMPLEMENTATION.md`.
244
+
245
+ [#gate-height-adjustment]
246
+ ==== Gate Height Adjustment (current stopgap)
247
+
248
+ When a multi-input gate (AND, OR) has input ports that are close to their upstream source Y positions, the gate height is expanded so that the input ports align with their sources. This eliminates small doglegs on incoming wires. (This expansion is the stopgap noted in <<gate-sizing-principle>>; the target behaviour keeps the gate at its port-count height and instead aligns the inputs.)
249
+
250
+ The algorithm:
251
+
252
+ . Sort the gate's input sources by their output Y position (ascending).
253
+ . Compute ideal port Y positions, enforcing a minimum vertical gap (`MIN_PORT_GAP` = 22px) between adjacent ports.
254
+ . Calculate the required gate height to contain all ideal ports with top and bottom padding of `MIN_PORT_GAP`.
255
+ . If the required height does not exceed the base gate height by more than `MIN_PORT_GAP × numInputs`, expand the gate and place ports at their ideal Y positions.
256
+ . If the expansion would be too large (sources span many rows), the gate keeps its base evenly-spaced ports.
257
+
258
+ For gates that were not expanded, a position optimisation pass shifts the gate vertically (within ±`MIN_DOGLEG`) to minimise doglegs. The optimisation uses a weighted score:
259
+
260
+ * A wire where source Y matches port Y (|diff| < 1px) scores **0** — this is a straight-through wire, the ideal outcome.
261
+ * A wire where 1px ≤ |diff| < `MIN_DOGLEG` scores **(MIN_DOGLEG - |diff|)²** — small doglegs are heavily penalised.
262
+ * A wire where |diff| ≥ `MIN_DOGLEG` scores **0** — large doglegs are visually clear and acceptable.
263
+
264
+ The optimisation finds the vertical shift that minimises the total score across all input ports of the gate.
265
+
266
+ For single-input gates (NOT, single-input AND/OR), the entire gate is shifted so that its input port aligns exactly with its source's output Y, producing a straight-through wire with no dogleg.
267
+
268
+ After all gate position adjustments, output nodes are re-aligned to their source gate outputs to maintain straight-through output wires.
269
+
270
+ [#straight-outputs]
271
+ ==== Output Straight-Through
272
+
273
+ Output ports SHOULD be positioned so that their input Y aligns with the source gate's output Y, so the wire from a gate to its output port is a straight horizontal line. When two outputs share (or nearly share) a source Y they cannot both be straight; the lower one is pushed down by at least `MIN_DOGLEG` (never a small jog) — see <<output-ordering>>.
274
+
275
+ [#output-ordering]
276
+ ==== Output Ordering
277
+
278
+ The vertical order of output ports in the final column is controlled by the
279
+ `OUTPUT_ORDER` option:
280
+
281
+ * `DECLARATION` (default) — outputs are stacked in the order they are declared (O1, O2,
282
+ …) from top to bottom. Predictable, but output wires may need to cross or dogleg to
283
+ reach a source on the opposite side.
284
+ * `AUTO` — outputs are reordered by the Y position of the gate that drives them (the same
285
+ barycentre principle applied to inputs in <<row-optimisation>>). Outputs then stack in
286
+ the order their wires arrive, so output wires fan out without crossing. Note: AUTO can
287
+ increase output-column congestion in some diagrams, so it is opt-in rather than default.
288
+
289
+ In both modes outputs in the same column are placed greedily in the chosen order: each is
290
+ placed at its source Y where possible, otherwise pushed down just enough to clear the
291
+ previous output, with any deviation kept at 0 or `>= MIN_DOGLEG`.
292
+
293
+ [#or-curve-ports]
294
+ ==== OR Gate Curve Ports
295
+
296
+ The OR gate's left edge is a concave curve that bulges rightward. Unlike the AND gate
297
+ (straight left edge), the OR gate's input ports sit **on the curve**, and incoming wires
298
+ MUST terminate on the curve in all cases — a wire never stops short of, or overshoots, the
299
+ curve.
300
+
301
+ * For each OR input at grid-aligned Y, the input-port X is the curve's X at that Y
302
+ (`bbox-left + curveDepth(Y)`). The connecting wire's final point is this curve point.
303
+ * The curve has a fixed maximum depth (`OR_CURVE_DEPTH`, ~10px) independent of the gate
304
+ height, so the concavity reads consistently regardless of the number of inputs or the
305
+ gate's expanded height.
306
+ * The gate's **bounding box, right-edge output port, and every port Y position MUST stay
307
+ on the 5px grid**. Only the input-port X (the curve tap) is exempt, since it follows the
308
+ curve.
309
+
310
+ [#common-subexpression-sharing]
311
+ ==== Common Subexpression Sharing
312
+
313
+ The renderer MUST deduplicate structurally identical subexpressions into a single gate node. Two subexpressions are structurally identical if they have the same gate type and the same set of resolved input IDs after sorting inputs for commutative operators (AND, OR).
314
+
315
+ When multiple outputs reference the same logical subexpression, the renderer MUST create one gate node and route wires from that node's output to all downstream consumers. This prevents overlapping gate nodes at identical positions and redundant wire routing.
316
+
317
+ Example:
318
+
319
+ [source]
320
+ ----
321
+ O1 = A AND B
322
+ O4 = NOT (A AND B)
323
+ ----
324
+
325
+ The subexpression `A AND B` is referenced by both O1 and O4. The renderer MUST create a single AND gate for `A AND B` and fan out its output to both O1 and the NOT gate feeding O4, rather than creating two separate AND gates at the same grid position.
326
+
327
+ For non-commutative operators (NOT) and future derived operators (NAND, NOR, XOR, XNOR), deduplication MUST preserve input ordering. Two expressions are identical only if their inputs appear in the same gate-specific order.
328
+
329
+ [#symmetric-channels]
330
+ [#fan-in-channels]
331
+ ==== Multi-Input Fan-In Channels
332
+
333
+ When several wires dogleg into the same gate, each gets its own vertical channel just to
334
+ the left of the gate, rather than all crowding into one bundle. The channels are:
335
+
336
+ * **Evenly spaced** — adjacent channels are `FANIN_SPACING` (15px, the same as the port
337
+ gap) apart, so the fan-in is never more crowded than the ports it feeds. Because the
338
+ channels live in the gap immediately left of the gate, this is how the layout keeps a
339
+ consistent gap without having to physically move the gate further right.
340
+ * **Nested** — wires arriving from above the gate and wires arriving from below are nested
341
+ independently, with the most extreme source (topmost / bottommost) turning closest to
342
+ the gate and progressively inner sources turning further left. This is the ordering that
343
+ is provably crossing-free for a single source column, and it reads symmetrically between
344
+ the top and bottom halves of the gate.
345
+ * **Obstacle-avoiding** — each wire does not take one fixed channel position; it SEARCHES from its
346
+ ideal nested channel leftward for the gate-most position that clears all gate bodies (so a wire
347
+ whose source row is blocked by a gate turns *before* that obstacle instead of the group giving up).
348
+ * **Validated** — a channel is only used if it clears all gate bodies and does not overlap a wire from
349
+ a different net; a group keeps its routed geometry if any wire finds no valid channel, and the whole
350
+ group reshape is reverted if it would add a crossing, so the optimisation never introduces a new
351
+ collision or crossover.
352
+
353
+ [#output-placement]
354
+ ==== Obstacle-Aware Output Placement
355
+
356
+ An output whose port Y falls inside the vertical shadow of a gate its incoming wire must cross is
357
+ forced to dodge that gate — a down-and-up / up-and-over detour that also tends to land on unrelated
358
+ wires' lines. When this happens the renderer moves the output to the nearest Y clear of every crossed
359
+ gate's shadow and redraws the wire as one clean H–V–H. The move is applied only when a clear position
360
+ and route exist that are strictly simpler (2 bends versus the detour's more) and validate through gate
361
+ clearance, the wire-separation contract, sibling-output spacing, a legal dogleg, and a no-new-crossing
362
+ guard; otherwise it is reverted. So it can only ever simplify a forced detour and never introduces a
363
+ crossing, crowd, overlap, or sub-`MIN_DOGLEG` dogleg.
364
+
365
+ [#input-unwrap]
366
+ ==== Obstacle-Aware Input Un-Wrap
367
+
368
+ The mirror of output placement, on the source side. A single-consumer input feeds one port of a gate,
369
+ but a sibling block that also feeds that gate — an SR seal-in latch or a sub-`OR` — can sit in the
370
+ horizontal span between them, occupying the input's straight-line Y. The router is then forced to wrap
371
+ that input up-and-over (or through) the block, landing its wire across the block's own I/O (e.g. a
372
+ cross-trip input into a trip `OR`, wrapped over the seal-in `SR`, crossing both the latch's `Q`-out and
373
+ its set/reset-in). When this happens the renderer moves the input's *source* Y to the near outer edge of
374
+ that shadow and redraws the wire as one clean H–V–H entering the fixed port from the correct side. It
375
+ searches source Ys outward from the port and keeps the fewest-crossing fully-clear route — a partial
376
+ un-wrap that merely lands on the block's other input wire is not the target. Because only that one wire
377
+ moves, its own crossing count is the exact whole-diagram delta, so the move is kept only when it
378
+ strictly reduces crossings and validates through gate clearance, the wire-separation contract,
379
+ sibling-input spacing, and a legal dogleg; otherwise it is reverted. So it can only ever remove a wrap
380
+ crossing and never introduces a crossing, crowd, overlap, or sub-`MIN_DOGLEG` dogleg.
381
+
382
+ [#fan-out-dots]
383
+ ==== Fan-Out Junction Dots
384
+
385
+ When one source drives several destinations, sibling branches whose vertical channels fall
386
+ within `FANIN_SPACING` of each other SHOULD share a single trunk channel (same-source
387
+ overlap is intended and reads as one trunk). A connection point is marked with a junction
388
+ dot at each true T-intersection; junction dots within a few pixels of each other are merged
389
+ into one so a split reads as a single clean dot rather than a cluster.
390
+
391
+ [#crossover-minimisation]
392
+ ==== Crossover Minimisation
393
+
394
+ Crossing count is a *measured optimisation objective*, not a side-effect of ordering heuristics. The
395
+ renderer generates more than one candidate layout and keeps the one that measures best on the drawn
396
+ geometry — because the classic combinatorial layer-crossing count diverges from the crossings actually
397
+ drawn once gate heights, multi-port entry and routing doglegs are taken into account. Candidates span
398
+ two orthogonal dimensions: input **ordering** — `heuristic` (barycentre + contiguity — always a
399
+ candidate, so the result can never be worse than it) and `crossmin` (a Sugiyama median/transpose
400
+ optimiser on a dummy-expanded layered graph, port-aware so fixed-port blocks stay uncrossed) — and
401
+ long-edge **lane packing** (loose / tight, see the pipeline). The winner is chosen lexicographically by
402
+ **fewest sub-min doglegs, then fewest crossings, then fewest bends, then smallest height**. Because the
403
+ loose-heuristic candidate is always present and always dogleg-free, the winner never has more doglegs
404
+ or crossings than it; a tighter/alternate candidate wins only when it strictly improves. Candidates
405
+ beyond loose-heuristic are computed lazily (e.g. `crossmin` only when `heuristic` is not already
406
+ crossing-free), keeping the common case cheap.
407
+
408
+ Ordering constraints the candidates honour: a **fixed-port block's** sources are ordered to match its
409
+ port order (S above R, …) so its inputs never cross; each gate's single-consumer inputs stay
410
+ **contiguous** so a large fan-in enters straight; a reconvergent gate settles between its inputs and
411
+ consumers so its outputs don't dogleg back across another gate's fan-out.
412
+
413
+ Rendered crossings per diagram MUST NOT exceed a recorded **ceiling** (guarded by the test suite);
414
+ most diagrams must render crossing-free, and lowering a ceiling is a reviewed improvement. When wires
415
+ from opposite sides of a gate converge, the renderer assigns channel positions to minimise crossings.
416
+
417
+ [#wire-wire-crossings]
418
+ ==== Wire-Wire Crossing Avoidance
419
+
420
+ Wire routing MUST consider previously-routed wires as obstacles. When routing a horizontal segment, the renderer MUST avoid crossing vertical segments of wires from different sources. When routing a vertical segment, the renderer MUST avoid crossing horizontal segments of wires from different sources.
421
+
422
+ When a crossing is unavoidable (the wire must pass through a region occupied by another wire), the renderer MUST prioritise clean straight-through wires over doglegs — a straight line crossing a vertical segment is visually clearer than a small dogleg crossing the same segment.
423
+
424
+ [#inversion-bubble-layout]
425
+ ==== Inversion Bubble Layout
426
+
427
+ When `OPTION INVERSION = BUBBLES` is set:
428
+
429
+ . NOT gates are not placed as separate columns. Instead, the affected port is marked with a bubble.
430
+ . The wire from the NOT's source connects directly to the bubbled port.
431
+ . The inversion bubble MUST sit fully **outside the gate body, just to the left of the gate
432
+ edge**, in reserved horizontal space between the incoming wire and the gate. It MUST NOT
433
+ overlap the gate body.
434
+ . **Input-side bubbles**: When a NOT operation feeds into a gate input, the bubble's inner
435
+ (right) edge touches the gate edge — the straight bounding-box left edge for AND/NOT, or
436
+ the **curve tap point** for OR — and the incoming wire terminates at the bubble's outer
437
+ (left) edge. Layout reserves space so the bubble does not collide with the gate body or
438
+ the curve.
439
+ . **Output-side bubbles**: When a NOT operation wraps a gate's output, the bubble sits just
440
+ right of the gate's right edge; the bubble's left edge touches the gate body and the wire
441
+ starts from the bubble's right edge.
442
+ . **Output node bubbles**: When a NOT operation feeds directly into an output node, the
443
+ bubble sits just left of the output node's input port, like a gate input-side bubble.
444
+ . The bubble does not affect the vertical position of any port -- Y positions are computed as normal.
445
+ . The NOT gate's column is eliminated, and the source signal is treated as if it directly connects to the target port (with inversion). This reduces the total column count when many NOT operations are present.
446
+
447
+ ===== Double-Inversion Cancellation
448
+
449
+ Consecutive `NOT` operations are algebraically simplified before bubble placement:
450
+
451
+ * An even number of consecutive `NOT` operations cancels entirely. No bubble is rendered, and the source signal connects directly without inversion.
452
+ * An odd number of consecutive `NOT` operations reduces to a single inversion, rendered as one bubble.
453
+
454
+ Examples:
455
+
456
+ * `NOT NOT I1` → `I1` (no bubble)
457
+ * `NOT NOT NOT I1` → `NOT I1` (one bubble)
458
+
459
+ [#input-bar-layout]
460
+ ==== Input Bar Layout
461
+
462
+ When `OPTION GATE_INPUT_STYLE = BARS` is set for a multi-input gate (3+ inputs):
463
+
464
+ . The gate body height remains the 2-input base height (44px for AND/OR).
465
+ . A vertical bar is drawn 12px to the left of the gate body edge, spanning the full height of the gate body.
466
+ . Bar stroke-width: 2.5px, colour: `#2c3e50`.
467
+ . The first two inputs render as normal ports at their usual positions on the left side of the gate body.
468
+ . Additional inputs (3rd, 4th, ...) tap into the vertical bar at evenly-spaced Y positions within the gate body bounds.
469
+ . Each bar tap has a short horizontal stub (6px) from the bar to the gate edge.
470
+ . Wires from source signals connect at the left end of the bar tap stubs.
471
+ . Wire routing treats the bar + stubs as part of the gate bounding box for obstacle avoidance.
472
+ . Row optimisation, gate height adjustment, position optimisation, and dogleg enforcement all apply to bar input ports the same as regular input ports.
473
+
474
+ === Layer Visibility
475
+
476
+ The rendered diagram supports multiple *layers* that can be independently shown or hidden:
477
+
478
+ [cols="1m,3m,2m"]
479
+ |===
480
+ | Layer | Content | Default
481
+ | `gates` | Gate bodies, wires, and port markers | Visible
482
+ | `labels` | `.Name` and `.Description` text on input and output ports | Visible
483
+ | `ids` | Bare identifiers (e.g. `I1`, `O1`) when labels layer is hidden, or as supplementary identifiers | Hidden
484
+ |===
485
+
486
+ Layers are toggled via CSS classes on the root `<svg>` element:
487
+
488
+ * `ldl-layer-labels` -- controls visibility of `.ldl-label` elements.
489
+ * `ldl-layer-ids` -- controls visibility of `.ldl-id` elements.
490
+
491
+ When the labels layer is active, `.ldl-label` elements are visible and `.ldl-id` elements are hidden by default. When the labels layer is inactive, `.ldl-id` elements are shown as the primary reference.
@@ -0,0 +1,68 @@
1
+ == Lexical Conventions
2
+
3
+ === Character Set
4
+
5
+ LDL source files are UTF-8 text.
6
+
7
+ === Keywords
8
+
9
+ The following keywords are reserved and MUST NOT be used as identifiers:
10
+
11
+ [cols="1m,1m,1m,1m,1m,1m"]
12
+ |===
13
+ | AND | OR | NOT | NAND | NOR | XOR
14
+ | XNOR | IMPORT | SYMBOL | PORT | INPUT | OUTPUT
15
+ | BIDI | ATTRIBUTE | TEMPLATE | LINK | AS | FROM
16
+ | OPTION | STYLE | END | STYLESHEET | CONNECT |
17
+ |===
18
+
19
+ === Identifiers
20
+
21
+ An identifier is a sequence of letters, digits, and underscores, beginning with a letter or underscore.
22
+
23
+ [literal]
24
+ ....
25
+ IDENTIFIER := [a-zA-Z_][a-zA-Z0-9_]*
26
+ ....
27
+
28
+ === Literals
29
+
30
+ [cols="2m,3m,2m"]
31
+ |===
32
+ | Kind | Syntax | Example
33
+ | Integer | `[0-9]+` | `30`
34
+ | Float | `[0-9]+\.[0-9]+` | `1.5`
35
+ | String | `"..."` | `"Main Trip"`
36
+ | Math String | `"..$..$.."` | `"Phase $I_a$ current"`
37
+ | Duration | `<number> <unit>` | `30 s`, `100 ms`
38
+ | Boolean | `TRUE` \| `FALSE` | `TRUE`
39
+ |===
40
+
41
+ Duration units: `ms` (milliseconds), `s` (seconds), `m` (minutes).
42
+
43
+ === Comments
44
+
45
+ A line comment begins with `//` and extends to end of line.
46
+
47
+ [literal]
48
+ ....
49
+ // This is a comment
50
+ ....
51
+
52
+ A block comment begins with `/*` and ends with `*/`. Block comments nest.
53
+
54
+ [literal]
55
+ ....
56
+ /* This is a
57
+ block comment */
58
+ ....
59
+
60
+ === Math Delimiters
61
+
62
+ Within string-valued attributes (`.Name` and `.Description`), TeX math notation is enclosed in dollar signs:
63
+
64
+ * `$...$` -- Inline TeX math expression.
65
+ * Inside math content, `\$` renders as a literal dollar sign (TeX syntax).
66
+ * Outside math content, `\\$` in LDL source produces `\$`, which the segment parser treats as a literal dollar sign.
67
+
68
+ Math delimiters are not recognized in expression code, only in quoted string attribute values.
@@ -0,0 +1,31 @@
1
+ == Objects
2
+
3
+ An *object* is an instance of a symbol. Every gate and composite element in a diagram is an object.
4
+
5
+ === Object Declaration
6
+
7
+ An object is declared by referencing its symbol type, optionally with an identifier:
8
+
9
+ [literal]
10
+ ....
11
+ SYMBOL_NAME#ID
12
+ ....
13
+
14
+ [cols="2m,5m,3m"]
15
+ |===
16
+ | Part | Meaning | Required
17
+ | `SYMBOL_NAME` | The symbol type | Yes
18
+ | `#ID` | A unique identifier for CSS targeting and cross-reference | No
19
+ |===
20
+
21
+ If `#ID` is omitted, the renderer assigns an anonymous identifier.
22
+
23
+ === Examples
24
+
25
+ [literal]
26
+ ....
27
+ AND#G1
28
+ NOT#INV3
29
+ SVT#1
30
+ TIMER#T2
31
+ ....