@adecore/diagram 0.39.0 → 0.40.1
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/dist/layout.js +24 -24
- package/dist/order.js +2 -3
- package/dist/route.d.ts +1 -4
- package/dist/route.js +5 -7
- package/package.json +5 -5
- package/src/across.ts +52 -0
- package/src/layout.ts +183 -25
- package/src/order.ts +49 -3
- package/src/protocol.ts +14 -0
- package/src/reading-order.ts +5 -0
- package/src/route.ts +53 -8
- package/src/shapes.ts +8 -0
- package/src/svg.ts +7 -0
- package/src/text.ts +19 -0
package/src/layout.ts
CHANGED
|
@@ -96,22 +96,27 @@ export function sizeOfNode(node: Pick<DiagramNode, 'label' | 'sub' | 'shape'>):
|
|
|
96
96
|
const sub = node.sub ? wrapText(node.sub, SUB_SIZE, false, maxWidth) : [];
|
|
97
97
|
const textWidth = Math.max(widestLine(label, LABEL_SIZE, true), widestLine(sub, SUB_SIZE, false));
|
|
98
98
|
const textHeight = label.length * LABEL_LINE + sub.length * SUB_LINE;
|
|
99
|
+
|
|
99
100
|
if (diamond) {
|
|
100
101
|
// A rhombus twice the size of a rectangle holds that rectangle with its corners on the outline.
|
|
101
102
|
return { w: even(Math.max(DIAMOND_MIN.w, (textWidth + 16) * 2)), h: even(Math.max(DIAMOND_MIN.h, (textHeight + 8) * 2)), label, sub };
|
|
102
103
|
}
|
|
104
|
+
|
|
103
105
|
let h = textHeight + NODE_PADDING_Y * 2;
|
|
104
106
|
// The round ends of a pill eat into the room of its outer lines.
|
|
105
107
|
const paddingX = node.shape === 'pill' ? Math.max(NODE_PADDING_X, Math.round(h / 2) - 4) : NODE_PADDING_X;
|
|
108
|
+
|
|
106
109
|
if (node.shape === 'cylinder') {
|
|
107
110
|
h += CYLINDER_LID;
|
|
108
111
|
}
|
|
112
|
+
|
|
109
113
|
return { w: even(Math.max(NODE_MIN_WIDTH, textWidth + paddingX * 2)), h: even(h), label, sub };
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/* The box of an edge's label before it is placed: wrapped narrower than a node, on a padding of its own. */
|
|
113
117
|
function sizeOfEdgeLabel(text: string): { w: number; h: number; lines: string[] } {
|
|
114
118
|
const lines = wrapText(text, SUB_SIZE, false, EDGE_LABEL_MAX_WIDTH);
|
|
119
|
+
|
|
115
120
|
return {
|
|
116
121
|
w: even(widestLine(lines, SUB_SIZE, false) + EDGE_LABEL_PADDING.x * 2),
|
|
117
122
|
h: lines.length * SUB_LINE + EDGE_LABEL_PADDING.y * 2,
|
|
@@ -129,11 +134,13 @@ export function layersOf(nodes: readonly Pick<DiagramNode, 'id'>[], edges: reado
|
|
|
129
134
|
const known = new Set(nodes.map((node) => node.id));
|
|
130
135
|
const outgoing = new Map<string, string[]>(nodes.map((node) => [node.id, []]));
|
|
131
136
|
const incoming = new Map<string, number>(nodes.map((node) => [node.id, 0]));
|
|
137
|
+
|
|
132
138
|
for (const edge of edges) {
|
|
133
139
|
// An edge to an id that is not there is refused before a layout is asked for; a loop has no direction to rank.
|
|
134
140
|
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) {
|
|
135
141
|
continue;
|
|
136
142
|
}
|
|
143
|
+
|
|
137
144
|
outgoing.get(edge.from)!.push(edge.to);
|
|
138
145
|
incoming.set(edge.to, incoming.get(edge.to)! + 1);
|
|
139
146
|
}
|
|
@@ -143,28 +150,37 @@ export function layersOf(nodes: readonly Pick<DiagramNode, 'id'>[], edges: reado
|
|
|
143
150
|
const state = new Map<string, number>();
|
|
144
151
|
const forward = new Map<string, string[]>(nodes.map((node) => [node.id, []]));
|
|
145
152
|
const roots = [...nodes.filter((node) => incoming.get(node.id) === 0), ...nodes];
|
|
153
|
+
|
|
146
154
|
for (const root of roots) {
|
|
147
155
|
if (state.has(root.id)) {
|
|
148
156
|
continue;
|
|
149
157
|
}
|
|
158
|
+
|
|
150
159
|
// Iterative, so a long chain written by an agent cannot run the stack out.
|
|
151
160
|
const stack: { id: string; next: number }[] = [{ id: root.id, next: 0 }];
|
|
152
161
|
state.set(root.id, ON_WALK);
|
|
162
|
+
|
|
153
163
|
while (stack.length > 0) {
|
|
154
164
|
const top = stack.at(-1)!;
|
|
155
165
|
const targets = outgoing.get(top.id)!;
|
|
166
|
+
|
|
156
167
|
if (top.next === targets.length) {
|
|
157
168
|
state.set(top.id, DONE);
|
|
158
169
|
stack.pop();
|
|
170
|
+
|
|
159
171
|
continue;
|
|
160
172
|
}
|
|
173
|
+
|
|
161
174
|
const target = targets[top.next]!;
|
|
162
175
|
top.next += 1;
|
|
163
176
|
const seen = state.get(target);
|
|
177
|
+
|
|
164
178
|
if (seen === ON_WALK) {
|
|
165
179
|
continue;
|
|
166
180
|
}
|
|
181
|
+
|
|
167
182
|
forward.get(top.id)!.push(target);
|
|
183
|
+
|
|
168
184
|
if (seen === undefined) {
|
|
169
185
|
state.set(target, ON_WALK);
|
|
170
186
|
stack.push({ id: target, next: 0 });
|
|
@@ -174,34 +190,44 @@ export function layersOf(nodes: readonly Pick<DiagramNode, 'id'>[], edges: reado
|
|
|
174
190
|
|
|
175
191
|
// What is left is acyclic, so a topological pass settles every longest path.
|
|
176
192
|
const remaining = new Map<string, number>(nodes.map((node) => [node.id, 0]));
|
|
193
|
+
|
|
177
194
|
for (const targets of forward.values()) {
|
|
178
195
|
for (const target of targets) {
|
|
179
196
|
remaining.set(target, remaining.get(target)! + 1);
|
|
180
197
|
}
|
|
181
198
|
}
|
|
199
|
+
|
|
182
200
|
const layers = new Map<string, number>(nodes.map((node) => [node.id, 0]));
|
|
183
201
|
const ready = nodes.filter((node) => remaining.get(node.id) === 0).map((node) => node.id);
|
|
202
|
+
|
|
184
203
|
for (let i = 0; i < ready.length; i++) {
|
|
185
204
|
const id = ready[i]!;
|
|
205
|
+
|
|
186
206
|
for (const target of forward.get(id)!) {
|
|
187
207
|
layers.set(target, Math.max(layers.get(target)!, layers.get(id)! + 1));
|
|
188
208
|
const left = remaining.get(target)! - 1;
|
|
189
209
|
remaining.set(target, left);
|
|
210
|
+
|
|
190
211
|
if (left === 0) {
|
|
191
212
|
ready.push(target);
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
}
|
|
216
|
+
|
|
195
217
|
return layers;
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
/* The layout works in the frame of a diagram that runs to the right; one that runs down is that frame mirrored across the diagonal. */
|
|
199
|
-
function flip<T extends Rect>(box: T): T {
|
|
200
|
-
return { ...box, x: box.y, y: box.x, w: box.h, h: box.w };
|
|
221
|
+
function flip<T extends Rect>(box: T, down: boolean): T {
|
|
222
|
+
return down ? { ...box, x: box.y, y: box.x, w: box.h, h: box.w } : box;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function flipSize(size: { w: number; h: number }, down: boolean): { w: number; h: number } {
|
|
226
|
+
return down ? { w: size.h, h: size.w } : { w: size.w, h: size.h };
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
function flipPoint(point: Point): Point {
|
|
204
|
-
return { x: point.y, y: point.x };
|
|
229
|
+
function flipPoint(point: Point, down: boolean): Point {
|
|
230
|
+
return down ? { x: point.y, y: point.x } : point;
|
|
205
231
|
}
|
|
206
232
|
|
|
207
233
|
/* Rounded, because a diagram lays its boxes and its lines out on a whole-pixel grid. */
|
|
@@ -213,22 +239,27 @@ function unionOf(rects: readonly Rect[]): Rect {
|
|
|
213
239
|
if (rects.length === 0) {
|
|
214
240
|
return { x: 0, y: 0, w: 0, h: 0 };
|
|
215
241
|
}
|
|
242
|
+
|
|
216
243
|
const left = Math.min(...rects.map((rect) => rect.x));
|
|
217
244
|
const top = Math.min(...rects.map((rect) => rect.y));
|
|
218
245
|
const right = Math.max(...rects.map((rect) => rect.x + rect.w));
|
|
219
246
|
const bottom = Math.max(...rects.map((rect) => rect.y + rect.h));
|
|
247
|
+
|
|
220
248
|
return { x: left, y: top, w: right - left, h: bottom - top };
|
|
221
249
|
}
|
|
222
250
|
|
|
223
251
|
/* Drops repeated corners and corners in the middle of a straight run. */
|
|
224
252
|
function simplify(points: readonly Point[]): Point[] {
|
|
225
253
|
const unique = points.filter((point, index) => index === 0 || point.x !== points[index - 1]!.x || point.y !== points[index - 1]!.y);
|
|
254
|
+
|
|
226
255
|
return unique.filter((point, index) => {
|
|
227
256
|
const before = unique[index - 1];
|
|
228
257
|
const after = unique[index + 1];
|
|
258
|
+
|
|
229
259
|
if (!before || !after) {
|
|
230
260
|
return true;
|
|
231
261
|
}
|
|
262
|
+
|
|
232
263
|
return !((before.x === point.x && point.x === after.x) || (before.y === point.y && point.y === after.y));
|
|
233
264
|
});
|
|
234
265
|
}
|
|
@@ -240,14 +271,19 @@ function simplify(points: readonly Point[]): Point[] {
|
|
|
240
271
|
function insetOf(shape: DiagramShape | undefined, box: Rect, offset: number, down: boolean): number {
|
|
241
272
|
const away = Math.abs(offset);
|
|
242
273
|
const half = box.h / 2;
|
|
274
|
+
|
|
243
275
|
const rounded = (radius: number): number => {
|
|
244
276
|
const straight = half - radius;
|
|
277
|
+
|
|
245
278
|
if (away <= straight) {
|
|
246
279
|
return 0;
|
|
247
280
|
}
|
|
281
|
+
|
|
248
282
|
const into = Math.min(radius, away - straight);
|
|
283
|
+
|
|
249
284
|
return Math.round(radius - Math.sqrt(radius * radius - into * into));
|
|
250
285
|
};
|
|
286
|
+
|
|
251
287
|
switch (shape ?? 'rect') {
|
|
252
288
|
case 'round':
|
|
253
289
|
return rounded(Math.min(10, box.w / 2, half));
|
|
@@ -260,9 +296,11 @@ function insetOf(shape: DiagramShape | undefined, box: Rect, offset: number, dow
|
|
|
260
296
|
if (!down) {
|
|
261
297
|
return 0;
|
|
262
298
|
}
|
|
299
|
+
|
|
263
300
|
const radiusX = Math.round(box.h / 2);
|
|
264
301
|
const radiusY = Math.round(Math.round(box.w * 0.18) / 2);
|
|
265
302
|
const share = Math.min(1, away / radiusX);
|
|
303
|
+
|
|
266
304
|
return Math.round(radiusY - radiusY * Math.sqrt(1 - share * share));
|
|
267
305
|
}
|
|
268
306
|
default:
|
|
@@ -274,8 +312,10 @@ function insetOf(shape: DiagramShape | undefined, box: Rect, offset: number, dow
|
|
|
274
312
|
function looseRoute(source: Rect, target: Rect, loop: boolean, down: boolean): Point[] {
|
|
275
313
|
const from = center(source);
|
|
276
314
|
const to = center(target);
|
|
315
|
+
|
|
277
316
|
if (loop) {
|
|
278
317
|
const right = source.x + source.w;
|
|
318
|
+
|
|
279
319
|
return [
|
|
280
320
|
{ x: right, y: from.y - 6 },
|
|
281
321
|
{ x: right + LOOP, y: from.y - 6 },
|
|
@@ -283,11 +323,14 @@ function looseRoute(source: Rect, target: Rect, loop: boolean, down: boolean): P
|
|
|
283
323
|
{ x: right, y: from.y + 6 }
|
|
284
324
|
];
|
|
285
325
|
}
|
|
326
|
+
|
|
286
327
|
// A diagram that runs down leaves a box downwards whenever the two are stacked, as its laid-out
|
|
287
328
|
// edges do; otherwise a dragged node's edges would come out of its side against the flow.
|
|
288
329
|
const stacked = target.y >= source.y + source.h || target.y + target.h <= source.y;
|
|
330
|
+
|
|
289
331
|
if (!(down && stacked) && target.x >= source.x + source.w) {
|
|
290
332
|
const middle = Math.round((source.x + source.w + target.x) / 2);
|
|
333
|
+
|
|
291
334
|
return simplify([
|
|
292
335
|
{ x: source.x + source.w, y: from.y },
|
|
293
336
|
{ x: middle, y: from.y },
|
|
@@ -295,8 +338,10 @@ function looseRoute(source: Rect, target: Rect, loop: boolean, down: boolean): P
|
|
|
295
338
|
{ x: target.x, y: to.y }
|
|
296
339
|
]);
|
|
297
340
|
}
|
|
341
|
+
|
|
298
342
|
if (!(down && stacked) && target.x + target.w <= source.x) {
|
|
299
343
|
const middle = Math.round((target.x + target.w + source.x) / 2);
|
|
344
|
+
|
|
300
345
|
return simplify([
|
|
301
346
|
{ x: source.x, y: from.y },
|
|
302
347
|
{ x: middle, y: from.y },
|
|
@@ -304,9 +349,11 @@ function looseRoute(source: Rect, target: Rect, loop: boolean, down: boolean): P
|
|
|
304
349
|
{ x: target.x + target.w, y: to.y }
|
|
305
350
|
]);
|
|
306
351
|
}
|
|
352
|
+
|
|
307
353
|
const middle = Math.round((from.y + to.y) / 2);
|
|
308
354
|
const start = to.y < from.y ? source.y : source.y + source.h;
|
|
309
355
|
const end = to.y < from.y ? target.y + target.h : target.y;
|
|
356
|
+
|
|
310
357
|
return simplify([
|
|
311
358
|
{ x: from.x, y: start },
|
|
312
359
|
{ x: from.x, y: middle },
|
|
@@ -357,45 +404,54 @@ function unitsOf(nodes: readonly DiagramNode[], layers: ReadonlyMap<string, numb
|
|
|
357
404
|
const units: Unit[] = [];
|
|
358
405
|
const unitOf = new Map<string, number>();
|
|
359
406
|
const unitOfNode = new Map<number, number>();
|
|
407
|
+
|
|
360
408
|
nodes.forEach((node, index) => {
|
|
361
409
|
if (node.pos) {
|
|
362
410
|
return;
|
|
363
411
|
}
|
|
412
|
+
|
|
364
413
|
const size = sizes[index]!;
|
|
365
414
|
unitOf.set(node.id, units.length);
|
|
366
415
|
unitOfNode.set(index, units.length);
|
|
416
|
+
|
|
367
417
|
units.push({
|
|
368
418
|
layer: layers.get(node.id) ?? 0,
|
|
369
419
|
node: index,
|
|
370
420
|
band: -1,
|
|
371
|
-
|
|
372
|
-
h: down ? size.w : size.h,
|
|
421
|
+
...flipSize(size, down),
|
|
373
422
|
margin: NODE_MARGIN,
|
|
374
423
|
line: -1,
|
|
375
424
|
up: [],
|
|
376
425
|
down: []
|
|
377
426
|
});
|
|
378
427
|
});
|
|
428
|
+
|
|
379
429
|
return { units, unitOf, unitOfNode };
|
|
380
430
|
}
|
|
381
431
|
|
|
382
432
|
function bandsOf(groups: readonly DiagramGroup[], units: Unit[], unitOf: ReadonlyMap<string, number>, down: boolean) {
|
|
383
433
|
const bands: Band[] = [];
|
|
384
434
|
const bandOfGroup = new Map<number, number>();
|
|
435
|
+
|
|
385
436
|
groups.forEach((group, groupIndex) => {
|
|
386
437
|
const inside = group.wraps.flatMap((id) => {
|
|
387
438
|
const unit = unitOf.get(id);
|
|
439
|
+
|
|
388
440
|
return unit === undefined || units[unit]!.band >= 0 ? [] : [unit];
|
|
389
441
|
});
|
|
442
|
+
|
|
390
443
|
if (inside.length === 0) {
|
|
391
444
|
return;
|
|
392
445
|
}
|
|
446
|
+
|
|
393
447
|
const first = Math.min(...inside.map((unit) => units[unit]!.layer));
|
|
394
448
|
const last = Math.max(...inside.map((unit) => units[unit]!.layer));
|
|
395
449
|
bandOfGroup.set(groupIndex, bands.length);
|
|
450
|
+
|
|
396
451
|
for (const unit of inside) {
|
|
397
452
|
units[unit]!.band = bands.length;
|
|
398
453
|
}
|
|
454
|
+
|
|
399
455
|
bands.push({
|
|
400
456
|
group: groupIndex,
|
|
401
457
|
first,
|
|
@@ -409,6 +465,7 @@ function bandsOf(groups: readonly DiagramGroup[], units: Unit[], unitOf: Readonl
|
|
|
409
465
|
labelWidth: estimateTextWidth(group.label, SUB_SIZE, true)
|
|
410
466
|
});
|
|
411
467
|
});
|
|
468
|
+
|
|
412
469
|
return { bands, bandOfGroup };
|
|
413
470
|
}
|
|
414
471
|
|
|
@@ -419,114 +476,147 @@ function edgesOf(edges: readonly DiagramEdge[], units: Unit[], unitOf: ReadonlyM
|
|
|
419
476
|
const loops: Loop[] = [];
|
|
420
477
|
const loose: number[] = [];
|
|
421
478
|
const lines: number[][] = [];
|
|
479
|
+
|
|
422
480
|
edges.forEach((edge, index) => {
|
|
423
481
|
if (!knownIds.has(edge.from) || !knownIds.has(edge.to)) {
|
|
424
482
|
return;
|
|
425
483
|
}
|
|
484
|
+
|
|
426
485
|
const source = unitOf.get(edge.from);
|
|
427
486
|
const target = unitOf.get(edge.to);
|
|
487
|
+
|
|
428
488
|
if (source === undefined || target === undefined) {
|
|
429
489
|
loose.push(index);
|
|
490
|
+
|
|
430
491
|
return;
|
|
431
492
|
}
|
|
493
|
+
|
|
432
494
|
if (source === target) {
|
|
433
495
|
loops.push({ edge: index, unit: source });
|
|
496
|
+
|
|
434
497
|
return;
|
|
435
498
|
}
|
|
499
|
+
|
|
436
500
|
const reversed = units[source]!.layer > units[target]!.layer;
|
|
437
501
|
const low = reversed ? target : source;
|
|
438
502
|
const high = reversed ? source : target;
|
|
503
|
+
|
|
439
504
|
if (units[low]!.layer === units[high]!.layer) {
|
|
440
505
|
loose.push(index);
|
|
506
|
+
|
|
441
507
|
return;
|
|
442
508
|
}
|
|
509
|
+
|
|
443
510
|
// A point of an edge between two members of one group belongs inside that group.
|
|
444
511
|
const band = units[low]!.band >= 0 && units[low]!.band === units[high]!.band ? units[low]!.band : -1;
|
|
445
512
|
const chain: number[] = [];
|
|
446
513
|
const line: number[] = [];
|
|
447
514
|
let previous = low;
|
|
515
|
+
|
|
448
516
|
for (let layer = units[low]!.layer + 1; layer <= units[high]!.layer; layer++) {
|
|
449
517
|
let next = high;
|
|
518
|
+
|
|
450
519
|
if (layer < units[high]!.layer) {
|
|
451
520
|
next = units.length;
|
|
452
521
|
line.push(next);
|
|
453
522
|
units.push({ layer, node: -1, band, w: 0, h: 0, margin: DUMMY_MARGIN, line: lines.length, up: [], down: [] });
|
|
454
523
|
}
|
|
524
|
+
|
|
455
525
|
chain.push(hops.length);
|
|
456
526
|
hops.push({ edge: index, from: previous, to: next });
|
|
457
527
|
units[previous]!.down.push(next);
|
|
458
528
|
units[next]!.up.push(previous);
|
|
459
529
|
previous = next;
|
|
460
530
|
}
|
|
531
|
+
|
|
461
532
|
if (line.length > 0) {
|
|
462
533
|
lines.push(line);
|
|
463
534
|
}
|
|
535
|
+
|
|
464
536
|
chains.push({ edge: index, hops: chain, reversed });
|
|
465
537
|
});
|
|
538
|
+
|
|
466
539
|
return { hops, chains, loops, loose, lines };
|
|
467
540
|
}
|
|
468
541
|
|
|
469
542
|
/* The top-level entries of every layer, with the members of each band filled in and its height measured. */
|
|
470
543
|
function itemsOf(units: readonly Unit[], bands: readonly Band[], layerCount: number, down: boolean): number[][] {
|
|
471
544
|
const items: number[][] = Array.from({ length: layerCount }, () => []);
|
|
545
|
+
|
|
472
546
|
units.forEach((unit, index) => {
|
|
473
547
|
if (unit.band < 0) {
|
|
474
548
|
items[unit.layer]!.push(index);
|
|
549
|
+
|
|
475
550
|
return;
|
|
476
551
|
}
|
|
552
|
+
|
|
477
553
|
const band = bands[unit.band]!;
|
|
478
554
|
const slot = band.members[unit.layer - band.first]!;
|
|
555
|
+
|
|
479
556
|
if (slot.length === 0) {
|
|
480
557
|
items[unit.layer]!.push(bandCode(unit.band));
|
|
481
558
|
}
|
|
559
|
+
|
|
482
560
|
slot.push(index);
|
|
483
561
|
});
|
|
562
|
+
|
|
484
563
|
bands.forEach((band, index) => {
|
|
485
564
|
band.members.forEach((slot, offset) => {
|
|
486
565
|
if (slot.length === 0) {
|
|
487
566
|
items[band.first + offset]!.push(bandCode(index));
|
|
488
567
|
}
|
|
489
568
|
});
|
|
569
|
+
|
|
490
570
|
const tallest = band.members.reduce((most, slot) => {
|
|
491
571
|
const extent = slot.reduce(
|
|
492
572
|
(sum, unit, position) => sum + units[unit]!.h + (position > 0 ? units[slot[position - 1]!]!.margin + units[unit]!.margin : 0),
|
|
493
573
|
0
|
|
494
574
|
);
|
|
575
|
+
|
|
495
576
|
return Math.max(most, extent);
|
|
496
577
|
}, 0);
|
|
578
|
+
|
|
497
579
|
band.height = band.before + tallest + band.after;
|
|
580
|
+
|
|
498
581
|
if (down) {
|
|
499
582
|
// Running down, a group's label runs across the flow, so the group is at least as wide as it.
|
|
500
583
|
band.height = Math.max(band.height, band.labelWidth + GROUP_LABEL_INSET * 2);
|
|
501
584
|
}
|
|
502
585
|
});
|
|
586
|
+
|
|
503
587
|
return items;
|
|
504
588
|
}
|
|
505
589
|
|
|
506
590
|
/* Every hop leaves its box on the side facing the flow and enters on the side facing back. */
|
|
507
591
|
function portsOf(units: readonly Unit[], top: readonly number[], hops: readonly Hop[], loops: readonly Loop[]): Ports {
|
|
508
592
|
const middleOf = (unit: number): number => top[unit]! + units[unit]!.h / 2;
|
|
593
|
+
|
|
509
594
|
const hopStart = new Array<number>(hops.length).fill(0);
|
|
510
595
|
const hopEnd = new Array<number>(hops.length).fill(0);
|
|
511
596
|
const loopStart = new Array<number>(loops.length).fill(0);
|
|
512
597
|
const loopEnd = new Array<number>(loops.length).fill(0);
|
|
513
598
|
const exits: Port[][] = units.map(() => []);
|
|
514
599
|
const entries: Port[][] = units.map(() => []);
|
|
600
|
+
|
|
515
601
|
hops.forEach((hop, index) => {
|
|
516
602
|
exits[hop.from]!.push({ key: middleOf(hop.to), edge: hop.edge, order: 0, apply: (y) => (hopStart[index] = y) });
|
|
517
603
|
entries[hop.to]!.push({ key: middleOf(hop.from), edge: hop.edge, order: 0, apply: (y) => (hopEnd[index] = y) });
|
|
518
604
|
});
|
|
605
|
+
|
|
519
606
|
loops.forEach((loop, index) => {
|
|
520
607
|
const key = middleOf(loop.unit);
|
|
608
|
+
|
|
521
609
|
exits[loop.unit]!.push(
|
|
522
610
|
{ key, edge: loop.edge, order: 0, apply: (y) => (loopStart[index] = y) },
|
|
523
611
|
{ key, edge: loop.edge, order: 1, apply: (y) => (loopEnd[index] = y) }
|
|
524
612
|
);
|
|
525
613
|
});
|
|
614
|
+
|
|
526
615
|
const spread = (unit: number, ports: Port[]): void => {
|
|
527
616
|
if (ports.length === 0) {
|
|
528
617
|
return;
|
|
529
618
|
}
|
|
619
|
+
|
|
530
620
|
// In the order of the other ends, so two hops leaving one box do not cross on the way out.
|
|
531
621
|
ports.sort((left, right) => left.key - right.key || left.edge - right.edge || left.order - right.order);
|
|
532
622
|
const middle = middleOf(unit);
|
|
@@ -534,23 +624,29 @@ function portsOf(units: readonly Unit[], top: readonly number[], hops: readonly
|
|
|
534
624
|
const step = ports.length > 1 ? Math.min(PORT_GAP, Math.floor(room / (ports.length - 1))) : 0;
|
|
535
625
|
ports.forEach((port, index) => port.apply(Math.round(middle + (index - (ports.length - 1) / 2) * step)));
|
|
536
626
|
};
|
|
627
|
+
|
|
537
628
|
units.forEach((_, unit) => {
|
|
538
629
|
spread(unit, exits[unit]!);
|
|
539
630
|
spread(unit, entries[unit]!);
|
|
540
631
|
});
|
|
632
|
+
|
|
541
633
|
const hasRoomAt = (unit: number, y: number): boolean =>
|
|
542
634
|
units[unit]!.node >= 0 && y >= top[unit]! + PORT_INSET && y <= top[unit]! + units[unit]!.h - PORT_INSET;
|
|
635
|
+
|
|
543
636
|
hops.forEach((hop, index) => {
|
|
544
637
|
const gap = Math.abs(hopEnd[index]! - hopStart[index]!);
|
|
638
|
+
|
|
545
639
|
if (gap === 0 || gap > SNAP) {
|
|
546
640
|
return;
|
|
547
641
|
}
|
|
642
|
+
|
|
548
643
|
if (entries[hop.to]!.length === 1 && hasRoomAt(hop.to, hopStart[index]!)) {
|
|
549
644
|
hopEnd[index] = hopStart[index]!;
|
|
550
645
|
} else if (exits[hop.from]!.length === 1 && hasRoomAt(hop.from, hopEnd[index]!)) {
|
|
551
646
|
hopStart[index] = hopEnd[index]!;
|
|
552
647
|
}
|
|
553
648
|
});
|
|
649
|
+
|
|
554
650
|
return { hopStart, hopEnd, loopStart, loopEnd };
|
|
555
651
|
}
|
|
556
652
|
|
|
@@ -577,11 +673,13 @@ function placeAlong(layering: Layering, edges: Edges, ports: Ports, labelSizes:
|
|
|
577
673
|
hops.forEach((hop, index) => hopsByLayer[units[hop.from]!.layer]!.push(index));
|
|
578
674
|
|
|
579
675
|
const labelHop = new Map<number, number>();
|
|
676
|
+
|
|
580
677
|
for (const chain of chains) {
|
|
581
678
|
if (labelSizes[chain.edge]) {
|
|
582
679
|
labelHop.set(chain.hops[Math.floor((chain.hops.length - 1) / 2)]!, chain.edge);
|
|
583
680
|
}
|
|
584
681
|
}
|
|
682
|
+
|
|
585
683
|
const frameLabels = new Map<number, Rect>();
|
|
586
684
|
const trackX = new Map<number, number>();
|
|
587
685
|
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
@@ -590,36 +688,49 @@ function placeAlong(layering: Layering, edges: Edges, ports: Ports, labelSizes:
|
|
|
590
688
|
for (const unit of byLayer[layer]!) {
|
|
591
689
|
unitX[unit] = columnX[layer]! + Math.floor((columnWidth[layer]! - units[unit]!.w) / 2);
|
|
592
690
|
}
|
|
691
|
+
|
|
593
692
|
if (layer === layerCount - 1) {
|
|
594
693
|
break;
|
|
595
694
|
}
|
|
695
|
+
|
|
596
696
|
const start = columnX[layer]! + columnWidth[layer]!;
|
|
597
697
|
const channelHops: ChannelHop[] = hopsByLayer[layer]!.map((hop) => ({ hop, ya: ports.hopStart[hop]!, yb: ports.hopEnd[hop]! }));
|
|
598
698
|
const channelLabels: ChannelLabel[] = [];
|
|
699
|
+
|
|
599
700
|
for (const hop of hopsByLayer[layer]!) {
|
|
600
701
|
const edge = labelHop.get(hop);
|
|
601
702
|
const size = edge === undefined ? null : labelSizes[edge];
|
|
703
|
+
|
|
602
704
|
if (edge !== undefined && size) {
|
|
603
|
-
|
|
705
|
+
const frame = flipSize(size, down);
|
|
706
|
+
channelLabels.push({ edge, hop, y: ports.hopStart[hop]!, along: frame.w, across: frame.h });
|
|
604
707
|
}
|
|
605
708
|
}
|
|
709
|
+
|
|
606
710
|
loops.forEach((loop, index) => {
|
|
607
711
|
const size = labelSizes[loop.edge];
|
|
712
|
+
|
|
608
713
|
if (size && units[loop.unit]!.layer === layer) {
|
|
609
|
-
|
|
714
|
+
const frame = flipSize(size, down);
|
|
715
|
+
channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index]!, along: frame.w, across: frame.h });
|
|
610
716
|
}
|
|
611
717
|
});
|
|
718
|
+
|
|
612
719
|
channelLabels.sort((left, right) => left.edge - right.edge);
|
|
613
720
|
const obstacles: { y: number; h: number }[] = [];
|
|
721
|
+
|
|
614
722
|
for (const band of bands) {
|
|
615
723
|
if (band.first > layer || band.last <= layer) {
|
|
616
724
|
continue;
|
|
617
725
|
}
|
|
726
|
+
|
|
618
727
|
// A group that runs through the channel crosses it with its two borders, and running right with its label band.
|
|
619
728
|
obstacles.push({ y: band.top - 3, h: down ? 6 : GROUP_LABEL_BAND }, { y: band.top + band.height - 3, h: 6 });
|
|
620
729
|
}
|
|
730
|
+
|
|
621
731
|
const endsHere = bands.some((band) => band.last === layer);
|
|
622
732
|
const startsNext = bands.some((band) => band.first === layer + 1);
|
|
733
|
+
|
|
623
734
|
const channel = routeChannel({
|
|
624
735
|
start,
|
|
625
736
|
clearBefore: EDGE_CLEAR + (endsHere ? GROUP_PADDING : 0),
|
|
@@ -628,27 +739,34 @@ function placeAlong(layering: Layering, edges: Edges, ports: Ports, labelSizes:
|
|
|
628
739
|
labels: channelLabels,
|
|
629
740
|
obstacles
|
|
630
741
|
});
|
|
742
|
+
|
|
631
743
|
channel.trackX.forEach((x, hop) => trackX.set(hop, x));
|
|
632
744
|
channel.labels.forEach((rect, edge) => frameLabels.set(edge, rect));
|
|
633
745
|
columnX[layer + 1] = start + channel.width;
|
|
634
746
|
}
|
|
747
|
+
|
|
635
748
|
return { columnX, columnWidth, unitX, trackX, frameLabels };
|
|
636
749
|
}
|
|
637
750
|
|
|
638
751
|
/* A label that no channel placed (a loop in the last layer, an edge with a pinned end) moves down until it is clear. */
|
|
639
752
|
function settleLabels(edges: readonly EdgeRoute[], unplaced: ReadonlySet<number>, nodes: readonly NodeBox[], groups: readonly GroupBox[]): void {
|
|
640
753
|
const settled = edges.flatMap((edge) => (edge.label && !unplaced.has(edge.index) ? [edge.label] : []));
|
|
754
|
+
|
|
641
755
|
for (const edge of edges) {
|
|
642
756
|
const label = edge.label;
|
|
757
|
+
|
|
643
758
|
if (!label || !unplaced.has(edge.index)) {
|
|
644
759
|
continue;
|
|
645
760
|
}
|
|
761
|
+
|
|
646
762
|
const taken = (): boolean =>
|
|
647
763
|
[...nodes, ...groups.map((group) => group.labelBox)].some((box) => intersects(box, label)) ||
|
|
648
764
|
settled.some((other) => intersects({ x: other.x - 4, y: other.y - 4, w: other.w + 8, h: other.h + 8 }, label));
|
|
765
|
+
|
|
649
766
|
for (let step = 0; step < 1000 && taken(); step++) {
|
|
650
767
|
label.y += 4;
|
|
651
768
|
}
|
|
769
|
+
|
|
652
770
|
settled.push(label);
|
|
653
771
|
}
|
|
654
772
|
}
|
|
@@ -673,43 +791,60 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
673
791
|
const layering: Layering = { units, hops, bands, lines: graphEdges.lines, items };
|
|
674
792
|
orderLayers(layering);
|
|
675
793
|
const top = placeAcross(layering);
|
|
794
|
+
|
|
676
795
|
const middleOf = (unit: number): number => top[unit]! + units[unit]!.h / 2;
|
|
796
|
+
|
|
677
797
|
const ports = portsOf(units, top, hops, loops);
|
|
678
798
|
const labelSizes = document.edges.map((edge) => (edge.label ? sizeOfEdgeLabel(edge.label) : null));
|
|
679
799
|
const { columnX, columnWidth, unitX, trackX, frameLabels } = placeAlong(layering, graphEdges, ports, labelSizes, layerCount, down);
|
|
680
800
|
|
|
681
801
|
const frameBoxOf = (unit: number): Rect => ({ x: unitX[unit]!, y: top[unit]!, w: units[unit]!.w, h: units[unit]!.h });
|
|
802
|
+
|
|
682
803
|
const shapeOf = (unit: number): DiagramShape | undefined => document.nodes[units[unit]!.node]!.shape;
|
|
804
|
+
|
|
683
805
|
const exitX = (unit: number, y: number): number => {
|
|
684
806
|
const self = units[unit]!;
|
|
807
|
+
|
|
685
808
|
if (self.node < 0) {
|
|
686
809
|
return columnX[self.layer]! + columnWidth[self.layer]!;
|
|
687
810
|
}
|
|
811
|
+
|
|
688
812
|
const box = frameBoxOf(unit);
|
|
813
|
+
|
|
689
814
|
return box.x + box.w - insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
690
815
|
};
|
|
816
|
+
|
|
691
817
|
const entryX = (unit: number, y: number): number => {
|
|
692
818
|
const self = units[unit]!;
|
|
819
|
+
|
|
693
820
|
if (self.node < 0) {
|
|
694
821
|
return columnX[self.layer]!;
|
|
695
822
|
}
|
|
823
|
+
|
|
696
824
|
const box = frameBoxOf(unit);
|
|
825
|
+
|
|
697
826
|
return box.x + insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
698
827
|
};
|
|
699
|
-
|
|
828
|
+
|
|
829
|
+
const toReal = (point: Point): Point => flipPoint(point, down);
|
|
830
|
+
|
|
700
831
|
const labelOf = (edge: number, frame: Rect | undefined): EdgeLabelBox | null => {
|
|
701
832
|
const size = labelSizes[edge];
|
|
833
|
+
|
|
702
834
|
if (!size || !frame) {
|
|
703
835
|
return null;
|
|
704
836
|
}
|
|
705
|
-
|
|
837
|
+
|
|
838
|
+
return { ...flip(frame, down), lines: size.lines };
|
|
706
839
|
};
|
|
707
840
|
|
|
708
841
|
const nodes: NodeBox[] = document.nodes.map((node, index) => {
|
|
709
842
|
const size = sizes[index]!;
|
|
710
843
|
const unit = unitOfNode.get(index);
|
|
844
|
+
|
|
711
845
|
if (unit === undefined) {
|
|
712
846
|
const [px, py] = node.pos!;
|
|
847
|
+
|
|
713
848
|
return {
|
|
714
849
|
id: node.id,
|
|
715
850
|
x: Math.round(px),
|
|
@@ -722,29 +857,35 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
722
857
|
sub: size.sub
|
|
723
858
|
};
|
|
724
859
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
return { id: node.id, ...box, layer: units[unit]!.layer, pinned: false, label: size.label, sub: size.sub };
|
|
860
|
+
|
|
861
|
+
return { id: node.id, ...flip(frameBoxOf(unit), down), layer: units[unit]!.layer, pinned: false, label: size.label, sub: size.sub };
|
|
728
862
|
});
|
|
863
|
+
|
|
729
864
|
const byId = new Map(nodes.map((box) => [box.id, box]));
|
|
730
865
|
|
|
731
866
|
const routes: (EdgeRoute | undefined)[] = [];
|
|
732
867
|
const unplaced = new Set<number>(loose);
|
|
868
|
+
|
|
733
869
|
for (const chain of chains) {
|
|
734
870
|
const points: Point[] = [];
|
|
871
|
+
|
|
735
872
|
for (const index of chain.hops) {
|
|
736
873
|
const hop = hops[index]!;
|
|
737
874
|
const ya = ports.hopStart[index]!;
|
|
738
875
|
const yb = ports.hopEnd[index]!;
|
|
739
876
|
points.push({ x: exitX(hop.from, ya), y: ya });
|
|
877
|
+
|
|
740
878
|
if (ya !== yb) {
|
|
741
879
|
const x = trackX.get(index)!;
|
|
742
880
|
points.push({ x, y: ya }, { x, y: yb });
|
|
743
881
|
}
|
|
882
|
+
|
|
744
883
|
points.push({ x: entryX(hop.to, yb), y: yb });
|
|
745
884
|
}
|
|
885
|
+
|
|
746
886
|
const ordered = chain.reversed ? simplify(points).reverse() : simplify(points);
|
|
747
887
|
const edge = document.edges[chain.edge]!;
|
|
888
|
+
|
|
748
889
|
routes[chain.edge] = {
|
|
749
890
|
index: chain.edge,
|
|
750
891
|
from: edge.from,
|
|
@@ -753,35 +894,42 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
753
894
|
label: labelOf(chain.edge, frameLabels.get(chain.edge))
|
|
754
895
|
};
|
|
755
896
|
}
|
|
897
|
+
|
|
756
898
|
loops.forEach((loop, index) => {
|
|
757
899
|
const box = frameBoxOf(loop.unit);
|
|
758
900
|
const reach = box.x + box.w + LOOP;
|
|
759
901
|
const startY = ports.loopStart[index]!;
|
|
760
902
|
const endY = ports.loopEnd[index]!;
|
|
903
|
+
|
|
761
904
|
const points = [
|
|
762
905
|
{ x: exitX(loop.unit, startY), y: startY },
|
|
763
906
|
{ x: reach, y: startY },
|
|
764
907
|
{ x: reach, y: endY },
|
|
765
908
|
{ x: exitX(loop.unit, endY), y: endY }
|
|
766
909
|
];
|
|
910
|
+
|
|
767
911
|
if (!frameLabels.has(loop.edge)) {
|
|
768
912
|
unplaced.add(loop.edge);
|
|
769
913
|
}
|
|
914
|
+
|
|
770
915
|
const size = labelSizes[loop.edge];
|
|
916
|
+
|
|
771
917
|
// In the last layer there is no channel to put a label in, so it sits beside the loop.
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
918
|
+
const beside = (): Rect | undefined => {
|
|
919
|
+
if (!size) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const turned = flipSize(size, down);
|
|
924
|
+
|
|
925
|
+
return { x: reach + 6, y: Math.round((startY + endY) / 2) - Math.floor(turned.h / 2), ...turned };
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const frame = frameLabels.get(loop.edge) ?? beside();
|
|
782
929
|
const edge = document.edges[loop.edge]!;
|
|
783
930
|
routes[loop.edge] = { index: loop.edge, from: edge.from, to: edge.to, points: points.map(toReal), label: labelOf(loop.edge, frame) };
|
|
784
931
|
});
|
|
932
|
+
|
|
785
933
|
for (const index of loose) {
|
|
786
934
|
const edge = document.edges[index]!;
|
|
787
935
|
const source = byId.get(edge.from)!;
|
|
@@ -789,6 +937,7 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
789
937
|
const points = looseRoute(source, target, edge.from === edge.to, down);
|
|
790
938
|
const size = labelSizes[index];
|
|
791
939
|
let label: EdgeLabelBox | null = null;
|
|
940
|
+
|
|
792
941
|
if (size) {
|
|
793
942
|
const at = Math.max(0, Math.floor((points.length - 2) / 2));
|
|
794
943
|
const first = points[at]!;
|
|
@@ -796,24 +945,29 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
796
945
|
const middle = { x: Math.round((first.x + second.x) / 2), y: Math.round((first.y + second.y) / 2) };
|
|
797
946
|
label = { x: middle.x - size.w / 2, y: middle.y - size.h - 4, w: size.w, h: size.h, lines: size.lines };
|
|
798
947
|
}
|
|
948
|
+
|
|
799
949
|
routes[index] = { index, from: edge.from, to: edge.to, points, label };
|
|
800
950
|
}
|
|
951
|
+
|
|
801
952
|
const edges = routes.filter((route): route is EdgeRoute => route !== undefined);
|
|
802
953
|
|
|
803
954
|
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
955
|
+
|
|
804
956
|
const groups: GroupBox[] = document.groups.flatMap((group, groupIndex) => {
|
|
805
957
|
const rects: Rect[] = [];
|
|
806
958
|
const bandIndex = bandOfGroup.get(groupIndex);
|
|
959
|
+
|
|
807
960
|
if (bandIndex !== undefined) {
|
|
808
961
|
const band = bands[bandIndex]!;
|
|
809
962
|
const members = band.members.flat().filter((unit) => units[unit]!.node >= 0);
|
|
810
963
|
const alongStart = Math.min(...band.members[0]!.map((unit) => unitX[unit]!)) - alongBefore;
|
|
811
964
|
const alongEnd = Math.max(...members.map((unit) => unitX[unit]! + units[unit]!.w)) + GROUP_PADDING;
|
|
812
|
-
|
|
813
|
-
rects.push(down ? flip(frame) : frame);
|
|
965
|
+
rects.push(flip({ x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height }, down));
|
|
814
966
|
}
|
|
967
|
+
|
|
815
968
|
for (const id of group.wraps) {
|
|
816
969
|
const box = byId.get(id);
|
|
970
|
+
|
|
817
971
|
if (box?.pinned) {
|
|
818
972
|
rects.push({
|
|
819
973
|
x: box.x - GROUP_PADDING,
|
|
@@ -823,11 +977,14 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
823
977
|
});
|
|
824
978
|
}
|
|
825
979
|
}
|
|
980
|
+
|
|
826
981
|
if (rects.length === 0) {
|
|
827
982
|
return [];
|
|
828
983
|
}
|
|
984
|
+
|
|
829
985
|
const rect = unionOf(rects);
|
|
830
986
|
const labelBox = { x: rect.x + GROUP_LABEL_INSET, y: rect.y + 4, w: estimateTextWidth(group.label, SUB_SIZE, true), h: SUB_LINE };
|
|
987
|
+
|
|
831
988
|
return [{ id: group.id, ...rect, labelBox }];
|
|
832
989
|
});
|
|
833
990
|
|
|
@@ -835,5 +992,6 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
835
992
|
const corners = edges.flatMap((edge) => edge.points.map((point) => ({ ...point, w: 0, h: 0 })));
|
|
836
993
|
const labels = edges.flatMap((edge) => (edge.label ? [edge.label] : []));
|
|
837
994
|
const groupLabels = groups.map((group) => group.labelBox);
|
|
995
|
+
|
|
838
996
|
return { nodes, groups, edges, bounds: unionOf([...nodes, ...groups, ...groupLabels, ...corners, ...labels]) };
|
|
839
997
|
}
|