@weasel-js/diagram 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,7 +129,7 @@ without a canvas. The layout defaults to `layered`, picked by name or passed as
129
129
  any `LayoutFn`; `layoutOptions` merge over `order: 'barycenter'`, and
130
130
  `'none'` leaves every node at its `at`.
131
131
 
132
- `DiagramView` is the front door for the whole package. The data owns the
132
+ The data owns the
133
133
  diagram: a new `specs` array is reconciled into the scene the view already
134
134
  holds (`reconcileSpecs` in core), so the view, the pick and anything moved
135
135
  since survive it. Editing reports rather than writes —
@@ -157,7 +157,7 @@ readable; the rest is a pan away.
157
157
 
158
158
  ## Design notes
159
159
 
160
- Five decisions the code cannot explain on its own.
160
+ Decisions the code cannot explain on its own.
161
161
 
162
162
  **`dependsOn: 'children'` is deliberately absent from the reverse dependents
163
163
  index.** Deleting a node deletes everything that names it in `dependsOn`, and a
@@ -1,5 +1,6 @@
1
- import { rectPath, polygonFromPoints, PathBuilder, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, AUTO_POSE_DESCRIPTOR, rotatePoint, createSimulation } from '@weasel-js/core/math';
2
- import { cssFamilyName } from '@weasel-js/core';
1
+ import { rectPath, polygonFromPoints, PathBuilder, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, AUTO_POSE_DESCRIPTOR, rotatePoint } from '@weasel-js/core/math';
2
+ import { cssFamilyName } from '@weasel-js/font';
3
+ import { createSimulation } from '@weasel-js/simulation';
3
4
 
4
5
  // src/outline.ts
5
6
  var LEAN = 0.2;
@@ -487,6 +488,30 @@ function settle(graph, slots, opts = {}) {
487
488
  }
488
489
  return translated(graph, placed, pinned, tolerance);
489
490
  }
491
+ function untilSettled(once, graph, opts = {}, passes = 8) {
492
+ const first = once(graph, opts);
493
+ if (first.size === 0) return first;
494
+ const at = new Map(first);
495
+ for (let pass = 1; pass < passes; pass++) {
496
+ const nodes = graph.nodes.map((node) => {
497
+ const to = at.get(node.id);
498
+ return to === void 0 ? node : { ...node, bounds: { ...node.bounds, x: to.x, y: to.y } };
499
+ });
500
+ const byId = new Map(nodes.map((n) => [n.id, n]));
501
+ const next = once({ ...graph, nodes, node: (id) => byId.get(id) }, opts);
502
+ if (next.size === 0) break;
503
+ for (const [id, to] of next) at.set(id, to);
504
+ }
505
+ const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
506
+ const out = /* @__PURE__ */ new Map();
507
+ for (const node of graph.nodes) {
508
+ const to = at.get(node.id);
509
+ if (to === void 0) continue;
510
+ if (Math.abs(to.x - node.bounds.x) <= tolerance && Math.abs(to.y - node.bounds.y) <= tolerance) continue;
511
+ out.set(node.id, to);
512
+ }
513
+ return out;
514
+ }
490
515
  function translated(graph, placed, pinned, tolerance = DEFAULT_TOLERANCE) {
491
516
  const anchor = anchorDelta(graph, placed, pinned);
492
517
  const out = /* @__PURE__ */ new Map();
@@ -565,10 +590,14 @@ function rankGaps(graph, rows, axes, rankGap) {
565
590
  const gaps = rows.slice(1).map(() => rankGap);
566
591
  const ends = /* @__PURE__ */ new Map();
567
592
  const starts = /* @__PURE__ */ new Map();
593
+ const reach = (span, side) => {
594
+ const own = canonical(span.group.inset, axes)[side === "lo" ? "rankLo" : "rankHi"];
595
+ const flush = span.children.filter((c) => c[side] === span[side]);
596
+ return own + Math.max(0, ...flush.map((c) => reach(c, side)));
597
+ };
568
598
  for (const span of groupSpans(graph, rows)) {
569
- const inset = canonical(span.group.inset, axes);
570
- ends.set(span.hi, Math.max(ends.get(span.hi) ?? 0, inset.rankHi));
571
- starts.set(span.lo, Math.max(starts.get(span.lo) ?? 0, inset.rankLo));
599
+ ends.set(span.hi, Math.max(ends.get(span.hi) ?? 0, reach(span, "hi")));
600
+ starts.set(span.lo, Math.max(starts.get(span.lo) ?? 0, reach(span, "lo")));
572
601
  }
573
602
  for (let i = 0; i < gaps.length; i++) {
574
603
  gaps[i] = Math.max(gaps[i], rankGap + (ends.get(i) ?? 0) + (starts.get(i + 1) ?? 0));
@@ -579,92 +608,125 @@ function groupSpans(graph, rows) {
579
608
  const rankOf = /* @__PURE__ */ new Map();
580
609
  rows.forEach((row, i) => row.forEach((n) => rankOf.set(n.id, i)));
581
610
  const spans = [];
611
+ const byId = /* @__PURE__ */ new Map();
582
612
  for (const group of graph.groups ?? []) {
583
613
  const ranks = group.members.map((id) => rankOf.get(id)).filter((r) => r !== void 0);
584
614
  if (ranks.length === 0) continue;
585
- spans.push({ group, lo: Math.min(...ranks), hi: Math.max(...ranks) });
615
+ const span = { group, lo: Math.min(...ranks), hi: Math.max(...ranks), children: [] };
616
+ spans.push(span);
617
+ byId.set(group.id, span);
618
+ }
619
+ for (const span of spans) {
620
+ const parent = span.group.parent === void 0 ? void 0 : byId.get(span.group.parent);
621
+ if (parent === void 0) continue;
622
+ span.parent = parent;
623
+ parent.children.push(span);
586
624
  }
587
625
  return spans;
588
626
  }
589
627
  function packClusters(graph, rows, preferred, axes, nodeGap) {
590
628
  const spans = groupSpans(graph, rows);
591
- const spanOf = /* @__PURE__ */ new Map();
592
- for (const span of spans) for (const id of span.group.members) spanOf.set(id, span);
629
+ const spanById = new Map(spans.map((s) => [s.group.id, s]));
630
+ const spanOf = (n) => n.group === void 0 ? void 0 : spanById.get(n.group);
593
631
  const width = (n) => extent(n.bounds, axes.cross);
594
632
  const pref = (n) => preferred.get(n.id) ?? 0;
595
- const packedWidth = (ns) => ns.reduce((s, n) => s + width(n), 0) + Math.max(0, ns.length - 1) * nodeGap;
596
633
  const insets = new Map(spans.map((s) => [s, canonical(s.group.inset, axes)]));
597
- const inner = /* @__PURE__ */ new Map();
598
634
  const centerOf = /* @__PURE__ */ new Map();
599
635
  for (const span of spans) {
600
- let widest = 0;
636
+ const inside = new Set(span.group.members);
601
637
  let sum = 0;
602
638
  let count = 0;
603
639
  for (const row of rows) {
604
- const members = row.filter((n) => spanOf.get(n.id) === span);
605
- widest = Math.max(widest, packedWidth(members));
606
- for (const n of members) {
640
+ for (const n of row) {
641
+ if (!inside.has(n.id)) continue;
607
642
  sum += pref(n) + width(n) / 2;
608
643
  count++;
609
644
  }
610
645
  }
611
- inner.set(span, widest);
612
646
  centerOf.set(span, count === 0 ? 0 : sum / count);
613
647
  }
614
- const outer = (span) => inner.get(span) + insets.get(span).crossLo + insets.get(span).crossHi;
615
- const itemsByRow = rows.map((row, r) => {
648
+ const itemsByRow = (owner, among) => rows.map((row, r) => {
616
649
  const items = [];
617
650
  for (const node of row) {
618
- if (!spanOf.has(node.id)) items.push({ kind: "node", node, center: pref(node) + width(node) / 2 });
651
+ if (spanOf(node) === owner) items.push({ kind: "node", node, center: pref(node) + width(node) / 2 });
619
652
  }
620
- for (const span of spans) {
621
- if (r < span.lo || r > span.hi) continue;
622
- items.push({
623
- kind: "group",
624
- span,
625
- members: row.filter((n) => spanOf.get(n.id) === span),
626
- center: centerOf.get(span)
627
- });
653
+ for (const span of among) {
654
+ if (r >= span.lo && r <= span.hi) items.push({ kind: "group", span, center: centerOf.get(span) });
628
655
  }
629
656
  return items.map((item, i) => ({ item, i })).sort((a, b) => a.item.center - b.item.center || a.i - b.i).map(({ item }) => item);
630
657
  });
631
- const start = new Map(spans.map((s) => [s, centerOf.get(s) - outer(s) / 2]));
632
- const at = /* @__PURE__ */ new Map();
633
- for (let pass = 0; pass <= spans.length; pass++) {
634
- let raised = false;
635
- for (const items of itemsByRow) {
636
- let cursor = -Infinity;
637
- for (const item of items) {
638
- if (item.kind === "node") {
639
- const x2 = Math.max(pref(item.node), cursor);
640
- at.set(item.node.id, x2);
641
- cursor = x2 + width(item.node) + nodeGap;
642
- continue;
658
+ const settle2 = (byRow, start2, nodeFloor, origin) => {
659
+ const nodeAt = /* @__PURE__ */ new Map();
660
+ let ends = [];
661
+ for (let pass = 0; pass <= start2.size; pass++) {
662
+ let raised = false;
663
+ ends = byRow.map((items) => {
664
+ let cursor = origin;
665
+ for (const item of items) {
666
+ if (item.kind === "node") {
667
+ const x2 = Math.max(nodeFloor(item.node), cursor);
668
+ nodeAt.set(item.node.id, x2);
669
+ cursor = x2 + width(item.node) + nodeGap;
670
+ continue;
671
+ }
672
+ const s = start2.get(item.span);
673
+ const x = Math.max(s, cursor);
674
+ if (x > s) {
675
+ start2.set(item.span, x);
676
+ raised = true;
677
+ }
678
+ cursor = x + outer(item.span) + nodeGap;
643
679
  }
644
- const s = start.get(item.span);
645
- const x = Math.max(s, cursor);
646
- if (x > s) {
647
- start.set(item.span, x);
648
- raised = true;
649
- }
650
- const inset = insets.get(item.span);
651
- let m = x + inset.crossLo + (inner.get(item.span) - packedWidth(item.members)) / 2;
652
- for (const node of item.members) {
653
- at.set(node.id, m);
654
- m += width(node) + nodeGap;
655
- }
656
- cursor = x + outer(item.span) + nodeGap;
657
- }
680
+ return items.length === 0 ? origin : cursor - nodeGap;
681
+ });
682
+ if (!raised) break;
658
683
  }
659
- if (!raised) break;
684
+ return { nodeAt, ends };
685
+ };
686
+ const blocks = /* @__PURE__ */ new Map();
687
+ const blockOf = (span) => {
688
+ const known = blocks.get(span);
689
+ if (known !== void 0) return known;
690
+ const byRow = itemsByRow(span, span.children);
691
+ const childAt = new Map(span.children.map((c) => [c, 0]));
692
+ const { nodeAt, ends } = settle2(byRow, childAt, () => 0, 0);
693
+ const block = { width: Math.max(0, ...ends), nodeAt, childAt };
694
+ byRow.forEach((items, r) => {
695
+ if (items.some((item) => item.kind === "group")) return;
696
+ const shift = (block.width - ends[r]) / 2;
697
+ for (const item of items) {
698
+ if (item.kind === "node") nodeAt.set(item.node.id, nodeAt.get(item.node.id) + shift);
699
+ }
700
+ });
701
+ blocks.set(span, block);
702
+ return block;
703
+ };
704
+ function outer(span) {
705
+ const inset = insets.get(span);
706
+ return blockOf(span).width + inset.crossLo + inset.crossHi;
660
707
  }
708
+ const roots = spans.filter((s) => s.parent === void 0);
709
+ const start = new Map(roots.map((s) => [s, centerOf.get(s) - outer(s) / 2]));
710
+ const { nodeAt: at } = settle2(itemsByRow(void 0, roots), start, pref, -Infinity);
711
+ const place = (span, from) => {
712
+ const origin = from + insets.get(span).crossLo;
713
+ const block = blockOf(span);
714
+ for (const [id, local] of block.nodeAt) at.set(id, origin + local);
715
+ for (const [child, local] of block.childAt) place(child, origin + local);
716
+ };
717
+ for (const root of roots) place(root, start.get(root));
661
718
  return at;
662
719
  }
663
720
  function expandGroupEdges(edges, groups) {
664
721
  const byId = new Map(groups.map((g) => [g.id, g]));
665
722
  if (byId.size === 0) return [...edges];
666
- const groupOf = /* @__PURE__ */ new Map();
667
- for (const g of groups) for (const m of g.members) groupOf.set(m, g.id);
723
+ const within = (id, group) => {
724
+ if (!byId.has(id)) return group.members.includes(id);
725
+ for (let at = byId.get(id); at !== void 0; at = at.parent === void 0 ? void 0 : byId.get(at.parent)) {
726
+ if (at === group) return true;
727
+ }
728
+ return false;
729
+ };
668
730
  const entries = /* @__PURE__ */ new Map();
669
731
  const exits = /* @__PURE__ */ new Map();
670
732
  for (const g of groups) {
@@ -690,9 +752,8 @@ function expandGroupEdges(edges, groups) {
690
752
  out.push(e);
691
753
  continue;
692
754
  }
693
- const fromOwner = fromGroup ?? groupOf.get(e.from);
694
- const toOwner = toGroup ?? groupOf.get(e.to);
695
- if (fromOwner !== void 0 && fromOwner === toOwner) continue;
755
+ if (fromGroup !== void 0 && within(e.to, byId.get(fromGroup))) continue;
756
+ if (toGroup !== void 0 && within(e.from, byId.get(toGroup))) continue;
696
757
  const froms = fromGroup !== void 0 ? exits.get(fromGroup) : [e.from];
697
758
  const tos = toGroup !== void 0 ? entries.get(toGroup) : [e.to];
698
759
  let k = 0;
@@ -702,13 +763,118 @@ function expandGroupEdges(edges, groups) {
702
763
  }
703
764
  return out;
704
765
  }
766
+
767
+ // src/forceGroups.ts
768
+ function resolve(groups, byId) {
769
+ const out = [];
770
+ for (const group of groups) {
771
+ const members = group.members.map((id) => byId.get(id)).filter((b) => b !== void 0);
772
+ if (members.length > 0) out.push({ members, group, children: [] });
773
+ }
774
+ const byGroup = new Map(out.map((g) => [g.group.id, g]));
775
+ for (const g of out) {
776
+ const parent = g.group.parent === void 0 ? void 0 : byGroup.get(g.group.parent);
777
+ if (parent === void 0) continue;
778
+ g.parent = parent;
779
+ parent.children.push(g);
780
+ }
781
+ return out;
782
+ }
783
+ function groupCohesion(groups, byId, strength) {
784
+ const resolved = resolve(groups, byId);
785
+ return (alpha) => {
786
+ for (const { members } of resolved) {
787
+ let cx = 0;
788
+ let cy = 0;
789
+ for (const m of members) {
790
+ cx += m.x;
791
+ cy += m.y;
792
+ }
793
+ cx /= members.length;
794
+ cy /= members.length;
795
+ for (const m of members) {
796
+ m.vx += (cx - m.x) * strength * alpha;
797
+ m.vy += (cy - m.y) * strength * alpha;
798
+ }
799
+ }
800
+ };
801
+ }
802
+ function boxOf(g) {
803
+ let x0 = Infinity;
804
+ let y0 = Infinity;
805
+ let x1 = -Infinity;
806
+ let y1 = -Infinity;
807
+ for (const m of g.members) {
808
+ x0 = Math.min(x0, m.x - m.hw);
809
+ y0 = Math.min(y0, m.y - m.hh);
810
+ x1 = Math.max(x1, m.x + m.hw);
811
+ y1 = Math.max(y1, m.y + m.hh);
812
+ }
813
+ for (const child of g.children) {
814
+ const b = boxOf(child);
815
+ x0 = Math.min(x0, b.x0);
816
+ y0 = Math.min(y0, b.y0);
817
+ x1 = Math.max(x1, b.x1);
818
+ y1 = Math.max(y1, b.y1);
819
+ }
820
+ const { inset } = g.group;
821
+ return { x0: x0 - inset.left, y0: y0 - inset.top, x1: x1 + inset.right, y1: y1 + inset.bottom };
822
+ }
823
+ function clearance(a, b, padding) {
824
+ const gapX = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0) + padding;
825
+ const gapY = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0) + padding;
826
+ if (gapX <= 0 || gapY <= 0) return null;
827
+ const dx = b.x0 + b.x1 - (a.x0 + a.x1);
828
+ const dy = b.y0 + b.y1 - (a.y0 + a.y1);
829
+ return gapX < gapY ? { x: (Math.sign(dx) || 1) * gapX, y: 0 } : { x: 0, y: (Math.sign(dy) || 1) * gapY };
830
+ }
831
+ function holds(outer, inner) {
832
+ for (let at = inner.parent; at !== void 0; at = at.parent) {
833
+ if (at === outer) return true;
834
+ }
835
+ return false;
836
+ }
837
+ function shove(bodies, x, y) {
838
+ for (const body of bodies) {
839
+ body.vx += x;
840
+ body.vy += y;
841
+ }
842
+ }
843
+ function separateGroups(groups, bodies, byId, padding) {
844
+ const resolved = resolve(groups, byId);
845
+ const memberSets = resolved.map((g) => new Set(g.members));
846
+ return () => {
847
+ const boxes = resolved.map(boxOf);
848
+ for (let i = 0; i < resolved.length; i++) {
849
+ for (let j = i + 1; j < resolved.length; j++) {
850
+ if (holds(resolved[i], resolved[j]) || holds(resolved[j], resolved[i])) continue;
851
+ const push = clearance(boxes[i], boxes[j], padding);
852
+ if (push === null) continue;
853
+ shove(resolved[i].members, -push.x / 2, -push.y / 2);
854
+ shove(resolved[j].members, push.x / 2, push.y / 2);
855
+ }
856
+ for (const body of bodies) {
857
+ if (memberSets[i].has(body)) continue;
858
+ const box = { x0: body.x - body.hw, y0: body.y - body.hh, x1: body.x + body.hw, y1: body.y + body.hh };
859
+ const push = clearance(boxes[i], box, padding);
860
+ if (push === null) continue;
861
+ shove(resolved[i].members, -push.x / 2, -push.y / 2);
862
+ body.vx += push.x / 2;
863
+ body.vy += push.y / 2;
864
+ }
865
+ }
866
+ };
867
+ }
868
+
869
+ // src/force.ts
705
870
  var DEFAULTS = {
706
871
  iterations: 300,
707
872
  linkDistance: 160,
708
873
  linkStrength: 0.35,
709
874
  charge: -3e3,
710
875
  gravity: 0.02,
711
- padding: 24
876
+ padding: 24,
877
+ groupStrength: 0.2
712
878
  };
713
879
  var GOLDEN = Math.PI * (3 - Math.sqrt(5));
714
880
  var NUDGE = 0.5;
@@ -787,7 +953,8 @@ function forceRelaxation(graph, opts = {}) {
787
953
  linkStrength = DEFAULTS.linkStrength,
788
954
  charge: chargeStrength = DEFAULTS.charge,
789
955
  gravity: gravityStrength = DEFAULTS.gravity,
790
- padding = DEFAULTS.padding
956
+ padding = DEFAULTS.padding,
957
+ groupStrength = DEFAULTS.groupStrength
791
958
  } = opts;
792
959
  const pinned = pinnedSet(graph, opts);
793
960
  const bodies = graph.nodes.map((node) => {
@@ -810,13 +977,15 @@ function forceRelaxation(graph, opts = {}) {
810
977
  x: bodies.reduce((sum, b) => sum + b.x, 0) / bodies.length,
811
978
  y: bodies.reduce((sum, b) => sum + b.y, 0) / bodies.length
812
979
  };
980
+ const groups = graph.groups ?? [];
813
981
  const sim = createSimulation({
814
982
  nodes: bodies,
815
983
  forces: [
816
984
  charge(bodies, chargeStrength),
817
985
  links(byId, graph, linkDistance, linkStrength),
818
986
  gravity(bodies, centroid, gravityStrength),
819
- separateBoxes(bodies, padding)
987
+ separateBoxes(bodies, padding),
988
+ ...groups.length > 0 ? [groupCohesion(groups, byId, groupStrength), separateGroups(groups, bodies, byId, padding)] : []
820
989
  ]
821
990
  });
822
991
  return {
@@ -920,7 +1089,12 @@ function ranksOf(graph, back) {
920
1089
  for (const node of graph.nodes) rankOf(node.id);
921
1090
  return ranks;
922
1091
  }
923
- var layered = (graph, opts = {}) => {
1092
+ var layered = (graph, opts = {}) => (
1093
+ // Barycenter ordering reorders on every run by design, so it has no
1094
+ // arrangement to settle on.
1095
+ (graph.groups?.length ?? 0) > 0 && opts.order !== "barycenter" ? untilSettled(layeredOnce, graph, opts) : layeredOnce(graph, opts)
1096
+ );
1097
+ var layeredOnce = (graph, opts = {}) => {
924
1098
  const axes = axesFor(opts.direction);
925
1099
  const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
926
1100
  const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
@@ -997,7 +1171,8 @@ function forestOf(graph, crossAxis) {
997
1171
  }
998
1172
  return { roots, children, depth };
999
1173
  }
1000
- var tree = (graph, opts = {}) => {
1174
+ var tree = (graph, opts = {}) => (graph.groups?.length ?? 0) > 0 ? untilSettled(treeOnce, graph, opts) : treeOnce(graph, opts);
1175
+ var treeOnce = (graph, opts = {}) => {
1001
1176
  const axes = axesFor(opts.direction);
1002
1177
  const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
1003
1178
  const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
@@ -1060,6 +1235,6 @@ var tree = (graph, opts = {}) => {
1060
1235
  return settle(graph, slots, opts);
1061
1236
  };
1062
1237
 
1063
- export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, extent, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, portOf, portsOf, portsOnOutline, rankGaps, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree };
1064
- //# sourceMappingURL=chunk-ERT3XWBS.js.map
1065
- //# sourceMappingURL=chunk-ERT3XWBS.js.map
1238
+ export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, extent, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, portOf, portsOf, portsOnOutline, rankGaps, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree, untilSettled };
1239
+ //# sourceMappingURL=chunk-BWQJYEA3.js.map
1240
+ //# sourceMappingURL=chunk-BWQJYEA3.js.map