@weasel-js/diagram 1.11.0 → 1.11.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.
@@ -1,5 +1,5 @@
1
- import { AUTO_POSE_DESCRIPTOR, rotatePoint, rectPath, polygonFromPoints, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, PathBuilder } from '@weasel-js/core/math';
2
- import { transformPath } from '@weasel-js/geom';
1
+ import { AUTO_POSE_DESCRIPTOR, rotatePoint, rectPath, polygonFromPoints, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, PathBuilder, pointAlongPath } from '@weasel-js/core/math';
2
+ import { transformPath, ellipsePath, PATH_M as PATH_M$1, PATH_CMD_LENGTHS, measurePath, forEachSegment, PATH_L as PATH_L$1, nearestOnLine, PATH_Q as PATH_Q$1, nearestOnQuadratic, PATH_C as PATH_C$1, nearestOnCubic } from '@weasel-js/geom';
3
3
  import { cssFamilyName } from '@weasel-js/font/metrics';
4
4
  import { createSimulation } from '@weasel-js/simulation';
5
5
 
@@ -48,6 +48,8 @@ function outlinePath(outline, bounds) {
48
48
  ]);
49
49
  case "stadium":
50
50
  return stadiumPath(x, y, w, h);
51
+ case "ellipse":
52
+ return ellipsePath(bounds);
51
53
  }
52
54
  }
53
55
  function stadiumPath(x, y, w, h) {
@@ -85,6 +87,11 @@ function contentBox(outline, bounds) {
85
87
  return { x, y: y + h / 4, width: w, height: h / 2 };
86
88
  case "hexagon":
87
89
  return { x: x + w * POINT, y, width: w * (1 - POINT * 2), height: h };
90
+ case "ellipse": {
91
+ const cw = w / Math.SQRT2;
92
+ const ch = h / Math.SQRT2;
93
+ return { x: x + (w - cw) / 2, y: y + (h - ch) / 2, width: cw, height: ch };
94
+ }
88
95
  case "stadium": {
89
96
  const r = Math.min(w, h) / 2;
90
97
  return w >= h ? { x: x + r, y, width: Math.max(w - r * 2, 0), height: h } : { x, y: y + r, width: w, height: Math.max(h - r * 2, 0) };
@@ -106,6 +113,8 @@ function boxForContent(outline, content) {
106
113
  return { width: content.width / (1 - POINT * 2), height: content.height };
107
114
  case "stadium":
108
115
  return { width: content.width + content.height, height: content.height };
116
+ case "ellipse":
117
+ return { width: content.width * Math.SQRT2, height: content.height * Math.SQRT2 };
109
118
  }
110
119
  }
111
120
  var TOLERANCE = 0.25;
@@ -217,6 +226,7 @@ var dataKeyReader = (node) => {
217
226
  if (trait === null || typeof trait !== "object") return null;
218
227
  if ("from" in trait && "to" in trait) return null;
219
228
  if ("label" in trait || "anchor" in trait || "mark" in trait) return null;
229
+ if (trait.container?.role === "lane") return null;
220
230
  return trait;
221
231
  };
222
232
  function createDiagramNodes(entries) {
@@ -735,6 +745,10 @@ function crossInset(inset, axes) {
735
745
  const c = canonical(inset, axes);
736
746
  return { lo: c.crossLo, hi: c.crossHi };
737
747
  }
748
+ function rankInset(inset, axes) {
749
+ const c = canonical(inset, axes);
750
+ return { lo: c.rankLo, hi: c.rankHi };
751
+ }
738
752
  function rankGaps(graph, rows, axes, rankGap) {
739
753
  const gaps = rows.slice(1).map(() => rankGap);
740
754
  const ends2 = /* @__PURE__ */ new Map();
@@ -922,6 +936,181 @@ function expandGroupEdges(edges, groups) {
922
936
  }
923
937
  return out;
924
938
  }
939
+ function subpathOf(path, index) {
940
+ if (path.kind !== "polygon") return index === 0 ? path : null;
941
+ const { commands, coords } = path;
942
+ let seen = -1;
943
+ let start = -1;
944
+ let startCoord = 0;
945
+ let coord2 = 0;
946
+ for (let i = 0; i <= commands.length; i++) {
947
+ const cmd = commands[i];
948
+ if (i === commands.length || cmd === PATH_M$1) {
949
+ if (seen === index) {
950
+ if (start === 0 && i === commands.length) return path;
951
+ return { kind: "polygon", fillRule: path.fillRule, commands: commands.slice(start, i), coords: coords.slice(startCoord, coord2) };
952
+ }
953
+ seen++;
954
+ start = i;
955
+ startCoord = coord2;
956
+ }
957
+ if (cmd !== void 0) coord2 += PATH_CMD_LENGTHS[cmd] ?? 0;
958
+ }
959
+ return null;
960
+ }
961
+ var NAMED = { start: 0, mid: 0.5, end: 1 };
962
+ function nearestStation(path, near) {
963
+ if (path.kind !== "polygon") return null;
964
+ const { commands, coords } = path;
965
+ let best = null;
966
+ let heading = { x: 1, y: 0 };
967
+ forEachSegment(commands, coords, (cmd, ci, x0, y0) => {
968
+ let hit;
969
+ let d;
970
+ if (cmd === PATH_L$1) {
971
+ hit = nearestOnLine(near.x, near.y, x0, y0, coords[ci], coords[ci + 1]);
972
+ d = { x: coords[ci] - x0, y: coords[ci + 1] - y0 };
973
+ } else if (cmd === PATH_Q$1) {
974
+ hit = nearestOnQuadratic(near.x, near.y, x0, y0, coords[ci], coords[ci + 1], coords[ci + 2], coords[ci + 3]);
975
+ const t = hit.t;
976
+ d = {
977
+ x: 2 * (1 - t) * (coords[ci] - x0) + 2 * t * (coords[ci + 2] - coords[ci]),
978
+ y: 2 * (1 - t) * (coords[ci + 1] - y0) + 2 * t * (coords[ci + 3] - coords[ci + 1])
979
+ };
980
+ } else if (cmd === PATH_C$1) {
981
+ hit = nearestOnCubic(
982
+ near.x,
983
+ near.y,
984
+ x0,
985
+ y0,
986
+ coords[ci],
987
+ coords[ci + 1],
988
+ coords[ci + 2],
989
+ coords[ci + 3],
990
+ coords[ci + 4],
991
+ coords[ci + 5]
992
+ );
993
+ const t = hit.t;
994
+ const u = 1 - t;
995
+ d = {
996
+ x: 3 * u * u * (coords[ci] - x0) + 6 * u * t * (coords[ci + 2] - coords[ci]) + 3 * t * t * (coords[ci + 4] - coords[ci + 2]),
997
+ y: 3 * u * u * (coords[ci + 1] - y0) + 6 * u * t * (coords[ci + 3] - coords[ci + 1]) + 3 * t * t * (coords[ci + 5] - coords[ci + 3])
998
+ };
999
+ } else return;
1000
+ const len = Math.hypot(d.x, d.y);
1001
+ if (len === 0 || best !== null && hit.dist >= best.dist) return;
1002
+ best = hit;
1003
+ heading = { x: d.x / len, y: d.y / len };
1004
+ });
1005
+ const found = best;
1006
+ return found === null ? null : { point: { x: found.x, y: found.y }, tangent: heading };
1007
+ }
1008
+ function labelCenter(whole, label, size) {
1009
+ const path = subpathOf(whole, 0) ?? whole;
1010
+ const at2 = label.placed ?? label;
1011
+ const station = typeof at2.at === "object" ? nearestStation(path, at2.at.near) ?? pointAlongPath(path, 0.5) : pointAlongPath(path, typeof at2.at === "number" ? at2.at : NAMED[at2.at] ?? 0.5);
1012
+ if (station === null) return null;
1013
+ return besideStation(station.point, station.tangent, at2.offset ?? 0, size);
1014
+ }
1015
+ function halfDepth(tangent, size) {
1016
+ return (Math.abs(tangent.y) * size.width + Math.abs(tangent.x) * size.height) / 2;
1017
+ }
1018
+ function besideStation(point, tangent, offset, size) {
1019
+ const reach = offset + Math.sign(offset) * halfDepth(tangent, size);
1020
+ return { x: point.x + tangent.y * reach, y: point.y - tangent.x * reach };
1021
+ }
1022
+ function offsetFrom(point, tangent, center2, size) {
1023
+ const d = (center2.x - point.x) * tangent.y - (center2.y - point.y) * tangent.x;
1024
+ const offset = Math.sign(d) * Math.max(Math.abs(d) - halfDepth(tangent, size), 0);
1025
+ return offset === 0 ? 0 : offset;
1026
+ }
1027
+ var NUDGE = 1e-9;
1028
+ function labelSpotAt(whole, center2, size) {
1029
+ const measure = measurePath(subpathOf(whole, 0) ?? whole);
1030
+ if (measure === null) return null;
1031
+ const station = measure.nearest(center2);
1032
+ if (measure.length === 0) return { at: 0.5, offset: offsetFrom(station.point, station.tangent, center2, size) };
1033
+ const at2 = station.s / measure.length;
1034
+ const step = NUDGE * measure.length;
1035
+ const before = measure.at(station.s - step).tangent;
1036
+ const after = measure.at(station.s + step).tangent;
1037
+ if (before.x === after.x && before.y === after.y) {
1038
+ return { at: at2, offset: offsetFrom(station.point, station.tangent, center2, size) };
1039
+ }
1040
+ const reads = measure.at(at2 * measure.length).tangent;
1041
+ let best = { at: at2, offset: 0 };
1042
+ let bestDist = Infinity;
1043
+ for (const [tangent, side] of [[before, -1], [after, 1]]) {
1044
+ const offset = offsetFrom(station.point, tangent, center2, size);
1045
+ const back = besideStation(station.point, tangent, offset, size);
1046
+ const dist = Math.hypot(back.x - center2.x, back.y - center2.y);
1047
+ if (dist >= bestDist) continue;
1048
+ const onLeg = reads.x === tangent.x && reads.y === tangent.y;
1049
+ best = { at: onLeg ? at2 : at2 + side * NUDGE, offset };
1050
+ bestDist = dist;
1051
+ }
1052
+ return best;
1053
+ }
1054
+
1055
+ // src/edgeTrait.ts
1056
+ function layoutOwnsPort(end) {
1057
+ return end.port === void 0 || end.port === end.laidOut;
1058
+ }
1059
+ function layoutOwnsRoute(edge) {
1060
+ const points = edge.waypoints ?? [];
1061
+ if (points.length === 0) return true;
1062
+ return edge.laidOut !== void 0 && samePoints(points, edge.laidOut);
1063
+ }
1064
+ function layoutWritesRoute(edge) {
1065
+ return edge.router !== "avoid" && layoutOwnsRoute(edge);
1066
+ }
1067
+ function samePoints(a, b) {
1068
+ return a.length === b.length && a.every((p, i) => p.x === b[i].x && p.y === b[i].y);
1069
+ }
1070
+ function diagramEdgeOf(node) {
1071
+ const data = node.data;
1072
+ if (data === null || typeof data !== "object") return null;
1073
+ const edge = data.diagram;
1074
+ if (edge === null || typeof edge !== "object") return null;
1075
+ return "from" in edge && "to" in edge ? edge : null;
1076
+ }
1077
+
1078
+ // src/avoid/holds.ts
1079
+ var EPS2 = 1e-6;
1080
+ function runsAlong(d, normal, sign) {
1081
+ const len = Math.hypot(d.x, d.y) * Math.hypot(normal.x, normal.y);
1082
+ if (len === 0) return false;
1083
+ const cross = d.x * normal.y - d.y * normal.x;
1084
+ return Math.abs(cross) <= EPS2 * len && sign * (d.x * normal.x + d.y * normal.y) > 0;
1085
+ }
1086
+ function onSegment(p, a, b) {
1087
+ const dx = b.x - a.x;
1088
+ const dy = b.y - a.y;
1089
+ const len2 = dx * dx + dy * dy;
1090
+ const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
1091
+ return Math.hypot(a.x + t * dx - p.x, a.y + t * dy - p.y) <= EPS2 * Math.max(1, Math.sqrt(len2)) ? t : null;
1092
+ }
1093
+ function routeHolds(from, to, waypoints, route) {
1094
+ const points = [from.point, ...route, to.point];
1095
+ const n = points.length;
1096
+ const first = { x: points[1].x - points[0].x, y: points[1].y - points[0].y };
1097
+ const last = { x: points[n - 1].x - points[n - 2].x, y: points[n - 1].y - points[n - 2].y };
1098
+ if (from.normal !== null && !runsAlong(first, from.normal, 1)) return false;
1099
+ if (to.normal !== null && !runsAlong(last, to.normal, -1)) return false;
1100
+ let seg = 0;
1101
+ let along = 0;
1102
+ for (const w of waypoints) {
1103
+ for (; seg < n - 1; seg++, along = 0) {
1104
+ const t = onSegment(w, points[seg], points[seg + 1]);
1105
+ if (t !== null && t >= along) {
1106
+ along = t;
1107
+ break;
1108
+ }
1109
+ }
1110
+ if (seg === n - 1) return false;
1111
+ }
1112
+ return true;
1113
+ }
925
1114
 
926
1115
  // src/forceGroups.ts
927
1116
  function resolve(groups, byId) {
@@ -1036,7 +1225,7 @@ var DEFAULTS = {
1036
1225
  groupStrength: 0.2
1037
1226
  };
1038
1227
  var GOLDEN = Math.PI * (3 - Math.sqrt(5));
1039
- var NUDGE = 0.5;
1228
+ var NUDGE2 = 0.5;
1040
1229
  function charge(bodies, strength) {
1041
1230
  return (alpha) => {
1042
1231
  for (let i = 0; i < bodies.length; i++) {
@@ -1179,7 +1368,7 @@ function separate(bodies) {
1179
1368
  seen.set(key, nth + 1);
1180
1369
  if (nth === 0 || body.fx != null || body.fy != null) return;
1181
1370
  const angle = GOLDEN * i;
1182
- const radius = NUDGE * nth;
1371
+ const radius = NUDGE2 * nth;
1183
1372
  body.x += Math.cos(angle) * radius;
1184
1373
  body.y += Math.sin(angle) * radius;
1185
1374
  });
@@ -1786,8 +1975,18 @@ function chainsOf(groups) {
1786
1975
  };
1787
1976
  return (group) => group === void 0 ? [] : chain2(group, /* @__PURE__ */ new Set());
1788
1977
  }
1789
- var newBlock = (first) => ({ items: [], sub: /* @__PURE__ */ new Map(), sum: 0, count: 0, first });
1790
- function sortRank(row, key, chainOf) {
1978
+ var newBlock = (first, group) => ({
1979
+ ...group !== void 0 ? { group } : {},
1980
+ items: [],
1981
+ sub: /* @__PURE__ */ new Map(),
1982
+ sum: 0,
1983
+ count: 0,
1984
+ first
1985
+ });
1986
+ function placesOf(groups) {
1987
+ return new Map(groups.filter((g) => g.ordered !== void 0).map((g) => [g.id, new Map(g.ordered.map((m, i) => [m, i]))]));
1988
+ }
1989
+ function sortRank(row, key, chainOf, places = /* @__PURE__ */ new Map()) {
1791
1990
  const root = newBlock(0);
1792
1991
  const keyOf = /* @__PURE__ */ new Map();
1793
1992
  const firstOf = /* @__PURE__ */ new Map();
@@ -1797,7 +1996,7 @@ function sortRank(row, key, chainOf) {
1797
1996
  for (const g of chainOf(n.group)) {
1798
1997
  let next = at2.sub.get(g);
1799
1998
  if (next === void 0) {
1800
- next = newBlock(i);
1999
+ next = newBlock(i, g);
1801
2000
  at2.sub.set(g, next);
1802
2001
  at2.items.push(next);
1803
2002
  }
@@ -1811,7 +2010,12 @@ function sortRank(row, key, chainOf) {
1811
2010
  });
1812
2011
  const out = [];
1813
2012
  const flatten = (block) => {
2013
+ const place = block.group === void 0 ? void 0 : places.get(block.group);
1814
2014
  const items = block.items.map((item) => {
2015
+ if (place !== void 0) {
2016
+ const k = place.get("id" in item ? item.id : item.group) ?? Infinity;
2017
+ return { item, k, first: "id" in item ? firstOf.get(item) : item.first };
2018
+ }
1815
2019
  if ("id" in item) return { item, k: keyOf.get(item), first: firstOf.get(item) };
1816
2020
  return { item, k: item.sum / item.count, first: item.first };
1817
2021
  });
@@ -1837,7 +2041,7 @@ function reorder(work, r, side) {
1837
2041
  if (near === void 0 || near.length === 0) return i;
1838
2042
  return near.reduce((s, m) => s + landing(m, n.id, work) - 0.5, 0) / near.length;
1839
2043
  };
1840
- work.rows[r] = sortRank(work.rows[r], key, work.chainOf);
2044
+ work.rows[r] = sortRank(work.rows[r], key, work.chainOf, work.places);
1841
2045
  reindex(work, r);
1842
2046
  }
1843
2047
  function between(work, side, u, v) {
@@ -1858,7 +2062,7 @@ function transpose(work) {
1858
2062
  for (let i = 0; i + 1 < row.length; i++) {
1859
2063
  const u = row[i];
1860
2064
  const v = row[i + 1];
1861
- if (u.group !== v.group) continue;
2065
+ if (u.group !== v.group || u.group !== void 0 && work.places.has(u.group)) continue;
1862
2066
  const now = cost(u.id, v.id);
1863
2067
  pos.set(v.id, i);
1864
2068
  pos.set(u.id, i + 1);
@@ -1878,6 +2082,7 @@ function orderRows(rows, hops, opts = {}) {
1878
2082
  const patience = opts.patience ?? 4;
1879
2083
  const maxSweeps = opts.maxSweeps ?? 24;
1880
2084
  const chainOf = chainsOf(opts.groups ?? []);
2085
+ const places = placesOf(opts.groups ?? []);
1881
2086
  const byRank = hopsByRank(rows, hops);
1882
2087
  const ups = /* @__PURE__ */ new Map();
1883
2088
  const downs = /* @__PURE__ */ new Map();
@@ -1887,9 +2092,9 @@ function orderRows(rows, hops, opts = {}) {
1887
2092
  (ups.get(p.to) ?? ups.set(p.to, []).get(p.to)).push({ id: p.from, frac: p.fromAt, places: p.fromPlaces, side: "down" });
1888
2093
  }
1889
2094
  }
1890
- const seeded = rows.map((row) => sortRank(row, (_, i) => i, chainOf));
2095
+ const seeded = rows.map((row) => sortRank(row, (_, i) => i, chainOf, places));
1891
2096
  const sides = sidesOf(hops);
1892
- const work = { rows: seeded, pos: positions(seeded), ups, downs, sides, chainOf };
2097
+ const work = { rows: seeded, pos: positions(seeded), ups, downs, sides, chainOf, places };
1893
2098
  let best = seeded.map((row) => [...row]);
1894
2099
  let fewest = total(work.rows, byRank, work.pos, sides);
1895
2100
  for (let sweep = 0, stale = 0; sweep < maxSweeps && stale < patience && fewest > 0; sweep++) {
@@ -1975,7 +2180,7 @@ function around(a, b, others, axes, rankGap) {
1975
2180
  }
1976
2181
 
1977
2182
  // src/layered/across.ts
1978
- var EPS2 = 1e-6;
2183
+ var EPS3 = 1e-6;
1979
2184
  function settleAcross(input) {
1980
2185
  const { graph, rows, axes } = input;
1981
2186
  const groups = new Map((graph.groups ?? []).map((g) => [g.id, g]));
@@ -2034,7 +2239,7 @@ function squeeze(items, order, xs, gap) {
2034
2239
  for (const list of order) {
2035
2240
  let edge = -Infinity;
2036
2241
  for (const i of list) {
2037
- if (free(i) && xs[i] < edge - EPS2) [xs[i], moved] = [edge, true];
2242
+ if (free(i) && xs[i] < edge - EPS3) [xs[i], moved] = [edge, true];
2038
2243
  edge = Math.max(edge, xs[i] + items[i].width + gap);
2039
2244
  }
2040
2245
  }
@@ -2045,7 +2250,7 @@ function squeeze(items, order, xs, gap) {
2045
2250
  let edge = Infinity;
2046
2251
  for (const i of [...list].reverse()) {
2047
2252
  const most = edge - items[i].width - gap;
2048
- if (free(i) && xs[i] > most + EPS2) [xs[i], moved] = [most, true];
2253
+ if (free(i) && xs[i] > most + EPS3) [xs[i], moved] = [most, true];
2049
2254
  edge = Math.min(edge, xs[i]);
2050
2255
  }
2051
2256
  }
@@ -2100,7 +2305,7 @@ function clear(items, list, xs, gap) {
2100
2305
  for (const j of list.slice(k + 1)) {
2101
2306
  const pin = (k2) => items[k2].fixed !== void 0 && items[k2].group === void 0;
2102
2307
  if (pin(i) && pin(j)) continue;
2103
- const apart = xs[j] >= xs[i] + items[i].width + gap - EPS2 || xs[i] >= xs[j] + items[j].width + gap - EPS2;
2308
+ const apart = xs[j] >= xs[i] + items[i].width + gap - EPS3 || xs[i] >= xs[j] + items[j].width + gap - EPS3;
2104
2309
  if (!apart) return false;
2105
2310
  }
2106
2311
  }
@@ -2118,7 +2323,7 @@ function jump(items, list, xs, gap) {
2118
2323
  for (let moved = true; moved; ) {
2119
2324
  moved = false;
2120
2325
  for (const b of blocks) {
2121
- if (x < b.hi - EPS2 && x + width > b.lo + EPS2) [x, moved] = [dir === 1 ? b.hi : b.lo - width, true];
2326
+ if (x < b.hi - EPS3 && x + width > b.lo + EPS3) [x, moved] = [dir === 1 ? b.hi : b.lo - width, true];
2122
2327
  }
2123
2328
  }
2124
2329
  out[i] = x;
@@ -2134,7 +2339,7 @@ function jump(items, list, xs, gap) {
2134
2339
  }
2135
2340
 
2136
2341
  // src/layered/bands.ts
2137
- var EPS3 = 1e-6;
2342
+ var EPS4 = 1e-6;
2138
2343
  function placeBands(input, covers) {
2139
2344
  const { natural, minGaps } = input;
2140
2345
  const n = natural.length;
@@ -2143,7 +2348,7 @@ function placeBands(input, covers) {
2143
2348
  for (let k = 0, lo = -Infinity; k < n; k++) {
2144
2349
  earliest.push(lo);
2145
2350
  const cover = covers.get(k);
2146
- if (cover !== void 0 && lo > cover.lo + EPS3) return void 0;
2351
+ if (cover !== void 0 && lo > cover.lo + EPS4) return void 0;
2147
2352
  lo = hiOf(k, lo) + (minGaps[k] ?? 0);
2148
2353
  }
2149
2354
  const los = preferred(input, covers, hiOf).map((lo, k) => Math.max(lo, earliest[k]));
@@ -2185,7 +2390,7 @@ function spread(input, covers, los, hiOf) {
2185
2390
  const share = (k) => {
2186
2391
  if (room >= usual) return gaps[k] + (usual > 0 ? (room - usual) * gaps[k] / usual : (room - usual) / ks.length);
2187
2392
  const slack = usual - least;
2188
- return minGaps[k] + (slack > EPS3 ? (room - least) * (gaps[k] - minGaps[k]) / slack : (room - least) / ks.length);
2393
+ return minGaps[k] + (slack > EPS4 ? (room - least) * (gaps[k] - minGaps[k]) / slack : (room - least) / ks.length);
2189
2394
  };
2190
2395
  for (let k = f + 1; k < g; k++) los[k] = hiOf(k - 1, los[k - 1]) + share(k - 1);
2191
2396
  }
@@ -2490,12 +2695,12 @@ function planOnce(graph, opts = {}) {
2490
2695
  spaced[r] = spaced[r] - rankGap * (1 - LABEL_GAP);
2491
2696
  }
2492
2697
  const pinned = pinnedSet(graph, opts);
2493
- const { crossAt, gaps, depths, rankAt, unheld } = holdPins({ graph, rows, crossAt: packed, gaps: spaced, pinned, axes, nodeGap, minGaps });
2698
+ const { crossAt, gaps, depths: depths2, rankAt, unheld } = holdPins({ graph, rows, crossAt: packed, gaps: spaced, pinned, axes, nodeGap, minGaps });
2494
2699
  const slots = /* @__PURE__ */ new Map();
2495
2700
  const bands = [];
2496
2701
  let along = 0;
2497
2702
  for (const [i, row] of rows.entries()) {
2498
- const depth = depths[i];
2703
+ const depth = depths2[i];
2499
2704
  bands.push([along, along + depth]);
2500
2705
  for (const node of row) {
2501
2706
  slots.set(node.id, { cross: crossAt.get(node.id), rank: rankAt.get(node.id) ?? along + (depth - extent(node.bounds, axes.rank)) / 2 });
@@ -2557,6 +2762,154 @@ var layered = Object.assign(
2557
2762
  { plan: layeredPlan }
2558
2763
  );
2559
2764
 
2765
+ // src/chainGroups.ts
2766
+ function depths(groups) {
2767
+ const byId = new Map(groups.map((g) => [g.id, g]));
2768
+ const depth = /* @__PURE__ */ new Map();
2769
+ const of = (g) => {
2770
+ if (!depth.has(g.id)) {
2771
+ const parent = g.parent === void 0 ? void 0 : byId.get(g.parent);
2772
+ depth.set(g.id, parent === void 0 ? 0 : of(parent) + 1);
2773
+ }
2774
+ return depth.get(g.id);
2775
+ };
2776
+ groups.forEach(of);
2777
+ return depth;
2778
+ }
2779
+ var deepestFirst = (groups) => {
2780
+ const depth = depths(groups);
2781
+ return [...groups].sort((p, q) => depth.get(q.id) - depth.get(p.id));
2782
+ };
2783
+ function groupHanging(graph, line, hanging) {
2784
+ const groups = graph.groups ?? [];
2785
+ if (groups.length === 0) return hanging;
2786
+ const index = new Map(line.map((n, i) => [n.id, i]));
2787
+ const byId = new Map(groups.map((g) => [g.id, g]));
2788
+ const onLine = new Map(groups.map((g) => [g.id, g.members.filter((m) => index.has(m))]));
2789
+ const owner = new Map(hanging.owner);
2790
+ for (const node of graph.nodes) {
2791
+ if (index.has(node.id)) continue;
2792
+ const first = index.get(hanging.owner.get(node.id));
2793
+ for (let g = node.group === void 0 ? void 0 : byId.get(node.group); g !== void 0; g = g.parent === void 0 ? void 0 : byId.get(g.parent)) {
2794
+ const members = onLine.get(g.id);
2795
+ if (members.length === 0) continue;
2796
+ owner.set(node.id, nearest(members, first, index));
2797
+ break;
2798
+ }
2799
+ }
2800
+ const ordered = deepestFirst(groups);
2801
+ for (const g of ordered) {
2802
+ if (onLine.get(g.id).length > 0) continue;
2803
+ const hung = g.members.filter((m) => owner.has(m));
2804
+ if (hung.length === 0) continue;
2805
+ const counts = /* @__PURE__ */ new Map();
2806
+ for (const m of hung) counts.set(owner.get(m), (counts.get(owner.get(m)) ?? 0) + 1);
2807
+ let best = owner.get(hung[0]);
2808
+ for (const [id, n] of counts) {
2809
+ const lead = counts.get(best);
2810
+ if (n > lead || n === lead && index.get(id) < index.get(best)) best = id;
2811
+ }
2812
+ for (const m of hung) owner.set(m, best);
2813
+ }
2814
+ return { owner, level: clearColumns(graph, ordered, index, owner, hanging.level) };
2815
+ }
2816
+ function nearest(members, from, index) {
2817
+ let best = members[0];
2818
+ for (const m of members) {
2819
+ const d = Math.abs(index.get(m) - from);
2820
+ const lead = Math.abs(index.get(best) - from);
2821
+ if (d < lead || d === lead && index.get(m) < index.get(best)) best = m;
2822
+ }
2823
+ return best;
2824
+ }
2825
+ function clearColumns(graph, ordered, index, owner, start) {
2826
+ const level = new Map(start);
2827
+ const columns = /* @__PURE__ */ new Map();
2828
+ for (const node of graph.nodes) {
2829
+ if (index.has(node.id)) continue;
2830
+ const id = owner.get(node.id);
2831
+ columns.set(id, [...columns.get(id) ?? [], node.id]);
2832
+ }
2833
+ for (const [lineId, hung] of columns) {
2834
+ const holding = ordered.filter((g) => g.members.some((m) => m === lineId || hung.includes(m)));
2835
+ const atLine = holding.filter((g) => g.members.includes(lineId));
2836
+ const kept = new Set(atLine.flatMap((g) => g.members));
2837
+ const clear2 = (g, fixed) => {
2838
+ const members = new Set(g.members);
2839
+ const own = hung.filter((id) => members.has(id)).map((id) => level.get(id));
2840
+ const low = members.has(lineId) ? 0 : Math.min(...own);
2841
+ const deep = Math.max(0, ...own);
2842
+ const movers = hung.filter((id) => !members.has(id) && !fixed.has(id) && level.get(id) >= low);
2843
+ if (!movers.some((id) => level.get(id) <= deep)) return;
2844
+ const steps = [...new Set(movers.map((id) => level.get(id)))].sort((p, q) => p - q);
2845
+ for (const id of movers) level.set(id, deep + 1 + steps.indexOf(level.get(id)));
2846
+ };
2847
+ for (const g of holding) if (!atLine.includes(g)) clear2(g, kept);
2848
+ for (const g of atLine) clear2(g, /* @__PURE__ */ new Set());
2849
+ }
2850
+ return level;
2851
+ }
2852
+ function lineGaps(graph, line, hanging, rankGap, axes, packed) {
2853
+ const index = new Map(line.map((n, i) => [n.id, i]));
2854
+ const columns = line.map((n) => [n]);
2855
+ for (const node of graph.nodes) {
2856
+ if (!index.has(node.id)) columns[index.get(hanging.owner.get(node.id))].push(node);
2857
+ }
2858
+ const gaps = rankGaps(graph, columns, axes, rankGap);
2859
+ const groups = graph.groups ?? [];
2860
+ if (axes.rank !== "x" || !groups.some((g) => g.minWidth !== void 0)) return gaps;
2861
+ const columnOf = (id) => index.get(id) ?? index.get(hanging.owner.get(id));
2862
+ const children = /* @__PURE__ */ new Map();
2863
+ for (const g of groups) if (g.parent !== void 0) children.set(g.parent, [...children.get(g.parent) ?? [], g.id]);
2864
+ const byId = new Map(groups.map((g) => [g.id, g]));
2865
+ const span = (id, now) => {
2866
+ let [lo, hi] = [Infinity, -Infinity];
2867
+ for (const m of byId.get(id).members) {
2868
+ const slot = packed.get(m);
2869
+ const node = graph.node(m);
2870
+ if (slot === void 0 || node === void 0) continue;
2871
+ const from = slot.rank + now.slice(0, columnOf(m)).reduce((s, gap) => s + gap, 0);
2872
+ [lo, hi] = [Math.min(lo, from), Math.max(hi, from + extent(node.bounds, axes.rank))];
2873
+ }
2874
+ return { lo, hi };
2875
+ };
2876
+ widenForMinWidth(deepestFirst(groups).map((g) => {
2877
+ const cols = g.members.filter((id) => packed.has(id)).map(columnOf);
2878
+ return {
2879
+ id: g.id,
2880
+ ...g.minWidth !== void 0 ? { minWidth: g.minWidth } : {},
2881
+ inset: rankInset(g.inset, axes),
2882
+ children: children.get(g.id) ?? [],
2883
+ before: cols.length === 0 ? -1 : Math.min(...cols) - 1,
2884
+ after: cols.length === 0 ? -1 : Math.max(...cols)
2885
+ };
2886
+ }), gaps, span);
2887
+ return gaps;
2888
+ }
2889
+ function widenForMinWidth(groups, gaps, span) {
2890
+ const byId = new Map(groups.map((g) => [g.id, g]));
2891
+ const boxOf2 = (g, minWidth) => {
2892
+ let { lo, hi } = span(g.id, gaps);
2893
+ [lo, hi] = [lo - g.inset.lo, hi + g.inset.hi];
2894
+ for (const id of g.children) {
2895
+ const child = byId.get(id);
2896
+ if (child === void 0) continue;
2897
+ const box = boxOf2(child, child.minWidth ?? 0);
2898
+ [lo, hi] = [Math.min(lo, box.lo - g.inset.lo), Math.max(hi, box.hi + g.inset.hi)];
2899
+ }
2900
+ const grow = Math.max(0, minWidth - (hi - lo)) / 2;
2901
+ return { lo: lo - grow, hi: hi + grow };
2902
+ };
2903
+ for (const g of groups) {
2904
+ if (g.minWidth === void 0) continue;
2905
+ const box = boxOf2(g, 0);
2906
+ if (!(box.lo <= box.hi)) continue;
2907
+ const half = Math.max(0, g.minWidth - (box.hi - box.lo)) / 2;
2908
+ if (g.before >= 0 && g.before < gaps.length) gaps[g.before] += half;
2909
+ if (g.after >= 0 && g.after < gaps.length) gaps[g.after] += half;
2910
+ }
2911
+ }
2912
+
2560
2913
  // src/chainRoutes.ts
2561
2914
  var portOf3 = (node, id) => id === void 0 ? void 0 : node.ports?.find((p) => p.id === id);
2562
2915
  function coord(node, at2, id, axis) {
@@ -2712,7 +3065,7 @@ function chainPlan(graph, opts = {}) {
2712
3065
  const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
2713
3066
  const line = lineOf(graph, opts.chain);
2714
3067
  if (line.length === 0) return { moves: /* @__PURE__ */ new Map() };
2715
- const hanging = hangFrom(graph, line);
3068
+ const hanging = groupHanging(graph, line, hangFrom(graph, line));
2716
3069
  const { owner, level } = hanging;
2717
3070
  const rows = new Map(line.map((n) => [n.id, []]));
2718
3071
  const levelSize = [Math.max(...line.map((n) => extent(n.bounds, axes.cross)))];
@@ -2725,7 +3078,9 @@ function chainPlan(graph, opts = {}) {
2725
3078
  }
2726
3079
  for (const [id, column] of rows) if (column[1]) column[1] = inPortOrder(graph, id, column[1], axes);
2727
3080
  const before = opts.chainSide === "before";
2728
- const place = (side2) => settle(graph, slotsFor(line, rows, levelSize, axes, { node: nodeGap, rank: rankGap, side: side2 }, before), opts);
3081
+ const slots = (rank, side2) => slotsFor(line, rows, levelSize, axes, { node: nodeGap, rank, side: side2 }, before);
3082
+ const gaps = graph.groups?.length ? lineGaps(graph, line, hanging, rankGap, axes, slots(line.map(() => 0), nodeGap)) : line.map(() => rankGap);
3083
+ const place = (side2) => settle(graph, slots(gaps, side2), opts);
2729
3084
  let moves = place(nodeGap);
2730
3085
  const side = (tracksNeeded(graph, hanging, moves, axes, before) + 1) * SIDE_TRACK_GAP;
2731
3086
  if (side > nodeGap) moves = place(side);
@@ -2742,7 +3097,7 @@ function slotsFor(line, rows, levelSize, axes, gaps, before) {
2742
3097
  };
2743
3098
  const slots = /* @__PURE__ */ new Map();
2744
3099
  let cursor = 0;
2745
- for (const node of line) {
3100
+ line.forEach((node, i) => {
2746
3101
  const column = rows.get(node.id);
2747
3102
  const span = Math.max(extent(node.bounds, axes.rank), ...Array.from(column, (row) => row ? rowSpan(row) : 0));
2748
3103
  slots.set(node.id, {
@@ -2758,8 +3113,8 @@ function slotsFor(line, rows, levelSize, axes, gaps, before) {
2758
3113
  along += extent(hung.bounds, axes.rank) + nodeGap;
2759
3114
  }
2760
3115
  });
2761
- cursor += span + rankGap;
2762
- }
3116
+ cursor += span + (rankGap[i] ?? 0);
3117
+ });
2763
3118
  return slots;
2764
3119
  }
2765
3120
 
@@ -2807,13 +3162,13 @@ var treeOnce = (graph, opts = {}) => {
2807
3162
  const at2 = depth.get(node.id) ?? 0;
2808
3163
  bands.set(at2, Math.max(bands.get(at2) ?? 0, extent(node.bounds, axes.rank)));
2809
3164
  }
2810
- const depths = [...bands.keys()].sort((a, b) => a - b);
2811
- const rowsAt = depths.map((at2) => graph.nodes.filter((n) => (depth.get(n.id) ?? 0) === at2));
3165
+ const depths2 = [...bands.keys()].sort((a, b) => a - b);
3166
+ const rowsAt = depths2.map((at2) => graph.nodes.filter((n) => (depth.get(n.id) ?? 0) === at2));
2812
3167
  const grouped = (graph.groups?.length ?? 0) > 0;
2813
- const gaps = grouped ? rankGaps(graph, rowsAt, axes, rankGap) : depths.slice(1).map(() => rankGap);
3168
+ const gaps = grouped ? rankGaps(graph, rowsAt, axes, rankGap) : depths2.slice(1).map(() => rankGap);
2814
3169
  const bandStart = /* @__PURE__ */ new Map();
2815
3170
  let along = 0;
2816
- for (const [i, at2] of depths.entries()) {
3171
+ for (const [i, at2] of depths2.entries()) {
2817
3172
  bandStart.set(at2, along);
2818
3173
  along += bands.get(at2) + (gaps[i] ?? 0);
2819
3174
  }
@@ -2860,6 +3215,6 @@ var treeOnce = (graph, opts = {}) => {
2860
3215
  return settle(graph, slots, opts);
2861
3216
  };
2862
3217
 
2863
- export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, PORT_LABEL_GAP, SIDE_TRACK_GAP, anchorOffset, authoredOver, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, chain, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, extent, footprintNode, force, forceRelaxation, forestOf, graphOrder, grownBy, insetOf, isPinned, isPromise, layered, layeredPlan, layeredRoutes, layoutBody, layoutRowPorts, marginOf, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, planOf, plannerPlan, portLabelBox, portLabelsOf, portOf, portSitesOf, portsOf, portsOnOutline, rankGaps, ranksByLength, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree, untilSettled };
2864
- //# sourceMappingURL=chunk-NUMMW5BI.js.map
2865
- //# sourceMappingURL=chunk-NUMMW5BI.js.map
3218
+ export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, PORT_LABEL_GAP, SIDE_TRACK_GAP, anchorOffset, authoredOver, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, chain, contentBox, createDiagramNodes, dataKeyReader, diagramEdgeOf, diagramNodeOf, expandGroupEdges, extent, footprintNode, force, forceRelaxation, forestOf, graphOrder, grownBy, insetOf, isPinned, isPromise, labelCenter, labelSpotAt, layered, layeredPlan, layeredRoutes, layoutBody, layoutOwnsPort, layoutOwnsRoute, layoutRowPorts, layoutWritesRoute, marginOf, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, planOf, plannerPlan, portLabelBox, portLabelsOf, portOf, portSitesOf, portsOf, portsOnOutline, rankGaps, ranksByLength, ranksOf, rayHit, routeHolds, samePoints, seededOrder, settle, sizeToBody, subpathOf, translated, tree, untilSettled, widenForMinWidth };
3219
+ //# sourceMappingURL=chunk-IM6DHWZO.js.map
3220
+ //# sourceMappingURL=chunk-IM6DHWZO.js.map