grid-router 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/corridors.d.ts +20 -0
- package/dist/corridors.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/layers.d.ts +39 -0
- package/dist/layers.js +36 -0
- package/dist/router.d.ts +109 -0
- package/dist/router.js +432 -0
- package/package.json +54 -0
- package/src/corridors.ts +29 -0
- package/src/index.ts +20 -0
- package/src/layers.ts +67 -0
- package/src/router.ts +519 -0
- package/src/svelte/GridDiagram.svelte +366 -0
- package/src/svelte/index.ts +21 -0
- package/src/svelte/types.ts +23 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orthogonal edge router on a grid.
|
|
3
|
+
*
|
|
4
|
+
* Rasterizes a canvas into square cells and routes every edge with A*:
|
|
5
|
+
*
|
|
6
|
+
* - node rects ("boxes", inflated) hard-block cells;
|
|
7
|
+
* - per-direction occupancy: a cell can host ONE horizontal and ONE vertical
|
|
8
|
+
* run — another bus reusing the same direction pays a near-prohibitive
|
|
9
|
+
* penalty (soft block, so routing never fails), while perpendicular
|
|
10
|
+
* crossings only pay a small toll → lanes separate themselves and crossings
|
|
11
|
+
* happen only where they're worth it;
|
|
12
|
+
* - same-bus discount + tree seeding: a bus's later edges branch from the
|
|
13
|
+
* OPTIMAL point of the already-routed trunk (Steiner-style joins), and
|
|
14
|
+
* `exitCost` prices opening a new trunk out of a source, so merges can win
|
|
15
|
+
* even over shorter direct paths;
|
|
16
|
+
* - turn penalty biases toward long straight runs;
|
|
17
|
+
* - endpoints are exclusive per bus (no stacked stubs/arrowheads);
|
|
18
|
+
* - an edge is NEVER dropped: when a node row walls the grid off, a desperate
|
|
19
|
+
* pass routes through obstacles at a huge cost and reports a violation.
|
|
20
|
+
*
|
|
21
|
+
* Pure geometry: the caller measures its DOM into {@link GridBox}es and
|
|
22
|
+
* renders the returned SVG path strings. Framework-free.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface GridBox {
|
|
26
|
+
t: number;
|
|
27
|
+
b: number;
|
|
28
|
+
l: number;
|
|
29
|
+
r: number;
|
|
30
|
+
cx: number;
|
|
31
|
+
cy: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GridEdge {
|
|
35
|
+
id: string;
|
|
36
|
+
source: string;
|
|
37
|
+
target: string;
|
|
38
|
+
/** Bus discriminator: edges sharing (source, bus) merge into one trunk
|
|
39
|
+
* tree. Defaults to the source alone. */
|
|
40
|
+
bus?: string;
|
|
41
|
+
/** Opaque consumer payload, passed through untouched onto the conn. */
|
|
42
|
+
data?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GridConn {
|
|
46
|
+
id: string;
|
|
47
|
+
source: string;
|
|
48
|
+
target: string;
|
|
49
|
+
/** Full bus key (`source|bus`) — stable grouping handle for rendering
|
|
50
|
+
* (e.g. crossing-hop halos are painted per bus). */
|
|
51
|
+
bus: string;
|
|
52
|
+
data?: unknown;
|
|
53
|
+
/** Orthogonal SVG path: source contact → routed cells → target contact. */
|
|
54
|
+
d: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GridDebugCell {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
kind: 'chip' | 'h' | 'v' | 'hv' | 'bad';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GridResult {
|
|
64
|
+
conns: GridConn[];
|
|
65
|
+
/** deepest y any path reaches (callers reserve layout height for it) */
|
|
66
|
+
bottom: number;
|
|
67
|
+
/** lane-sharing incidents (should be 0 — non-zero means the layout does
|
|
68
|
+
* not supply enough corridor space, see corridorGaps()) */
|
|
69
|
+
violations: number;
|
|
70
|
+
debug: { res: number; cols: number; rows: number; cells: GridDebugCell[] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Costs are in "cells". Tuning notes: TURN < the detour a straighter but
|
|
74
|
+
// longer route would take; CROSS small enough that crossing beats detouring
|
|
75
|
+
// around a whole row; OVERLAP practically prohibitive — any detour that
|
|
76
|
+
// exists at all must win, and a path that still pays it had NO alternative
|
|
77
|
+
// (a corridor-supply problem, reported as a violation).
|
|
78
|
+
const COST_STEP = 1;
|
|
79
|
+
const COST_CROSS = 0.4;
|
|
80
|
+
const COST_OVERLAP = 4000;
|
|
81
|
+
const GAP = 3; // endpoint stub clearance off the box edge
|
|
82
|
+
|
|
83
|
+
/** Box inflation in px: routed runs keep this clearance off every box. */
|
|
84
|
+
export const BOX_INFLATE = 3;
|
|
85
|
+
|
|
86
|
+
export interface GridOpts {
|
|
87
|
+
/** grid cell size in px (default 12) */
|
|
88
|
+
res?: number;
|
|
89
|
+
/** cost per cell when riding cells the bus already owns (default 0.02) */
|
|
90
|
+
costOwn?: number;
|
|
91
|
+
/** penalty per 90° turn (default 1) */
|
|
92
|
+
costTurn?: number;
|
|
93
|
+
/** penalty for opening a NEW trunk out of a source (default 0) — raise it
|
|
94
|
+
* to make a bus's edges merge even when direct paths would be shorter */
|
|
95
|
+
exitCost?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Cost layers: arbitrary rects (px) adding a per-cell bias to every step
|
|
98
|
+
* inside them — positive repels routes, negative attracts them. This is
|
|
99
|
+
* the generic hook for consumer concepts the router deliberately does not
|
|
100
|
+
* know about (e.g. a "lanes" layer biasing corridors negative, keep-out
|
|
101
|
+
* zones, preferred highways). The effective step cost is floored so
|
|
102
|
+
* negative biases can't break A*.
|
|
103
|
+
*/
|
|
104
|
+
costRegions?: { l: number; t: number; r: number; b: number; bias: number }[];
|
|
105
|
+
/**
|
|
106
|
+
* Port constraints: restrict which box sides edges may attach to, per node
|
|
107
|
+
* id (schematic semantics — a horizontal resistor only connects via its
|
|
108
|
+
* left/right leads, an IC via its pin sides). Unlisted nodes accept all
|
|
109
|
+
* four sides. The desperate fallback relaxes the constraint rather than
|
|
110
|
+
* dropping the edge.
|
|
111
|
+
*/
|
|
112
|
+
nodeSides?: Record<string, BoxSide[]>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type BoxSide = 'top' | 'bottom' | 'left' | 'right';
|
|
116
|
+
|
|
117
|
+
export function routeGrid(
|
|
118
|
+
boxes: Map<string, GridBox>,
|
|
119
|
+
edges: GridEdge[],
|
|
120
|
+
width: number,
|
|
121
|
+
height: number,
|
|
122
|
+
opts: GridOpts = {}
|
|
123
|
+
): GridResult {
|
|
124
|
+
const res = opts.res ?? 12;
|
|
125
|
+
const costOwn = opts.costOwn ?? 0.02;
|
|
126
|
+
const costTurn = opts.costTurn ?? 1;
|
|
127
|
+
const exitCost = opts.exitCost ?? 0;
|
|
128
|
+
const cols = Math.max(2, Math.ceil(width / res));
|
|
129
|
+
const rows = Math.max(2, Math.ceil(height / res));
|
|
130
|
+
const N = cols * rows;
|
|
131
|
+
const blocked = new Uint8Array(N);
|
|
132
|
+
const usageH = new Int32Array(N).fill(-1);
|
|
133
|
+
const usageV = new Int32Array(N).fill(-1);
|
|
134
|
+
// Path endpoints are EXCLUSIVE per bus: two buses sharing a start/goal cell
|
|
135
|
+
// (one entering it vertically, one horizontally) would stack their stubs and
|
|
136
|
+
// arrowheads on the same spot at the box edge.
|
|
137
|
+
const endpointOwner = new Int32Array(N).fill(-1);
|
|
138
|
+
// Consumer cost layers rasterized to a per-cell bias.
|
|
139
|
+
const bias = new Float64Array(N);
|
|
140
|
+
for (const rg of opts.costRegions ?? []) {
|
|
141
|
+
const x0 = Math.max(0, Math.floor(rg.l / res));
|
|
142
|
+
const x1 = Math.min(cols - 1, Math.floor(rg.r / res));
|
|
143
|
+
const y0 = Math.max(0, Math.floor(rg.t / res));
|
|
144
|
+
const y1 = Math.min(rows - 1, Math.floor(rg.b / res));
|
|
145
|
+
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) bias[y * cols + x] += rg.bias;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const centerOf = (i: number) => ({
|
|
149
|
+
x: ((i % cols) + 0.5) * res,
|
|
150
|
+
y: (Math.floor(i / cols) + 0.5) * res
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// --- block box cells (inflated so runs keep visual clearance) ---
|
|
154
|
+
for (const b of boxes.values()) {
|
|
155
|
+
const x0 = Math.max(0, Math.floor((b.l - BOX_INFLATE) / res));
|
|
156
|
+
const x1 = Math.min(cols - 1, Math.floor((b.r + BOX_INFLATE) / res));
|
|
157
|
+
const y0 = Math.max(0, Math.floor((b.t - BOX_INFLATE) / res));
|
|
158
|
+
const y1 = Math.min(rows - 1, Math.floor((b.b + BOX_INFLATE) / res));
|
|
159
|
+
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) blocked[y * cols + x] = 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Ring of free cells just outside a box, CORNERS EXCLUDED: cells strictly
|
|
163
|
+
* over the box's horizontal span connect vertically, cells strictly beside
|
|
164
|
+
* its vertical span connect horizontally — so a path can never end with a
|
|
165
|
+
* sideways segment kissing a box corner. `includeBlocked` is the desperate
|
|
166
|
+
* mode: a fully sealed box still needs endpoints. */
|
|
167
|
+
function ring(
|
|
168
|
+
b: GridBox,
|
|
169
|
+
includeBlocked = false,
|
|
170
|
+
sides?: BoxSide[]
|
|
171
|
+
): { cell: number; dir: 0 | 1 }[] {
|
|
172
|
+
const out: { cell: number; dir: 0 | 1 }[] = [];
|
|
173
|
+
const ok = (i: number) => includeBlocked || !blocked[i];
|
|
174
|
+
const allow = (s: BoxSide) => !sides || sides.includes(s);
|
|
175
|
+
// Small-box tolerance: a box narrower than two cells has no strict span —
|
|
176
|
+
// fall back to the single column/row nearest its center, so tiny nodes
|
|
177
|
+
// (consumer "port" pads) still get endpoints.
|
|
178
|
+
const xs: number[] = [];
|
|
179
|
+
for (let x = Math.max(0, Math.ceil(b.l / res)); x <= Math.floor(b.r / res) - 1; x++) {
|
|
180
|
+
if (x <= cols - 1) xs.push(x);
|
|
181
|
+
}
|
|
182
|
+
if (!xs.length) xs.push(Math.min(cols - 1, Math.max(0, Math.floor(b.cx / res))));
|
|
183
|
+
const yTop = Math.floor((b.t - BOX_INFLATE) / res) - 1;
|
|
184
|
+
const yBot = Math.floor((b.b + BOX_INFLATE) / res) + 1;
|
|
185
|
+
for (const x of xs) {
|
|
186
|
+
if (allow('top') && yTop >= 0 && ok(yTop * cols + x)) {
|
|
187
|
+
out.push({ cell: yTop * cols + x, dir: 1 });
|
|
188
|
+
}
|
|
189
|
+
if (allow('bottom') && yBot <= rows - 1 && ok(yBot * cols + x)) {
|
|
190
|
+
out.push({ cell: yBot * cols + x, dir: 1 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const ys: number[] = [];
|
|
194
|
+
for (let y = Math.max(0, Math.ceil(b.t / res)); y <= Math.floor(b.b / res) - 1; y++) {
|
|
195
|
+
if (y <= rows - 1) ys.push(y);
|
|
196
|
+
}
|
|
197
|
+
if (!ys.length) ys.push(Math.min(rows - 1, Math.max(0, Math.floor(b.cy / res))));
|
|
198
|
+
const xL = Math.floor((b.l - BOX_INFLATE) / res) - 1;
|
|
199
|
+
const xR = Math.floor((b.r + BOX_INFLATE) / res) + 1;
|
|
200
|
+
for (const y of ys) {
|
|
201
|
+
if (allow('left') && xL >= 0 && ok(y * cols + xL)) out.push({ cell: y * cols + xL, dir: 0 });
|
|
202
|
+
if (allow('right') && xR <= cols - 1 && ok(y * cols + xR)) {
|
|
203
|
+
out.push({ cell: y * cols + xR, dir: 0 });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- group edges into buses ---
|
|
210
|
+
interface Bus {
|
|
211
|
+
key: string;
|
|
212
|
+
idx: number;
|
|
213
|
+
items: GridEdge[];
|
|
214
|
+
own: Set<number>; // cell*2+dir claimed by this bus
|
|
215
|
+
/** routed tree: cell → previous cell toward the source (trunk parent) */
|
|
216
|
+
prev: Map<number, number>;
|
|
217
|
+
}
|
|
218
|
+
const busMap = new Map<string, Bus>();
|
|
219
|
+
for (const e of edges) {
|
|
220
|
+
if (!boxes.has(e.source) || !boxes.has(e.target)) continue;
|
|
221
|
+
const key = `${e.source}|${e.bus ?? ''}`;
|
|
222
|
+
let bus = busMap.get(key);
|
|
223
|
+
if (!bus) {
|
|
224
|
+
bus = { key, idx: busMap.size, items: [], own: new Set(), prev: new Map() };
|
|
225
|
+
busMap.set(key, bus);
|
|
226
|
+
}
|
|
227
|
+
bus.items.push(e);
|
|
228
|
+
}
|
|
229
|
+
// Bigger fans first: they claim their trunks before loners squeeze through.
|
|
230
|
+
const buses = [...busMap.values()].sort((a, b) => b.items.length - a.items.length);
|
|
231
|
+
|
|
232
|
+
// --- A* over (cell, dir) states ---
|
|
233
|
+
// Tiny binary heap on f; lazy decrease (re-push, skip stale pops).
|
|
234
|
+
function astar(
|
|
235
|
+
bus: Bus,
|
|
236
|
+
starts: { cell: number; dir: 0 | 1; g0: number }[],
|
|
237
|
+
goals: Set<number>,
|
|
238
|
+
/** desperate mode: blocked cells become traversable at a huge cost so a
|
|
239
|
+
* path ALWAYS exists — silently missing edges is worse than an ugly one */
|
|
240
|
+
desperate = false
|
|
241
|
+
): { cell: number; dir: 0 | 1 }[] | null {
|
|
242
|
+
const S = N * 2;
|
|
243
|
+
const g = new Float64Array(S).fill(Infinity);
|
|
244
|
+
const parent = new Int32Array(S).fill(-1);
|
|
245
|
+
const closed = new Uint8Array(S);
|
|
246
|
+
// goal heuristic anchor: mean of goal cells
|
|
247
|
+
let gx = 0;
|
|
248
|
+
let gy = 0;
|
|
249
|
+
for (const c of goals) {
|
|
250
|
+
gx += c % cols;
|
|
251
|
+
gy += Math.floor(c / cols);
|
|
252
|
+
}
|
|
253
|
+
gx /= goals.size || 1;
|
|
254
|
+
gy /= goals.size || 1;
|
|
255
|
+
const h = (cell: number) =>
|
|
256
|
+
Math.abs((cell % cols) - gx) + Math.abs(Math.floor(cell / cols) - gy);
|
|
257
|
+
|
|
258
|
+
type Entry = { f: number; s: number };
|
|
259
|
+
const heap: Entry[] = [];
|
|
260
|
+
const push = (e: Entry) => {
|
|
261
|
+
heap.push(e);
|
|
262
|
+
let i = heap.length - 1;
|
|
263
|
+
while (i > 0) {
|
|
264
|
+
const p = (i - 1) >> 1;
|
|
265
|
+
if (heap[p].f <= heap[i].f) break;
|
|
266
|
+
[heap[p], heap[i]] = [heap[i], heap[p]];
|
|
267
|
+
i = p;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const pop = (): Entry | undefined => {
|
|
271
|
+
const top = heap[0];
|
|
272
|
+
const last = heap.pop();
|
|
273
|
+
if (heap.length && last) {
|
|
274
|
+
heap[0] = last;
|
|
275
|
+
let i = 0;
|
|
276
|
+
for (;;) {
|
|
277
|
+
const l = 2 * i + 1;
|
|
278
|
+
const r = l + 1;
|
|
279
|
+
let m = i;
|
|
280
|
+
if (l < heap.length && heap[l].f < heap[m].f) m = l;
|
|
281
|
+
if (r < heap.length && heap[r].f < heap[m].f) m = r;
|
|
282
|
+
if (m === i) break;
|
|
283
|
+
[heap[m], heap[i]] = [heap[i], heap[m]];
|
|
284
|
+
i = m;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return top;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (const st of starts) {
|
|
291
|
+
const s = st.cell * 2 + st.dir;
|
|
292
|
+
if (g[s] > st.g0) {
|
|
293
|
+
g[s] = st.g0;
|
|
294
|
+
push({ f: st.g0 + h(st.cell), s });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// A step covers HALF of each of its two cells — price the worst of both,
|
|
299
|
+
// or an elbow's outgoing half would be free to overlap.
|
|
300
|
+
const halfCost = (cell: number, dir: 0 | 1) => {
|
|
301
|
+
const same = dir === 0 ? usageH[cell] : usageV[cell];
|
|
302
|
+
if (same === bus.idx || bus.own.has(cell * 2 + dir)) return costOwn;
|
|
303
|
+
if (same !== -1) return COST_OVERLAP;
|
|
304
|
+
return COST_STEP;
|
|
305
|
+
};
|
|
306
|
+
const stepCost = (from: number, next: number, dir: 0 | 1) => {
|
|
307
|
+
// floored so negative cost-layer biases can't break A* (no negative steps)
|
|
308
|
+
let c = Math.max(0.05, Math.max(halfCost(from, dir), halfCost(next, dir)) + bias[next]);
|
|
309
|
+
const cross = dir === 0 ? usageV[next] : usageH[next];
|
|
310
|
+
if (cross !== -1 && cross !== bus.idx) c += COST_CROSS;
|
|
311
|
+
return c;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let goalState = -1;
|
|
315
|
+
while (heap.length) {
|
|
316
|
+
const cur = pop()!;
|
|
317
|
+
const s = cur.s;
|
|
318
|
+
if (closed[s]) continue;
|
|
319
|
+
closed[s] = 1;
|
|
320
|
+
const cell = s >> 1;
|
|
321
|
+
const dir = (s & 1) as 0 | 1;
|
|
322
|
+
if (goals.has(cell)) {
|
|
323
|
+
goalState = s;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
const x = cell % cols;
|
|
327
|
+
const y = (cell - x) / cols;
|
|
328
|
+
const neigh: { cell: number; dir: 0 | 1 }[] = [];
|
|
329
|
+
if (x > 0) neigh.push({ cell: cell - 1, dir: 0 });
|
|
330
|
+
if (x < cols - 1) neigh.push({ cell: cell + 1, dir: 0 });
|
|
331
|
+
if (y > 0) neigh.push({ cell: cell - cols, dir: 1 });
|
|
332
|
+
if (y < rows - 1) neigh.push({ cell: cell + cols, dir: 1 });
|
|
333
|
+
for (const nb of neigh) {
|
|
334
|
+
if (blocked[nb.cell] && !desperate) continue;
|
|
335
|
+
const ns = nb.cell * 2 + nb.dir;
|
|
336
|
+
if (closed[ns]) continue;
|
|
337
|
+
const cost =
|
|
338
|
+
stepCost(cell, nb.cell, nb.dir) +
|
|
339
|
+
(nb.dir === dir ? 0 : costTurn) +
|
|
340
|
+
(blocked[nb.cell] ? 800 : 0);
|
|
341
|
+
const ng = g[s] + cost;
|
|
342
|
+
if (ng < g[ns]) {
|
|
343
|
+
g[ns] = ng;
|
|
344
|
+
parent[ns] = s;
|
|
345
|
+
push({ f: ng + h(nb.cell), s: ns });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (goalState < 0) return null;
|
|
350
|
+
const path: { cell: number; dir: 0 | 1 }[] = [];
|
|
351
|
+
for (let s = goalState; s >= 0; s = parent[s]) {
|
|
352
|
+
path.push({ cell: s >> 1, dir: (s & 1) as 0 | 1 });
|
|
353
|
+
}
|
|
354
|
+
path.reverse();
|
|
355
|
+
return path;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Contact point on a box edge for the path end at ring cell `i`. Stays
|
|
359
|
+
* aligned with the cell CENTER (corner-free rings guarantee it lies over
|
|
360
|
+
* the edge), so the stub is colinear with the routed run — never nudge the
|
|
361
|
+
* routed geometry instead: that would shift a run off its claimed lane. */
|
|
362
|
+
function contact(b: GridBox, i: number): { x: number; y: number } {
|
|
363
|
+
const c = centerOf(i);
|
|
364
|
+
if (c.y < b.t) return { x: Math.min(Math.max(c.x, b.l + 2), b.r - 2), y: b.t - GAP };
|
|
365
|
+
if (c.y > b.b) return { x: Math.min(Math.max(c.x, b.l + 2), b.r - 2), y: b.b + GAP };
|
|
366
|
+
if (c.x < b.l) return { x: b.l - GAP, y: Math.min(Math.max(c.y, b.t + 2), b.b - 2) };
|
|
367
|
+
return { x: b.r + GAP, y: Math.min(Math.max(c.y, b.t + 2), b.b - 2) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const conns: GridConn[] = [];
|
|
371
|
+
let bottom = 0;
|
|
372
|
+
let violations = 0;
|
|
373
|
+
const badCells = new Set<number>();
|
|
374
|
+
|
|
375
|
+
for (const bus of buses) {
|
|
376
|
+
const s = boxes.get(bus.items[0].source)!;
|
|
377
|
+
// Route nearest targets first so the trunk grows outward and later
|
|
378
|
+
// edges find own-bus cells to ride.
|
|
379
|
+
const items = [...bus.items].sort((a, b) => {
|
|
380
|
+
const ta = boxes.get(a.target)!;
|
|
381
|
+
const tb = boxes.get(b.target)!;
|
|
382
|
+
return (
|
|
383
|
+
Math.abs(ta.cy - s.cy) + Math.abs(ta.cx - s.cx) -
|
|
384
|
+
(Math.abs(tb.cy - s.cy) + Math.abs(tb.cx - s.cx))
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
for (const e of items) {
|
|
388
|
+
const t = boxes.get(e.target)!;
|
|
389
|
+
// Seeds: the source's ring (opening a NEW trunk costs exitCost) PLUS
|
|
390
|
+
// every cell the bus already routed at cost 0 — a later edge branches
|
|
391
|
+
// from the OPTIMAL point of the existing trunk (Steiner-style join).
|
|
392
|
+
// Endpoint cells owned by other buses are off limits (stacked stubs).
|
|
393
|
+
const sSides = opts.nodeSides?.[e.source];
|
|
394
|
+
const tSides = opts.nodeSides?.[e.target];
|
|
395
|
+
const starts = ring(s, false, sSides)
|
|
396
|
+
.filter((r) => endpointOwner[r.cell] === -1 || endpointOwner[r.cell] === bus.idx)
|
|
397
|
+
.map((r) => ({ ...r, g0: exitCost }));
|
|
398
|
+
for (const od of bus.own) {
|
|
399
|
+
starts.push({ cell: od >> 1, dir: (od & 1) as 0 | 1, g0: 0 });
|
|
400
|
+
}
|
|
401
|
+
const goals = new Set(
|
|
402
|
+
ring(t, false, tSides)
|
|
403
|
+
.map((r) => r.cell)
|
|
404
|
+
.filter((c) => endpointOwner[c] === -1 || endpointOwner[c] === bus.idx)
|
|
405
|
+
);
|
|
406
|
+
let path = starts.length && goals.size ? astar(bus, starts, goals) : null;
|
|
407
|
+
if (!path) {
|
|
408
|
+
// No legal route (a dense node row can wall the grid off at coarse
|
|
409
|
+
// cell sizes, or port constraints seal a node in). NEVER drop the
|
|
410
|
+
// edge silently: retry with blocked cells traversable at a huge
|
|
411
|
+
// cost — and if even that fails, relax the port constraints too.
|
|
412
|
+
const dStarts = ring(s, true, sSides).map((r) => ({ ...r, g0: exitCost }));
|
|
413
|
+
for (const od of bus.own) {
|
|
414
|
+
dStarts.push({ cell: od >> 1, dir: (od & 1) as 0 | 1, g0: 0 });
|
|
415
|
+
}
|
|
416
|
+
const dGoals = new Set(ring(t, true, tSides).map((r) => r.cell));
|
|
417
|
+
path = dStarts.length && dGoals.size ? astar(bus, dStarts, dGoals, true) : null;
|
|
418
|
+
if (!path && (sSides || tSides)) {
|
|
419
|
+
const uStarts = ring(s, true).map((r) => ({ ...r, g0: exitCost }));
|
|
420
|
+
for (const od of bus.own) {
|
|
421
|
+
uStarts.push({ cell: od >> 1, dir: (od & 1) as 0 | 1, g0: 0 });
|
|
422
|
+
}
|
|
423
|
+
const uGoals = new Set(ring(t, true).map((r) => r.cell));
|
|
424
|
+
path = uStarts.length && uGoals.size ? astar(bus, uStarts, uGoals, true) : null;
|
|
425
|
+
}
|
|
426
|
+
if (!path) continue;
|
|
427
|
+
violations++;
|
|
428
|
+
badCells.add(path[Math.floor(path.length / 2)].cell);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Claim per STEP on BOTH cells (an elbow claims its outgoing direction
|
|
432
|
+
// too) + extend the bus tree (first writer keeps the link).
|
|
433
|
+
for (let i = 1; i < path.length; i++) {
|
|
434
|
+
const dir = path[i].dir;
|
|
435
|
+
for (const cell of [path[i - 1].cell, path[i].cell]) {
|
|
436
|
+
const cur = dir === 0 ? usageH[cell] : usageV[cell];
|
|
437
|
+
if (cur !== -1 && cur !== bus.idx && !bus.own.has(cell * 2 + dir)) {
|
|
438
|
+
violations++;
|
|
439
|
+
badCells.add(cell);
|
|
440
|
+
}
|
|
441
|
+
if (dir === 0) {
|
|
442
|
+
if (usageH[cell] === -1) usageH[cell] = bus.idx;
|
|
443
|
+
} else if (usageV[cell] === -1) usageV[cell] = bus.idx;
|
|
444
|
+
bus.own.add(cell * 2 + dir);
|
|
445
|
+
}
|
|
446
|
+
if (!bus.prev.has(path[i].cell) && path[i - 1].cell !== path[i].cell) {
|
|
447
|
+
bus.prev.set(path[i].cell, path[i - 1].cell);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (endpointOwner[path[0].cell] === -1) endpointOwner[path[0].cell] = bus.idx;
|
|
451
|
+
const goalCell = path[path.length - 1].cell;
|
|
452
|
+
if (endpointOwner[goalCell] === -1) endpointOwner[goalCell] = bus.idx;
|
|
453
|
+
|
|
454
|
+
// Full source→target chain: if the A* seed was a mid-trunk cell, walk
|
|
455
|
+
// the bus tree back to the source ring and prepend that trunk prefix.
|
|
456
|
+
const chain: number[] = [];
|
|
457
|
+
{
|
|
458
|
+
let c = path[0].cell;
|
|
459
|
+
const guard = new Set<number>();
|
|
460
|
+
while (bus.prev.has(c) && !guard.has(c)) {
|
|
461
|
+
guard.add(c);
|
|
462
|
+
c = bus.prev.get(c)!;
|
|
463
|
+
chain.push(c);
|
|
464
|
+
}
|
|
465
|
+
chain.reverse();
|
|
466
|
+
}
|
|
467
|
+
const fullCells = [...chain, ...path.map((p) => p.cell)];
|
|
468
|
+
|
|
469
|
+
// polyline: source contact → cell centers (collinear-compressed) → target contact
|
|
470
|
+
const pts: { x: number; y: number }[] = [];
|
|
471
|
+
const first = contact(s, fullCells[0]);
|
|
472
|
+
const last = contact(t, fullCells[fullCells.length - 1]);
|
|
473
|
+
pts.push(first);
|
|
474
|
+
for (const c of fullCells) pts.push(centerOf(c));
|
|
475
|
+
pts.push(last);
|
|
476
|
+
const simp: { x: number; y: number }[] = [];
|
|
477
|
+
for (const p of pts) {
|
|
478
|
+
const n = simp.length;
|
|
479
|
+
if (n >= 2) {
|
|
480
|
+
const a = simp[n - 2];
|
|
481
|
+
const b3 = simp[n - 1];
|
|
482
|
+
if ((a.x === b3.x && b3.x === p.x) || (a.y === b3.y && b3.y === p.y)) {
|
|
483
|
+
simp[n - 1] = p;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
simp.push(p);
|
|
488
|
+
}
|
|
489
|
+
// force pure orthogonal corners (cell centers already are; stubs may
|
|
490
|
+
// introduce tiny diagonals — insert a corner point when needed)
|
|
491
|
+
const ortho: { x: number; y: number }[] = [simp[0]];
|
|
492
|
+
for (let i = 1; i < simp.length; i++) {
|
|
493
|
+
const a = ortho[ortho.length - 1];
|
|
494
|
+
const b4 = simp[i];
|
|
495
|
+
if (a.x !== b4.x && a.y !== b4.y) ortho.push({ x: b4.x, y: a.y });
|
|
496
|
+
ortho.push(b4);
|
|
497
|
+
}
|
|
498
|
+
const d = ortho
|
|
499
|
+
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${Math.round(p.x * 2) / 2} ${Math.round(p.y * 2) / 2}`)
|
|
500
|
+
.join(' ');
|
|
501
|
+
for (const p of ortho) bottom = Math.max(bottom, p.y);
|
|
502
|
+
conns.push({ id: `e:${e.id}`, source: e.source, target: e.target, bus: bus.key, data: e.data, d });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// --- debug cells ---
|
|
507
|
+
const cells: GridDebugCell[] = [];
|
|
508
|
+
for (let i = 0; i < N; i++) {
|
|
509
|
+
const x = i % cols;
|
|
510
|
+
const y = (i - x) / cols;
|
|
511
|
+
if (badCells.has(i)) cells.push({ x, y, kind: 'bad' });
|
|
512
|
+
else if (blocked[i]) cells.push({ x, y, kind: 'chip' });
|
|
513
|
+
else if (usageH[i] !== -1 && usageV[i] !== -1) cells.push({ x, y, kind: 'hv' });
|
|
514
|
+
else if (usageH[i] !== -1) cells.push({ x, y, kind: 'h' });
|
|
515
|
+
else if (usageV[i] !== -1) cells.push({ x, y, kind: 'v' });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { conns, bottom, violations, debug: { res, cols, rows, cells } };
|
|
519
|
+
}
|