@weasel-js/diagram 1.9.3 → 1.9.4

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.
@@ -1,5 +1,5 @@
1
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';
2
+ import { cssFamilyName } from '@weasel-js/font';
3
3
 
4
4
  // src/outline.ts
5
5
  var LEAN = 0.2;
@@ -487,6 +487,30 @@ function settle(graph, slots, opts = {}) {
487
487
  }
488
488
  return translated(graph, placed, pinned, tolerance);
489
489
  }
490
+ function untilSettled(once, graph, opts = {}, passes = 8) {
491
+ const first = once(graph, opts);
492
+ if (first.size === 0) return first;
493
+ const at = new Map(first);
494
+ for (let pass = 1; pass < passes; pass++) {
495
+ const nodes = graph.nodes.map((node) => {
496
+ const to = at.get(node.id);
497
+ return to === void 0 ? node : { ...node, bounds: { ...node.bounds, x: to.x, y: to.y } };
498
+ });
499
+ const byId = new Map(nodes.map((n) => [n.id, n]));
500
+ const next = once({ ...graph, nodes, node: (id) => byId.get(id) }, opts);
501
+ if (next.size === 0) break;
502
+ for (const [id, to] of next) at.set(id, to);
503
+ }
504
+ const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
505
+ const out = /* @__PURE__ */ new Map();
506
+ for (const node of graph.nodes) {
507
+ const to = at.get(node.id);
508
+ if (to === void 0) continue;
509
+ if (Math.abs(to.x - node.bounds.x) <= tolerance && Math.abs(to.y - node.bounds.y) <= tolerance) continue;
510
+ out.set(node.id, to);
511
+ }
512
+ return out;
513
+ }
490
514
  function translated(graph, placed, pinned, tolerance = DEFAULT_TOLERANCE) {
491
515
  const anchor = anchorDelta(graph, placed, pinned);
492
516
  const out = /* @__PURE__ */ new Map();
@@ -565,10 +589,14 @@ function rankGaps(graph, rows, axes, rankGap) {
565
589
  const gaps = rows.slice(1).map(() => rankGap);
566
590
  const ends = /* @__PURE__ */ new Map();
567
591
  const starts = /* @__PURE__ */ new Map();
592
+ const reach = (span, side) => {
593
+ const own = canonical(span.group.inset, axes)[side === "lo" ? "rankLo" : "rankHi"];
594
+ const flush = span.children.filter((c) => c[side] === span[side]);
595
+ return own + Math.max(0, ...flush.map((c) => reach(c, side)));
596
+ };
568
597
  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));
598
+ ends.set(span.hi, Math.max(ends.get(span.hi) ?? 0, reach(span, "hi")));
599
+ starts.set(span.lo, Math.max(starts.get(span.lo) ?? 0, reach(span, "lo")));
572
600
  }
573
601
  for (let i = 0; i < gaps.length; i++) {
574
602
  gaps[i] = Math.max(gaps[i], rankGap + (ends.get(i) ?? 0) + (starts.get(i + 1) ?? 0));
@@ -579,92 +607,125 @@ function groupSpans(graph, rows) {
579
607
  const rankOf = /* @__PURE__ */ new Map();
580
608
  rows.forEach((row, i) => row.forEach((n) => rankOf.set(n.id, i)));
581
609
  const spans = [];
610
+ const byId = /* @__PURE__ */ new Map();
582
611
  for (const group of graph.groups ?? []) {
583
612
  const ranks = group.members.map((id) => rankOf.get(id)).filter((r) => r !== void 0);
584
613
  if (ranks.length === 0) continue;
585
- spans.push({ group, lo: Math.min(...ranks), hi: Math.max(...ranks) });
614
+ const span = { group, lo: Math.min(...ranks), hi: Math.max(...ranks), children: [] };
615
+ spans.push(span);
616
+ byId.set(group.id, span);
617
+ }
618
+ for (const span of spans) {
619
+ const parent = span.group.parent === void 0 ? void 0 : byId.get(span.group.parent);
620
+ if (parent === void 0) continue;
621
+ span.parent = parent;
622
+ parent.children.push(span);
586
623
  }
587
624
  return spans;
588
625
  }
589
626
  function packClusters(graph, rows, preferred, axes, nodeGap) {
590
627
  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);
628
+ const spanById = new Map(spans.map((s) => [s.group.id, s]));
629
+ const spanOf = (n) => n.group === void 0 ? void 0 : spanById.get(n.group);
593
630
  const width = (n) => extent(n.bounds, axes.cross);
594
631
  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
632
  const insets = new Map(spans.map((s) => [s, canonical(s.group.inset, axes)]));
597
- const inner = /* @__PURE__ */ new Map();
598
633
  const centerOf = /* @__PURE__ */ new Map();
599
634
  for (const span of spans) {
600
- let widest = 0;
635
+ const inside = new Set(span.group.members);
601
636
  let sum = 0;
602
637
  let count = 0;
603
638
  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) {
639
+ for (const n of row) {
640
+ if (!inside.has(n.id)) continue;
607
641
  sum += pref(n) + width(n) / 2;
608
642
  count++;
609
643
  }
610
644
  }
611
- inner.set(span, widest);
612
645
  centerOf.set(span, count === 0 ? 0 : sum / count);
613
646
  }
614
- const outer = (span) => inner.get(span) + insets.get(span).crossLo + insets.get(span).crossHi;
615
- const itemsByRow = rows.map((row, r) => {
647
+ const itemsByRow = (owner, among) => rows.map((row, r) => {
616
648
  const items = [];
617
649
  for (const node of row) {
618
- if (!spanOf.has(node.id)) items.push({ kind: "node", node, center: pref(node) + width(node) / 2 });
650
+ if (spanOf(node) === owner) items.push({ kind: "node", node, center: pref(node) + width(node) / 2 });
619
651
  }
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
- });
652
+ for (const span of among) {
653
+ if (r >= span.lo && r <= span.hi) items.push({ kind: "group", span, center: centerOf.get(span) });
628
654
  }
629
655
  return items.map((item, i) => ({ item, i })).sort((a, b) => a.item.center - b.item.center || a.i - b.i).map(({ item }) => item);
630
656
  });
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;
657
+ const settle2 = (byRow, start2, nodeFloor, origin) => {
658
+ const nodeAt = /* @__PURE__ */ new Map();
659
+ let ends = [];
660
+ for (let pass = 0; pass <= start2.size; pass++) {
661
+ let raised = false;
662
+ ends = byRow.map((items) => {
663
+ let cursor = origin;
664
+ for (const item of items) {
665
+ if (item.kind === "node") {
666
+ const x2 = Math.max(nodeFloor(item.node), cursor);
667
+ nodeAt.set(item.node.id, x2);
668
+ cursor = x2 + width(item.node) + nodeGap;
669
+ continue;
670
+ }
671
+ const s = start2.get(item.span);
672
+ const x = Math.max(s, cursor);
673
+ if (x > s) {
674
+ start2.set(item.span, x);
675
+ raised = true;
676
+ }
677
+ cursor = x + outer(item.span) + nodeGap;
643
678
  }
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
- }
679
+ return items.length === 0 ? origin : cursor - nodeGap;
680
+ });
681
+ if (!raised) break;
658
682
  }
659
- if (!raised) break;
683
+ return { nodeAt, ends };
684
+ };
685
+ const blocks = /* @__PURE__ */ new Map();
686
+ const blockOf = (span) => {
687
+ const known = blocks.get(span);
688
+ if (known !== void 0) return known;
689
+ const byRow = itemsByRow(span, span.children);
690
+ const childAt = new Map(span.children.map((c) => [c, 0]));
691
+ const { nodeAt, ends } = settle2(byRow, childAt, () => 0, 0);
692
+ const block = { width: Math.max(0, ...ends), nodeAt, childAt };
693
+ byRow.forEach((items, r) => {
694
+ if (items.some((item) => item.kind === "group")) return;
695
+ const shift = (block.width - ends[r]) / 2;
696
+ for (const item of items) {
697
+ if (item.kind === "node") nodeAt.set(item.node.id, nodeAt.get(item.node.id) + shift);
698
+ }
699
+ });
700
+ blocks.set(span, block);
701
+ return block;
702
+ };
703
+ function outer(span) {
704
+ const inset = insets.get(span);
705
+ return blockOf(span).width + inset.crossLo + inset.crossHi;
660
706
  }
707
+ const roots = spans.filter((s) => s.parent === void 0);
708
+ const start = new Map(roots.map((s) => [s, centerOf.get(s) - outer(s) / 2]));
709
+ const { nodeAt: at } = settle2(itemsByRow(void 0, roots), start, pref, -Infinity);
710
+ const place = (span, from) => {
711
+ const origin = from + insets.get(span).crossLo;
712
+ const block = blockOf(span);
713
+ for (const [id, local] of block.nodeAt) at.set(id, origin + local);
714
+ for (const [child, local] of block.childAt) place(child, origin + local);
715
+ };
716
+ for (const root of roots) place(root, start.get(root));
661
717
  return at;
662
718
  }
663
719
  function expandGroupEdges(edges, groups) {
664
720
  const byId = new Map(groups.map((g) => [g.id, g]));
665
721
  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);
722
+ const within = (id, group) => {
723
+ if (!byId.has(id)) return group.members.includes(id);
724
+ for (let at = byId.get(id); at !== void 0; at = at.parent === void 0 ? void 0 : byId.get(at.parent)) {
725
+ if (at === group) return true;
726
+ }
727
+ return false;
728
+ };
668
729
  const entries = /* @__PURE__ */ new Map();
669
730
  const exits = /* @__PURE__ */ new Map();
670
731
  for (const g of groups) {
@@ -690,9 +751,8 @@ function expandGroupEdges(edges, groups) {
690
751
  out.push(e);
691
752
  continue;
692
753
  }
693
- const fromOwner = fromGroup ?? groupOf.get(e.from);
694
- const toOwner = toGroup ?? groupOf.get(e.to);
695
- if (fromOwner !== void 0 && fromOwner === toOwner) continue;
754
+ if (fromGroup !== void 0 && within(e.to, byId.get(fromGroup))) continue;
755
+ if (toGroup !== void 0 && within(e.from, byId.get(toGroup))) continue;
696
756
  const froms = fromGroup !== void 0 ? exits.get(fromGroup) : [e.from];
697
757
  const tos = toGroup !== void 0 ? entries.get(toGroup) : [e.to];
698
758
  let k = 0;
@@ -702,13 +762,118 @@ function expandGroupEdges(edges, groups) {
702
762
  }
703
763
  return out;
704
764
  }
765
+
766
+ // src/forceGroups.ts
767
+ function resolve(groups, byId) {
768
+ const out = [];
769
+ for (const group of groups) {
770
+ const members = group.members.map((id) => byId.get(id)).filter((b) => b !== void 0);
771
+ if (members.length > 0) out.push({ members, group, children: [] });
772
+ }
773
+ const byGroup = new Map(out.map((g) => [g.group.id, g]));
774
+ for (const g of out) {
775
+ const parent = g.group.parent === void 0 ? void 0 : byGroup.get(g.group.parent);
776
+ if (parent === void 0) continue;
777
+ g.parent = parent;
778
+ parent.children.push(g);
779
+ }
780
+ return out;
781
+ }
782
+ function groupCohesion(groups, byId, strength) {
783
+ const resolved = resolve(groups, byId);
784
+ return (alpha) => {
785
+ for (const { members } of resolved) {
786
+ let cx = 0;
787
+ let cy = 0;
788
+ for (const m of members) {
789
+ cx += m.x;
790
+ cy += m.y;
791
+ }
792
+ cx /= members.length;
793
+ cy /= members.length;
794
+ for (const m of members) {
795
+ m.vx += (cx - m.x) * strength * alpha;
796
+ m.vy += (cy - m.y) * strength * alpha;
797
+ }
798
+ }
799
+ };
800
+ }
801
+ function boxOf(g) {
802
+ let x0 = Infinity;
803
+ let y0 = Infinity;
804
+ let x1 = -Infinity;
805
+ let y1 = -Infinity;
806
+ for (const m of g.members) {
807
+ x0 = Math.min(x0, m.x - m.hw);
808
+ y0 = Math.min(y0, m.y - m.hh);
809
+ x1 = Math.max(x1, m.x + m.hw);
810
+ y1 = Math.max(y1, m.y + m.hh);
811
+ }
812
+ for (const child of g.children) {
813
+ const b = boxOf(child);
814
+ x0 = Math.min(x0, b.x0);
815
+ y0 = Math.min(y0, b.y0);
816
+ x1 = Math.max(x1, b.x1);
817
+ y1 = Math.max(y1, b.y1);
818
+ }
819
+ const { inset } = g.group;
820
+ return { x0: x0 - inset.left, y0: y0 - inset.top, x1: x1 + inset.right, y1: y1 + inset.bottom };
821
+ }
822
+ function clearance(a, b, padding) {
823
+ const gapX = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0) + padding;
824
+ const gapY = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0) + padding;
825
+ if (gapX <= 0 || gapY <= 0) return null;
826
+ const dx = b.x0 + b.x1 - (a.x0 + a.x1);
827
+ const dy = b.y0 + b.y1 - (a.y0 + a.y1);
828
+ return gapX < gapY ? { x: (Math.sign(dx) || 1) * gapX, y: 0 } : { x: 0, y: (Math.sign(dy) || 1) * gapY };
829
+ }
830
+ function holds(outer, inner) {
831
+ for (let at = inner.parent; at !== void 0; at = at.parent) {
832
+ if (at === outer) return true;
833
+ }
834
+ return false;
835
+ }
836
+ function shove(bodies, x, y) {
837
+ for (const body of bodies) {
838
+ body.vx += x;
839
+ body.vy += y;
840
+ }
841
+ }
842
+ function separateGroups(groups, bodies, byId, padding) {
843
+ const resolved = resolve(groups, byId);
844
+ const memberSets = resolved.map((g) => new Set(g.members));
845
+ return () => {
846
+ const boxes = resolved.map(boxOf);
847
+ for (let i = 0; i < resolved.length; i++) {
848
+ for (let j = i + 1; j < resolved.length; j++) {
849
+ if (holds(resolved[i], resolved[j]) || holds(resolved[j], resolved[i])) continue;
850
+ const push = clearance(boxes[i], boxes[j], padding);
851
+ if (push === null) continue;
852
+ shove(resolved[i].members, -push.x / 2, -push.y / 2);
853
+ shove(resolved[j].members, push.x / 2, push.y / 2);
854
+ }
855
+ for (const body of bodies) {
856
+ if (memberSets[i].has(body)) continue;
857
+ const box = { x0: body.x - body.hw, y0: body.y - body.hh, x1: body.x + body.hw, y1: body.y + body.hh };
858
+ const push = clearance(boxes[i], box, padding);
859
+ if (push === null) continue;
860
+ shove(resolved[i].members, -push.x / 2, -push.y / 2);
861
+ body.vx += push.x / 2;
862
+ body.vy += push.y / 2;
863
+ }
864
+ }
865
+ };
866
+ }
867
+
868
+ // src/force.ts
705
869
  var DEFAULTS = {
706
870
  iterations: 300,
707
871
  linkDistance: 160,
708
872
  linkStrength: 0.35,
709
873
  charge: -3e3,
710
874
  gravity: 0.02,
711
- padding: 24
875
+ padding: 24,
876
+ groupStrength: 0.2
712
877
  };
713
878
  var GOLDEN = Math.PI * (3 - Math.sqrt(5));
714
879
  var NUDGE = 0.5;
@@ -787,7 +952,8 @@ function forceRelaxation(graph, opts = {}) {
787
952
  linkStrength = DEFAULTS.linkStrength,
788
953
  charge: chargeStrength = DEFAULTS.charge,
789
954
  gravity: gravityStrength = DEFAULTS.gravity,
790
- padding = DEFAULTS.padding
955
+ padding = DEFAULTS.padding,
956
+ groupStrength = DEFAULTS.groupStrength
791
957
  } = opts;
792
958
  const pinned = pinnedSet(graph, opts);
793
959
  const bodies = graph.nodes.map((node) => {
@@ -810,13 +976,15 @@ function forceRelaxation(graph, opts = {}) {
810
976
  x: bodies.reduce((sum, b) => sum + b.x, 0) / bodies.length,
811
977
  y: bodies.reduce((sum, b) => sum + b.y, 0) / bodies.length
812
978
  };
979
+ const groups = graph.groups ?? [];
813
980
  const sim = createSimulation({
814
981
  nodes: bodies,
815
982
  forces: [
816
983
  charge(bodies, chargeStrength),
817
984
  links(byId, graph, linkDistance, linkStrength),
818
985
  gravity(bodies, centroid, gravityStrength),
819
- separateBoxes(bodies, padding)
986
+ separateBoxes(bodies, padding),
987
+ ...groups.length > 0 ? [groupCohesion(groups, byId, groupStrength), separateGroups(groups, bodies, byId, padding)] : []
820
988
  ]
821
989
  });
822
990
  return {
@@ -920,7 +1088,12 @@ function ranksOf(graph, back) {
920
1088
  for (const node of graph.nodes) rankOf(node.id);
921
1089
  return ranks;
922
1090
  }
923
- var layered = (graph, opts = {}) => {
1091
+ var layered = (graph, opts = {}) => (
1092
+ // Barycenter ordering reorders on every run by design, so it has no
1093
+ // arrangement to settle on.
1094
+ (graph.groups?.length ?? 0) > 0 && opts.order !== "barycenter" ? untilSettled(layeredOnce, graph, opts) : layeredOnce(graph, opts)
1095
+ );
1096
+ var layeredOnce = (graph, opts = {}) => {
924
1097
  const axes = axesFor(opts.direction);
925
1098
  const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
926
1099
  const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
@@ -997,7 +1170,8 @@ function forestOf(graph, crossAxis) {
997
1170
  }
998
1171
  return { roots, children, depth };
999
1172
  }
1000
- var tree = (graph, opts = {}) => {
1173
+ var tree = (graph, opts = {}) => (graph.groups?.length ?? 0) > 0 ? untilSettled(treeOnce, graph, opts) : treeOnce(graph, opts);
1174
+ var treeOnce = (graph, opts = {}) => {
1001
1175
  const axes = axesFor(opts.direction);
1002
1176
  const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
1003
1177
  const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
@@ -1060,6 +1234,6 @@ var tree = (graph, opts = {}) => {
1060
1234
  return settle(graph, slots, opts);
1061
1235
  };
1062
1236
 
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
1237
+ 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 };
1238
+ //# sourceMappingURL=chunk-EDSBULQM.js.map
1239
+ //# sourceMappingURL=chunk-EDSBULQM.js.map