@yalehwang/plantuml-js 0.2.0 → 0.3.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/README.md +20 -0
- package/dist/core.d.ts +102 -9
- package/dist/core.js +91 -51
- package/dist/foundation/layers.d.ts +25 -0
- package/dist/foundation/layers.js +101 -0
- package/dist/foundation/layout.d.ts +116 -0
- package/dist/foundation/layout.js +298 -0
- package/dist/{model.d.ts → foundation/model.d.ts} +20 -0
- package/dist/foundation/relation-semantics.d.ts +49 -0
- package/dist/foundation/relation-semantics.js +112 -0
- package/dist/{text-measure.d.ts → foundation/text-measure.d.ts} +2 -0
- package/dist/{text-measure.js → foundation/text-measure.js} +20 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/compound.d.ts +59 -0
- package/dist/layout/compound.js +1178 -0
- package/dist/layout/elk-graph.d.ts +36 -0
- package/dist/layout/elk-graph.js +220 -0
- package/dist/layout/elk-result.d.ts +18 -0
- package/dist/layout/elk-result.js +127 -0
- package/dist/layout/elk.d.ts +63 -33
- package/dist/layout/elk.js +215 -321
- package/dist/layout/fanout-trunk.d.ts +18 -0
- package/dist/layout/fanout-trunk.js +380 -0
- package/dist/layout/grid-router.d.ts +69 -0
- package/dist/layout/grid-router.js +615 -0
- package/dist/layout/lane-spread.d.ts +11 -0
- package/dist/layout/lane-spread.js +302 -0
- package/dist/layout/layout-metrics.d.ts +183 -0
- package/dist/layout/layout-metrics.js +766 -0
- package/dist/layout/package-endpoints.d.ts +10 -0
- package/dist/layout/package-endpoints.js +54 -0
- package/dist/layout/placement-strategy.d.ts +98 -0
- package/dist/layout/placement-strategy.js +26 -0
- package/dist/layout/strategy.d.ts +27 -0
- package/dist/layout/strategy.js +19 -0
- package/dist/layout/trunk-lanes.d.ts +29 -0
- package/dist/layout/trunk-lanes.js +308 -0
- package/dist/layout/viz.d.ts +3 -3
- package/dist/layout/viz.js +13 -17
- package/dist/parser.d.ts +27 -1
- package/dist/parser.js +111 -5
- package/dist/render/labels.d.ts +44 -0
- package/dist/render/labels.js +231 -0
- package/dist/render/legend.d.ts +15 -0
- package/dist/render/legend.js +42 -0
- package/dist/render/svg.d.ts +60 -3
- package/dist/render/svg.js +326 -98
- package/dist/render/theme.d.ts +109 -0
- package/dist/render/theme.js +99 -0
- package/dist/serialize.d.ts +32 -0
- package/dist/serialize.js +218 -0
- package/dist/transform.d.ts +55 -1
- package/dist/transform.js +277 -29
- package/package.json +9 -3
- package/dist/layout-metrics.d.ts +0 -42
- package/dist/layout-metrics.js +0 -241
- package/dist/layout.d.ts +0 -75
- package/dist/layout.js +0 -51
- /package/dist/{edge-router.d.ts → foundation/edge-router.d.ts} +0 -0
- /package/dist/{edge-router.js → foundation/edge-router.js} +0 -0
- /package/dist/{model.js → foundation/model.js} +0 -0
- /package/dist/{render-dot.d.ts → foundation/render-dot.d.ts} +0 -0
- /package/dist/{render-dot.js → foundation/render-dot.js} +0 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid-based orthogonal edge router.
|
|
3
|
+
*
|
|
4
|
+
* ELK's hierarchical routing only resolves edges inside one layered run, and a
|
|
5
|
+
* layered run cannot keep a large compound graph at a sensible aspect ratio.
|
|
6
|
+
* The compound layout therefore lays blocks out first and hands the edges that
|
|
7
|
+
* cross block boundaries to this router: an A* search on a lattice whose
|
|
8
|
+
* vertices are blocked inside every node rectangle (plus a clearance margin),
|
|
9
|
+
* with a bend penalty and a soft penalty for sharing a lane with an already
|
|
10
|
+
* routed edge so parallel wires spread out instead of stacking.
|
|
11
|
+
*
|
|
12
|
+
* Every returned polyline is axis-aligned, starts and ends on the border of the
|
|
13
|
+
* source/target node, and avoids every other node rectangle unless no path
|
|
14
|
+
* exists (then a plain dog-leg is returned).
|
|
15
|
+
*/
|
|
16
|
+
/** Distance (px) a lane keeps from a package frame border when it can. */
|
|
17
|
+
export const FRAME_CLEARANCE = 12;
|
|
18
|
+
const FRAME_FLUSH_COST = 5;
|
|
19
|
+
/**
|
|
20
|
+
* Extra cost per lattice step through the interior of a package frame that
|
|
21
|
+
* neither endpoint of the wire lives in, so corridors prefer the gutters
|
|
22
|
+
* between frames over cutting through unrelated packages.
|
|
23
|
+
*/
|
|
24
|
+
export const FOREIGN_FRAME_COST = 7;
|
|
25
|
+
const BEND_COST = 3;
|
|
26
|
+
const SHARED_LANE_COST = 2;
|
|
27
|
+
const CROSSING_COST = 1.5;
|
|
28
|
+
/**
|
|
29
|
+
* Cost of a step along a lane already used by a wire with the same source (or
|
|
30
|
+
* the same target), against 1 for a fresh step: fan-outs grow a shared trunk
|
|
31
|
+
* instead of spreading into a comb of parallel staircases.
|
|
32
|
+
*/
|
|
33
|
+
const GROUP_LANE_COST = 0.4;
|
|
34
|
+
/** Re-routing passes over the wires that still cross others, and how many wires a pass lifts at most. */
|
|
35
|
+
const RIPUP_PASSES = 3;
|
|
36
|
+
const RIPUP_MAX = 400;
|
|
37
|
+
/** Search window around a wire's two nodes: at least this many px beyond them, or this fraction of their span. */
|
|
38
|
+
const WINDOW_MIN = 240;
|
|
39
|
+
const WINDOW_FRACTION = 0.3;
|
|
40
|
+
/** Direction indices: 0 up, 1 right, 2 down, 3 left. */
|
|
41
|
+
const DX = [0, 1, 0, -1];
|
|
42
|
+
const DY = [-1, 0, 1, 0];
|
|
43
|
+
/** Binary min-heap keyed by priority, over parallel typed arrays. */
|
|
44
|
+
class MinHeap {
|
|
45
|
+
keys = [];
|
|
46
|
+
values = [];
|
|
47
|
+
get size() {
|
|
48
|
+
return this.keys.length;
|
|
49
|
+
}
|
|
50
|
+
push(key, value) {
|
|
51
|
+
const keys = this.keys;
|
|
52
|
+
const values = this.values;
|
|
53
|
+
let i = keys.length;
|
|
54
|
+
keys.push(key);
|
|
55
|
+
values.push(value);
|
|
56
|
+
while (i > 0) {
|
|
57
|
+
const parent = (i - 1) >> 1;
|
|
58
|
+
if (keys[parent] <= key)
|
|
59
|
+
break;
|
|
60
|
+
keys[i] = keys[parent];
|
|
61
|
+
values[i] = values[parent];
|
|
62
|
+
i = parent;
|
|
63
|
+
}
|
|
64
|
+
keys[i] = key;
|
|
65
|
+
values[i] = value;
|
|
66
|
+
}
|
|
67
|
+
/** Remove the minimum; returns its value (priority is read via [[lastKey]]). */
|
|
68
|
+
pop() {
|
|
69
|
+
const keys = this.keys;
|
|
70
|
+
const values = this.values;
|
|
71
|
+
const topValue = values[0];
|
|
72
|
+
this.lastKey = keys[0];
|
|
73
|
+
const key = keys.pop();
|
|
74
|
+
const value = values.pop();
|
|
75
|
+
const n = keys.length;
|
|
76
|
+
if (n > 0) {
|
|
77
|
+
let i = 0;
|
|
78
|
+
for (;;) {
|
|
79
|
+
let child = 2 * i + 1;
|
|
80
|
+
if (child >= n)
|
|
81
|
+
break;
|
|
82
|
+
if (child + 1 < n && keys[child + 1] < keys[child])
|
|
83
|
+
child++;
|
|
84
|
+
if (keys[child] >= key)
|
|
85
|
+
break;
|
|
86
|
+
keys[i] = keys[child];
|
|
87
|
+
values[i] = values[child];
|
|
88
|
+
i = child;
|
|
89
|
+
}
|
|
90
|
+
keys[i] = key;
|
|
91
|
+
values[i] = value;
|
|
92
|
+
}
|
|
93
|
+
return topValue;
|
|
94
|
+
}
|
|
95
|
+
lastKey = 0;
|
|
96
|
+
}
|
|
97
|
+
/** Lanes one pitch apart must clear the near-parallel proximity (8 px) so they read as distinct lines. */
|
|
98
|
+
const MIN_PITCH = 9;
|
|
99
|
+
function choosePitch(width, height) {
|
|
100
|
+
const area = Math.max(1, width * height);
|
|
101
|
+
return Math.min(20, Math.max(MIN_PITCH, Math.round(Math.sqrt(area / 250000))));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Route every request. Returns a polyline per request id; requests whose
|
|
105
|
+
* endpoints are the same node get an empty polyline.
|
|
106
|
+
*/
|
|
107
|
+
export function routeOrthogonal(requests, obstacles, options) {
|
|
108
|
+
const margin = options.margin ?? 6;
|
|
109
|
+
const crossingCost = options.crossingCost ?? CROSSING_COST;
|
|
110
|
+
const sharedLaneCost = options.sharedLaneCost ?? SHARED_LANE_COST;
|
|
111
|
+
const pitch = options.pitch ?? choosePitch(options.width, options.height);
|
|
112
|
+
const cols = Math.ceil(options.width / pitch) + 1;
|
|
113
|
+
const rows = Math.ceil(options.height / pitch) + 1;
|
|
114
|
+
const vertexCount = cols * rows;
|
|
115
|
+
// Blocked lattice vertices: strictly inside any node rectangle grown by the margin.
|
|
116
|
+
const blocked = new Uint8Array(vertexCount);
|
|
117
|
+
for (const node of obstacles) {
|
|
118
|
+
const c0 = Math.max(0, Math.floor((node.x - margin) / pitch) + 1);
|
|
119
|
+
const c1 = Math.min(cols - 1, Math.ceil((node.x + node.width + margin) / pitch) - 1);
|
|
120
|
+
const r0 = Math.max(0, Math.floor((node.y - margin) / pitch) + 1);
|
|
121
|
+
const r1 = Math.min(rows - 1, Math.ceil((node.y + node.height + margin) / pitch) - 1);
|
|
122
|
+
for (let r = r0; r <= r1; r++) {
|
|
123
|
+
for (let c = c0; c <= c1; c++)
|
|
124
|
+
blocked[r * cols + c] = 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Lattice vertices hugging a frame border; `nearV` marks vertical sides (x),
|
|
128
|
+
// `nearH` horizontal ones (y). Only moves *along* the side are penalised.
|
|
129
|
+
const nearV = new Uint8Array(vertexCount);
|
|
130
|
+
const nearH = new Uint8Array(vertexCount);
|
|
131
|
+
for (const f of options.frames ?? []) {
|
|
132
|
+
const markCols = (x, y0, y1) => {
|
|
133
|
+
const c0 = Math.max(0, Math.ceil((x - FRAME_CLEARANCE) / pitch));
|
|
134
|
+
const c1 = Math.min(cols - 1, Math.floor((x + FRAME_CLEARANCE) / pitch));
|
|
135
|
+
const r0 = Math.max(0, Math.ceil(y0 / pitch));
|
|
136
|
+
const r1 = Math.min(rows - 1, Math.floor(y1 / pitch));
|
|
137
|
+
for (let r = r0; r <= r1; r++)
|
|
138
|
+
for (let c = c0; c <= c1; c++)
|
|
139
|
+
nearV[r * cols + c] = 1;
|
|
140
|
+
};
|
|
141
|
+
const markRows = (y, x0, x1) => {
|
|
142
|
+
const r0 = Math.max(0, Math.ceil((y - FRAME_CLEARANCE) / pitch));
|
|
143
|
+
const r1 = Math.min(rows - 1, Math.floor((y + FRAME_CLEARANCE) / pitch));
|
|
144
|
+
const c0 = Math.max(0, Math.ceil(x0 / pitch));
|
|
145
|
+
const c1 = Math.min(cols - 1, Math.floor(x1 / pitch));
|
|
146
|
+
for (let r = r0; r <= r1; r++)
|
|
147
|
+
for (let c = c0; c <= c1; c++)
|
|
148
|
+
nearH[r * cols + c] = 1;
|
|
149
|
+
};
|
|
150
|
+
markCols(f.x, f.y, f.y + f.height);
|
|
151
|
+
markCols(f.x + f.width, f.y, f.y + f.height);
|
|
152
|
+
markRows(f.y, f.x, f.x + f.width);
|
|
153
|
+
markRows(f.y + f.height, f.x, f.x + f.width);
|
|
154
|
+
}
|
|
155
|
+
// Deepest package frame whose interior holds each lattice vertex (-1 = none).
|
|
156
|
+
// Frames nest, so painting larger frames first lets smaller ones overwrite.
|
|
157
|
+
const frameList = options.frames ?? [];
|
|
158
|
+
const frameOf = new Int32Array(vertexCount).fill(-1);
|
|
159
|
+
frameList
|
|
160
|
+
.map((f, index) => ({ f, index }))
|
|
161
|
+
.sort((a, b) => b.f.width * b.f.height - a.f.width * a.f.height || a.index - b.index)
|
|
162
|
+
.forEach(({ f, index }) => {
|
|
163
|
+
const c0 = Math.max(0, Math.floor((f.x + 2) / pitch) + 1);
|
|
164
|
+
const c1 = Math.min(cols - 1, Math.ceil((f.x + f.width - 2) / pitch) - 1);
|
|
165
|
+
const r0 = Math.max(0, Math.floor((f.y + 2) / pitch) + 1);
|
|
166
|
+
const r1 = Math.min(rows - 1, Math.ceil((f.y + f.height - 2) / pitch) - 1);
|
|
167
|
+
for (let r = r0; r <= r1; r++)
|
|
168
|
+
for (let c = c0; c <= c1; c++)
|
|
169
|
+
frameOf[r * cols + c] = index;
|
|
170
|
+
});
|
|
171
|
+
/** Frames holding the centre of `node`: the ones a wire to or from it may cross freely. */
|
|
172
|
+
const framesAround = (node) => {
|
|
173
|
+
const cx = node.x + node.width / 2;
|
|
174
|
+
const cy = node.y + node.height / 2;
|
|
175
|
+
const out = [];
|
|
176
|
+
frameList.forEach((f, index) => {
|
|
177
|
+
if (cx > f.x && cx < f.x + f.width && cy > f.y && cy < f.y + f.height)
|
|
178
|
+
out.push(index);
|
|
179
|
+
});
|
|
180
|
+
return out;
|
|
181
|
+
};
|
|
182
|
+
const stateCount = vertexCount * 4;
|
|
183
|
+
const hCol = new Float32Array(cols);
|
|
184
|
+
const hRow = new Float32Array(rows);
|
|
185
|
+
const goalStamp = new Int32Array(vertexCount);
|
|
186
|
+
const ownFrames = new Uint8Array(frameList.length);
|
|
187
|
+
const gScore = new Float32Array(stateCount);
|
|
188
|
+
const stamp = new Int32Array(stateCount);
|
|
189
|
+
const parent = new Int32Array(stateCount);
|
|
190
|
+
// Wires through each lattice vertex, per axis (counts, so a wire can be lifted again).
|
|
191
|
+
const usedH = new Uint16Array(vertexCount);
|
|
192
|
+
const usedV = new Uint16Array(vertexCount);
|
|
193
|
+
// Who used each lane: the source (src*) and target (dst*) node index + 1 of the
|
|
194
|
+
// wires through the vertex, or -1 once wires with different ends share it.
|
|
195
|
+
const srcH = new Int32Array(vertexCount);
|
|
196
|
+
const srcV = new Int32Array(vertexCount);
|
|
197
|
+
const dstH = new Int32Array(vertexCount);
|
|
198
|
+
const dstV = new Int32Array(vertexCount);
|
|
199
|
+
const nodeIndex = new Map();
|
|
200
|
+
const indexOf = (node) => {
|
|
201
|
+
let i = nodeIndex.get(node.id);
|
|
202
|
+
if (i === undefined)
|
|
203
|
+
nodeIndex.set(node.id, (i = nodeIndex.size + 1));
|
|
204
|
+
return i;
|
|
205
|
+
};
|
|
206
|
+
const claim = (owners, vertex, who) => {
|
|
207
|
+
const cur = owners[vertex];
|
|
208
|
+
if (cur === 0)
|
|
209
|
+
owners[vertex] = who;
|
|
210
|
+
else if (cur !== who)
|
|
211
|
+
owners[vertex] = -1;
|
|
212
|
+
};
|
|
213
|
+
for (const wire of options.existing ?? []) {
|
|
214
|
+
const src = indexOf({ id: wire.source });
|
|
215
|
+
const dst = indexOf({ id: wire.target });
|
|
216
|
+
for (let i = 0; i + 1 < wire.points.length; i++) {
|
|
217
|
+
const a = wire.points[i];
|
|
218
|
+
const b = wire.points[i + 1];
|
|
219
|
+
const horizontal = Math.abs(a.y - b.y) < 1e-6;
|
|
220
|
+
if (!horizontal && Math.abs(a.x - b.x) > 1e-6)
|
|
221
|
+
continue;
|
|
222
|
+
const mark = horizontal ? usedH : usedV;
|
|
223
|
+
const srcOwners = horizontal ? srcH : srcV;
|
|
224
|
+
const dstOwners = horizontal ? dstH : dstV;
|
|
225
|
+
const fixed = Math.round((horizontal ? a.y : a.x) / pitch);
|
|
226
|
+
const from = Math.round(Math.min(horizontal ? a.x : a.y, horizontal ? b.x : b.y) / pitch);
|
|
227
|
+
const to = Math.round(Math.max(horizontal ? a.x : a.y, horizontal ? b.x : b.y) / pitch);
|
|
228
|
+
for (let k = from; k <= to; k++) {
|
|
229
|
+
const c = horizontal ? k : fixed;
|
|
230
|
+
const r = horizontal ? fixed : k;
|
|
231
|
+
if (c < 0 || r < 0 || c >= cols || r >= rows)
|
|
232
|
+
continue;
|
|
233
|
+
const v = r * cols + c;
|
|
234
|
+
mark[v]++;
|
|
235
|
+
claim(srcOwners, v, src);
|
|
236
|
+
claim(dstOwners, v, dst);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let generation = 0;
|
|
241
|
+
const terminalsOf = (node) => {
|
|
242
|
+
const out = [];
|
|
243
|
+
const addVertex = (c, r, border, outward) => {
|
|
244
|
+
if (c < 0 || r < 0 || c >= cols || r >= rows)
|
|
245
|
+
return;
|
|
246
|
+
const vertex = r * cols + c;
|
|
247
|
+
if (blocked[vertex])
|
|
248
|
+
return;
|
|
249
|
+
out.push({ vertex, border, outward });
|
|
250
|
+
};
|
|
251
|
+
const colLo = Math.ceil((node.x + 4) / pitch);
|
|
252
|
+
const colHi = Math.floor((node.x + node.width - 4) / pitch);
|
|
253
|
+
const rowLo = Math.ceil((node.y + 4) / pitch);
|
|
254
|
+
const rowHi = Math.floor((node.y + node.height - 4) / pitch);
|
|
255
|
+
const topRow = Math.floor((node.y - margin) / pitch);
|
|
256
|
+
const bottomRow = Math.ceil((node.y + node.height + margin) / pitch);
|
|
257
|
+
const leftCol = Math.floor((node.x - margin) / pitch);
|
|
258
|
+
const rightCol = Math.ceil((node.x + node.width + margin) / pitch);
|
|
259
|
+
for (let c = colLo; c <= colHi; c++) {
|
|
260
|
+
addVertex(c, topRow, { x: c * pitch, y: node.y }, 0);
|
|
261
|
+
addVertex(c, bottomRow, { x: c * pitch, y: node.y + node.height }, 2);
|
|
262
|
+
}
|
|
263
|
+
for (let r = rowLo; r <= rowHi; r++) {
|
|
264
|
+
addVertex(rightCol, r, { x: node.x + node.width, y: r * pitch }, 1);
|
|
265
|
+
addVertex(leftCol, r, { x: node.x, y: r * pitch }, 3);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
};
|
|
269
|
+
const sorted = requests
|
|
270
|
+
.map((request, index) => ({ request, index }))
|
|
271
|
+
.sort((a, b) => {
|
|
272
|
+
const da = manhattan(a.request.source, a.request.target);
|
|
273
|
+
const db = manhattan(b.request.source, b.request.target);
|
|
274
|
+
return da - db || a.index - b.index;
|
|
275
|
+
});
|
|
276
|
+
const result = new Map();
|
|
277
|
+
const routed = new Map();
|
|
278
|
+
for (const { request } of sorted) {
|
|
279
|
+
if (request.source.id === request.target.id) {
|
|
280
|
+
result.set(request.id, []);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const path = routeOne(request);
|
|
284
|
+
if (path) {
|
|
285
|
+
mark(path.vertices, request, 1);
|
|
286
|
+
routed.set(request.id, { request, vertices: path.vertices });
|
|
287
|
+
}
|
|
288
|
+
result.set(request.id, path?.points ?? dogLeg(request.source, request.target));
|
|
289
|
+
}
|
|
290
|
+
// Rip-up and re-route: wires that still cross others are lifted one at a time
|
|
291
|
+
// and routed again against everything else, which can only keep or lower
|
|
292
|
+
// their lattice cost. Crossings with the fixed (`existing`) wires count too.
|
|
293
|
+
const passes = options.ripupPasses ?? RIPUP_PASSES;
|
|
294
|
+
let lastTotal = Infinity;
|
|
295
|
+
for (let pass = 0; pass < passes && routed.size > 0; pass++) {
|
|
296
|
+
const ids = [...routed.keys()];
|
|
297
|
+
const lines = ids.map((id) => result.get(id));
|
|
298
|
+
const fixed = (options.existing ?? []).map((w) => w.points);
|
|
299
|
+
const counts = crossingCounts(lines, fixed);
|
|
300
|
+
const total = counts.reduce((sum, n) => sum + n, 0);
|
|
301
|
+
if (total >= lastTotal)
|
|
302
|
+
break;
|
|
303
|
+
lastTotal = total;
|
|
304
|
+
const order = ids
|
|
305
|
+
.map((id, i) => ({ id, n: counts[i] }))
|
|
306
|
+
.filter((c) => c.n > 0)
|
|
307
|
+
.sort((a, b) => b.n - a.n || (a.id < b.id ? -1 : 1))
|
|
308
|
+
.slice(0, RIPUP_MAX);
|
|
309
|
+
if (order.length === 0)
|
|
310
|
+
break;
|
|
311
|
+
for (const { id } of order) {
|
|
312
|
+
const entry = routed.get(id);
|
|
313
|
+
mark(entry.vertices, entry.request, -1);
|
|
314
|
+
const path = routeOne(entry.request);
|
|
315
|
+
if (path) {
|
|
316
|
+
mark(path.vertices, entry.request, 1);
|
|
317
|
+
entry.vertices = path.vertices;
|
|
318
|
+
result.set(id, path.points);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
mark(entry.vertices, entry.request, 1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
/** Add (+1) or remove (−1) a route's lane usage; ownership is only ever claimed. */
|
|
327
|
+
function mark(vertices, request, delta) {
|
|
328
|
+
const srcId = indexOf(request.source);
|
|
329
|
+
const dstId = indexOf(request.target);
|
|
330
|
+
for (let i = 0; i + 1 < vertices.length; i++) {
|
|
331
|
+
const a = vertices[i];
|
|
332
|
+
const b = vertices[i + 1];
|
|
333
|
+
const horizontal = Math.abs(a - b) === 1;
|
|
334
|
+
const lane = horizontal ? usedH : usedV;
|
|
335
|
+
lane[a] += delta;
|
|
336
|
+
lane[b] += delta;
|
|
337
|
+
if (delta > 0) {
|
|
338
|
+
const srcOwners = horizontal ? srcH : srcV;
|
|
339
|
+
const dstOwners = horizontal ? dstH : dstV;
|
|
340
|
+
claim(srcOwners, a, srcId);
|
|
341
|
+
claim(srcOwners, b, srcId);
|
|
342
|
+
claim(dstOwners, a, dstId);
|
|
343
|
+
claim(dstOwners, b, dstId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/** Route inside a window around the two nodes first (fast); the whole lattice only if that finds nothing. */
|
|
348
|
+
function routeOne(request) {
|
|
349
|
+
const { source, target } = request;
|
|
350
|
+
const x0 = Math.min(source.x, target.x);
|
|
351
|
+
const x1 = Math.max(source.x + source.width, target.x + target.width);
|
|
352
|
+
const y0 = Math.min(source.y, target.y);
|
|
353
|
+
const y1 = Math.max(source.y + source.height, target.y + target.height);
|
|
354
|
+
const reach = Math.max(WINDOW_MIN, WINDOW_FRACTION * Math.max(x1 - x0, y1 - y0));
|
|
355
|
+
const window = {
|
|
356
|
+
c0: Math.max(0, Math.floor((x0 - reach) / pitch)),
|
|
357
|
+
c1: Math.min(cols - 1, Math.ceil((x1 + reach) / pitch)),
|
|
358
|
+
r0: Math.max(0, Math.floor((y0 - reach) / pitch)),
|
|
359
|
+
r1: Math.min(rows - 1, Math.ceil((y1 + reach) / pitch)),
|
|
360
|
+
};
|
|
361
|
+
const whole = window.c0 === 0 && window.r0 === 0 && window.c1 === cols - 1 && window.r1 === rows - 1;
|
|
362
|
+
return searchPath(request, window) ?? (whole ? undefined : searchPath(request, { c0: 0, r0: 0, c1: cols - 1, r1: rows - 1 }));
|
|
363
|
+
}
|
|
364
|
+
function searchPath(request, win) {
|
|
365
|
+
const starts = terminalsOf(request.source);
|
|
366
|
+
const goals = terminalsOf(request.target);
|
|
367
|
+
if (starts.length === 0 || goals.length === 0)
|
|
368
|
+
return undefined;
|
|
369
|
+
generation++;
|
|
370
|
+
const goalsByVertex = new Map();
|
|
371
|
+
for (const goal of goals) {
|
|
372
|
+
const list = goalsByVertex.get(goal.vertex);
|
|
373
|
+
if (list)
|
|
374
|
+
list.push(goal);
|
|
375
|
+
else
|
|
376
|
+
goalsByVertex.set(goal.vertex, [goal]);
|
|
377
|
+
}
|
|
378
|
+
// Manhattan distance to the target box, split per column and per row.
|
|
379
|
+
const tx0 = request.target.x;
|
|
380
|
+
const tx1 = request.target.x + request.target.width;
|
|
381
|
+
const ty0 = request.target.y;
|
|
382
|
+
const ty1 = request.target.y + request.target.height;
|
|
383
|
+
for (let c = 0; c < cols; c++) {
|
|
384
|
+
const x = c * pitch;
|
|
385
|
+
hCol[c] = Math.max(0, tx0 - x, x - tx1) / pitch;
|
|
386
|
+
}
|
|
387
|
+
for (let r = 0; r < rows; r++) {
|
|
388
|
+
const y = r * pitch;
|
|
389
|
+
hRow[r] = Math.max(0, ty0 - y, y - ty1) / pitch;
|
|
390
|
+
}
|
|
391
|
+
for (const goal of goals)
|
|
392
|
+
goalStamp[goal.vertex] = generation;
|
|
393
|
+
const heuristic = (vertex) => {
|
|
394
|
+
const c = vertex % cols;
|
|
395
|
+
return hCol[c] + hRow[(vertex - c) / cols];
|
|
396
|
+
};
|
|
397
|
+
const srcId = indexOf(request.source);
|
|
398
|
+
const dstId = indexOf(request.target);
|
|
399
|
+
ownFrames.fill(0);
|
|
400
|
+
for (const f of framesAround(request.source))
|
|
401
|
+
ownFrames[f] = 1;
|
|
402
|
+
for (const f of framesAround(request.target))
|
|
403
|
+
ownFrames[f] = 1;
|
|
404
|
+
const heap = new MinHeap();
|
|
405
|
+
// Goal entries are encoded as stateCount + index into `goalEntries`.
|
|
406
|
+
const goalEntries = [];
|
|
407
|
+
const startOf = new Map();
|
|
408
|
+
for (const start of starts) {
|
|
409
|
+
const state = start.vertex * 4 + start.outward;
|
|
410
|
+
if (stamp[state] === generation && gScore[state] <= 0)
|
|
411
|
+
continue;
|
|
412
|
+
stamp[state] = generation;
|
|
413
|
+
gScore[state] = 0;
|
|
414
|
+
parent[state] = -1;
|
|
415
|
+
startOf.set(state, start);
|
|
416
|
+
heap.push(heuristic(start.vertex), state);
|
|
417
|
+
}
|
|
418
|
+
let found;
|
|
419
|
+
while (heap.size > 0) {
|
|
420
|
+
const entry = heap.pop();
|
|
421
|
+
const lastKey = heap.lastKey;
|
|
422
|
+
if (entry >= stateCount) {
|
|
423
|
+
found = goalEntries[entry - stateCount];
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
const state = entry;
|
|
427
|
+
const g = gScore[state];
|
|
428
|
+
if (lastKey > g + heuristic(state >> 2) + 1e-6)
|
|
429
|
+
continue; // stale entry
|
|
430
|
+
const vertex = state >> 2;
|
|
431
|
+
const dir = state & 3;
|
|
432
|
+
const here = goalStamp[vertex] === generation ? goalsByVertex.get(vertex) : undefined;
|
|
433
|
+
if (here) {
|
|
434
|
+
for (const terminal of here) {
|
|
435
|
+
// Arriving head-on (moving opposite to the terminal's outward side) is free.
|
|
436
|
+
const inward = (terminal.outward + 2) & 3;
|
|
437
|
+
const total = g + (dir === inward ? 0 : BEND_COST);
|
|
438
|
+
goalEntries.push({ state, terminal });
|
|
439
|
+
heap.push(total, stateCount + goalEntries.length - 1);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const c = vertex % cols;
|
|
443
|
+
const r = (vertex - c) / cols;
|
|
444
|
+
for (let nd = 0; nd < 4; nd++) {
|
|
445
|
+
if (((nd + 2) & 3) === dir)
|
|
446
|
+
continue; // no U-turns
|
|
447
|
+
const nc = c + DX[nd];
|
|
448
|
+
const nr = r + DY[nd];
|
|
449
|
+
if (nc < win.c0 || nr < win.r0 || nc > win.c1 || nr > win.r1)
|
|
450
|
+
continue;
|
|
451
|
+
const nv = nr * cols + nc;
|
|
452
|
+
if (blocked[nv])
|
|
453
|
+
continue;
|
|
454
|
+
let step = 1;
|
|
455
|
+
if (nd !== dir)
|
|
456
|
+
step += BEND_COST;
|
|
457
|
+
if (nd & 1 ? nearH[nv] : nearV[nv])
|
|
458
|
+
step += FRAME_FLUSH_COST;
|
|
459
|
+
const fo = frameOf[nv];
|
|
460
|
+
if (fo >= 0 && ownFrames[fo] === 0)
|
|
461
|
+
step += FOREIGN_FRAME_COST;
|
|
462
|
+
if (nd & 1) {
|
|
463
|
+
if (usedH[nv]) {
|
|
464
|
+
if (srcH[nv] === srcId || dstH[nv] === dstId)
|
|
465
|
+
step = Math.min(step, step - 1 + GROUP_LANE_COST);
|
|
466
|
+
else
|
|
467
|
+
step += sharedLaneCost;
|
|
468
|
+
}
|
|
469
|
+
if (usedV[nv])
|
|
470
|
+
step += crossingCost;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
if (usedV[nv]) {
|
|
474
|
+
if (srcV[nv] === srcId || dstV[nv] === dstId)
|
|
475
|
+
step = Math.min(step, step - 1 + GROUP_LANE_COST);
|
|
476
|
+
else
|
|
477
|
+
step += sharedLaneCost;
|
|
478
|
+
}
|
|
479
|
+
if (usedH[nv])
|
|
480
|
+
step += crossingCost;
|
|
481
|
+
}
|
|
482
|
+
const ns = nv * 4 + nd;
|
|
483
|
+
const ng = g + step;
|
|
484
|
+
if (stamp[ns] === generation && gScore[ns] <= ng)
|
|
485
|
+
continue;
|
|
486
|
+
stamp[ns] = generation;
|
|
487
|
+
gScore[ns] = ng;
|
|
488
|
+
parent[ns] = state;
|
|
489
|
+
heap.push(ng + heuristic(nv), ns);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!found)
|
|
493
|
+
return undefined;
|
|
494
|
+
// Walk parents back to a start terminal.
|
|
495
|
+
const vertices = [];
|
|
496
|
+
let state = found.state;
|
|
497
|
+
let startTerminal;
|
|
498
|
+
for (;;) {
|
|
499
|
+
vertices.push(state >> 2);
|
|
500
|
+
const p = parent[state];
|
|
501
|
+
if (p < 0) {
|
|
502
|
+
startTerminal = startOf.get(state);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
state = p;
|
|
506
|
+
}
|
|
507
|
+
vertices.reverse();
|
|
508
|
+
if (!startTerminal)
|
|
509
|
+
return undefined;
|
|
510
|
+
const points = [startTerminal.border];
|
|
511
|
+
for (const v of vertices) {
|
|
512
|
+
points.push({ x: (v % cols) * pitch, y: Math.floor(v / cols) * pitch });
|
|
513
|
+
}
|
|
514
|
+
points.push(found.terminal.border);
|
|
515
|
+
return { points: simplify(points), vertices };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* For each of `lines`, the number of proper crossings with every other line in
|
|
520
|
+
* `lines` and with the static `fixed` wires (axis-aligned segments only).
|
|
521
|
+
*/
|
|
522
|
+
function crossingCounts(lines, fixed) {
|
|
523
|
+
const segs = [];
|
|
524
|
+
const add = (pts, owner) => {
|
|
525
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
526
|
+
const a = pts[i];
|
|
527
|
+
const b = pts[i + 1];
|
|
528
|
+
if (Math.abs(a.y - b.y) < 1e-6)
|
|
529
|
+
segs.push({ owner, horizontal: true, c: a.y, lo: Math.min(a.x, b.x), hi: Math.max(a.x, b.x) });
|
|
530
|
+
else if (Math.abs(a.x - b.x) < 1e-6)
|
|
531
|
+
segs.push({ owner, horizontal: false, c: a.x, lo: Math.min(a.y, b.y), hi: Math.max(a.y, b.y) });
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
lines.forEach((pts, i) => add(pts, i));
|
|
535
|
+
fixed.forEach((pts) => add(pts, -1));
|
|
536
|
+
const horizontals = segs.filter((s) => s.horizontal).sort((a, b) => a.c - b.c);
|
|
537
|
+
const verticals = segs.filter((s) => !s.horizontal);
|
|
538
|
+
const counts = lines.map(() => 0);
|
|
539
|
+
for (const v of verticals) {
|
|
540
|
+
// First horizontal with c > v.lo (binary search), then scan while c < v.hi.
|
|
541
|
+
let lo = 0;
|
|
542
|
+
let hi = horizontals.length;
|
|
543
|
+
while (lo < hi) {
|
|
544
|
+
const mid = (lo + hi) >> 1;
|
|
545
|
+
if (horizontals[mid].c <= v.lo + 1e-6)
|
|
546
|
+
lo = mid + 1;
|
|
547
|
+
else
|
|
548
|
+
hi = mid;
|
|
549
|
+
}
|
|
550
|
+
for (let k = lo; k < horizontals.length && horizontals[k].c < v.hi - 1e-6; k++) {
|
|
551
|
+
const h = horizontals[k];
|
|
552
|
+
if (h.owner === v.owner || h.lo >= v.c - 1e-6 || h.hi <= v.c + 1e-6)
|
|
553
|
+
continue;
|
|
554
|
+
if (v.owner >= 0)
|
|
555
|
+
counts[v.owner]++;
|
|
556
|
+
if (h.owner >= 0)
|
|
557
|
+
counts[h.owner]++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return counts;
|
|
561
|
+
}
|
|
562
|
+
function manhattan(a, b) {
|
|
563
|
+
return (Math.abs(a.x + a.width / 2 - (b.x + b.width / 2)) +
|
|
564
|
+
Math.abs(a.y + a.height / 2 - (b.y + b.height / 2)));
|
|
565
|
+
}
|
|
566
|
+
/** Drop repeated and collinear interior points. */
|
|
567
|
+
function simplify(points) {
|
|
568
|
+
const out = [];
|
|
569
|
+
for (const p of points) {
|
|
570
|
+
const last = out[out.length - 1];
|
|
571
|
+
if (last && Math.abs(last.x - p.x) < 0.01 && Math.abs(last.y - p.y) < 0.01)
|
|
572
|
+
continue;
|
|
573
|
+
out.push(p);
|
|
574
|
+
}
|
|
575
|
+
for (let i = out.length - 2; i >= 1; i--) {
|
|
576
|
+
const a = out[i - 1];
|
|
577
|
+
const b = out[i];
|
|
578
|
+
const c = out[i + 1];
|
|
579
|
+
const sameX = Math.abs(a.x - b.x) < 0.01 && Math.abs(b.x - c.x) < 0.01;
|
|
580
|
+
const sameY = Math.abs(a.y - b.y) < 0.01 && Math.abs(b.y - c.y) < 0.01;
|
|
581
|
+
if (sameX || sameY)
|
|
582
|
+
out.splice(i, 1);
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
/** Last-resort orthogonal route when the lattice search finds no path. */
|
|
587
|
+
function dogLeg(source, target) {
|
|
588
|
+
const sx = source.x + source.width / 2;
|
|
589
|
+
const sy = source.y + source.height / 2;
|
|
590
|
+
const tx = target.x + target.width / 2;
|
|
591
|
+
const ty = target.y + target.height / 2;
|
|
592
|
+
const vertical = source.y + source.height <= target.y || target.y + target.height <= source.y;
|
|
593
|
+
if (vertical) {
|
|
594
|
+
const down = ty > sy;
|
|
595
|
+
const y0 = down ? source.y + source.height : source.y;
|
|
596
|
+
const y1 = down ? target.y : target.y + target.height;
|
|
597
|
+
const mid = (y0 + y1) / 2;
|
|
598
|
+
return simplify([
|
|
599
|
+
{ x: sx, y: y0 },
|
|
600
|
+
{ x: sx, y: mid },
|
|
601
|
+
{ x: tx, y: mid },
|
|
602
|
+
{ x: tx, y: y1 },
|
|
603
|
+
]);
|
|
604
|
+
}
|
|
605
|
+
const right = tx > sx;
|
|
606
|
+
const x0 = right ? source.x + source.width : source.x;
|
|
607
|
+
const x1 = right ? target.x : target.x + target.width;
|
|
608
|
+
const mid = (x0 + x1) / 2;
|
|
609
|
+
return simplify([
|
|
610
|
+
{ x: x0, y: sy },
|
|
611
|
+
{ x: mid, y: sy },
|
|
612
|
+
{ x: mid, y: ty },
|
|
613
|
+
{ x: x1, y: ty },
|
|
614
|
+
]);
|
|
615
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane spreading. The grid router and ELK both let unrelated orthogonal edges
|
|
3
|
+
* share one channel centre, so several edges end up drawn on top of each other
|
|
4
|
+
* and read as one thicker line. This pass fans such coincident (or nearly
|
|
5
|
+
* coincident) interior segments out onto distinct, evenly spaced lanes around
|
|
6
|
+
* their common centre. Only interior segments move — the first and last
|
|
7
|
+
* segments stay attached to their ports — and a move is dropped when it would
|
|
8
|
+
* run through a node or fold an adjacent segment back on itself.
|
|
9
|
+
*/
|
|
10
|
+
import { LayoutResult } from '../foundation/layout.js';
|
|
11
|
+
export declare function spreadParallelLanes(layout: LayoutResult): LayoutResult;
|