@dryui/ui 0.5.2 → 1.0.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 (99) hide show
  1. package/dist/alert/{alert-root.svelte → alert.svelte} +78 -20
  2. package/dist/alert/alert.svelte.d.ts +15 -0
  3. package/dist/alert/index.d.ts +15 -14
  4. package/dist/alert/index.js +3 -12
  5. package/dist/breadcrumb/breadcrumb-link.svelte +1 -1
  6. package/dist/button/button.svelte +1 -1
  7. package/dist/card/card-root.svelte +2 -2
  8. package/dist/chromatic-shift/chromatic-shift.svelte +2 -2
  9. package/dist/code-block/code-block-button.svelte +1 -1
  10. package/dist/color-picker/color-picker-area.svelte +2 -2
  11. package/dist/color-picker/color-picker-channel-input.svelte +2 -2
  12. package/dist/color-picker/color-picker-input-alpha-slider.svelte +2 -2
  13. package/dist/color-picker/color-picker-input-hue-slider.svelte +2 -2
  14. package/dist/color-picker/color-picker-input.svelte +9 -9
  15. package/dist/color-picker/color-picker-swatch.svelte +2 -2
  16. package/dist/combobox/combobox-input.svelte +9 -9
  17. package/dist/command-palette/command-palette-item.svelte +1 -1
  18. package/dist/data-grid/data-grid-button-input-column.svelte +1 -1
  19. package/dist/diagram/diagram.svelte +222 -32
  20. package/dist/diagram/diagram.svelte.d.ts +1 -0
  21. package/dist/diagram/edge-routing.d.ts +63 -1
  22. package/dist/diagram/edge-routing.js +316 -26
  23. package/dist/diagram/layout.js +633 -62
  24. package/dist/diagram/types.d.ts +58 -0
  25. package/dist/drag-and-drop/drag-and-drop-handle.svelte +1 -1
  26. package/dist/drag-and-drop/drag-and-drop-item.svelte +1 -1
  27. package/dist/file-select/file-select-root.svelte +2 -2
  28. package/dist/file-upload/file-upload-dropzone.svelte +2 -2
  29. package/dist/gauge/gauge.svelte +1 -1
  30. package/dist/image-comparison/image-comparison.svelte +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.js +1 -1
  33. package/dist/input/input.svelte +10 -11
  34. package/dist/label/label.svelte +1 -1
  35. package/dist/link/link.svelte +1 -1
  36. package/dist/list/list-item.svelte +2 -2
  37. package/dist/multi-select-combobox/multi-select-combobox-selection-item.svelte +9 -3
  38. package/dist/multi-select-combobox/multi-select-combobox-selection-remove-button.svelte +2 -0
  39. package/dist/navigation-menu/navigation-menu-link.svelte +1 -1
  40. package/dist/notification-center/notification-center-item.svelte +1 -1
  41. package/dist/number-input/number-input-button.svelte +3 -3
  42. package/dist/option-picker/context.svelte.d.ts +9 -0
  43. package/dist/option-picker/context.svelte.js +2 -0
  44. package/dist/option-picker/option-picker-item.svelte +31 -4
  45. package/dist/option-picker/option-picker-preview.svelte +2 -2
  46. package/dist/option-picker/option-picker-root.svelte +2 -2
  47. package/dist/phone-input/phone-input-select.svelte +2 -2
  48. package/dist/pin-input/pin-input-cell.svelte +1 -1
  49. package/dist/pin-input/pin-input-root.svelte +1 -1
  50. package/dist/progress/progress.svelte +1 -1
  51. package/dist/scroll-area/scroll-area.svelte +1 -1
  52. package/dist/shimmer/index.d.ts +8 -0
  53. package/dist/shimmer/index.js +1 -0
  54. package/dist/shimmer/shimmer.svelte +87 -0
  55. package/dist/shimmer/shimmer.svelte.d.ts +10 -0
  56. package/dist/sidebar/sidebar-item.svelte +1 -1
  57. package/dist/slider/slider-input.svelte +2 -2
  58. package/dist/splitter/splitter-handle.svelte +1 -1
  59. package/dist/table-of-contents/table-of-contents-item.svelte +1 -1
  60. package/dist/table-of-contents/table-of-contents-list.svelte +1 -1
  61. package/dist/tags-input/tags-input-root.svelte +1 -1
  62. package/dist/tags-input/tags-input-tag-delete-button.svelte +2 -0
  63. package/dist/tags-input/tags-input-tag.svelte +9 -3
  64. package/dist/textarea/textarea.svelte +11 -11
  65. package/dist/themes/default.css +31 -0
  66. package/dist/toast/toast-root.svelte +1 -1
  67. package/dist/tour/tour-root.css +3 -3
  68. package/dist/tree/tree-item-children.svelte +1 -1
  69. package/dist/tree/tree-item-label.svelte +1 -1
  70. package/dist/video-embed/video-embed-button.svelte +1 -1
  71. package/package.json +11 -750
  72. package/skills/dryui/SKILL.md +26 -21
  73. package/skills/dryui/rules/compound-components.md +3 -3
  74. package/skills/dryui/rules/theming.md +1 -1
  75. package/dist/alert/alert-button-close.svelte +0 -29
  76. package/dist/alert/alert-button-close.svelte.d.ts +0 -8
  77. package/dist/alert/alert-description.svelte +0 -28
  78. package/dist/alert/alert-description.svelte.d.ts +0 -8
  79. package/dist/alert/alert-icon.svelte +0 -26
  80. package/dist/alert/alert-icon.svelte.d.ts +0 -8
  81. package/dist/alert/alert-root.svelte.d.ts +0 -12
  82. package/dist/alert/alert-title.svelte +0 -29
  83. package/dist/alert/alert-title.svelte.d.ts +0 -8
  84. package/dist/alert/context.svelte.d.ts +0 -9
  85. package/dist/alert/context.svelte.js +0 -10
  86. package/dist/option-swatch-group/context.svelte.d.ts +0 -9
  87. package/dist/option-swatch-group/context.svelte.js +0 -2
  88. package/dist/option-swatch-group/index.d.ts +0 -29
  89. package/dist/option-swatch-group/index.js +0 -12
  90. package/dist/option-swatch-group/option-swatch-group-item-button.svelte +0 -214
  91. package/dist/option-swatch-group/option-swatch-group-item-button.svelte.d.ts +0 -12
  92. package/dist/option-swatch-group/option-swatch-group-label.svelte +0 -24
  93. package/dist/option-swatch-group/option-swatch-group-label.svelte.d.ts +0 -8
  94. package/dist/option-swatch-group/option-swatch-group-meta.svelte +0 -24
  95. package/dist/option-swatch-group/option-swatch-group-meta.svelte.d.ts +0 -8
  96. package/dist/option-swatch-group/option-swatch-group-root.svelte +0 -81
  97. package/dist/option-swatch-group/option-swatch-group-root.svelte.d.ts +0 -12
  98. package/dist/option-swatch-group/option-swatch-group-swatch.svelte +0 -52
  99. package/dist/option-swatch-group/option-swatch-group-swatch.svelte.d.ts +0 -10
@@ -1,11 +1,25 @@
1
1
  const EDGE_GAP = 14;
2
2
  const BUS_OFFSET_MIN = 28;
3
3
  const BUS_OFFSET_MAX = 48;
4
- export function computeEdgePaths(edges, positions, nodeDims, direction) {
4
+ const DEFAULT_CORNER_RADIUS = 6;
5
+ const BACK_EDGE_LANE_GAP = 32;
6
+ const BACK_EDGE_LANE_STEP = 22;
7
+ /** Minimum distance (in path units, measured along the polyline) between a
8
+ * forward edge label and a directed-cluster boundary. Cross-boundary edges
9
+ * whose natural midpoint sits closer than this to the cluster are slid along
10
+ * the polyline toward the outside endpoint. */
11
+ export const LABEL_BORDER_AVOID_PX = 28;
12
+ function edgeKey(edge) {
13
+ return `${edge.from}->${edge.to}`;
14
+ }
15
+ export function computeEdgePaths(edges, positions, nodeDims, direction, opts = {}) {
16
+ const { cornerRadius = DEFAULT_CORNER_RADIUS, reversedEdges = new Set(), bounds, backEdgeLaneGap = BACK_EDGE_LANE_GAP, superNodeIds, backEdgeAnchorOverrides } = opts;
5
17
  const horizontal = direction === 'LR' || direction === 'RL';
6
18
  const bySource = new Map();
7
19
  const byTarget = new Map();
8
20
  for (const edge of edges) {
21
+ if (reversedEdges.has(edgeKey(edge)))
22
+ continue;
9
23
  const sourceList = bySource.get(edge.from) || [];
10
24
  sourceList.push(edge);
11
25
  bySource.set(edge.from, sourceList);
@@ -15,10 +29,53 @@ export function computeEdgePaths(edges, positions, nodeDims, direction) {
15
29
  }
16
30
  const sourceBusCache = new Map();
17
31
  const targetBusCache = new Map();
18
- return edges.map((edge) => {
32
+ const laneCounters = {
33
+ over: 0,
34
+ under: 0,
35
+ left: 0,
36
+ right: 0
37
+ };
38
+ const out = [];
39
+ const collapsedOut = [];
40
+ edges.forEach((edge) => {
41
+ const isBack = reversedEdges.has(edgeKey(edge));
42
+ if (isBack && bounds) {
43
+ const side = pickBackEdgeSide(edge.loop, direction);
44
+ const overrides = backEdgeAnchorOverrides?.get(edgeKey(edge));
45
+ const sourceId = overrides?.source ?? edge.from;
46
+ const targetId = overrides?.target ?? edge.to;
47
+ const endpoints = getBackEdgeEndpoints(sourceId, targetId, positions, nodeDims, side);
48
+ if (!endpoints) {
49
+ out.push(emptyEdge(edge));
50
+ collapsedOut.push(null);
51
+ return;
52
+ }
53
+ const laneIndex = laneCounters[side]++;
54
+ const points = buildBackEdgePoints(endpoints, side, bounds, laneIndex, backEdgeLaneGap);
55
+ const collapsed = collapsePoints(points);
56
+ const path = buildPathFromCollapsed(collapsed, cornerRadius);
57
+ const midpoint = getMidpointFromCollapsed(collapsed);
58
+ const labelOffset = side === 'over' ? -10 : side === 'under' ? 14 : 0;
59
+ out.push({
60
+ from: edge.from,
61
+ to: edge.to,
62
+ path,
63
+ label: edge.label,
64
+ labelX: midpoint.x,
65
+ labelY: midpoint.y + labelOffset,
66
+ arrow: edge.arrow || 'end',
67
+ dashed: edge.dashed || false,
68
+ color: edge.color || 'neutral',
69
+ bounds: boundsFromPoints(collapsed)
70
+ });
71
+ collapsedOut.push(collapsed);
72
+ return;
73
+ }
19
74
  const endpoints = getEndpoints(edge, positions, nodeDims, horizontal);
20
75
  if (!endpoints) {
21
- return emptyEdge(edge);
76
+ out.push(emptyEdge(edge));
77
+ collapsedOut.push(null);
78
+ return;
22
79
  }
23
80
  const sourceSiblings = bySource.get(edge.from) || [];
24
81
  const targetSiblings = byTarget.get(edge.to) || [];
@@ -64,20 +121,114 @@ export function computeEdgePaths(edges, positions, nodeDims, direction) {
64
121
  points = buildVerticalPoints(endpoints);
65
122
  }
66
123
  const collapsed = collapsePoints(points);
67
- const path = buildPathFromCollapsed(collapsed);
68
- const midpoint = getMidpointFromCollapsed(collapsed);
69
- return {
124
+ const path = buildPathFromCollapsed(collapsed, cornerRadius);
125
+ const labelPoint = computeLabelAnchor(edge, collapsed, superNodeIds);
126
+ out.push({
70
127
  from: edge.from,
71
128
  to: edge.to,
72
129
  path,
73
130
  label: edge.label,
74
- labelX: midpoint.x,
75
- labelY: midpoint.y - (horizontal ? 12 : 0),
131
+ labelX: labelPoint.x,
132
+ labelY: labelPoint.y - (horizontal ? 12 : 0),
76
133
  arrow: edge.arrow || 'end',
77
134
  dashed: edge.dashed || false,
78
- color: edge.color || 'neutral'
79
- };
135
+ color: edge.color || 'neutral',
136
+ bounds: boundsFromPoints(collapsed)
137
+ });
138
+ collapsedOut.push(collapsed);
80
139
  });
140
+ return { edges: out, collapsed: collapsedOut };
141
+ }
142
+ function pickBackEdgeSide(override, direction) {
143
+ if (override)
144
+ return override;
145
+ return direction === 'LR' || direction === 'RL' ? 'over' : 'right';
146
+ }
147
+ function getBackEdgeEndpoints(fromId, toId, positions, nodeDims, side) {
148
+ const fromPos = positions.get(fromId);
149
+ const toPos = positions.get(toId);
150
+ const fromDims = nodeDims.get(fromId);
151
+ const toDims = nodeDims.get(toId);
152
+ if (!fromPos || !toPos || !fromDims || !toDims)
153
+ return undefined;
154
+ const fromCenter = {
155
+ x: fromPos.x + fromDims.w / 2,
156
+ y: fromPos.y + fromDims.h / 2
157
+ };
158
+ const toCenter = {
159
+ x: toPos.x + toDims.w / 2,
160
+ y: toPos.y + toDims.h / 2
161
+ };
162
+ switch (side) {
163
+ case 'over':
164
+ return {
165
+ fx: fromCenter.x,
166
+ fy: fromPos.y - EDGE_GAP,
167
+ tx: toCenter.x,
168
+ ty: toPos.y - EDGE_GAP
169
+ };
170
+ case 'under':
171
+ return {
172
+ fx: fromCenter.x,
173
+ fy: fromPos.y + fromDims.h + EDGE_GAP,
174
+ tx: toCenter.x,
175
+ ty: toPos.y + toDims.h + EDGE_GAP
176
+ };
177
+ case 'right':
178
+ return {
179
+ fx: fromPos.x + fromDims.w + EDGE_GAP,
180
+ fy: fromCenter.y,
181
+ tx: toPos.x + toDims.w + EDGE_GAP,
182
+ ty: toCenter.y
183
+ };
184
+ case 'left':
185
+ return {
186
+ fx: fromPos.x - EDGE_GAP,
187
+ fy: fromCenter.y,
188
+ tx: toPos.x - EDGE_GAP,
189
+ ty: toCenter.y
190
+ };
191
+ }
192
+ }
193
+ function buildBackEdgePoints(endpoints, side, bounds, laneIndex, laneGap) {
194
+ switch (side) {
195
+ case 'over': {
196
+ const outerY = bounds.minY - laneGap - laneIndex * BACK_EDGE_LANE_STEP;
197
+ return [
198
+ { x: endpoints.fx, y: endpoints.fy },
199
+ { x: endpoints.fx, y: outerY },
200
+ { x: endpoints.tx, y: outerY },
201
+ { x: endpoints.tx, y: endpoints.ty }
202
+ ];
203
+ }
204
+ case 'under': {
205
+ const outerY = bounds.maxY + laneGap + laneIndex * BACK_EDGE_LANE_STEP;
206
+ return [
207
+ { x: endpoints.fx, y: endpoints.fy },
208
+ { x: endpoints.fx, y: outerY },
209
+ { x: endpoints.tx, y: outerY },
210
+ { x: endpoints.tx, y: endpoints.ty }
211
+ ];
212
+ }
213
+ case 'right': {
214
+ const outerX = bounds.maxX + laneGap + laneIndex * BACK_EDGE_LANE_STEP;
215
+ return [
216
+ { x: endpoints.fx, y: endpoints.fy },
217
+ { x: outerX, y: endpoints.fy },
218
+ { x: outerX, y: endpoints.ty },
219
+ { x: endpoints.tx, y: endpoints.ty }
220
+ ];
221
+ }
222
+ case 'left': {
223
+ const outerX = bounds.minX - laneGap - laneIndex * BACK_EDGE_LANE_STEP;
224
+ return [
225
+ { x: endpoints.fx, y: endpoints.fy },
226
+ { x: outerX, y: endpoints.fy },
227
+ { x: outerX, y: endpoints.ty },
228
+ { x: endpoints.tx, y: endpoints.ty }
229
+ ];
230
+ }
231
+ }
81
232
  }
82
233
  function getEndpoints(edge, positions, nodeDims, horizontal) {
83
234
  const fromPos = positions.get(edge.from);
@@ -199,16 +350,61 @@ function buildVerticalBusPoints(endpoints, busY) {
199
350
  points.push({ x: endpoints.tx, y: endpoints.ty });
200
351
  return points;
201
352
  }
202
- function buildPathFromCollapsed(collapsed) {
353
+ function buildPathFromCollapsed(collapsed, cornerRadius = 0) {
203
354
  if (collapsed.length === 0)
204
355
  return '';
205
356
  if (collapsed.length === 1) {
206
357
  const p = collapsed[0];
207
358
  return `M ${p.x} ${p.y}`;
208
359
  }
209
- return collapsed
210
- .map((point, index) => (index === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`))
211
- .join(' ');
360
+ if (cornerRadius <= 0 || collapsed.length < 3) {
361
+ return collapsed
362
+ .map((point, index) => (index === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`))
363
+ .join(' ');
364
+ }
365
+ const first = collapsed[0];
366
+ const segments = [`M ${first.x} ${first.y}`];
367
+ for (let i = 1; i < collapsed.length - 1; i++) {
368
+ const prev = collapsed[i - 1];
369
+ const corner = collapsed[i];
370
+ const next = collapsed[i + 1];
371
+ const inDX = corner.x - prev.x;
372
+ const inDY = corner.y - prev.y;
373
+ const inLen = Math.hypot(inDX, inDY);
374
+ const outDX = next.x - corner.x;
375
+ const outDY = next.y - corner.y;
376
+ const outLen = Math.hypot(outDX, outDY);
377
+ if (inLen < 0.01 || outLen < 0.01) {
378
+ segments.push(`L ${corner.x} ${corner.y}`);
379
+ continue;
380
+ }
381
+ const r = Math.min(cornerRadius, inLen / 2, outLen / 2);
382
+ const startX = corner.x - (inDX / inLen) * r;
383
+ const startY = corner.y - (inDY / inLen) * r;
384
+ const endX = corner.x + (outDX / outLen) * r;
385
+ const endY = corner.y + (outDY / outLen) * r;
386
+ segments.push(`L ${startX} ${startY}`);
387
+ segments.push(`Q ${corner.x} ${corner.y} ${endX} ${endY}`);
388
+ }
389
+ const last = collapsed[collapsed.length - 1];
390
+ segments.push(`L ${last.x} ${last.y}`);
391
+ return segments.join(' ');
392
+ }
393
+ function boundsFromPoints(points) {
394
+ if (points.length === 0)
395
+ return undefined;
396
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
397
+ for (const p of points) {
398
+ if (p.x < minX)
399
+ minX = p.x;
400
+ if (p.x > maxX)
401
+ maxX = p.x;
402
+ if (p.y < minY)
403
+ minY = p.y;
404
+ if (p.y > maxY)
405
+ maxY = p.y;
406
+ }
407
+ return { minX, minY, maxX, maxY };
212
408
  }
213
409
  function collapsePoints(points) {
214
410
  const collapsed = [];
@@ -233,7 +429,14 @@ function snapPoint(point) {
233
429
  function snapCoordinate(value) {
234
430
  return Math.round(value) + 0.5;
235
431
  }
236
- function getMidpointFromCollapsed(collapsed) {
432
+ export function getPointAtFraction(collapsed, t) {
433
+ const fallback = {
434
+ point: collapsed[0] ?? { x: 0, y: 0 },
435
+ segmentIndex: 0,
436
+ axis: 'h'
437
+ };
438
+ if (collapsed.length < 2)
439
+ return fallback;
237
440
  let totalLength = 0;
238
441
  const segments = [];
239
442
  for (let index = 1; index < collapsed.length; index += 1) {
@@ -242,32 +445,119 @@ function getMidpointFromCollapsed(collapsed) {
242
445
  const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
243
446
  if (length === 0)
244
447
  continue;
245
- segments.push({ from, to, length });
448
+ const axis = from.x !== to.x ? 'h' : 'v';
449
+ segments.push({ from, to, length, index, axis });
246
450
  totalLength += length;
247
451
  }
248
- if (totalLength === 0) {
249
- return collapsed[0] ?? { x: 0, y: 0 };
250
- }
251
- let remaining = totalLength / 2;
452
+ if (totalLength === 0)
453
+ return fallback;
454
+ const clamped = Math.max(0, Math.min(1, t));
455
+ let remaining = totalLength * clamped;
252
456
  for (const segment of segments) {
253
457
  if (remaining <= segment.length) {
254
- if (segment.from.x !== segment.to.x) {
458
+ if (segment.axis === 'h') {
255
459
  const direction = Math.sign(segment.to.x - segment.from.x);
256
460
  return {
257
- x: segment.from.x + direction * remaining,
258
- y: segment.from.y
461
+ point: { x: segment.from.x + direction * remaining, y: segment.from.y },
462
+ segmentIndex: segment.index,
463
+ axis: 'h'
259
464
  };
260
465
  }
261
466
  const direction = Math.sign(segment.to.y - segment.from.y);
262
467
  return {
263
- x: segment.from.x,
264
- y: segment.from.y + direction * remaining
468
+ point: { x: segment.from.x, y: segment.from.y + direction * remaining },
469
+ segmentIndex: segment.index,
470
+ axis: 'v'
265
471
  };
266
472
  }
267
473
  remaining -= segment.length;
268
474
  }
269
- return collapsed[collapsed.length - 1] ?? { x: 0, y: 0 };
475
+ const lastIndex = collapsed.length - 1;
476
+ const last = collapsed[lastIndex] ?? { x: 0, y: 0 };
477
+ return { point: last, segmentIndex: lastIndex, axis: 'h' };
478
+ }
479
+ function getMidpointFromCollapsed(collapsed) {
480
+ return getPointAtFraction(collapsed, 0.5).point;
481
+ }
482
+ function getPolylineLength(collapsed) {
483
+ let total = 0;
484
+ for (let i = 1; i < collapsed.length; i += 1) {
485
+ const a = collapsed[i - 1];
486
+ const b = collapsed[i];
487
+ total += Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
488
+ }
489
+ return total;
490
+ }
491
+ /** Compute the on-polyline anchor point for an edge label. Defaults to the
492
+ * geometric midpoint, but biases away from a directed-cluster boundary when
493
+ * the natural midpoint would land within LABEL_BORDER_AVOID_PX of it. */
494
+ function computeLabelAnchor(edge, collapsed, superNodeIds) {
495
+ if (collapsed.length < 2 || !superNodeIds) {
496
+ return getMidpointFromCollapsed(collapsed);
497
+ }
498
+ const fromIsCluster = superNodeIds.has(edge.from);
499
+ const toIsCluster = superNodeIds.has(edge.to);
500
+ if (fromIsCluster === toIsCluster) {
501
+ // Either both ends are clusters (not yet a real case) or neither —
502
+ // no directional bias to apply.
503
+ return getMidpointFromCollapsed(collapsed);
504
+ }
505
+ const length = getPolylineLength(collapsed);
506
+ if (length <= 0)
507
+ return getMidpointFromCollapsed(collapsed);
508
+ let labelT = 0.5;
509
+ if (toIsCluster) {
510
+ // Cluster is at t=1. Pull label toward t=0 so it stays AVOID_PX from
511
+ // the boundary. Cap above 0.1 so it never collides with the source.
512
+ const maxT = 1 - LABEL_BORDER_AVOID_PX / length;
513
+ labelT = Math.max(0.1, Math.min(0.5, maxT));
514
+ }
515
+ else {
516
+ // Cluster is at t=0. Push label toward t=1.
517
+ const minT = LABEL_BORDER_AVOID_PX / length;
518
+ labelT = Math.min(0.9, Math.max(0.5, minT));
519
+ }
520
+ if (labelT === 0.5)
521
+ return getMidpointFromCollapsed(collapsed);
522
+ return getPointAtFraction(collapsed, labelT).point;
523
+ }
524
+ /** Split a collapsed polyline at the entry/exit intersections with a box centered on a point.
525
+ * The box is placed perpendicular to the segment containing the split point, so the polyline
526
+ * enters one side of the box and exits the opposite side. Returns null if the split is degenerate.
527
+ */
528
+ export function splitCollapsedAtBox(collapsed, segmentIndex, box, axis) {
529
+ if (collapsed.length < 2 || segmentIndex < 1 || segmentIndex >= collapsed.length)
530
+ return null;
531
+ const segFrom = collapsed[segmentIndex - 1];
532
+ const segTo = collapsed[segmentIndex];
533
+ let entryPoint;
534
+ let exitPoint;
535
+ if (axis === 'h') {
536
+ // Horizontal segment — polyline enters left side of box, exits right
537
+ const goingRight = segTo.x > segFrom.x;
538
+ const leftEdge = box.x;
539
+ const rightEdge = box.x + box.width;
540
+ entryPoint = { x: goingRight ? leftEdge : rightEdge, y: segFrom.y };
541
+ exitPoint = { x: goingRight ? rightEdge : leftEdge, y: segFrom.y };
542
+ }
543
+ else {
544
+ // Vertical segment — polyline enters top, exits bottom (or vice versa)
545
+ const goingDown = segTo.y > segFrom.y;
546
+ const topEdge = box.y;
547
+ const bottomEdge = box.y + box.height;
548
+ entryPoint = { x: segFrom.x, y: goingDown ? topEdge : bottomEdge };
549
+ exitPoint = { x: segFrom.x, y: goingDown ? bottomEdge : topEdge };
550
+ }
551
+ // Box edges are already in the snapped frame (derived from box coords which
552
+ // came from collapsed/snapped points). Re-snapping would drift them by 1px.
553
+ const entry = collapsed.slice(0, segmentIndex);
554
+ entry.push(entryPoint);
555
+ const exit = [exitPoint, ...collapsed.slice(segmentIndex)];
556
+ if (entry.length < 2 || exit.length < 2)
557
+ return null;
558
+ return { entry, exit };
270
559
  }
560
+ export { collapsePoints, buildPathFromCollapsed };
271
561
  export function emptyEdge(edge) {
272
562
  return {
273
563
  from: edge.from,