@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,3121 @@
|
|
|
1
|
+
import { DEFAULT_OPTIONS } from '../parser/ast.js';
|
|
2
|
+
import { hasMathContent } from './math-renderer.js';
|
|
3
|
+
import { routeWireAStar } from './astar-router.js';
|
|
4
|
+
import { orCurveTapX } from './gates.js';
|
|
5
|
+
import { buildGraph } from './graph.js';
|
|
6
|
+
const GATE_W = 60;
|
|
7
|
+
const INPUT_BAR_OFFSET = 12;
|
|
8
|
+
const GATE_W_MULTI = 75;
|
|
9
|
+
const AND_GATE_H_BASE = 45;
|
|
10
|
+
const PORT_SPACING = 15;
|
|
11
|
+
const NOT_TRIANGLE_W = 50;
|
|
12
|
+
const BUBBLE_R = 5;
|
|
13
|
+
const NOT_GATE_TOTAL_W = NOT_TRIANGLE_W + BUBBLE_R * 2 + 5;
|
|
14
|
+
const NOT_GATE_H = 40;
|
|
15
|
+
const INPUT_LABEL_W = 90;
|
|
16
|
+
const OUTPUT_LABEL_W = 90;
|
|
17
|
+
const INPUT_STUB = 10;
|
|
18
|
+
const OUTPUT_STUB = 10;
|
|
19
|
+
const PORT_SIZE = 5;
|
|
20
|
+
const COL_SPACING = 260;
|
|
21
|
+
const ROW_SPACING = 80;
|
|
22
|
+
const PAD_X = 170;
|
|
23
|
+
const PAD_Y = 50;
|
|
24
|
+
const MIN_PORT_GAP = 25;
|
|
25
|
+
const MIN_DOGLEG = 30;
|
|
26
|
+
const MIN_WIRE_SPACING = 10;
|
|
27
|
+
const MIN_CHANNEL_SPACING = 20;
|
|
28
|
+
const WIRE_PAD = MIN_WIRE_SPACING / 2;
|
|
29
|
+
const BUBBLE_STUB = 5;
|
|
30
|
+
const GRID = 5;
|
|
31
|
+
// Round a height UP to an even number of grid cells so the vertical centre (h/2) is exactly
|
|
32
|
+
// on the grid. AND/OR gate output ports and the OR arc tip both sit at h/2; without this the
|
|
33
|
+
// drawn arc tip drifts off-grid and no longer coincides with the port / junction dot.
|
|
34
|
+
const EVEN_CELL = 2 * GRID;
|
|
35
|
+
function evenGridHeight(v) { return Math.ceil(v / EVEN_CELL) * EVEN_CELL; }
|
|
36
|
+
let _id = 0;
|
|
37
|
+
function uid(prefix) { return `${prefix}_${++_id}`; }
|
|
38
|
+
function naturalCompare(a, b) {
|
|
39
|
+
const aParts = a.match(/\d+|\D+/g) ?? [a];
|
|
40
|
+
const bParts = b.match(/\d+|\D+/g) ?? [b];
|
|
41
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
42
|
+
const aIsNum = /^\d+$/.test(aParts[i]);
|
|
43
|
+
const bIsNum = /^\d+$/.test(bParts[i]);
|
|
44
|
+
if (aIsNum && bIsNum) {
|
|
45
|
+
const diff = parseInt(aParts[i]) - parseInt(bParts[i]);
|
|
46
|
+
if (diff !== 0)
|
|
47
|
+
return diff;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const diff = aParts[i].localeCompare(bParts[i]);
|
|
51
|
+
if (diff !== 0)
|
|
52
|
+
return diff;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return aParts.length - bParts.length;
|
|
56
|
+
}
|
|
57
|
+
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh, pad = 0) {
|
|
58
|
+
return !(ax + aw + pad < bx || bx + bw + pad < ax || ay + ah + pad < by || by + bh + pad < ay);
|
|
59
|
+
}
|
|
60
|
+
// The base (pre-expansion) height of a node, mirroring the height logic in the node-creation
|
|
61
|
+
// loop. Used by the coordinate assignment to space nodes within a column.
|
|
62
|
+
function baseNodeHeight(n) {
|
|
63
|
+
if (n.gateType === 'DUMMY')
|
|
64
|
+
return 0; // a thin long-edge lane; spacing comes from VGAP
|
|
65
|
+
if (n.kind === 'input' || n.kind === 'output') {
|
|
66
|
+
const math = (!!n.name && hasMathContent(n.name)) || (!!n.description && hasMathContent(n.description));
|
|
67
|
+
return n.description || math ? 30 : 20;
|
|
68
|
+
}
|
|
69
|
+
if (n.blockType === 'FB')
|
|
70
|
+
return fbDims(n).h + (n.description ? 18 : 0);
|
|
71
|
+
if (n.blockType)
|
|
72
|
+
return blockSize(n.blockType).h + (n.name ? 18 : 0) + (n.description ? 18 : 0);
|
|
73
|
+
if (n.gateType === 'NOT')
|
|
74
|
+
return NOT_GATE_H;
|
|
75
|
+
const numInputs = n.inputIds.length || 2;
|
|
76
|
+
const labelSpace = (n.name ? 18 : 0) + (n.description ? 18 : 0);
|
|
77
|
+
return gateBodyHeight(numInputs, gateGap(n), labelSpace);
|
|
78
|
+
}
|
|
79
|
+
// ── Gate vertical layout: single source of truth ──────────────────────────────────────────────
|
|
80
|
+
// A gate's body is sized and its ports laid out PURELY from its port count and a per-gate vertical
|
|
81
|
+
// port spacing `gap`: input port i sits at top + GATE_END_PAD + i*gap, so adjacent ports are `gap`
|
|
82
|
+
// apart with a fixed GATE_END_PAD above the first and below the last; the body height is the span
|
|
83
|
+
// plus both pads (rounded up to an even grid so the dead-centre output stays on-grid). With the
|
|
84
|
+
// default gap = PORT_SPACING this reproduces the historical (n+1)*PORT_SPACING layout exactly; a
|
|
85
|
+
// larger gap (for a gate fed by labelled inputs) widens the port spacing without other passes
|
|
86
|
+
// needing to know. Every pass that sizes or re-places a gate body derives geometry from these.
|
|
87
|
+
const GATE_END_PAD = PORT_SPACING;
|
|
88
|
+
function gateBodyHeight(numInputs, gap = PORT_SPACING, labelSpace = 0) {
|
|
89
|
+
return evenGridHeight(Math.max(AND_GATE_H_BASE, (numInputs - 1) * gap + 2 * GATE_END_PAD) + labelSpace);
|
|
90
|
+
}
|
|
91
|
+
function gateInputPortY(top, i, gap = PORT_SPACING) {
|
|
92
|
+
return top + GATE_END_PAD + i * gap;
|
|
93
|
+
}
|
|
94
|
+
// A gate's first-class vertical port spacing (`portGap`), defaulting to PORT_SPACING.
|
|
95
|
+
function gateGap(n) { return n.portGap ?? PORT_SPACING; }
|
|
96
|
+
// Body dimensions for a generic FB block: square-ish, sized to its port counts and labels.
|
|
97
|
+
function fbDims(n) {
|
|
98
|
+
const ni = n.inputIds.length;
|
|
99
|
+
const no = Math.max(1, n.usedPorts?.size ?? 1);
|
|
100
|
+
// Outputs sit at the output-stack gap (40) so the output nodes they feed line up straight;
|
|
101
|
+
// inputs need only MIN_PORT_GAP. Height fits whichever side has more ports.
|
|
102
|
+
const h = Math.max(50, Math.max((no - 1) * 40, (ni - 1) * MIN_PORT_GAP) + 50);
|
|
103
|
+
const textW = (s) => (s ? s.length * 6.5 : 0);
|
|
104
|
+
const inMax = Math.max(0, ...(n.inputLabels ?? []).map(textW));
|
|
105
|
+
const outMax = Math.max(0, ...[...(n.usedPorts ?? [])].map(p => textW(p === 'OUT' ? undefined : p)));
|
|
106
|
+
// Room for the left labels, the centred name and the right labels without overlap.
|
|
107
|
+
const w = Math.max(70, inMax + outMax + textW(n.name) + 30);
|
|
108
|
+
return { w: Math.ceil(w / GRID) * GRID, h: Math.ceil(h / GRID) * GRID };
|
|
109
|
+
}
|
|
110
|
+
// Body dimensions for each SEL function block.
|
|
111
|
+
function blockSize(blockType) {
|
|
112
|
+
switch (blockType) {
|
|
113
|
+
case 'TIMER': return { w: 85, h: 50 };
|
|
114
|
+
case 'SR': return { w: 60, h: 55 };
|
|
115
|
+
case 'COMPARE': return { w: 65, h: 50 };
|
|
116
|
+
case 'RISING':
|
|
117
|
+
case 'FALLING': return { w: 50, h: 40 };
|
|
118
|
+
default: return { w: 60, h: 45 };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Priority-method vertical coordinate assignment (Sugiyama/Tagawa style). Each column keeps
|
|
122
|
+
// its fixed order (the barycentre ordering); we then alternate downward (align to sources)
|
|
123
|
+
// and upward (align to consumers) sweeps. In each sweep a node is moved toward the median of
|
|
124
|
+
// its neighbours on that side, but it may not displace a neighbour of higher-or-equal
|
|
125
|
+
// priority (priority = degree on the sweep side); lower-priority neighbours are pushed to keep
|
|
126
|
+
// the minimum gap. Returns each node's top-left Y (grid-snapped, normalised to PAD_Y).
|
|
127
|
+
function assignCoordinates(nodes, depthGroups, rowMap, maxDepth, rowSpacing, laneTight = false) {
|
|
128
|
+
// Feedback edges (input is an output node looped back) are excluded from placement: the
|
|
129
|
+
// back-edge would otherwise drag a gate toward the far-right output it feeds.
|
|
130
|
+
const isFeedback = (inputId) => nodes.get(inputId)?.kind === 'output';
|
|
131
|
+
const successors = new Map();
|
|
132
|
+
for (const n of nodes.values()) {
|
|
133
|
+
for (const id of n.inputIds) {
|
|
134
|
+
if (isFeedback(id))
|
|
135
|
+
continue;
|
|
136
|
+
const a = successors.get(id) ?? [];
|
|
137
|
+
a.push(n.id);
|
|
138
|
+
successors.set(id, a);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const columns = [];
|
|
142
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
143
|
+
columns[d] = (depthGroups.get(d) ?? []).slice()
|
|
144
|
+
.sort((a, b) => (rowMap.get(a.id) ?? 0) - (rowMap.get(b.id) ?? 0));
|
|
145
|
+
}
|
|
146
|
+
const H = new Map();
|
|
147
|
+
for (const n of nodes.values())
|
|
148
|
+
H.set(n.id, baseNodeHeight(n));
|
|
149
|
+
const VGAP = Math.max(MIN_PORT_GAP, Math.round(rowSpacing / GRID) * GRID);
|
|
150
|
+
// Minimum centre-to-centre gap between two adjacent inputs in the input column, based on what
|
|
151
|
+
// their actual rendered labels need plus a margin — keeps the input column tight (matching the
|
|
152
|
+
// gate's own height) rather than packing at rowSpacing, which left large white gaps for high
|
|
153
|
+
// fan-in (e.g. 20-input AND had a 1920px stack for a 320px gate). The MIN_PORT_GAP floor
|
|
154
|
+
// preserves the dot diameter; the description line bumps the gap just enough not to overlap
|
|
155
|
+
// an adjacent input's description. Gates keep the rowSpacing-based sep() so routing channels
|
|
156
|
+
// between gate columns stay generous.
|
|
157
|
+
const minInputGap = (a, b) => {
|
|
158
|
+
// The gate's port-expansion pass enforces MIN_PORT_GAP (25px) between successive ports.
|
|
159
|
+
// For an input to leave the gate-port Ys at their natural (PORT_SPACING=15) positions, the
|
|
160
|
+
// inputs must be at LEAST MIN_PORT_GAP apart — otherwise the expansion pass widens the gate
|
|
161
|
+
// body to match the input column, ballooning it. Add label-line space on top of MIN_PORT_GAP
|
|
162
|
+
// when name/description is present so e.g. a stack of labelled inputs has room for them.
|
|
163
|
+
// A 2×MIN_PORT_GAP floor leaves headroom for multi-consumer inputs (an input feeding two
|
|
164
|
+
// different multi-input gates spans more port-Ys than a single-consumer), keeping the gate
|
|
165
|
+
// expansion pass within budget and avoiding the dogleg-killer's all-or-nothing failure.
|
|
166
|
+
const lineA = (a.name ? 14 : 0) + (a.description ? 10 : 0);
|
|
167
|
+
const lineB = (b.name ? 14 : 0) + (b.description ? 10 : 0);
|
|
168
|
+
return Math.round((MIN_PORT_GAP + 10 + Math.max(lineA, lineB)) / GRID) * GRID;
|
|
169
|
+
};
|
|
170
|
+
// Two adjacent long-edge dummies are parallel WIRE LANES, not gates. Stacking them at the full
|
|
171
|
+
// gate-sized routing channel (VGAP) inflates a column and strands a rank-extreme lane far from its
|
|
172
|
+
// natural line (the "input floated to the top" void). Under `laneTight`, adjacent lanes pack at
|
|
173
|
+
// MIN_DOGLEG instead (a wire leaving one lane into a nearby port still turns with a legal dogleg).
|
|
174
|
+
// This is offered as a candidate layout (see layoutDiagram) and kept only where it measurably
|
|
175
|
+
// improves the geometry, so it can never regress a diagram that relies on the generous spacing. A
|
|
176
|
+
// dummy adjacent to a real GATE always keeps the full channel, so gates never shift toward a lane.
|
|
177
|
+
const LANE_GAP = MIN_DOGLEG;
|
|
178
|
+
const sep = (a, b) => {
|
|
179
|
+
if (a.kind === 'input' && b.kind === 'input') {
|
|
180
|
+
// If both inputs feed a common multi-input gate, space them at that gate's port gap so they
|
|
181
|
+
// land directly on adjacent ports — a straight fan-in with no gate growth. (Clamp to at least
|
|
182
|
+
// half their combined height so labels never overlap.)
|
|
183
|
+
const sa = new Set(successors.get(a.id) ?? []);
|
|
184
|
+
const sharedGap = (successors.get(b.id) ?? [])
|
|
185
|
+
.map(id => (sa.has(id) ? nodes.get(id)?.portGap : undefined))
|
|
186
|
+
.filter((g) => g !== undefined);
|
|
187
|
+
if (sharedGap.length)
|
|
188
|
+
return Math.max(Math.max(...sharedGap), (H.get(a.id) + H.get(b.id)) / 2);
|
|
189
|
+
return Math.min((H.get(a.id) + H.get(b.id)) / 2 + VGAP, minInputGap(a, b));
|
|
190
|
+
}
|
|
191
|
+
if (laneTight && (a.gateType === 'DUMMY' || b.gateType === 'DUMMY'))
|
|
192
|
+
return (H.get(a.id) + H.get(b.id)) / 2 + LANE_GAP; // lane↔lane / lane↔gate: pack to a dogleg
|
|
193
|
+
return (H.get(a.id) + H.get(b.id)) / 2 + VGAP; // gate↔gate (or loose): full channel
|
|
194
|
+
};
|
|
195
|
+
const centre = new Map();
|
|
196
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
197
|
+
const col = columns[d];
|
|
198
|
+
let y = 0;
|
|
199
|
+
for (let i = 0; i < col.length; i++) {
|
|
200
|
+
if (i > 0)
|
|
201
|
+
y += sep(col[i - 1], col[i]);
|
|
202
|
+
centre.set(col[i].id, y);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Weighted isotonic regression (Pool Adjacent Violators) — the exact L2 optimum for
|
|
206
|
+
// min Σ wᵢ(xᵢ − tᵢ)² subject to x non-decreasing. Applied per column after removing the
|
|
207
|
+
// separation gaps (substitute zᵢ = centreᵢ − Σ_{k<i} sepₖ, turning "centre_{i+1} − centreᵢ ≥ sep"
|
|
208
|
+
// into "z monotone"), so a whole column is placed at its JOINT optimum in one shot — no greedy
|
|
209
|
+
// node-at-a-time blocking that leaves a column stuck in a locally-fixed arrangement.
|
|
210
|
+
const pava = (t, w) => {
|
|
211
|
+
const val = [], wt = [], cnt = [];
|
|
212
|
+
for (let i = 0; i < t.length; i++) {
|
|
213
|
+
let v = t[i], ww = w[i], c = 1;
|
|
214
|
+
while (val.length && val[val.length - 1] > v) {
|
|
215
|
+
const pv = val.pop(), pw = wt.pop(), pc = cnt.pop();
|
|
216
|
+
v = (v * ww + pv * pw) / (ww + pw);
|
|
217
|
+
ww += pw;
|
|
218
|
+
c += pc;
|
|
219
|
+
}
|
|
220
|
+
val.push(v);
|
|
221
|
+
wt.push(ww);
|
|
222
|
+
cnt.push(c);
|
|
223
|
+
}
|
|
224
|
+
const out = [];
|
|
225
|
+
for (let b = 0; b < val.length; b++)
|
|
226
|
+
for (let k = 0; k < cnt[b]; k++)
|
|
227
|
+
out.push(val[b]);
|
|
228
|
+
return out;
|
|
229
|
+
};
|
|
230
|
+
// Absolute centre-space Y of the port that `sourceId` feeds in consumer `c`. AND/OR assign ports
|
|
231
|
+
// to sources in ascending Y; a fixed-port block / NOT / output uses its centre.
|
|
232
|
+
const portYForSource = (c, sourceId, cCentre) => {
|
|
233
|
+
if ((c.gateType === 'AND' || c.gateType === 'OR') && c.inputIds.length >= 2) {
|
|
234
|
+
const gap = gateGap(c);
|
|
235
|
+
const rank = c.inputIds.filter(id => !isFeedback(id))
|
|
236
|
+
.map(id => ({ id, y: centre.get(id) ?? cCentre }))
|
|
237
|
+
.sort((a, b) => a.y - b.y)
|
|
238
|
+
.findIndex(r => r.id === sourceId);
|
|
239
|
+
if (rank >= 0)
|
|
240
|
+
return cCentre - H.get(c.id) / 2 + GATE_END_PAD + rank * gap;
|
|
241
|
+
}
|
|
242
|
+
return cCentre;
|
|
243
|
+
};
|
|
244
|
+
// A node's desired centre = mean over ALL its edges of the centre that draws that edge straight:
|
|
245
|
+
// a source edge wants port_i aligned to source_i; a consumer edge wants the node's output aligned
|
|
246
|
+
// to the consumer's port. Two sources feeding ADJACENT ports of one gate therefore pull to
|
|
247
|
+
// positions ≥ the port gap apart — fan-in spreads to the port spacing rather than crowding (which
|
|
248
|
+
// is what previously forced the cramped-port / sub-MIN-dogleg compromise).
|
|
249
|
+
const nodeTarget = (n) => {
|
|
250
|
+
let sum = 0, wsum = 0;
|
|
251
|
+
const h = H.get(n.id), gap = gateGap(n);
|
|
252
|
+
const srcYs = n.inputIds.filter(id => !isFeedback(id))
|
|
253
|
+
.map(id => ({ id, y: centre.get(id) }))
|
|
254
|
+
.filter((s) => s.y !== undefined);
|
|
255
|
+
if ((n.gateType === 'AND' || n.gateType === 'OR') && srcYs.length >= 2) {
|
|
256
|
+
srcYs.sort((a, b) => a.y - b.y).forEach((s, rank) => { sum += s.y - (GATE_END_PAD + rank * gap) + h / 2; wsum++; });
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
for (const s of srcYs) {
|
|
260
|
+
sum += s.y;
|
|
261
|
+
wsum++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const cid of successors.get(n.id) ?? []) {
|
|
265
|
+
const c = nodes.get(cid), cy = centre.get(cid);
|
|
266
|
+
if (c && cy !== undefined) {
|
|
267
|
+
sum += portYForSource(c, n.id, cy);
|
|
268
|
+
wsum++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return wsum === 0 ? null : sum / wsum;
|
|
272
|
+
};
|
|
273
|
+
// Place a whole column at its joint optimum: target per node, then PAVA in gap-removed space.
|
|
274
|
+
// Node weight = its degree, so a well-connected node holds its target more firmly.
|
|
275
|
+
const solveColumn = (col) => {
|
|
276
|
+
const n = col.length;
|
|
277
|
+
if (n === 0)
|
|
278
|
+
return;
|
|
279
|
+
const G = [0];
|
|
280
|
+
for (let i = 1; i < n; i++)
|
|
281
|
+
G[i] = G[i - 1] + sep(col[i - 1], col[i]);
|
|
282
|
+
const t = [], w = [];
|
|
283
|
+
for (let i = 0; i < n; i++) {
|
|
284
|
+
t.push((nodeTarget(col[i]) ?? centre.get(col[i].id)) - G[i]);
|
|
285
|
+
w.push(Math.max(1, col[i].inputIds.filter(id => !isFeedback(id)).length + (successors.get(col[i].id) ?? []).length));
|
|
286
|
+
}
|
|
287
|
+
const z = pava(t, w);
|
|
288
|
+
for (let i = 0; i < n; i++)
|
|
289
|
+
centre.set(col[i].id, z[i] + G[i]);
|
|
290
|
+
};
|
|
291
|
+
// Iterate to convergence, alternating direction so both source and consumer influence propagate.
|
|
292
|
+
// Each column solve is the exact constrained optimum given its neighbours' current positions.
|
|
293
|
+
for (let it = 0; it < 12; it++) {
|
|
294
|
+
if (it % 2 === 0)
|
|
295
|
+
for (let d = 0; d <= maxDepth; d++)
|
|
296
|
+
solveColumn(columns[d]);
|
|
297
|
+
else
|
|
298
|
+
for (let d = maxDepth; d >= 0; d--)
|
|
299
|
+
solveColumn(columns[d]);
|
|
300
|
+
}
|
|
301
|
+
let minTop = Infinity;
|
|
302
|
+
for (const n of nodes.values())
|
|
303
|
+
minTop = Math.min(minTop, centre.get(n.id) - H.get(n.id) / 2);
|
|
304
|
+
const result = new Map();
|
|
305
|
+
for (const n of nodes.values()) {
|
|
306
|
+
const top = centre.get(n.id) - H.get(n.id) / 2 - minTop + PAD_Y;
|
|
307
|
+
result.set(n.id, Math.round(top / GRID) * GRID);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
// Public entry: lay the diagram out with the input ORDERING that renders the fewest crossings. Each
|
|
312
|
+
// candidate strategy is a full, independent layout (layoutOnce rebuilds from scratch), and we keep
|
|
313
|
+
// whichever measures fewer crossings on the REAL geometry. 'heuristic' is always a candidate, so we
|
|
314
|
+
// can never render worse than before; 'crossmin' is tried only when 'heuristic' isn't already clean
|
|
315
|
+
// (the common case), keeping cost ~1x. Smarter candidates can be added later — each only ever helps.
|
|
316
|
+
export function layoutDiagram(diagram, portMeta = [], options) {
|
|
317
|
+
// Candidates span two independent dimensions: ordering ('heuristic' | 'crossmin') and long-edge
|
|
318
|
+
// lane packing (loose | tight — see assignCoordinates `laneTight`). Each is a full independent
|
|
319
|
+
// layout; we keep the best on MEASURED geometry, ranked lexicographically by: fewest sub-min
|
|
320
|
+
// doglegs, then fewest crossings, then fewest bends, then shortest. The loose-heuristic layout is
|
|
321
|
+
// always a candidate and is always dogleg-free, so the winner can never have more doglegs or
|
|
322
|
+
// crossings than today — tight only wins when it genuinely removes a crossing or collapses a void
|
|
323
|
+
// (shorter height) without cost. Ties keep the earlier (loose) candidate, so diagrams without long
|
|
324
|
+
// edges stay byte-identical.
|
|
325
|
+
const subMinDoglegs = (l) => {
|
|
326
|
+
let c = 0;
|
|
327
|
+
for (const w of l.wires) {
|
|
328
|
+
if (w.feedback)
|
|
329
|
+
continue;
|
|
330
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
331
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
332
|
+
if (Math.abs(a.x - b.x) < 0.5) {
|
|
333
|
+
const len = Math.abs(a.y - b.y);
|
|
334
|
+
if (len >= 0.5 && len < MIN_DOGLEG - 0.01)
|
|
335
|
+
c++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return c;
|
|
340
|
+
};
|
|
341
|
+
const cr = (l) => findWireCrossings(l.wires, l.junctions).length;
|
|
342
|
+
const bends = (l) => l.wires.reduce((s, w) => s + Math.max(0, w.points.length - 2), 0);
|
|
343
|
+
// Cross-net COINCIDENT parallel overlap: two different nets' same-orientation segments sharing an
|
|
344
|
+
// axis line (co-linear) and overlapping along it — i.e. drawn on top of each other. This is the
|
|
345
|
+
// wire-separation contract's worst failure (worse than a crossing): the routing passes refuse to
|
|
346
|
+
// trade it for a crossing, so when a placement/ordering leaves two nets genuinely contending for
|
|
347
|
+
// one channel an overlap can survive a single layout. Scoring it as the TOP-priority term makes
|
|
348
|
+
// the candidate selection reject any ordering/strategy that overlaps in favour of one that doesn't
|
|
349
|
+
// — so no single pass's all-or-nothing fallback can leak an overlap into the chosen layout when a
|
|
350
|
+
// clean candidate exists. Backed by the invariants.spec "no cross-net overlapping parallel" test.
|
|
351
|
+
const overlaps = (l) => {
|
|
352
|
+
const V = [], H = [];
|
|
353
|
+
for (const w of l.wires) {
|
|
354
|
+
if (w.feedback)
|
|
355
|
+
continue;
|
|
356
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
357
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
358
|
+
if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) >= 0.5)
|
|
359
|
+
V.push({ p: a.x, a0: Math.min(a.y, b.y), a1: Math.max(a.y, b.y), from: w.fromId });
|
|
360
|
+
else if (Math.abs(a.y - b.y) < 0.5 && Math.abs(a.x - b.x) >= 0.5)
|
|
361
|
+
H.push({ p: a.y, a0: Math.min(a.x, b.x), a1: Math.max(a.x, b.x), from: w.fromId });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
let c = 0;
|
|
365
|
+
for (const set of [V, H])
|
|
366
|
+
for (let i = 0; i < set.length; i++)
|
|
367
|
+
for (let j = i + 1; j < set.length; j++) {
|
|
368
|
+
const a = set[i], b = set[j];
|
|
369
|
+
if (a.from === b.from || Math.abs(a.p - b.p) >= 0.5)
|
|
370
|
+
continue; // different net, same axis line
|
|
371
|
+
if (Math.min(a.a1, b.a1) - Math.max(a.a0, b.a0) >= GRID)
|
|
372
|
+
c++; // co-linear overlap (matches the invariant)
|
|
373
|
+
}
|
|
374
|
+
return c;
|
|
375
|
+
};
|
|
376
|
+
// Cross-net gate/block BODY intrusion: a wire segment (not entering that node) running through or
|
|
377
|
+
// along a non-endpoint gate/block body. Uses the FULL body rect (no inset), so it also catches a
|
|
378
|
+
// wire grazing the body's edge — a straight pass-through drawn along a block's border reads as part
|
|
379
|
+
// of the block outline. A* can pick such a graze when it is the fewest-crossing route (the body
|
|
380
|
+
// graze is not itself a crossing), so scoring it lets the candidate selection prefer a layout that
|
|
381
|
+
// routes clear of the body when one exists. Ranked just below overlaps (both are "a wire drawn on
|
|
382
|
+
// top of something it must stay clear of").
|
|
383
|
+
const bodyIntrusions = (l) => {
|
|
384
|
+
const gates = l.nodes.filter(n => n.gateType !== 'INPUT' && n.gateType !== 'OUTPUT');
|
|
385
|
+
let c = 0;
|
|
386
|
+
for (const w of l.wires) {
|
|
387
|
+
if (w.feedback)
|
|
388
|
+
continue;
|
|
389
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
390
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
391
|
+
const xm = Math.min(a.x, b.x), xM = Math.max(a.x, b.x), ym = Math.min(a.y, b.y), yM = Math.max(a.y, b.y);
|
|
392
|
+
for (const g of gates) {
|
|
393
|
+
if (g.id === w.fromId || g.id === w.toId)
|
|
394
|
+
continue; // legitimately enters this node
|
|
395
|
+
// Real overlap with the body rect, edges included (so an on-edge graze counts).
|
|
396
|
+
if (xM > g.absX + 0.5 && g.absX + g.width > xm + 0.5 && yM > g.absY - 0.5 && g.absY + g.height > ym - 0.5)
|
|
397
|
+
c++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return c;
|
|
402
|
+
};
|
|
403
|
+
const score = (l) => [overlaps(l), bodyIntrusions(l), subMinDoglegs(l), cr(l), bends(l), l.height];
|
|
404
|
+
const better = (l, b) => {
|
|
405
|
+
const sl = score(l), sb = score(b);
|
|
406
|
+
for (let i = 0; i < sl.length; i++)
|
|
407
|
+
if (sl[i] !== sb[i])
|
|
408
|
+
return sl[i] < sb[i];
|
|
409
|
+
return false; // tie → keep the earlier candidate
|
|
410
|
+
};
|
|
411
|
+
let best = layoutOnce(diagram, portMeta, options, 'heuristic', false);
|
|
412
|
+
const consider = (l) => { if (better(l, best))
|
|
413
|
+
best = l; };
|
|
414
|
+
consider(layoutOnce(diagram, portMeta, options, 'heuristic', true)); // lane-tight variant (collapses voids)
|
|
415
|
+
// Reach for the crossmin candidates when the current best is not already clean — it still has
|
|
416
|
+
// crossings, or (rarer) carries a cross-net overlap or a gate-body intrusion a different ordering
|
|
417
|
+
// may avoid.
|
|
418
|
+
if (cr(best) > 0 || overlaps(best) > 0 || bodyIntrusions(best) > 0) {
|
|
419
|
+
consider(layoutOnce(diagram, portMeta, options, 'crossmin', false));
|
|
420
|
+
consider(layoutOnce(diagram, portMeta, options, 'crossmin', true));
|
|
421
|
+
}
|
|
422
|
+
symmetriseSmallGates(best); // cosmetic, validated post-pass
|
|
423
|
+
return best;
|
|
424
|
+
}
|
|
425
|
+
// Cosmetic symmetry for small gates: for a ≤3-input gate with exactly one dogleg fan-in above the
|
|
426
|
+
// middle and one below (each a clean H–V–H), align their turn channels to a single X so the two
|
|
427
|
+
// doglegs mirror — a symmetric funnel, which reads much cleaner. Runs on the FINAL chosen layout, so
|
|
428
|
+
// it cannot flip the candidate choice; and every move is fully validated — kept only if it neither
|
|
429
|
+
// adds a crossing nor brings the moved wire within MIN_WIRE_SPACING of another net (else reverted),
|
|
430
|
+
// so it can never introduce a new problem.
|
|
431
|
+
function symmetriseSmallGates(l) {
|
|
432
|
+
const crossings = () => findWireCrossings(l.wires, l.junctions).length;
|
|
433
|
+
const hvh = (w) => w.points.length === 4 && Math.abs(w.points[1].x - w.points[2].x) < 0.5
|
|
434
|
+
&& Math.abs(w.points[0].y - w.points[1].y) < 0.5 && Math.abs(w.points[2].y - w.points[3].y) < 0.5;
|
|
435
|
+
const crowds = (self) => {
|
|
436
|
+
for (let s = 0; s < self.points.length - 1; s++) {
|
|
437
|
+
const a = self.points[s], b = self.points[s + 1];
|
|
438
|
+
const horiz = Math.abs(a.y - b.y) < 0.5;
|
|
439
|
+
const perp = horiz ? a.y : a.x, mn = horiz ? Math.min(a.x, b.x) : Math.min(a.y, b.y), mx = horiz ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
440
|
+
for (const o of l.wires) {
|
|
441
|
+
if (o === self || o.fromId === self.fromId)
|
|
442
|
+
continue;
|
|
443
|
+
for (let k = 0; k < o.points.length - 1; k++) {
|
|
444
|
+
const c = o.points[k], d = o.points[k + 1];
|
|
445
|
+
const oh = Math.abs(c.y - d.y) < 0.5;
|
|
446
|
+
if (oh !== horiz || (!oh && Math.abs(c.x - d.x) >= 0.5) || (oh && Math.abs(c.x - d.x) < 0.5))
|
|
447
|
+
continue;
|
|
448
|
+
const dp = Math.abs((oh ? c.y : c.x) - perp);
|
|
449
|
+
if (dp >= MIN_WIRE_SPACING - 0.5)
|
|
450
|
+
continue; // clear (dp≈0 exact-overlap counts as crowd)
|
|
451
|
+
const omn = oh ? Math.min(c.x, d.x) : Math.min(c.y, d.y), omx = oh ? Math.max(c.x, d.x) : Math.max(c.y, d.y);
|
|
452
|
+
if (Math.min(mx, omx) - Math.max(mn, omn) > 0.5)
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
};
|
|
459
|
+
for (const g of l.nodes) {
|
|
460
|
+
if (g.gateType === 'INPUT' || g.gateType === 'OUTPUT' || g.inputs.length < 2 || g.inputs.length > 3)
|
|
461
|
+
continue;
|
|
462
|
+
const cy = g.absY + g.height / 2;
|
|
463
|
+
const fan = l.wires.filter(w => w.toId === g.id && !w.feedback && hvh(w));
|
|
464
|
+
const above = fan.filter(w => w.points[3].y < cy - 0.5), below = fan.filter(w => w.points[3].y > cy + 0.5);
|
|
465
|
+
if (above.length !== 1 || below.length !== 1)
|
|
466
|
+
continue;
|
|
467
|
+
const aw = above[0], bw = below[0], ax = aw.points[1].x, bx = bw.points[1].x;
|
|
468
|
+
if (Math.abs(ax - bx) < 0.5)
|
|
469
|
+
continue; // already symmetric
|
|
470
|
+
const targetX = Math.max(ax, bx); // align to the channel nearer the gate
|
|
471
|
+
const mover = ax === targetX ? bw : aw;
|
|
472
|
+
const p = mover.points, saved = p.slice();
|
|
473
|
+
if (targetX < p[0].x + 0.5)
|
|
474
|
+
continue; // would backtrack
|
|
475
|
+
if (l.junctions.some(j => (Math.abs(j.x - p[1].x) < 0.5 && Math.abs(j.y - p[1].y) < 0.5)
|
|
476
|
+
|| (Math.abs(j.x - p[2].x) < 0.5 && Math.abs(j.y - p[2].y) < 0.5)))
|
|
477
|
+
continue; // turn carries a fan-out dot
|
|
478
|
+
const before = crossings();
|
|
479
|
+
mover.points = [p[0], { x: targetX, y: p[0].y }, { x: targetX, y: p[3].y }, p[3]];
|
|
480
|
+
if (crossings() > before || crowds(mover))
|
|
481
|
+
mover.points = saved; // revert unless strictly clean
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Candidate input ordering that MINIMISES crossings (Sugiyama step 2), built on its OWN local
|
|
485
|
+
// layered graph (real nodes + local dummy chains for long edges, so every edge joins adjacent layers
|
|
486
|
+
// and long-edge crossings are countable). Returns a per-layer rank for the REAL nodes only — the
|
|
487
|
+
// caller's existing dummy pass then reserves lanes as usual. Crossings are counted with PORT
|
|
488
|
+
// AWARENESS: an edge into a FIXED-PORT block (SR/timer/…) carries the block's declared port index, so
|
|
489
|
+
// a reversed source order counts as a crossing (this is what keeps SR S/R uncrossed); AND/OR ports
|
|
490
|
+
// follow source order (no internal cross). This is only ever a CANDIDATE — the caller keeps it only
|
|
491
|
+
// if it renders fewer crossings than the heuristic — so the combinatorial count need not be perfect.
|
|
492
|
+
function crossminOrder(nodes, maxDepth, opts) {
|
|
493
|
+
const isFeedback = (id) => nodes.get(id)?.kind === 'output';
|
|
494
|
+
const fixedPort = (id) => { const n = id ? nodes.get(id) : undefined; return !!n?.gateType && !['AND', 'OR', 'NOT', 'DUMMY', 'INPUT', 'OUTPUT'].includes(n.gateType); };
|
|
495
|
+
const layer = Array.from({ length: maxDepth + 1 }, () => []);
|
|
496
|
+
const depthOf = new Map();
|
|
497
|
+
const real = new Set();
|
|
498
|
+
for (const n of nodes.values()) {
|
|
499
|
+
layer[n.depth].push(n.id);
|
|
500
|
+
depthOf.set(n.id, n.depth);
|
|
501
|
+
real.add(n.id);
|
|
502
|
+
}
|
|
503
|
+
const edges = [];
|
|
504
|
+
let dc = 0;
|
|
505
|
+
for (const v of nodes.values()) {
|
|
506
|
+
v.inputIds.forEach((u, k) => {
|
|
507
|
+
if (isFeedback(u))
|
|
508
|
+
return;
|
|
509
|
+
const du = depthOf.get(u);
|
|
510
|
+
if (du === undefined)
|
|
511
|
+
return;
|
|
512
|
+
if (v.depth - du <= 1) {
|
|
513
|
+
edges.push({ u, v: v.id, k, vReal: v.id });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
let prev = u; // long edge → local dummy chain
|
|
517
|
+
for (let d = du + 1; d < v.depth; d++) {
|
|
518
|
+
const did = `cmD${dc++}`;
|
|
519
|
+
layer[d].push(did);
|
|
520
|
+
depthOf.set(did, d);
|
|
521
|
+
edges.push({ u: prev, v: did, k: 0, vReal: null });
|
|
522
|
+
prev = did;
|
|
523
|
+
}
|
|
524
|
+
edges.push({ u: prev, v: v.id, k, vReal: v.id });
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const edgesFrom = Array.from({ length: maxDepth + 1 }, () => []);
|
|
528
|
+
const inAdj = new Map(), outAdj = new Map();
|
|
529
|
+
for (const e of edges) {
|
|
530
|
+
edgesFrom[depthOf.get(e.u)].push(e);
|
|
531
|
+
(inAdj.get(e.v) ?? inAdj.set(e.v, []).get(e.v)).push(e.u);
|
|
532
|
+
(outAdj.get(e.u) ?? outAdj.set(e.u, []).get(e.u)).push(e.v);
|
|
533
|
+
}
|
|
534
|
+
const pos = new Map();
|
|
535
|
+
const reindex = () => { for (const l of layer)
|
|
536
|
+
l.forEach((id, i) => pos.set(id, i)); };
|
|
537
|
+
const portOff = (e) => {
|
|
538
|
+
if (!e.vReal || !fixedPort(e.vReal))
|
|
539
|
+
return 0;
|
|
540
|
+
const c = nodes.get(e.vReal);
|
|
541
|
+
if (c.inputIds.length < 2)
|
|
542
|
+
return 0;
|
|
543
|
+
return (e.k - (c.inputIds.length - 1) / 2) / c.inputIds.length;
|
|
544
|
+
};
|
|
545
|
+
const countLayer = (d) => {
|
|
546
|
+
const es = edgesFrom[d];
|
|
547
|
+
let c = 0;
|
|
548
|
+
for (let i = 0; i < es.length; i++)
|
|
549
|
+
for (let j = i + 1; j < es.length; j++) {
|
|
550
|
+
const du = pos.get(es[i].u) - pos.get(es[j].u);
|
|
551
|
+
const dv = (pos.get(es[i].v) + portOff(es[i])) - (pos.get(es[j].v) + portOff(es[j]));
|
|
552
|
+
if (du * dv < -1e-9)
|
|
553
|
+
c++;
|
|
554
|
+
}
|
|
555
|
+
return c;
|
|
556
|
+
};
|
|
557
|
+
const countAll = () => { let c = 0; for (let d = 0; d < maxDepth; d++)
|
|
558
|
+
c += countLayer(d); return c; };
|
|
559
|
+
const median = (xs) => { xs.sort((a, b) => a - b); const m = xs.length >> 1; return xs.length % 2 ? xs[m] : (xs[m - 1] + xs[m]) / 2; };
|
|
560
|
+
const mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
561
|
+
const sweep = (d, adj, useMean) => {
|
|
562
|
+
const key = new Map();
|
|
563
|
+
for (const id of layer[d]) {
|
|
564
|
+
const ps = (adj.get(id) ?? []).map(x => pos.get(x));
|
|
565
|
+
key.set(id, ps.length ? (useMean ? mean(ps) : median(ps)) : pos.get(id));
|
|
566
|
+
}
|
|
567
|
+
layer[d] = layer[d].map((id, i) => ({ id, i })).sort((a, b) => (key.get(a.id) - key.get(b.id)) || (a.i - b.i)).map(x => x.id);
|
|
568
|
+
reindex();
|
|
569
|
+
};
|
|
570
|
+
const transpose = () => {
|
|
571
|
+
for (let g = 0; g < 6; g++) {
|
|
572
|
+
let improved = false;
|
|
573
|
+
for (let d = 0; d <= maxDepth; d++)
|
|
574
|
+
for (let i = 0; i + 1 < layer[d].length; i++) {
|
|
575
|
+
const local = () => (d > 0 ? countLayer(d - 1) : 0) + (d < maxDepth ? countLayer(d) : 0);
|
|
576
|
+
const before = local();
|
|
577
|
+
[layer[d][i], layer[d][i + 1]] = [layer[d][i + 1], layer[d][i]];
|
|
578
|
+
reindex();
|
|
579
|
+
if (local() < before)
|
|
580
|
+
improved = true;
|
|
581
|
+
else {
|
|
582
|
+
[layer[d][i], layer[d][i + 1]] = [layer[d][i + 1], layer[d][i]];
|
|
583
|
+
reindex();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (!improved)
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
layer[0].sort((a, b) => naturalCompare(nodes.get(a)?.label ?? a, nodes.get(b)?.label ?? b));
|
|
591
|
+
reindex();
|
|
592
|
+
for (let d = 1; d <= maxDepth; d++)
|
|
593
|
+
sweep(d, inAdj, false);
|
|
594
|
+
let best = layer.map(l => l.slice()), bestCr = countAll();
|
|
595
|
+
const record = () => { const cr = countAll(); if (cr < bestCr) {
|
|
596
|
+
bestCr = cr;
|
|
597
|
+
best = layer.map(l => l.slice());
|
|
598
|
+
} };
|
|
599
|
+
const rounds = opts.inputOrder === 'DECLARATION' ? 0 : 24;
|
|
600
|
+
for (let r = 0; r < rounds && bestCr > 0; r++) {
|
|
601
|
+
const useMean = r % 2 === 1;
|
|
602
|
+
for (let d = 1; d <= maxDepth; d++)
|
|
603
|
+
sweep(d, inAdj, useMean);
|
|
604
|
+
transpose();
|
|
605
|
+
record();
|
|
606
|
+
for (let d = maxDepth - 1; d >= 0; d--)
|
|
607
|
+
sweep(d, outAdj, useMean);
|
|
608
|
+
transpose();
|
|
609
|
+
record();
|
|
610
|
+
}
|
|
611
|
+
// Fan-in contiguity: keep each gate's single-consumer inputs together (the combinatorial optimum
|
|
612
|
+
// can interleave independent chains — cheap combinatorially but crossing once drawn).
|
|
613
|
+
const soleC = (id) => { const cs = outAdj.get(id) ?? []; return cs.length === 1 ? cs[0] : id; };
|
|
614
|
+
const bpos = new Map();
|
|
615
|
+
best[0].forEach((id, i) => bpos.set(id, i));
|
|
616
|
+
const grp = new Map();
|
|
617
|
+
for (const id of best[0]) {
|
|
618
|
+
const k = soleC(id);
|
|
619
|
+
(grp.get(k) ?? grp.set(k, []).get(k)).push(id);
|
|
620
|
+
}
|
|
621
|
+
for (let d = 0; d <= maxDepth; d++)
|
|
622
|
+
layer[d] = best[d];
|
|
623
|
+
layer[0] = [...grp.values()].sort((a, b) => Math.min(...a.map(x => bpos.get(x))) - Math.min(...b.map(x => bpos.get(x))))
|
|
624
|
+
.map(g => g.sort((a, b) => bpos.get(a) - bpos.get(b))).flat();
|
|
625
|
+
reindex();
|
|
626
|
+
for (let d = 1; d <= maxDepth; d++)
|
|
627
|
+
sweep(d, inAdj, false);
|
|
628
|
+
transpose();
|
|
629
|
+
const rowMap = new Map();
|
|
630
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
631
|
+
let r = 0;
|
|
632
|
+
for (const id of layer[d])
|
|
633
|
+
if (real.has(id))
|
|
634
|
+
rowMap.set(id, r++);
|
|
635
|
+
}
|
|
636
|
+
return rowMap;
|
|
637
|
+
}
|
|
638
|
+
function layoutOnce(diagram, portMeta = [], options, strategy, laneTight = false) {
|
|
639
|
+
_id = 0;
|
|
640
|
+
const opts = options ?? DEFAULT_OPTIONS;
|
|
641
|
+
// Phase 1 — build the flattened logic graph (semantic model) from the parsed AST.
|
|
642
|
+
const { nodes, intermediateLabels } = buildGraph(diagram, portMeta, opts, uid);
|
|
643
|
+
// Per-gate first-class port spacing. A multi-input AND/OR fed by a labelled INPUT spaces its
|
|
644
|
+
// ports at a label-safe gap so those inputs can be placed directly on its ports (in
|
|
645
|
+
// assignCoordinates) without their labels colliding — the gate is then sized by port count, not
|
|
646
|
+
// grown to span far-apart sources. Gates fed only by other gates keep the tight PORT_SPACING.
|
|
647
|
+
const INPUT_PORT_GAP = 30;
|
|
648
|
+
for (const n of nodes.values()) {
|
|
649
|
+
if (n.kind !== 'gate' || !n.gateType || n.gateType === 'NOT' || n.inputIds.length < 2)
|
|
650
|
+
continue;
|
|
651
|
+
if (opts.gateInputStyle === 'BARS' && n.inputIds.length > 2)
|
|
652
|
+
continue; // BARS gates own their port layout
|
|
653
|
+
// Only widen ports for a labelled input that is ADJACENT (one column left), because only then
|
|
654
|
+
// is the input actually placed ON the port (a straight fan-in whose label needs the room). A
|
|
655
|
+
// labelled input feeding a deeper gate is columns away and doglegs in regardless, so widening
|
|
656
|
+
// there just bloats the gate — keep it at the tight PORT_SPACING.
|
|
657
|
+
const hasAdjacentInputSource = n.inputIds.some(id => {
|
|
658
|
+
const s = nodes.get(id);
|
|
659
|
+
return s?.kind === 'input' && s.depth === n.depth - 1;
|
|
660
|
+
});
|
|
661
|
+
if (hasAdjacentInputSource)
|
|
662
|
+
n.portGap = INPUT_PORT_GAP;
|
|
663
|
+
}
|
|
664
|
+
const rowMap = new Map();
|
|
665
|
+
const depthGroups = new Map();
|
|
666
|
+
for (const n of nodes.values()) {
|
|
667
|
+
if (!depthGroups.has(n.depth))
|
|
668
|
+
depthGroups.set(n.depth, []);
|
|
669
|
+
depthGroups.get(n.depth).push(n);
|
|
670
|
+
}
|
|
671
|
+
const inputGroup = depthGroups.get(0) ?? [];
|
|
672
|
+
inputGroup.sort((a, b) => naturalCompare(a.label ?? a.id, b.label ?? b.id));
|
|
673
|
+
for (let i = 0; i < inputGroup.length; i++) {
|
|
674
|
+
rowMap.set(inputGroup[i].id, i);
|
|
675
|
+
}
|
|
676
|
+
const maxDepth = Math.max(...Array.from(nodes.values()).map(n => n.depth), 0);
|
|
677
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
678
|
+
const group = depthGroups.get(depth) ?? [];
|
|
679
|
+
for (const node of group) {
|
|
680
|
+
if (node.inputIds.length === 0) {
|
|
681
|
+
rowMap.set(node.id, 0);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const inputRows = node.inputIds
|
|
685
|
+
.map(id => rowMap.get(id))
|
|
686
|
+
.filter((r) => r !== undefined);
|
|
687
|
+
if (inputRows.length === 0) {
|
|
688
|
+
rowMap.set(node.id, 0);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const minR = Math.min(...inputRows);
|
|
692
|
+
const maxR = Math.max(...inputRows);
|
|
693
|
+
rowMap.set(node.id, (minR + maxR) / 2);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Fixed-port blocks (SR, timers, comparators, edge-triggers, FB) bind their arguments to ports
|
|
697
|
+
// in a FIXED order — unlike AND/OR, which assign ports by ascending source Y and so never cross.
|
|
698
|
+
// Bias a source's barycentre row by the port index it feeds (top port → lower row) so the two
|
|
699
|
+
// sources land in port order and their wires don't cross entering the block. The bias (±<1 row)
|
|
700
|
+
// only breaks ties / near-ties between siblings; it never reorders across distinct gate rows.
|
|
701
|
+
const FIXED_PORT = (gt) => !!gt && !['AND', 'OR', 'NOT', 'DUMMY', 'INPUT', 'OUTPUT'].includes(gt);
|
|
702
|
+
const portBias = (consumer, inputId) => {
|
|
703
|
+
if (!FIXED_PORT(consumer.gateType) || consumer.inputIds.length < 2)
|
|
704
|
+
return 0;
|
|
705
|
+
const idx = consumer.inputIds.indexOf(inputId);
|
|
706
|
+
return idx < 0 ? 0 : (idx - (consumer.inputIds.length - 1) / 2) * 0.5;
|
|
707
|
+
};
|
|
708
|
+
// INPUT_ORDER = AUTO (default): reorder input rows by the Sugiyama barycentre method to
|
|
709
|
+
// minimise wire crossings. INPUT_ORDER = DECLARATION: keep inputs in their declared
|
|
710
|
+
// (natural-sorted) order and only propagate gate rows from that fixed input order.
|
|
711
|
+
const barycentreIterations = opts.inputOrder === 'AUTO' ? 3 : 0;
|
|
712
|
+
for (let iteration = 0; iteration < barycentreIterations; iteration++) {
|
|
713
|
+
const sortedInputGroup = [...inputGroup];
|
|
714
|
+
for (const node of sortedInputGroup) {
|
|
715
|
+
const downNodes = Array.from(nodes.values()).filter(n => n.inputIds.includes(node.id));
|
|
716
|
+
if (downNodes.length > 0) {
|
|
717
|
+
const bary = downNodes.reduce((s, n) => s + (rowMap.get(n.id) ?? 0) + portBias(n, node.id), 0) / downNodes.length;
|
|
718
|
+
rowMap.set(node.id, bary);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
sortedInputGroup.sort((a, b) => (rowMap.get(a.id) ?? 0) - (rowMap.get(b.id) ?? 0));
|
|
722
|
+
for (let i = 0; i < sortedInputGroup.length; i++) {
|
|
723
|
+
rowMap.set(sortedInputGroup[i].id, i);
|
|
724
|
+
}
|
|
725
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
726
|
+
const group = depthGroups.get(depth) ?? [];
|
|
727
|
+
for (const node of group) {
|
|
728
|
+
if (node.inputIds.length === 0) {
|
|
729
|
+
rowMap.set(node.id, 0);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const inputRows = node.inputIds
|
|
733
|
+
.map(id => rowMap.get(id))
|
|
734
|
+
.filter((r) => r !== undefined);
|
|
735
|
+
if (inputRows.length === 0) {
|
|
736
|
+
rowMap.set(node.id, 0);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const minR = Math.min(...inputRows);
|
|
740
|
+
const maxR = Math.max(...inputRows);
|
|
741
|
+
rowMap.set(node.id, (minR + maxR) / 2);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// 2-hop downstream median re-sort (INPUT_ORDER = AUTO only). After the barycentre pass each
|
|
746
|
+
// input's row is approximately at its IMMEDIATE consumer's row (1-hop), but a path through an
|
|
747
|
+
// intermediate gate like a NOT still crosses other horizontal corridors (e.g. HBLK -> not_7
|
|
748
|
+
// -> and_8 puts HBLK at not_7's row; if and_8 sits elsewhere the not_7->and_8 vertical
|
|
749
|
+
// crosses unrelated wires). Re-sorting the input column by the 2-hop median of consumer ranks
|
|
750
|
+
// (consumer's consumer) instead places HBLK at and_8's row, straightening the full path.
|
|
751
|
+
//
|
|
752
|
+
// IMPORTANT: this re-sorts the input column and re-assigns INTEGER ranks 0..n in the new
|
|
753
|
+
// order — `assignCoordinates` then space-packs them at uniform `sep()` spacing. So the input
|
|
754
|
+
// column's Y RANGE stays bounded to the original (height never balloons), unlike a direct
|
|
755
|
+
// placement at the raw 2-hop Y, which put START/EXT_ALARM at extreme Ys (4400+) because their
|
|
756
|
+
// 2-hop consumers were bottom outputs (the AUTO reordering placed them at the bottom). Only
|
|
757
|
+
// the SORT ORDER changes; uniform spacing holds; invariants stay satisfied.
|
|
758
|
+
if (opts.inputOrder === 'AUTO' && inputGroup.length > 4) {
|
|
759
|
+
// Successor map (node -> nodes it feeds into), excluding feedback edges (output sinks).
|
|
760
|
+
const isFeedback = (id) => nodes.get(id)?.kind === 'output';
|
|
761
|
+
const succ = new Map();
|
|
762
|
+
for (const n of nodes.values()) {
|
|
763
|
+
for (const id of n.inputIds) {
|
|
764
|
+
if (isFeedback(id))
|
|
765
|
+
continue;
|
|
766
|
+
const a = succ.get(id) ?? [];
|
|
767
|
+
a.push(n.id);
|
|
768
|
+
succ.set(id, a);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const med = (vals) => { const s = vals.slice().sort((a, b) => a - b); const m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
|
|
772
|
+
// Two-hop downstream median: use the rank of the consumer's consumer (NOT the consumer
|
|
773
|
+
// itself) as the sort key. For an input feeding through an intermediate single-input
|
|
774
|
+
// gate like a NOT, the 1-hop barycentre places the input at the NOT's row, but the
|
|
775
|
+
// NOT then feeds a gate elsewhere — the input's straightest target is the consumer's
|
|
776
|
+
// CONSUMER's row (e.g. HBLK -> not_7 -> and_8 places HBLK at and_8's row). For inputs
|
|
777
|
+
// feeding multi-input gates directly, 1-hop and 2-hop converge.
|
|
778
|
+
//
|
|
779
|
+
// Bounded: clamp each input's movement to within ±ceil(n/3) ranks of its barycentre
|
|
780
|
+
// position. Unbounded 2-hop occasionally throws an input clean across the column when
|
|
781
|
+
// its 2-hop consumer (an output or a far-flung gate) sits at an extreme — the new row
|
|
782
|
+
// then crosses un-related wires (RESET jumping from bottom to top in Shared Intermediates
|
|
783
|
+
// overlapped a COMPARE fan-out trunk). Clamping preserves the barycentre's overall
|
|
784
|
+
// structure while letting 2-hop nudge inputs toward straighter positions locally.
|
|
785
|
+
const twoHop = (id) => {
|
|
786
|
+
const cons = succ.get(id) ?? [];
|
|
787
|
+
if (cons.length === 0)
|
|
788
|
+
return rowMap.get(id) ?? 0;
|
|
789
|
+
const ranks = [];
|
|
790
|
+
for (const c of cons) {
|
|
791
|
+
const cc = succ.get(c);
|
|
792
|
+
if (cc && cc.length > 0)
|
|
793
|
+
ranks.push(...cc.map(x => rowMap.get(x) ?? 0));
|
|
794
|
+
else
|
|
795
|
+
ranks.push(rowMap.get(c) ?? 0);
|
|
796
|
+
}
|
|
797
|
+
return med(ranks);
|
|
798
|
+
};
|
|
799
|
+
const bary = new Map();
|
|
800
|
+
for (const n of inputGroup)
|
|
801
|
+
bary.set(n.id, rowMap.get(n.id) ?? 0);
|
|
802
|
+
const n = inputGroup.length;
|
|
803
|
+
const maxMove = Math.max(1, Math.ceil(n / 3));
|
|
804
|
+
const clampedTwoHop = (id) => {
|
|
805
|
+
const b = bary.get(id) ?? 0;
|
|
806
|
+
const t = twoHop(id);
|
|
807
|
+
return Math.max(b - maxMove, Math.min(b + maxMove, t));
|
|
808
|
+
};
|
|
809
|
+
// Stable tie-broken sort by the clamped 2-hop median; preserve the existing order on ties.
|
|
810
|
+
inputGroup.sort((a, b) => (clampedTwoHop(a.id) - clampedTwoHop(b.id)) || ((bary.get(a.id) ?? 0) - (bary.get(b.id) ?? 0)));
|
|
811
|
+
for (let i = 0; i < inputGroup.length; i++)
|
|
812
|
+
rowMap.set(inputGroup[i].id, i);
|
|
813
|
+
// Fan-in contiguity: keep each gate's single-consumer inputs contiguous, so a large fan-in
|
|
814
|
+
// aligns straight to its ports instead of being split — and doglegged around — by an input
|
|
815
|
+
// that feeds a DIFFERENT gate (the split is what tangles a big OR/AND and crowds its wires).
|
|
816
|
+
// Group single-consumer inputs by consumer; order the blocks by their members' average rank
|
|
817
|
+
// (keeps each block roughly where it was); within a block keep rank/Y order so it still matches
|
|
818
|
+
// the gate's ascending-Y port assignment. Multi-consumer inputs stay singletons (they serve
|
|
819
|
+
// several gates legitimately). Then reassign contiguous ranks.
|
|
820
|
+
const soleConsumer = new Map();
|
|
821
|
+
for (const inp of inputGroup) {
|
|
822
|
+
const cs = [...nodes.values()].filter(nd => nd.inputIds.includes(inp.id));
|
|
823
|
+
if (cs.length === 1)
|
|
824
|
+
soleConsumer.set(inp.id, cs[0].id);
|
|
825
|
+
}
|
|
826
|
+
const rankOf = (inp) => rowMap.get(inp.id) ?? 0;
|
|
827
|
+
const blocks = new Map();
|
|
828
|
+
for (const inp of inputGroup) {
|
|
829
|
+
const key = soleConsumer.get(inp.id) ?? inp.id;
|
|
830
|
+
(blocks.get(key) ?? blocks.set(key, []).get(key)).push(inp);
|
|
831
|
+
}
|
|
832
|
+
const ordered = [...blocks.values()].sort((a, b) => a.reduce((s, x) => s + rankOf(x), 0) / a.length - b.reduce((s, x) => s + rankOf(x), 0) / b.length);
|
|
833
|
+
for (const blk of ordered)
|
|
834
|
+
blk.sort((a, b) => rankOf(a) - rankOf(b));
|
|
835
|
+
ordered.flat().forEach((inp, i) => rowMap.set(inp.id, i));
|
|
836
|
+
// Multi-layer crossing minimisation: order the DERIVED layers (gates + outputs) by BOTH sides,
|
|
837
|
+
// not just their inputs. Alternate a down-sweep (rank = mean of input ranks) with an up-sweep
|
|
838
|
+
// (rank = mean of consumer ranks) over layers 1..maxDepth, iterating; the input layer stays
|
|
839
|
+
// fixed. The up-sweep is the half that was missing: a gate driving outputs far below it (a
|
|
840
|
+
// cascade OR feeding bottom outputs) is pulled toward its consumers, so its output wires don't
|
|
841
|
+
// dogleg back across another gate's fan-out. Continuous ranks are fine — only the order is used.
|
|
842
|
+
const meanRank = (ids) => {
|
|
843
|
+
const rs = ids.map(id => rowMap.get(id)).filter((r) => r !== undefined);
|
|
844
|
+
return rs.length ? rs.reduce((s, r) => s + r, 0) / rs.length : null;
|
|
845
|
+
};
|
|
846
|
+
const succOf = new Map();
|
|
847
|
+
for (const nd of nodes.values())
|
|
848
|
+
for (const id of nd.inputIds) {
|
|
849
|
+
if (nodes.get(id)?.kind === 'output')
|
|
850
|
+
continue; // feedback edge, not a forward consumer
|
|
851
|
+
(succOf.get(id) ?? succOf.set(id, []).get(id)).push(nd.id);
|
|
852
|
+
}
|
|
853
|
+
for (let it = 0; it < 6; it++) {
|
|
854
|
+
for (let depth = 1; depth <= maxDepth; depth++)
|
|
855
|
+
for (const node of depthGroups.get(depth) ?? []) {
|
|
856
|
+
// Side-balanced barycentre: the midpoint between the input barycentre and the consumer
|
|
857
|
+
// barycentre (each SIDE weighted equally, regardless of how many edges it has). This pulls
|
|
858
|
+
// a reconvergent gate — many inputs high, few outputs low — to the true middle so its
|
|
859
|
+
// output wires don't dogleg back across another gate's fan-out, while a gate whose inputs
|
|
860
|
+
// and consumers already align barely moves (so it doesn't dogleg its own fan-in). Outputs
|
|
861
|
+
// have no consumers and keep their driver order.
|
|
862
|
+
const inM = meanRank(node.inputIds), outM = meanRank(succOf.get(node.id) ?? []);
|
|
863
|
+
const t = inM !== null && outM !== null ? (inM + outM) / 2 : inM ?? outM;
|
|
864
|
+
if (t !== null)
|
|
865
|
+
rowMap.set(node.id, t);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// crossmin candidate: replace the heuristic ranks with the crossing-minimised ordering (real
|
|
870
|
+
// nodes only; the dummy pass below reserves lanes exactly as for the heuristic path).
|
|
871
|
+
if (strategy === 'crossmin') {
|
|
872
|
+
const cm = crossminOrder(nodes, maxDepth, opts);
|
|
873
|
+
for (const [id, r] of cm)
|
|
874
|
+
rowMap.set(id, r);
|
|
875
|
+
}
|
|
876
|
+
const layoutNodes = [];
|
|
877
|
+
const nodeMap = new Map();
|
|
878
|
+
// OPTION COMPACTNESS scales spacing per axis. COMPACT_V / COMPACT tighten vertical (row)
|
|
879
|
+
// spacing; COMPACT_H / COMPACT tighten horizontal (column) spacing; SPACIOUS loosens
|
|
880
|
+
// vertical. Tighter spacing still respects the minimum gaps enforced by the collision and
|
|
881
|
+
// protected-zone passes, so it never causes overlaps.
|
|
882
|
+
const cmp = opts.compactness;
|
|
883
|
+
const vScale = opts.compactnessFactors ? opts.compactnessFactors[0]
|
|
884
|
+
: cmp === 'COMPACT' || cmp === 'COMPACT_V' ? 0.7 : cmp === 'SPACIOUS' ? 1.35 : 1;
|
|
885
|
+
const hScale = opts.compactnessFactors ? opts.compactnessFactors[1]
|
|
886
|
+
: cmp === 'COMPACT' || cmp === 'COMPACT_H' ? 0.72 : 1;
|
|
887
|
+
const rowSpacing = Math.round(ROW_SPACING * vScale / GRID) * GRID;
|
|
888
|
+
const colSpacing = Math.round(COL_SPACING * hScale / GRID) * GRID;
|
|
889
|
+
// Obstacle-aware placement: decompose every edge spanning more than one depth column into a
|
|
890
|
+
// chain of thin DUMMY nodes (one per intermediate column), so the coordinate assignment reserves
|
|
891
|
+
// a vertical lane for that wire and never drops a real gate into its straight path. The
|
|
892
|
+
// ordering (rowMap) above is computed on REAL nodes only — dummies don't reorder gates; each
|
|
893
|
+
// dummy is slotted at the row interpolated between the edge's endpoints (i.e. on the wire's
|
|
894
|
+
// line). Dummies reserve space only: they are removed after placement and routing uses the
|
|
895
|
+
// original edges through the now-clear lane.
|
|
896
|
+
const dummyIds = new Set();
|
|
897
|
+
const restoreEdges = [];
|
|
898
|
+
for (const c of [...nodes.values()]) {
|
|
899
|
+
if (c.inputIds.length === 0)
|
|
900
|
+
continue;
|
|
901
|
+
let changed = false;
|
|
902
|
+
const rc = rowMap.get(c.id) ?? 0;
|
|
903
|
+
const newInputs = c.inputIds.map(sid => {
|
|
904
|
+
const s = nodes.get(sid);
|
|
905
|
+
if (!s || s.kind === 'output' || c.depth - s.depth <= 1)
|
|
906
|
+
return sid; // short edge / feedback
|
|
907
|
+
changed = true;
|
|
908
|
+
const rs = rowMap.get(sid) ?? 0;
|
|
909
|
+
let prev = sid;
|
|
910
|
+
for (let d = s.depth + 1; d < c.depth; d++) {
|
|
911
|
+
const did = uid('dummy');
|
|
912
|
+
const node = { id: did, kind: 'gate', gateType: 'DUMMY', depth: d, inputIds: [prev] };
|
|
913
|
+
nodes.set(did, node);
|
|
914
|
+
rowMap.set(did, rs + ((rc - rs) * (d - s.depth)) / (c.depth - s.depth)); // on the edge line
|
|
915
|
+
(depthGroups.get(d) ?? (depthGroups.set(d, []).get(d))).push(node);
|
|
916
|
+
dummyIds.add(did);
|
|
917
|
+
prev = did;
|
|
918
|
+
}
|
|
919
|
+
return prev;
|
|
920
|
+
});
|
|
921
|
+
if (changed) {
|
|
922
|
+
restoreEdges.push({ node: c, inputIds: c.inputIds, inputPorts: c.inputPorts });
|
|
923
|
+
c.inputIds = newInputs;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// ---- Priority-method coordinate assignment (spec: Coordinate Assignment) ----
|
|
927
|
+
// Each node's vertical centre is aligned to the median of its neighbours on BOTH sides
|
|
928
|
+
// (sources and consumers), keeping the per-column barycentre order. Replaces the old global
|
|
929
|
+
// row-rank mapping, which spread nodes apart and ignored the consumer side.
|
|
930
|
+
const assignedY = assignCoordinates(nodes, depthGroups, rowMap, maxDepth, rowSpacing, laneTight);
|
|
931
|
+
// Dummies have done their job (reserving lanes) — restore the original edges and drop them so
|
|
932
|
+
// geometry and routing see only real nodes, now placed clear of the long-edge lanes.
|
|
933
|
+
for (const { node, inputIds, inputPorts } of restoreEdges) {
|
|
934
|
+
node.inputIds = inputIds;
|
|
935
|
+
node.inputPorts = inputPorts;
|
|
936
|
+
}
|
|
937
|
+
for (const id of dummyIds)
|
|
938
|
+
nodes.delete(id);
|
|
939
|
+
for (const node of nodes.values()) {
|
|
940
|
+
const absY = assignedY.get(node.id) ?? PAD_Y;
|
|
941
|
+
const absX = PAD_X + node.depth * colSpacing;
|
|
942
|
+
let w, h;
|
|
943
|
+
if (node.kind === 'input') {
|
|
944
|
+
w = INPUT_LABEL_W;
|
|
945
|
+
h = node.description ? 30 : 20;
|
|
946
|
+
if (node.name && hasMathContent(node.name))
|
|
947
|
+
h = Math.max(h, 30);
|
|
948
|
+
if (node.description && hasMathContent(node.description))
|
|
949
|
+
h = Math.max(h, 30);
|
|
950
|
+
h = Math.ceil(h / 10) * 10;
|
|
951
|
+
const outX = absX + w + INPUT_STUB;
|
|
952
|
+
const outY = absY + h / 2;
|
|
953
|
+
const ln = {
|
|
954
|
+
id: node.id, gateType: 'INPUT', label: node.label,
|
|
955
|
+
name: node.name, description: node.description,
|
|
956
|
+
absX, absY, width: w + INPUT_STUB, height: h,
|
|
957
|
+
inputs: [], outputs: [{ name: 'out', absX: outX, absY: outY }],
|
|
958
|
+
depth: node.depth,
|
|
959
|
+
};
|
|
960
|
+
layoutNodes.push(ln);
|
|
961
|
+
nodeMap.set(node.id, ln);
|
|
962
|
+
}
|
|
963
|
+
else if (node.kind === 'output') {
|
|
964
|
+
w = OUTPUT_LABEL_W;
|
|
965
|
+
h = node.description ? 30 : 20;
|
|
966
|
+
if (node.name && hasMathContent(node.name))
|
|
967
|
+
h = Math.max(h, 30);
|
|
968
|
+
if (node.description && hasMathContent(node.description))
|
|
969
|
+
h = Math.max(h, 30);
|
|
970
|
+
h = Math.ceil(h / 10) * 10;
|
|
971
|
+
let inX = absX;
|
|
972
|
+
const inY = absY + h / 2;
|
|
973
|
+
let bubbledInput = false;
|
|
974
|
+
// Mark bubbled input (BUBBLES mode: NOT feeding into output)
|
|
975
|
+
if (node.invertedInputs && node.invertedInputs.has(0)) {
|
|
976
|
+
bubbledInput = true;
|
|
977
|
+
inX -= BUBBLE_R * 2;
|
|
978
|
+
}
|
|
979
|
+
const ln = {
|
|
980
|
+
id: node.id, gateType: 'OUTPUT', label: node.label,
|
|
981
|
+
name: node.name, description: node.description,
|
|
982
|
+
absX, absY, width: w + OUTPUT_STUB, height: h,
|
|
983
|
+
inputs: [{ name: 'in', absX: inX, absY: inY, bubbled: bubbledInput || undefined }], outputs: [],
|
|
984
|
+
depth: node.depth,
|
|
985
|
+
};
|
|
986
|
+
layoutNodes.push(ln);
|
|
987
|
+
nodeMap.set(node.id, ln);
|
|
988
|
+
}
|
|
989
|
+
else if (node.gateType === 'NOT') {
|
|
990
|
+
w = NOT_GATE_TOTAL_W;
|
|
991
|
+
h = NOT_GATE_H;
|
|
992
|
+
const ln = {
|
|
993
|
+
id: node.id, gateType: 'NOT', label: node.label,
|
|
994
|
+
absX, absY, width: w, height: h,
|
|
995
|
+
inputs: [{ name: 'in_0', absX: absX, absY: absY + h / 2 }],
|
|
996
|
+
outputs: [{ name: 'out', absX: absX + w, absY: absY + h / 2 }],
|
|
997
|
+
depth: node.depth,
|
|
998
|
+
};
|
|
999
|
+
layoutNodes.push(ln);
|
|
1000
|
+
nodeMap.set(node.id, ln);
|
|
1001
|
+
}
|
|
1002
|
+
else if (node.blockType) {
|
|
1003
|
+
const bt = node.blockType;
|
|
1004
|
+
({ w, h } = bt === 'FB' ? fbDims(node) : blockSize(bt));
|
|
1005
|
+
const right = absX + w;
|
|
1006
|
+
let inputs;
|
|
1007
|
+
let outputs;
|
|
1008
|
+
if (bt === 'FB') {
|
|
1009
|
+
// Generic block: evenly-spaced input ports (labelled) on the left, one output port per
|
|
1010
|
+
// referenced .name on the right (default OUT). Ports are re-aligned to sources later.
|
|
1011
|
+
const place = (count, x) => Array.from({ length: count }, (_, i) => ({
|
|
1012
|
+
name: '', absX: x, absY: Math.round((absY + (h * (i + 1)) / (count + 1)) / GRID) * GRID,
|
|
1013
|
+
}));
|
|
1014
|
+
const used = [...(node.usedPorts ?? new Set())];
|
|
1015
|
+
if (used.length === 0)
|
|
1016
|
+
used.push('OUT');
|
|
1017
|
+
inputs = place(node.inputIds.length, absX);
|
|
1018
|
+
inputs.forEach((p, i) => { p.name = `in_${i}`; p.label = node.inputLabels?.[i]; });
|
|
1019
|
+
outputs = place(used.length, right);
|
|
1020
|
+
outputs.forEach((p, i) => { p.name = used[i]; p.label = used[i] === 'OUT' ? undefined : used[i]; });
|
|
1021
|
+
}
|
|
1022
|
+
else if (bt === 'SR') {
|
|
1023
|
+
inputs = [
|
|
1024
|
+
{ name: 'S', absX, absY: absY + 15 },
|
|
1025
|
+
{ name: 'R', absX, absY: absY + 40 },
|
|
1026
|
+
];
|
|
1027
|
+
const used = node.usedPorts ?? new Set(['Q']);
|
|
1028
|
+
outputs = [];
|
|
1029
|
+
if (used.has('Q'))
|
|
1030
|
+
outputs.push({ name: 'Q', absX: right, absY: absY + 15 });
|
|
1031
|
+
if (used.has('NQ'))
|
|
1032
|
+
outputs.push({ name: 'NQ', absX: right, absY: absY + 40 });
|
|
1033
|
+
if (outputs.length === 0)
|
|
1034
|
+
outputs.push({ name: 'Q', absX: right, absY: absY + h / 2 });
|
|
1035
|
+
}
|
|
1036
|
+
else if (bt === 'COMPARE') {
|
|
1037
|
+
inputs = [
|
|
1038
|
+
{ name: '+', absX, absY: absY + 15 },
|
|
1039
|
+
{ name: '-', absX, absY: absY + 35 },
|
|
1040
|
+
];
|
|
1041
|
+
outputs = [{ name: 'OUT', absX: right, absY: absY + h / 2 }];
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
// TIMER, RISING, FALLING — single input/output, centred.
|
|
1045
|
+
inputs = [{ name: 'in', absX, absY: absY + h / 2 }];
|
|
1046
|
+
outputs = [{ name: 'OUT', absX: right, absY: absY + h / 2 }];
|
|
1047
|
+
}
|
|
1048
|
+
const ln = {
|
|
1049
|
+
id: node.id, gateType: bt, label: node.label,
|
|
1050
|
+
name: node.name, description: node.description,
|
|
1051
|
+
absX, absY, width: w, height: h,
|
|
1052
|
+
inputs, outputs, depth: node.depth,
|
|
1053
|
+
blockType: bt, params: node.params,
|
|
1054
|
+
};
|
|
1055
|
+
layoutNodes.push(ln);
|
|
1056
|
+
nodeMap.set(node.id, ln);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
const numInputs = node.inputIds.length || 2;
|
|
1060
|
+
const isMultiInput = numInputs > 2;
|
|
1061
|
+
const useBars = opts.gateInputStyle === 'BARS' && numInputs > 2;
|
|
1062
|
+
const gGap = gateGap(node);
|
|
1063
|
+
if (useBars) {
|
|
1064
|
+
h = AND_GATE_H_BASE;
|
|
1065
|
+
w = isMultiInput ? GATE_W_MULTI : GATE_W;
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
h = gateBodyHeight(numInputs, gGap);
|
|
1069
|
+
w = isMultiInput ? GATE_W_MULTI : GATE_W;
|
|
1070
|
+
}
|
|
1071
|
+
const inputs = [];
|
|
1072
|
+
if (useBars) {
|
|
1073
|
+
// First two inputs: normal ports on the gate body at 1/3 and 2/3 height.
|
|
1074
|
+
const portSpacing = Math.round(h / 3 / GRID) * GRID;
|
|
1075
|
+
for (let i = 0; i < Math.min(2, numInputs); i++) {
|
|
1076
|
+
const portY = absY + (i + 1) * portSpacing;
|
|
1077
|
+
inputs.push({ name: `in_${i}`, absX: absX, absY: portY });
|
|
1078
|
+
}
|
|
1079
|
+
// Bar-tapped inputs (3rd+): evenly distribute across the full gate body height.
|
|
1080
|
+
// Each tap's port sits at the bar X (absX - 12 per spec), so wires connect there
|
|
1081
|
+
// and the stub from bar to gate body is rendered as part of the gate symbol.
|
|
1082
|
+
const barX = absX - INPUT_BAR_OFFSET;
|
|
1083
|
+
const barCount = numInputs - 2;
|
|
1084
|
+
const barSpan = h - GRID * 2; // 1 grid inset top and bottom
|
|
1085
|
+
for (let i = 0; i < barCount; i++) {
|
|
1086
|
+
const portY = Math.round((absY + GRID + (barSpan * (i + 0.5)) / barCount) / GRID) * GRID;
|
|
1087
|
+
inputs.push({ name: `in_${i + 2}`, absX: barX, absY: portY });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
for (let i = 0; i < numInputs; i++) {
|
|
1092
|
+
const portY = gateInputPortY(absY, i, gGap);
|
|
1093
|
+
inputs.push({ name: `in_${i}`, absX: absX, absY: portY });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// Inversion bubbles for multi-input gates are assigned in a later pass (see "Inversion bubble
|
|
1097
|
+
// port assignment"), once source-Y ordering — which decides source→port mapping — is final.
|
|
1098
|
+
// Apply per-port style overrides
|
|
1099
|
+
const styleMap = new Map();
|
|
1100
|
+
for (const m of portMeta) {
|
|
1101
|
+
if (m.property === 'Style')
|
|
1102
|
+
styleMap.set(m.identifier, m.value.toUpperCase());
|
|
1103
|
+
}
|
|
1104
|
+
const gateCenterY = Math.round((absY + h / 2) / GRID) * GRID;
|
|
1105
|
+
const outputs = [{ name: 'out', absX: absX + w, absY: gateCenterY }];
|
|
1106
|
+
// Mark bubbled output (BUBBLES mode) and shift output port right for bubble
|
|
1107
|
+
if (node.bubbledOutput) {
|
|
1108
|
+
outputs[0].bubbledOutput = true;
|
|
1109
|
+
outputs[0].absX += BUBBLE_R * 2;
|
|
1110
|
+
}
|
|
1111
|
+
const ln = {
|
|
1112
|
+
id: node.id, gateType: node.gateType ?? 'AND', label: node.label,
|
|
1113
|
+
name: node.name, description: node.description,
|
|
1114
|
+
absX, absY, width: w, height: h,
|
|
1115
|
+
inputs, outputs, depth: node.depth,
|
|
1116
|
+
barsMode: useBars ? true : undefined,
|
|
1117
|
+
};
|
|
1118
|
+
layoutNodes.push(ln);
|
|
1119
|
+
nodeMap.set(node.id, ln);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
// OPTION COLUMN_SPACING = ADAPTIVE: replace the fixed COL_SPACING pitch with a per-gap width sized
|
|
1123
|
+
// to each column's content. The gap between column d-1 and d must hold column d's fan-in dogleg
|
|
1124
|
+
// channels (nested at FANIN spacing), a gate-clearance turn at the gate, and a MIN_DOGLEG on the
|
|
1125
|
+
// source side — so gap = GATE_CLEARANCE + MIN_DOGLEG + (maxInDegree-1)*FANIN + slack. In-degree is
|
|
1126
|
+
// an upper bound on dogleg channels (straight inputs need none), so the estimate is conservative:
|
|
1127
|
+
// it only ever narrows relative to the uniform pitch and never cramps a fan-in. Shift is uniform
|
|
1128
|
+
// per column, so ports move with their node; runs before routing and label placement.
|
|
1129
|
+
if (opts.columnSpacing === 'ADAPTIVE') {
|
|
1130
|
+
const CLEAR = 20, FANIN = 15, SLACK = 30;
|
|
1131
|
+
const colWidth = [], colX = [PAD_X];
|
|
1132
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
1133
|
+
let w = 0;
|
|
1134
|
+
for (const n of layoutNodes)
|
|
1135
|
+
if (n.depth === d)
|
|
1136
|
+
w = Math.max(w, n.width);
|
|
1137
|
+
colWidth[d] = w;
|
|
1138
|
+
}
|
|
1139
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
1140
|
+
let indeg = 0;
|
|
1141
|
+
for (const n of layoutNodes)
|
|
1142
|
+
if (n.depth === d && n.gateType !== 'INPUT' && n.gateType !== 'OUTPUT')
|
|
1143
|
+
indeg = Math.max(indeg, n.inputs.length);
|
|
1144
|
+
const gap = Math.round((CLEAR + MIN_DOGLEG + Math.max(0, indeg - 1) * FANIN + SLACK) / GRID) * GRID;
|
|
1145
|
+
colX[d] = Math.min(colX[d - 1] + colWidth[d - 1] + gap, PAD_X + d * colSpacing); // never wider than uniform
|
|
1146
|
+
}
|
|
1147
|
+
for (const n of layoutNodes) {
|
|
1148
|
+
const dx = colX[n.depth] - n.absX;
|
|
1149
|
+
if (dx === 0)
|
|
1150
|
+
continue;
|
|
1151
|
+
n.absX += dx;
|
|
1152
|
+
for (const p of n.inputs)
|
|
1153
|
+
p.absX += dx;
|
|
1154
|
+
for (const p of n.outputs)
|
|
1155
|
+
p.absX += dx;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const gateNode of layoutNodes) {
|
|
1159
|
+
if (gateNode.gateType === 'INPUT' || gateNode.gateType === 'OUTPUT')
|
|
1160
|
+
continue;
|
|
1161
|
+
const gateTop = gateNode.absY;
|
|
1162
|
+
const gateBottom = gateNode.absY + gateNode.height;
|
|
1163
|
+
const gateRight = gateNode.absX;
|
|
1164
|
+
for (const inputNode of layoutNodes) {
|
|
1165
|
+
if (inputNode.gateType !== 'INPUT')
|
|
1166
|
+
continue;
|
|
1167
|
+
if (inputNode.absX >= gateRight)
|
|
1168
|
+
continue;
|
|
1169
|
+
const inputBottom = inputNode.absY + inputNode.height;
|
|
1170
|
+
if (inputBottom > gateTop && inputNode.absY < gateBottom && inputNode.absY < gateTop + 5) {
|
|
1171
|
+
const shift = Math.round((inputBottom - gateTop + 5) / GRID) * GRID;
|
|
1172
|
+
gateNode.absY += shift;
|
|
1173
|
+
for (const port of gateNode.inputs)
|
|
1174
|
+
port.absY += shift;
|
|
1175
|
+
for (const port of gateNode.outputs)
|
|
1176
|
+
port.absY += shift;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
// Position a gate's output port(s): a single output at the body centre; a multi-output block
|
|
1181
|
+
// (e.g. a generic FB) spreads its outputs evenly over the body. Used wherever the body is moved
|
|
1182
|
+
// or resized, so outputs stay placed (and downstream consumers, processed in depth order, align).
|
|
1183
|
+
const recenterOutputs = (g) => {
|
|
1184
|
+
const no = g.outputs.length;
|
|
1185
|
+
if (no <= 1) {
|
|
1186
|
+
if (g.outputs[0])
|
|
1187
|
+
g.outputs[0].absY = Math.round((g.absY + g.height / 2) / GRID) * GRID;
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
// FB outputs sit a fixed 40 apart (the output-stack gap) so the output nodes they drive
|
|
1191
|
+
// line up straight; other multi-output blocks just spread evenly.
|
|
1192
|
+
const gap = g.blockType === 'FB' ? 40 : Math.max(MIN_PORT_GAP, (g.height - 20) / no);
|
|
1193
|
+
const start = g.absY + g.height / 2 - ((no - 1) * gap) / 2;
|
|
1194
|
+
g.outputs.forEach((p, i) => { p.absY = Math.round((start + i * gap) / GRID) * GRID; });
|
|
1195
|
+
};
|
|
1196
|
+
// Align each single-input gate (e.g. NOT) so its input sits exactly on its source's output
|
|
1197
|
+
// Y — a straight wire. Processed in depth order so a chain of NOTs aligns left-to-right.
|
|
1198
|
+
// Run again AFTER the multi-input height/position passes below, because those move the
|
|
1199
|
+
// source gates and would otherwise leave a single-input gate stranded at a stale Y (which
|
|
1200
|
+
// makes its output collide with, and detour around, a neighbour in the next column).
|
|
1201
|
+
const singleInputGates = Array.from(nodes.values())
|
|
1202
|
+
.filter(n => n.kind === 'gate' && n.inputIds.length === 1)
|
|
1203
|
+
.sort((a, b) => a.depth - b.depth);
|
|
1204
|
+
const alignSingleInputGates = () => {
|
|
1205
|
+
for (const node of singleInputGates) {
|
|
1206
|
+
const gateNode = nodeMap.get(node.id);
|
|
1207
|
+
if (!gateNode || gateNode.inputs.length !== 1)
|
|
1208
|
+
continue;
|
|
1209
|
+
const sourceNode = nodeMap.get(node.inputIds[0]);
|
|
1210
|
+
if (!sourceNode || sourceNode.outputs.length === 0)
|
|
1211
|
+
continue;
|
|
1212
|
+
const sourceOutputY = Math.round(sourceNode.outputs[0].absY / GRID) * GRID;
|
|
1213
|
+
const offsetY = sourceOutputY - gateNode.inputs[0].absY;
|
|
1214
|
+
gateNode.absY = Math.round((gateNode.absY + offsetY) / GRID) * GRID;
|
|
1215
|
+
gateNode.inputs[0].absY = sourceOutputY;
|
|
1216
|
+
recenterOutputs(gateNode);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
alignSingleInputGates();
|
|
1220
|
+
for (const node of nodes.values()) {
|
|
1221
|
+
if (node.kind !== 'output')
|
|
1222
|
+
continue;
|
|
1223
|
+
const outputNode = nodeMap.get(node.id);
|
|
1224
|
+
if (!outputNode || node.inputIds.length === 0)
|
|
1225
|
+
continue;
|
|
1226
|
+
const sourceId = node.inputIds[0];
|
|
1227
|
+
const sourceNode = nodeMap.get(sourceId);
|
|
1228
|
+
if (!sourceNode || sourceNode.outputs.length === 0)
|
|
1229
|
+
continue;
|
|
1230
|
+
const sourceOutputY = Math.round(sourceNode.outputs[0].absY / GRID) * GRID;
|
|
1231
|
+
outputNode.inputs[0].absY = sourceOutputY;
|
|
1232
|
+
outputNode.absY = Math.round((sourceOutputY - outputNode.height / 2) / GRID) * GRID;
|
|
1233
|
+
}
|
|
1234
|
+
// ── Phase: gate placement. Every multi-input AND/OR gate is sized by PORT COUNT only (label-aware
|
|
1235
|
+
// gap) and slid to the position that minimises sub-MIN_DOGLEG jogs on its input wires — one
|
|
1236
|
+
// principled min-jog fit, whether the gate is fed by inputs or by other gates. Processed in depth
|
|
1237
|
+
// order so each gate sees its drivers already placed. The body is never grown to span far-apart
|
|
1238
|
+
// sources; such wires read as clean Z-routes. (NOT gates and blocks are aligned by their own
|
|
1239
|
+
// passes; outputs/inputs by their placement phases.)
|
|
1240
|
+
{
|
|
1241
|
+
const placeGate = (node) => {
|
|
1242
|
+
const gateNode = nodeMap.get(node.id);
|
|
1243
|
+
if (!gateNode || gateNode.inputs.length < 2)
|
|
1244
|
+
return;
|
|
1245
|
+
const gap = gateGap(node);
|
|
1246
|
+
const h = gateBodyHeight(node.inputIds.length, gap);
|
|
1247
|
+
const srcYs = node.inputIds
|
|
1248
|
+
.map(id => nodeMap.get(id)?.outputs[0]?.absY)
|
|
1249
|
+
.filter((y) => y !== undefined && Number.isFinite(y))
|
|
1250
|
+
.sort((a, b) => a - b);
|
|
1251
|
+
if (srcYs.length < 2)
|
|
1252
|
+
return;
|
|
1253
|
+
const cen = (srcYs[0] + srcYs[srcYs.length - 1]) / 2;
|
|
1254
|
+
let bestTop = gateNode.absY, bestSc = Infinity;
|
|
1255
|
+
for (let top = srcYs[0] - h; top <= srcYs[srcYs.length - 1] + GRID; top += GRID) {
|
|
1256
|
+
let sc = Math.abs(top + h / 2 - cen) * 0.001;
|
|
1257
|
+
for (let k = 0; k < srcYs.length; k++) {
|
|
1258
|
+
const d = Math.abs(srcYs[k] - gateInputPortY(top, k, gap));
|
|
1259
|
+
if (d >= 1 && d < MIN_DOGLEG)
|
|
1260
|
+
sc += 1000;
|
|
1261
|
+
sc += d * 0.1;
|
|
1262
|
+
}
|
|
1263
|
+
if (sc < bestSc) {
|
|
1264
|
+
bestSc = sc;
|
|
1265
|
+
bestTop = Math.round(top / GRID) * GRID;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
gateNode.absY = bestTop;
|
|
1269
|
+
gateNode.height = h;
|
|
1270
|
+
for (let k = 0; k < gateNode.inputs.length; k++) {
|
|
1271
|
+
gateNode.inputs[k].absY = Math.round(gateInputPortY(bestTop, k, gap) / GRID) * GRID;
|
|
1272
|
+
}
|
|
1273
|
+
recenterOutputs(gateNode);
|
|
1274
|
+
};
|
|
1275
|
+
for (const node of [...nodes.values()]
|
|
1276
|
+
.filter(n => n.kind === 'gate' && n.gateType && n.gateType !== 'NOT' && n.inputIds.length >= 2)
|
|
1277
|
+
.sort((a, b) => a.depth - b.depth))
|
|
1278
|
+
placeGate(node);
|
|
1279
|
+
}
|
|
1280
|
+
// (Output nodes are placed in the single "Phase: output placement" pass after all gate moves.)
|
|
1281
|
+
// Resolve gate-gate overlaps at the same depth column by pushing the lower
|
|
1282
|
+
// gate down so their bounding boxes no longer intersect.
|
|
1283
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
1284
|
+
let anyOverlap = false;
|
|
1285
|
+
// Gates vs gates
|
|
1286
|
+
for (let i = 0; i < layoutNodes.length; i++) {
|
|
1287
|
+
const a = layoutNodes[i];
|
|
1288
|
+
if (a.gateType === 'INPUT' || a.gateType === 'OUTPUT')
|
|
1289
|
+
continue;
|
|
1290
|
+
for (let j = i + 1; j < layoutNodes.length; j++) {
|
|
1291
|
+
const b = layoutNodes[j];
|
|
1292
|
+
if (b.gateType === 'INPUT' || b.gateType === 'OUTPUT')
|
|
1293
|
+
continue;
|
|
1294
|
+
if (a.depth !== b.depth)
|
|
1295
|
+
continue;
|
|
1296
|
+
const xOverlap = Math.min(a.absX + a.width, b.absX + b.width) - Math.max(a.absX, b.absX);
|
|
1297
|
+
if (xOverlap <= 0)
|
|
1298
|
+
continue;
|
|
1299
|
+
const yOverlap = Math.min(a.absY + a.height, b.absY + b.height) - Math.max(a.absY, b.absY);
|
|
1300
|
+
if (yOverlap <= 0)
|
|
1301
|
+
continue;
|
|
1302
|
+
const shift = Math.round((yOverlap + MIN_PORT_GAP) / GRID) * GRID;
|
|
1303
|
+
if (a.absY < b.absY) {
|
|
1304
|
+
b.absY += shift;
|
|
1305
|
+
for (const port of b.inputs)
|
|
1306
|
+
port.absY += shift;
|
|
1307
|
+
for (const port of b.outputs)
|
|
1308
|
+
port.absY += shift;
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
a.absY += shift;
|
|
1312
|
+
for (const port of a.inputs)
|
|
1313
|
+
port.absY += shift;
|
|
1314
|
+
for (const port of a.outputs)
|
|
1315
|
+
port.absY += shift;
|
|
1316
|
+
}
|
|
1317
|
+
anyOverlap = true;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// Gates vs outputs
|
|
1321
|
+
for (const node of layoutNodes) {
|
|
1322
|
+
if (node.gateType === 'INPUT')
|
|
1323
|
+
continue;
|
|
1324
|
+
if (node.gateType !== 'OUTPUT')
|
|
1325
|
+
continue;
|
|
1326
|
+
for (const gate of layoutNodes) {
|
|
1327
|
+
if (gate.gateType === 'INPUT' || gate.gateType === 'OUTPUT')
|
|
1328
|
+
continue;
|
|
1329
|
+
if (node.depth !== gate.depth)
|
|
1330
|
+
continue;
|
|
1331
|
+
const xOverlap = Math.min(node.absX + node.width, gate.absX + gate.width) - Math.max(node.absX, gate.absX);
|
|
1332
|
+
if (xOverlap <= 0)
|
|
1333
|
+
continue;
|
|
1334
|
+
const yOverlap = Math.min(node.absY + node.height, gate.absY + gate.height) - Math.max(node.absY, gate.absY);
|
|
1335
|
+
if (yOverlap <= 0)
|
|
1336
|
+
continue;
|
|
1337
|
+
const shift = Math.round((yOverlap + MIN_PORT_GAP) / GRID) * GRID;
|
|
1338
|
+
if (gate.absY < node.absY) {
|
|
1339
|
+
node.absY += shift;
|
|
1340
|
+
node.inputs[0].absY += shift;
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
gate.absY += shift;
|
|
1344
|
+
for (const port of gate.inputs)
|
|
1345
|
+
port.absY += shift;
|
|
1346
|
+
for (const port of gate.outputs)
|
|
1347
|
+
port.absY += shift;
|
|
1348
|
+
}
|
|
1349
|
+
anyOverlap = true;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (!anyOverlap)
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
// The collision pass above is the last thing that moves multi-input gates; re-align every
|
|
1356
|
+
// single-input gate (NOT) to its now-final source so it stays straight-through and out of
|
|
1357
|
+
// the next column's horizontal corridor (e.g. a NOT feeding an output mustn't sit in the
|
|
1358
|
+
// straight path of another gate's output wire).
|
|
1359
|
+
alignSingleInputGates();
|
|
1360
|
+
// (Output placement is done in a single pass after all gate moves — see "Phase: output
|
|
1361
|
+
// placement" below.)
|
|
1362
|
+
// Place feedback input ports. A feedback input has no left-hand source (it loops back from
|
|
1363
|
+
// an output), so the source-alignment passes above leave its port unset (non-finite). The
|
|
1364
|
+
// loop-back enters from whichever side it approaches: if the consumer sits ABOVE its
|
|
1365
|
+
// feedback driver the loop comes over the top (top port → fewer crossings), otherwise it
|
|
1366
|
+
// comes under (bottom port). Place the port on that side, expanding the gate body if needed.
|
|
1367
|
+
const feedbackPorts = new Set();
|
|
1368
|
+
for (const ln of layoutNodes) {
|
|
1369
|
+
const fb = ln.inputs.filter(p => !Number.isFinite(p.absY));
|
|
1370
|
+
for (const p of fb)
|
|
1371
|
+
feedbackPorts.add(p);
|
|
1372
|
+
if (fb.length === 0)
|
|
1373
|
+
continue;
|
|
1374
|
+
const real = ln.inputs.filter(p => Number.isFinite(p.absY));
|
|
1375
|
+
const fbOut = nodes.get(ln.id)?.inputIds.find(id => nodeMap.get(id)?.gateType === 'OUTPUT');
|
|
1376
|
+
const driverId = fbOut ? nodes.get(fbOut)?.inputIds[0] : undefined;
|
|
1377
|
+
const driver = driverId ? nodeMap.get(driverId) : undefined;
|
|
1378
|
+
const overTop = !!driver && (ln.absY + ln.height / 2) < (driver.absY + driver.height / 2);
|
|
1379
|
+
if (overTop) {
|
|
1380
|
+
let y = real.length ? Math.min(...real.map(p => p.absY)) : ln.absY + ln.height - PORT_SPACING / 2;
|
|
1381
|
+
for (const p of fb) {
|
|
1382
|
+
y = Math.round((y - PORT_SPACING) / GRID) * GRID;
|
|
1383
|
+
p.absY = y;
|
|
1384
|
+
p.absX = ln.absX;
|
|
1385
|
+
}
|
|
1386
|
+
const topEdge = y - PORT_SPACING;
|
|
1387
|
+
if (topEdge < ln.absY) {
|
|
1388
|
+
const grow = Math.ceil((ln.absY - topEdge) / GRID) * GRID;
|
|
1389
|
+
ln.absY -= grow;
|
|
1390
|
+
ln.height += grow;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
let y = real.length ? Math.max(...real.map(p => p.absY)) : ln.absY + PORT_SPACING / 2;
|
|
1395
|
+
for (const p of fb) {
|
|
1396
|
+
y = Math.round((y + PORT_SPACING) / GRID) * GRID;
|
|
1397
|
+
p.absY = y;
|
|
1398
|
+
p.absX = ln.absX;
|
|
1399
|
+
}
|
|
1400
|
+
const bottom = y + PORT_SPACING;
|
|
1401
|
+
if (bottom > ln.absY + ln.height)
|
|
1402
|
+
ln.height = Math.ceil((bottom - ln.absY) / GRID) * GRID;
|
|
1403
|
+
}
|
|
1404
|
+
if (ln.outputs[0])
|
|
1405
|
+
ln.outputs[0].absY = Math.round((ln.absY + ln.height / 2) / GRID) * GRID;
|
|
1406
|
+
}
|
|
1407
|
+
// Snap all node and port positions to the grid BEFORE routing. This guarantees that
|
|
1408
|
+
// an aligned source/dest pair has exactly equal Y, so the router takes the clean
|
|
1409
|
+
// straight-line fast-path instead of a 1px dogleg (which the router can otherwise
|
|
1410
|
+
// mis-handle). Done before the OR curve-tap pass so curve taps are not re-snapped.
|
|
1411
|
+
for (const n of layoutNodes) {
|
|
1412
|
+
n.absX = Math.round(n.absX / GRID) * GRID;
|
|
1413
|
+
n.absY = Math.round(n.absY / GRID) * GRID;
|
|
1414
|
+
for (const p of [...n.inputs, ...n.outputs]) {
|
|
1415
|
+
p.absX = Math.round(p.absX / GRID) * GRID;
|
|
1416
|
+
p.absY = Math.round(p.absY / GRID) * GRID;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// ── Phase: dogleg cleanup. Gate placement MINIMISES sub-MIN_DOGLEG jogs, but a few can survive a
|
|
1420
|
+
// placement compromise — a multi-consumer input that cannot sit on every gate's port, or a body
|
|
1421
|
+
// nudged by the protected zone. This phase enforces the clean-wire rule: for any input port within
|
|
1422
|
+
// MIN_DOGLEG of (but not on) its source, shift the WHOLE gate to align one port without creating a
|
|
1423
|
+
// new small jog elsewhere; only if no such shift exists, nudge the single port (keeping its
|
|
1424
|
+
// PORT_SPACING gap to neighbours). Feedback ports have no left-hand source and are skipped.
|
|
1425
|
+
const isSmall = (d) => Math.abs(d) >= 0.5 && Math.abs(d) < MIN_DOGLEG;
|
|
1426
|
+
for (const node of nodes.values()) {
|
|
1427
|
+
if (node.inputIds.length === 0)
|
|
1428
|
+
continue;
|
|
1429
|
+
const ln = nodeMap.get(node.id);
|
|
1430
|
+
if (!ln || ln.inputs.length === 0)
|
|
1431
|
+
continue;
|
|
1432
|
+
const srcYs = node.inputIds
|
|
1433
|
+
.map(id => nodeMap.get(id)?.outputs[0]?.absY)
|
|
1434
|
+
.filter((y) => y !== undefined)
|
|
1435
|
+
.sort((a, b) => a - b);
|
|
1436
|
+
// Exclude feedback ports — they have no left-hand source, so pairing them to a real
|
|
1437
|
+
// source Y by sorted index mis-detects a phantom dogleg and shifts the whole gate.
|
|
1438
|
+
const ports = [...ln.inputs].filter(p => !feedbackPorts.has(p)).sort((a, b) => a.absY - b.absY);
|
|
1439
|
+
const n = Math.min(ports.length, srcYs.length);
|
|
1440
|
+
const diffs = () => ports.map((p, i) => (i < n ? p.absY - srcYs[i] : 0));
|
|
1441
|
+
if (!diffs().some(isSmall))
|
|
1442
|
+
continue;
|
|
1443
|
+
// Candidate whole-gate shifts: the offset that would align each currently-small port.
|
|
1444
|
+
const candidates = diffs().map((d, i) => (isSmall(d) ? -d : null)).filter((x) => x !== null);
|
|
1445
|
+
let applied = false;
|
|
1446
|
+
for (const delta of candidates) {
|
|
1447
|
+
if (!Number.isInteger(delta / GRID))
|
|
1448
|
+
continue;
|
|
1449
|
+
const after = ports.map((p, i) => (i < n ? p.absY + delta - srcYs[i] : 0));
|
|
1450
|
+
if (after.some(isSmall))
|
|
1451
|
+
continue; // would still leave a small jog somewhere
|
|
1452
|
+
ln.absY += delta;
|
|
1453
|
+
for (const p of ln.inputs)
|
|
1454
|
+
p.absY += delta;
|
|
1455
|
+
for (const p of ln.outputs)
|
|
1456
|
+
p.absY += delta;
|
|
1457
|
+
applied = true;
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
if (applied)
|
|
1461
|
+
continue;
|
|
1462
|
+
// Fallback: nudge an individual port onto its source, but never closer than
|
|
1463
|
+
// PORT_SPACING to a neighbour (so we don't trade a dogleg for a too-tight port gap).
|
|
1464
|
+
for (let i = 0; i < n; i++) {
|
|
1465
|
+
const port = ports[i];
|
|
1466
|
+
const want = srcYs[i];
|
|
1467
|
+
if (!isSmall(port.absY - want) || !Number.isInteger(want / GRID))
|
|
1468
|
+
continue;
|
|
1469
|
+
const prevY = i > 0 ? ports[i - 1].absY : -Infinity;
|
|
1470
|
+
const nextY = i < ports.length - 1 ? ports[i + 1].absY : Infinity;
|
|
1471
|
+
const insideBody = ln.gateType === 'OUTPUT' || (want > ln.absY && want < ln.absY + ln.height);
|
|
1472
|
+
if (want - prevY >= PORT_SPACING - 0.5 && nextY - want >= PORT_SPACING - 0.5 && insideBody) {
|
|
1473
|
+
port.absY = want;
|
|
1474
|
+
if (ln.gateType === 'OUTPUT')
|
|
1475
|
+
ln.absY = Math.round((want - ln.height / 2) / GRID) * GRID;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// Helper: the output Y of a driver's port `portName` (or its first output) — used by the
|
|
1480
|
+
// input/output placement phases to align a wire's far end to its driver.
|
|
1481
|
+
const blkSrcY = (srcId, portName) => {
|
|
1482
|
+
const s = nodeMap.get(srcId);
|
|
1483
|
+
if (!s)
|
|
1484
|
+
return undefined;
|
|
1485
|
+
return ((portName ? s.outputs.find(o => o.name === portName) : undefined) ?? s.outputs[0])?.absY;
|
|
1486
|
+
};
|
|
1487
|
+
// Protected zone: keep a minimum vertical gap between adjacent gate bodies in a column. The
|
|
1488
|
+
// alignment passes pull gates toward their sources, which can jam two gates that share an
|
|
1489
|
+
// input right up against each other (e.g. an AND and a NOT both reading the same signal).
|
|
1490
|
+
// Push the lower gate down to open the gap. The push is at least MIN_DOGLEG so the gate's
|
|
1491
|
+
// (now slightly offset) wires read as clean Z-routes rather than small jogs — and this runs
|
|
1492
|
+
// after the dogleg-killer so it is not pulled back. Done before routing so wires are fresh.
|
|
1493
|
+
for (const d of new Set(layoutNodes.filter(n => n.gateType !== 'INPUT' && n.gateType !== 'OUTPUT').map(n => n.depth))) {
|
|
1494
|
+
const col = layoutNodes
|
|
1495
|
+
.filter(n => n.depth === d && n.gateType !== 'INPUT' && n.gateType !== 'OUTPUT')
|
|
1496
|
+
.sort((a, b) => a.absY - b.absY);
|
|
1497
|
+
for (let i = 1; i < col.length; i++) {
|
|
1498
|
+
const gap = col[i].absY - (col[i - 1].absY + col[i - 1].height);
|
|
1499
|
+
if (gap < MIN_PORT_GAP - 0.5) {
|
|
1500
|
+
const dy = Math.round(Math.max(MIN_PORT_GAP - gap, MIN_DOGLEG) / GRID) * GRID;
|
|
1501
|
+
col[i].absY += dy;
|
|
1502
|
+
for (const p of col[i].inputs)
|
|
1503
|
+
p.absY += dy;
|
|
1504
|
+
for (const p of col[i].outputs)
|
|
1505
|
+
p.absY += dy;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// ── Phase: input placement. Snap each single-consumer INPUT onto the port row of the gate it
|
|
1510
|
+
// feeds, so its wire is straight (the gate is already port-count-sized and positioned). Process
|
|
1511
|
+
// each input column top-to-bottom, cascading to keep label-safe spacing; a multi-consumer input
|
|
1512
|
+
// can't sit on one gate's port so it keeps its swept position.
|
|
1513
|
+
{
|
|
1514
|
+
const consumerCount = new Map();
|
|
1515
|
+
const consumerOf = new Map();
|
|
1516
|
+
for (const nd of nodes.values())
|
|
1517
|
+
for (const id of nd.inputIds) {
|
|
1518
|
+
consumerCount.set(id, (consumerCount.get(id) ?? 0) + 1);
|
|
1519
|
+
if (!consumerOf.has(id))
|
|
1520
|
+
consumerOf.set(id, nd.id);
|
|
1521
|
+
}
|
|
1522
|
+
// The port Y that input `id` maps to on its consumer gate: gates connect sources to ports in
|
|
1523
|
+
// ascending source-Y order, so input rank r (by current source Y) -> the r-th port by Y.
|
|
1524
|
+
const targetPortY = (id) => {
|
|
1525
|
+
const consumerId = consumerOf.get(id);
|
|
1526
|
+
const cFlat = consumerId ? nodes.get(consumerId) : undefined;
|
|
1527
|
+
const cNode = consumerId ? nodeMap.get(consumerId) : undefined;
|
|
1528
|
+
// Only align to AND/OR gate ports (uniform gap). Blocks (COMPARE/TIMER/...) and BARS gates
|
|
1529
|
+
// have their own fixed/asymmetric port layouts, and NOT has a single input.
|
|
1530
|
+
if (!cFlat || !cNode || (cFlat.gateType !== 'AND' && cFlat.gateType !== 'OR') || cNode.barsMode || cNode.inputs.length < 2)
|
|
1531
|
+
return undefined;
|
|
1532
|
+
const ranked = cFlat.inputIds
|
|
1533
|
+
.map(sid => ({ sid, y: nodeMap.get(sid)?.outputs[0]?.absY }))
|
|
1534
|
+
.filter((e) => e.y !== undefined)
|
|
1535
|
+
.sort((a, b) => a.y - b.y);
|
|
1536
|
+
const rank = ranked.findIndex(e => e.sid === id);
|
|
1537
|
+
if (rank < 0)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
const portsByY = [...cNode.inputs].sort((a, b) => a.absY - b.absY);
|
|
1540
|
+
return portsByY[rank]?.absY;
|
|
1541
|
+
};
|
|
1542
|
+
const LABEL_GAP = 30;
|
|
1543
|
+
const cols = new Map();
|
|
1544
|
+
for (const ln of layoutNodes) {
|
|
1545
|
+
if (ln.gateType !== 'INPUT')
|
|
1546
|
+
continue;
|
|
1547
|
+
(cols.get(ln.absX) ?? cols.set(ln.absX, []).get(ln.absX)).push(ln);
|
|
1548
|
+
}
|
|
1549
|
+
for (const col of cols.values()) {
|
|
1550
|
+
col.sort((a, b) => a.absY - b.absY);
|
|
1551
|
+
// Snap each single-consumer input onto its gate port — but only if the slot is clear of its
|
|
1552
|
+
// neighbours (so we never collide a snapped input with another input). Non-snappable inputs
|
|
1553
|
+
// keep their swept position.
|
|
1554
|
+
for (const ln of col) {
|
|
1555
|
+
if (consumerCount.get(ln.id) !== 1)
|
|
1556
|
+
continue;
|
|
1557
|
+
const want = targetPortY(ln.id);
|
|
1558
|
+
if (want === undefined)
|
|
1559
|
+
continue;
|
|
1560
|
+
const y = Math.round(want / GRID) * GRID;
|
|
1561
|
+
if (col.some(o => o !== ln && Math.abs(o.outputs[0].absY - y) < LABEL_GAP - 0.5))
|
|
1562
|
+
continue;
|
|
1563
|
+
const d = y - (ln.outputs[0]?.absY ?? ln.absY);
|
|
1564
|
+
ln.absY += d;
|
|
1565
|
+
for (const p of ln.outputs)
|
|
1566
|
+
p.absY += d;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// ── Phase: output placement (single pass, runs after every gate move so it sees final driver
|
|
1571
|
+
// positions). Order each output column (AUTO by source Y, else declaration), then place each
|
|
1572
|
+
// output at its driver's output Y — a straight wire where the column allows, otherwise pushed
|
|
1573
|
+
// down to a clean >= MIN_DOGLEG below the output above. Subsumes the earlier align / snap /
|
|
1574
|
+
// de-overlap output passes.
|
|
1575
|
+
{
|
|
1576
|
+
const declIndex = new Map();
|
|
1577
|
+
let di = 0;
|
|
1578
|
+
for (const node of nodes.values())
|
|
1579
|
+
if (node.kind === 'output')
|
|
1580
|
+
declIndex.set(node.id, di++);
|
|
1581
|
+
const sourceY = (o) => {
|
|
1582
|
+
const fn = nodes.get(o.id);
|
|
1583
|
+
const sy = fn ? blkSrcY(fn.inputIds[0], fn.inputPorts?.[0]) : undefined;
|
|
1584
|
+
return sy !== undefined ? Math.round(sy / GRID) * GRID : o.absY + o.height / 2;
|
|
1585
|
+
};
|
|
1586
|
+
const sourceDepth = (o) => {
|
|
1587
|
+
const srcId = nodes.get(o.id)?.inputIds[0];
|
|
1588
|
+
return srcId ? nodes.get(srcId)?.depth ?? 0 : 0;
|
|
1589
|
+
};
|
|
1590
|
+
const cols = new Map();
|
|
1591
|
+
for (const n of layoutNodes) {
|
|
1592
|
+
if (n.gateType !== 'OUTPUT')
|
|
1593
|
+
continue;
|
|
1594
|
+
(cols.get(n.absX) ?? cols.set(n.absX, []).get(n.absX)).push(n);
|
|
1595
|
+
}
|
|
1596
|
+
const minGap = 40; // centre-to-centre clearance between stacked output labels
|
|
1597
|
+
for (const outs of cols.values()) {
|
|
1598
|
+
// AUTO orders by source Y (deeper source wins a tie so its short wire stays straight); else
|
|
1599
|
+
// declaration order.
|
|
1600
|
+
outs.sort((a, b) => opts.outputOrder === 'AUTO'
|
|
1601
|
+
? sourceY(a) - sourceY(b) || sourceDepth(b) - sourceDepth(a) || declIndex.get(a.id) - declIndex.get(b.id)
|
|
1602
|
+
: declIndex.get(a.id) - declIndex.get(b.id));
|
|
1603
|
+
let prevCenter = -Infinity;
|
|
1604
|
+
for (const o of outs) {
|
|
1605
|
+
if (!o.inputs[0])
|
|
1606
|
+
continue;
|
|
1607
|
+
const want = sourceY(o);
|
|
1608
|
+
let center = Math.max(want, prevCenter + minGap);
|
|
1609
|
+
if (center - want > 0 && center - want < MIN_DOGLEG)
|
|
1610
|
+
center = want + MIN_DOGLEG;
|
|
1611
|
+
center = Math.round(center / GRID) * GRID;
|
|
1612
|
+
o.absY = Math.round((center - o.height / 2) / GRID) * GRID;
|
|
1613
|
+
o.inputs[0].absY = center;
|
|
1614
|
+
prevCenter = center;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
// ---- Inversion bubble port assignment (BUBBLES mode) ----
|
|
1619
|
+
// Input ports connect to their sources in ascending source-Y order (the wire router's rule),
|
|
1620
|
+
// so an inverted input's bubble must follow its SIGNAL to whatever port that source lands on —
|
|
1621
|
+
// not a fixed input index. Assigning by index put the bubble on the wrong port whenever the
|
|
1622
|
+
// gate's inputs were reordered (e.g. `... AND NOT HBLK` with INPUT_ORDER = AUTO).
|
|
1623
|
+
for (const node of nodes.values()) {
|
|
1624
|
+
if (!node.invertedInputs || node.invertedInputs.size === 0)
|
|
1625
|
+
continue;
|
|
1626
|
+
const gate = nodeMap.get(node.id);
|
|
1627
|
+
if (!gate || gate.gateType === 'OUTPUT' || gate.inputs.length === 0)
|
|
1628
|
+
continue;
|
|
1629
|
+
const order = node.inputIds.map((id, i) => ({ i, y: blkSrcY(id, node.inputPorts?.[i]) ?? Infinity }));
|
|
1630
|
+
if (gate.gateType === 'AND' || gate.gateType === 'OR')
|
|
1631
|
+
order.sort((a, b) => a.y - b.y);
|
|
1632
|
+
order.forEach((e, rank) => {
|
|
1633
|
+
const port = gate.inputs[Math.min(rank, gate.inputs.length - 1)];
|
|
1634
|
+
if (port && node.invertedInputs.has(e.i)) {
|
|
1635
|
+
port.bubbled = true;
|
|
1636
|
+
port.absX -= BUBBLE_R * 2;
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
// ---- Generic FB block: attach each input's label to the port its source lands on ----
|
|
1641
|
+
// (Inputs map to ports in ascending source-Y order, so the declared label list is permuted.)
|
|
1642
|
+
for (const node of nodes.values()) {
|
|
1643
|
+
if (node.blockType !== 'FB')
|
|
1644
|
+
continue;
|
|
1645
|
+
const gate = nodeMap.get(node.id);
|
|
1646
|
+
if (!gate)
|
|
1647
|
+
continue;
|
|
1648
|
+
const order = node.inputIds.map((id, i) => ({ i, y: blkSrcY(id, node.inputPorts?.[i]) ?? Infinity }));
|
|
1649
|
+
order.sort((a, b) => a.y - b.y);
|
|
1650
|
+
order.forEach((e, rank) => { if (gate.inputs[rank])
|
|
1651
|
+
gate.inputs[rank].label = node.inputLabels?.[e.i]; });
|
|
1652
|
+
// Pull each output port onto its single consuming output node so the wire runs straight
|
|
1653
|
+
// (the output nodes are already placed by the AUTO stack + output-snap above).
|
|
1654
|
+
for (const port of gate.outputs) {
|
|
1655
|
+
const consumers = layoutNodes.filter(o => o.gateType === 'OUTPUT' &&
|
|
1656
|
+
nodes.get(o.id)?.inputIds[0] === node.id &&
|
|
1657
|
+
(nodes.get(o.id)?.inputPorts?.[0] ?? 'OUT') === port.name && o.inputs[0]);
|
|
1658
|
+
if (consumers.length === 1)
|
|
1659
|
+
port.absY = consumers[0].inputs[0].absY;
|
|
1660
|
+
}
|
|
1661
|
+
// Scale the box to ENCOMPASS its ports (each already sitting on its connection), grid-aligned,
|
|
1662
|
+
// so it grows with the input/output counts without forcing any wire into a jog/dogleg.
|
|
1663
|
+
const ys = [...gate.inputs, ...gate.outputs].map(p => p.absY);
|
|
1664
|
+
if (ys.length) {
|
|
1665
|
+
// Pad clears the router's vertical gate buffer (GATE_BUFFER_MIN_Y) so the top/bottom ports'
|
|
1666
|
+
// wires don't get nudged off their Y on the way out of the box.
|
|
1667
|
+
const pad = 30;
|
|
1668
|
+
const top = Math.floor((Math.min(...ys) - pad) / GRID) * GRID;
|
|
1669
|
+
const bottom = Math.ceil((Math.max(...ys) + pad) / GRID) * GRID;
|
|
1670
|
+
gate.absY = top;
|
|
1671
|
+
gate.height = bottom - top;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
// Keep same-column block bodies from touching. The FB encompass pass above grows each block's box
|
|
1675
|
+
// symmetrically around its ports, which can consume the vertical gap `sep()` reserved between two
|
|
1676
|
+
// adjacent blocks (their grown boxes meet — 50P1/50Q1 stacked body-to-body). This runs BEFORE
|
|
1677
|
+
// routing, so pushing a block (and its ports) down to restore a clean gap simply lets its wires
|
|
1678
|
+
// route to the new position — no wire is disturbed. Only blocks that actually overlap move, and
|
|
1679
|
+
// only downward, so a column without the problem is untouched.
|
|
1680
|
+
{
|
|
1681
|
+
const MIN_BLOCK_GAP = 20;
|
|
1682
|
+
const byCol = new Map();
|
|
1683
|
+
for (const n of layoutNodes)
|
|
1684
|
+
if (n.blockType) {
|
|
1685
|
+
const a = byCol.get(n.absX);
|
|
1686
|
+
if (a)
|
|
1687
|
+
a.push(n);
|
|
1688
|
+
else
|
|
1689
|
+
byCol.set(n.absX, [n]);
|
|
1690
|
+
}
|
|
1691
|
+
for (const col of byCol.values()) {
|
|
1692
|
+
if (col.length < 2)
|
|
1693
|
+
continue;
|
|
1694
|
+
col.sort((a, b) => a.absY - b.absY);
|
|
1695
|
+
for (let i = 1; i < col.length; i++) {
|
|
1696
|
+
const prev = col[i - 1], cur = col[i];
|
|
1697
|
+
const need = prev.absY + prev.height + MIN_BLOCK_GAP;
|
|
1698
|
+
if (cur.absY < need) {
|
|
1699
|
+
const dy = Math.ceil((need - cur.absY) / GRID) * GRID;
|
|
1700
|
+
cur.absY += dy;
|
|
1701
|
+
for (const p of cur.inputs)
|
|
1702
|
+
p.absY += dy;
|
|
1703
|
+
for (const p of cur.outputs)
|
|
1704
|
+
p.absY += dy;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// OR gate input ports tap the concave left curve. Done as a final pass so it uses
|
|
1710
|
+
// the gate's final height and each port's final (aligned) Y. The bbox, output port
|
|
1711
|
+
// and port Y positions stay on the grid; only the input-port X follows the curve.
|
|
1712
|
+
// Bubbled inputs shift left by BUBBLE_R*2 so the bubble's inner edge meets the curve.
|
|
1713
|
+
for (const gateNode of layoutNodes) {
|
|
1714
|
+
if (gateNode.gateType !== 'OR')
|
|
1715
|
+
continue;
|
|
1716
|
+
for (let i = 0; i < gateNode.inputs.length; i++) {
|
|
1717
|
+
const port = gateNode.inputs[i];
|
|
1718
|
+
if (gateNode.barsMode && i >= 2)
|
|
1719
|
+
continue; // bar-tap ports stay on the bar
|
|
1720
|
+
const localY = port.absY - gateNode.absY;
|
|
1721
|
+
const tapX = gateNode.absX + orCurveTapX(gateNode.height, localY);
|
|
1722
|
+
port.absX = port.bubbled ? tapX - BUBBLE_R * 2 : tapX;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const wires = [];
|
|
1726
|
+
const junctions = [];
|
|
1727
|
+
const junctionSet = new Set();
|
|
1728
|
+
function addJunction(x, y) {
|
|
1729
|
+
const key = `${Math.round(x / GRID) * GRID},${Math.round(y / GRID) * GRID}`;
|
|
1730
|
+
if (!junctionSet.has(key)) {
|
|
1731
|
+
junctionSet.add(key);
|
|
1732
|
+
junctions.push({ x: Math.round(x / GRID) * GRID, y: Math.round(y / GRID) * GRID });
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const allObstacles = layoutNodes.map(n => ({ x: n.absX, y: n.absY, w: n.width, h: n.height, id: n.id }));
|
|
1736
|
+
// Place each intermediate net label just above its driver's output, and register it as a
|
|
1737
|
+
// routing obstacle so the fan-out wires route around (not through) the text.
|
|
1738
|
+
const labels = [];
|
|
1739
|
+
const textW = (s, size = 11) => (s ? s.length * size * 0.6 : 0);
|
|
1740
|
+
for (const il of intermediateLabels) {
|
|
1741
|
+
const driver = nodeMap.get(il.driverId);
|
|
1742
|
+
if (!driver)
|
|
1743
|
+
continue;
|
|
1744
|
+
const port = (il.port ? driver.outputs.find(o => o.name === il.port) : undefined) ?? driver.outputs[0];
|
|
1745
|
+
if (!port)
|
|
1746
|
+
continue;
|
|
1747
|
+
const w = Math.ceil((Math.max(textW(il.name, 11), textW(il.description, 9)) + 8) / GRID) * GRID;
|
|
1748
|
+
const lines = (il.name ? 1 : 0) + (il.description ? 1 : 0);
|
|
1749
|
+
const h = Math.ceil((lines * 13 + 6) / GRID) * GRID;
|
|
1750
|
+
const x = Math.round((port.absX + 6) / GRID) * GRID; // just right of the output stub
|
|
1751
|
+
const y = Math.round((port.absY - h - 6) / GRID) * GRID; // above the fan-out trunk
|
|
1752
|
+
labels.push({ x, y, width: w, height: h, anchorX: port.absX, anchorY: port.absY, driverId: il.driverId, name: il.name, description: il.description });
|
|
1753
|
+
allObstacles.push({ x, y, w, h, id: `label_${il.driverId}` });
|
|
1754
|
+
}
|
|
1755
|
+
const routedSegments = [];
|
|
1756
|
+
const canvasW = Math.max(...layoutNodes.map(n => n.absX + n.width), ...layoutNodes.map(n => n.outputs[0]?.absX ?? n.absX + n.width)) + 200;
|
|
1757
|
+
const canvasH = Math.max(...layoutNodes.map(n => n.absY + n.height)) + 200;
|
|
1758
|
+
// The output port a wire leaves from: a named port (SR .Q/.NQ) if the consumer selected one,
|
|
1759
|
+
// otherwise the source's first output.
|
|
1760
|
+
const sourcePort = (srcId, portName) => {
|
|
1761
|
+
const src = nodeMap.get(srcId);
|
|
1762
|
+
if (!src)
|
|
1763
|
+
return undefined;
|
|
1764
|
+
return (portName ? src.outputs.find(o => o.name === portName) : undefined) ?? src.outputs[0];
|
|
1765
|
+
};
|
|
1766
|
+
// Build fan-out groups: destinations per (source, output port). Keyed `id::PORT` so a block
|
|
1767
|
+
// with two outputs (SR Q and NQ) routes each from its own port.
|
|
1768
|
+
const fanOutGroups = new Map();
|
|
1769
|
+
const wireRoutingOrder = Array.from(nodes.values()).sort((a, b) => a.depth - b.depth);
|
|
1770
|
+
for (const node of wireRoutingOrder) {
|
|
1771
|
+
if (node.inputIds.length === 0)
|
|
1772
|
+
continue;
|
|
1773
|
+
const toLayoutNode = nodeMap.get(node.id);
|
|
1774
|
+
if (!toLayoutNode)
|
|
1775
|
+
continue;
|
|
1776
|
+
let entries = node.inputIds.map((id, i) => ({ id, port: node.inputPorts?.[i] }));
|
|
1777
|
+
if (node.kind === 'gate' && (node.gateType === 'AND' || node.gateType === 'OR' || node.blockType === 'FB')) {
|
|
1778
|
+
entries = entries
|
|
1779
|
+
.map(e => ({ ...e, y: sourcePort(e.id, e.port)?.absY ?? Infinity }))
|
|
1780
|
+
.sort((a, b) => a.y - b.y);
|
|
1781
|
+
}
|
|
1782
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1783
|
+
const { id: fromId, port } = entries[i];
|
|
1784
|
+
const toPortIdx = Math.min(i, toLayoutNode.inputs.length - 1);
|
|
1785
|
+
const toPort = toLayoutNode.inputs[toPortIdx];
|
|
1786
|
+
if (!toPort)
|
|
1787
|
+
continue;
|
|
1788
|
+
const destIsGate = node.kind === 'gate';
|
|
1789
|
+
const groupKey = `${fromId}::${port ?? ''}`;
|
|
1790
|
+
if (!fanOutGroups.has(groupKey))
|
|
1791
|
+
fanOutGroups.set(groupKey, []);
|
|
1792
|
+
fanOutGroups.get(groupKey).push({ toId: node.id, toPort, toLayoutNode, destIsGate });
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// Route wires. Each destination is routed independently from the source output
|
|
1796
|
+
// port, which guarantees every consumer connects (including a source that feeds
|
|
1797
|
+
// both a gate and an output). Wires from the same source naturally overlap on a
|
|
1798
|
+
// shared horizontal "trunk" near the source (same-source crossings are cheap) and
|
|
1799
|
+
// diverge into separate channels; junction dots are added afterwards wherever
|
|
1800
|
+
// same-source wires form a T-intersection.
|
|
1801
|
+
for (const [groupKey, destinations] of fanOutGroups) {
|
|
1802
|
+
const sep = groupKey.lastIndexOf('::');
|
|
1803
|
+
const fromId = groupKey.slice(0, sep);
|
|
1804
|
+
const portName = groupKey.slice(sep + 2);
|
|
1805
|
+
const fromLayoutNode = nodeMap.get(fromId);
|
|
1806
|
+
const fromPort = sourcePort(fromId, portName || undefined);
|
|
1807
|
+
if (!fromLayoutNode || !fromPort)
|
|
1808
|
+
continue;
|
|
1809
|
+
const fx = fromPort.absX;
|
|
1810
|
+
const fy = fromPort.absY;
|
|
1811
|
+
// Route the destinations closest in Y to the source first, so the shared trunk
|
|
1812
|
+
// is established before farther branches need to find their channels.
|
|
1813
|
+
const ordered = [...destinations].sort((a, b) => Math.abs(a.toPort.absY - fy) - Math.abs(b.toPort.absY - fy));
|
|
1814
|
+
for (const dest of ordered) {
|
|
1815
|
+
const points = routeWireAStar(fx, fy, dest.toPort.absX, dest.toPort.absY, allObstacles, fromLayoutNode.absX, fromLayoutNode.absY, fromLayoutNode.width, fromLayoutNode.height, dest.toLayoutNode.absX, dest.toLayoutNode.absY, dest.toLayoutNode.width, dest.toLayoutNode.height, dest.destIsGate, routedSegments, canvasW, canvasH, fromId);
|
|
1816
|
+
routedSegments.push({ points, fromId });
|
|
1817
|
+
wires.push({ id: uid('wire'), points, fromId, toId: dest.toId });
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
// Gate-clearance helpers, shared by the channel track-assignment pass and the fan-in
|
|
1821
|
+
// nesting below. A vertical/horizontal run must stay GATE_CLEARANCE clear of every gate body.
|
|
1822
|
+
const GATE_CLEARANCE = 20;
|
|
1823
|
+
function vGateClear(x, y0, y1, skipId) {
|
|
1824
|
+
const yMin = Math.min(y0, y1), yMax = Math.max(y0, y1);
|
|
1825
|
+
for (const o of allObstacles) {
|
|
1826
|
+
if (o.id === skipId)
|
|
1827
|
+
continue;
|
|
1828
|
+
if (x > o.x - GATE_CLEARANCE && x < o.x + o.w + GATE_CLEARANCE &&
|
|
1829
|
+
yMax > o.y - 1 && yMin < o.y + o.h + 1)
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
function hGateClear(y, x0, x1, skipId) {
|
|
1835
|
+
const xMin = Math.min(x0, x1), xMax = Math.max(x0, x1);
|
|
1836
|
+
for (const o of allObstacles) {
|
|
1837
|
+
if (o.id === skipId)
|
|
1838
|
+
continue;
|
|
1839
|
+
if (y > o.y - 1 && y < o.y + o.h + 1 && xMax > o.x - 1 && xMin < o.x + o.w + 1)
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
// ── Wire separation contract (single source of truth) ───────────────────────────────────────
|
|
1845
|
+
// A segment is "clear" only if it keeps >= MIN_WIRE_SPACING from every PARALLEL same-orientation
|
|
1846
|
+
// segment of a DIFFERENT net (both orientations); perpendicular crossings are fine (crossovers,
|
|
1847
|
+
// not crowding). EVERY wire-reshaping pass below (channel track assignment, fan-in nesting,
|
|
1848
|
+
// shared-trunk merge, output snap) validates its proposed geometry through this ONE contract
|
|
1849
|
+
// before committing, so no pass can silently override another's separation. (Gate-body clearance
|
|
1850
|
+
// is vGateClear/hGateClear.) Reads `wires` live, so it always reflects current geometry.
|
|
1851
|
+
const segCrowds = (x0, y0, x1, y1, skip) => {
|
|
1852
|
+
const horiz = Math.abs(y0 - y1) < 0.5;
|
|
1853
|
+
if (!horiz && Math.abs(x0 - x1) >= 0.5)
|
|
1854
|
+
return false; // only axis-aligned segments participate
|
|
1855
|
+
const perp = horiz ? y0 : x0;
|
|
1856
|
+
const aMin = horiz ? Math.min(x0, x1) : Math.min(y0, y1);
|
|
1857
|
+
const aMax = horiz ? Math.max(x0, x1) : Math.max(y0, y1);
|
|
1858
|
+
for (const w of wires) {
|
|
1859
|
+
if (skip(w))
|
|
1860
|
+
continue;
|
|
1861
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
1862
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
1863
|
+
const oHoriz = Math.abs(a.y - b.y) < 0.5;
|
|
1864
|
+
if (oHoriz !== horiz || (!oHoriz && Math.abs(a.x - b.x) >= 0.5) || (oHoriz && Math.abs(a.x - b.x) < 0.5))
|
|
1865
|
+
continue;
|
|
1866
|
+
const oPerp = horiz ? a.y : a.x, dp = Math.abs(oPerp - perp);
|
|
1867
|
+
if (dp < 0.5 || dp >= MIN_WIRE_SPACING - 0.5)
|
|
1868
|
+
continue; // co-linear (overlap check elsewhere) or clear
|
|
1869
|
+
const oMin = horiz ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
1870
|
+
const oMax = horiz ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
1871
|
+
if (Math.min(aMax, oMax) - Math.max(aMin, oMin) > 0.5)
|
|
1872
|
+
return true; // parallel & overlapping & too close
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return false;
|
|
1876
|
+
};
|
|
1877
|
+
// A proposed wire (its point list) satisfies the contract iff none of its segments crowds another
|
|
1878
|
+
// net. `skip` excludes the wire being moved and any co-moved siblings (same source shares a trunk).
|
|
1879
|
+
const wireClear = (pts, skip) => {
|
|
1880
|
+
for (let i = 0; i < pts.length - 1; i++)
|
|
1881
|
+
if (segCrowds(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, skip))
|
|
1882
|
+
return false;
|
|
1883
|
+
return true;
|
|
1884
|
+
};
|
|
1885
|
+
// ── Channel track assignment ────────────────────────────────────────────────
|
|
1886
|
+
// Every wire that turns once (H–V–H) carries a single vertical segment living in the
|
|
1887
|
+
// inter-column channel between its two horizontal runs. Verticals that overlap in Y must
|
|
1888
|
+
// occupy distinct X "tracks" or they collide (the no-parallel-overlap invariant). A greedy
|
|
1889
|
+
// per-wire search can't resolve a *mutual* conflict — it never moves the other wire to make
|
|
1890
|
+
// room — so we assign tracks jointly: (1) collect the movable verticals, (2) group them into
|
|
1891
|
+
// channels by overlapping X-window, (3) interval-graph-colour each channel by Y so overlapping
|
|
1892
|
+
// cross-net verticals land on different tracks (same-source verticals may share a track — they
|
|
1893
|
+
// form a trunk), then (4) pick the track→X placement that minimises wire crossings, spreading
|
|
1894
|
+
// tracks across the channel's gate-clear window. All-or-nothing per channel and only for
|
|
1895
|
+
// channels that actually collide: a channel whose assignment can't be validated (gate in the
|
|
1896
|
+
// way, window too narrow) keeps its routed geometry, so we never trade a clean route for a
|
|
1897
|
+
// collision, and clean channels are left untouched.
|
|
1898
|
+
// A wire entering a gate input port must arrive with a horizontal run of at least
|
|
1899
|
+
// GATE_ENTRANCE, so the vertical turn sits clear of the gate body/curve and the wire visibly
|
|
1900
|
+
// enters horizontally rather than turning on the gate's edge (see spec: minimum gate entrance).
|
|
1901
|
+
const GATE_ENTRANCE = 20;
|
|
1902
|
+
// The track pass adjusts a wire's single vertical (a clean H–V–H turn). It deliberately does NOT
|
|
1903
|
+
// touch multi-bend wires: their extra bends are obstacle detours, and sliding the approach vertical
|
|
1904
|
+
// of such a wire would just drag its entrance horizontal along a sibling's wire (trading a gate-hug
|
|
1905
|
+
// for a horizontal overlap). Those cases are congestion to solve at placement, not here.
|
|
1906
|
+
const movables = [];
|
|
1907
|
+
for (const w of wires) {
|
|
1908
|
+
if (w.feedback)
|
|
1909
|
+
continue;
|
|
1910
|
+
const p = w.points;
|
|
1911
|
+
let vi = -1, vcount = 0;
|
|
1912
|
+
for (let i = 0; i < p.length - 1; i++) {
|
|
1913
|
+
if (Math.abs(p[i].x - p[i + 1].x) < 0.5 && Math.abs(p[i].y - p[i + 1].y) >= GRID) {
|
|
1914
|
+
vi = i;
|
|
1915
|
+
vcount++;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (vcount !== 1 || vi <= 0 || vi + 2 >= p.length)
|
|
1919
|
+
continue; // single clean H–V–H only
|
|
1920
|
+
if (Math.abs(p[vi - 1].y - p[vi].y) > 0.5)
|
|
1921
|
+
continue; // segment before V is horizontal
|
|
1922
|
+
if (Math.abs(p[vi + 1].y - p[vi + 2].y) > 0.5)
|
|
1923
|
+
continue; // segment after V is horizontal
|
|
1924
|
+
const aX = p[vi - 1].x, bX = p[vi + 2].x;
|
|
1925
|
+
if (bX <= aX + 0.5)
|
|
1926
|
+
continue; // left-to-right turns only
|
|
1927
|
+
const yA = p[vi].y, yB = p[vi + 1].y;
|
|
1928
|
+
const destNode = nodeMap.get(w.toId);
|
|
1929
|
+
const destIsGate = !!destNode && destNode.gateType !== 'INPUT' && destNode.gateType !== 'OUTPUT';
|
|
1930
|
+
movables.push({ w, vi, aX, bX, yA, yB, yTop: Math.min(yA, yB), yBot: Math.max(yA, yB), destIsGate });
|
|
1931
|
+
}
|
|
1932
|
+
// Fixed verticals: every vertical segment the track pass does NOT adjust (straight wires have
|
|
1933
|
+
// none; a multi-bend wire's non-tail verticals are pinned by the obstacles they detour around).
|
|
1934
|
+
// The pass treats these as occupied tracks so it keeps MIN_WIRE_SPACING clear of them — otherwise
|
|
1935
|
+
// a track can land 5px from a pinned vertical and read as a cluster. Only the specific adjustable
|
|
1936
|
+
// tail vertical of each movable is excluded (by wire id + segment index), not the whole wire.
|
|
1937
|
+
const movVi = new Map();
|
|
1938
|
+
for (const m of movables)
|
|
1939
|
+
movVi.set(m.w.id, m.vi);
|
|
1940
|
+
const fixedVerts = [];
|
|
1941
|
+
for (const w of wires) {
|
|
1942
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
1943
|
+
if (movVi.get(w.id) === i)
|
|
1944
|
+
continue; // the adjustable tail vertical
|
|
1945
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
1946
|
+
if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) >= GRID)
|
|
1947
|
+
fixedVerts.push({ x: a.x, y0: Math.min(a.y, b.y), y1: Math.max(a.y, b.y), from: w.fromId });
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
// Cross-net fixed verticals overlapping m's Y-span (excludes m's own source).
|
|
1951
|
+
const nearFixed = (m) => fixedVerts.filter(fv => fv.from !== m.w.fromId && Math.min(m.yBot, fv.y1) - Math.max(m.yTop, fv.y0) > 0);
|
|
1952
|
+
const yOverlap = (a, b) => Math.min(a.yBot, b.yBot) - Math.max(a.yTop, b.yTop) >= GRID;
|
|
1953
|
+
const placeValid = (m, X) => X >= m.aX - 0.5 && X <= m.bX + 0.5 &&
|
|
1954
|
+
vGateClear(X, m.yA, m.yB) && hGateClear(m.yA, m.aX, X, m.w.fromId) && hGateClear(m.yB, X, m.bX, m.w.toId);
|
|
1955
|
+
// Interior crossing between a horizontal run and another wire's vertical.
|
|
1956
|
+
const orthCross = (hx0, hx1, hy, vx, vy0, vy1) => vx > Math.min(hx0, hx1) + 0.5 && vx < Math.max(hx0, hx1) - 0.5 &&
|
|
1957
|
+
hy > Math.min(vy0, vy1) + 0.5 && hy < Math.max(vy0, vy1) - 0.5;
|
|
1958
|
+
const pairCross = (m1, x1, m2, x2) => {
|
|
1959
|
+
if (m1.w.fromId === m2.w.fromId)
|
|
1960
|
+
return 0; // same source: shared trunk, not a crossing
|
|
1961
|
+
let c = 0;
|
|
1962
|
+
if (orthCross(m1.aX, x1, m1.yA, x2, m2.yA, m2.yB))
|
|
1963
|
+
c++;
|
|
1964
|
+
if (orthCross(x1, m1.bX, m1.yB, x2, m2.yA, m2.yB))
|
|
1965
|
+
c++;
|
|
1966
|
+
if (orthCross(m2.aX, x2, m2.yA, x1, m1.yA, m1.yB))
|
|
1967
|
+
c++;
|
|
1968
|
+
if (orthCross(x2, m2.bX, m2.yB, x1, m1.yA, m1.yB))
|
|
1969
|
+
c++;
|
|
1970
|
+
return c;
|
|
1971
|
+
};
|
|
1972
|
+
const SPREAD = 3 * GRID; // preferred gap between cross-net tracks
|
|
1973
|
+
// Current X of each adjustable tail vertical (mutated as we place); every movable, whether or not
|
|
1974
|
+
// it is re-placed, acts as an obstacle at its current X. Working globally (no channel grouping)
|
|
1975
|
+
// means two verticals can never be blind to each other, so we can't reintroduce a collision.
|
|
1976
|
+
const curX = new Map(movables.map(m => [m, m.w.points[m.vi].x]));
|
|
1977
|
+
const crossNet = (m) => movables.filter(o => o !== m && o.w.fromId !== m.w.fromId && yOverlap(o, m));
|
|
1978
|
+
// The wire's two horizontal runs (run A: yA over [aX,X]; run B: yB over [X,bX]) at a candidate X,
|
|
1979
|
+
// checked against other nets' horizontals via the shared contract. The vertical stays under curX
|
|
1980
|
+
// coordination above; this adds the horizontal half so the track pass honours the full contract.
|
|
1981
|
+
const runsCrowd = (m, X) => {
|
|
1982
|
+
const skip = (o) => o.id === m.w.id || o.fromId === m.w.fromId;
|
|
1983
|
+
return segCrowds(m.aX, m.yA, X, m.yA, skip) || segCrowds(X, m.yB, m.bX, m.yB, skip);
|
|
1984
|
+
};
|
|
1985
|
+
// A movable violates if it hugs a gate (fails its own gate-clearance), has too short a gate
|
|
1986
|
+
// entrance, or runs within MIN_WIRE_SPACING of any cross-net vertical (fixed or another movable)
|
|
1987
|
+
// it overlaps in Y, or its horizontal runs crowd another net. Only violating movables are
|
|
1988
|
+
// re-placed; clean ones stay put (minimal churn).
|
|
1989
|
+
const crowded = (m, X) => nearFixed(m).some(fv => Math.abs(fv.x - X) < MIN_WIRE_SPACING - 0.5) ||
|
|
1990
|
+
crossNet(m).some(o => Math.abs(curX.get(o) - X) < MIN_WIRE_SPACING - 0.5) ||
|
|
1991
|
+
runsCrowd(m, X);
|
|
1992
|
+
const violates = (m) => {
|
|
1993
|
+
const X = curX.get(m);
|
|
1994
|
+
return !placeValid(m, X) || (m.destIsGate && m.bX - X < GATE_ENTRANCE - 0.5) || crowded(m, X);
|
|
1995
|
+
};
|
|
1996
|
+
// Candidate X's: grid positions in [aX, cap] that keep both runs gate-clear; a gate-bound vertical
|
|
1997
|
+
// is capped at bX-GATE_ENTRANCE so its horizontal entrance stays >= GATE_ENTRANCE.
|
|
1998
|
+
const candsFor = (m) => {
|
|
1999
|
+
const cands = [];
|
|
2000
|
+
const capX = m.destIsGate ? m.bX - GATE_ENTRANCE : m.bX;
|
|
2001
|
+
for (let x = Math.ceil(m.aX / GRID) * GRID; x <= Math.floor(capX / GRID) * GRID; x += GRID)
|
|
2002
|
+
if (placeValid(m, x))
|
|
2003
|
+
cands.push(x);
|
|
2004
|
+
return cands;
|
|
2005
|
+
};
|
|
2006
|
+
// Place violating movables most-constrained-first. Each avoids exact overlap with every fixed
|
|
2007
|
+
// vertical and every other movable's current X, and is scored to keep MIN_WIRE_SPACING clear,
|
|
2008
|
+
// minimise crossings, and stay near its routed X.
|
|
2009
|
+
const toPlace = movables.filter(violates).map(m => ({ m, cands: candsFor(m) }))
|
|
2010
|
+
.sort((a, b) => a.cands.length - b.cands.length);
|
|
2011
|
+
for (const { m, cands } of toPlace) {
|
|
2012
|
+
const origX = curX.get(m);
|
|
2013
|
+
const fixedForM = nearFixed(m), others = crossNet(m);
|
|
2014
|
+
let bestX = null, bestCost = Infinity;
|
|
2015
|
+
for (const x of cands) {
|
|
2016
|
+
let bad = false, cost = Math.abs(x - origX);
|
|
2017
|
+
for (const fv of fixedForM) {
|
|
2018
|
+
const d = Math.abs(fv.x - x);
|
|
2019
|
+
if (d < 0.5) {
|
|
2020
|
+
bad = true;
|
|
2021
|
+
break;
|
|
2022
|
+
} // exact overlap with a pinned vertical
|
|
2023
|
+
if (d < MIN_WIRE_SPACING - 0.5)
|
|
2024
|
+
cost += 2000;
|
|
2025
|
+
else if (d < SPREAD)
|
|
2026
|
+
cost += 50;
|
|
2027
|
+
}
|
|
2028
|
+
if (bad)
|
|
2029
|
+
continue;
|
|
2030
|
+
for (const o of others) {
|
|
2031
|
+
const d = Math.abs(curX.get(o) - x);
|
|
2032
|
+
if (d < 0.5) {
|
|
2033
|
+
bad = true;
|
|
2034
|
+
break;
|
|
2035
|
+
} // exact overlap with another track
|
|
2036
|
+
cost += pairCross(m, x, o, curX.get(o)) * 100000; // avoid new crossings above all
|
|
2037
|
+
if (d < MIN_WIRE_SPACING - 0.5)
|
|
2038
|
+
cost += 2000;
|
|
2039
|
+
else if (d < SPREAD)
|
|
2040
|
+
cost += 50;
|
|
2041
|
+
}
|
|
2042
|
+
if (bad)
|
|
2043
|
+
continue;
|
|
2044
|
+
if (runsCrowd(m, x))
|
|
2045
|
+
cost += 2000; // horizontal half of the contract
|
|
2046
|
+
if (cost < bestCost) {
|
|
2047
|
+
bestCost = cost;
|
|
2048
|
+
bestX = x;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (bestX !== null)
|
|
2052
|
+
curX.set(m, bestX); // no valid slot → keep routed X
|
|
2053
|
+
}
|
|
2054
|
+
for (const m of movables) {
|
|
2055
|
+
m.w.points[m.vi].x = curX.get(m);
|
|
2056
|
+
m.w.points[m.vi + 1].x = curX.get(m);
|
|
2057
|
+
}
|
|
2058
|
+
// Nested fan-in channels: when several wires dogleg into the same gate, give each its own
|
|
2059
|
+
// vertical channel just left of the gate, evenly spaced (FANIN_SPACING) and nested so they
|
|
2060
|
+
// neither cross nor crowd. Inputs arriving from above and from below are nested
|
|
2061
|
+
// independently, with the most extreme source turning closest to the gate — the arrangement
|
|
2062
|
+
// that avoids crossings and reads symmetrically. EVERY incoming dogleg wire is reshaped to
|
|
2063
|
+
// a clean H–V–H through its channel (regardless of the shape A* produced), so congested
|
|
2064
|
+
// multi-input gates get straight nested fan-in instead of A*'s small jogs. The all-or-
|
|
2065
|
+
// nothing validation falls back to the existing geometry if any channel hits a gate body or
|
|
2066
|
+
// a non-fan-in wire, so a genuinely obstacle-routed input is never forced through a gate.
|
|
2067
|
+
const FANIN_SPACING = 15;
|
|
2068
|
+
const lastPt = (w) => w.points[w.points.length - 1];
|
|
2069
|
+
const nestHit = (h1, h2, v1, v2) => Math.abs(h1.y - h2.y) < 1 && Math.abs(v1.x - v2.x) < 1 &&
|
|
2070
|
+
h1.y > Math.min(v1.y, v2.y) - 1 && h1.y < Math.max(v1.y, v2.y) + 1 &&
|
|
2071
|
+
v1.x > Math.min(h1.x, h2.x) - 1 && v1.x < Math.max(h1.x, h2.x) + 1;
|
|
2072
|
+
const nestCross = () => {
|
|
2073
|
+
let c = 0;
|
|
2074
|
+
for (let i = 0; i < wires.length; i++)
|
|
2075
|
+
for (let j = i + 1; j < wires.length; j++) {
|
|
2076
|
+
const a = wires[i], b = wires[j];
|
|
2077
|
+
if (a.fromId === b.fromId || a.feedback || b.feedback)
|
|
2078
|
+
continue;
|
|
2079
|
+
for (let s = 0; s < a.points.length - 1; s++)
|
|
2080
|
+
for (let t = 0; t < b.points.length - 1; t++)
|
|
2081
|
+
if (nestHit(a.points[s], a.points[s + 1], b.points[t], b.points[t + 1]) ||
|
|
2082
|
+
nestHit(b.points[t], b.points[t + 1], a.points[s], a.points[s + 1]))
|
|
2083
|
+
c++;
|
|
2084
|
+
}
|
|
2085
|
+
return c;
|
|
2086
|
+
};
|
|
2087
|
+
// Interior crossings of ONE wire against all other cross-net wires (same rule as nestCross). When a
|
|
2088
|
+
// reshape moves a single wire and nothing else, its own crossing count is the exact whole-diagram
|
|
2089
|
+
// delta — so this O(N) check replaces the O(N^2) nestCross() inside per-wire move searches.
|
|
2090
|
+
const wireCross = (self) => {
|
|
2091
|
+
if (self.feedback)
|
|
2092
|
+
return 0;
|
|
2093
|
+
let c = 0;
|
|
2094
|
+
for (const o of wires) {
|
|
2095
|
+
if (o === self || o.fromId === self.fromId || o.feedback)
|
|
2096
|
+
continue;
|
|
2097
|
+
for (let s = 0; s < self.points.length - 1; s++)
|
|
2098
|
+
for (let t = 0; t < o.points.length - 1; t++)
|
|
2099
|
+
if (nestHit(self.points[s], self.points[s + 1], o.points[t], o.points[t + 1]) ||
|
|
2100
|
+
nestHit(o.points[t], o.points[t + 1], self.points[s], self.points[s + 1]))
|
|
2101
|
+
c++;
|
|
2102
|
+
}
|
|
2103
|
+
return c;
|
|
2104
|
+
};
|
|
2105
|
+
for (const gate of layoutNodes) {
|
|
2106
|
+
if (gate.gateType === 'INPUT' || gate.gateType === 'OUTPUT')
|
|
2107
|
+
continue;
|
|
2108
|
+
const fanWires = wires.filter(w => w.toId === gate.id && Math.abs(w.points[0].y - lastPt(w).y) >= 1);
|
|
2109
|
+
if (fanWires.length < 2)
|
|
2110
|
+
continue;
|
|
2111
|
+
const above = fanWires.filter(w => w.points[0].y < lastPt(w).y);
|
|
2112
|
+
const below = fanWires.filter(w => w.points[0].y > lastPt(w).y);
|
|
2113
|
+
above.sort((a, b) => a.points[0].y - b.points[0].y); // topmost source first
|
|
2114
|
+
below.sort((a, b) => b.points[0].y - a.points[0].y); // bottommost source first
|
|
2115
|
+
// A reshaped channel is rejected if it would hit a gate body or overlap a wire from a
|
|
2116
|
+
// different source that is NOT part of this gate's nested fan-in (those are crossing-free
|
|
2117
|
+
// by construction). Rejected groups keep their original geometry.
|
|
2118
|
+
// Above and below groups are placed with an interleave offset (below shifts half a step
|
|
2119
|
+
// leftward) so their channels interleave and don't share the same X — without the offset,
|
|
2120
|
+
// above[0] and below[0] both map to gate.absX - GATE_CLEARANCE and overlap.
|
|
2121
|
+
const fanSet = new Set(fanWires);
|
|
2122
|
+
// Gate clearance + the shared wire-separation contract (both orientations). Co-reshaped
|
|
2123
|
+
// siblings and the same source are skipped; the mutually-distinct channel Xs and interleave
|
|
2124
|
+
// below keep the siblings themselves apart.
|
|
2125
|
+
const channelOk = (w, channelX, srcX, srcY, portX, portY) => {
|
|
2126
|
+
if (!vGateClear(channelX, srcY, portY))
|
|
2127
|
+
return false;
|
|
2128
|
+
if (!hGateClear(srcY, srcX, channelX, w.fromId))
|
|
2129
|
+
return false;
|
|
2130
|
+
if (!hGateClear(portY, channelX, portX, w.toId))
|
|
2131
|
+
return false;
|
|
2132
|
+
// Reject a channel that lands EXACTLY on another net's vertical (same X, overlapping Y).
|
|
2133
|
+
// wireClear/segCrowds deliberately ignore exact-collinear overlap ("overlap check elsewhere"),
|
|
2134
|
+
// so two gates sitting at the same X would otherwise nest their fan-in channels onto one line
|
|
2135
|
+
// (e.g. TRS and TRT both at x=875 -> both channels at 855). The leftward search then steps to a
|
|
2136
|
+
// distinct channel instead.
|
|
2137
|
+
const my0 = Math.min(srcY, portY), my1 = Math.max(srcY, portY);
|
|
2138
|
+
for (const o of wires) {
|
|
2139
|
+
if (o === w || o.fromId === w.fromId || fanSet.has(o))
|
|
2140
|
+
continue;
|
|
2141
|
+
for (let i = 0; i < o.points.length - 1; i++) {
|
|
2142
|
+
const a = o.points[i], b = o.points[i + 1];
|
|
2143
|
+
if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.x - channelX) < 0.5 &&
|
|
2144
|
+
Math.min(my1, Math.max(a.y, b.y)) - Math.max(my0, Math.min(a.y, b.y)) >= GRID)
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const reshaped = [{ x: srcX, y: srcY }, { x: channelX, y: srcY }, { x: channelX, y: portY }, { x: portX, y: portY }];
|
|
2149
|
+
return wireClear(reshaped, o => o === w || o.fromId === w.fromId || fanSet.has(o));
|
|
2150
|
+
};
|
|
2151
|
+
// All-or-nothing per group. For EACH wire, take the gate-most channel that validates by
|
|
2152
|
+
// SEARCHING leftward from its ideal (nested) X down to its min: a wire whose source-side run at
|
|
2153
|
+
// the near-gate channel would cross a gate body instead turns *before* that obstacle (a slid
|
|
2154
|
+
// channel), rather than the whole group bailing to A*'s detour. Channels stay >= MIN_WIRE_SPACING
|
|
2155
|
+
// apart (mutually distinct, nested order preserved). If any wire finds no valid channel the group
|
|
2156
|
+
// keeps its routed geometry; and the applied reshape is reverted whole if it adds any crossing,
|
|
2157
|
+
// so a robust fan-in is never traded for a new crossover.
|
|
2158
|
+
const place = (group, offset) => {
|
|
2159
|
+
const plan = [];
|
|
2160
|
+
const used = [];
|
|
2161
|
+
const groupMinX = Math.max(0, ...group.map(w => Math.round((w.points[0].x + MIN_DOGLEG) / GRID) * GRID));
|
|
2162
|
+
const room = gate.absX - GATE_CLEARANCE - groupMinX;
|
|
2163
|
+
const step = group.length > 1
|
|
2164
|
+
? Math.max(GRID, Math.min(FANIN_SPACING, Math.floor(room / (group.length - 1) / GRID) * GRID))
|
|
2165
|
+
: FANIN_SPACING;
|
|
2166
|
+
const gateMost = Math.round((gate.absX - GATE_CLEARANCE) / GRID) * GRID;
|
|
2167
|
+
for (let i = 0; i < group.length; i++) {
|
|
2168
|
+
const w = group[i];
|
|
2169
|
+
const srcX = w.points[0].x, srcY = w.points[0].y;
|
|
2170
|
+
const e = lastPt(w), portX = e.x, portY = e.y;
|
|
2171
|
+
const ideal = Math.round((gate.absX - GATE_CLEARANCE - i * step - offset) / GRID) * GRID;
|
|
2172
|
+
const minX = Math.round((srcX + MIN_DOGLEG) / GRID) * GRID;
|
|
2173
|
+
let cx = null;
|
|
2174
|
+
for (let x = Math.min(ideal, gateMost); x >= minX; x -= GRID) { // gate-most valid channel, sliding before obstacles
|
|
2175
|
+
if (used.some(u => Math.abs(u - x) < MIN_WIRE_SPACING - 0.5))
|
|
2176
|
+
continue;
|
|
2177
|
+
if (channelOk(w, x, srcX, srcY, portX, portY)) {
|
|
2178
|
+
cx = x;
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
if (cx === null)
|
|
2183
|
+
return; // no valid channel → keep routed geometry
|
|
2184
|
+
used.push(cx);
|
|
2185
|
+
plan.push({ w, cx, srcX, srcY, portX, portY });
|
|
2186
|
+
}
|
|
2187
|
+
const saved = group.map(w => w.points.map(p => ({ x: p.x, y: p.y })));
|
|
2188
|
+
const before = nestCross();
|
|
2189
|
+
for (const { w, cx, srcX, srcY, portX, portY } of plan) {
|
|
2190
|
+
w.points = [{ x: srcX, y: srcY }, { x: cx, y: srcY }, { x: cx, y: portY }, { x: portX, y: portY }];
|
|
2191
|
+
}
|
|
2192
|
+
if (nestCross() > before)
|
|
2193
|
+
group.forEach((w, k) => { w.points = saved[k]; }); // never add a crossing
|
|
2194
|
+
};
|
|
2195
|
+
// Interleave above and below groups so their channels don't share the same X. Without
|
|
2196
|
+
// the half-step offset, above[0] and below[0] both map to gate.absX - GATE_CLEARANCE and
|
|
2197
|
+
// end up at the same channel — overlapping. Below shifts by step/2 leftward to interleave.
|
|
2198
|
+
const aboveStep = (() => {
|
|
2199
|
+
const aboveMinX = Math.max(0, ...above.map(w => Math.round((w.points[0].x + MIN_DOGLEG) / GRID) * GRID));
|
|
2200
|
+
const aboveRoom = gate.absX - GATE_CLEARANCE - aboveMinX;
|
|
2201
|
+
return above.length > 1 ? Math.max(GRID, Math.min(FANIN_SPACING, Math.floor(aboveRoom / (above.length - 1) / GRID) * GRID)) : FANIN_SPACING;
|
|
2202
|
+
})();
|
|
2203
|
+
place(above, 0);
|
|
2204
|
+
place(below, Math.round(aboveStep / 2 / GRID) * GRID);
|
|
2205
|
+
}
|
|
2206
|
+
// Obstacle-aware output placement. An output whose port Y lands inside the vertical shadow of a gate
|
|
2207
|
+
// its incoming wire must cross is forced either to detour around that gate (a down-and-up /
|
|
2208
|
+
// up-and-over that lands on unrelated wires' lines) OR to drive straight THROUGH the gate body (a
|
|
2209
|
+
// gate crossing — e.g. PSV02's LED/ICMS outputs cutting through the TRS gate under reconvergence).
|
|
2210
|
+
// Move such an output to the nearest Y clear of every gate shadow its wire crosses, and redraw the
|
|
2211
|
+
// wire as one clean H–V–H. It is applied only when a clear position + route exists that validates
|
|
2212
|
+
// through gate clearance, the separation contract, sibling-output spacing, a legal dogleg, and a
|
|
2213
|
+
// no-new-crossing guard — otherwise fully reverted. So it can only ever remove a detour or a gate
|
|
2214
|
+
// crossing and can never introduce a crossing, crowd, overlap, or sub-min dogleg. Running before the
|
|
2215
|
+
// entrance pass frees any straight input the detour was blocking (the entrance pass straightens it).
|
|
2216
|
+
{
|
|
2217
|
+
const outGates = layoutNodes.filter(n => n.gateType !== 'INPUT' && n.gateType !== 'OUTPUT');
|
|
2218
|
+
const bendsOf = (pts) => {
|
|
2219
|
+
let c = 0;
|
|
2220
|
+
for (let i = 1; i < pts.length - 1; i++)
|
|
2221
|
+
if ((Math.abs(pts[i - 1].y - pts[i].y) < 0.5) !== (Math.abs(pts[i].y - pts[i + 1].y) < 0.5))
|
|
2222
|
+
c++;
|
|
2223
|
+
return c;
|
|
2224
|
+
};
|
|
2225
|
+
// Does any of the wire's segments cut through a gate body that is not its own endpoint?
|
|
2226
|
+
const crossesGate = (w) => {
|
|
2227
|
+
for (let i = 0; i < w.points.length - 1; i++) {
|
|
2228
|
+
const a = w.points[i], b = w.points[i + 1];
|
|
2229
|
+
const xm = Math.min(a.x, b.x), xM = Math.max(a.x, b.x), ym = Math.min(a.y, b.y), yM = Math.max(a.y, b.y);
|
|
2230
|
+
for (const g of outGates) {
|
|
2231
|
+
if (g.id === w.fromId || g.id === w.toId)
|
|
2232
|
+
continue;
|
|
2233
|
+
if (xM > g.absX + 1 && g.absX + g.width - 1 > xm && yM > g.absY + 1 && g.absY + g.height - 1 > ym)
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return false;
|
|
2238
|
+
};
|
|
2239
|
+
for (const O of layoutNodes) {
|
|
2240
|
+
if (O.gateType !== 'OUTPUT' || !O.inputs[0])
|
|
2241
|
+
continue;
|
|
2242
|
+
const w = wires.find(x => x.toId === O.id && !x.feedback);
|
|
2243
|
+
// Act on a detour (>2 bends) OR a wire that cuts through a gate body; a clean approach is left alone.
|
|
2244
|
+
if (!w || w.points.length < 4 || (bendsOf(w.points) <= 2 && !crossesGate(w)))
|
|
2245
|
+
continue;
|
|
2246
|
+
const sx = w.points[0].x, sy = w.points[0].y, ox = O.inputs[0].absX, oy = O.inputs[0].absY;
|
|
2247
|
+
const spanL = Math.min(sx, ox) + 1, spanR = Math.max(sx, ox) - 1;
|
|
2248
|
+
const crossing = outGates.filter(g => g.id !== w.fromId && g.absX + g.width > spanL && g.absX < spanR);
|
|
2249
|
+
const yClear = (ny) => crossing.every(g => ny <= g.absY - 1 || ny >= g.absY + g.height + 1);
|
|
2250
|
+
if (yClear(oy))
|
|
2251
|
+
continue; // output not actually in a shadow
|
|
2252
|
+
const sibClear = (ny) => layoutNodes.every(s => s === O || s.gateType !== 'OUTPUT' ||
|
|
2253
|
+
s.absX !== O.absX || Math.abs((s.absY + s.height / 2) - ny) >= MIN_PORT_GAP - 0.5);
|
|
2254
|
+
const before = nestCross();
|
|
2255
|
+
let done = false;
|
|
2256
|
+
for (let d = GRID; d <= 500 && !done; d += GRID) { // nearest clear Y first (above or below)
|
|
2257
|
+
for (const ny of [oy - d, oy + d]) {
|
|
2258
|
+
if (ny < 0 || !yClear(ny) || !sibClear(ny))
|
|
2259
|
+
continue;
|
|
2260
|
+
const jog = Math.abs(ny - sy);
|
|
2261
|
+
if (jog >= 0.5 && jog < MIN_DOGLEG)
|
|
2262
|
+
continue; // would be a sub-min dogleg
|
|
2263
|
+
for (let tapX = Math.round((sx + MIN_DOGLEG) / GRID) * GRID; tapX <= ox - GATE_ENTRANCE; tapX += GRID) {
|
|
2264
|
+
const route = [{ x: sx, y: sy }, { x: tapX, y: sy }, { x: tapX, y: ny }, { x: ox, y: ny }]
|
|
2265
|
+
.filter((p, i, a) => i === 0 || Math.abs(p.x - a[i - 1].x) >= 0.5 || Math.abs(p.y - a[i - 1].y) >= 0.5);
|
|
2266
|
+
const skip = (o) => o === w || o.fromId === w.fromId;
|
|
2267
|
+
if (!vGateClear(tapX, sy, ny) || !hGateClear(sy, sx, tapX, w.fromId) ||
|
|
2268
|
+
!hGateClear(ny, tapX, ox, w.toId) || !wireClear(route, skip))
|
|
2269
|
+
continue;
|
|
2270
|
+
const savedPts = w.points, savedOutY = O.absY, savedPortY = O.inputs[0].absY;
|
|
2271
|
+
w.points = route;
|
|
2272
|
+
O.inputs[0].absY = ny;
|
|
2273
|
+
O.absY = ny - O.height / 2;
|
|
2274
|
+
if (nestCross() > before) {
|
|
2275
|
+
w.points = savedPts;
|
|
2276
|
+
O.absY = savedOutY;
|
|
2277
|
+
O.inputs[0].absY = savedPortY;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
done = true;
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
if (done)
|
|
2284
|
+
break;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
// Un-wrap a single-consumer INPUT that wraps a block to reach its gate port (mirror of the output
|
|
2290
|
+
// placement above). A free input — exactly one consumer — feeds one port of a gate, but a sibling
|
|
2291
|
+
// block that also feeds that gate (an SR seal-in latch / sub-OR) sits in the horizontal span between
|
|
2292
|
+
// them, occupying the input's straight-line Y. The A* router is then forced up-and-over (or through)
|
|
2293
|
+
// that block, landing its wire across the block's own I/O wires (e.g. CTR3 → PSV03, wrapped over the
|
|
2294
|
+
// seal-in SR, crossing SR.Q→OR and OR→SR). Move the input's SOURCE Y to the near outer edge of that
|
|
2295
|
+
// shadow so a clean gate-clear H–V–H enters the fixed port from the correct side. Because only this
|
|
2296
|
+
// one wire moves, its own crossing count (wireCross, O(N)) is the exact whole-diagram delta; the move
|
|
2297
|
+
// is kept only if that STRICTLY drops and the route validates (gate clearance, separation contract,
|
|
2298
|
+
// sibling-input spacing, legal dogleg), reverted otherwise. So it can only ever remove a wrap
|
|
2299
|
+
// crossing and never introduces a crossing, crowd, overlap, or sub-min dogleg.
|
|
2300
|
+
{
|
|
2301
|
+
const fanout = new Map();
|
|
2302
|
+
for (const w of wires)
|
|
2303
|
+
if (!w.feedback)
|
|
2304
|
+
fanout.set(w.fromId, (fanout.get(w.fromId) ?? 0) + 1);
|
|
2305
|
+
for (const S of layoutNodes) {
|
|
2306
|
+
if (S.gateType !== 'INPUT' || !S.outputs[0])
|
|
2307
|
+
continue;
|
|
2308
|
+
if ((fanout.get(S.id) ?? 0) !== 1)
|
|
2309
|
+
continue; // single-consumer input only
|
|
2310
|
+
const w = wires.find(x => x.fromId === S.id && !x.feedback);
|
|
2311
|
+
if (!w || w.points.length < 4)
|
|
2312
|
+
continue; // a straight/one-bend input is already clean
|
|
2313
|
+
const before = wireCross(w);
|
|
2314
|
+
if (before === 0)
|
|
2315
|
+
continue; // only act on an input wire that crosses
|
|
2316
|
+
const dest = nodeMap.get(w.toId);
|
|
2317
|
+
if (!dest || dest.gateType === 'INPUT' || dest.gateType === 'OUTPUT')
|
|
2318
|
+
continue;
|
|
2319
|
+
const sx = w.points[0].x, port = lastPt(w), px = port.x, py = port.y;
|
|
2320
|
+
const sibClear = (sy) => layoutNodes.every(s => s === S || s.gateType !== 'INPUT' ||
|
|
2321
|
+
s.absX !== S.absX || Math.abs((s.absY + s.height / 2) - sy) >= MIN_PORT_GAP - 0.5);
|
|
2322
|
+
const savedPts = w.points, savedSY = S.absY, savedOutY = S.outputs[0].absY;
|
|
2323
|
+
// Search source Ys nearest the port outward; keep the FEWEST-crossing clean route (a partial
|
|
2324
|
+
// un-wrap that still clips a sibling wire, e.g. lands on the block's other input, is not the goal —
|
|
2325
|
+
// the fully-clear position just past that wire is). Only touch `w`, so wireCross is the exact delta.
|
|
2326
|
+
let best = null;
|
|
2327
|
+
let done = false;
|
|
2328
|
+
for (let d = GRID; d <= 500 && !done; d += GRID) { // source Y nearest the port first, above or below
|
|
2329
|
+
for (const sy of [py - d, py + d]) {
|
|
2330
|
+
if (sy < 0 || !sibClear(sy))
|
|
2331
|
+
continue;
|
|
2332
|
+
const jog = Math.abs(sy - py);
|
|
2333
|
+
if (jog >= 0.5 && jog < MIN_DOGLEG)
|
|
2334
|
+
continue; // would be a sub-min dogleg
|
|
2335
|
+
for (let tapX = Math.round((px - GATE_ENTRANCE) / GRID) * GRID; tapX > sx + MIN_DOGLEG; tapX -= GRID) {
|
|
2336
|
+
const route = [{ x: sx, y: sy }, { x: tapX, y: sy }, { x: tapX, y: py }, { x: px, y: py }]
|
|
2337
|
+
.filter((p, i, a) => i === 0 || Math.abs(p.x - a[i - 1].x) >= 0.5 || Math.abs(p.y - a[i - 1].y) >= 0.5);
|
|
2338
|
+
const skip = (o) => o === w || o.fromId === w.fromId;
|
|
2339
|
+
if (!vGateClear(tapX, sy, py) || !hGateClear(sy, sx, tapX, w.fromId) ||
|
|
2340
|
+
!hGateClear(py, tapX, px, w.toId) || !wireClear(route, skip))
|
|
2341
|
+
continue;
|
|
2342
|
+
w.points = route;
|
|
2343
|
+
const c = wireCross(w);
|
|
2344
|
+
w.points = savedPts;
|
|
2345
|
+
if (c < (best?.cross ?? before))
|
|
2346
|
+
best = { pts: route, sy, cross: c };
|
|
2347
|
+
if (c === 0) {
|
|
2348
|
+
done = true;
|
|
2349
|
+
break;
|
|
2350
|
+
} // fully clean — take it (nearest wins)
|
|
2351
|
+
}
|
|
2352
|
+
if (done)
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
if (best && best.cross < before) {
|
|
2357
|
+
w.points = best.pts;
|
|
2358
|
+
S.outputs[0].absY = best.sy;
|
|
2359
|
+
S.absY = best.sy - S.height / 2;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
// Straighten gratuitous sub-MIN_DOGLEG jogs: a small vertical step between two horizontal runs
|
|
2364
|
+
// is collapsed when the far run can slide onto the near run's Y with the span clear of gate
|
|
2365
|
+
// bodies (the obstacle-aware placement lanes can leave such a jog where the route had room to
|
|
2366
|
+
// run straight). Conservative: only when gate-clear, validated against the dogleg invariant.
|
|
2367
|
+
for (const w of wires) {
|
|
2368
|
+
if (w.feedback)
|
|
2369
|
+
continue;
|
|
2370
|
+
let changed = true;
|
|
2371
|
+
while (changed) {
|
|
2372
|
+
changed = false;
|
|
2373
|
+
const p = w.points;
|
|
2374
|
+
for (let k = 1; k + 3 < p.length; k++) { // need run B AND a segment after it (so p[k+2] is never the terminal port)
|
|
2375
|
+
if (Math.abs(p[k].x - p[k + 1].x) > 0.5)
|
|
2376
|
+
continue; // segment k vertical
|
|
2377
|
+
const len = Math.abs(p[k].y - p[k + 1].y);
|
|
2378
|
+
if (len < 0.5 || len >= MIN_DOGLEG)
|
|
2379
|
+
continue; // small jog only
|
|
2380
|
+
if (Math.abs(p[k - 1].y - p[k].y) > 0.5)
|
|
2381
|
+
continue; // run A horizontal
|
|
2382
|
+
if (Math.abs(p[k + 1].y - p[k + 2].y) > 0.5)
|
|
2383
|
+
continue; // run B horizontal
|
|
2384
|
+
const yA = p[k].y;
|
|
2385
|
+
const spanClear = hGateClear(yA, p[k + 1].x, p[k + 2].x, w.toId) && hGateClear(yA, p[k + 1].x, p[k + 2].x, w.fromId);
|
|
2386
|
+
const nextOk = vGateClear(p[k + 2].x, yA, p[k + 3].y, w.toId); // skip the destination gate: a port approach is meant to be near it
|
|
2387
|
+
if (!spanClear || !nextOk)
|
|
2388
|
+
continue;
|
|
2389
|
+
p[k + 1].y = yA;
|
|
2390
|
+
p[k + 2].y = yA; // slide run B onto run A's Y
|
|
2391
|
+
w.points = p.filter((pt, i) => i === 0 || Math.abs(pt.x - p[i - 1].x) >= 0.5 || Math.abs(pt.y - p[i - 1].y) >= 0.5);
|
|
2392
|
+
changed = true;
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
// Feedback (loop-back) wires. A source that is an output node feeds back into the logic
|
|
2398
|
+
// (e.g. a seal-in latch `Q = SET OR (Q AND NOT RESET)`). It cannot flow left-to-right, so it
|
|
2399
|
+
// is routed by the obstacle-aware A* router from the output's signal line back to the
|
|
2400
|
+
// consuming gate's input port — finding a local loop around the gates rather than a fixed
|
|
2401
|
+
// full-width lane (which looks bad when the consumer is far from the output).
|
|
2402
|
+
for (const [groupKey, dests] of fanOutGroups) {
|
|
2403
|
+
const fromId = groupKey.slice(0, groupKey.lastIndexOf('::'));
|
|
2404
|
+
const outNode = nodeMap.get(fromId);
|
|
2405
|
+
if (!outNode || outNode.gateType !== 'OUTPUT')
|
|
2406
|
+
continue; // feedback only
|
|
2407
|
+
// Share the output's driving signal: tap at the gate that drives the output (its output
|
|
2408
|
+
// port), so the feedback fans out from the same point as the output label rather than
|
|
2409
|
+
// retracing the driver→output wire. Same fromId => they share the trunk / a junction dot.
|
|
2410
|
+
const driverId = nodes.get(fromId)?.inputIds[0];
|
|
2411
|
+
const driver = driverId ? nodeMap.get(driverId) : undefined;
|
|
2412
|
+
const tapPort = driver?.outputs[0] ?? outNode.inputs[0];
|
|
2413
|
+
const tapFrom = driver ? driverId : fromId;
|
|
2414
|
+
if (!tapPort)
|
|
2415
|
+
continue;
|
|
2416
|
+
void driver;
|
|
2417
|
+
// Keep the loop-back a clear distance (3x the wire gap) off the driver output and the
|
|
2418
|
+
// consumer input. The A* runs to a waypoint on the SAME side as the consumer's feedback
|
|
2419
|
+
// port (under a bottom port, over a top port) so the loop approaches the port from the
|
|
2420
|
+
// correct side without crossing the consumer's other input wire. Zero-size endpoint boxes
|
|
2421
|
+
// keep the driver and consumer as regular obstacles (full clearance, never hugged).
|
|
2422
|
+
const clearance = Math.round((3 * MIN_WIRE_SPACING) / GRID) * GRID;
|
|
2423
|
+
for (const dest of dests) {
|
|
2424
|
+
const port = dest.toPort;
|
|
2425
|
+
const g = dest.toLayoutNode;
|
|
2426
|
+
const startX = tapPort.absX + clearance;
|
|
2427
|
+
const endX = port.absX - clearance;
|
|
2428
|
+
const bottomPort = port.absY > g.absY + g.height / 2;
|
|
2429
|
+
const laneY = bottomPort ? g.absY + g.height + clearance : g.absY - clearance;
|
|
2430
|
+
const mid = routeWireAStar(startX, tapPort.absY, endX, laneY, allObstacles, startX, tapPort.absY, 0, 0, endX, laneY, 0, 0, false, routedSegments, canvasW, canvasH, tapFrom);
|
|
2431
|
+
const pts = [
|
|
2432
|
+
{ x: tapPort.absX, y: tapPort.absY }, ...mid,
|
|
2433
|
+
{ x: endX, y: port.absY }, { x: port.absX, y: port.absY },
|
|
2434
|
+
];
|
|
2435
|
+
const clean = [];
|
|
2436
|
+
for (const p of pts) {
|
|
2437
|
+
const last = clean[clean.length - 1];
|
|
2438
|
+
if (last && Math.abs(last.x - p.x) < 0.5 && Math.abs(last.y - p.y) < 0.5)
|
|
2439
|
+
continue;
|
|
2440
|
+
if (clean.length >= 2) {
|
|
2441
|
+
const a = clean[clean.length - 2], b = clean[clean.length - 1];
|
|
2442
|
+
const colinH = Math.abs(a.y - b.y) < 0.5 && Math.abs(b.y - p.y) < 0.5;
|
|
2443
|
+
const colinV = Math.abs(a.x - b.x) < 0.5 && Math.abs(b.x - p.x) < 0.5;
|
|
2444
|
+
if (colinH || colinV)
|
|
2445
|
+
clean.pop();
|
|
2446
|
+
}
|
|
2447
|
+
clean.push(p);
|
|
2448
|
+
}
|
|
2449
|
+
wires.push({ id: uid('wire'), points: clean, fromId: tapFrom, toId: dest.toId, feedback: true });
|
|
2450
|
+
routedSegments.push({ points: clean, fromId: tapFrom });
|
|
2451
|
+
addJunction(tapPort.absX, tapPort.absY);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
// Shared fan-out trunk: when one source feeds several destinations and two of its wires
|
|
2455
|
+
// turn vertically at nearly the same X, snap them to a single shared channel. Same-source
|
|
2456
|
+
// overlap is intentional (it reads as one trunk) and it collapses the near-duplicate
|
|
2457
|
+
// junction dots into one clean T-tap. Snapping toward the gate-most X only shortens the
|
|
2458
|
+
// peel-off horizontals, so it cannot introduce a backtrack.
|
|
2459
|
+
{
|
|
2460
|
+
const bySource = new Map();
|
|
2461
|
+
for (const w of wires) {
|
|
2462
|
+
if (w.points.length !== 4)
|
|
2463
|
+
continue;
|
|
2464
|
+
if (Math.abs(w.points[1].x - w.points[2].x) >= 1)
|
|
2465
|
+
continue; // middle segment vertical
|
|
2466
|
+
if (Math.abs(w.points[0].y - w.points[1].y) >= 1)
|
|
2467
|
+
continue; // exits horizontally
|
|
2468
|
+
const arr = bySource.get(w.fromId) ?? [];
|
|
2469
|
+
arr.push({ w, x: w.points[1].x });
|
|
2470
|
+
bySource.set(w.fromId, arr);
|
|
2471
|
+
}
|
|
2472
|
+
// The relocated branch (vertical at sharedX + its peel-off horizontal) must satisfy the shared
|
|
2473
|
+
// separation contract. Same-source wires are skipped — the whole point is that they merge into
|
|
2474
|
+
// one trunk. Crossovers with other nets are fine; only sub-MIN parallel crowding is rejected.
|
|
2475
|
+
// segCrowds/wireClear DELIBERATELY ignore exact-collinear overlap (deferring to the "overlap
|
|
2476
|
+
// check elsewhere" — the no-parallel-overlap invariant), so this pass must ALSO reject a move
|
|
2477
|
+
// that lands a segment exactly on a CROSS-NET parallel run (overlap >= GRID) — otherwise snapping
|
|
2478
|
+
// a branch to the gate-most X can drop it right on top of another net's channel. Its sibling
|
|
2479
|
+
// passes (fan-in nesting, the entrance pass) carry this same collinear guard; without it the
|
|
2480
|
+
// trunk merge can silently undo the track pass's overlap-avoiding placement.
|
|
2481
|
+
const collinearHitsOtherNet = (pts, selfFrom) => {
|
|
2482
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
2483
|
+
const a = pts[i], b = pts[i + 1];
|
|
2484
|
+
const horiz = Math.abs(a.y - b.y) < 0.5;
|
|
2485
|
+
if (!horiz && Math.abs(a.x - b.x) >= 0.5)
|
|
2486
|
+
continue; // only axis-aligned segments
|
|
2487
|
+
const perp = horiz ? a.y : a.x;
|
|
2488
|
+
const s0 = horiz ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
2489
|
+
const s1 = horiz ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
2490
|
+
for (const w of wires) {
|
|
2491
|
+
if (w.fromId === selfFrom)
|
|
2492
|
+
continue; // same source shares the trunk (intentional)
|
|
2493
|
+
for (let j = 0; j < w.points.length - 1; j++) {
|
|
2494
|
+
const c = w.points[j], d = w.points[j + 1];
|
|
2495
|
+
const oHoriz = Math.abs(c.y - d.y) < 0.5;
|
|
2496
|
+
if (oHoriz !== horiz || (!oHoriz && Math.abs(c.x - d.x) >= 0.5) || (oHoriz && Math.abs(c.x - d.x) < 0.5))
|
|
2497
|
+
continue;
|
|
2498
|
+
if (Math.abs((oHoriz ? c.y : c.x) - perp) >= 0.5)
|
|
2499
|
+
continue; // not collinear
|
|
2500
|
+
const o0 = oHoriz ? Math.min(c.x, d.x) : Math.min(c.y, d.y);
|
|
2501
|
+
const o1 = oHoriz ? Math.max(c.x, d.x) : Math.max(c.y, d.y);
|
|
2502
|
+
if (Math.min(s1, o1) - Math.max(s0, o0) >= GRID)
|
|
2503
|
+
return true; // collinear overlap
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
return false;
|
|
2508
|
+
};
|
|
2509
|
+
const moveClear = (self, sharedX) => {
|
|
2510
|
+
const moved = [self.points[0], { x: sharedX, y: self.points[1].y }, { x: sharedX, y: self.points[2].y }, self.points[3]];
|
|
2511
|
+
return wireClear(moved, o => o.fromId === self.fromId) && !collinearHitsOtherNet(moved, self.fromId);
|
|
2512
|
+
};
|
|
2513
|
+
for (const group of bySource.values()) {
|
|
2514
|
+
if (group.length < 2)
|
|
2515
|
+
continue;
|
|
2516
|
+
group.sort((a, b) => a.x - b.x);
|
|
2517
|
+
let start = 0;
|
|
2518
|
+
for (let i = 1; i <= group.length; i++) {
|
|
2519
|
+
if (i === group.length || group[i].x - group[i - 1].x > FANIN_SPACING) {
|
|
2520
|
+
if (i - start >= 2) {
|
|
2521
|
+
const sharedX = group[i - 1].x; // gate-most X in the cluster
|
|
2522
|
+
if (group.slice(start, i).every(g => moveClear(g.w, sharedX))) {
|
|
2523
|
+
for (let k = start; k < i; k++) {
|
|
2524
|
+
group[k].w.points[1].x = sharedX;
|
|
2525
|
+
group[k].w.points[2].x = sharedX;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
start = i;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
// ── Gate-entrance contract (single guarantee) ────────────────────────────────────────────────
|
|
2535
|
+
// Every wire entering a gate input port MUST arrive with a horizontal approach of at least
|
|
2536
|
+
// GATE_ENTRANCE, so its final turn sits clear of the gate body/curve and the wire visibly enters
|
|
2537
|
+
// horizontally instead of turning on the gate's edge. The reshaping passes above (channel tracks,
|
|
2538
|
+
// fan-in nesting) satisfy this only as a SIDE EFFECT and each has an escape hatch that can leave a
|
|
2539
|
+
// gate-hugging entrance behind — the track pass skips multi-bend wires by design, and nesting is
|
|
2540
|
+
// all-or-nothing (it reverts a whole group to raw A* geometry when one channel won't fit). So a
|
|
2541
|
+
// multi-bend A* route whose final vertical lands a few px off the gate was fixed by NEITHER pass.
|
|
2542
|
+
// This is the ONE place that GUARANTEES the contract for EVERY gate-input wire regardless of how it
|
|
2543
|
+
// was routed, backed by an invariant (invariants.spec: "gate-input wires keep the GATE_ENTRANCE
|
|
2544
|
+
// approach"). It only pulls a turn back (never toward the gate) and validates every move through
|
|
2545
|
+
// the shared separation contract (wireClear), gate clearance, and a no-new-crossing check, so it
|
|
2546
|
+
// can never introduce a crowd, a hug, or a crossing; a move that can't be made cleanly is left as
|
|
2547
|
+
// routed (genuine congestion, which the invariant then surfaces rather than hiding).
|
|
2548
|
+
{
|
|
2549
|
+
const hvHit = (h1, h2, v1, v2) => Math.abs(h1.y - h2.y) < 1 && Math.abs(v1.x - v2.x) < 1 &&
|
|
2550
|
+
h1.y > Math.min(v1.y, v2.y) - 1 && h1.y < Math.max(v1.y, v2.y) + 1 &&
|
|
2551
|
+
v1.x > Math.min(h1.x, h2.x) - 1 && v1.x < Math.max(h1.x, h2.x) + 1;
|
|
2552
|
+
const wireCrosses = (self) => {
|
|
2553
|
+
let c = 0;
|
|
2554
|
+
for (const o of wires) {
|
|
2555
|
+
if (o === self || o.fromId === self.fromId || o.feedback)
|
|
2556
|
+
continue;
|
|
2557
|
+
for (let i = 0; i < self.points.length - 1; i++)
|
|
2558
|
+
for (let j = 0; j < o.points.length - 1; j++) {
|
|
2559
|
+
const p1 = self.points[i], p2 = self.points[i + 1], q1 = o.points[j], q2 = o.points[j + 1];
|
|
2560
|
+
if (hvHit(p1, p2, q1, q2) || hvHit(q1, q2, p1, p2))
|
|
2561
|
+
c++;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return c;
|
|
2565
|
+
};
|
|
2566
|
+
// Collinear-overlap check: segCrowds (the spacing contract) deliberately ignores segments at the
|
|
2567
|
+
// SAME perpendicular coordinate ("overlap check elsewhere" — the no-parallel-overlap invariant).
|
|
2568
|
+
// The entrance pass must honour that half too, or a straightened/pulled-back segment could land
|
|
2569
|
+
// exactly on another net's parallel run. Rejects a proposed geometry whose axis-aligned segment
|
|
2570
|
+
// overlaps a cross-net parallel segment at the same coordinate by >= GRID.
|
|
2571
|
+
const overlapsCollinear = (pts, skip) => {
|
|
2572
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
2573
|
+
const a = pts[i], b = pts[i + 1];
|
|
2574
|
+
const horiz = Math.abs(a.y - b.y) < 0.5;
|
|
2575
|
+
if (!horiz && Math.abs(a.x - b.x) >= 0.5)
|
|
2576
|
+
continue;
|
|
2577
|
+
const perp = horiz ? a.y : a.x, mn = horiz ? Math.min(a.x, b.x) : Math.min(a.y, b.y), mx = horiz ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
2578
|
+
for (const o of wires) {
|
|
2579
|
+
if (skip(o))
|
|
2580
|
+
continue;
|
|
2581
|
+
for (let k = 0; k < o.points.length - 1; k++) {
|
|
2582
|
+
const c = o.points[k], d = o.points[k + 1];
|
|
2583
|
+
const oh = Math.abs(c.y - d.y) < 0.5;
|
|
2584
|
+
if (oh !== horiz || (!oh && Math.abs(c.x - d.x) >= 0.5) || (oh && Math.abs(c.x - d.x) < 0.5))
|
|
2585
|
+
continue;
|
|
2586
|
+
if (Math.abs((oh ? c.y : c.x) - perp) >= 0.5)
|
|
2587
|
+
continue; // only exact-collinear runs
|
|
2588
|
+
const omn = oh ? Math.min(c.x, d.x) : Math.min(c.y, d.y), omx = oh ? Math.max(c.x, d.x) : Math.max(c.y, d.y);
|
|
2589
|
+
if (Math.min(mx, omx) - Math.max(mn, omn) >= GRID)
|
|
2590
|
+
return true;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
return false;
|
|
2595
|
+
};
|
|
2596
|
+
for (const w of wires) {
|
|
2597
|
+
if (w.feedback)
|
|
2598
|
+
continue;
|
|
2599
|
+
const p = w.points, n = p.length;
|
|
2600
|
+
if (n < 3)
|
|
2601
|
+
continue;
|
|
2602
|
+
const dest = nodeMap.get(w.toId);
|
|
2603
|
+
if (!dest || dest.gateType === 'INPUT' || dest.gateType === 'OUTPUT')
|
|
2604
|
+
continue; // gate inputs only
|
|
2605
|
+
const port = p[n - 1];
|
|
2606
|
+
if (Math.abs(p[n - 2].y - port.y) >= 0.5)
|
|
2607
|
+
continue; // enters horizontally (invariant)
|
|
2608
|
+
if (Math.abs(port.x - p[n - 2].x) >= GATE_ENTRANCE - 0.5)
|
|
2609
|
+
continue; // already clear
|
|
2610
|
+
const before = wireCrosses(w);
|
|
2611
|
+
const skip = (o) => o.id === w.id || o.fromId === w.fromId;
|
|
2612
|
+
// (1) Straight-first: collinear source & port whose A* route wandered — one straight segment
|
|
2613
|
+
// removes the wander AND the hug at once, if the straight path is clean.
|
|
2614
|
+
if (Math.abs(p[0].y - port.y) < 0.5 && port.x - p[0].x >= GATE_ENTRANCE - 0.5) {
|
|
2615
|
+
const straight = [{ x: p[0].x, y: port.y }, { x: port.x, y: port.y }];
|
|
2616
|
+
if (hGateClear(port.y, p[0].x, port.x, w.fromId) && hGateClear(port.y, p[0].x, port.x, w.toId) &&
|
|
2617
|
+
wireClear(straight, skip) && !overlapsCollinear(straight, skip)) {
|
|
2618
|
+
const saved = w.points;
|
|
2619
|
+
w.points = straight;
|
|
2620
|
+
if (wireCrosses(w) <= before)
|
|
2621
|
+
continue; // accept
|
|
2622
|
+
w.points = saved; // else revert, try (2)
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
// (2) Pull the tail vertical back to the gate-most clear track <= port.x - GATE_ENTRANCE. Needs
|
|
2626
|
+
// a movable tail vertical (>= 4 points, so it isn't the segment anchored at the source).
|
|
2627
|
+
if (n < 4)
|
|
2628
|
+
continue;
|
|
2629
|
+
const tv = n - 3; // tail vertical p[tv]->p[tv+1]
|
|
2630
|
+
if (Math.abs(p[tv].x - p[tv + 1].x) >= 0.5)
|
|
2631
|
+
continue; // must be vertical
|
|
2632
|
+
if (Math.abs(p[tv - 1].y - p[tv].y) >= 0.5)
|
|
2633
|
+
continue; // preceded by a horizontal run
|
|
2634
|
+
const preStartX = p[tv - 1].x, preY = p[tv].y, portY = port.y;
|
|
2635
|
+
const saved = p.map(pt => ({ x: pt.x, y: pt.y }));
|
|
2636
|
+
let placed = false;
|
|
2637
|
+
const hi = Math.floor((port.x - GATE_ENTRANCE) / GRID) * GRID;
|
|
2638
|
+
for (let x = hi; x > preStartX + 0.5; x -= GRID) { // leftward, keep pre-horizontal non-degenerate
|
|
2639
|
+
p[tv].x = x;
|
|
2640
|
+
p[tv + 1].x = x;
|
|
2641
|
+
const seg = [{ x: preStartX, y: preY }, { x, y: preY }, { x, y: portY }, { x: port.x, y: portY }];
|
|
2642
|
+
if (hGateClear(preY, preStartX, x, w.fromId) && vGateClear(x, preY, portY) &&
|
|
2643
|
+
hGateClear(portY, x, port.x, w.toId) && wireClear(seg, skip) &&
|
|
2644
|
+
!overlapsCollinear(seg, skip) && wireCrosses(w) <= before) {
|
|
2645
|
+
placed = true;
|
|
2646
|
+
break;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
if (!placed)
|
|
2650
|
+
for (let i = 0; i < n; i++) {
|
|
2651
|
+
p[i].x = saved[i].x;
|
|
2652
|
+
p[i].y = saved[i].y;
|
|
2653
|
+
} // revert
|
|
2654
|
+
}
|
|
2655
|
+
// Joint fallback: a wire can still hug a gate when a SIBLING fan-in channel occupies the only
|
|
2656
|
+
// clean track (e.g. three inputs converging where the gate-clear boundary, one input's channel,
|
|
2657
|
+
// and an unrelated trunk-end all coincide). Re-pack ALL of that gate's incoming tail verticals
|
|
2658
|
+
// onto distinct tracks stepping left from the gate-clear boundary, giving the gate-most track to
|
|
2659
|
+
// the LEAST-deflected (straightest) wire so no crossing is introduced. All-or-nothing and fully
|
|
2660
|
+
// validated (entrance + separation + gate clearance + no net new crossings): committed only if
|
|
2661
|
+
// every incoming wire then clears, else the whole gate reverts — so it can only resolve a hug.
|
|
2662
|
+
const countCross = () => {
|
|
2663
|
+
let c = 0;
|
|
2664
|
+
for (let i = 0; i < wires.length; i++)
|
|
2665
|
+
for (let j = i + 1; j < wires.length; j++) {
|
|
2666
|
+
const a = wires[i], b = wires[j];
|
|
2667
|
+
if (a.fromId === b.fromId || a.feedback || b.feedback)
|
|
2668
|
+
continue;
|
|
2669
|
+
for (let si = 0; si < a.points.length - 1; si++)
|
|
2670
|
+
for (let sj = 0; sj < b.points.length - 1; sj++)
|
|
2671
|
+
if (hvHit(a.points[si], a.points[si + 1], b.points[sj], b.points[sj + 1]) ||
|
|
2672
|
+
hvHit(b.points[sj], b.points[sj + 1], a.points[si], a.points[si + 1]))
|
|
2673
|
+
c++;
|
|
2674
|
+
}
|
|
2675
|
+
return c;
|
|
2676
|
+
};
|
|
2677
|
+
const violates = (w) => {
|
|
2678
|
+
const p = w.points, port = p[p.length - 1];
|
|
2679
|
+
return p.length >= 4 && Math.abs(p[p.length - 2].y - port.y) < 0.5 &&
|
|
2680
|
+
Math.abs(port.x - p[p.length - 2].x) < GATE_ENTRANCE - 0.5;
|
|
2681
|
+
};
|
|
2682
|
+
for (const gate of layoutNodes) {
|
|
2683
|
+
if (gate.gateType === 'INPUT' || gate.gateType === 'OUTPUT')
|
|
2684
|
+
continue;
|
|
2685
|
+
const incoming = wires.filter(w => !w.feedback && w.toId === gate.id && w.points.length >= 4);
|
|
2686
|
+
if (incoming.length < 2 || !incoming.some(violates))
|
|
2687
|
+
continue;
|
|
2688
|
+
// Each incoming wire must expose a movable tail vertical (clean H–V–H tail).
|
|
2689
|
+
const items = [];
|
|
2690
|
+
let shapesOk = true;
|
|
2691
|
+
for (const w of incoming) {
|
|
2692
|
+
const p = w.points, m = p.length, port = p[m - 1], tv = m - 3;
|
|
2693
|
+
if (Math.abs(p[m - 2].y - port.y) >= 0.5 || Math.abs(p[tv].x - p[tv + 1].x) >= 0.5 || Math.abs(p[tv - 1].y - p[tv].y) >= 0.5) {
|
|
2694
|
+
shapesOk = false;
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
items.push({ w, tv, preStartX: p[tv - 1].x, preY: p[tv].y, portY: port.y, portX: port.x, deflect: Math.abs(p[0].y - port.y) });
|
|
2698
|
+
}
|
|
2699
|
+
if (!shapesOk)
|
|
2700
|
+
continue;
|
|
2701
|
+
items.sort((a, b) => a.deflect - b.deflect); // straightest first → gate-most track
|
|
2702
|
+
const boundary = Math.floor((gate.absX - GATE_CLEARANCE) / GRID) * GRID;
|
|
2703
|
+
const crossBefore = countCross();
|
|
2704
|
+
const saved = items.map(it => it.w.points.map(pt => ({ x: pt.x, y: pt.y })));
|
|
2705
|
+
const used = [];
|
|
2706
|
+
let good = true;
|
|
2707
|
+
for (const it of items) {
|
|
2708
|
+
const skip = (o) => o.id === it.w.id || o.fromId === it.w.fromId;
|
|
2709
|
+
let placedX = null;
|
|
2710
|
+
const top = Math.min(boundary, Math.floor((it.portX - GATE_ENTRANCE) / GRID) * GRID);
|
|
2711
|
+
for (let x = top; x > it.preStartX + 0.5; x -= GRID) {
|
|
2712
|
+
if (used.some(u => Math.abs(u - x) < MIN_WIRE_SPACING - 0.5))
|
|
2713
|
+
continue;
|
|
2714
|
+
it.w.points[it.tv].x = x;
|
|
2715
|
+
it.w.points[it.tv + 1].x = x;
|
|
2716
|
+
const seg = [{ x: it.preStartX, y: it.preY }, { x, y: it.preY }, { x, y: it.portY }, { x: it.portX, y: it.portY }];
|
|
2717
|
+
if (hGateClear(it.preY, it.preStartX, x, it.w.fromId) && vGateClear(x, it.preY, it.portY) &&
|
|
2718
|
+
hGateClear(it.portY, x, it.portX, it.w.toId) && wireClear(seg, skip) && !overlapsCollinear(seg, skip)) {
|
|
2719
|
+
placedX = x;
|
|
2720
|
+
break;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
if (placedX === null) {
|
|
2724
|
+
good = false;
|
|
2725
|
+
break;
|
|
2726
|
+
}
|
|
2727
|
+
used.push(placedX);
|
|
2728
|
+
}
|
|
2729
|
+
if (good && countCross() > crossBefore)
|
|
2730
|
+
good = false; // never add a crossing
|
|
2731
|
+
if (!good)
|
|
2732
|
+
items.forEach((it, i) => { it.w.points = saved[i]; }); // revert the whole gate
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
// Snap an output to its incoming wire's approach Y. A wire that had to clear a gate body
|
|
2736
|
+
// (vertical clearance) can arrive a few px off its output port, leaving a small terminal
|
|
2737
|
+
// jog. Since an output is a sink, just move it to where the wire arrives — eliminating the
|
|
2738
|
+
// jog — provided it stays clear of its sibling outputs in the same column.
|
|
2739
|
+
for (const o of layoutNodes) {
|
|
2740
|
+
if (o.gateType !== 'OUTPUT' || !o.inputs[0])
|
|
2741
|
+
continue;
|
|
2742
|
+
const w = wires.find(x => x.toId === o.id && !x.feedback);
|
|
2743
|
+
if (!w || w.points.length < 3)
|
|
2744
|
+
continue;
|
|
2745
|
+
const p = w.points;
|
|
2746
|
+
// Find a small vertical jog (< MIN_DOGLEG) in the last two segments — whether the wire
|
|
2747
|
+
// enters with the jog as its final segment, or jogs then runs a short horizontal into the
|
|
2748
|
+
// port. The "run" Y on the far side is where the wire actually travels; move the output
|
|
2749
|
+
// there (and collapse the jog), as long as it stays clear of its sibling outputs.
|
|
2750
|
+
let k = -1;
|
|
2751
|
+
for (let s = p.length - 2; s >= Math.max(0, p.length - 3); s--) {
|
|
2752
|
+
const a = p[s], b = p[s + 1];
|
|
2753
|
+
if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) >= 0.5 && Math.abs(a.y - b.y) < MIN_DOGLEG) {
|
|
2754
|
+
k = s;
|
|
2755
|
+
break;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
if (k < 0)
|
|
2759
|
+
continue;
|
|
2760
|
+
const newY = Math.round(p[k].y / GRID) * GRID; // the run Y (far side of the jog)
|
|
2761
|
+
const clash = layoutNodes.some(s => s !== o && s.gateType === 'OUTPUT' && s.absX === o.absX &&
|
|
2762
|
+
Math.abs((s.absY + s.height / 2) - newY) < MIN_PORT_GAP - 0.5);
|
|
2763
|
+
if (clash)
|
|
2764
|
+
continue;
|
|
2765
|
+
o.inputs[0].absY = newY;
|
|
2766
|
+
o.absY = Math.round((newY - o.height / 2) / GRID) * GRID;
|
|
2767
|
+
for (let m = k + 1; m < p.length; m++)
|
|
2768
|
+
p[m].y = newY; // collapse the jog onto the run
|
|
2769
|
+
w.points = p.filter((pt, idx) => idx === 0 ||
|
|
2770
|
+
Math.abs(pt.x - p[idx - 1].x) >= 0.5 || Math.abs(pt.y - p[idx - 1].y) >= 0.5);
|
|
2771
|
+
}
|
|
2772
|
+
// Final straightening for a block output port that feeds a single output node directly (no
|
|
2773
|
+
// gate between them, e.g. a generic FB): co-locate the port and the node and draw one straight
|
|
2774
|
+
// segment, so multi-output blocks never leave a residual jog after the earlier snap passes.
|
|
2775
|
+
for (const w of wires) {
|
|
2776
|
+
if (w.feedback || w.points.length < 2)
|
|
2777
|
+
continue;
|
|
2778
|
+
const src = nodeMap.get(w.fromId);
|
|
2779
|
+
const dst = nodeMap.get(w.toId);
|
|
2780
|
+
if (!src || src.blockType !== 'FB' || !dst || dst.gateType !== 'OUTPUT' || !dst.inputs[0])
|
|
2781
|
+
continue;
|
|
2782
|
+
const fn = nodes.get(dst.id);
|
|
2783
|
+
const sp = src.outputs.find(op => op.name === (fn?.inputPorts?.[0] ?? 'OUT'));
|
|
2784
|
+
if (!sp || Math.abs(sp.absX - w.points[0].x) > 0.5 || sp.absX >= dst.inputs[0].absX)
|
|
2785
|
+
continue;
|
|
2786
|
+
// Only safe to move the port if this is its sole wire (not shared with another consumer).
|
|
2787
|
+
if (wires.filter(o => o.fromId === w.fromId && Math.abs(o.points[0].y - sp.absY) < 0.5).length > 1)
|
|
2788
|
+
continue;
|
|
2789
|
+
const y = dst.inputs[0].absY;
|
|
2790
|
+
sp.absY = y;
|
|
2791
|
+
w.points = [{ x: sp.absX, y }, { x: dst.inputs[0].absX, y }];
|
|
2792
|
+
}
|
|
2793
|
+
// Junction dots mark where a NET actually branches — a point where its wires' segments leave in
|
|
2794
|
+
// three or more distinct directions (a T or a cross). A point where two same-source wires merely
|
|
2795
|
+
// bend together (only two directions, e.g. a shared trunk turning a corner) is NOT a branch and
|
|
2796
|
+
// gets no dot; a point where the trunk continues straight and one branch peels off (a T-tap) does.
|
|
2797
|
+
{
|
|
2798
|
+
const bySource = new Map();
|
|
2799
|
+
for (const w of wires) {
|
|
2800
|
+
const a = bySource.get(w.fromId);
|
|
2801
|
+
if (a)
|
|
2802
|
+
a.push(w);
|
|
2803
|
+
else
|
|
2804
|
+
bySource.set(w.fromId, [w]);
|
|
2805
|
+
}
|
|
2806
|
+
const dirFrom = (ax, ay, bx, by) => Math.abs(ax - bx) >= 0.5 ? (bx > ax ? 'R' : 'L') : (by > ay ? 'D' : 'U');
|
|
2807
|
+
for (const group of bySource.values()) {
|
|
2808
|
+
if (group.length < 2)
|
|
2809
|
+
continue; // a single wire never taps itself
|
|
2810
|
+
const pts = new Map(); // candidate points: every vertex in the net
|
|
2811
|
+
for (const w of group)
|
|
2812
|
+
for (const p of w.points)
|
|
2813
|
+
pts.set(`${Math.round(p.x)},${Math.round(p.y)}`, p);
|
|
2814
|
+
for (const { x: px, y: py } of pts.values()) {
|
|
2815
|
+
const set = new Set();
|
|
2816
|
+
for (const w of group) {
|
|
2817
|
+
const pp = w.points;
|
|
2818
|
+
for (let s = 0; s < pp.length - 1; s++) {
|
|
2819
|
+
const a = pp[s], b = pp[s + 1];
|
|
2820
|
+
const atA = Math.abs(a.x - px) < 1 && Math.abs(a.y - py) < 1;
|
|
2821
|
+
const atB = Math.abs(b.x - px) < 1 && Math.abs(b.y - py) < 1;
|
|
2822
|
+
if (atA)
|
|
2823
|
+
set.add(dirFrom(a.x, a.y, b.x, b.y)); // segment leaves pk toward b
|
|
2824
|
+
else if (atB)
|
|
2825
|
+
set.add(dirFrom(b.x, b.y, a.x, a.y)); // toward a
|
|
2826
|
+
else { // pk strictly interior → the run passes through both ways
|
|
2827
|
+
const horiz = Math.abs(a.y - b.y) < 0.5;
|
|
2828
|
+
const through = horiz
|
|
2829
|
+
? Math.abs(py - a.y) < 1 && px > Math.min(a.x, b.x) + 0.5 && px < Math.max(a.x, b.x) - 0.5
|
|
2830
|
+
: Math.abs(px - a.x) < 1 && py > Math.min(a.y, b.y) + 0.5 && py < Math.max(a.y, b.y) - 0.5;
|
|
2831
|
+
if (through) {
|
|
2832
|
+
set.add(dirFrom(px, py, a.x, a.y));
|
|
2833
|
+
set.add(dirFrom(px, py, b.x, b.y));
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (set.size >= 3)
|
|
2839
|
+
addJunction(px, py);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
// Node and port positions were already grid-snapped before routing. Snap only the
|
|
2844
|
+
// interior wire vertices to the grid here, leaving each wire's first/last point exact
|
|
2845
|
+
// so endpoints stay glued to their ports (notably OR inputs that tap the curve off-grid).
|
|
2846
|
+
for (const w of wires) {
|
|
2847
|
+
for (let i = 1; i < w.points.length - 1; i++) {
|
|
2848
|
+
w.points[i].x = Math.round(w.points[i].x / GRID) * GRID;
|
|
2849
|
+
w.points[i].y = Math.round(w.points[i].y / GRID) * GRID;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
for (const j of junctions) {
|
|
2853
|
+
j.x = Math.round(j.x / GRID) * GRID;
|
|
2854
|
+
j.y = Math.round(j.y / GRID) * GRID;
|
|
2855
|
+
}
|
|
2856
|
+
// Merge near-duplicate junction dots (e.g. two fan-out branches that peel off within a
|
|
2857
|
+
// few px of each other) so a split reads as one clean dot rather than a smudge.
|
|
2858
|
+
const MERGE_DIST = 8;
|
|
2859
|
+
const mergedJunctions = [];
|
|
2860
|
+
for (const j of junctions) {
|
|
2861
|
+
if (!mergedJunctions.some(m => Math.abs(m.x - j.x) <= MERGE_DIST && Math.abs(m.y - j.y) <= MERGE_DIST)) {
|
|
2862
|
+
mergedJunctions.push(j);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
// Post-routing net-label relocation. A consumed-intermediate label is placed just above its
|
|
2866
|
+
// driver's output BEFORE routing (and registered as a routing obstacle). That default spot can
|
|
2867
|
+
// still end up on a wire: a fan-out branch that turns UP to a consumer above the driver pierces
|
|
2868
|
+
// it, or a wide name reaches into the downstream block whose port the branch must enter (so the
|
|
2869
|
+
// router cannot avoid it). Wires are final here, and a label is a pure annotation, so relocating
|
|
2870
|
+
// it never disturbs a wire. For each label whose box overlaps a wire or a gate/block body, search
|
|
2871
|
+
// a grid of candidate boxes around its anchor and move it to the one with the fewest wire
|
|
2872
|
+
// crossings, then fewest body overlaps, then least displacement. Keep the current spot if nothing
|
|
2873
|
+
// scores better, so an already-clean label never moves.
|
|
2874
|
+
{
|
|
2875
|
+
const boxHitsWire = (x, y, w, h) => {
|
|
2876
|
+
let c = 0;
|
|
2877
|
+
for (const wire of wires)
|
|
2878
|
+
for (let i = 0; i < wire.points.length - 1; i++) {
|
|
2879
|
+
const a = wire.points[i], b = wire.points[i + 1];
|
|
2880
|
+
if (Math.max(a.x, b.x) > x + 0.5 && Math.min(a.x, b.x) < x + w - 0.5 &&
|
|
2881
|
+
Math.max(a.y, b.y) > y + 0.5 && Math.min(a.y, b.y) < y + h - 0.5)
|
|
2882
|
+
c++;
|
|
2883
|
+
}
|
|
2884
|
+
return c;
|
|
2885
|
+
};
|
|
2886
|
+
const boxHitsBody = (x, y, w, h) => {
|
|
2887
|
+
let c = 0;
|
|
2888
|
+
for (const n of layoutNodes) {
|
|
2889
|
+
if (n.gateType === 'INPUT' || n.gateType === 'OUTPUT')
|
|
2890
|
+
continue;
|
|
2891
|
+
if (x + w > n.absX + 0.5 && n.absX + n.width > x + 0.5 &&
|
|
2892
|
+
y + h > n.absY + 0.5 && n.absY + n.height > y + 0.5)
|
|
2893
|
+
c++;
|
|
2894
|
+
}
|
|
2895
|
+
return c;
|
|
2896
|
+
};
|
|
2897
|
+
const boxHitsLabel = (x, y, w, h, self) => {
|
|
2898
|
+
let c = 0;
|
|
2899
|
+
for (const o of labels) {
|
|
2900
|
+
if (o === self)
|
|
2901
|
+
continue;
|
|
2902
|
+
if (x + w > o.x + 0.5 && o.x + o.width > x + 0.5 && y + h > o.y + 0.5 && o.y + o.height > y + 0.5)
|
|
2903
|
+
c++;
|
|
2904
|
+
}
|
|
2905
|
+
return c;
|
|
2906
|
+
};
|
|
2907
|
+
// Nearest gap (0 if overlapping) between a label box and an axis-aligned wire segment, treating
|
|
2908
|
+
// the segment as its degenerate bounding rect.
|
|
2909
|
+
const boxSegDist = (x, y, w, h, a, b) => {
|
|
2910
|
+
const dx = Math.max(0, Math.max(x - Math.max(a.x, b.x), Math.min(a.x, b.x) - (x + w)));
|
|
2911
|
+
const dy = Math.max(0, Math.max(y - Math.max(a.y, b.y), Math.min(a.y, b.y) - (y + h)));
|
|
2912
|
+
return Math.hypot(dx, dy);
|
|
2913
|
+
};
|
|
2914
|
+
for (const lb of labels) {
|
|
2915
|
+
const { width: w, height: h, anchorX: ax, anchorY: ay } = lb;
|
|
2916
|
+
// The label NAMES its driver's output net, so it should sit RIGHT NEXT TO that net's wire.
|
|
2917
|
+
// Primary objective: clear of wires (the bug) and bodies; among clear spots, MINIMISE the
|
|
2918
|
+
// distance to the driver's own fan-out wire so the label hugs the signal it names (rather than
|
|
2919
|
+
// floating off to distant whitespace). A tiny bias keeps it above / on the output side only to
|
|
2920
|
+
// break ties between equally-close spots.
|
|
2921
|
+
// The net's full geometry: EVERY wire driven by this node (all fan-out branches share the
|
|
2922
|
+
// driver's id), so placement optimises against the whole net, not one branch. Its fan-out
|
|
2923
|
+
// JUNCTION dots (branch points that lie on those wires) are added as point-segments too — a
|
|
2924
|
+
// junction is the net's identity node and a natural leader attach point.
|
|
2925
|
+
const netWires = wires.filter(wr => wr.fromId === lb.driverId);
|
|
2926
|
+
const netSegs = [];
|
|
2927
|
+
for (const wr of netWires)
|
|
2928
|
+
for (let i = 0; i < wr.points.length - 1; i++)
|
|
2929
|
+
netSegs.push([wr.points[i], wr.points[i + 1]]);
|
|
2930
|
+
const onNet = (jx, jy) => netSegs.some(([a, b]) => Math.min(a.x, b.x) - 0.5 <= jx && jx <= Math.max(a.x, b.x) + 0.5 &&
|
|
2931
|
+
Math.min(a.y, b.y) - 0.5 <= jy && jy <= Math.max(a.y, b.y) + 0.5);
|
|
2932
|
+
for (const j of mergedJunctions)
|
|
2933
|
+
if (onNet(j.x, j.y))
|
|
2934
|
+
netSegs.push([{ x: j.x, y: j.y }, { x: j.x, y: j.y }]);
|
|
2935
|
+
const distToNet = (x, y) => {
|
|
2936
|
+
let d = Infinity;
|
|
2937
|
+
for (const [a, b] of netSegs)
|
|
2938
|
+
d = Math.min(d, boxSegDist(x, y, w, h, a, b));
|
|
2939
|
+
return Number.isFinite(d) ? d : Math.hypot(x - ax, y - ay);
|
|
2940
|
+
};
|
|
2941
|
+
// With a leader line the label should sit a readable distance OFF the net (so the connector is
|
|
2942
|
+
// visible and the text isn't jammed against the wire); without one, it should hug the net.
|
|
2943
|
+
const idealGap = opts.wireLabelLeader ? 16 : 0;
|
|
2944
|
+
const cost = (x, y) => {
|
|
2945
|
+
const tieBias = (x + w < ax - 0.5 ? 6 : 0) + (y > ay + 0.5 ? 3 : 0); // ≪ 1px of distance; ties only
|
|
2946
|
+
return boxHitsWire(x, y, w, h) * 100000 + boxHitsBody(x, y, w, h) * 3000 + boxHitsLabel(x, y, w, h, lb) * 2000 +
|
|
2947
|
+
Math.abs(distToNet(x, y) - idealGap) + tieBias;
|
|
2948
|
+
};
|
|
2949
|
+
let best = { x: lb.x, y: lb.y, c: cost(lb.x, lb.y) };
|
|
2950
|
+
const defaultClean = boxHitsWire(lb.x, lb.y, w, h) === 0 && boxHitsBody(lb.x, lb.y, w, h) === 0;
|
|
2951
|
+
// Relocate when the default overlaps, OR always under WIRE_LABEL_LEADER (to seat every label at
|
|
2952
|
+
// the readable gap its connector needs).
|
|
2953
|
+
if (!defaultClean || opts.wireLabelLeader) {
|
|
2954
|
+
// Dense grid around the anchor. Left edge sweeps from just left of the output to well PAST it:
|
|
2955
|
+
// the net is a shared fan-out, so a spot to the RIGHT of the junction (hugging a branch) is
|
|
2956
|
+
// valid too. distToNet then selects the clear spot nearest the net wire.
|
|
2957
|
+
for (let x = Math.round((ax - w - 20) / GRID) * GRID; x <= ax + 180; x += GRID) {
|
|
2958
|
+
for (let y = Math.round((ay - 200) / GRID) * GRID; y <= ay + 200; y += GRID) {
|
|
2959
|
+
if (x < 0 || y < 0)
|
|
2960
|
+
continue;
|
|
2961
|
+
const c = cost(x, y);
|
|
2962
|
+
if (c < best.c)
|
|
2963
|
+
best = { x, y, c };
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
lb.x = best.x;
|
|
2967
|
+
lb.y = best.y;
|
|
2968
|
+
}
|
|
2969
|
+
// Leader target: the point on the net (any branch OR a junction, via netSegs) nearest the
|
|
2970
|
+
// (final) label box, so an optional leader line (OPTION WIRE_LABEL_LEADER) connects the label to
|
|
2971
|
+
// the signal it names. Clamp the label's centre onto each net segment and keep the closest.
|
|
2972
|
+
const cx = lb.x + w / 2, cy = lb.y + h / 2;
|
|
2973
|
+
let bd = Infinity;
|
|
2974
|
+
for (const [a, b] of netSegs) {
|
|
2975
|
+
const px = Math.max(Math.min(a.x, b.x), Math.min(Math.max(a.x, b.x), cx));
|
|
2976
|
+
const py = Math.max(Math.min(a.y, b.y), Math.min(Math.max(a.y, b.y), cy));
|
|
2977
|
+
const d = Math.hypot(px - cx, py - cy);
|
|
2978
|
+
if (d < bd) {
|
|
2979
|
+
bd = d;
|
|
2980
|
+
lb.leaderX = px;
|
|
2981
|
+
lb.leaderY = py;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
// Re-normalise vertical position: the alignment/collision passes can drift content downward
|
|
2987
|
+
// from the assigned coordinates, leaving empty space at the top. Shift everything uniformly
|
|
2988
|
+
// (preserving every relative position and wire shape) so the topmost content sits at PAD_Y.
|
|
2989
|
+
{
|
|
2990
|
+
let minY = Infinity;
|
|
2991
|
+
for (const n of layoutNodes)
|
|
2992
|
+
minY = Math.min(minY, n.absY);
|
|
2993
|
+
for (const w of wires)
|
|
2994
|
+
for (const p of w.points)
|
|
2995
|
+
minY = Math.min(minY, p.y);
|
|
2996
|
+
for (const l of labels)
|
|
2997
|
+
minY = Math.min(minY, l.y); // a label may sit above the topmost node
|
|
2998
|
+
const dy = PAD_Y - minY;
|
|
2999
|
+
if (Number.isFinite(dy) && Math.abs(dy) >= GRID) {
|
|
3000
|
+
for (const n of layoutNodes) {
|
|
3001
|
+
n.absY += dy;
|
|
3002
|
+
for (const p of n.inputs)
|
|
3003
|
+
p.absY += dy;
|
|
3004
|
+
for (const p of n.outputs)
|
|
3005
|
+
p.absY += dy;
|
|
3006
|
+
}
|
|
3007
|
+
for (const w of wires)
|
|
3008
|
+
for (const p of w.points)
|
|
3009
|
+
p.y += dy;
|
|
3010
|
+
for (const j of mergedJunctions)
|
|
3011
|
+
j.y += dy;
|
|
3012
|
+
for (const l of labels) {
|
|
3013
|
+
l.y += dy;
|
|
3014
|
+
l.anchorY += dy;
|
|
3015
|
+
if (l.leaderY !== undefined)
|
|
3016
|
+
l.leaderY += dy;
|
|
3017
|
+
} // labels are anchored to gates — shift with them
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
// Collapse blank vertical bands between disconnected logic sections. A fully-empty horizontal band
|
|
3021
|
+
// (no node body, label, or wire — including verticals passing through) means the content above and
|
|
3022
|
+
// below it are not connected by any wire, so pulling the lower section up cannot distort a wire or
|
|
3023
|
+
// cause an overlap. Each such band wider than SECTION_GAP is reduced to SECTION_GAP, removing the
|
|
3024
|
+
// wasted space while keeping sections visually separated. Uniform per-section shift, so relative
|
|
3025
|
+
// geometry (and therefore every crossing/dogleg) is preserved.
|
|
3026
|
+
{
|
|
3027
|
+
const SECTION_GAP = 50;
|
|
3028
|
+
const occ = new Set();
|
|
3029
|
+
const mark = (y0, y1) => { for (let y = Math.floor(Math.min(y0, y1) / GRID) * GRID; y <= Math.max(y0, y1) + 0.5; y += GRID)
|
|
3030
|
+
occ.add(y); };
|
|
3031
|
+
for (const n of layoutNodes)
|
|
3032
|
+
mark(n.absY, n.absY + n.height);
|
|
3033
|
+
for (const l of labels)
|
|
3034
|
+
mark(l.y, l.y + l.height);
|
|
3035
|
+
for (const w of wires)
|
|
3036
|
+
for (let i = 0; i < w.points.length - 1; i++)
|
|
3037
|
+
mark(w.points[i].y, w.points[i + 1].y);
|
|
3038
|
+
const ys = [...occ].sort((a, b) => a - b);
|
|
3039
|
+
const shifts = [];
|
|
3040
|
+
for (let k = 1; k < ys.length; k++) {
|
|
3041
|
+
const emptySpan = ys[k] - ys[k - 1] - GRID; // clear cells strictly between two occupied rows
|
|
3042
|
+
if (emptySpan > SECTION_GAP)
|
|
3043
|
+
shifts.push({ fromY: ys[k] - 0.5, amount: emptySpan - SECTION_GAP });
|
|
3044
|
+
}
|
|
3045
|
+
if (shifts.length) {
|
|
3046
|
+
const shiftFor = (y) => shifts.reduce((s, sh) => s + (y >= sh.fromY ? sh.amount : 0), 0);
|
|
3047
|
+
for (const n of layoutNodes) {
|
|
3048
|
+
const dy = shiftFor(n.absY);
|
|
3049
|
+
if (!dy)
|
|
3050
|
+
continue;
|
|
3051
|
+
n.absY -= dy;
|
|
3052
|
+
for (const p of n.inputs)
|
|
3053
|
+
p.absY -= dy;
|
|
3054
|
+
for (const p of n.outputs)
|
|
3055
|
+
p.absY -= dy;
|
|
3056
|
+
}
|
|
3057
|
+
for (const w of wires)
|
|
3058
|
+
for (const p of w.points)
|
|
3059
|
+
p.y -= shiftFor(p.y);
|
|
3060
|
+
for (const j of mergedJunctions)
|
|
3061
|
+
j.y -= shiftFor(j.y);
|
|
3062
|
+
for (const l of labels) {
|
|
3063
|
+
const dy = shiftFor(l.y);
|
|
3064
|
+
l.y -= dy;
|
|
3065
|
+
l.anchorY -= dy;
|
|
3066
|
+
if (l.leaderY !== undefined)
|
|
3067
|
+
l.leaderY -= dy;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
const maxX = Math.max(...layoutNodes.map(n => n.absX + n.width), ...wires.flatMap(w => w.points.map(p => p.x)), ...labels.map(l => l.x + l.width));
|
|
3072
|
+
const maxY = Math.max(...layoutNodes.map(n => n.absY + n.height), ...wires.flatMap(w => w.points.map(p => p.y)), ...labels.map(l => l.y + l.height));
|
|
3073
|
+
return {
|
|
3074
|
+
nodes: layoutNodes,
|
|
3075
|
+
wires,
|
|
3076
|
+
junctions: mergedJunctions,
|
|
3077
|
+
labels,
|
|
3078
|
+
width: maxX,
|
|
3079
|
+
height: maxY,
|
|
3080
|
+
options: opts,
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
export function findWireCrossings(wires, junctions) {
|
|
3084
|
+
const crossings = [];
|
|
3085
|
+
const junctionSet = new Set(junctions.map(j => `${Math.round(j.x)},${Math.round(j.y)}`));
|
|
3086
|
+
for (let i = 0; i < wires.length; i++) {
|
|
3087
|
+
for (let j = i + 1; j < wires.length; j++) {
|
|
3088
|
+
if (wires[i].fromId === wires[j].fromId)
|
|
3089
|
+
continue;
|
|
3090
|
+
if (wires[i].feedback || wires[j].feedback)
|
|
3091
|
+
continue; // loop-backs run in their own lane
|
|
3092
|
+
for (let si = 0; si < wires[i].points.length - 1; si++) {
|
|
3093
|
+
for (let sj = 0; sj < wires[j].points.length - 1; sj++) {
|
|
3094
|
+
const p1 = wires[i].points[si], p2 = wires[i].points[si + 1];
|
|
3095
|
+
const q1 = wires[j].points[sj], q2 = wires[j].points[sj + 1];
|
|
3096
|
+
// Test a horizontal segment against a vertical segment in EITHER orientation
|
|
3097
|
+
// (i-horiz×j-vert and i-vert×j-horiz), so crossings are caught regardless of
|
|
3098
|
+
// which wire happens to come first in the list.
|
|
3099
|
+
const cross = (h1, h2, v1, v2) => {
|
|
3100
|
+
if (Math.abs(h1.y - h2.y) >= 1 || Math.abs(v1.x - v2.x) >= 1)
|
|
3101
|
+
return null;
|
|
3102
|
+
const y = h1.y, x = v1.x;
|
|
3103
|
+
const yMin = Math.min(v1.y, v2.y), yMax = Math.max(v1.y, v2.y);
|
|
3104
|
+
const xMin = Math.min(h1.x, h2.x), xMax = Math.max(h1.x, h2.x);
|
|
3105
|
+
return (y >= yMin - 1 && y <= yMax + 1 && x >= xMin - 1 && x <= xMax + 1) ? { x, y } : null;
|
|
3106
|
+
};
|
|
3107
|
+
const hit = cross(p1, p2, q1, q2) ?? cross(q1, q2, p1, p2);
|
|
3108
|
+
if (hit && !junctionSet.has(`${Math.round(hit.x)},${Math.round(hit.y)}`)) {
|
|
3109
|
+
crossings.push({
|
|
3110
|
+
wire1From: wires[i].fromId, wire1To: wires[i].toId,
|
|
3111
|
+
wire2From: wires[j].fromId, wire2To: wires[j].toId,
|
|
3112
|
+
x: Math.round(hit.x), y: Math.round(hit.y),
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return crossings;
|
|
3120
|
+
}
|
|
3121
|
+
export { MIN_PORT_GAP, MIN_DOGLEG };
|