@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/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
- w: down ? size.h : size.w,
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
- channelLabels.push({ edge, hop, y: ports.hopStart[hop]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
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
- channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
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
- const toReal = (point: Point): Point => (down ? flipPoint(point) : point);
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
- return { ...(down ? flip(frame) : frame), lines: size.lines };
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
- const frame = frameBoxOf(unit);
726
- const box = down ? flip(frame) : frame;
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 frame =
773
- frameLabels.get(loop.edge) ??
774
- (size
775
- ? {
776
- x: reach + 6,
777
- y: Math.round((startY + endY) / 2) - Math.floor((down ? size.w : size.h) / 2),
778
- w: down ? size.h : size.w,
779
- h: down ? size.w : size.h
780
- }
781
- : undefined);
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
- const frame = { x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height };
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
  }