@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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/examples.d.ts +2 -0
- package/dist/examples.js +469 -0
- package/dist/export-image.d.ts +10 -0
- package/dist/export-image.js +47 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.html +13 -0
- package/dist/index.js +18 -0
- package/dist/parser/ast.d.ts +118 -0
- package/dist/parser/ast.js +79 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.js +2 -0
- package/dist/parser/parser.d.ts +2 -0
- package/dist/parser/parser.js +635 -0
- package/dist/renderer/astar-router.d.ts +16 -0
- package/dist/renderer/astar-router.js +532 -0
- package/dist/renderer/gates.d.ts +19 -0
- package/dist/renderer/gates.js +92 -0
- package/dist/renderer/graph.d.ts +31 -0
- package/dist/renderer/graph.js +403 -0
- package/dist/renderer/layout.d.ts +76 -0
- package/dist/renderer/layout.js +3121 -0
- package/dist/renderer/math-renderer.d.ts +16 -0
- package/dist/renderer/math-renderer.js +119 -0
- package/dist/renderer/svg-renderer.d.ts +3 -0
- package/dist/renderer/svg-renderer.js +599 -0
- package/dist/renderer/wires.d.ts +5 -0
- package/dist/renderer/wires.js +12 -0
- package/dist/theme/themes.d.ts +58 -0
- package/dist/theme/themes.js +104 -0
- package/docs/api.adoc +196 -0
- package/docs/ldl-for-llms.md +163 -0
- package/docs/user-guide.adoc +318 -0
- package/package.json +78 -0
- package/spec/render.sh +22 -0
- package/spec/sections/attributes.adoc +80 -0
- package/spec/sections/connections.adoc +39 -0
- package/spec/sections/examples.adoc +212 -0
- package/spec/sections/expressions.adoc +182 -0
- package/spec/sections/file-extension.adoc +5 -0
- package/spec/sections/function-blocks.adoc +120 -0
- package/spec/sections/grammar.adoc +64 -0
- package/spec/sections/hyperlinks.adoc +18 -0
- package/spec/sections/introduction.adoc +16 -0
- package/spec/sections/layout-rules.adoc +491 -0
- package/spec/sections/lexical-conventions.adoc +68 -0
- package/spec/sections/objects.adoc +31 -0
- package/spec/sections/options.adoc +146 -0
- package/spec/sections/ports.adoc +77 -0
- package/spec/sections/rendering-contract.adoc +11 -0
- package/spec/sections/revision-history.adoc +9 -0
- package/spec/sections/styling.adoc +83 -0
- package/spec/sections/svg-symbol-specification.adoc +149 -0
- package/spec/sections/symbol-definitions.adoc +143 -0
- package/spec/sections/templates.adoc +31 -0
- 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
|
+
....
|