@statelyai/layout 0.3.0 → 0.4.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.
Files changed (110) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +150 -11
  3. package/dist/elkjs/index.cjs +2 -2
  4. package/dist/elkjs/index.d.cts +2 -0
  5. package/dist/elkjs/index.d.mts +2 -0
  6. package/dist/elkjs/index.mjs +2 -2
  7. package/dist/elkjs/main.cjs +1 -1
  8. package/dist/elkjs/main.mjs +1 -1
  9. package/dist/elkjs/worker.cjs +2 -2
  10. package/dist/elkjs/worker.mjs +2 -2
  11. package/dist/elkjs-UciYx7lD.mjs +2973 -0
  12. package/dist/elkjs-ggjsBuVP.cjs +3002 -0
  13. package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
  14. package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
  15. package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
  16. package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
  17. package/dist/index.cjs +6 -3
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.mts +3 -3
  20. package/dist/index.mjs +4 -4
  21. package/dist/layered/index.cjs +3 -1
  22. package/dist/layered/index.d.cts +2 -2
  23. package/dist/layered/index.d.mts +2 -2
  24. package/dist/layered/index.mjs +2 -2
  25. package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
  26. package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
  27. package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
  28. package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
  29. package/dist/routing/index.cjs +2 -1
  30. package/dist/routing/index.d.cts +2 -2
  31. package/dist/routing/index.d.mts +2 -2
  32. package/dist/routing/index.mjs +2 -2
  33. package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
  34. package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
  35. package/package.json +8 -2
  36. package/src/authoring/coordinates.ts +28 -8
  37. package/src/authoring/partial.ts +7 -2
  38. package/src/authoring/routing.ts +4 -1
  39. package/src/constraints.ts +174 -6
  40. package/src/elkjs/index.ts +1392 -449
  41. package/src/elkjs/layout-quality.ts +219 -0
  42. package/src/elkjs/opposing-tracks.ts +161 -0
  43. package/src/elkjs/orthogonal-junctions.ts +174 -0
  44. package/src/elkjs/statechart.ts +9 -3
  45. package/src/elkjs/types.ts +2 -0
  46. package/src/index.ts +2 -0
  47. package/src/internal/elkjs-compatibility.ts +4 -1
  48. package/src/internal/layered-trace.ts +48 -0
  49. package/src/internal/layout-engine.ts +2 -1
  50. package/src/java-random.ts +6 -0
  51. package/src/layered/bk-node-placement.ts +403 -92
  52. package/src/layered/center-labels.ts +398 -0
  53. package/src/layered/compaction-bounds.ts +26 -0
  54. package/src/layered/compaction-errors.ts +2 -0
  55. package/src/layered/compaction-scanline.ts +63 -0
  56. package/src/layered/compound-boundaries.ts +99 -0
  57. package/src/layered/compound.ts +35 -7
  58. package/src/layered/constraint-orientation.ts +141 -0
  59. package/src/layered/crossing-constraints.ts +243 -0
  60. package/src/layered/crossing-counter.ts +221 -0
  61. package/src/layered/crossing-graph.ts +401 -0
  62. package/src/layered/cycle-random.ts +22 -0
  63. package/src/layered/elk-port-lists.ts +243 -0
  64. package/src/layered/end-labels.ts +124 -0
  65. package/src/layered/external-port-dummy.ts +137 -0
  66. package/src/layered/feedback-chains.ts +114 -0
  67. package/src/layered/fixed-self-loop.ts +150 -0
  68. package/src/layered/greedy-switch-decider.ts +356 -0
  69. package/src/layered/grouped-compaction.ts +787 -0
  70. package/src/layered/gwt-sort.ts +53 -0
  71. package/src/layered/hierarchical-port-phases.ts +242 -0
  72. package/src/layered/hierarchical-port-restoration.ts +233 -0
  73. package/src/layered/hierarchical-port-routing.ts +179 -0
  74. package/src/layered/hierarchy-crossing.ts +121 -0
  75. package/src/layered/hierarchy-sweepiness.ts +83 -0
  76. package/src/layered/hints.ts +371 -0
  77. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  78. package/src/layered/index.ts +886 -161
  79. package/src/layered/initial-model-order.ts +404 -0
  80. package/src/layered/inverted-ports.ts +207 -0
  81. package/src/layered/label-dummy-switcher.ts +200 -0
  82. package/src/layered/long-edges.ts +135 -44
  83. package/src/layered/loop-envelopes.ts +184 -0
  84. package/src/layered/native-routing.ts +275 -0
  85. package/src/layered/network-simplex.ts +11 -15
  86. package/src/layered/node-margins.ts +59 -0
  87. package/src/layered/north-south-ports.ts +271 -0
  88. package/src/layered/opposing-tracks.ts +944 -0
  89. package/src/layered/orthogonal-cycle-order.ts +106 -0
  90. package/src/layered/orthogonal-hypersegments.ts +92 -0
  91. package/src/layered/orthogonal-junctions.ts +202 -0
  92. package/src/layered/orthogonal-segments.ts +243 -0
  93. package/src/layered/placement-orientation.ts +22 -0
  94. package/src/layered/port-distributor.ts +192 -0
  95. package/src/layered/port-edge-order.ts +20 -0
  96. package/src/layered/replace-routing.ts +45 -0
  97. package/src/layered/routing-coordinates.ts +32 -0
  98. package/src/layered/separate-exterior-labels.ts +24 -6
  99. package/src/layered/spacing.ts +34 -12
  100. package/src/layered/splitter-order.ts +106 -0
  101. package/src/layered/strategies.ts +2024 -883
  102. package/src/layered/types.ts +13 -0
  103. package/src/layered/weighted-compaction.ts +84 -0
  104. package/src/routing/adapters.ts +32 -0
  105. package/src/routing/algorithms.ts +433 -21
  106. package/src/routing/coordination.ts +8 -3
  107. package/src/routing/index.ts +1 -0
  108. package/src/routing/search.ts +93 -11
  109. package/dist/elkjs-DaHMISnQ.mjs +0 -1677
  110. package/dist/elkjs-dMghiiWW.cjs +0 -1706
@@ -8,6 +8,7 @@ export interface SearchContext {
8
8
  edgeCost?(a: RoutePoint, b: RoutePoint): number;
9
9
  guides?(bounds: RouteBounds): readonly RouteBounds[];
10
10
  readonly maxSearchNodes: number;
11
+ readonly maxGridNodes?: number;
11
12
  readonly bendPenalty: number;
12
13
  visited: number;
13
14
  budgetExceeded: boolean;
@@ -82,6 +83,18 @@ export function findPath(
82
83
  context: SearchContext,
83
84
  terminals: { incoming?: RoutePoint; outgoing?: RoutePoint } = {},
84
85
  ): RoutePoint[] | undefined {
86
+ const maxGridNodes = context.maxGridNodes ?? context.maxSearchNodes;
87
+ // Match the admissible lower bound to the route geometry. Euclidean distance
88
+ // badly underestimates long orthogonal paths and exhausts the search budget.
89
+ const remainingDistance = (point: RoutePoint) => {
90
+ const dx = Math.abs(point.x - end.x),
91
+ dy = Math.abs(point.y - end.y);
92
+ return style === "orthogonal"
93
+ ? dx + dy
94
+ : style === "octilinear"
95
+ ? Math.max(dx, dy) + (Math.SQRT2 - 1) * Math.min(dx, dy)
96
+ : distance(point, end);
97
+ };
85
98
  const turnCost = (a: RoutePoint | undefined, b: RoutePoint | undefined) => {
86
99
  if (!a || !b || !Math.hypot(a.x, a.y) || !Math.hypot(b.x, b.y)) return 0;
87
100
  const dot = a.x * b.x + a.y * b.y,
@@ -134,13 +147,34 @@ export function findPath(
134
147
  points = [start, end, ...corners];
135
148
  neighbors = () => points.map((_, i) => i);
136
149
  } else {
137
- const xs = [
138
- ...new Set([start.x, end.x, area.x, area.x + area.width, ...corners.map((p) => p.x)]),
139
- ].sort((a, b) => a - b);
140
- const ys = [
141
- ...new Set([start.y, end.y, area.y, area.y + area.height, ...corners.map((p) => p.y)]),
142
- ].sort((a, b) => a - b);
143
- if (xs.length * ys.length > context.maxSearchNodes) {
150
+ // Fractional compound offsets can produce almost-identical grid tracks.
151
+ // Zero-length steps between them bypass terminal direction constraints.
152
+ const coordinates = (values: number[]) => {
153
+ const endpoints = values.slice(0, 2);
154
+ const sorted = [...values].sort((a, b) => a - b);
155
+ const unique: number[] = [];
156
+ for (const value of sorted) {
157
+ if (unique.length && Math.abs(value - unique.at(-1)!) < 1e-8) continue;
158
+ unique.push(value);
159
+ }
160
+ // Preserve exact terminal values so reconstructed leads stay aligned.
161
+ return unique.map((value) => endpoints.find((p) => Math.abs(p - value) < 1e-8) ?? value);
162
+ };
163
+ const xs = coordinates([
164
+ start.x,
165
+ end.x,
166
+ area.x,
167
+ area.x + area.width,
168
+ ...corners.map((p) => p.x),
169
+ ]);
170
+ const ys = coordinates([
171
+ start.y,
172
+ end.y,
173
+ area.y,
174
+ area.y + area.height,
175
+ ...corners.map((p) => p.y),
176
+ ]);
177
+ if (xs.length * ys.length > maxGridNodes) {
144
178
  context.budgetExceeded = true;
145
179
  return undefined;
146
180
  }
@@ -164,7 +198,7 @@ export function findPath(
164
198
  const unique = new Map(points.map((p) => [`${p.x}:${p.y}`, p]));
165
199
  for (const p of extra) unique.set(`${p.x}:${p.y}`, p);
166
200
  points = [...unique.values()];
167
- if (points.length > context.maxSearchNodes) {
201
+ if (points.length > maxGridNodes) {
168
202
  context.budgetExceeded = true;
169
203
  return undefined;
170
204
  }
@@ -200,7 +234,7 @@ export function findPath(
200
234
  i + width < points.length ? i + width : -1,
201
235
  ].filter((v) => v >= 0);
202
236
  }
203
- if (points.length > context.maxSearchNodes) {
237
+ if (points.length > maxGridNodes) {
204
238
  context.budgetExceeded = true;
205
239
  return undefined;
206
240
  }
@@ -224,7 +258,7 @@ export function findPath(
224
258
  const first = `${from}:-1`;
225
259
  costs.set(first, 0);
226
260
  vertices.set(first, from);
227
- queue.push({ index: from, previous: -1, key: first, cost: 0 }, distance(start, end));
261
+ queue.push({ index: from, previous: -1, key: first, cost: 0 }, remainingDistance(start));
228
262
  while (queue.items.length && context.visited < context.maxSearchNodes) {
229
263
  const current = queue.pop();
230
264
  if (costs.get(current.key) !== current.cost) continue;
@@ -246,6 +280,14 @@ export function findPath(
246
280
  const before =
247
281
  current.previous < 0 ? terminals.incoming : delta(points[current.previous]!, a);
248
282
  const step = delta(a, b);
283
+ const reverses = (incoming: RoutePoint | undefined, outgoing: RoutePoint | undefined) =>
284
+ incoming &&
285
+ outgoing &&
286
+ Math.abs(incoming.x * outgoing.y - incoming.y * outgoing.x) < 1e-8 &&
287
+ incoming.x * outgoing.x + incoming.y * outgoing.y < -1e-8;
288
+ // Immediate reversal retraces the preceding segment. It is never a
289
+ // valid bend, including inside the grid or at a terminal lead.
290
+ if (reverses(before, step) || (next === to && reverses(step, terminals.outgoing))) continue;
249
291
  const bend =
250
292
  turnCost(before, step) + (next === to ? turnCost(step, terminals.outgoing) : 0);
251
293
  const pair = Math.min(current.index, next) * points.length + Math.max(current.index, next);
@@ -260,7 +302,10 @@ export function findPath(
260
302
  costs.set(key, cost);
261
303
  parents.set(key, current.key);
262
304
  vertices.set(key, next);
263
- queue.push({ index: next, previous: current.index, key, cost }, cost + distance(b, end));
305
+ queue.push(
306
+ { index: next, previous: current.index, key, cost },
307
+ cost + remainingDistance(b),
308
+ );
264
309
  }
265
310
  }
266
311
  }
@@ -274,3 +319,40 @@ export function pointsBounds(points: readonly RoutePoint[]): RouteBounds {
274
319
  height: 0,
275
320
  });
276
321
  }
322
+
323
+ // Search must not consume the terminal leads that are appended afterwards.
324
+ // A distant excursion can retrace a lead even without an immediate reversal.
325
+ export const findPathBetweenLeads = (
326
+ start: RoutePoint,
327
+ end: RoutePoint,
328
+ style: "orthogonal" | "polyline" | "octilinear",
329
+ ctx: SearchContext,
330
+ leads: readonly [RoutePoint, RoutePoint][],
331
+ directions: { incoming?: RoutePoint; outgoing?: RoutePoint } = {},
332
+ ) => {
333
+ const protectedContext = {
334
+ ...ctx,
335
+ edgeCost: (p: RoutePoint, q: RoutePoint) => {
336
+ for (const [from, to] of leads) {
337
+ const dx = q.x - p.x,
338
+ dy = q.y - p.y;
339
+ const length = Math.hypot(dx, dy);
340
+ if (length < 1e-8) continue;
341
+ if (
342
+ Math.abs(dx * (to.y - from.y) - dy * (to.x - from.x)) > 1e-8 * length ||
343
+ Math.abs(dx * (from.y - p.y) - dy * (from.x - p.x)) > 1e-8 * length
344
+ )
345
+ continue;
346
+ const lo = ((from.x - p.x) * dx + (from.y - p.y) * dy) / length;
347
+ const hi = ((to.x - p.x) * dx + (to.y - p.y) * dy) / length;
348
+ if (Math.min(length, Math.max(lo, hi)) - Math.max(0, Math.min(lo, hi)) > 1e-8)
349
+ return Infinity;
350
+ }
351
+ return ctx.edgeCost?.(p, q) ?? 0;
352
+ },
353
+ };
354
+ const result = findPath(start, end, style, protectedContext, directions);
355
+ ctx.visited = protectedContext.visited;
356
+ ctx.budgetExceeded = protectedContext.budgetExceeded;
357
+ return result;
358
+ };