dsh-model-everything 0.5.0 → 0.5.2
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.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2829 -224
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +1227 -69
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +146 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/trunk.js +213 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +157 -10
- package/lib/types/render/place.d.ts +40 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/trunk.d.ts +104 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +85 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1570 -290
- package/src/render/place.ts +532 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +274 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/trunk.ts +275 -0
- package/src/render/wrap.ts +103 -0
package/lib/client.js
CHANGED
|
@@ -37,17 +37,844 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
});
|
|
38
38
|
module.exports = __toCommonJS(client_exports);
|
|
39
39
|
|
|
40
|
+
// src/render/components.ts
|
|
41
|
+
var PAGE_ASPECT = 1.6;
|
|
42
|
+
function weakComponents(nodes, edges) {
|
|
43
|
+
const known = new Set(nodes.map((node) => node.id));
|
|
44
|
+
const neighbours = /* @__PURE__ */ new Map();
|
|
45
|
+
const link = (from, to) => {
|
|
46
|
+
neighbours.set(from, [...neighbours.get(from) ?? [], to]);
|
|
47
|
+
};
|
|
48
|
+
for (const edge of edges) {
|
|
49
|
+
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
|
|
50
|
+
link(edge.from, edge.to);
|
|
51
|
+
link(edge.to, edge.from);
|
|
52
|
+
}
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const components = [];
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
if (seen.has(node.id)) continue;
|
|
57
|
+
const members = [];
|
|
58
|
+
const queue = [node.id];
|
|
59
|
+
seen.add(node.id);
|
|
60
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
61
|
+
const id = queue[head];
|
|
62
|
+
members.push(id);
|
|
63
|
+
for (const next of neighbours.get(id) ?? []) {
|
|
64
|
+
if (seen.has(next)) continue;
|
|
65
|
+
seen.add(next);
|
|
66
|
+
queue.push(next);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
components.push(members);
|
|
70
|
+
}
|
|
71
|
+
return components;
|
|
72
|
+
}
|
|
73
|
+
function packRectangles(rectangles, options = {}) {
|
|
74
|
+
const gap = options.gap ?? 30;
|
|
75
|
+
const aspect = options.aspect ?? PAGE_ASPECT;
|
|
76
|
+
if (rectangles.length === 0) return { placements: [], width: 0, height: 0 };
|
|
77
|
+
const area = rectangles.reduce((total, rectangle) => total + (rectangle.width + gap) * (rectangle.height + gap), 0);
|
|
78
|
+
const targetWidth = Math.max(gap, Math.sqrt(area * aspect));
|
|
79
|
+
const order2 = rectangles.map((rectangle, index) => ({ rectangle, index })).sort((left, right) => right.rectangle.height - left.rectangle.height || right.rectangle.width - left.rectangle.width || left.index - right.index);
|
|
80
|
+
const placements = rectangles.map((rectangle) => ({ x: 0, y: 0, width: rectangle.width, height: rectangle.height }));
|
|
81
|
+
let x = 0;
|
|
82
|
+
let y = 0;
|
|
83
|
+
let shelfHeight = 0;
|
|
84
|
+
let width = 0;
|
|
85
|
+
for (const entry of order2) {
|
|
86
|
+
const rectangle = entry.rectangle;
|
|
87
|
+
if (x > 0 && x + rectangle.width > targetWidth) {
|
|
88
|
+
y += shelfHeight + gap;
|
|
89
|
+
x = 0;
|
|
90
|
+
shelfHeight = 0;
|
|
91
|
+
}
|
|
92
|
+
placements[entry.index] = { x, y, width: rectangle.width, height: rectangle.height };
|
|
93
|
+
x += rectangle.width + gap;
|
|
94
|
+
width = Math.max(width, x - gap);
|
|
95
|
+
shelfHeight = Math.max(shelfHeight, rectangle.height);
|
|
96
|
+
}
|
|
97
|
+
return { placements, width, height: y + shelfHeight };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/render/channels.ts
|
|
101
|
+
var PORT_INSET = 10;
|
|
102
|
+
function spanOf(box) {
|
|
103
|
+
const lo = box.x + PORT_INSET;
|
|
104
|
+
const hi = box.x + box.width - PORT_INSET;
|
|
105
|
+
return hi > lo ? { lo, hi } : { lo: box.x + box.width / 2, hi: box.x + box.width / 2 };
|
|
106
|
+
}
|
|
107
|
+
function clamp(value, span) {
|
|
108
|
+
return Math.min(span.hi, Math.max(span.lo, value));
|
|
109
|
+
}
|
|
110
|
+
function inside(value, span) {
|
|
111
|
+
return value >= span.lo - 1e-3 && value <= span.hi + 1e-3;
|
|
112
|
+
}
|
|
113
|
+
function cut(free, blocked) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const span of free) {
|
|
116
|
+
if (blocked.hi <= span.lo || blocked.lo >= span.hi) {
|
|
117
|
+
out.push(span);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (blocked.lo > span.lo) out.push({ lo: span.lo, hi: blocked.lo });
|
|
121
|
+
if (blocked.hi < span.hi) out.push({ lo: blocked.hi, hi: span.hi });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function atOrAfter(value, free) {
|
|
126
|
+
let found = value;
|
|
127
|
+
for (const span of free) {
|
|
128
|
+
if (span.hi < value) continue;
|
|
129
|
+
found = Math.max(value, span.lo);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
function choose(entry) {
|
|
135
|
+
const candidates = [entry.proposal, entry.source.lo, entry.source.hi, entry.target.lo, entry.target.hi];
|
|
136
|
+
candidates.push((clamp(entry.proposal, entry.source) + clamp(entry.proposal, entry.target)) / 2);
|
|
137
|
+
let best = entry.proposal;
|
|
138
|
+
let bestBends = Infinity;
|
|
139
|
+
let bestFar = Infinity;
|
|
140
|
+
let bestReach = Infinity;
|
|
141
|
+
for (const span of entry.free) {
|
|
142
|
+
for (const value of candidates) {
|
|
143
|
+
const x = clamp(value, span);
|
|
144
|
+
const bends = (inside(x, entry.source) ? 0 : 1) + (inside(x, entry.target) ? 0 : 1);
|
|
145
|
+
const far = Math.abs(x - entry.proposal);
|
|
146
|
+
const reach = Math.abs(x - clamp(x, entry.source)) + Math.abs(x - clamp(x, entry.target));
|
|
147
|
+
const better = bends < bestBends || bends === bestBends && (far < bestFar - 1e-3 || Math.abs(far - bestFar) <= 1e-3 && reach < bestReach);
|
|
148
|
+
if (!better) continue;
|
|
149
|
+
best = x;
|
|
150
|
+
bestBends = bends;
|
|
151
|
+
bestFar = far;
|
|
152
|
+
bestReach = reach;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return best;
|
|
156
|
+
}
|
|
157
|
+
function adjacentPorts(source, target) {
|
|
158
|
+
const fromMiddle = source.x + source.width / 2;
|
|
159
|
+
const toMiddle = target.x + target.width / 2;
|
|
160
|
+
const fromSpan = spanOf(source);
|
|
161
|
+
const toSpan = spanOf(target);
|
|
162
|
+
const chosen = inside(fromMiddle, toSpan) ? fromMiddle : clamp(toMiddle, fromSpan);
|
|
163
|
+
return { from: clamp(chosen, fromSpan), to: clamp(chosen, toSpan) };
|
|
164
|
+
}
|
|
165
|
+
function assignChannels(input, options) {
|
|
166
|
+
const { layers, chains, chainOf, boxes, layerY, layerHeight, laneX } = input;
|
|
167
|
+
const rowOf = /* @__PURE__ */ new Map();
|
|
168
|
+
layers.forEach((row, depth) => {
|
|
169
|
+
for (const id of row) rowOf.set(id, depth);
|
|
170
|
+
});
|
|
171
|
+
const long = [];
|
|
172
|
+
for (const chain of chains) {
|
|
173
|
+
if (chain.path.length <= 2) continue;
|
|
174
|
+
const head = chain.path[0];
|
|
175
|
+
const tail = chain.path[chain.path.length - 1];
|
|
176
|
+
const from = rowOf.get(head);
|
|
177
|
+
const to = rowOf.get(tail);
|
|
178
|
+
if (from === void 0 || to === void 0 || to <= from) continue;
|
|
179
|
+
const lane = chainOf.get(chain.path[1]);
|
|
180
|
+
const source = boxes.get(head);
|
|
181
|
+
const target = boxes.get(tail);
|
|
182
|
+
if (lane === void 0 || source === void 0 || target === void 0) continue;
|
|
183
|
+
let free = [{ lo: -Infinity, hi: Infinity }];
|
|
184
|
+
for (let depth = from + 1; depth < to; depth += 1) {
|
|
185
|
+
for (const id of layers[depth]) {
|
|
186
|
+
const box = boxes.get(id);
|
|
187
|
+
if (box === void 0) continue;
|
|
188
|
+
free = cut(free, { lo: box.x - options.clearance, hi: box.x + box.width + options.clearance });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const centre = (source.x + source.width / 2 + target.x + target.width / 2) / 2;
|
|
192
|
+
long.push({
|
|
193
|
+
edge: chain.index,
|
|
194
|
+
lane,
|
|
195
|
+
path: chain.path,
|
|
196
|
+
from,
|
|
197
|
+
to,
|
|
198
|
+
free,
|
|
199
|
+
source: spanOf(source),
|
|
200
|
+
target: spanOf(target),
|
|
201
|
+
proposal: laneX.get(lane) ?? centre,
|
|
202
|
+
x: 0
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
for (const entry of long) entry.x = choose(entry);
|
|
206
|
+
const byGap = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const entry of long) {
|
|
208
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) {
|
|
209
|
+
const row = gap + 1 < entry.to ? gap + 1 : gap;
|
|
210
|
+
const id = entry.path[row - entry.from];
|
|
211
|
+
const index = layers[row].indexOf(id);
|
|
212
|
+
byGap.set(gap, [...byGap.get(gap) ?? [], { entry, index }]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const [, list] of byGap) list.sort((left, right) => left.index - right.index);
|
|
216
|
+
for (let round = 0; round < 4; round += 1) {
|
|
217
|
+
let moved = false;
|
|
218
|
+
for (const [, list] of byGap) {
|
|
219
|
+
let previous = -Infinity;
|
|
220
|
+
for (const item of list) {
|
|
221
|
+
const x = item.entry.x < previous + options.spacing ? atOrAfter(previous + options.spacing, item.entry.free) : item.entry.x;
|
|
222
|
+
if (x !== item.entry.x) {
|
|
223
|
+
item.entry.x = x;
|
|
224
|
+
moved = true;
|
|
225
|
+
}
|
|
226
|
+
previous = item.entry.x;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!moved) break;
|
|
230
|
+
}
|
|
231
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
232
|
+
const want = (gap, edge, left) => {
|
|
233
|
+
stacks.set(gap, [...stacks.get(gap) ?? [], { edge, gap, left }]);
|
|
234
|
+
};
|
|
235
|
+
for (const entry of long) {
|
|
236
|
+
const head = clamp(entry.x, entry.source);
|
|
237
|
+
const tail = clamp(entry.x, entry.target);
|
|
238
|
+
if (Math.abs(head - entry.x) > 0.5) want(entry.from, entry.edge, Math.min(head, entry.x));
|
|
239
|
+
if (Math.abs(tail - entry.x) > 0.5) want(entry.to - 1, entry.edge, Math.min(tail, entry.x));
|
|
240
|
+
}
|
|
241
|
+
for (const chain of chains) {
|
|
242
|
+
if (chain.path.length > 2) continue;
|
|
243
|
+
const head = chain.path[0];
|
|
244
|
+
const tail = chain.path[chain.path.length - 1];
|
|
245
|
+
const from = rowOf.get(head);
|
|
246
|
+
const to = rowOf.get(tail);
|
|
247
|
+
if (from === void 0 || to === void 0 || Math.abs(from - to) !== 1) continue;
|
|
248
|
+
const source = boxes.get(head);
|
|
249
|
+
const target = boxes.get(tail);
|
|
250
|
+
if (source === void 0 || target === void 0) continue;
|
|
251
|
+
const ports = adjacentPorts(source, target);
|
|
252
|
+
want(Math.min(from, to), chain.index, Math.min(ports.from, ports.to));
|
|
253
|
+
}
|
|
254
|
+
const midOf = (gap) => (layerY[gap] + layerHeight[gap] + layerY[gap + 1]) / 2;
|
|
255
|
+
const heightOf = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const [gap, list] of stacks) {
|
|
257
|
+
const top = layerY[gap] + layerHeight[gap];
|
|
258
|
+
const bottom = layerY[gap + 1];
|
|
259
|
+
const pad = Math.min(8, Math.max(1, (bottom - top) / 3));
|
|
260
|
+
const lo = top + pad;
|
|
261
|
+
const hi = Math.max(lo, bottom - pad);
|
|
262
|
+
const sorted = [...list].sort((left, right) => left.left - right.left || left.edge - right.edge);
|
|
263
|
+
sorted.forEach((item, index) => {
|
|
264
|
+
const y = sorted.length === 1 ? (lo + hi) / 2 : lo + (hi - lo) * (index + 1) / (sorted.length + 1);
|
|
265
|
+
const heights = heightOf.get(item.edge) ?? /* @__PURE__ */ new Map();
|
|
266
|
+
heights.set(item.gap, y);
|
|
267
|
+
heightOf.set(item.edge, heights);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const lanes2 = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const entry of long) {
|
|
272
|
+
const y = [];
|
|
273
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) y.push(heightOf.get(entry.edge)?.get(gap) ?? midOf(gap));
|
|
274
|
+
lanes2.set(entry.edge, { x: entry.x, y });
|
|
275
|
+
}
|
|
276
|
+
const runs = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const [edge, heights] of heightOf) {
|
|
278
|
+
if (lanes2.has(edge)) continue;
|
|
279
|
+
runs.set(edge, [...heights.values()][0]);
|
|
280
|
+
}
|
|
281
|
+
return { lanes: lanes2, runs };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/render/coords.ts
|
|
285
|
+
function positions(rows) {
|
|
286
|
+
const at = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const row of rows) row.forEach((id, index) => at.set(id, index));
|
|
288
|
+
return at;
|
|
289
|
+
}
|
|
290
|
+
function alignBlocks(frame, at) {
|
|
291
|
+
const root = /* @__PURE__ */ new Map();
|
|
292
|
+
const joined = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const row of frame.rows) {
|
|
294
|
+
for (const id of row) {
|
|
295
|
+
root.set(id, id);
|
|
296
|
+
joined.set(id, id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (let depth = 1; depth < frame.rows.length; depth += 1) {
|
|
300
|
+
let previous = -1;
|
|
301
|
+
for (const id of frame.rows[depth]) {
|
|
302
|
+
const close = frame.near.get(id) ?? [];
|
|
303
|
+
if (close.length === 0) continue;
|
|
304
|
+
const sorted = [...close].sort((left, right) => at.get(left) - at.get(right));
|
|
305
|
+
const middle = (sorted.length - 1) / 2;
|
|
306
|
+
for (let cursor = Math.floor(middle); cursor <= Math.ceil(middle); cursor += 1) {
|
|
307
|
+
if (joined.get(id) !== id) break;
|
|
308
|
+
const target = sorted[cursor];
|
|
309
|
+
if (previous >= at.get(target)) continue;
|
|
310
|
+
joined.set(target, id);
|
|
311
|
+
root.set(id, root.get(target));
|
|
312
|
+
joined.set(id, root.get(id));
|
|
313
|
+
previous = at.get(target);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return root;
|
|
318
|
+
}
|
|
319
|
+
function compactBlocks(rows, root, widthOf, gap) {
|
|
320
|
+
const clears = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const row of rows) {
|
|
322
|
+
for (let index = 1; index < row.length; index += 1) {
|
|
323
|
+
const left = row[index - 1];
|
|
324
|
+
const right = row[index];
|
|
325
|
+
const leftBlock = root.get(left);
|
|
326
|
+
const rightBlock = root.get(right);
|
|
327
|
+
const space = (widthOf(left) + widthOf(right)) / 2 + gap;
|
|
328
|
+
clears.set(rightBlock, [...clears.get(rightBlock) ?? [], { block: leftBlock, space }]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const placed = /* @__PURE__ */ new Map();
|
|
332
|
+
const place2 = (block) => {
|
|
333
|
+
const known = placed.get(block);
|
|
334
|
+
if (known !== void 0) return known;
|
|
335
|
+
let value = 0;
|
|
336
|
+
for (const step of clears.get(block) ?? []) value = Math.max(value, place2(step.block) + step.space);
|
|
337
|
+
placed.set(block, value);
|
|
338
|
+
return value;
|
|
339
|
+
};
|
|
340
|
+
const x = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const row of rows) {
|
|
342
|
+
for (const id of row) {
|
|
343
|
+
const block = root.get(id);
|
|
344
|
+
if (!placed.has(block)) place2(block);
|
|
345
|
+
x.set(id, placed.get(block));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return x;
|
|
349
|
+
}
|
|
350
|
+
function extentOf(x, rows, widthOf) {
|
|
351
|
+
let left = Infinity;
|
|
352
|
+
let right = -Infinity;
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
for (const id of row) {
|
|
355
|
+
const value = x.get(id);
|
|
356
|
+
left = Math.min(left, value - widthOf(id) / 2);
|
|
357
|
+
right = Math.max(right, value + widthOf(id) / 2);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { left, right };
|
|
361
|
+
}
|
|
362
|
+
function onePass(frame, widthOf, gap) {
|
|
363
|
+
const at = positions(frame.rows);
|
|
364
|
+
const root = alignBlocks(frame, at);
|
|
365
|
+
const x = compactBlocks(frame.rows, root, widthOf, gap);
|
|
366
|
+
if (!frame.mirrored) return x;
|
|
367
|
+
const mirrored = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const [id, value] of x) mirrored.set(id, -value);
|
|
369
|
+
return mirrored;
|
|
370
|
+
}
|
|
371
|
+
function frames(layers, above, below) {
|
|
372
|
+
const fromTop = layers;
|
|
373
|
+
const fromBottom = [...layers].reverse();
|
|
374
|
+
const flip = (rows) => rows.map((row) => [...row].reverse());
|
|
375
|
+
return [
|
|
376
|
+
{ rows: fromTop, near: above, mirrored: false },
|
|
377
|
+
{ rows: flip(fromTop), near: above, mirrored: true },
|
|
378
|
+
{ rows: fromBottom, near: below, mirrored: false },
|
|
379
|
+
{ rows: flip(fromBottom), near: below, mirrored: true }
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
function assignCoordinates(input, options) {
|
|
383
|
+
const { layers, chains, sizeOf, chainOf } = input;
|
|
384
|
+
const gap = options.gap;
|
|
385
|
+
const laneIds = new Set(chainOf.values());
|
|
386
|
+
const isDummy = (id) => chainOf.has(id);
|
|
387
|
+
const isPoint = (id) => isDummy(id) || laneIds.has(id);
|
|
388
|
+
const widthOf = (id) => isPoint(id) ? 0 : sizeOf(id).width;
|
|
389
|
+
const heightOf = (id) => isPoint(id) ? 0 : sizeOf(id).height;
|
|
390
|
+
const laneOf = (id) => chainOf.get(id) ?? id;
|
|
391
|
+
const x = /* @__PURE__ */ new Map();
|
|
392
|
+
const read = (id) => x.get(laneOf(id));
|
|
393
|
+
const above = /* @__PURE__ */ new Map();
|
|
394
|
+
const below = /* @__PURE__ */ new Map();
|
|
395
|
+
const laneEnds = /* @__PURE__ */ new Map();
|
|
396
|
+
const link = (into, id, other) => {
|
|
397
|
+
into.set(id, [...into.get(id) ?? [], other]);
|
|
398
|
+
};
|
|
399
|
+
for (const chain of chains) {
|
|
400
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
401
|
+
const top2 = chain.path[cursor];
|
|
402
|
+
const bottom = chain.path[cursor + 1];
|
|
403
|
+
if (!isDummy(top2) && !isDummy(bottom)) {
|
|
404
|
+
link(below, top2, bottom);
|
|
405
|
+
link(above, bottom, top2);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (chain.path.length > 2) {
|
|
409
|
+
const lane = chainOf.get(chain.path[1]);
|
|
410
|
+
if (lane !== void 0) laneEnds.set(lane, [chain.path[0], chain.path[chain.path.length - 1]]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const candidates = frames(layers, above, below).map((frame) => onePass(frame, widthOf, gap));
|
|
414
|
+
const leftOrigin = extentOf(candidates[0], layers, widthOf).left;
|
|
415
|
+
const rightOrigin = extentOf(candidates[1], layers, widthOf).right;
|
|
416
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
417
|
+
const extent = extentOf(candidate, layers, widthOf);
|
|
418
|
+
const shift = index % 2 === 0 ? leftOrigin - extent.left : rightOrigin - extent.right;
|
|
419
|
+
for (const [id, value] of candidate) candidate.set(id, value + shift);
|
|
420
|
+
}
|
|
421
|
+
for (const row of layers) {
|
|
422
|
+
for (const id of row) {
|
|
423
|
+
let total = 0;
|
|
424
|
+
for (const candidate of candidates) total += candidate.get(id);
|
|
425
|
+
x.set(id, total / candidates.length);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const reserved = layers.map((layer2) => {
|
|
429
|
+
let lowest = Infinity;
|
|
430
|
+
let highest = -Infinity;
|
|
431
|
+
for (const id of layer2) {
|
|
432
|
+
const value = x.get(id);
|
|
433
|
+
lowest = Math.min(lowest, value - widthOf(id) / 2);
|
|
434
|
+
highest = Math.max(highest, value + widthOf(id) / 2);
|
|
435
|
+
}
|
|
436
|
+
return { lowest, highest };
|
|
437
|
+
});
|
|
438
|
+
for (const [lane, ends] of laneEnds) x.set(lane, (x.get(ends[0]) + x.get(ends[1])) / 2);
|
|
439
|
+
for (const id of chainOf.keys()) x.delete(id);
|
|
440
|
+
for (const row of layers) {
|
|
441
|
+
let cursor = -Infinity;
|
|
442
|
+
for (const id of row) {
|
|
443
|
+
if (isDummy(id)) {
|
|
444
|
+
cursor = Math.max(cursor, read(id));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const width2 = widthOf(id);
|
|
448
|
+
const centre = Math.max(x.get(id), cursor + width2 / 2);
|
|
449
|
+
x.set(id, centre);
|
|
450
|
+
cursor = centre + width2 / 2 + gap;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
let left = 0;
|
|
454
|
+
for (const [id, value] of x) left = Math.min(left, value - widthOf(id) / 2);
|
|
455
|
+
for (const [id, value] of x) x.set(id, value - left);
|
|
456
|
+
const layerHeight = layers.map((layer2) => Math.max(0, ...layer2.map((id) => heightOf(id))));
|
|
457
|
+
const layerWidth = layers.map((layer2, depth) => {
|
|
458
|
+
let lowest = reserved[depth].lowest - left;
|
|
459
|
+
let highest = reserved[depth].highest - left;
|
|
460
|
+
for (const id of layer2) {
|
|
461
|
+
const value = read(id);
|
|
462
|
+
lowest = Math.min(lowest, value - widthOf(id) / 2);
|
|
463
|
+
highest = Math.max(highest, value + widthOf(id) / 2);
|
|
464
|
+
}
|
|
465
|
+
return highest - lowest;
|
|
466
|
+
});
|
|
467
|
+
const layerY = [];
|
|
468
|
+
let top = 0;
|
|
469
|
+
for (const [depth, height] of layerHeight.entries()) {
|
|
470
|
+
layerY.push(top);
|
|
471
|
+
top += height + options.layerGap;
|
|
472
|
+
}
|
|
473
|
+
let width = 0;
|
|
474
|
+
for (const [id, value] of x) width = Math.max(width, value + widthOf(id) / 2);
|
|
475
|
+
return {
|
|
476
|
+
x,
|
|
477
|
+
layerY,
|
|
478
|
+
layerHeight,
|
|
479
|
+
layerWidth,
|
|
480
|
+
width,
|
|
481
|
+
height: layerY.length === 0 ? 0 : layerY[layerY.length - 1] + layerHeight[layerHeight.length - 1]
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/render/cycle-break.ts
|
|
486
|
+
function tarjan(nodes, outgoing) {
|
|
487
|
+
let counter = 0;
|
|
488
|
+
const index = /* @__PURE__ */ new Map();
|
|
489
|
+
const low = /* @__PURE__ */ new Map();
|
|
490
|
+
const stack = [];
|
|
491
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
492
|
+
const found = [];
|
|
493
|
+
const visit = (node) => {
|
|
494
|
+
index.set(node, counter);
|
|
495
|
+
low.set(node, counter);
|
|
496
|
+
counter += 1;
|
|
497
|
+
stack.push(node);
|
|
498
|
+
onStack.add(node);
|
|
499
|
+
for (const next of outgoing.get(node) ?? []) {
|
|
500
|
+
if (!index.has(next)) {
|
|
501
|
+
visit(next);
|
|
502
|
+
low.set(node, Math.min(low.get(node), low.get(next)));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (onStack.has(next)) low.set(node, Math.min(low.get(node), index.get(next)));
|
|
506
|
+
}
|
|
507
|
+
if (low.get(node) !== index.get(node)) return;
|
|
508
|
+
const component = [];
|
|
509
|
+
let member;
|
|
510
|
+
do {
|
|
511
|
+
member = stack.pop();
|
|
512
|
+
onStack.delete(member);
|
|
513
|
+
component.push(member);
|
|
514
|
+
} while (member !== node);
|
|
515
|
+
found.push(component);
|
|
516
|
+
};
|
|
517
|
+
for (const node of nodes) if (!index.has(node)) visit(node);
|
|
518
|
+
return found;
|
|
519
|
+
}
|
|
520
|
+
function breakCycles(graph) {
|
|
521
|
+
const known = new Set(graph.nodes.map((node) => node.id));
|
|
522
|
+
const position = new Map(graph.nodes.map((node, offset) => [node.id, offset]));
|
|
523
|
+
const edges = graph.edges.map((edge) => ({ ...edge }));
|
|
524
|
+
const reversed = /* @__PURE__ */ new Set();
|
|
525
|
+
const flip = (index) => {
|
|
526
|
+
const edge = edges[index];
|
|
527
|
+
const next = edge.from;
|
|
528
|
+
edge.from = edge.to;
|
|
529
|
+
edge.to = next;
|
|
530
|
+
};
|
|
531
|
+
if (graph.init !== void 0 && known.has(graph.init)) {
|
|
532
|
+
for (const [index, edge] of edges.entries()) {
|
|
533
|
+
if (edge.to === graph.init && edge.from !== graph.init) flip(index);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const edge of edges) {
|
|
538
|
+
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
|
|
539
|
+
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
540
|
+
}
|
|
541
|
+
const nodes = graph.nodes.map((node) => node.id);
|
|
542
|
+
const raw = tarjan(nodes, outgoing);
|
|
543
|
+
raw.reverse();
|
|
544
|
+
const componentOf = /* @__PURE__ */ new Map();
|
|
545
|
+
const components = raw.map((members, id) => {
|
|
546
|
+
for (const member of members) componentOf.set(member, id);
|
|
547
|
+
return { id, members };
|
|
548
|
+
});
|
|
549
|
+
const condensation = [];
|
|
550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
551
|
+
for (const edge of edges) {
|
|
552
|
+
const from = componentOf.get(edge.from);
|
|
553
|
+
const to = componentOf.get(edge.to);
|
|
554
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
555
|
+
const key = String(from) + ">" + String(to);
|
|
556
|
+
if (seen.has(key)) continue;
|
|
557
|
+
seen.add(key);
|
|
558
|
+
condensation.push({ from, to });
|
|
559
|
+
}
|
|
560
|
+
const rank = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const component of components) {
|
|
562
|
+
if (component.members.length < 2) {
|
|
563
|
+
rank.set(component.members[0], 0);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const inside2 = new Set(component.members);
|
|
567
|
+
const score = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const member of component.members) score.set(member, 0);
|
|
569
|
+
for (const edge of edges) {
|
|
570
|
+
if (!inside2.has(edge.from) || !inside2.has(edge.to) || edge.from === edge.to) continue;
|
|
571
|
+
score.set(edge.from, score.get(edge.from) + 1);
|
|
572
|
+
score.set(edge.to, score.get(edge.to) - 1);
|
|
573
|
+
}
|
|
574
|
+
const ordered = [...component.members].sort((left, right) => {
|
|
575
|
+
const difference = score.get(right) - score.get(left);
|
|
576
|
+
return difference !== 0 ? difference : position.get(left) - position.get(right);
|
|
577
|
+
});
|
|
578
|
+
ordered.forEach((member, offset) => rank.set(member, offset));
|
|
579
|
+
}
|
|
580
|
+
for (const [index, edge] of edges.entries()) {
|
|
581
|
+
const from = rank.get(edge.from);
|
|
582
|
+
const to = rank.get(edge.to);
|
|
583
|
+
if (from === void 0 || to === void 0 || edge.from === edge.to) continue;
|
|
584
|
+
if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
|
|
585
|
+
if (from > to) flip(index);
|
|
586
|
+
}
|
|
587
|
+
edges.forEach((edge, index) => {
|
|
588
|
+
if (graph.edges[index].from !== edge.from) reversed.add(edge.index);
|
|
589
|
+
});
|
|
590
|
+
return { edges, reversed, components, componentOf, rank, condensation };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/render/dummy.ts
|
|
594
|
+
function addDummies(layering, edges) {
|
|
595
|
+
const layers = layering.layers.map((layer2) => [...layer2]);
|
|
596
|
+
const chains = [];
|
|
597
|
+
const chainOf = /* @__PURE__ */ new Map();
|
|
598
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
599
|
+
for (const edge of edges) {
|
|
600
|
+
if (edge.from === edge.to) {
|
|
601
|
+
chains.push({ index: edge.index, path: [edge.from, edge.to] });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const from = layering.layerOf.get(edge.from);
|
|
605
|
+
const to = layering.layerOf.get(edge.to);
|
|
606
|
+
if (from === void 0 || to === void 0 || to <= from) {
|
|
607
|
+
chains.push({ index: edge.index, path: [edge.from, edge.to] });
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const path = [edge.from];
|
|
611
|
+
for (let cursor = from + 1; cursor < to; cursor += 1) {
|
|
612
|
+
const instance = "#d" + String(edge.index) + "_" + String(cursor);
|
|
613
|
+
const lane = "#e" + String(edge.index);
|
|
614
|
+
chainOf.set(instance, lane);
|
|
615
|
+
laneOf.set(edge.index, lane);
|
|
616
|
+
layers[cursor].push(instance);
|
|
617
|
+
path.push(instance);
|
|
618
|
+
}
|
|
619
|
+
path.push(edge.to);
|
|
620
|
+
chains.push({ index: edge.index, path });
|
|
621
|
+
}
|
|
622
|
+
return { layers, chains, chainOf, laneOf };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/render/network-simplex.ts
|
|
626
|
+
function slackOf(edge, rank) {
|
|
627
|
+
return rank[edge.to] - rank[edge.from] - edge.minLength;
|
|
628
|
+
}
|
|
629
|
+
function componentsOf(count, edges) {
|
|
630
|
+
const neighbours = [];
|
|
631
|
+
for (let node = 0; node < count; node += 1) neighbours.push([]);
|
|
632
|
+
for (const edge of edges) {
|
|
633
|
+
neighbours[edge.from].push(edge.to);
|
|
634
|
+
neighbours[edge.to].push(edge.from);
|
|
635
|
+
}
|
|
636
|
+
const seen = new Array(count).fill(false);
|
|
637
|
+
const components = [];
|
|
638
|
+
for (let node = 0; node < count; node += 1) {
|
|
639
|
+
if (seen[node]) continue;
|
|
640
|
+
seen[node] = true;
|
|
641
|
+
const members = [node];
|
|
642
|
+
for (let head = 0; head < members.length; head += 1) {
|
|
643
|
+
for (const next of neighbours[members[head]]) {
|
|
644
|
+
if (seen[next]) continue;
|
|
645
|
+
seen[next] = true;
|
|
646
|
+
members.push(next);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
members.sort((left, right) => left - right);
|
|
650
|
+
components.push(members);
|
|
651
|
+
}
|
|
652
|
+
return components;
|
|
653
|
+
}
|
|
654
|
+
function buildTightTree(members, edges, rank, tree) {
|
|
655
|
+
const inside2 = /* @__PURE__ */ new Set([members[0]]);
|
|
656
|
+
while (inside2.size < members.length) {
|
|
657
|
+
let tight = -1;
|
|
658
|
+
for (const [index, edge] of edges.entries()) {
|
|
659
|
+
if (tree.has(index)) continue;
|
|
660
|
+
if (inside2.has(edge.from) === inside2.has(edge.to)) continue;
|
|
661
|
+
if (slackOf(edge, rank) === 0) {
|
|
662
|
+
tight = index;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (tight >= 0) {
|
|
667
|
+
tree.add(tight);
|
|
668
|
+
inside2.add(edges[tight].from);
|
|
669
|
+
inside2.add(edges[tight].to);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let lowest = -Infinity;
|
|
673
|
+
let highest = Infinity;
|
|
674
|
+
for (const edge of edges) {
|
|
675
|
+
const from = inside2.has(edge.from);
|
|
676
|
+
const to = inside2.has(edge.to);
|
|
677
|
+
if (from === to) continue;
|
|
678
|
+
const slack = slackOf(edge, rank);
|
|
679
|
+
if (from) lowest = Math.max(lowest, -slack);
|
|
680
|
+
else highest = Math.min(highest, slack);
|
|
681
|
+
}
|
|
682
|
+
const step = lowest > -Infinity ? lowest : highest;
|
|
683
|
+
for (const node of members) if (!inside2.has(node)) rank[node] += step;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function walkTree(members, edges, tree) {
|
|
687
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
688
|
+
const link = (node, other) => {
|
|
689
|
+
adjacency.set(node, [...adjacency.get(node) ?? [], { node: other }]);
|
|
690
|
+
};
|
|
691
|
+
for (const index of tree) {
|
|
692
|
+
const edge = edges[index];
|
|
693
|
+
link(edge.from, edge.to);
|
|
694
|
+
link(edge.to, edge.from);
|
|
695
|
+
}
|
|
696
|
+
const parent = /* @__PURE__ */ new Map();
|
|
697
|
+
const first = /* @__PURE__ */ new Map();
|
|
698
|
+
const size = /* @__PURE__ */ new Map();
|
|
699
|
+
const order2 = [];
|
|
700
|
+
const seen = /* @__PURE__ */ new Set();
|
|
701
|
+
const root = members[0];
|
|
702
|
+
const stack = [root];
|
|
703
|
+
seen.add(root);
|
|
704
|
+
parent.set(root, -1);
|
|
705
|
+
while (stack.length > 0) {
|
|
706
|
+
const node = stack.pop();
|
|
707
|
+
first.set(node, order2.length);
|
|
708
|
+
order2.push(node);
|
|
709
|
+
for (const step of adjacency.get(node)) {
|
|
710
|
+
if (seen.has(step.node)) continue;
|
|
711
|
+
seen.add(step.node);
|
|
712
|
+
parent.set(step.node, node);
|
|
713
|
+
stack.push(step.node);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (let index = order2.length - 1; index >= 0; index -= 1) {
|
|
717
|
+
const node = order2[index];
|
|
718
|
+
size.set(node, (size.get(node) ?? 0) + 1);
|
|
719
|
+
const above = parent.get(node);
|
|
720
|
+
if (above !== void 0 && above >= 0) size.set(above, (size.get(above) ?? 0) + size.get(node));
|
|
721
|
+
}
|
|
722
|
+
return { parent, first, size, order: order2 };
|
|
723
|
+
}
|
|
724
|
+
function solveComponent(size, members, edges, rank) {
|
|
725
|
+
if (members.length < 2) {
|
|
726
|
+
if (members.length === 1) rank[members[0]] = 0;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const tree = /* @__PURE__ */ new Set();
|
|
730
|
+
buildTightTree(members, edges, rank, tree);
|
|
731
|
+
const spanning = tree.size === members.length - 1;
|
|
732
|
+
if (spanning) improve(size, members, edges, tree, rank);
|
|
733
|
+
let top = Infinity;
|
|
734
|
+
for (const node of members) top = Math.min(top, rank[node]);
|
|
735
|
+
for (const node of members) rank[node] -= top;
|
|
736
|
+
}
|
|
737
|
+
function improve(size, members, edges, tree, rank) {
|
|
738
|
+
const incoming = new Float64Array(size);
|
|
739
|
+
const outgoing = new Float64Array(size);
|
|
740
|
+
for (const edge of edges) {
|
|
741
|
+
outgoing[edge.from] += edge.weight;
|
|
742
|
+
incoming[edge.to] += edge.weight;
|
|
743
|
+
}
|
|
744
|
+
const cap = Math.max(50, 8 * members.length + 4 * edges.length);
|
|
745
|
+
for (let iteration = 0; iteration < cap; iteration += 1) {
|
|
746
|
+
const walk = walkTree(members, edges, tree);
|
|
747
|
+
const entering = /* @__PURE__ */ new Map();
|
|
748
|
+
const leaving = /* @__PURE__ */ new Map();
|
|
749
|
+
for (let index = walk.order.length - 1; index >= 0; index -= 1) {
|
|
750
|
+
const node = walk.order[index];
|
|
751
|
+
entering.set(node, incoming[node] + (entering.get(node) ?? 0));
|
|
752
|
+
leaving.set(node, outgoing[node] + (leaving.get(node) ?? 0));
|
|
753
|
+
const above = walk.parent.get(node);
|
|
754
|
+
if (above === void 0 || above < 0) continue;
|
|
755
|
+
entering.set(above, (entering.get(above) ?? 0) + entering.get(node));
|
|
756
|
+
leaving.set(above, (leaving.get(above) ?? 0) + leaving.get(node));
|
|
757
|
+
}
|
|
758
|
+
const holds = (node, child) => {
|
|
759
|
+
const start = walk.first.get(child);
|
|
760
|
+
const at = walk.first.get(node);
|
|
761
|
+
return at >= start && at < start + walk.size.get(child);
|
|
762
|
+
};
|
|
763
|
+
let worst = -1;
|
|
764
|
+
let worstEdge = -1;
|
|
765
|
+
let worstChild = -1;
|
|
766
|
+
for (const index of tree) {
|
|
767
|
+
const edge = edges[index];
|
|
768
|
+
const child = walk.parent.get(edge.from) === edge.to ? edge.from : edge.to;
|
|
769
|
+
const tailInside = holds(edge.from, child);
|
|
770
|
+
const value = tailInside ? leaving.get(child) - entering.get(child) : entering.get(child) - leaving.get(child);
|
|
771
|
+
if (value < 0 && (worstEdge < 0 || value < worst)) {
|
|
772
|
+
worst = value;
|
|
773
|
+
worstEdge = index;
|
|
774
|
+
worstChild = child;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (worstEdge < 0) return;
|
|
778
|
+
const leavingEdge = edges[worstEdge];
|
|
779
|
+
const headInside = !holds(leavingEdge.from, worstChild);
|
|
780
|
+
const inHead = (node) => headInside ? holds(node, worstChild) : !holds(node, worstChild);
|
|
781
|
+
let enteringEdge = -1;
|
|
782
|
+
let step = Infinity;
|
|
783
|
+
for (const [index, edge] of edges.entries()) {
|
|
784
|
+
if (tree.has(index)) continue;
|
|
785
|
+
if (!inHead(edge.from) || inHead(edge.to)) continue;
|
|
786
|
+
const slack = slackOf(edge, rank);
|
|
787
|
+
if (slack < step || slack === step && (enteringEdge < 0 || index < enteringEdge)) {
|
|
788
|
+
step = slack;
|
|
789
|
+
enteringEdge = index;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (enteringEdge < 0 || step === Infinity) return;
|
|
793
|
+
if (step !== 0) {
|
|
794
|
+
for (const node of members) if (inHead(node)) rank[node] += step;
|
|
795
|
+
}
|
|
796
|
+
tree.delete(worstEdge);
|
|
797
|
+
tree.add(enteringEdge);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function minimumLength(problem) {
|
|
801
|
+
const rank = problem.start.slice();
|
|
802
|
+
for (const members of componentsOf(problem.count, problem.edges)) {
|
|
803
|
+
const inside2 = new Set(members);
|
|
804
|
+
const local = problem.edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
|
|
805
|
+
const pinned = problem.pinned !== void 0 && inside2.has(problem.pinned) ? problem.pinned : void 0;
|
|
806
|
+
const pinnedRank = pinned === void 0 ? void 0 : rank[pinned];
|
|
807
|
+
if (pinned !== void 0 && pinnedRank !== void 0 && members.every((node) => rank[node] >= pinnedRank)) {
|
|
808
|
+
for (const node of members) if (node !== pinned) local.push({ from: pinned, to: node, minLength: 0, weight: 0 });
|
|
809
|
+
}
|
|
810
|
+
solveComponent(problem.count, members, local, rank);
|
|
811
|
+
}
|
|
812
|
+
return rank;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/render/layer.ts
|
|
816
|
+
function longestPath(ids, edges, index) {
|
|
817
|
+
const rank = new Array(ids.length).fill(0);
|
|
818
|
+
const pending = new Array(ids.length).fill(0);
|
|
819
|
+
const outgoing = ids.map(() => []);
|
|
820
|
+
for (const edge of edges) {
|
|
821
|
+
outgoing[index.get(edge.from)].push(index.get(edge.to));
|
|
822
|
+
pending[index.get(edge.to)] += 1;
|
|
823
|
+
}
|
|
824
|
+
const queue = [];
|
|
825
|
+
for (let node = 0; node < ids.length; node += 1) if (pending[node] === 0) queue.push(node);
|
|
826
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
827
|
+
const node = queue[head];
|
|
828
|
+
for (const target of outgoing[node]) {
|
|
829
|
+
rank[target] = Math.max(rank[target], rank[node] + 1);
|
|
830
|
+
pending[target] -= 1;
|
|
831
|
+
if (pending[target] === 0) queue.push(target);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return rank;
|
|
835
|
+
}
|
|
836
|
+
function layer(graph, edges) {
|
|
837
|
+
const ids = [];
|
|
838
|
+
const seen = /* @__PURE__ */ new Set();
|
|
839
|
+
for (const node of graph.nodes) {
|
|
840
|
+
if (seen.has(node.id)) continue;
|
|
841
|
+
seen.add(node.id);
|
|
842
|
+
ids.push(node.id);
|
|
843
|
+
}
|
|
844
|
+
const index = new Map(ids.map((id, offset) => [id, offset]));
|
|
845
|
+
const forward = edges.filter((edge) => index.has(edge.from) && index.has(edge.to) && edge.from !== edge.to);
|
|
846
|
+
const rankEdges = forward.map((edge) => ({
|
|
847
|
+
from: index.get(edge.from),
|
|
848
|
+
to: index.get(edge.to),
|
|
849
|
+
minLength: 1,
|
|
850
|
+
weight: 1
|
|
851
|
+
}));
|
|
852
|
+
let pinned;
|
|
853
|
+
if (graph.init !== void 0 && index.has(graph.init)) {
|
|
854
|
+
const start = index.get(graph.init);
|
|
855
|
+
const forced = forward.some((edge) => edge.to === graph.init);
|
|
856
|
+
if (!forced) pinned = start;
|
|
857
|
+
}
|
|
858
|
+
const rank = minimumLength({ count: ids.length, edges: rankEdges, start: longestPath(ids, forward, index), ...pinned === void 0 ? {} : { pinned } });
|
|
859
|
+
const layers = [];
|
|
860
|
+
for (const [offset, id] of ids.entries()) {
|
|
861
|
+
const depth = rank[offset];
|
|
862
|
+
while (layers.length <= depth) layers.push([]);
|
|
863
|
+
layers[depth].push(id);
|
|
864
|
+
}
|
|
865
|
+
const layerOf = new Map(ids.map((id, offset) => [id, rank[offset]]));
|
|
866
|
+
return { layers, layerOf };
|
|
867
|
+
}
|
|
868
|
+
|
|
40
869
|
// src/render/layout.ts
|
|
41
870
|
function rankStates(nodes, edges, init) {
|
|
42
871
|
const ids = nodes.map((node) => node.id);
|
|
43
872
|
const known = new Set(ids);
|
|
44
873
|
const start = init !== void 0 && known.has(init) ? init : ids[0];
|
|
45
874
|
const outgoing = /* @__PURE__ */ new Map();
|
|
46
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
47
875
|
for (const edge of edges) {
|
|
48
876
|
if (!known.has(edge.from) || !known.has(edge.to)) continue;
|
|
49
877
|
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
50
|
-
incoming.set(edge.to, [...incoming.get(edge.to) ?? [], edge.from]);
|
|
51
878
|
}
|
|
52
879
|
const depth = /* @__PURE__ */ new Map();
|
|
53
880
|
if (start !== void 0) {
|
|
@@ -72,40 +899,224 @@ window.__ModuleLoader__.load({
|
|
|
72
899
|
}
|
|
73
900
|
return { rows, unreachable: ids.filter((id) => !depth.has(id)) };
|
|
74
901
|
}
|
|
902
|
+
function seedOrder(graph) {
|
|
903
|
+
const ranked = rankStates(graph.nodes, graph.edges, graph.init);
|
|
904
|
+
const order2 = /* @__PURE__ */ new Map();
|
|
905
|
+
let index = 0;
|
|
906
|
+
for (const row of ranked.rows) for (const id of row) order2.set(id, index++);
|
|
907
|
+
for (const id of ranked.unreachable) order2.set(id, index++);
|
|
908
|
+
return order2;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/render/order.ts
|
|
912
|
+
function locate(layers) {
|
|
913
|
+
const found = /* @__PURE__ */ new Map();
|
|
914
|
+
layers.forEach((layer2, depth) => layer2.forEach((id, index) => found.set(id, { layer: depth, index })));
|
|
915
|
+
return found;
|
|
916
|
+
}
|
|
917
|
+
function medianOf(id, neighbours, reference2, positions2) {
|
|
918
|
+
const found = (neighbours.get(id) ?? []).filter((other) => positions2.get(other)?.layer === reference2).map((other) => positions2.get(other).index).sort((left, right) => left - right);
|
|
919
|
+
if (found.length === 0) return positions2.get(id).index;
|
|
920
|
+
const middle = Math.floor(found.length / 2);
|
|
921
|
+
return found.length % 2 === 1 ? found[middle] : (found[middle - 1] + found[middle]) / 2;
|
|
922
|
+
}
|
|
923
|
+
function countCrossings(layers, chains) {
|
|
924
|
+
const positions2 = locate(layers);
|
|
925
|
+
const segments = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const chain of chains) {
|
|
927
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
928
|
+
const top = positions2.get(chain.path[cursor]);
|
|
929
|
+
const bottom = positions2.get(chain.path[cursor + 1]);
|
|
930
|
+
if (top === void 0 || bottom === void 0 || bottom.layer !== top.layer + 1) continue;
|
|
931
|
+
const list = segments.get(top.layer) ?? [];
|
|
932
|
+
list.push({ top: top.index, bottom: bottom.index });
|
|
933
|
+
segments.set(top.layer, list);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
let crossings = 0;
|
|
937
|
+
for (const list of segments.values()) {
|
|
938
|
+
for (let left = 0; left < list.length; left += 1) {
|
|
939
|
+
for (let right = left + 1; right < list.length; right += 1) {
|
|
940
|
+
if ((list[left].top - list[right].top) * (list[left].bottom - list[right].bottom) < 0) crossings += 1;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return crossings;
|
|
945
|
+
}
|
|
946
|
+
function order(seedLayers, chains, seed) {
|
|
947
|
+
const layers = seedLayers.map(
|
|
948
|
+
(layer2) => layer2.map((id, index) => ({ id, index })).sort((left, right) => (seed?.get(left.id) ?? Infinity) - (seed?.get(right.id) ?? Infinity) || left.index - right.index).map((entry) => entry.id)
|
|
949
|
+
);
|
|
950
|
+
const down = /* @__PURE__ */ new Map();
|
|
951
|
+
const up = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const chain of chains) {
|
|
953
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
954
|
+
const top = chain.path[cursor];
|
|
955
|
+
const bottom = chain.path[cursor + 1];
|
|
956
|
+
down.set(top, [...down.get(top) ?? [], bottom]);
|
|
957
|
+
up.set(bottom, [...up.get(bottom) ?? [], top]);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
let positions2 = locate(layers);
|
|
961
|
+
for (let round = 0; round < 4; round += 1) {
|
|
962
|
+
for (let depth = 1; depth < layers.length; depth += 1) {
|
|
963
|
+
const layer2 = layers[depth];
|
|
964
|
+
const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, up, depth - 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
|
|
965
|
+
layers[depth] = sorted;
|
|
966
|
+
positions2 = locate(layers);
|
|
967
|
+
}
|
|
968
|
+
for (let depth = layers.length - 2; depth >= 0; depth -= 1) {
|
|
969
|
+
const layer2 = layers[depth];
|
|
970
|
+
const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, down, depth + 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
|
|
971
|
+
layers[depth] = sorted;
|
|
972
|
+
positions2 = locate(layers);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
let crossings = countCrossings(layers, chains);
|
|
976
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
977
|
+
let improved = false;
|
|
978
|
+
for (const layer2 of layers) {
|
|
979
|
+
for (let index = 0; index + 1 < layer2.length; index += 1) {
|
|
980
|
+
const before = layer2[index];
|
|
981
|
+
layer2[index] = layer2[index + 1];
|
|
982
|
+
layer2[index + 1] = before;
|
|
983
|
+
const next = countCrossings(layers, chains);
|
|
984
|
+
if (next < crossings) {
|
|
985
|
+
crossings = next;
|
|
986
|
+
improved = true;
|
|
987
|
+
} else {
|
|
988
|
+
layer2[index + 1] = layer2[index];
|
|
989
|
+
layer2[index] = before;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (!improved) break;
|
|
994
|
+
}
|
|
995
|
+
return { layers, crossings };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/render/text.ts
|
|
999
|
+
var CHAR_WIDTH = 6.6;
|
|
1000
|
+
var MAX_LINE_CHARS = 30;
|
|
1001
|
+
var LINE_HEIGHT = 15;
|
|
1002
|
+
var TEXT_PADDING = 12;
|
|
1003
|
+
var MIN_NODE_WIDTH = 88;
|
|
1004
|
+
var MAX_NODE_WIDTH = 260;
|
|
1005
|
+
var BREAK = /<br\s*\/?>|\r?\n/gi;
|
|
1006
|
+
function explicitLines(label) {
|
|
1007
|
+
return label.split(BREAK).map((line) => line.trim());
|
|
1008
|
+
}
|
|
1009
|
+
function wrapLine(line, limit = MAX_LINE_CHARS) {
|
|
1010
|
+
if (line === "") return [""];
|
|
1011
|
+
const out = [];
|
|
1012
|
+
let current = "";
|
|
1013
|
+
for (const word of line.split(/\s+/).filter((part) => part !== "")) {
|
|
1014
|
+
const candidate = current === "" ? word : current + " " + word;
|
|
1015
|
+
if (candidate.length <= limit) {
|
|
1016
|
+
current = candidate;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (current !== "") out.push(current);
|
|
1020
|
+
if (word.length <= limit) {
|
|
1021
|
+
current = word;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
let rest = word;
|
|
1025
|
+
while (rest.length > limit) {
|
|
1026
|
+
out.push(rest.slice(0, limit));
|
|
1027
|
+
rest = rest.slice(limit);
|
|
1028
|
+
}
|
|
1029
|
+
current = rest;
|
|
1030
|
+
}
|
|
1031
|
+
if (current !== "") out.push(current);
|
|
1032
|
+
return out.length === 0 ? [""] : out;
|
|
1033
|
+
}
|
|
1034
|
+
function measure(label) {
|
|
1035
|
+
const lines = explicitLines(label).flatMap((line) => wrapLine(line));
|
|
1036
|
+
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
1037
|
+
const width = Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, longest * CHAR_WIDTH + TEXT_PADDING * 2));
|
|
1038
|
+
const height = lines.length * LINE_HEIGHT + TEXT_PADDING;
|
|
1039
|
+
return { lines, width, height };
|
|
1040
|
+
}
|
|
1041
|
+
function textWidth(text2) {
|
|
1042
|
+
return text2.length * CHAR_WIDTH;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/render/wrap.ts
|
|
1046
|
+
var PAGE_ASPECT2 = 1.6;
|
|
1047
|
+
var MIN_LAYERS_TO_WRAP = 4;
|
|
1048
|
+
var STRIP_RATIO = 2;
|
|
1049
|
+
function runHeight(layerHeight, first, last, gap) {
|
|
1050
|
+
let total = 0;
|
|
1051
|
+
for (let index = first; index <= last; index += 1) {
|
|
1052
|
+
total += layerHeight[index];
|
|
1053
|
+
if (index < last) total += gap;
|
|
1054
|
+
}
|
|
1055
|
+
return total;
|
|
1056
|
+
}
|
|
1057
|
+
function runWidth(layerWidth, first, last) {
|
|
1058
|
+
let widest = 0;
|
|
1059
|
+
for (let index = first; index <= last; index += 1) widest = Math.max(widest, layerWidth[index]);
|
|
1060
|
+
return widest;
|
|
1061
|
+
}
|
|
1062
|
+
function shapeError(width, height, aspect) {
|
|
1063
|
+
if (width <= 0 || height <= 0) return Infinity;
|
|
1064
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
1065
|
+
return Math.abs(Math.log(ratio / aspect));
|
|
1066
|
+
}
|
|
1067
|
+
function planBands(layerWidth, layerHeight, gap, aspect = PAGE_ASPECT2) {
|
|
1068
|
+
const count = layerWidth.length;
|
|
1069
|
+
if (count === 0) return { bands: [], width: 0, height: 0 };
|
|
1070
|
+
const single = {
|
|
1071
|
+
bands: [{ first: 0, last: count - 1, offsetX: 0 }],
|
|
1072
|
+
width: runWidth(layerWidth, 0, count - 1),
|
|
1073
|
+
height: runHeight(layerHeight, 0, count - 1, gap)
|
|
1074
|
+
};
|
|
1075
|
+
if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO) return single;
|
|
1076
|
+
const columnGap = gap * 2;
|
|
1077
|
+
let best = single;
|
|
1078
|
+
let bestError = shapeError(single.width, single.height, aspect);
|
|
1079
|
+
for (let bands = 2; bands < count; bands += 1) {
|
|
1080
|
+
const base = Math.floor(count / bands);
|
|
1081
|
+
const extra = count % bands;
|
|
1082
|
+
const ranges = [];
|
|
1083
|
+
let cursor = 0;
|
|
1084
|
+
for (let band = 0; band < bands; band += 1) {
|
|
1085
|
+
const size = base + (band < extra ? 1 : 0);
|
|
1086
|
+
ranges.push({ first: cursor, last: cursor + size - 1 });
|
|
1087
|
+
cursor += size;
|
|
1088
|
+
}
|
|
1089
|
+
let width = columnGap * (bands - 1);
|
|
1090
|
+
let height = 0;
|
|
1091
|
+
for (const range of ranges) {
|
|
1092
|
+
width += runWidth(layerWidth, range.first, range.last);
|
|
1093
|
+
height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap));
|
|
1094
|
+
}
|
|
1095
|
+
if (shapeError(width, height, aspect) >= bestError) continue;
|
|
1096
|
+
const planned = [];
|
|
1097
|
+
let offsetX = 0;
|
|
1098
|
+
for (const range of ranges) {
|
|
1099
|
+
planned.push({ first: range.first, last: range.last, offsetX });
|
|
1100
|
+
offsetX += runWidth(layerWidth, range.first, range.last) + columnGap;
|
|
1101
|
+
}
|
|
1102
|
+
best = { bands: planned, width, height };
|
|
1103
|
+
bestError = shapeError(width, height, aspect);
|
|
1104
|
+
}
|
|
1105
|
+
return best;
|
|
1106
|
+
}
|
|
75
1107
|
|
|
76
1108
|
// src/render/place.ts
|
|
77
1109
|
var MARGIN = 20;
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
var
|
|
81
|
-
var
|
|
82
|
-
var
|
|
83
|
-
var
|
|
1110
|
+
var LAYER_GAP = 44;
|
|
1111
|
+
var LABELLED_LAYER_GAP = 54;
|
|
1112
|
+
var NODE_GAP = 30;
|
|
1113
|
+
var LANE_CLEARANCE = 12;
|
|
1114
|
+
var LANE_SPACING = 14;
|
|
1115
|
+
var COMPONENT_GAP = 30;
|
|
84
1116
|
var GROUP_PADDING = 10;
|
|
85
1117
|
var GROUP_HEADER = 16;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function placeByRanks(diagram, nodes, top) {
|
|
89
|
-
const ranked = rankStates(diagram.nodes, diagram.edges, diagram.init);
|
|
90
|
-
const rows = [...ranked.rows];
|
|
91
|
-
if (ranked.unreachable.length > 0) rows.push(ranked.unreachable);
|
|
92
|
-
const boxes = /* @__PURE__ */ new Map();
|
|
93
|
-
let widest = 0;
|
|
94
|
-
rows.forEach((row, index) => {
|
|
95
|
-
const widths = row.map((id) => nodeWidth(id));
|
|
96
|
-
widest = Math.max(widest, widths.reduce((total, width) => total + width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP);
|
|
97
|
-
let x = 0;
|
|
98
|
-
row.forEach((id, position) => {
|
|
99
|
-
boxes.set(id, { x, y: top + index * (NODE_HEIGHT + ROW_GAP), width: widths[position], height: NODE_HEIGHT });
|
|
100
|
-
x += widths[position] + COLUMN_GAP;
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
rows.forEach((row) => {
|
|
104
|
-
const rowWidth = row.reduce((total, id) => total + boxes.get(id).width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP;
|
|
105
|
-
const offset = (widest - rowWidth) / 2;
|
|
106
|
-
for (const id of row) boxes.get(id).x += offset + MARGIN;
|
|
107
|
-
});
|
|
108
|
-
return { boxes, groupRects: /* @__PURE__ */ new Map(), width: widest + MARGIN * 2 };
|
|
1118
|
+
function textOf(node) {
|
|
1119
|
+
return measure(node.label !== void 0 && node.label !== "" ? node.label : node.id);
|
|
109
1120
|
}
|
|
110
1121
|
function groupTree(groups) {
|
|
111
1122
|
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
@@ -162,69 +1173,619 @@ window.__ModuleLoader__.load({
|
|
|
162
1173
|
if (parent === void 0) roots.push(group.id);
|
|
163
1174
|
else children.set(parent, [...children.get(parent) ?? [], group.id]);
|
|
164
1175
|
}
|
|
165
|
-
return { owner, children, roots };
|
|
1176
|
+
return { owner, parentOf, children, roots };
|
|
1177
|
+
}
|
|
1178
|
+
function place(diagram, top) {
|
|
1179
|
+
const groups = diagram.groups ?? [];
|
|
1180
|
+
const nodes = [];
|
|
1181
|
+
const seenNode = /* @__PURE__ */ new Set();
|
|
1182
|
+
for (const node of diagram.nodes) {
|
|
1183
|
+
if (seenNode.has(node.id)) continue;
|
|
1184
|
+
seenNode.add(node.id);
|
|
1185
|
+
nodes.push(node);
|
|
1186
|
+
}
|
|
1187
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
1188
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
1189
|
+
const tree = groupTree(groups);
|
|
1190
|
+
const appearance = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const node of nodes) if (!appearance.has(node.id)) appearance.set(node.id, appearance.size);
|
|
1192
|
+
for (const group of groups) if (!appearance.has(group.id)) appearance.set(group.id, appearance.size);
|
|
1193
|
+
const visible = /* @__PURE__ */ new Set();
|
|
1194
|
+
const asked = /* @__PURE__ */ new Map();
|
|
1195
|
+
const isVisible = (id) => {
|
|
1196
|
+
const known = asked.get(id);
|
|
1197
|
+
if (known !== void 0) return known;
|
|
1198
|
+
const group = groupById.get(id);
|
|
1199
|
+
let found = false;
|
|
1200
|
+
for (const member of group.members) {
|
|
1201
|
+
if (tree.owner.get(member) === id && nodeById.has(member)) {
|
|
1202
|
+
found = true;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (!found) {
|
|
1207
|
+
for (const child of tree.children.get(id) ?? []) {
|
|
1208
|
+
if (isVisible(child)) {
|
|
1209
|
+
found = true;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
asked.set(id, found);
|
|
1215
|
+
if (found) visible.add(id);
|
|
1216
|
+
return found;
|
|
1217
|
+
};
|
|
1218
|
+
for (const group of groups) isVisible(group.id);
|
|
1219
|
+
const directNodes = (scope) => nodes.filter((node) => tree.owner.get(node.id) === scope).map((node) => node.id);
|
|
1220
|
+
const directGroups = (scope) => (tree.children.get(scope) ?? []).filter((child) => visible.has(child));
|
|
1221
|
+
const firstIndexCache = /* @__PURE__ */ new Map();
|
|
1222
|
+
const firstIndexOf = (id) => {
|
|
1223
|
+
const cached = firstIndexCache.get(id);
|
|
1224
|
+
if (cached !== void 0) return cached;
|
|
1225
|
+
if (nodeById.has(id)) return appearance.get(id);
|
|
1226
|
+
const members = groupById.get(id).members.filter((member) => tree.owner.get(member) === id && nodeById.has(member)).map((member) => appearance.get(member));
|
|
1227
|
+
const children = directGroups(id).map(firstIndexOf);
|
|
1228
|
+
const all = [...members, ...children];
|
|
1229
|
+
const found = Math.min(...all);
|
|
1230
|
+
firstIndexCache.set(id, found);
|
|
1231
|
+
return found;
|
|
1232
|
+
};
|
|
1233
|
+
const entityAt = (scope, id) => {
|
|
1234
|
+
const boundary = groupById.get(id);
|
|
1235
|
+
if (boundary !== void 0) {
|
|
1236
|
+
if (!visible.has(id)) return void 0;
|
|
1237
|
+
if (scope === void 0) {
|
|
1238
|
+
let cursor = id;
|
|
1239
|
+
while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
|
|
1240
|
+
return cursor;
|
|
1241
|
+
}
|
|
1242
|
+
return tree.parentOf.get(id) === scope ? id : void 0;
|
|
1243
|
+
}
|
|
1244
|
+
if (!nodeById.has(id)) return void 0;
|
|
1245
|
+
const group = tree.owner.get(id);
|
|
1246
|
+
if (scope === void 0) {
|
|
1247
|
+
if (group === void 0 || !visible.has(group)) return id;
|
|
1248
|
+
let cursor = group;
|
|
1249
|
+
while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
|
|
1250
|
+
return cursor;
|
|
1251
|
+
}
|
|
1252
|
+
if (group === scope) return id;
|
|
1253
|
+
if (group === void 0) return void 0;
|
|
1254
|
+
return tree.parentOf.get(group) === scope && visible.has(group) ? group : void 0;
|
|
1255
|
+
};
|
|
1256
|
+
const groupLayouts = /* @__PURE__ */ new Map();
|
|
1257
|
+
const measureGroup = (id) => {
|
|
1258
|
+
const inside2 = buildLevel(id);
|
|
1259
|
+
const size = {
|
|
1260
|
+
width: Math.max(MIN_NODE_WIDTH, inside2.width) + GROUP_PADDING * 2,
|
|
1261
|
+
height: inside2.height + GROUP_HEADER + GROUP_PADDING * 2
|
|
1262
|
+
};
|
|
1263
|
+
groupLayouts.set(id, inside2);
|
|
1264
|
+
return size;
|
|
1265
|
+
};
|
|
1266
|
+
const entityOf = (id) => {
|
|
1267
|
+
const node = nodeById.get(id);
|
|
1268
|
+
if (node !== void 0) {
|
|
1269
|
+
const text2 = textOf(node);
|
|
1270
|
+
return { id, group: false, width: text2.width, height: text2.height };
|
|
1271
|
+
}
|
|
1272
|
+
const size = measureGroup(id);
|
|
1273
|
+
return { id, group: true, width: size.width, height: size.height };
|
|
1274
|
+
};
|
|
1275
|
+
const entitiesIn = (scope) => {
|
|
1276
|
+
const found = [];
|
|
1277
|
+
if (scope === void 0) {
|
|
1278
|
+
for (const node of nodes) {
|
|
1279
|
+
const group = tree.owner.get(node.id);
|
|
1280
|
+
if (group === void 0 || !visible.has(group)) found.push(entityOf(node.id));
|
|
1281
|
+
}
|
|
1282
|
+
for (const root2 of tree.roots) if (visible.has(root2)) found.push(entityOf(root2));
|
|
1283
|
+
} else {
|
|
1284
|
+
for (const id of directNodes(scope)) found.push(entityOf(id));
|
|
1285
|
+
for (const id of directGroups(scope)) found.push(entityOf(id));
|
|
1286
|
+
}
|
|
1287
|
+
return found.sort((left, right2) => firstIndexOf(left.id) - firstIndexOf(right2.id));
|
|
1288
|
+
};
|
|
1289
|
+
const edgesIn = (scope) => {
|
|
1290
|
+
const found = [];
|
|
1291
|
+
diagram.edges.forEach((edge, index) => {
|
|
1292
|
+
if (edge.from === edge.to) return;
|
|
1293
|
+
const from = entityAt(scope, edge.from);
|
|
1294
|
+
const to = entityAt(scope, edge.to);
|
|
1295
|
+
if (from === void 0 || to === void 0 || from === to) return;
|
|
1296
|
+
found.push({ index, from, to });
|
|
1297
|
+
});
|
|
1298
|
+
return found;
|
|
1299
|
+
};
|
|
1300
|
+
const pinnedAt = (scope) => {
|
|
1301
|
+
if (diagram.init === void 0) return void 0;
|
|
1302
|
+
const entity = entityAt(scope, diagram.init);
|
|
1303
|
+
return entity === diagram.init || entity !== void 0 && groupById.has(entity) ? entity : void 0;
|
|
1304
|
+
};
|
|
1305
|
+
const layoutPiece = (entities, edges, pinned, seed) => {
|
|
1306
|
+
const byId = new Map(entities.map((entity) => [entity.id, entity]));
|
|
1307
|
+
const graph = {
|
|
1308
|
+
nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })),
|
|
1309
|
+
edges,
|
|
1310
|
+
...pinned === void 0 ? {} : { init: pinned }
|
|
1311
|
+
};
|
|
1312
|
+
const broken = breakCycles(graph);
|
|
1313
|
+
const layered = layer(graph, broken.edges);
|
|
1314
|
+
const augmented = addDummies(layered, broken.edges);
|
|
1315
|
+
const ordered = order(augmented.layers, augmented.chains, seed);
|
|
1316
|
+
const gap = edges.some((edge) => diagram.edges[edge.index].label !== "") ? LABELLED_LAYER_GAP : LAYER_GAP;
|
|
1317
|
+
const coordinates = assignCoordinates(
|
|
1318
|
+
{
|
|
1319
|
+
layers: ordered.layers,
|
|
1320
|
+
chains: augmented.chains,
|
|
1321
|
+
sizeOf: (id) => {
|
|
1322
|
+
const entity = byId.get(id);
|
|
1323
|
+
return { width: entity.width, height: entity.height };
|
|
1324
|
+
},
|
|
1325
|
+
chainOf: augmented.chainOf
|
|
1326
|
+
},
|
|
1327
|
+
{ gap: NODE_GAP, layerGap: gap }
|
|
1328
|
+
);
|
|
1329
|
+
const bands = planBands(coordinates.layerWidth, coordinates.layerHeight, gap);
|
|
1330
|
+
const bandOfLayer = new Array(coordinates.layerHeight.length).fill(0);
|
|
1331
|
+
bands.bands.forEach((band, index) => {
|
|
1332
|
+
for (let depth = band.first; depth <= band.last; depth += 1) bandOfLayer[depth] = index;
|
|
1333
|
+
});
|
|
1334
|
+
const layerTop = [];
|
|
1335
|
+
for (const band of bands.bands) {
|
|
1336
|
+
let y = 0;
|
|
1337
|
+
for (let depth = band.first; depth <= band.last; depth += 1) {
|
|
1338
|
+
layerTop[depth] = y;
|
|
1339
|
+
y += coordinates.layerHeight[depth] + gap;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const items = /* @__PURE__ */ new Map();
|
|
1343
|
+
ordered.layers.forEach((row, depth) => {
|
|
1344
|
+
const shift = bands.bands[bandOfLayer[depth]].offsetX;
|
|
1345
|
+
const top2 = layerTop[depth];
|
|
1346
|
+
const rowHeight = coordinates.layerHeight[depth];
|
|
1347
|
+
for (const id of row) {
|
|
1348
|
+
const entity = byId.get(id);
|
|
1349
|
+
if (entity === void 0) continue;
|
|
1350
|
+
const centre = coordinates.x.get(id);
|
|
1351
|
+
items.set(id, {
|
|
1352
|
+
box: {
|
|
1353
|
+
x: centre + shift - entity.width / 2,
|
|
1354
|
+
y: top2 + (rowHeight - entity.height) / 2,
|
|
1355
|
+
width: entity.width,
|
|
1356
|
+
height: entity.height
|
|
1357
|
+
},
|
|
1358
|
+
group: entity.group
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
const across2 = /* @__PURE__ */ new Set();
|
|
1363
|
+
for (const edge of edges) {
|
|
1364
|
+
const from = layered.layerOf.get(edge.from);
|
|
1365
|
+
const to = layered.layerOf.get(edge.to);
|
|
1366
|
+
if (bandOfLayer[from] !== bandOfLayer[to]) across2.add(edge.index);
|
|
1367
|
+
}
|
|
1368
|
+
const laneProposal = /* @__PURE__ */ new Map();
|
|
1369
|
+
for (const chain of augmented.chains) {
|
|
1370
|
+
if (chain.path.length <= 2) continue;
|
|
1371
|
+
const lane = augmented.chainOf.get(chain.path[1]);
|
|
1372
|
+
const fromLayer = layered.layerOf.get(chain.path[0]);
|
|
1373
|
+
const proposed = coordinates.x.get(lane);
|
|
1374
|
+
laneProposal.set(lane, proposed + bands.bands[bandOfLayer[fromLayer]].offsetX);
|
|
1375
|
+
}
|
|
1376
|
+
const itemBoxes = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const [id, item] of items) itemBoxes.set(id, item.box);
|
|
1378
|
+
const channels = assignChannels(
|
|
1379
|
+
{
|
|
1380
|
+
layers: ordered.layers,
|
|
1381
|
+
chains: augmented.chains,
|
|
1382
|
+
chainOf: augmented.chainOf,
|
|
1383
|
+
boxes: itemBoxes,
|
|
1384
|
+
layerY: layerTop,
|
|
1385
|
+
layerHeight: coordinates.layerHeight,
|
|
1386
|
+
laneX: laneProposal
|
|
1387
|
+
},
|
|
1388
|
+
{ clearance: LANE_CLEARANCE, spacing: LANE_SPACING }
|
|
1389
|
+
);
|
|
1390
|
+
const lanes3 = /* @__PURE__ */ new Map();
|
|
1391
|
+
for (const chain of augmented.chains) {
|
|
1392
|
+
if (chain.path.length <= 2) continue;
|
|
1393
|
+
const start = byId.get(chain.path[0]);
|
|
1394
|
+
const end = byId.get(chain.path[chain.path.length - 1]);
|
|
1395
|
+
if (start.group || end.group) continue;
|
|
1396
|
+
const fromLayer = layered.layerOf.get(chain.path[0]);
|
|
1397
|
+
const toLayer = layered.layerOf.get(chain.path[chain.path.length - 1]);
|
|
1398
|
+
if (bandOfLayer[fromLayer] !== bandOfLayer[toLayer]) continue;
|
|
1399
|
+
const channel = channels.lanes.get(chain.index);
|
|
1400
|
+
lanes3.set(chain.index, {
|
|
1401
|
+
points: channel.y.map((y) => ({ x: channel.x, y })),
|
|
1402
|
+
reversed: broken.reversed.has(chain.index)
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const runs2 = /* @__PURE__ */ new Map();
|
|
1406
|
+
for (const [edgeIndex, y] of channels.runs) if (!across2.has(edgeIndex)) runs2.set(edgeIndex, y);
|
|
1407
|
+
let right2 = 0;
|
|
1408
|
+
let bottom = 0;
|
|
1409
|
+
for (const entry of items.values()) {
|
|
1410
|
+
right2 = Math.max(right2, entry.box.x + entry.box.width);
|
|
1411
|
+
bottom = Math.max(bottom, entry.box.y + entry.box.height);
|
|
1412
|
+
}
|
|
1413
|
+
return { width: right2, height: bottom, items, lanes: lanes3, runs: runs2, across: across2 };
|
|
1414
|
+
};
|
|
1415
|
+
function buildLevel(scope) {
|
|
1416
|
+
const entities = entitiesIn(scope);
|
|
1417
|
+
if (entities.length === 0) {
|
|
1418
|
+
return { width: 0, height: 0, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
|
|
1419
|
+
}
|
|
1420
|
+
const edges = edgesIn(scope);
|
|
1421
|
+
const pinned = pinnedAt(scope);
|
|
1422
|
+
const seed = seedOrder({ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })), edges, ...pinned === void 0 ? {} : { init: pinned } });
|
|
1423
|
+
const components = weakComponents(entities, edges);
|
|
1424
|
+
const pieces = [];
|
|
1425
|
+
for (const members of components) {
|
|
1426
|
+
const inside2 = new Set(members);
|
|
1427
|
+
const pieceEntities = entities.filter((entity) => inside2.has(entity.id));
|
|
1428
|
+
const pieceEdges = edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
|
|
1429
|
+
const pin = pinned !== void 0 && inside2.has(pinned) ? pinned : void 0;
|
|
1430
|
+
pieces.push(layoutPiece(pieceEntities, pieceEdges, pin, seed));
|
|
1431
|
+
}
|
|
1432
|
+
const packed = packRectangles(pieces.map((piece) => ({ width: piece.width, height: piece.height })), { gap: COMPONENT_GAP });
|
|
1433
|
+
const layout = { width: packed.width, height: packed.height, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
|
|
1434
|
+
pieces.forEach((piece, index) => {
|
|
1435
|
+
for (const edgeIndex of piece.across) layout.across.add(edgeIndex);
|
|
1436
|
+
const at = packed.placements[index];
|
|
1437
|
+
for (const [id, item] of piece.items) {
|
|
1438
|
+
layout.items.set(id, { box: { x: item.box.x + at.x, y: item.box.y + at.y, width: item.box.width, height: item.box.height }, group: item.group });
|
|
1439
|
+
}
|
|
1440
|
+
for (const [edgeIndex, lane] of piece.lanes) {
|
|
1441
|
+
layout.lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + at.x, y: point.y + at.y })), reversed: lane.reversed });
|
|
1442
|
+
}
|
|
1443
|
+
for (const [edgeIndex, y] of piece.runs) layout.runs.set(edgeIndex, y + at.y);
|
|
1444
|
+
});
|
|
1445
|
+
return layout;
|
|
1446
|
+
}
|
|
1447
|
+
const root = buildLevel(void 0);
|
|
1448
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
1449
|
+
const groupRects = /* @__PURE__ */ new Map();
|
|
1450
|
+
const lanes2 = /* @__PURE__ */ new Map();
|
|
1451
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1452
|
+
const across = /* @__PURE__ */ new Set();
|
|
1453
|
+
const placeLevel = (layout, originX, originY) => {
|
|
1454
|
+
for (const edgeIndex of layout.across) across.add(edgeIndex);
|
|
1455
|
+
for (const [id, item] of layout.items) {
|
|
1456
|
+
const box = { x: originX + item.box.x, y: originY + item.box.y, width: item.box.width, height: item.box.height };
|
|
1457
|
+
if (item.group) {
|
|
1458
|
+
groupRects.set(id, box);
|
|
1459
|
+
const inside2 = groupLayouts.get(id);
|
|
1460
|
+
placeLevel(inside2, box.x + (box.width - inside2.width) / 2, box.y + GROUP_HEADER + GROUP_PADDING);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
boxes.set(id, box);
|
|
1464
|
+
}
|
|
1465
|
+
for (const [edgeIndex, lane] of layout.lanes) {
|
|
1466
|
+
lanes2.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + originX, y: point.y + originY })), reversed: lane.reversed });
|
|
1467
|
+
}
|
|
1468
|
+
for (const [edgeIndex, y] of layout.runs) runs.set(edgeIndex, y + originY);
|
|
1469
|
+
};
|
|
1470
|
+
placeLevel(root, MARGIN, top);
|
|
1471
|
+
let right = MARGIN;
|
|
1472
|
+
for (const box of boxes.values()) right = Math.max(right, box.x + box.width);
|
|
1473
|
+
for (const box of groupRects.values()) right = Math.max(right, box.x + box.width);
|
|
1474
|
+
return { boxes, groupRects, width: right + MARGIN, lanes: lanes2, runs, across };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/render/route.ts
|
|
1478
|
+
var LANE_GAP = 10;
|
|
1479
|
+
var ROUTE_STUB = 16;
|
|
1480
|
+
function normalOf(side) {
|
|
1481
|
+
switch (side) {
|
|
1482
|
+
case "top":
|
|
1483
|
+
return { x: 0, y: -1 };
|
|
1484
|
+
case "bottom":
|
|
1485
|
+
return { x: 0, y: 1 };
|
|
1486
|
+
case "left":
|
|
1487
|
+
return { x: -1, y: 0 };
|
|
1488
|
+
case "right":
|
|
1489
|
+
return { x: 1, y: 0 };
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
var BEND_COST = 24;
|
|
1493
|
+
function crossesBox(x1, y1, x2, y2, box) {
|
|
1494
|
+
const minX = box.x + 0.5;
|
|
1495
|
+
const maxX = box.x + box.width - 0.5;
|
|
1496
|
+
const minY = box.y + 0.5;
|
|
1497
|
+
const maxY = box.y + box.height - 0.5;
|
|
1498
|
+
if (Math.abs(y1 - y2) < 1e-3) {
|
|
1499
|
+
const y = y1;
|
|
1500
|
+
if (y <= minY || y >= maxY) return false;
|
|
1501
|
+
return Math.max(x1, x2) > minX && Math.min(x1, x2) < maxX;
|
|
1502
|
+
}
|
|
1503
|
+
if (Math.abs(x1 - x2) < 1e-3) {
|
|
1504
|
+
const x = x1;
|
|
1505
|
+
if (x <= minX || x >= maxX) return false;
|
|
1506
|
+
return Math.max(y1, y2) > minY && Math.min(y1, y2) < maxY;
|
|
1507
|
+
}
|
|
1508
|
+
const steps = Math.max(8, Math.round(Math.hypot(x2 - x1, y2 - y1)));
|
|
1509
|
+
for (let index = 0; index <= steps; index += 1) {
|
|
1510
|
+
const x = x1 + (x2 - x1) * index / steps;
|
|
1511
|
+
const y = y1 + (y2 - y1) * index / steps;
|
|
1512
|
+
if (x > minX && x < maxX && y > minY && y < maxY) return true;
|
|
1513
|
+
}
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
function anchor(box, side) {
|
|
1517
|
+
switch (side) {
|
|
1518
|
+
case "top":
|
|
1519
|
+
return { x: box.x + box.width / 2, y: box.y };
|
|
1520
|
+
case "bottom":
|
|
1521
|
+
return { x: box.x + box.width / 2, y: box.y + box.height };
|
|
1522
|
+
case "left":
|
|
1523
|
+
return { x: box.x, y: box.y + box.height / 2 };
|
|
1524
|
+
case "right":
|
|
1525
|
+
return { x: box.x + box.width, y: box.y + box.height / 2 };
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function approachSides(from, to) {
|
|
1529
|
+
const fromCy = from.y + from.height / 2;
|
|
1530
|
+
const toCy = to.y + to.height / 2;
|
|
1531
|
+
if (Math.abs(toCy - fromCy) > 0.5) return toCy > fromCy ? { exit: "bottom", entry: "top" } : { exit: "top", entry: "bottom" };
|
|
1532
|
+
const fromCx = from.x + from.width / 2;
|
|
1533
|
+
const toCx = to.x + to.width / 2;
|
|
1534
|
+
return toCx >= fromCx ? { exit: "right", entry: "left" } : { exit: "left", entry: "right" };
|
|
1535
|
+
}
|
|
1536
|
+
function lanes(obstacles, extra) {
|
|
1537
|
+
const values = new Set(extra);
|
|
1538
|
+
for (const box of obstacles) {
|
|
1539
|
+
values.add(box.x - LANE_GAP);
|
|
1540
|
+
values.add(box.x + box.width + LANE_GAP);
|
|
1541
|
+
values.add(box.y - LANE_GAP);
|
|
1542
|
+
values.add(box.y + box.height + LANE_GAP);
|
|
1543
|
+
}
|
|
1544
|
+
return [...values].sort((left, right) => left - right);
|
|
1545
|
+
}
|
|
1546
|
+
function facesAway(side, port, other) {
|
|
1547
|
+
const normal = normalOf(side);
|
|
1548
|
+
return normal.x * (other.x + other.width / 2 - port.x) + normal.y * (other.y + other.height / 2 - port.y) < 0;
|
|
1549
|
+
}
|
|
1550
|
+
function routeEdge(from, to, obstacles, anchors) {
|
|
1551
|
+
const sides = approachSides(from, to);
|
|
1552
|
+
const exitSide = anchors?.exitSide ?? sides.exit;
|
|
1553
|
+
const entrySide = anchors?.entrySide ?? sides.entry;
|
|
1554
|
+
const exit = anchors?.exit ?? anchor(from, exitSide);
|
|
1555
|
+
const entry = anchors?.entry ?? anchor(to, entrySide);
|
|
1556
|
+
const others = obstacles.filter((box) => box !== from && box !== to);
|
|
1557
|
+
const own = [];
|
|
1558
|
+
if (facesAway(exitSide, exit, to)) own.push(from);
|
|
1559
|
+
if (facesAway(entrySide, entry, from)) own.push(to);
|
|
1560
|
+
const blockers = own.length === 0 ? others : [...others, ...own];
|
|
1561
|
+
const blockedStub = (point) => blockers.some((box) => point.x > box.x && point.x < box.x + box.width && point.y > box.y && point.y < box.y + box.height);
|
|
1562
|
+
const outNormal = normalOf(exitSide);
|
|
1563
|
+
const inNormal = normalOf(entrySide);
|
|
1564
|
+
let exitStub = { x: exit.x + outNormal.x * ROUTE_STUB, y: exit.y + outNormal.y * ROUTE_STUB };
|
|
1565
|
+
let entryStub = { x: entry.x + inNormal.x * ROUTE_STUB, y: entry.y + inNormal.y * ROUTE_STUB };
|
|
1566
|
+
if (blockedStub(exitStub) || blockedStub(entryStub)) {
|
|
1567
|
+
exitStub = exit;
|
|
1568
|
+
entryStub = entry;
|
|
1569
|
+
}
|
|
1570
|
+
const straight = [exit, exitStub, entryStub, entry];
|
|
1571
|
+
const axisAligned = Math.abs(exitStub.x - entryStub.x) < 0.5 || Math.abs(exitStub.y - entryStub.y) < 0.5;
|
|
1572
|
+
if (axisAligned && !blockers.some((box) => crossesBox(exitStub.x, exitStub.y, entryStub.x, entryStub.y, box))) {
|
|
1573
|
+
return { points: simplify(straight), routed: false };
|
|
1574
|
+
}
|
|
1575
|
+
const xs = lanes(blockers, [exitStub.x, entryStub.x]);
|
|
1576
|
+
const ys = lanes(blockers, [exitStub.y, entryStub.y]);
|
|
1577
|
+
const blocked = (x1, y1, x2, y2) => blockers.some((box) => crossesBox(x1, y1, x2, y2, box));
|
|
1578
|
+
const start = { point: exitStub, axis: -1, cost: 0, bends: 0, path: exitStub === exit ? [exit] : [exit, exitStub] };
|
|
1579
|
+
const queue = [start];
|
|
1580
|
+
const best = /* @__PURE__ */ new Map();
|
|
1581
|
+
const key = (point) => String(Math.round(point.x)) + "," + String(Math.round(point.y));
|
|
1582
|
+
while (queue.length > 0) {
|
|
1583
|
+
let index = 0;
|
|
1584
|
+
for (let cursor = 1; cursor < queue.length; cursor += 1) if (queue[cursor].cost < queue[index].cost) index = cursor;
|
|
1585
|
+
const current = queue.splice(index, 1)[0];
|
|
1586
|
+
if (current.cost > (best.get(key(current.point)) ?? Infinity)) continue;
|
|
1587
|
+
if (Math.abs(current.point.x - entryStub.x) < 0.5 && Math.abs(current.point.y - entryStub.y) < 0.5) {
|
|
1588
|
+
return { points: simplify([...current.path, entryStub, entry]), routed: true };
|
|
1589
|
+
}
|
|
1590
|
+
const options = [];
|
|
1591
|
+
for (const x of xs) if (Math.abs(x - current.point.x) > 0.5) options.push({ x, y: current.point.y });
|
|
1592
|
+
for (const y of ys) if (Math.abs(y - current.point.y) > 0.5) options.push({ x: current.point.x, y });
|
|
1593
|
+
for (const next of options) {
|
|
1594
|
+
if (blocked(current.point.x, current.point.y, next.x, next.y)) continue;
|
|
1595
|
+
const axis = Math.abs(next.y - current.point.y) < 0.5 ? 0 : 1;
|
|
1596
|
+
const bend = current.axis === -1 || current.axis === axis ? 0 : 1;
|
|
1597
|
+
const cost = current.cost + Math.abs(next.x - current.point.x) + Math.abs(next.y - current.point.y) + bend * BEND_COST;
|
|
1598
|
+
const state = key(next);
|
|
1599
|
+
if (cost >= (best.get(state) ?? Infinity)) continue;
|
|
1600
|
+
best.set(state, cost);
|
|
1601
|
+
queue.push({ point: next, axis, cost, bends: current.bends + bend, path: [...current.path, next] });
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return { points: simplify(straight), routed: false };
|
|
1605
|
+
}
|
|
1606
|
+
function simplify(points) {
|
|
1607
|
+
if (points.length <= 2) return points;
|
|
1608
|
+
const out = [points[0]];
|
|
1609
|
+
for (let index = 1; index < points.length - 1; index += 1) {
|
|
1610
|
+
const previous = out[out.length - 1];
|
|
1611
|
+
const current = points[index];
|
|
1612
|
+
const next = points[index + 1];
|
|
1613
|
+
const collinear = Math.abs(previous.x - current.x) < 0.5 && Math.abs(current.x - next.x) < 0.5 || Math.abs(previous.y - current.y) < 0.5 && Math.abs(current.y - next.y) < 0.5;
|
|
1614
|
+
if (!collinear) out.push(current);
|
|
1615
|
+
}
|
|
1616
|
+
out.push(points[points.length - 1]);
|
|
1617
|
+
return out;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/render/trunk.ts
|
|
1621
|
+
var TRUNK_MIN_EDGES = 3;
|
|
1622
|
+
function isVerticalSide(side) {
|
|
1623
|
+
return side === "top" || side === "bottom";
|
|
1624
|
+
}
|
|
1625
|
+
function trunkAt(box, side) {
|
|
1626
|
+
if (side === "top") return box.y - ROUTE_STUB;
|
|
1627
|
+
if (side === "bottom") return box.y + box.height + ROUTE_STUB;
|
|
1628
|
+
if (side === "left") return box.x - ROUTE_STUB;
|
|
1629
|
+
return box.x + box.width + ROUTE_STUB;
|
|
1630
|
+
}
|
|
1631
|
+
function alongSide(point, side) {
|
|
1632
|
+
return isVerticalSide(side) ? point.x : point.y;
|
|
1633
|
+
}
|
|
1634
|
+
function alongNormal(point, side) {
|
|
1635
|
+
return isVerticalSide(side) ? point.y : point.x;
|
|
1636
|
+
}
|
|
1637
|
+
function facesTrunk(point, side, at) {
|
|
1638
|
+
const normal = normalOf(side);
|
|
1639
|
+
const outward = isVerticalSide(side) ? normal.y : normal.x;
|
|
1640
|
+
return (at - alongNormal(point, side)) * outward > 0.5;
|
|
1641
|
+
}
|
|
1642
|
+
function keyOf(end) {
|
|
1643
|
+
return end.to + "\0" + end.entry + "\0" + (end.arrow ?? "");
|
|
1644
|
+
}
|
|
1645
|
+
function clearSegment(boxes, skip, x1, y1, x2, y2) {
|
|
1646
|
+
for (const box of boxes) {
|
|
1647
|
+
if (skip.includes(box)) continue;
|
|
1648
|
+
if (crossesBox(x1, y1, x2, y2, box)) return false;
|
|
1649
|
+
}
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
function onOneLine(group, side, at, low, high) {
|
|
1653
|
+
if (isVerticalSide(group.side) !== isVerticalSide(side)) return false;
|
|
1654
|
+
if (group.at !== at) return false;
|
|
1655
|
+
return Math.min(group.to, high) - Math.max(group.from, low) > 0.5;
|
|
1656
|
+
}
|
|
1657
|
+
function planTrunks(input) {
|
|
1658
|
+
const groups = [];
|
|
1659
|
+
const groupOf = /* @__PURE__ */ new Map();
|
|
1660
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1661
|
+
for (const end of input.ends) {
|
|
1662
|
+
if (end.runY === void 0) continue;
|
|
1663
|
+
if (isVerticalSide(end.exit) !== isVerticalSide(end.entry)) continue;
|
|
1664
|
+
const source = input.boxOf(end.from);
|
|
1665
|
+
const target = input.boxOf(end.to);
|
|
1666
|
+
if (source === void 0 || target === void 0) continue;
|
|
1667
|
+
if (source === target) continue;
|
|
1668
|
+
const exit = input.exitPortOf(end.index);
|
|
1669
|
+
if (exit === void 0) continue;
|
|
1670
|
+
const at = trunkAt(target, end.entry);
|
|
1671
|
+
if (!facesTrunk(exit, end.exit, at)) continue;
|
|
1672
|
+
const candidate = { end, source, target, at, exit, branch: alongSide(exit, end.exit) };
|
|
1673
|
+
const key = keyOf(end);
|
|
1674
|
+
const list = buckets.get(key);
|
|
1675
|
+
if (list === void 0) buckets.set(key, [candidate]);
|
|
1676
|
+
else list.push(candidate);
|
|
1677
|
+
}
|
|
1678
|
+
for (const list of buckets.values()) {
|
|
1679
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const candidate of list) {
|
|
1681
|
+
const seen = bySource.get(candidate.end.from);
|
|
1682
|
+
if (seen === void 0) bySource.set(candidate.end.from, [candidate]);
|
|
1683
|
+
else seen.push(candidate);
|
|
1684
|
+
}
|
|
1685
|
+
const members = list.filter((candidate) => bySource.get(candidate.end.from).length === 1);
|
|
1686
|
+
if (members.length < TRUNK_MIN_EDGES) continue;
|
|
1687
|
+
const target = members[0].target;
|
|
1688
|
+
const side = members[0].end.entry;
|
|
1689
|
+
const at = members[0].at;
|
|
1690
|
+
const entryAlong = isVerticalSide(side) ? target.x + target.width / 2 : target.y + target.height / 2;
|
|
1691
|
+
let lead = members[0];
|
|
1692
|
+
for (const candidate of members) {
|
|
1693
|
+
if (Math.abs(candidate.branch - entryAlong) < Math.abs(lead.branch - entryAlong) - 1e-9) lead = candidate;
|
|
1694
|
+
}
|
|
1695
|
+
let low = entryAlong;
|
|
1696
|
+
let high = entryAlong;
|
|
1697
|
+
for (const candidate of members) {
|
|
1698
|
+
low = Math.min(low, candidate.branch);
|
|
1699
|
+
high = Math.max(high, candidate.branch);
|
|
1700
|
+
}
|
|
1701
|
+
const vertical = isVerticalSide(side);
|
|
1702
|
+
const run = vertical ? [low, at, high, at] : [at, low, at, high];
|
|
1703
|
+
if (!clearSegment(input.boxes, [target], run[0], run[1], run[2], run[3])) continue;
|
|
1704
|
+
let blocked = false;
|
|
1705
|
+
for (const candidate of members) {
|
|
1706
|
+
const drop = vertical ? [candidate.branch, candidate.exit.y, candidate.branch, at] : [candidate.exit.x, candidate.branch, at, candidate.branch];
|
|
1707
|
+
if (!clearSegment(input.boxes, [candidate.source, target], drop[0], drop[1], drop[2], drop[3])) blocked = true;
|
|
1708
|
+
}
|
|
1709
|
+
if (blocked) continue;
|
|
1710
|
+
if (groups.some((group) => onOneLine(group, side, at, low, high))) continue;
|
|
1711
|
+
const id = groups.length;
|
|
1712
|
+
groups.push({
|
|
1713
|
+
id,
|
|
1714
|
+
target: members[0].end.to,
|
|
1715
|
+
side,
|
|
1716
|
+
at,
|
|
1717
|
+
entryAlong,
|
|
1718
|
+
from: low,
|
|
1719
|
+
to: high,
|
|
1720
|
+
lead: lead.end.index,
|
|
1721
|
+
members: members.map((candidate) => candidate.end.index)
|
|
1722
|
+
});
|
|
1723
|
+
for (const candidate of members) groupOf.set(candidate.end.index, id);
|
|
1724
|
+
}
|
|
1725
|
+
return { groups, groupOf };
|
|
166
1726
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return [...own, ...nested];
|
|
179
|
-
};
|
|
180
|
-
const contents = (groupId) => rawContents(groupId).filter((child) => !byId.has(child) || rawContents(child).length > 0);
|
|
181
|
-
const measure = (id) => {
|
|
182
|
-
if (!byId.has(id)) return { width: nodeWidth(id), height: NODE_HEIGHT };
|
|
183
|
-
const items = contents(id);
|
|
184
|
-
const sizes2 = items.map(measure);
|
|
185
|
-
return {
|
|
186
|
-
width: Math.max(MIN_NODE_WIDTH, ...sizes2.map((size) => size.width)) + GROUP_PADDING * 2,
|
|
187
|
-
height: GROUP_HEADER + GROUP_PADDING * 2 + sizes2.reduce((total, size) => total + size.height, 0) + Math.max(0, items.length - 1) * BLOCK_GAP
|
|
188
|
-
};
|
|
1727
|
+
|
|
1728
|
+
// src/render/symbol.ts
|
|
1729
|
+
function cubicPoint(curve, t) {
|
|
1730
|
+
const u = 1 - t;
|
|
1731
|
+
const a = u * u * u;
|
|
1732
|
+
const b = 3 * u * u * t;
|
|
1733
|
+
const c = 3 * u * t * t;
|
|
1734
|
+
const d = t * t * t;
|
|
1735
|
+
return {
|
|
1736
|
+
x: a * curve.from.x + b * curve.c1.x + c * curve.c2.x + d * curve.to.x,
|
|
1737
|
+
y: a * curve.from.y + b * curve.c1.y + c * curve.c2.y + d * curve.to.y
|
|
189
1738
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1739
|
+
}
|
|
1740
|
+
function diamondPoints(box) {
|
|
1741
|
+
return [
|
|
1742
|
+
{ x: box.x + box.width / 2, y: box.y },
|
|
1743
|
+
{ x: box.x + box.width, y: box.y + box.height / 2 },
|
|
1744
|
+
{ x: box.x + box.width / 2, y: box.y + box.height },
|
|
1745
|
+
{ x: box.x, y: box.y + box.height / 2 }
|
|
1746
|
+
];
|
|
1747
|
+
}
|
|
1748
|
+
function hasDiscretePorts(shape) {
|
|
1749
|
+
return shape === "diamond" || shape === "circle";
|
|
1750
|
+
}
|
|
1751
|
+
function hexagonCut(width) {
|
|
1752
|
+
return Math.min(12, width / 4);
|
|
1753
|
+
}
|
|
1754
|
+
function hexagonPoints(box) {
|
|
1755
|
+
const cut2 = hexagonCut(box.width);
|
|
1756
|
+
return [
|
|
1757
|
+
{ x: box.x + cut2, y: box.y },
|
|
1758
|
+
{ x: box.x + box.width - cut2, y: box.y },
|
|
1759
|
+
{ x: box.x + box.width, y: box.y + box.height / 2 },
|
|
1760
|
+
{ x: box.x + box.width - cut2, y: box.y + box.height },
|
|
1761
|
+
{ x: box.x + cut2, y: box.y + box.height },
|
|
1762
|
+
{ x: box.x, y: box.y + box.height / 2 }
|
|
1763
|
+
];
|
|
1764
|
+
}
|
|
1765
|
+
function stadiumRadius(box) {
|
|
1766
|
+
return Math.min(box.height / 2, box.width / 2);
|
|
1767
|
+
}
|
|
1768
|
+
function cylinderCaps(box) {
|
|
1769
|
+
const cap = Math.min(10, box.height / 3);
|
|
1770
|
+
return {
|
|
1771
|
+
cap,
|
|
1772
|
+
top: {
|
|
1773
|
+
from: { x: box.x, y: box.y + cap },
|
|
1774
|
+
c1: { x: box.x, y: box.y },
|
|
1775
|
+
c2: { x: box.x + box.width, y: box.y },
|
|
1776
|
+
to: { x: box.x + box.width, y: box.y + cap }
|
|
1777
|
+
},
|
|
1778
|
+
bottom: {
|
|
1779
|
+
from: { x: box.x + box.width, y: box.y + box.height - cap },
|
|
1780
|
+
c1: { x: box.x + box.width, y: box.y + box.height },
|
|
1781
|
+
c2: { x: box.x, y: box.y + box.height },
|
|
1782
|
+
to: { x: box.x, y: box.y + box.height - cap }
|
|
206
1783
|
}
|
|
207
|
-
return size;
|
|
208
1784
|
};
|
|
209
|
-
const visibleRoots = roots.filter((id) => contents(id).length > 0);
|
|
210
|
-
const firstIndex = (id) => {
|
|
211
|
-
if (!byId.has(id)) return order.get(id);
|
|
212
|
-
return Math.min(...contents(id).map(firstIndex));
|
|
213
|
-
};
|
|
214
|
-
const topItems = [...loose, ...visibleRoots].sort((left, right) => firstIndex(left) - firstIndex(right));
|
|
215
|
-
const sizes = topItems.map(measure);
|
|
216
|
-
const width = Math.max(MIN_NODE_WIDTH, ...sizes.map((size) => size.width));
|
|
217
|
-
let cursor = top;
|
|
218
|
-
for (const item of topItems) {
|
|
219
|
-
const size = measure(item);
|
|
220
|
-
place2(item, MARGIN + (width - size.width) / 2, cursor);
|
|
221
|
-
cursor += size.height + BLOCK_GAP;
|
|
222
|
-
}
|
|
223
|
-
return { boxes, groupRects, width: width + MARGIN * 2 };
|
|
224
1785
|
}
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
return
|
|
1786
|
+
function cylinderApex(box, side) {
|
|
1787
|
+
const caps = cylinderCaps(box);
|
|
1788
|
+
return cubicPoint(side === "top" ? caps.top : caps.bottom, 0.5);
|
|
228
1789
|
}
|
|
229
1790
|
|
|
230
1791
|
// src/render/palette.ts
|
|
@@ -327,12 +1888,36 @@ window.__ModuleLoader__.load({
|
|
|
327
1888
|
|
|
328
1889
|
// src/render/paint.ts
|
|
329
1890
|
var TITLE_HEIGHT = 26;
|
|
330
|
-
var NODE_HEIGHT2 = 34;
|
|
331
|
-
var CHANNEL_GAP = 16;
|
|
332
1891
|
var CAPTION_CHARS = 78;
|
|
333
1892
|
var CAPTION_LINE = 15;
|
|
334
1893
|
var CAPTION_GAP = 16;
|
|
335
1894
|
var CAPTION_CHAR_WIDTH = 6.2;
|
|
1895
|
+
var PORT_SEPARATION = 14;
|
|
1896
|
+
var PORT_SIDES = ["top", "right", "bottom", "left"];
|
|
1897
|
+
var isVerticalSide2 = (side) => side === "top" || side === "bottom";
|
|
1898
|
+
var MULTI_EDGE_SPACING = 20;
|
|
1899
|
+
function packPorts(wanted, low, high) {
|
|
1900
|
+
const placed = [];
|
|
1901
|
+
let previous = low - PORT_SEPARATION;
|
|
1902
|
+
for (const value of wanted) {
|
|
1903
|
+
previous = Math.min(high, Math.max(value, previous + PORT_SEPARATION));
|
|
1904
|
+
placed.push(previous);
|
|
1905
|
+
}
|
|
1906
|
+
let next = high + PORT_SEPARATION;
|
|
1907
|
+
for (let index = placed.length - 1; index >= 0; index -= 1) {
|
|
1908
|
+
next = Math.max(low, Math.min(placed[index], next - PORT_SEPARATION));
|
|
1909
|
+
placed[index] = next;
|
|
1910
|
+
}
|
|
1911
|
+
return placed;
|
|
1912
|
+
}
|
|
1913
|
+
function centreRun(placed, low, high) {
|
|
1914
|
+
if (placed.length === 0) return placed;
|
|
1915
|
+
const first = placed[0];
|
|
1916
|
+
const last = placed[placed.length - 1];
|
|
1917
|
+
const middle = (low + high) / 2;
|
|
1918
|
+
const shift = Math.min(high - last, Math.max(low - first, middle - (first + last) / 2));
|
|
1919
|
+
return shift === 0 ? placed : placed.map((value) => value + shift);
|
|
1920
|
+
}
|
|
336
1921
|
function roleOfNode(node) {
|
|
337
1922
|
if (node.role === "error") return "fail";
|
|
338
1923
|
if (node.role === "warning") return "findings";
|
|
@@ -363,6 +1948,113 @@ window.__ModuleLoader__.load({
|
|
|
363
1948
|
function captionRole(caption) {
|
|
364
1949
|
return roleForVerdict(caption.split(" \u2014 ")[0]);
|
|
365
1950
|
}
|
|
1951
|
+
var LABEL_HEIGHT = 12;
|
|
1952
|
+
var LABEL_PAD = 2;
|
|
1953
|
+
var LABEL_STEP = 3;
|
|
1954
|
+
function runsOf(points) {
|
|
1955
|
+
const runs = [];
|
|
1956
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
1957
|
+
const from = points[index];
|
|
1958
|
+
const to = points[index + 1];
|
|
1959
|
+
const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
|
|
1960
|
+
if (length < 1) continue;
|
|
1961
|
+
runs.push({ from, to, length, index });
|
|
1962
|
+
}
|
|
1963
|
+
return runs.sort((left, right) => right.length - left.length || left.index - right.index);
|
|
1964
|
+
}
|
|
1965
|
+
function placeLabel(label, points, boxes, placed, otherRoutes) {
|
|
1966
|
+
const width = textWidth(label);
|
|
1967
|
+
const height = LABEL_HEIGHT;
|
|
1968
|
+
const spotAt = (x, y) => ({ x, y, width, height });
|
|
1969
|
+
const hitsBox = (spot, box) => spot.x - spot.width / 2 < box.x + box.width + LABEL_PAD && spot.x + spot.width / 2 > box.x - LABEL_PAD && spot.y - spot.height / 2 < box.y + box.height + LABEL_PAD && spot.y + spot.height / 2 > box.y - LABEL_PAD;
|
|
1970
|
+
const hitsSpot = (spot, other) => spot.x - spot.width / 2 < other.x + other.width / 2 + LABEL_PAD && spot.x + spot.width / 2 > other.x - other.width / 2 - LABEL_PAD && spot.y - spot.height / 2 < other.y + other.height / 2 + LABEL_PAD && spot.y + spot.height / 2 > other.y - other.height / 2 - LABEL_PAD;
|
|
1971
|
+
const hitsSegment = (spot, segment) => spot.x + spot.width / 2 > Math.min(segment.x1, segment.x2) - LABEL_PAD && spot.x - spot.width / 2 < Math.max(segment.x1, segment.x2) + LABEL_PAD && spot.y + spot.height / 2 > Math.min(segment.y1, segment.y2) - LABEL_PAD && spot.y - spot.height / 2 < Math.max(segment.y1, segment.y2) + LABEL_PAD;
|
|
1972
|
+
const collisionsAt = (spot, run) => {
|
|
1973
|
+
let boxHits = 0;
|
|
1974
|
+
let labelHits = 0;
|
|
1975
|
+
let edgeHits = 0;
|
|
1976
|
+
for (const box of boxes) if (hitsBox(spot, box)) boxHits += 1;
|
|
1977
|
+
for (const earlier of placed) if (hitsSpot(spot, earlier)) labelHits += 1;
|
|
1978
|
+
for (const route of otherRoutes) {
|
|
1979
|
+
for (const segment of route) {
|
|
1980
|
+
if (!hitsSegment(spot, segment)) continue;
|
|
1981
|
+
edgeHits += 1;
|
|
1982
|
+
break;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return { run, collisions: boxHits + labelHits + edgeHits, boxes: boxHits, labels: labelHits, edges: edgeHits };
|
|
1986
|
+
};
|
|
1987
|
+
const runs = runsOf(points);
|
|
1988
|
+
if (runs.length === 0) {
|
|
1989
|
+
const point = points[0] ?? { x: 0, y: 0 };
|
|
1990
|
+
return { ...spotAt(point.x, point.y), run: -1, collisions: 0, boxes: 0, labels: 0, edges: 0 };
|
|
1991
|
+
}
|
|
1992
|
+
let best;
|
|
1993
|
+
for (const [runIndex, run] of runs.entries()) {
|
|
1994
|
+
const dx = run.to.x - run.from.x;
|
|
1995
|
+
const dy = run.to.y - run.from.y;
|
|
1996
|
+
const ux = dx / run.length;
|
|
1997
|
+
const uy = dy / run.length;
|
|
1998
|
+
const middle = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2 };
|
|
1999
|
+
const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
|
|
2000
|
+
const bound = Math.max(0, run.length / 2 - along);
|
|
2001
|
+
const walk = [0];
|
|
2002
|
+
for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
|
|
2003
|
+
for (const offset of walk) {
|
|
2004
|
+
const spot = spotAt(middle.x + ux * offset, middle.y + uy * offset);
|
|
2005
|
+
const placement = collisionsAt(spot, runIndex);
|
|
2006
|
+
if (placement.collisions === 0) return { ...spot, ...placement };
|
|
2007
|
+
if (best === void 0 || placement.collisions < best.collisions || placement.collisions === best.collisions && placement.boxes + placement.labels < best.boxes + best.labels) {
|
|
2008
|
+
best = { ...spot, ...placement };
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return best;
|
|
2013
|
+
}
|
|
2014
|
+
function drawnExtent(shapes) {
|
|
2015
|
+
let width = 0;
|
|
2016
|
+
let height = 0;
|
|
2017
|
+
const consider = (x, y) => {
|
|
2018
|
+
if (x > width) width = x;
|
|
2019
|
+
if (y > height) height = y;
|
|
2020
|
+
};
|
|
2021
|
+
for (const shape of shapes) {
|
|
2022
|
+
switch (shape.kind) {
|
|
2023
|
+
case "box":
|
|
2024
|
+
case "group":
|
|
2025
|
+
consider(shape.x + shape.width, shape.y + shape.height);
|
|
2026
|
+
break;
|
|
2027
|
+
case "link":
|
|
2028
|
+
consider(Math.max(shape.x1, shape.x2), Math.max(shape.y1, shape.y2));
|
|
2029
|
+
if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
|
|
2030
|
+
break;
|
|
2031
|
+
case "path": {
|
|
2032
|
+
for (const point of shape.points) consider(point.x, point.y);
|
|
2033
|
+
if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
case "caption":
|
|
2037
|
+
case "title":
|
|
2038
|
+
consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
|
|
2039
|
+
break;
|
|
2040
|
+
case "junction":
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return { width, height };
|
|
2045
|
+
}
|
|
2046
|
+
function boxesCrossedBy(points, from, to, boxes) {
|
|
2047
|
+
const crossed = [];
|
|
2048
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
2049
|
+
const a = points[index];
|
|
2050
|
+
const b = points[index + 1];
|
|
2051
|
+
for (const box of boxes) {
|
|
2052
|
+
if (box === from || box === to) continue;
|
|
2053
|
+
if (crossesBox(a.x, a.y, b.x, b.y, box)) crossed.push(box);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return crossed;
|
|
2057
|
+
}
|
|
366
2058
|
function paintWith(diagram, overrides = {}, options = {}) {
|
|
367
2059
|
const colours = palette(overrides, options);
|
|
368
2060
|
const shapes = [];
|
|
@@ -370,16 +2062,23 @@ window.__ModuleLoader__.load({
|
|
|
370
2062
|
const topOffset = MARGIN + TITLE_HEIGHT;
|
|
371
2063
|
const placement = place(diagram, topOffset);
|
|
372
2064
|
const { boxes, groupRects } = placement;
|
|
2065
|
+
const endBox = (id) => boxes.get(id) ?? groupRects.get(id);
|
|
2066
|
+
const shapeById = /* @__PURE__ */ new Map();
|
|
2067
|
+
for (const node of diagram.nodes) if (node.shape !== void 0 && !shapeById.has(node.id)) shapeById.set(node.id, node.shape);
|
|
2068
|
+
const shapeOf = (id) => shapeById.get(id) ?? "rect";
|
|
373
2069
|
for (const group of groups) {
|
|
374
2070
|
const rect = groupRects.get(group.id);
|
|
375
2071
|
if (rect === void 0) continue;
|
|
376
2072
|
shapes.push({ kind: "group", id: group.id, x: rect.x, y: rect.y, width: rect.width, height: rect.height, label: group.label, colour: "muted" });
|
|
377
2073
|
}
|
|
378
2074
|
const drawn = /* @__PURE__ */ new Set();
|
|
2075
|
+
const boxList = Array.from(boxes.values());
|
|
2076
|
+
const routes = [];
|
|
379
2077
|
for (const node of diagram.nodes) {
|
|
380
2078
|
if (drawn.has(node.id)) continue;
|
|
381
2079
|
const box = boxes.get(node.id);
|
|
382
2080
|
drawn.add(node.id);
|
|
2081
|
+
const text2 = textOf(node);
|
|
383
2082
|
shapes.push({
|
|
384
2083
|
kind: "box",
|
|
385
2084
|
x: box.x,
|
|
@@ -387,23 +2086,540 @@ window.__ModuleLoader__.load({
|
|
|
387
2086
|
width: box.width,
|
|
388
2087
|
height: box.height,
|
|
389
2088
|
label: labelOf(node),
|
|
2089
|
+
lines: text2.lines,
|
|
2090
|
+
...node.shape === void 0 ? {} : { shape: node.shape },
|
|
390
2091
|
colour: roleOfNode(node),
|
|
391
2092
|
initial: diagram.init === node.id
|
|
392
2093
|
});
|
|
393
2094
|
}
|
|
394
2095
|
const roleById = new Map(diagram.nodes.map((node) => [node.id, roleOfNode(node)]));
|
|
395
|
-
|
|
396
|
-
|
|
2096
|
+
const multiEdgeSets = /* @__PURE__ */ new Map();
|
|
2097
|
+
diagram.edges.forEach((edge, index) => {
|
|
2098
|
+
if (edge.from === edge.to) return;
|
|
2099
|
+
if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) return;
|
|
2100
|
+
const key = edge.from < edge.to ? edge.from + "\0" + edge.to : edge.to + "\0" + edge.from;
|
|
2101
|
+
const list = multiEdgeSets.get(key);
|
|
2102
|
+
if (list === void 0) multiEdgeSets.set(key, [index]);
|
|
2103
|
+
else list.push(index);
|
|
2104
|
+
});
|
|
2105
|
+
const fanOf = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const list of multiEdgeSets.values()) {
|
|
2107
|
+
if (list.length < 2) continue;
|
|
2108
|
+
list.forEach((index, position) => {
|
|
2109
|
+
if (position === 0) return;
|
|
2110
|
+
fanOf.set(index, (position % 2 === 1 ? 1 : -1) * Math.ceil(position / 2) * MULTI_EDGE_SPACING);
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
const endKey = (index, end) => String(index) + ":" + end;
|
|
2114
|
+
const endsAt = /* @__PURE__ */ new Map();
|
|
397
2115
|
for (const edge of diagram.edges) {
|
|
398
|
-
|
|
399
|
-
|
|
2116
|
+
if (edge.from === edge.to) continue;
|
|
2117
|
+
if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) continue;
|
|
2118
|
+
endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
|
|
2119
|
+
endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
|
|
2120
|
+
}
|
|
2121
|
+
const spanOf2 = (boxId, side, corner) => {
|
|
2122
|
+
const box = endBox(boxId);
|
|
2123
|
+
const vertical = side === "top" || side === "bottom";
|
|
2124
|
+
const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
|
|
2125
|
+
const within = (low, high) => ({ low, high: Math.max(low, high) });
|
|
2126
|
+
switch (shapeOf(boxId)) {
|
|
2127
|
+
// The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
|
|
2128
|
+
case "diamond":
|
|
2129
|
+
case "circle":
|
|
2130
|
+
return within(middle, middle);
|
|
2131
|
+
case "hexagon":
|
|
2132
|
+
return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
|
|
2133
|
+
case "stadium": {
|
|
2134
|
+
const radius = stadiumRadius(box);
|
|
2135
|
+
return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
|
|
2136
|
+
}
|
|
2137
|
+
case "cylinder": {
|
|
2138
|
+
if (vertical) return within(middle, middle);
|
|
2139
|
+
const cap = cylinderCaps(box).cap;
|
|
2140
|
+
return within(box.y + cap, box.y + box.height - cap);
|
|
2141
|
+
}
|
|
2142
|
+
default: {
|
|
2143
|
+
const inset = corner ? 2 : vertical ? PORT_INSET : 8;
|
|
2144
|
+
return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
const clampTo = (boxId, side, value, corner) => {
|
|
2149
|
+
const span = spanOf2(boxId, side, corner);
|
|
2150
|
+
return Math.min(span.high, Math.max(span.low, value));
|
|
2151
|
+
};
|
|
2152
|
+
const pointOn = (boxId, side, along) => {
|
|
2153
|
+
const box = endBox(boxId);
|
|
2154
|
+
if (side === "left") return { x: box.x, y: along };
|
|
2155
|
+
if (side === "right") return { x: box.x + box.width, y: along };
|
|
2156
|
+
if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
|
|
2157
|
+
return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
|
|
2158
|
+
};
|
|
2159
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
2160
|
+
const claimedAt = /* @__PURE__ */ new Map();
|
|
2161
|
+
const claim = (id, index, end, side) => {
|
|
2162
|
+
claimed.set(endKey(index, end), side);
|
|
2163
|
+
claimedAt.set(id, [...claimedAt.get(id) ?? [], { index, side }]);
|
|
2164
|
+
};
|
|
2165
|
+
const takenSides = (id, except) => (claimedAt.get(id) ?? []).filter((entry) => entry.index !== except).map((entry) => entry.side);
|
|
2166
|
+
const sideMiddle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
|
|
2167
|
+
const sidePoint = (boxId, side) => pointOn(boxId, side, sideMiddle(endBox(boxId), side));
|
|
2168
|
+
const facesAway2 = (box, other, side) => {
|
|
2169
|
+
const out = normalOf(side);
|
|
2170
|
+
const dx = other.x + other.width / 2 - (box.x + box.width / 2);
|
|
2171
|
+
const dy = other.y + other.height / 2 - (box.y + box.height / 2);
|
|
2172
|
+
return out.x * dx + out.y * dy < 0;
|
|
2173
|
+
};
|
|
2174
|
+
const stubAlong = (pinnedId, pinnedSide, freeSide) => {
|
|
2175
|
+
const point = sidePoint(pinnedId, pinnedSide);
|
|
2176
|
+
const out = normalOf(pinnedSide);
|
|
2177
|
+
return isVerticalSide2(freeSide) ? point.x + out.x * ROUTE_STUB : point.y + out.y * ROUTE_STUB;
|
|
2178
|
+
};
|
|
2179
|
+
const offers = (boxId, side, value) => {
|
|
2180
|
+
const span = spanOf2(boxId, side, true);
|
|
2181
|
+
return value >= span.low - 0.01 && value <= span.high + 0.01;
|
|
2182
|
+
};
|
|
2183
|
+
const bendBudget = (fromId, toId, exit, entry, index) => {
|
|
2184
|
+
const from = endBox(fromId);
|
|
2185
|
+
const to = endBox(toId);
|
|
2186
|
+
if (facesAway2(from, to, exit) || facesAway2(to, from, entry)) return 4;
|
|
2187
|
+
if (isVerticalSide2(exit) === isVerticalSide2(entry)) {
|
|
2188
|
+
const out = normalOf(exit);
|
|
2189
|
+
const into = normalOf(entry);
|
|
2190
|
+
const sameWay = out.x === -into.x && out.y === -into.y;
|
|
2191
|
+
const fromSpan = spanOf2(fromId, exit, true);
|
|
2192
|
+
const toSpan = spanOf2(toId, entry, true);
|
|
2193
|
+
const meet = fromSpan.low <= toSpan.high + 0.01 && toSpan.low <= fromSpan.high + 0.01;
|
|
2194
|
+
return sameWay && meet ? 0 : 2;
|
|
2195
|
+
}
|
|
2196
|
+
const horizontalId = isVerticalSide2(exit) ? toId : fromId;
|
|
2197
|
+
const horizontalSide = isVerticalSide2(exit) ? entry : exit;
|
|
2198
|
+
const verticalId = isVerticalSide2(exit) ? fromId : toId;
|
|
2199
|
+
const verticalSide = isVerticalSide2(exit) ? exit : entry;
|
|
2200
|
+
if (!offers(verticalId, verticalSide, stubAlong(horizontalId, horizontalSide, verticalSide))) return 2;
|
|
2201
|
+
const crowded = takenSides(fromId, index).includes(exit) || takenSides(toId, index).includes(entry);
|
|
2202
|
+
return crowded ? 2 : 1;
|
|
2203
|
+
};
|
|
2204
|
+
const bestSide = (id, otherId, end, partner, index) => {
|
|
2205
|
+
const box = endBox(id);
|
|
2206
|
+
const other = endBox(otherId);
|
|
2207
|
+
const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
2208
|
+
const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
|
|
2209
|
+
const reach = Math.hypot(toward.x, toward.y);
|
|
2210
|
+
const taken = takenSides(id, index);
|
|
2211
|
+
let best;
|
|
2212
|
+
for (const side of PORT_SIDES) {
|
|
2213
|
+
if (taken.includes(side)) continue;
|
|
2214
|
+
const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
|
|
2215
|
+
const normal = normalOf(side);
|
|
2216
|
+
const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
|
|
2217
|
+
const point = anchor(box, side);
|
|
2218
|
+
const near = Math.hypot(
|
|
2219
|
+
Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
|
|
2220
|
+
Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
|
|
2221
|
+
);
|
|
2222
|
+
const partnerPoint = anchor(other, partner);
|
|
2223
|
+
const ground = Math.abs(point.x - partnerPoint.x) + Math.abs(point.y - partnerPoint.y);
|
|
2224
|
+
const better = best === void 0 || bends < best.bends || bends === best.bends && (score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && (near < best.near - 1e-9 || Math.abs(near - best.near) <= 1e-9 && ground < best.ground - 1e-9));
|
|
2225
|
+
if (better) best = { side, bends, score, near, ground };
|
|
2226
|
+
}
|
|
2227
|
+
return best?.side;
|
|
2228
|
+
};
|
|
2229
|
+
const bestPair = (fromId, toId, index) => {
|
|
2230
|
+
const approach = approachSides(endBox(fromId), endBox(toId));
|
|
2231
|
+
const pairs = [
|
|
2232
|
+
{ exit: "bottom", entry: "top" },
|
|
2233
|
+
{ exit: "top", entry: "bottom" },
|
|
2234
|
+
{ exit: "right", entry: "left" },
|
|
2235
|
+
{ exit: "left", entry: "right" }
|
|
2236
|
+
];
|
|
2237
|
+
let best = approach;
|
|
2238
|
+
let bestBends = bendBudget(fromId, toId, approach.exit, approach.entry, index);
|
|
2239
|
+
for (const pair of pairs) {
|
|
2240
|
+
const bends = bendBudget(fromId, toId, pair.exit, pair.entry, index);
|
|
2241
|
+
if (bends >= bestBends) continue;
|
|
2242
|
+
best = pair;
|
|
2243
|
+
bestBends = bends;
|
|
2244
|
+
}
|
|
2245
|
+
return best;
|
|
2246
|
+
};
|
|
2247
|
+
const bestEnd = (id, otherId, end, partner, index) => {
|
|
2248
|
+
const approach = end === "exit" ? approachSides(endBox(id), endBox(otherId)).exit : approachSides(endBox(otherId), endBox(id)).entry;
|
|
2249
|
+
let best = approach;
|
|
2250
|
+
let bestBends = end === "exit" ? bendBudget(id, otherId, approach, partner, index) : bendBudget(otherId, id, partner, approach, index);
|
|
2251
|
+
for (const side of PORT_SIDES) {
|
|
2252
|
+
const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
|
|
2253
|
+
if (bends >= bestBends) continue;
|
|
2254
|
+
best = side;
|
|
2255
|
+
bestBends = bends;
|
|
2256
|
+
}
|
|
2257
|
+
return best;
|
|
2258
|
+
};
|
|
2259
|
+
diagram.edges.forEach((edge, index) => {
|
|
2260
|
+
if (edge.from === edge.to) return;
|
|
2261
|
+
const source = endBox(edge.from);
|
|
2262
|
+
const target = endBox(edge.to);
|
|
2263
|
+
if (source === void 0 || target === void 0) return;
|
|
2264
|
+
if (placement.across.has(index)) {
|
|
2265
|
+
claim(edge.from, index, "exit", "right");
|
|
2266
|
+
claim(edge.to, index, "entry", "left");
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
const lane = placement.lanes.get(index);
|
|
2270
|
+
if (lane === void 0 || lane.points.length === 0) return;
|
|
2271
|
+
const down = target.y > source.y;
|
|
2272
|
+
claim(edge.from, index, "exit", down ? "bottom" : "top");
|
|
2273
|
+
claim(edge.to, index, "entry", down ? "top" : "bottom");
|
|
2274
|
+
});
|
|
2275
|
+
diagram.edges.forEach((edge, index) => {
|
|
2276
|
+
if (edge.from === edge.to) return;
|
|
2277
|
+
const source = endBox(edge.from);
|
|
2278
|
+
const target = endBox(edge.to);
|
|
2279
|
+
if (source === void 0 || target === void 0) return;
|
|
2280
|
+
for (const end of ["exit", "entry"]) {
|
|
2281
|
+
if (claimed.has(endKey(index, end))) continue;
|
|
2282
|
+
const id = end === "exit" ? edge.from : edge.to;
|
|
2283
|
+
if ((endsAt.get(id) ?? 0) < 2) continue;
|
|
2284
|
+
if (!hasDiscretePorts(shapeOf(id))) continue;
|
|
2285
|
+
const approached = approachSides(source, target);
|
|
2286
|
+
const partner = claimed.get(endKey(index, end === "exit" ? "entry" : "exit")) ?? (end === "exit" ? approached.entry : approached.exit);
|
|
2287
|
+
const side = bestSide(id, end === "exit" ? edge.to : edge.from, end, partner, index);
|
|
2288
|
+
if (side === void 0) continue;
|
|
2289
|
+
claim(id, index, end, side);
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
const facing = diagram.edges.map((edge, index) => {
|
|
2293
|
+
const source = endBox(edge.from);
|
|
2294
|
+
const target = endBox(edge.to);
|
|
2295
|
+
if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
|
|
2296
|
+
if (placement.across.has(index)) return { exit: "right", entry: "left" };
|
|
2297
|
+
const lane = placement.lanes.get(index);
|
|
2298
|
+
const down = target.y > source.y;
|
|
2299
|
+
if (lane !== void 0 && lane.points.length > 0) {
|
|
2300
|
+
return {
|
|
2301
|
+
exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
|
|
2302
|
+
entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
|
|
2303
|
+
at: lane.points[0].x
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
const exit = claimed.get(endKey(index, "exit"));
|
|
2307
|
+
const entry = claimed.get(endKey(index, "entry"));
|
|
2308
|
+
if (exit !== void 0 && entry !== void 0) return { exit, entry };
|
|
2309
|
+
if (exit !== void 0) return { exit, entry: bestEnd(edge.to, edge.from, "entry", exit, index) };
|
|
2310
|
+
if (entry !== void 0) return { exit: bestEnd(edge.from, edge.to, "exit", entry, index), entry };
|
|
2311
|
+
return bestPair(edge.from, edge.to, index);
|
|
2312
|
+
});
|
|
2313
|
+
const laneClear = (points, x, from, to) => {
|
|
2314
|
+
for (let at = 0; at + 1 < points.length; at += 1) {
|
|
2315
|
+
for (const box of boxList) {
|
|
2316
|
+
if (box === from || box === to) continue;
|
|
2317
|
+
if (crossesBox(x, points[at].y, x, points[at + 1].y, box)) return false;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return true;
|
|
2321
|
+
};
|
|
2322
|
+
const laneShift = /* @__PURE__ */ new Map();
|
|
2323
|
+
for (const list of multiEdgeSets.values()) {
|
|
2324
|
+
if (list.length < 2) continue;
|
|
2325
|
+
const lead = placement.lanes.get(list[0]);
|
|
2326
|
+
if (lead === void 0 || lead.points.length === 0) continue;
|
|
2327
|
+
const base = lead.points[0].x;
|
|
2328
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2329
|
+
for (const index of list) {
|
|
2330
|
+
const lane = placement.lanes.get(index);
|
|
2331
|
+
const own = lane.points[0].x;
|
|
2332
|
+
const offset = fanOf.get(index) ?? 0;
|
|
2333
|
+
const edge = diagram.edges[index];
|
|
2334
|
+
const from = endBox(edge.from);
|
|
2335
|
+
const to = endBox(edge.to);
|
|
2336
|
+
let target = own;
|
|
2337
|
+
if (offset !== 0) {
|
|
2338
|
+
for (const candidate of [base + offset, own + offset]) {
|
|
2339
|
+
if (taken.has(candidate) || !laneClear(lane.points, candidate, from, to)) continue;
|
|
2340
|
+
target = candidate;
|
|
2341
|
+
break;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
taken.add(target);
|
|
2345
|
+
if (target !== own) laneShift.set(index, target - own);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2349
|
+
const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
|
|
2350
|
+
for (const [index, entry] of facing.entries()) {
|
|
2351
|
+
if (entry === void 0) continue;
|
|
2352
|
+
const edge = diagram.edges[index];
|
|
2353
|
+
const from = endBox(edge.from);
|
|
2354
|
+
const to = endBox(edge.to);
|
|
2355
|
+
const middle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
|
|
2356
|
+
const fromMiddle = middle(from, entry.exit);
|
|
2357
|
+
const toMiddle = middle(to, entry.entry);
|
|
2358
|
+
const corner = entry.at !== void 0;
|
|
2359
|
+
const offset = fanOf.get(index) ?? 0;
|
|
2360
|
+
let exitWanted;
|
|
2361
|
+
let entryWanted;
|
|
2362
|
+
if (entry.at !== void 0) {
|
|
2363
|
+
const chosen = entry.at + (laneShift.get(index) ?? 0);
|
|
2364
|
+
exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
|
|
2365
|
+
entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
|
|
2366
|
+
} else if (isVerticalSide2(entry.exit) === isVerticalSide2(entry.entry)) {
|
|
2367
|
+
const target = spanOf2(edge.to, entry.entry, false);
|
|
2368
|
+
const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
|
|
2369
|
+
exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
|
|
2370
|
+
entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
|
|
2371
|
+
} else {
|
|
2372
|
+
exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, stubAlong(edge.to, entry.entry, entry.exit), false) + offset;
|
|
2373
|
+
entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, stubAlong(edge.from, entry.exit, entry.entry), false) + offset;
|
|
2374
|
+
}
|
|
2375
|
+
wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
|
|
2376
|
+
wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
|
|
2377
|
+
}
|
|
2378
|
+
const sides = /* @__PURE__ */ new Map();
|
|
2379
|
+
for (const [index, entry] of facing.entries()) {
|
|
2380
|
+
if (entry === void 0) continue;
|
|
2381
|
+
const edge = diagram.edges[index];
|
|
2382
|
+
sides.set(edge.from + ":" + entry.exit, [
|
|
2383
|
+
...sides.get(edge.from + ":" + entry.exit) ?? [],
|
|
2384
|
+
{ index, wanted: wanted.get(slotKey(edge.from, entry.exit, index)) }
|
|
2385
|
+
]);
|
|
2386
|
+
sides.set(edge.to + ":" + entry.entry, [
|
|
2387
|
+
...sides.get(edge.to + ":" + entry.entry) ?? [],
|
|
2388
|
+
{ index, wanted: wanted.get(slotKey(edge.to, entry.entry, index)) }
|
|
2389
|
+
]);
|
|
2390
|
+
}
|
|
2391
|
+
let placedPort = /* @__PURE__ */ new Map();
|
|
2392
|
+
for (let round = 0; round < 3; round += 1) {
|
|
2393
|
+
placedPort = /* @__PURE__ */ new Map();
|
|
2394
|
+
for (const [key, list] of sides) {
|
|
2395
|
+
const [boxId, side] = key.split(":");
|
|
2396
|
+
const span = spanOf2(boxId, side, true);
|
|
2397
|
+
const order2 = [...list].sort((left, right) => left.wanted - right.wanted || left.index - right.index);
|
|
2398
|
+
const packed = packPorts(
|
|
2399
|
+
order2.map((entry) => clampTo(boxId, side, entry.wanted, true)),
|
|
2400
|
+
span.low,
|
|
2401
|
+
span.high
|
|
2402
|
+
);
|
|
2403
|
+
order2.forEach((entry, at) => placedPort.set(slotKey(boxId, side, entry.index), packed[at]));
|
|
2404
|
+
}
|
|
2405
|
+
let moved = false;
|
|
2406
|
+
for (const [index, entry] of facing.entries()) {
|
|
2407
|
+
if (entry === void 0) continue;
|
|
2408
|
+
const edge = diagram.edges[index];
|
|
2409
|
+
const vertical = entry.exit === "bottom" || entry.exit === "top";
|
|
2410
|
+
if (!vertical) continue;
|
|
2411
|
+
const exit = placedPort.get(slotKey(edge.from, entry.exit, index));
|
|
2412
|
+
if (Math.abs(exit - placedPort.get(slotKey(edge.to, entry.entry, index))) < 0.01) continue;
|
|
2413
|
+
if (Math.abs(clampTo(edge.to, entry.entry, exit, true) - exit) > 0.01) continue;
|
|
2414
|
+
for (const slot of sides.get(edge.to + ":" + entry.entry)) {
|
|
2415
|
+
if (slot.index !== index) continue;
|
|
2416
|
+
slot.wanted = exit;
|
|
2417
|
+
moved = true;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (!moved) break;
|
|
2421
|
+
}
|
|
2422
|
+
const trunkEnds = [];
|
|
2423
|
+
diagram.edges.forEach((edge, index) => {
|
|
2424
|
+
const entry = facing[index];
|
|
2425
|
+
if (entry === void 0) return;
|
|
2426
|
+
const runY = placement.runs.get(index);
|
|
2427
|
+
trunkEnds.push({
|
|
2428
|
+
index,
|
|
2429
|
+
from: edge.from,
|
|
2430
|
+
to: edge.to,
|
|
2431
|
+
exit: entry.exit,
|
|
2432
|
+
entry: entry.entry,
|
|
2433
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow },
|
|
2434
|
+
...runY === void 0 ? {} : { runY }
|
|
2435
|
+
});
|
|
2436
|
+
});
|
|
2437
|
+
const planFor = (exitPortOf) => planTrunks({
|
|
2438
|
+
ends: trunkEnds,
|
|
2439
|
+
// Only a node's own box is a trunk target: an arrow into a boundary points past it, not at it.
|
|
2440
|
+
boxOf: (id) => boxes.get(id),
|
|
2441
|
+
boxes: boxList,
|
|
2442
|
+
exitPortOf
|
|
2443
|
+
});
|
|
2444
|
+
const packedExit = (index) => {
|
|
2445
|
+
const entry = facing[index];
|
|
2446
|
+
if (entry === void 0) return void 0;
|
|
2447
|
+
const edge = diagram.edges[index];
|
|
2448
|
+
const coordinate = placedPort.get(slotKey(edge.from, entry.exit, index));
|
|
2449
|
+
return coordinate === void 0 ? void 0 : pointOn(edge.from, entry.exit, coordinate);
|
|
2450
|
+
};
|
|
2451
|
+
const planned = planFor(packedExit);
|
|
2452
|
+
const onLane = (index, coordinate) => {
|
|
2453
|
+
if (planned.groupOf.has(index)) return false;
|
|
2454
|
+
const lane = placement.lanes.get(index);
|
|
2455
|
+
if (lane === void 0 || lane.points.length === 0) return false;
|
|
2456
|
+
return Math.abs(lane.points[0].x - coordinate) <= 0.5;
|
|
2457
|
+
};
|
|
2458
|
+
const rows = [];
|
|
2459
|
+
const rowOf = /* @__PURE__ */ new Map();
|
|
2460
|
+
for (const [key, list] of sides) {
|
|
2461
|
+
const [boxId, side] = key.split(":");
|
|
2462
|
+
const span = spanOf2(boxId, side, true);
|
|
2463
|
+
const slots = [...list].map((entry) => ({ index: entry.index, at: placedPort.get(slotKey(boxId, side, entry.index)) })).sort((left, right) => left.at - right.at || left.index - right.index);
|
|
2464
|
+
const centred = centreRun(
|
|
2465
|
+
slots.map((slot) => slot.at),
|
|
2466
|
+
span.low,
|
|
2467
|
+
span.high
|
|
2468
|
+
);
|
|
2469
|
+
const first = slots[0];
|
|
2470
|
+
const row = {
|
|
2471
|
+
boxId,
|
|
2472
|
+
side,
|
|
2473
|
+
indexes: slots.map((slot) => slot.index),
|
|
2474
|
+
at: slots.map((slot) => slot.at),
|
|
2475
|
+
position: new Map(slots.map((slot, at) => [slot.index, at])),
|
|
2476
|
+
shift: centred[0] - first.at,
|
|
2477
|
+
allowed: true
|
|
2478
|
+
};
|
|
2479
|
+
rows.push(row);
|
|
2480
|
+
rowOf.set(key, row);
|
|
2481
|
+
}
|
|
2482
|
+
const movedAt = (row, at) => row.at[at] + (row.allowed ? row.shift : 0);
|
|
2483
|
+
const partnerOf = (boxId, side, index) => {
|
|
2484
|
+
const entry = facing[index];
|
|
2485
|
+
if (entry === void 0) return void 0;
|
|
2486
|
+
const edge = diagram.edges[index];
|
|
2487
|
+
const isExit = edge.from === boxId && entry.exit === side;
|
|
2488
|
+
const groupId = planned.groupOf.get(index);
|
|
2489
|
+
if (groupId !== void 0) {
|
|
2490
|
+
if (!isExit) return void 0;
|
|
2491
|
+
const group = planned.groups[groupId];
|
|
2492
|
+
if (isVerticalSide2(side) !== isVerticalSide2(group.side)) return void 0;
|
|
2493
|
+
return { coordinate: group.entryAlong };
|
|
2494
|
+
}
|
|
2495
|
+
const partnerId = isExit ? edge.to : edge.from;
|
|
2496
|
+
const partnerSide = isExit ? entry.entry : entry.exit;
|
|
2497
|
+
if (isVerticalSide2(side) !== isVerticalSide2(partnerSide)) return void 0;
|
|
2498
|
+
const coordinate = placedPort.get(slotKey(partnerId, partnerSide, index));
|
|
2499
|
+
if (coordinate === void 0) return void 0;
|
|
2500
|
+
const row = rowOf.get(partnerId + ":" + partnerSide);
|
|
2501
|
+
return { coordinate, ...row === void 0 ? {} : { row, at: row.position.get(index) } };
|
|
2502
|
+
};
|
|
2503
|
+
for (let pass = 0; pass < rows.length; pass += 1) {
|
|
2504
|
+
let refused = false;
|
|
2505
|
+
for (const row of rows) {
|
|
2506
|
+
if (!row.allowed || row.shift === 0) continue;
|
|
2507
|
+
const pinned = row.indexes.some((index, at) => {
|
|
2508
|
+
if (onLane(index, row.at[at])) return true;
|
|
2509
|
+
const partner = partnerOf(row.boxId, row.side, index);
|
|
2510
|
+
if (partner === void 0) return false;
|
|
2511
|
+
if (Math.abs(row.at[at] - partner.coordinate) > 0.5) return false;
|
|
2512
|
+
const partnerAt = partner.row === void 0 ? partner.coordinate : movedAt(partner.row, partner.at);
|
|
2513
|
+
return Math.abs(movedAt(row, at) - partnerAt) > 0.5;
|
|
2514
|
+
});
|
|
2515
|
+
if (!pinned) continue;
|
|
2516
|
+
row.allowed = false;
|
|
2517
|
+
refused = true;
|
|
2518
|
+
}
|
|
2519
|
+
if (!refused) break;
|
|
2520
|
+
}
|
|
2521
|
+
for (const row of rows) {
|
|
2522
|
+
if (!row.allowed || row.shift === 0) continue;
|
|
2523
|
+
row.indexes.forEach((index, at) => placedPort.set(slotKey(row.boxId, row.side, index), movedAt(row, at)));
|
|
2524
|
+
}
|
|
2525
|
+
const portOf = (boxId, side, edgeIndex) => {
|
|
2526
|
+
const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
|
|
2527
|
+
return pointOn(boxId, side, coordinate);
|
|
2528
|
+
};
|
|
2529
|
+
const anchorsFor = /* @__PURE__ */ new Map();
|
|
2530
|
+
for (const [index, entry] of facing.entries()) {
|
|
2531
|
+
if (entry === void 0) continue;
|
|
2532
|
+
const edge = diagram.edges[index];
|
|
2533
|
+
anchorsFor.set(index, {
|
|
2534
|
+
exit: portOf(edge.from, entry.exit, index),
|
|
2535
|
+
entry: portOf(edge.to, entry.entry, index),
|
|
2536
|
+
exitSide: entry.exit,
|
|
2537
|
+
entrySide: entry.entry
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
const trunks = planFor((index) => anchorsFor.get(index)?.exit);
|
|
2541
|
+
const sharedEntry = /* @__PURE__ */ new Map();
|
|
2542
|
+
for (const group of trunks.groups) sharedEntry.set(group.id, pointOn(group.target, group.side, group.entryAlong));
|
|
2543
|
+
const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
|
|
2544
|
+
const segmentsOfRoute = (shape) => {
|
|
2545
|
+
const points = pointsOfRoute(shape);
|
|
2546
|
+
const out = [];
|
|
2547
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
2548
|
+
const from = points[index];
|
|
2549
|
+
const to = points[index + 1];
|
|
2550
|
+
out.push({ x1: from.x, y1: from.y, x2: to.x, y2: to.y });
|
|
2551
|
+
}
|
|
2552
|
+
return out;
|
|
2553
|
+
};
|
|
2554
|
+
const paintPath = (edgeIndex, edge, points, colour) => {
|
|
2555
|
+
const shape = {
|
|
2556
|
+
kind: "path",
|
|
2557
|
+
points,
|
|
2558
|
+
label: edge.label,
|
|
2559
|
+
labelX: 0,
|
|
2560
|
+
labelY: 0,
|
|
2561
|
+
colour,
|
|
2562
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2563
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
2564
|
+
};
|
|
2565
|
+
shapes.push(shape);
|
|
2566
|
+
routes.push({ shape, edge: edgeIndex, fixed: false });
|
|
2567
|
+
};
|
|
2568
|
+
const paintRoute = (edgeIndex, edge, points, colour) => {
|
|
2569
|
+
if (points.length > 2) {
|
|
2570
|
+
paintPath(edgeIndex, edge, points, colour);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const exit = points[0];
|
|
2574
|
+
const entry = points[points.length - 1];
|
|
2575
|
+
const shape = {
|
|
2576
|
+
kind: "link",
|
|
2577
|
+
x1: exit.x,
|
|
2578
|
+
y1: exit.y,
|
|
2579
|
+
x2: entry.x,
|
|
2580
|
+
y2: entry.y,
|
|
2581
|
+
label: edge.label,
|
|
2582
|
+
labelX: 0,
|
|
2583
|
+
labelY: 0,
|
|
2584
|
+
colour,
|
|
2585
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2586
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
2587
|
+
};
|
|
2588
|
+
shapes.push(shape);
|
|
2589
|
+
routes.push({ shape, edge: edgeIndex, fixed: false });
|
|
2590
|
+
};
|
|
2591
|
+
const clearOfBoxes = (points, from, to) => boxesCrossedBy(points, from, to, boxList).length === 0;
|
|
2592
|
+
const runRoute = (ports, runY) => {
|
|
2593
|
+
const out = isVerticalSide2(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
|
|
2594
|
+
const into = isVerticalSide2(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
|
|
2595
|
+
const exitRun = out !== 0 && into === 0 && Math.sign(ports.entry.x - ports.exit.x) === Math.sign(out) ? ports.entry.x : ports.exit.x + out;
|
|
2596
|
+
const entryRun = into !== 0 && out === 0 && Math.sign(ports.exit.x - ports.entry.x) === Math.sign(into) ? ports.exit.x : ports.entry.x + into;
|
|
2597
|
+
const points = [ports.exit];
|
|
2598
|
+
if (out !== 0) points.push({ x: exitRun, y: ports.exit.y });
|
|
2599
|
+
points.push({ x: exitRun, y: runY }, { x: entryRun, y: runY });
|
|
2600
|
+
if (into !== 0) points.push({ x: entryRun, y: ports.entry.y });
|
|
2601
|
+
points.push(ports.entry);
|
|
2602
|
+
return simplify(points);
|
|
2603
|
+
};
|
|
2604
|
+
const runFits = (ports, runY) => {
|
|
2605
|
+
if (isVerticalSide2(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
|
|
2606
|
+
if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
|
|
2607
|
+
}
|
|
2608
|
+
if (isVerticalSide2(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
|
|
2609
|
+
if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
|
|
2610
|
+
}
|
|
2611
|
+
return true;
|
|
2612
|
+
};
|
|
2613
|
+
const junctions = [];
|
|
2614
|
+
for (const [edgeIndex, edge] of diagram.edges.entries()) {
|
|
2615
|
+
const source = endBox(edge.from);
|
|
2616
|
+
const target = endBox(edge.to);
|
|
400
2617
|
if (source === void 0 || target === void 0) continue;
|
|
401
|
-
const colour = roleById.get(edge.from);
|
|
402
|
-
const sameRow = Math.abs(target.y - source.y) < NODE_HEIGHT2 / 2;
|
|
2618
|
+
const colour = roleById.get(edge.from) ?? "muted";
|
|
403
2619
|
if (edge.from === edge.to) {
|
|
404
2620
|
const x = source.x + source.width;
|
|
405
2621
|
const y = source.y + source.height / 2;
|
|
406
|
-
|
|
2622
|
+
const shape = {
|
|
407
2623
|
kind: "path",
|
|
408
2624
|
points: [
|
|
409
2625
|
{ x, y: y - 9 },
|
|
@@ -412,57 +2628,116 @@ window.__ModuleLoader__.load({
|
|
|
412
2628
|
{ x, y: y + 9 }
|
|
413
2629
|
],
|
|
414
2630
|
label: edge.label,
|
|
415
|
-
|
|
2631
|
+
// The one label that is not on its own edge: a self loop's longest run is eighteen points, and its name
|
|
2632
|
+
// does not fit on that. It sits beside the loop, where it belongs to the edge and touches nothing.
|
|
2633
|
+
labelX: x + 20 + textWidth(edge.label) / 2,
|
|
416
2634
|
labelY: y,
|
|
417
|
-
colour
|
|
418
|
-
|
|
2635
|
+
colour,
|
|
2636
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2637
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
2638
|
+
};
|
|
2639
|
+
shapes.push(shape);
|
|
2640
|
+
routes.push({ shape, edge: edgeIndex, fixed: true });
|
|
419
2641
|
continue;
|
|
420
2642
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
2643
|
+
const anchors = anchorsFor.get(edgeIndex);
|
|
2644
|
+
const groupId = trunks.groupOf.get(edgeIndex);
|
|
2645
|
+
if (groupId !== void 0 && anchors !== void 0) {
|
|
2646
|
+
const group = trunks.groups[groupId];
|
|
2647
|
+
const entry = sharedEntry.get(groupId);
|
|
2648
|
+
const branch = isVerticalSide2(group.side) ? [anchors.exit, { x: anchors.exit.x, y: group.at }, { x: entry.x, y: group.at }, entry] : [anchors.exit, { x: group.at, y: anchors.exit.y }, { x: group.at, y: entry.y }, entry];
|
|
2649
|
+
const points = simplify(branch);
|
|
2650
|
+
if (clearOfBoxes(points, source, target)) {
|
|
2651
|
+
paintRoute(edgeIndex, edge, points, colour);
|
|
2652
|
+
const meet = isVerticalSide2(group.side) ? { x: anchors.exit.x, y: group.at } : { x: group.at, y: anchors.exit.y };
|
|
2653
|
+
junctions.push({ kind: "junction", x: meet.x, y: meet.y, colour, trunk: group.id });
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
428
2656
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
2657
|
+
const lane = placement.lanes.get(edgeIndex);
|
|
2658
|
+
if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
|
|
2659
|
+
const shift = laneShift.get(edgeIndex) ?? 0;
|
|
2660
|
+
const ordered = lane.reversed ? [...lane.points].reverse() : lane.points;
|
|
2661
|
+
const waypoints = shift === 0 ? ordered : ordered.map((point) => ({ x: point.x + shift, y: point.y }));
|
|
2662
|
+
const points = [anchors.exit];
|
|
2663
|
+
let previous = anchors.exit;
|
|
2664
|
+
for (const waypoint of waypoints) {
|
|
2665
|
+
if (Math.abs(previous.x - waypoint.x) > 0.5 && Math.abs(previous.y - waypoint.y) > 0.5) points.push({ x: previous.x, y: waypoint.y });
|
|
2666
|
+
points.push(waypoint);
|
|
2667
|
+
previous = waypoint;
|
|
2668
|
+
}
|
|
2669
|
+
if (Math.abs(previous.x - anchors.entry.x) > 0.5 && Math.abs(previous.y - anchors.entry.y) > 0.5) points.push({ x: anchors.entry.x, y: previous.y });
|
|
2670
|
+
points.push(anchors.entry);
|
|
2671
|
+
const simplified = simplify(points);
|
|
2672
|
+
if (clearOfBoxes(simplified, source, target)) {
|
|
2673
|
+
paintPath(edgeIndex, edge, simplified, colour);
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
446
2676
|
}
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
{ x: lane, y: startY },
|
|
457
|
-
{ x: lane, y: endY },
|
|
458
|
-
{ x: target.x + target.width, y: endY }
|
|
459
|
-
],
|
|
460
|
-
label: edge.label,
|
|
461
|
-
labelX: lane + 6,
|
|
462
|
-
labelY: (startY + endY) / 2,
|
|
463
|
-
colour
|
|
464
|
-
});
|
|
2677
|
+
const runY = placement.runs.get(edgeIndex);
|
|
2678
|
+
if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
|
|
2679
|
+
const points = runRoute(anchors, runY);
|
|
2680
|
+
if (clearOfBoxes(points, source, target)) {
|
|
2681
|
+
paintRoute(edgeIndex, edge, points, colour);
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
paintRoute(edgeIndex, edge, routeEdge(source, target, boxList, anchors).points, colour);
|
|
465
2686
|
}
|
|
2687
|
+
for (const dot of junctions) shapes.push(dot);
|
|
2688
|
+
const boxEntries = Array.from(boxes.entries());
|
|
2689
|
+
const routingFailures = [];
|
|
2690
|
+
for (const { shape, edge } of routes) {
|
|
2691
|
+
const diagramEdge = diagram.edges[edge];
|
|
2692
|
+
const source = endBox(diagramEdge.from);
|
|
2693
|
+
const target = endBox(diagramEdge.to);
|
|
2694
|
+
if (source === void 0 || target === void 0) continue;
|
|
2695
|
+
for (const segment of segmentsOfRoute(shape)) {
|
|
2696
|
+
for (const [id, box] of boxEntries) {
|
|
2697
|
+
if (box === source || box === target) continue;
|
|
2698
|
+
if (!crossesBox(segment.x1, segment.y1, segment.x2, segment.y2, box)) continue;
|
|
2699
|
+
routingFailures.push({
|
|
2700
|
+
edge,
|
|
2701
|
+
from: diagramEdge.from,
|
|
2702
|
+
to: diagramEdge.to,
|
|
2703
|
+
box: id,
|
|
2704
|
+
x1: segment.x1,
|
|
2705
|
+
y1: segment.y1,
|
|
2706
|
+
x2: segment.x2,
|
|
2707
|
+
y2: segment.y2
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
const routeSegments = routes.map(({ shape }) => segmentsOfRoute(shape));
|
|
2713
|
+
const placedLabels = [];
|
|
2714
|
+
routes.forEach(({ shape, fixed }, at) => {
|
|
2715
|
+
if (fixed) {
|
|
2716
|
+
if (shape.label !== "") {
|
|
2717
|
+
placedLabels.push({ x: shape.labelX, y: shape.labelY, width: textWidth(shape.label), height: LABEL_HEIGHT });
|
|
2718
|
+
}
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
if (shape.label === "") return;
|
|
2722
|
+
const placement2 = placeLabel(
|
|
2723
|
+
shape.label,
|
|
2724
|
+
pointsOfRoute(shape),
|
|
2725
|
+
boxList,
|
|
2726
|
+
placedLabels,
|
|
2727
|
+
routeSegments.filter((_, other) => other !== at)
|
|
2728
|
+
);
|
|
2729
|
+
shape.labelX = placement2.x;
|
|
2730
|
+
shape.labelY = placement2.y;
|
|
2731
|
+
shape.labelPlacement = {
|
|
2732
|
+
run: placement2.run,
|
|
2733
|
+
collisions: placement2.collisions,
|
|
2734
|
+
boxes: placement2.boxes,
|
|
2735
|
+
labels: placement2.labels,
|
|
2736
|
+
edges: placement2.edges
|
|
2737
|
+
};
|
|
2738
|
+
placedLabels.push({ x: placement2.x, y: placement2.y, width: placement2.width, height: placement2.height });
|
|
2739
|
+
});
|
|
2740
|
+
const labelCollisions = routes.reduce((total, { shape }) => total + ((shape.labelPlacement?.collisions ?? 0) > 0 ? 1 : 0), 0);
|
|
466
2741
|
shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
|
|
467
2742
|
const contentRight = Math.max(
|
|
468
2743
|
MARGIN,
|
|
@@ -471,7 +2746,7 @@ window.__ModuleLoader__.load({
|
|
|
471
2746
|
);
|
|
472
2747
|
const captionLines = wrap(diagram.caption, CAPTION_CHARS);
|
|
473
2748
|
const captionWidth = Math.max(...captionLines.map((line) => line.length * CAPTION_CHAR_WIDTH)) + MARGIN * 2;
|
|
474
|
-
const width = Math.max(contentRight + MARGIN
|
|
2749
|
+
const width = Math.max(contentRight + MARGIN, captionWidth, 240);
|
|
475
2750
|
const contentBottom = Math.max(
|
|
476
2751
|
topOffset,
|
|
477
2752
|
...Array.from(boxes.values(), (box) => box.y + box.height),
|
|
@@ -480,7 +2755,13 @@ window.__ModuleLoader__.load({
|
|
|
480
2755
|
const captionTop = contentBottom + CAPTION_GAP;
|
|
481
2756
|
shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
|
|
482
2757
|
const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
|
|
483
|
-
|
|
2758
|
+
const extent = drawnExtent(shapes);
|
|
2759
|
+
const canvasWidth = Math.max(width, extent.width + MARGIN);
|
|
2760
|
+
const canvasHeight = Math.max(height, extent.height + MARGIN);
|
|
2761
|
+
return {
|
|
2762
|
+
canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes, routingFailures, labelCollisions },
|
|
2763
|
+
colours
|
|
2764
|
+
};
|
|
484
2765
|
}
|
|
485
2766
|
|
|
486
2767
|
// src/render/from-mermaid.ts
|
|
@@ -505,10 +2786,15 @@ window.__ModuleLoader__.load({
|
|
|
505
2786
|
"requirementdiagram",
|
|
506
2787
|
"sankey-beta"
|
|
507
2788
|
]);
|
|
508
|
-
function ensure(builder, id, label) {
|
|
2789
|
+
function ensure(builder, id, label, shape) {
|
|
509
2790
|
const existing = builder.nodes.get(id);
|
|
510
2791
|
if (existing === void 0) {
|
|
511
|
-
builder.nodes.set(id, {
|
|
2792
|
+
builder.nodes.set(id, {
|
|
2793
|
+
id,
|
|
2794
|
+
role: "normal",
|
|
2795
|
+
...label === void 0 ? {} : { label },
|
|
2796
|
+
...shape === void 0 ? {} : { shape }
|
|
2797
|
+
});
|
|
512
2798
|
const innermost = builder.open[builder.open.length - 1];
|
|
513
2799
|
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
514
2800
|
return;
|
|
@@ -520,7 +2806,12 @@ window.__ModuleLoader__.load({
|
|
|
520
2806
|
if (match === null) return void 0;
|
|
521
2807
|
const id = match[1];
|
|
522
2808
|
const label = [match[2], match[3], match[4], match[5], match[6], match[7]].find((value) => value !== void 0 && value.trim() !== "");
|
|
523
|
-
|
|
2809
|
+
const shape = match[2] !== void 0 ? "cylinder" : match[3] !== void 0 ? "subroutine" : match[4] !== void 0 ? "rect" : match[5] !== void 0 ? "stadium" : match[6] !== void 0 ? "diamond" : match[7] !== void 0 ? "hexagon" : void 0;
|
|
2810
|
+
return {
|
|
2811
|
+
id,
|
|
2812
|
+
...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") },
|
|
2813
|
+
...shape === void 0 ? {} : { shape }
|
|
2814
|
+
};
|
|
524
2815
|
}
|
|
525
2816
|
function fromMermaid(source, title = "mermaid") {
|
|
526
2817
|
const lines = source.split(/\r?\n/);
|
|
@@ -537,13 +2828,13 @@ window.__ModuleLoader__.load({
|
|
|
537
2828
|
if (line === "") return;
|
|
538
2829
|
const where = "line " + String(index + 1) + ": ";
|
|
539
2830
|
const lower = line.toLowerCase();
|
|
540
|
-
const keyword = lower.split(/[\s{}]/)[0]
|
|
2831
|
+
const keyword = lower.split(/[\s{}]/)[0];
|
|
541
2832
|
if (UNSUPPORTED.has(keyword)) {
|
|
542
2833
|
refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
|
|
543
2834
|
return;
|
|
544
2835
|
}
|
|
545
2836
|
if (DIRECTIVE.test(line)) {
|
|
546
|
-
refused.push(where + "`" +
|
|
2837
|
+
refused.push(where + "`" + line.split(/\s/)[0] + "` is a directive this renderer ignores");
|
|
547
2838
|
return;
|
|
548
2839
|
}
|
|
549
2840
|
if (/^(graph|flowchart)\b/.test(lower)) {
|
|
@@ -615,8 +2906,8 @@ window.__ModuleLoader__.load({
|
|
|
615
2906
|
refused.push(where + "`" + line + "` does not name two nodes");
|
|
616
2907
|
return;
|
|
617
2908
|
}
|
|
618
|
-
ensure(builder, left.id, left.label);
|
|
619
|
-
ensure(builder, right.id, right.label);
|
|
2909
|
+
ensure(builder, left.id, left.label, left.shape);
|
|
2910
|
+
ensure(builder, right.id, right.label, right.shape);
|
|
620
2911
|
push({ from: left.id, to: right.id, label: labelled[2].trim() });
|
|
621
2912
|
return;
|
|
622
2913
|
}
|
|
@@ -628,8 +2919,8 @@ window.__ModuleLoader__.load({
|
|
|
628
2919
|
refused.push(where + "`" + line + "` does not name two nodes");
|
|
629
2920
|
return;
|
|
630
2921
|
}
|
|
631
|
-
ensure(builder, left.id, left.label);
|
|
632
|
-
ensure(builder, right.id, right.label);
|
|
2922
|
+
ensure(builder, left.id, left.label, left.shape);
|
|
2923
|
+
ensure(builder, right.id, right.label, right.shape);
|
|
633
2924
|
push({
|
|
634
2925
|
from: left.id,
|
|
635
2926
|
to: right.id,
|
|
@@ -640,7 +2931,7 @@ window.__ModuleLoader__.load({
|
|
|
640
2931
|
}
|
|
641
2932
|
const single = nodeFrom(line);
|
|
642
2933
|
if (single !== void 0) {
|
|
643
|
-
ensure(builder, single.id, single.label);
|
|
2934
|
+
ensure(builder, single.id, single.label, single.shape);
|
|
644
2935
|
return;
|
|
645
2936
|
}
|
|
646
2937
|
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
@@ -664,17 +2955,15 @@ window.__ModuleLoader__.load({
|
|
|
664
2955
|
}
|
|
665
2956
|
|
|
666
2957
|
// src/render/collapse.ts
|
|
667
|
-
function subtreeSize(
|
|
2958
|
+
function subtreeSize(groups, groupId, seen) {
|
|
668
2959
|
if (seen.has(groupId)) return 0;
|
|
669
2960
|
seen.add(groupId);
|
|
670
|
-
const groups = diagram.groups ?? [];
|
|
671
2961
|
const group = groups.find((entry) => entry.id === groupId);
|
|
672
|
-
if (group === void 0) return 0;
|
|
673
2962
|
const children = groups.filter((entry) => entry.parent === groupId);
|
|
674
|
-
return group.members.length + children.reduce((total, child) => total + subtreeSize(
|
|
2963
|
+
return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0);
|
|
675
2964
|
}
|
|
676
|
-
function ownerOf(
|
|
677
|
-
return
|
|
2965
|
+
function ownerOf(groups, nodeId) {
|
|
2966
|
+
return groups.find((group) => group.members.includes(nodeId))?.id;
|
|
678
2967
|
}
|
|
679
2968
|
function collapse(diagram, folded) {
|
|
680
2969
|
const asked = new Set(folded);
|
|
@@ -684,7 +2973,7 @@ window.__ModuleLoader__.load({
|
|
|
684
2973
|
const targets = /* @__PURE__ */ new Set();
|
|
685
2974
|
for (const id of asked) {
|
|
686
2975
|
const group = byId.get(id);
|
|
687
|
-
if (group !== void 0 && subtreeSize(
|
|
2976
|
+
if (group !== void 0 && subtreeSize(groups, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
|
|
688
2977
|
}
|
|
689
2978
|
if (targets.size === 0) return diagram;
|
|
690
2979
|
for (const id of [...targets]) {
|
|
@@ -701,16 +2990,13 @@ window.__ModuleLoader__.load({
|
|
|
701
2990
|
}
|
|
702
2991
|
const removedNodes = /* @__PURE__ */ new Set();
|
|
703
2992
|
const removedGroups = /* @__PURE__ */ new Set();
|
|
704
|
-
const descend = (groupId
|
|
705
|
-
if (seen.has(groupId)) return;
|
|
706
|
-
seen.add(groupId);
|
|
2993
|
+
const descend = (groupId) => {
|
|
707
2994
|
removedGroups.add(groupId);
|
|
708
2995
|
const group = byId.get(groupId);
|
|
709
|
-
if (group === void 0) return;
|
|
710
2996
|
for (const member of group.members) removedNodes.add(member);
|
|
711
|
-
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id
|
|
2997
|
+
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id);
|
|
712
2998
|
};
|
|
713
|
-
for (const id of targets) descend(id
|
|
2999
|
+
for (const id of targets) descend(id);
|
|
714
3000
|
const foldOf = (id) => {
|
|
715
3001
|
for (const target of targets) {
|
|
716
3002
|
const inSubtree = (groupId, seen) => {
|
|
@@ -720,7 +3006,7 @@ window.__ModuleLoader__.load({
|
|
|
720
3006
|
return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
|
|
721
3007
|
};
|
|
722
3008
|
const group = byId.get(target);
|
|
723
|
-
if (group.members.includes(id) || inSubtree(ownerOf(
|
|
3009
|
+
if (group.members.includes(id) || inSubtree(ownerOf(groups, id), /* @__PURE__ */ new Set())) return target;
|
|
724
3010
|
}
|
|
725
3011
|
return void 0;
|
|
726
3012
|
};
|
|
@@ -735,12 +3021,12 @@ window.__ModuleLoader__.load({
|
|
|
735
3021
|
if (fold === void 0 || emitted.has(fold)) continue;
|
|
736
3022
|
emitted.add(fold);
|
|
737
3023
|
const group = byId.get(fold);
|
|
738
|
-
nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(
|
|
3024
|
+
nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(groups, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
739
3025
|
}
|
|
740
3026
|
for (const id of targets) {
|
|
741
3027
|
if (emitted.has(id)) continue;
|
|
742
3028
|
const group = byId.get(id);
|
|
743
|
-
nodes.push({ id, label: group.label + " (" + String(subtreeSize(
|
|
3029
|
+
nodes.push({ id, label: group.label + " (" + String(subtreeSize(groups, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
744
3030
|
}
|
|
745
3031
|
const edges = [];
|
|
746
3032
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -757,7 +3043,7 @@ window.__ModuleLoader__.load({
|
|
|
757
3043
|
const adopted = /* @__PURE__ */ new Map();
|
|
758
3044
|
for (const id of targets) {
|
|
759
3045
|
const parent = byId.get(id)?.parent;
|
|
760
|
-
if (parent === void 0
|
|
3046
|
+
if (parent === void 0) continue;
|
|
761
3047
|
adopted.set(parent, [...adopted.get(parent) ?? [], id]);
|
|
762
3048
|
}
|
|
763
3049
|
const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
|
|
@@ -776,38 +3062,139 @@ window.__ModuleLoader__.load({
|
|
|
776
3062
|
}
|
|
777
3063
|
|
|
778
3064
|
// src/render/from-plantuml.ts
|
|
779
|
-
var
|
|
780
|
-
|
|
3065
|
+
var SHAPES = {
|
|
3066
|
+
component: "rect",
|
|
3067
|
+
node: "rect",
|
|
3068
|
+
rectangle: "rect",
|
|
3069
|
+
artifact: "rect",
|
|
3070
|
+
state: "rect",
|
|
3071
|
+
database: "cylinder",
|
|
3072
|
+
// PlantUML draws an entity as a cylinder in a component diagram; `entity` in an ER diagram is a different
|
|
3073
|
+
// symbol, and that diagram family is not one this renderer accepts.
|
|
3074
|
+
entity: "cylinder",
|
|
3075
|
+
// A use case is an ellipse. The `circle` symbol is written as an ellipse whose radii follow the box, so it
|
|
3076
|
+
// reads as the familiar use-case oval; `stadium` is the pill a terminal state is drawn as.
|
|
3077
|
+
usecase: "circle"
|
|
3078
|
+
};
|
|
3079
|
+
var BOUNDARY = /* @__PURE__ */ new Set(["package", "folder", "node", "component", "rectangle", "artifact"]);
|
|
3080
|
+
var NOT_DRAWN = {
|
|
3081
|
+
actor: "an actor is a stick figure, not one of the shapes this renderer draws",
|
|
3082
|
+
participant: "a participant is a lifeline, not one of the shapes this renderer draws",
|
|
3083
|
+
class: "a class box has compartments this renderer does not draw",
|
|
3084
|
+
object: "an object box has compartments this renderer does not draw",
|
|
3085
|
+
interface: "an interface is a circle or a boxed stereotype, neither of which this renderer draws",
|
|
3086
|
+
enum: "an enum box has compartments this renderer does not draw",
|
|
3087
|
+
annotation: "an annotation is not one of the shapes this renderer draws",
|
|
3088
|
+
cloud: "a cloud outline is not one of the shapes this renderer draws",
|
|
3089
|
+
frame: "a frame tab is not one of the shapes this renderer draws",
|
|
3090
|
+
card: "a card is not one of the shapes this renderer draws",
|
|
3091
|
+
file: "a file outline is not one of the shapes this renderer draws",
|
|
3092
|
+
agent: "an agent is not one of the shapes this renderer draws",
|
|
3093
|
+
boundary: "the boundary symbol is not one of the shapes this renderer draws",
|
|
3094
|
+
control: "the control symbol is not one of the shapes this renderer draws",
|
|
3095
|
+
collections: "a collections symbol is not one of the shapes this renderer draws",
|
|
3096
|
+
queue: "a queue is not one of the shapes this renderer draws",
|
|
3097
|
+
stack: "a stack is not one of the shapes this renderer draws",
|
|
3098
|
+
storage: "a storage symbol is not one of the shapes this renderer draws",
|
|
3099
|
+
note: "a note is not one of the shapes this renderer draws",
|
|
3100
|
+
legend: "a legend is not one of the shapes this renderer draws"
|
|
3101
|
+
};
|
|
3102
|
+
var FRAGMENT = /* @__PURE__ */ new Set(["sequence", "alt", "opt", "loop", "par", "group", "ref", "break", "critical", "activate", "deactivate", "destroy", "create", "newpage", "autonumber"]);
|
|
3103
|
+
var ARROW = /^(.+?)\s*(<\|?|[*o])?(-{2,}|\.{2,})(\|?>|[*o])?\s*(.+?)(?:\s*:\s*(.+))?$/;
|
|
3104
|
+
var SHAPE_KEYWORDS = Object.keys(SHAPES);
|
|
3105
|
+
var DECLARED = new RegExp("^(" + SHAPE_KEYWORDS.join("|") + ")\\s+(.+)$");
|
|
781
3106
|
var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
782
3107
|
var BRACKET_ALIAS = /^\[(.+?)\]\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
3108
|
+
var PLAIN_ALIAS = /^([A-Za-z_][\w.-]*)\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
783
3109
|
var BRACKET = /^\[(.+?)\]$/;
|
|
784
|
-
var
|
|
785
|
-
|
|
786
|
-
function ensure2(builder, id, label) {
|
|
3110
|
+
var IGNORED = /^(skinparam|!theme|!define|!include|!pragma|hide|show|scale|left to right direction|top to bottom direction|together|allowmixing)\b/;
|
|
3111
|
+
function ensure2(builder, id, label, shape, implicit = false) {
|
|
787
3112
|
const existing = builder.nodes.get(id);
|
|
788
3113
|
if (existing === void 0) {
|
|
789
|
-
builder.nodes.set(id, {
|
|
3114
|
+
builder.nodes.set(id, {
|
|
3115
|
+
id,
|
|
3116
|
+
role: "normal",
|
|
3117
|
+
...label === void 0 ? {} : { label },
|
|
3118
|
+
...shape === void 0 ? {} : { shape }
|
|
3119
|
+
});
|
|
3120
|
+
if (implicit) builder.implicit.add(id);
|
|
790
3121
|
const innermost = builder.open[builder.open.length - 1];
|
|
791
3122
|
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
792
3123
|
return;
|
|
793
3124
|
}
|
|
3125
|
+
if (!implicit) builder.implicit.delete(id);
|
|
794
3126
|
if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
|
|
3127
|
+
if (shape !== void 0 && existing.shape === void 0) existing.shape = shape;
|
|
795
3128
|
}
|
|
796
3129
|
function reference(token) {
|
|
797
3130
|
const text2 = token.trim();
|
|
798
3131
|
const bracket = BRACKET.exec(text2);
|
|
799
3132
|
if (bracket !== null) {
|
|
800
3133
|
const label = bracket[1].trim();
|
|
801
|
-
return { id: label, label };
|
|
3134
|
+
return { id: label, label, shape: "rect" };
|
|
802
3135
|
}
|
|
803
3136
|
const quoted = /^["'](.+?)["']$/.exec(text2);
|
|
804
3137
|
if (quoted !== null) return { id: quoted[1], label: quoted[1] };
|
|
805
3138
|
return { id: text2 };
|
|
806
3139
|
}
|
|
3140
|
+
function keywordOf(line) {
|
|
3141
|
+
return line.split(/[\s{]/)[0].toLowerCase();
|
|
3142
|
+
}
|
|
3143
|
+
function boundaryOf(declaration, keyword, ordinal) {
|
|
3144
|
+
const rest = declaration.slice(keyword.length).trim();
|
|
3145
|
+
const alias = QUOTED_ALIAS.exec(rest) ?? BRACKET_ALIAS.exec(rest) ?? PLAIN_ALIAS.exec(rest);
|
|
3146
|
+
if (alias !== null) return { id: alias[2], label: alias[1] };
|
|
3147
|
+
const label = rest.replace(/^["']|["']$/g, "");
|
|
3148
|
+
return { id: "group-" + String(ordinal), label: label === "" ? keyword : label };
|
|
3149
|
+
}
|
|
3150
|
+
function readArrow(line) {
|
|
3151
|
+
const match = ARROW.exec(line);
|
|
3152
|
+
if (match === null) return void 0;
|
|
3153
|
+
const left = reference(match[1]);
|
|
3154
|
+
const right = reference(match[5]);
|
|
3155
|
+
const leftMark = match[2];
|
|
3156
|
+
const rightMark = match[4];
|
|
3157
|
+
const dotted = match[3].startsWith(".");
|
|
3158
|
+
const label = (match[6] ?? "").trim();
|
|
3159
|
+
if (leftMark !== void 0 && rightMark !== void 0) {
|
|
3160
|
+
return "an edge marked at both ends is not one this renderer draws";
|
|
3161
|
+
}
|
|
3162
|
+
if (leftMark === "*" || leftMark === "o" || rightMark === "*" || rightMark === "o") {
|
|
3163
|
+
if (dotted) return "a dotted composition or aggregation is not one this renderer draws";
|
|
3164
|
+
const atRight = rightMark === "*" || rightMark === "o";
|
|
3165
|
+
const mark = atRight ? rightMark : leftMark;
|
|
3166
|
+
return {
|
|
3167
|
+
from: atRight ? right : left,
|
|
3168
|
+
to: atRight ? left : right,
|
|
3169
|
+
label,
|
|
3170
|
+
dashed: false,
|
|
3171
|
+
arrow: mark === "*" ? "composition" : "aggregation"
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
if (leftMark !== void 0) {
|
|
3175
|
+
return {
|
|
3176
|
+
from: right,
|
|
3177
|
+
to: left,
|
|
3178
|
+
label,
|
|
3179
|
+
dashed: dotted,
|
|
3180
|
+
arrow: leftMark === "<|" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
if (rightMark !== void 0) {
|
|
3184
|
+
return {
|
|
3185
|
+
from: left,
|
|
3186
|
+
to: right,
|
|
3187
|
+
label,
|
|
3188
|
+
dashed: dotted,
|
|
3189
|
+
arrow: rightMark === "|>" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
return "a line with no arrow end is not one this renderer draws; name the end it points at";
|
|
3193
|
+
}
|
|
807
3194
|
function fromPlantUml(source, title = "plantuml") {
|
|
808
3195
|
const lines = source.split(/\r?\n/);
|
|
809
3196
|
const refused = [];
|
|
810
|
-
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
|
|
3197
|
+
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], edgeLines: [], edgeScopes: [], groups: [], open: [], implicit: /* @__PURE__ */ new Set() };
|
|
811
3198
|
let startId;
|
|
812
3199
|
let seenAny = false;
|
|
813
3200
|
let groupCount = 0;
|
|
@@ -819,8 +3206,14 @@ window.__ModuleLoader__.load({
|
|
|
819
3206
|
seenAny = true;
|
|
820
3207
|
if (/^title\b/.test(line)) return;
|
|
821
3208
|
if (IGNORED.test(line)) return;
|
|
822
|
-
|
|
823
|
-
|
|
3209
|
+
const keyword = keywordOf(line);
|
|
3210
|
+
const notDrawn = NOT_DRAWN[keyword];
|
|
3211
|
+
if (notDrawn !== void 0) {
|
|
3212
|
+
refused.push(where + "`" + keyword + "`: " + notDrawn);
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
if (FRAGMENT.has(keyword)) {
|
|
3216
|
+
refused.push(where + "`" + keyword + "`: a sequence statement is not part of a drawing this renderer makes");
|
|
824
3217
|
return;
|
|
825
3218
|
}
|
|
826
3219
|
if (line === "}") {
|
|
@@ -834,56 +3227,176 @@ window.__ModuleLoader__.load({
|
|
|
834
3227
|
}
|
|
835
3228
|
if (line.endsWith("{")) {
|
|
836
3229
|
const declaration = line.slice(0, -1).trim();
|
|
837
|
-
const
|
|
838
|
-
|
|
3230
|
+
const opener = keywordOf(declaration);
|
|
3231
|
+
if (!BOUNDARY.has(opener)) {
|
|
3232
|
+
const reason = SHAPES[opener] === void 0 ? "`" + declaration + "` is not a boundary this renderer draws" : "a `" + opener + "` block is not a boundary this renderer draws";
|
|
3233
|
+
refused.push(where + reason);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
839
3236
|
groupCount += 1;
|
|
840
|
-
|
|
3237
|
+
const boundary = boundaryOf(declaration, opener, groupCount);
|
|
3238
|
+
builder.open.push({ id: boundary.id, label: boundary.label, members: [] });
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
if (line === "[*] --> [*]") return;
|
|
3242
|
+
const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
|
|
3243
|
+
if (startMarker !== null) {
|
|
3244
|
+
const target = reference(startMarker[1]);
|
|
3245
|
+
ensure2(builder, target.id, target.label, target.shape);
|
|
3246
|
+
if (startId === void 0) startId = target.id;
|
|
841
3247
|
return;
|
|
842
3248
|
}
|
|
843
3249
|
const declared = DECLARED.exec(line);
|
|
844
3250
|
if (declared !== null) {
|
|
3251
|
+
const shape = SHAPES[declared[1]];
|
|
845
3252
|
const rest = declared[2].trim();
|
|
846
3253
|
const quoted = QUOTED_ALIAS.exec(rest);
|
|
847
3254
|
if (quoted !== null) {
|
|
848
|
-
ensure2(builder, quoted[2], quoted[1]);
|
|
3255
|
+
ensure2(builder, quoted[2], quoted[1], shape);
|
|
849
3256
|
return;
|
|
850
3257
|
}
|
|
851
3258
|
const bracketed = BRACKET_ALIAS.exec(rest);
|
|
852
3259
|
if (bracketed !== null) {
|
|
853
|
-
ensure2(builder, bracketed[2], bracketed[1]);
|
|
3260
|
+
ensure2(builder, bracketed[2], bracketed[1], shape);
|
|
854
3261
|
return;
|
|
855
3262
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (startMarker !== null) {
|
|
864
|
-
const target = reference(startMarker[1]);
|
|
865
|
-
ensure2(builder, target.id, target.label);
|
|
866
|
-
if (startId === void 0) startId = target.id;
|
|
3263
|
+
const plain = PLAIN_ALIAS.exec(rest);
|
|
3264
|
+
if (plain !== null) {
|
|
3265
|
+
ensure2(builder, plain[2], plain[1], shape);
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const written = reference(rest);
|
|
3269
|
+
ensure2(builder, written.id, written.label, shape);
|
|
867
3270
|
return;
|
|
868
3271
|
}
|
|
869
|
-
const arrow =
|
|
870
|
-
if (arrow !==
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
ensure2(builder, from.id, from.label);
|
|
876
|
-
ensure2(builder, to.id, to.label);
|
|
877
|
-
builder.edges.push({
|
|
3272
|
+
const arrow = readArrow(line);
|
|
3273
|
+
if (arrow !== void 0) {
|
|
3274
|
+
if (typeof arrow === "string") {
|
|
3275
|
+
refused.push(where + arrow);
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
ensure2(builder, arrow.from.id, arrow.from.label, arrow.from.shape, true);
|
|
3279
|
+
ensure2(builder, arrow.to.id, arrow.to.label, arrow.to.shape, true);
|
|
3280
|
+
builder.edges.push({
|
|
3281
|
+
from: arrow.from.id,
|
|
3282
|
+
to: arrow.to.id,
|
|
3283
|
+
label: arrow.label,
|
|
3284
|
+
...arrow.dashed ? { dashed: true } : {},
|
|
3285
|
+
arrow: arrow.arrow
|
|
3286
|
+
});
|
|
3287
|
+
builder.edgeLines.push(index + 1);
|
|
3288
|
+
builder.edgeScopes.push(builder.open[builder.open.length - 1]?.id);
|
|
878
3289
|
return;
|
|
879
3290
|
}
|
|
880
3291
|
const single = BRACKET.exec(line);
|
|
881
3292
|
if (single !== null) {
|
|
882
|
-
ensure2(builder, single[1].trim(), single[1].trim());
|
|
3293
|
+
ensure2(builder, single[1].trim(), single[1].trim(), "rect");
|
|
883
3294
|
return;
|
|
884
3295
|
}
|
|
885
3296
|
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
886
3297
|
});
|
|
3298
|
+
const boundaryNames = /* @__PURE__ */ new Map();
|
|
3299
|
+
const disputedNames = /* @__PURE__ */ new Set();
|
|
3300
|
+
for (const group of builder.groups) {
|
|
3301
|
+
for (const name of [group.id, group.label]) {
|
|
3302
|
+
const existing = boundaryNames.get(name);
|
|
3303
|
+
if (existing !== void 0 && existing !== group.id) disputedNames.add(name);
|
|
3304
|
+
else boundaryNames.set(name, group.id);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
for (const name of disputedNames) boundaryNames.delete(name);
|
|
3308
|
+
const boundaryAt = /* @__PURE__ */ new Map();
|
|
3309
|
+
const ambiguousAt = /* @__PURE__ */ new Set();
|
|
3310
|
+
for (const id of builder.implicit) {
|
|
3311
|
+
if (disputedNames.has(id)) {
|
|
3312
|
+
ambiguousAt.add(id);
|
|
3313
|
+
builder.nodes.delete(id);
|
|
3314
|
+
} else {
|
|
3315
|
+
const groupId = boundaryNames.get(id);
|
|
3316
|
+
if (groupId === void 0) continue;
|
|
3317
|
+
boundaryAt.set(id, groupId);
|
|
3318
|
+
builder.nodes.delete(id);
|
|
3319
|
+
}
|
|
3320
|
+
for (const group of builder.groups) {
|
|
3321
|
+
const at = group.members.indexOf(id);
|
|
3322
|
+
if (at >= 0) group.members.splice(at, 1);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
const groupById = new Map(builder.groups.map((group) => [group.id, group]));
|
|
3326
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3327
|
+
for (const group of builder.groups) {
|
|
3328
|
+
if (group.parent === void 0) continue;
|
|
3329
|
+
childrenOf.set(group.parent, [...childrenOf.get(group.parent) ?? [], group.id]);
|
|
3330
|
+
}
|
|
3331
|
+
const drawable = /* @__PURE__ */ new Map();
|
|
3332
|
+
const hasContents = (id) => {
|
|
3333
|
+
const known = drawable.get(id);
|
|
3334
|
+
if (known !== void 0) return known;
|
|
3335
|
+
const group = groupById.get(id);
|
|
3336
|
+
let found = false;
|
|
3337
|
+
for (const member of group.members) {
|
|
3338
|
+
if (builder.nodes.has(member)) {
|
|
3339
|
+
found = true;
|
|
3340
|
+
break;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
if (!found) {
|
|
3344
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
3345
|
+
if (hasContents(child)) {
|
|
3346
|
+
found = true;
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
drawable.set(id, found);
|
|
3352
|
+
return found;
|
|
3353
|
+
};
|
|
3354
|
+
const boundaryLabel = (id) => groupById.get(id).label;
|
|
3355
|
+
const within = (scope, boundary) => {
|
|
3356
|
+
let cursor = scope;
|
|
3357
|
+
while (cursor !== void 0) {
|
|
3358
|
+
if (cursor === boundary) return true;
|
|
3359
|
+
cursor = groupById.get(cursor).parent;
|
|
3360
|
+
}
|
|
3361
|
+
return false;
|
|
3362
|
+
};
|
|
3363
|
+
const keptEdges = [];
|
|
3364
|
+
builder.edges.forEach((edge, index) => {
|
|
3365
|
+
const where = "line " + String(builder.edgeLines[index]) + ": ";
|
|
3366
|
+
const fromBoundary = boundaryAt.get(edge.from);
|
|
3367
|
+
const toBoundary = boundaryAt.get(edge.to);
|
|
3368
|
+
if (ambiguousAt.has(edge.from) || ambiguousAt.has(edge.to)) {
|
|
3369
|
+
const name = ambiguousAt.has(edge.from) ? edge.from : edge.to;
|
|
3370
|
+
refused.push(where + "`" + name + "` is the name of more than one block; an edge cannot say which boundary it means");
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (fromBoundary !== void 0 && toBoundary !== void 0) {
|
|
3374
|
+
refused.push(where + "`" + boundaryLabel(fromBoundary) + "` and `" + boundaryLabel(toBoundary) + "` are boundaries, not elements; an edge between two boundaries is not one this renderer draws");
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const boundary = fromBoundary ?? toBoundary;
|
|
3378
|
+
if (boundary !== void 0) {
|
|
3379
|
+
const group = groupById.get(boundary);
|
|
3380
|
+
if (group.parent !== void 0) {
|
|
3381
|
+
refused.push(where + "`" + group.label + "` is a block inside `" + boundaryLabel(group.parent) + "`, not an element; an edge to a nested boundary would have to enter another boundary");
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
if (within(builder.edgeScopes[index], boundary)) {
|
|
3385
|
+
refused.push(where + "`" + group.label + "` is the block this edge is written inside, not an element; an edge cannot leave a boundary and arrive at the same boundary");
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
if (!hasContents(boundary)) {
|
|
3389
|
+
refused.push(where + "`" + group.label + "` is a block with nothing drawn inside it, so there is no rectangle to attach the edge to");
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
keptEdges.push({
|
|
3394
|
+
...edge,
|
|
3395
|
+
...fromBoundary === void 0 ? {} : { from: fromBoundary },
|
|
3396
|
+
...toBoundary === void 0 ? {} : { to: toBoundary }
|
|
3397
|
+
});
|
|
3398
|
+
});
|
|
3399
|
+
builder.edges = keptEdges;
|
|
887
3400
|
if (!seenAny || builder.nodes.size === 0) {
|
|
888
3401
|
return { diagram: emptyDiagram(title, "no nodes found"), refused };
|
|
889
3402
|
}
|
|
@@ -900,6 +3413,13 @@ window.__ModuleLoader__.load({
|
|
|
900
3413
|
}
|
|
901
3414
|
|
|
902
3415
|
// src/render/svg.ts
|
|
3416
|
+
function boxText(shape, stroke) {
|
|
3417
|
+
const lines = shape.lines === void 0 || shape.lines.length === 0 ? [shape.label] : shape.lines;
|
|
3418
|
+
const lineHeight = 15;
|
|
3419
|
+
const first = shape.y + shape.height / 2 - (lines.length - 1) * lineHeight / 2 + 4;
|
|
3420
|
+
const spans = lines.map((line, index) => '<tspan x="' + n(shape.x + shape.width / 2) + '" y="' + n(first + index * lineHeight) + '">' + escape(line) + "</tspan>").join("");
|
|
3421
|
+
return '<text font-size="11" text-anchor="middle" fill="' + stroke + '">' + spans + "</text>";
|
|
3422
|
+
}
|
|
903
3423
|
var n = (value) => {
|
|
904
3424
|
const rounded = Math.round(value * 10) / 10;
|
|
905
3425
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
@@ -914,45 +3434,130 @@ window.__ModuleLoader__.load({
|
|
|
914
3434
|
box: (shape, colours) => {
|
|
915
3435
|
const stroke = colours[shape.colour];
|
|
916
3436
|
const width = shape.initial ? 2.5 : 1;
|
|
917
|
-
|
|
3437
|
+
const tail = '" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" />';
|
|
3438
|
+
const x = shape.x;
|
|
3439
|
+
const y = shape.y;
|
|
3440
|
+
const w = shape.width;
|
|
3441
|
+
const h = shape.height;
|
|
3442
|
+
const at = (point) => n(point.x) + " " + n(point.y);
|
|
3443
|
+
const points = (list) => list.map((point) => n(point.x) + "," + n(point.y)).join(",");
|
|
3444
|
+
const curve = (segment) => " C" + at(segment.c1) + " " + at(segment.c2) + " " + at(segment.to);
|
|
3445
|
+
switch (shape.shape) {
|
|
3446
|
+
// A decision: the notation wrote `{…}`, and a rounded rectangle would say "step".
|
|
3447
|
+
case "diamond":
|
|
3448
|
+
return '<polygon points="' + points(diamondPoints(shape)) + tail + boxText(shape, stroke);
|
|
3449
|
+
case "hexagon":
|
|
3450
|
+
return '<polygon points="' + points(hexagonPoints(shape)) + tail + boxText(shape, stroke);
|
|
3451
|
+
// A datastore: the cylinder UML draws for a stored collection.
|
|
3452
|
+
case "cylinder": {
|
|
3453
|
+
const caps = cylinderCaps(shape);
|
|
3454
|
+
const path = "M" + at(caps.top.from) + curve(caps.top) + " L" + at(caps.bottom.from) + curve(caps.bottom) + " Z";
|
|
3455
|
+
return '<path d="' + path + tail + boxText(shape, stroke);
|
|
3456
|
+
}
|
|
3457
|
+
// A start or end state: the stadium a terminal node is drawn as.
|
|
3458
|
+
case "stadium":
|
|
3459
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="' + n(stadiumRadius(shape)) + tail + boxText(shape, stroke);
|
|
3460
|
+
case "circle":
|
|
3461
|
+
return '<ellipse cx="' + n(x + w / 2) + '" cy="' + n(y + h / 2) + '" rx="' + n(w / 2) + '" ry="' + n(h / 2) + tail + boxText(shape, stroke);
|
|
3462
|
+
// A subroutine carries its double side lines.
|
|
3463
|
+
case "subroutine":
|
|
3464
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + tail + '<line x1="' + n(x + 6) + '" y1="' + n(y) + '" x2="' + n(x + 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" /><line x1="' + n(x + w - 6) + '" y1="' + n(y) + '" x2="' + n(x + w - 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" />' + boxText(shape, stroke);
|
|
3465
|
+
default:
|
|
3466
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="6' + tail + boxText(shape, stroke);
|
|
3467
|
+
}
|
|
918
3468
|
},
|
|
919
3469
|
link: (shape, colours) => {
|
|
920
3470
|
const stroke = colours[shape.colour];
|
|
921
|
-
const
|
|
922
|
-
|
|
3471
|
+
const marks = edgeMarks(
|
|
3472
|
+
{ x: shape.x1, y: shape.y1 },
|
|
3473
|
+
{ x: shape.x2, y: shape.y2 },
|
|
3474
|
+
{ x: shape.x1, y: shape.y1 },
|
|
3475
|
+
{ x: shape.x2, y: shape.y2 },
|
|
3476
|
+
shape.arrow,
|
|
3477
|
+
stroke,
|
|
3478
|
+
colours.background
|
|
3479
|
+
);
|
|
3480
|
+
return '<line x1="' + n(shape.x1) + '" y1="' + n(shape.y1) + '" x2="' + n(shape.x2) + '" y2="' + n(shape.y2) + '" stroke="' + stroke + '" stroke-width="1"' + dash(shape) + " />" + marks + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
923
3481
|
},
|
|
924
3482
|
path: (shape, colours) => {
|
|
925
3483
|
const stroke = colours[shape.colour];
|
|
926
3484
|
const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
|
|
927
3485
|
const last = shape.points[shape.points.length - 1];
|
|
928
3486
|
const before = shape.points[shape.points.length - 2] ?? last;
|
|
929
|
-
|
|
3487
|
+
const first = shape.points[0];
|
|
3488
|
+
const after = shape.points[1] ?? first;
|
|
3489
|
+
return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round"' + dash(shape) + " />" + edgeMarks(first, after, before, last, shape.arrow, stroke, colours.background) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
930
3490
|
},
|
|
3491
|
+
// A junction dot: the visible answer to "is this a join or a crossing?". Filled in the line's own colour, so it
|
|
3492
|
+
// reads as the line thickening where a branch meets it rather than as a new mark on the page.
|
|
3493
|
+
junction: (shape, colours) => '<circle cx="' + n(shape.x) + '" cy="' + n(shape.y) + '" r="2.5" fill="' + colours[shape.colour] + '" />',
|
|
931
3494
|
title: (shape, colours) => {
|
|
932
3495
|
return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
|
|
933
3496
|
},
|
|
934
3497
|
caption: (shape, colours) => {
|
|
935
3498
|
const colour = colours[shape.colour];
|
|
936
3499
|
const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
|
|
937
|
-
return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" fill="' + colour + '">' + lines + "</text>";
|
|
3500
|
+
return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
|
|
938
3501
|
}
|
|
939
3502
|
};
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
3503
|
+
function dash(shape) {
|
|
3504
|
+
return shape.dashed === true ? ' stroke-dasharray="4 3"' : "";
|
|
3505
|
+
}
|
|
3506
|
+
function edgeMarks(source, sourceNext, targetPrev, target, arrow, stroke, background) {
|
|
3507
|
+
if (arrow === "composition" || arrow === "aggregation") {
|
|
3508
|
+
return diamond(source, sourceNext, stroke, arrow === "composition" ? stroke : background);
|
|
3509
|
+
}
|
|
3510
|
+
if (arrow === "generalization" || arrow === "realization") {
|
|
3511
|
+
return hollowTriangle(targetPrev, target, stroke, background);
|
|
3512
|
+
}
|
|
3513
|
+
return arrowHead(targetPrev.x, targetPrev.y, target.x, target.y, stroke);
|
|
3514
|
+
}
|
|
3515
|
+
function tipAndBase(from, to) {
|
|
3516
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
3517
|
+
if (length < 1) return void 0;
|
|
3518
|
+
const ux = (to.x - from.x) / length;
|
|
3519
|
+
const uy = (to.y - from.y) / length;
|
|
945
3520
|
const size = 7;
|
|
946
3521
|
const half = 3.2;
|
|
947
|
-
const baseX =
|
|
948
|
-
const baseY =
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
3522
|
+
const baseX = to.x - ux * size;
|
|
3523
|
+
const baseY = to.y - uy * size;
|
|
3524
|
+
return {
|
|
3525
|
+
tip: n(to.x) + "," + n(to.y),
|
|
3526
|
+
left: n(baseX - uy * half) + "," + n(baseY + ux * half),
|
|
3527
|
+
right: n(baseX + uy * half) + "," + n(baseY - ux * half)
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
function arrowHead(x1, y1, x2, y2, stroke) {
|
|
3531
|
+
const points = tipAndBase({ x: x1, y: y1 }, { x: x2, y: y2 });
|
|
3532
|
+
if (points === void 0) return "";
|
|
3533
|
+
return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + stroke + '" />';
|
|
3534
|
+
}
|
|
3535
|
+
function hollowTriangle(from, to, stroke, background) {
|
|
3536
|
+
const points = tipAndBase(from, to);
|
|
3537
|
+
if (points === void 0) return "";
|
|
3538
|
+
return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + background + '" stroke="' + stroke + '" stroke-width="1" />';
|
|
3539
|
+
}
|
|
3540
|
+
function diamond(at, next, stroke, fill) {
|
|
3541
|
+
const length = Math.hypot(next.x - at.x, next.y - at.y);
|
|
3542
|
+
if (length < 1) return "";
|
|
3543
|
+
const ux = (next.x - at.x) / length;
|
|
3544
|
+
const uy = (next.y - at.y) / length;
|
|
3545
|
+
const size = 12;
|
|
3546
|
+
const half = 4;
|
|
3547
|
+
const back = { x: at.x + ux * size, y: at.y + uy * size };
|
|
3548
|
+
const middle = { x: at.x + ux * size / 2, y: at.y + uy * size / 2 };
|
|
3549
|
+
const left = { x: middle.x - uy * half, y: middle.y + ux * half };
|
|
3550
|
+
const right = { x: middle.x + uy * half, y: middle.y - ux * half };
|
|
3551
|
+
const points = [at, left, back, right].map((point) => n(point.x) + "," + n(point.y)).join(" ");
|
|
3552
|
+
return '<polygon points="' + points + '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="1" />';
|
|
952
3553
|
}
|
|
953
3554
|
function edgeLabel(label, x, y, colours) {
|
|
954
3555
|
if (typeof label !== "string" || label === "") return "";
|
|
955
|
-
|
|
3556
|
+
const width = textWidth(label);
|
|
3557
|
+
const left = x - width / 2;
|
|
3558
|
+
const top = y - LABEL_HEIGHT / 2;
|
|
3559
|
+
const baseline = y + 3.5;
|
|
3560
|
+
return '<rect x="' + n(left) + '" y="' + n(top) + '" width="' + n(width) + '" height="' + n(LABEL_HEIGHT) + '" fill="' + colours.background + '" /><text x="' + n(x) + '" y="' + n(baseline) + '" font-size="10" text-anchor="middle" fill="' + colours.muted + '">' + escape(label) + "</text>";
|
|
956
3561
|
}
|
|
957
3562
|
function toSvg(canvas, colours, label) {
|
|
958
3563
|
let dropped = 0;
|