@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.
- package/dist/alert/{alert-root.svelte → alert.svelte} +78 -20
- package/dist/alert/alert.svelte.d.ts +15 -0
- package/dist/alert/index.d.ts +15 -14
- package/dist/alert/index.js +3 -12
- package/dist/breadcrumb/breadcrumb-link.svelte +1 -1
- package/dist/button/button.svelte +1 -1
- package/dist/card/card-root.svelte +2 -2
- package/dist/chromatic-shift/chromatic-shift.svelte +2 -2
- package/dist/code-block/code-block-button.svelte +1 -1
- package/dist/color-picker/color-picker-area.svelte +2 -2
- package/dist/color-picker/color-picker-channel-input.svelte +2 -2
- package/dist/color-picker/color-picker-input-alpha-slider.svelte +2 -2
- package/dist/color-picker/color-picker-input-hue-slider.svelte +2 -2
- package/dist/color-picker/color-picker-input.svelte +9 -9
- package/dist/color-picker/color-picker-swatch.svelte +2 -2
- package/dist/combobox/combobox-input.svelte +9 -9
- package/dist/command-palette/command-palette-item.svelte +1 -1
- package/dist/data-grid/data-grid-button-input-column.svelte +1 -1
- package/dist/diagram/diagram.svelte +222 -32
- package/dist/diagram/diagram.svelte.d.ts +1 -0
- package/dist/diagram/edge-routing.d.ts +63 -1
- package/dist/diagram/edge-routing.js +316 -26
- package/dist/diagram/layout.js +633 -62
- package/dist/diagram/types.d.ts +58 -0
- package/dist/drag-and-drop/drag-and-drop-handle.svelte +1 -1
- package/dist/drag-and-drop/drag-and-drop-item.svelte +1 -1
- package/dist/file-select/file-select-root.svelte +2 -2
- package/dist/file-upload/file-upload-dropzone.svelte +2 -2
- package/dist/gauge/gauge.svelte +1 -1
- package/dist/image-comparison/image-comparison.svelte +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/input/input.svelte +10 -11
- package/dist/label/label.svelte +1 -1
- package/dist/link/link.svelte +1 -1
- package/dist/list/list-item.svelte +2 -2
- package/dist/multi-select-combobox/multi-select-combobox-selection-item.svelte +9 -3
- package/dist/multi-select-combobox/multi-select-combobox-selection-remove-button.svelte +2 -0
- package/dist/navigation-menu/navigation-menu-link.svelte +1 -1
- package/dist/notification-center/notification-center-item.svelte +1 -1
- package/dist/number-input/number-input-button.svelte +3 -3
- package/dist/option-picker/context.svelte.d.ts +9 -0
- package/dist/option-picker/context.svelte.js +2 -0
- package/dist/option-picker/option-picker-item.svelte +31 -4
- package/dist/option-picker/option-picker-preview.svelte +2 -2
- package/dist/option-picker/option-picker-root.svelte +2 -2
- package/dist/phone-input/phone-input-select.svelte +2 -2
- package/dist/pin-input/pin-input-cell.svelte +1 -1
- package/dist/pin-input/pin-input-root.svelte +1 -1
- package/dist/progress/progress.svelte +1 -1
- package/dist/scroll-area/scroll-area.svelte +1 -1
- package/dist/shimmer/index.d.ts +8 -0
- package/dist/shimmer/index.js +1 -0
- package/dist/shimmer/shimmer.svelte +87 -0
- package/dist/shimmer/shimmer.svelte.d.ts +10 -0
- package/dist/sidebar/sidebar-item.svelte +1 -1
- package/dist/slider/slider-input.svelte +2 -2
- package/dist/splitter/splitter-handle.svelte +1 -1
- package/dist/table-of-contents/table-of-contents-item.svelte +1 -1
- package/dist/table-of-contents/table-of-contents-list.svelte +1 -1
- package/dist/tags-input/tags-input-root.svelte +1 -1
- package/dist/tags-input/tags-input-tag-delete-button.svelte +2 -0
- package/dist/tags-input/tags-input-tag.svelte +9 -3
- package/dist/textarea/textarea.svelte +11 -11
- package/dist/themes/default.css +31 -0
- package/dist/toast/toast-root.svelte +1 -1
- package/dist/tour/tour-root.css +3 -3
- package/dist/tree/tree-item-children.svelte +1 -1
- package/dist/tree/tree-item-label.svelte +1 -1
- package/dist/video-embed/video-embed-button.svelte +1 -1
- package/package.json +11 -750
- package/skills/dryui/SKILL.md +26 -21
- package/skills/dryui/rules/compound-components.md +3 -3
- package/skills/dryui/rules/theming.md +1 -1
- package/dist/alert/alert-button-close.svelte +0 -29
- package/dist/alert/alert-button-close.svelte.d.ts +0 -8
- package/dist/alert/alert-description.svelte +0 -28
- package/dist/alert/alert-description.svelte.d.ts +0 -8
- package/dist/alert/alert-icon.svelte +0 -26
- package/dist/alert/alert-icon.svelte.d.ts +0 -8
- package/dist/alert/alert-root.svelte.d.ts +0 -12
- package/dist/alert/alert-title.svelte +0 -29
- package/dist/alert/alert-title.svelte.d.ts +0 -8
- package/dist/alert/context.svelte.d.ts +0 -9
- package/dist/alert/context.svelte.js +0 -10
- package/dist/option-swatch-group/context.svelte.d.ts +0 -9
- package/dist/option-swatch-group/context.svelte.js +0 -2
- package/dist/option-swatch-group/index.d.ts +0 -29
- package/dist/option-swatch-group/index.js +0 -12
- package/dist/option-swatch-group/option-swatch-group-item-button.svelte +0 -214
- package/dist/option-swatch-group/option-swatch-group-item-button.svelte.d.ts +0 -12
- package/dist/option-swatch-group/option-swatch-group-label.svelte +0 -24
- package/dist/option-swatch-group/option-swatch-group-label.svelte.d.ts +0 -8
- package/dist/option-swatch-group/option-swatch-group-meta.svelte +0 -24
- package/dist/option-swatch-group/option-swatch-group-meta.svelte.d.ts +0 -8
- package/dist/option-swatch-group/option-swatch-group-root.svelte +0 -81
- package/dist/option-swatch-group/option-swatch-group-root.svelte.d.ts +0 -12
- package/dist/option-swatch-group/option-swatch-group-swatch.svelte +0 -52
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
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:
|
|
75
|
-
labelY:
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
let remaining = totalLength
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|