@statelyai/layout 0.0.4 → 0.1.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/README.md +14 -0
- package/dist/elkjs/index.d.mts +44 -1
- package/dist/elkjs/index.mjs +269 -7
- package/dist/index.mjs +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-Bp8MkxWK.mjs → layered-CZs9YWVx.mjs} +204 -28
- package/dist/{spore-ePWz2LtF.mjs → spore-9N0W9dZn.mjs} +1 -1
- package/package.json +2 -1
- package/src/elkjs/index.ts +15 -1
- package/src/elkjs/statechart.ts +338 -0
- package/src/layered/index.ts +99 -28
- package/src/layered/separate-exterior-labels.ts +139 -0
- package/src/layered/strategies.ts +188 -13
|
@@ -392,7 +392,8 @@ function addDepthFirstBackEdges(input, initialReversedEdgeIds, targetOrder) {
|
|
|
392
392
|
const targetId = reversed ? edge.sourceId : edge.targetId;
|
|
393
393
|
if (sourceId === targetId) continue;
|
|
394
394
|
const targetState = state.get(targetId);
|
|
395
|
-
if (targetState === "active") reversedEdgeIds.
|
|
395
|
+
if (targetState === "active") if (reversed) reversedEdgeIds.delete(edge.id);
|
|
396
|
+
else reversedEdgeIds.add(edge.id);
|
|
396
397
|
else if (targetState === void 0) {
|
|
397
398
|
state.set(targetId, "active");
|
|
398
399
|
stack.push({
|
|
@@ -664,7 +665,7 @@ function applyLayerConstraintOrientation(input, orientation) {
|
|
|
664
665
|
if (reversedEdgeIds.has(edge.id)) reversedEdgeIds.delete(edge.id);
|
|
665
666
|
else reversedEdgeIds.add(edge.id);
|
|
666
667
|
}
|
|
667
|
-
return { reversedEdgeIds };
|
|
668
|
+
return { reversedEdgeIds: addDepthFirstBackEdges(input, reversedEdgeIds, "model") };
|
|
668
669
|
}
|
|
669
670
|
/** Keep activated ELK partitions in ascending, contiguous layer blocks. */
|
|
670
671
|
function applyPartitions(input, assignment) {
|
|
@@ -1730,7 +1731,7 @@ function applyDirectionCongruency(input, order) {
|
|
|
1730
1731
|
return { layers: order.layers.map((layer) => [...layer].reverse()) };
|
|
1731
1732
|
}
|
|
1732
1733
|
/** ELK post-placement one-dimensional graph compaction. */
|
|
1733
|
-
function applyPostCompaction(input, placement, routes) {
|
|
1734
|
+
function applyPostCompaction(input, placement, routes, labelEdges = input.graph.edges) {
|
|
1734
1735
|
const strategy = input.settings["compaction.postCompaction.strategy"] ?? "NONE";
|
|
1735
1736
|
input.settings["compaction.postCompaction.constraints"];
|
|
1736
1737
|
if (strategy === "NONE") return placement;
|
|
@@ -1778,7 +1779,15 @@ function applyPostCompaction(input, placement, routes) {
|
|
|
1778
1779
|
}
|
|
1779
1780
|
}
|
|
1780
1781
|
const verticalSpacing = (left, right) => left.kind === "node" && right.kind === "node" ? input.spacing.node : left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId ? 1 : Number(input.settings[left.kind === "segment" && right.kind === "segment" ? "spacing.edgeEdge" : "spacing.edgeNode"] ?? 10);
|
|
1781
|
-
const
|
|
1782
|
+
const inlineLabelSpaceByPair = /* @__PURE__ */ new Map();
|
|
1783
|
+
if (strategy !== "EDGE_LENGTH" && (input.direction === "left" || input.direction === "right")) for (const edge of labelEdges) {
|
|
1784
|
+
const settings = input.edgeSettings?.(edge);
|
|
1785
|
+
if (edge.sourceId === edge.targetId || settings?.["edgeLabels.inline"] !== true || (settings["edgeLabels.placement"] ?? "CENTER") !== "CENTER") continue;
|
|
1786
|
+
const key = [edge.sourceId, edge.targetId].sort().join("\0");
|
|
1787
|
+
inlineLabelSpaceByPair.set(key, Math.max(inlineLabelSpaceByPair.get(key) ?? 0, (edge.width ?? 0) + 2 * input.spacing.layer));
|
|
1788
|
+
}
|
|
1789
|
+
const labelSpace = (left, right) => left.nodeId !== void 0 && right.nodeId !== void 0 ? inlineLabelSpaceByPair.get([left.nodeId, right.nodeId].sort().join("\0")) ?? 0 : 0;
|
|
1790
|
+
const horizontalSpacing = (left, right) => left.kind === "node" && right.kind === "node" ? Math.max(input.spacing.node, labelSpace(left, right)) : left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId ? 0 : Number(input.settings[left.kind === "segment" && right.kind === "segment" ? "spacing.edgeEdge" : "spacing.edgeNode"] ?? 10);
|
|
1782
1791
|
const centerLabeledEdgeIds = new Set(input.graph.edges.filter((edge) => (edge.width ?? 0) > 0 && (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER").map((edge) => edge.id));
|
|
1783
1792
|
const labelFlowLocked = new Set(compactables.filter((item) => item.kind === "segment" && item.edgeId !== void 0 && centerLabeledEdgeIds.has(item.edgeId)).map((item) => item.id));
|
|
1784
1793
|
const compact$1 = (direction, locked = /* @__PURE__ */ new Set()) => {
|
|
@@ -1794,7 +1803,7 @@ function applyPostCompaction(input, placement, routes) {
|
|
|
1794
1803
|
if (left === right) continue;
|
|
1795
1804
|
const ordered = right.x > left.x || right.x === left.x && left.width < right.width;
|
|
1796
1805
|
const verticalCollision = right.y + right.height + verticalSpacing(left, right) > left.y + 1e-9 && right.y < left.y + left.height + verticalSpacing(left, right) - 1e-9;
|
|
1797
|
-
if (!ordered || !verticalCollision) continue;
|
|
1806
|
+
if (!ordered || !verticalCollision && labelSpace(left, right) === 0) continue;
|
|
1798
1807
|
constraints.get(left.id).push(right.id);
|
|
1799
1808
|
incoming.set(right.id, (incoming.get(right.id) ?? 0) + 1);
|
|
1800
1809
|
}
|
|
@@ -1884,7 +1893,7 @@ function applyPostCompaction(input, placement, routes) {
|
|
|
1884
1893
|
const itemByNodeId = new Map(compactables.flatMap((item) => item.kind === "node" && item.nodeId ? [[item.nodeId, item]] : []));
|
|
1885
1894
|
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
1886
1895
|
for (const edge of input.graph.edges) {
|
|
1887
|
-
if (!centerLabeledEdgeIds.has(edge.id)) continue;
|
|
1896
|
+
if (!centerLabeledEdgeIds.has(edge.id) || strategy !== "EDGE_LENGTH" && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true) continue;
|
|
1888
1897
|
const source = itemByNodeId.get(edge.sourceId);
|
|
1889
1898
|
const target = itemByNodeId.get(edge.targetId);
|
|
1890
1899
|
const originalEndpoints = originalEndpointsByEdgeId.get(edge.id);
|
|
@@ -1933,7 +1942,13 @@ function applyPostCompaction(input, placement, routes) {
|
|
|
1933
1942
|
...originalEndpoints.end,
|
|
1934
1943
|
x: originalEndpoints.end.x + (nodeDeltaById.get(edge.targetId) ?? 0)
|
|
1935
1944
|
};
|
|
1936
|
-
if (routes.outsideFeedbackEdgeIds?.has(edgeId)) {
|
|
1945
|
+
if (edge.sourceId === edge.targetId && routes.outsideFeedbackEdgeIds?.has(edgeId)) {
|
|
1946
|
+
const delta = nodeDeltaById.get(edge.sourceId) ?? 0;
|
|
1947
|
+
for (let index = 1; index + 1 < points.length; index++) points[index] = {
|
|
1948
|
+
...points[index],
|
|
1949
|
+
x: points[index].x + delta
|
|
1950
|
+
};
|
|
1951
|
+
} else if (routes.outsideFeedbackEdgeIds?.has(edgeId)) {
|
|
1937
1952
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
1938
1953
|
const startDelta = points[0].x - originalEndpoints.start.x;
|
|
1939
1954
|
const endDelta = points.at(-1).x - originalEndpoints.end.x;
|
|
@@ -2350,6 +2365,34 @@ function routeEdges(style) {
|
|
|
2350
2365
|
});
|
|
2351
2366
|
}
|
|
2352
2367
|
}
|
|
2368
|
+
const loopSide = (distribution, index, edge) => {
|
|
2369
|
+
const node = edge ? nodeById.get(edge.sourceId) : void 0;
|
|
2370
|
+
const sourcePort = node?.ports?.find((port) => port.name === edge?.sourcePort);
|
|
2371
|
+
const targetPort = node?.ports?.find((port) => port.name === edge?.targetPort);
|
|
2372
|
+
const sourceSide = node && sourcePort ? input.portSettings?.(sourcePort, node)?.["port.side"] : void 0;
|
|
2373
|
+
const targetSide = node && targetPort ? input.portSettings?.(targetPort, node)?.["port.side"] : void 0;
|
|
2374
|
+
if (sourceSide && sourceSide === targetSide && [
|
|
2375
|
+
"NORTH",
|
|
2376
|
+
"SOUTH",
|
|
2377
|
+
"EAST",
|
|
2378
|
+
"WEST"
|
|
2379
|
+
].includes(sourceSide)) return sourceSide;
|
|
2380
|
+
return distribution === "EQUALLY" ? [
|
|
2381
|
+
"NORTH",
|
|
2382
|
+
"SOUTH",
|
|
2383
|
+
"EAST",
|
|
2384
|
+
"WEST"
|
|
2385
|
+
][index % 4] : distribution === "NORTH_SOUTH" && index % 2 === 1 ? "SOUTH" : "NORTH";
|
|
2386
|
+
};
|
|
2387
|
+
const loopDistance = (loops, edge, side, ordering) => {
|
|
2388
|
+
const axis = side === "NORTH" || side === "SOUTH" ? "height" : "width";
|
|
2389
|
+
const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
|
|
2390
|
+
const index = ordered.indexOf(edge);
|
|
2391
|
+
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
2392
|
+
const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
2393
|
+
const size = (candidate) => input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true ? candidate[axis] ?? 0 : 0;
|
|
2394
|
+
return spacing * (ordering === "SEQUENCED" ? 1 : index + 1) + ordered.slice(0, index).reduce((extent$1, candidate) => extent$1 + (size(candidate) > 0 ? size(candidate) + labelSpacing : 0), 0) + (size(edge) > 0 ? size(edge) / 2 + labelSpacing : 0);
|
|
2395
|
+
};
|
|
2353
2396
|
const northReserveByLayer = /* @__PURE__ */ new Map();
|
|
2354
2397
|
for (const [id, loops] of selfLoopsByNodeId) {
|
|
2355
2398
|
const rect = mutableRects.get(id);
|
|
@@ -2379,6 +2422,41 @@ function routeEdges(style) {
|
|
|
2379
2422
|
y: candidateRect.y + reserve
|
|
2380
2423
|
});
|
|
2381
2424
|
}
|
|
2425
|
+
if (style === "ORTHOGONAL") for (const axis of ["x", "y"]) {
|
|
2426
|
+
const size = axis === "x" ? "width" : "height";
|
|
2427
|
+
const reservations = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const [id, loops] of selfLoopsByNodeId) {
|
|
2429
|
+
const rect = mutableRects.get(id);
|
|
2430
|
+
const node = nodeById.get(id);
|
|
2431
|
+
if (!rect || !node) continue;
|
|
2432
|
+
const settings = input.nodeSettings?.(node);
|
|
2433
|
+
const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
|
|
2434
|
+
const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
|
|
2435
|
+
for (const [index, edge] of loops.entries()) {
|
|
2436
|
+
if (input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true) continue;
|
|
2437
|
+
const side = loopSide(distribution, index, edge);
|
|
2438
|
+
if (axis === "y" !== (side === "NORTH" || side === "SOUTH")) continue;
|
|
2439
|
+
const extent$1 = loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering) + (edge[size] ?? 0) / 2;
|
|
2440
|
+
const negative = side === "NORTH" || side === "WEST";
|
|
2441
|
+
const boundary = rect[axis] + (negative ? 0 : rect[size]);
|
|
2442
|
+
const neighboringBounds = [...mutableRects.entries()].flatMap(([otherId, other]) => {
|
|
2443
|
+
if (otherId === id) return [];
|
|
2444
|
+
const bound = other[axis] + (negative ? other[size] : 0);
|
|
2445
|
+
return negative ? bound <= boundary ? [bound] : [] : bound >= boundary ? [bound] : [];
|
|
2446
|
+
});
|
|
2447
|
+
const existingGap = neighboringBounds.length === 0 ? 0 : negative ? boundary - Math.max(...neighboringBounds) : Math.min(...neighboringBounds) - boundary;
|
|
2448
|
+
const required = Math.max(0, extent$1 + (neighboringBounds.length > 0 ? input.spacing.node : 0) - existingGap);
|
|
2449
|
+
reservations.set(boundary, Math.max(reservations.get(boundary) ?? 0, required));
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
for (const [id, rect] of mutableRects) {
|
|
2453
|
+
const shift = [...reservations].reduce((sum, [boundary, extent$1]) => sum + (rect[axis] >= boundary - 1e-9 ? extent$1 : 0), 0);
|
|
2454
|
+
if (shift > 0) mutableRects.set(id, {
|
|
2455
|
+
...rect,
|
|
2456
|
+
[axis]: rect[axis] + shift
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2382
2460
|
const edgeLabelSideSelection = input.settings["edgeLabels.sideSelection"] ?? "SMART_DOWN";
|
|
2383
2461
|
if (edgeLabelSideSelection === "ALWAYS_UP" || edgeLabelSideSelection === "SMART_UP" || edgeLabelSideSelection === "DIRECTION_UP") {
|
|
2384
2462
|
let crossShift = 0;
|
|
@@ -2671,7 +2749,7 @@ function routeEdges(style) {
|
|
|
2671
2749
|
const slots = slotsByGap[layerNo] ?? 0;
|
|
2672
2750
|
const routesNearTarget = candidatesByGap[layerNo]?.some((candidate) => hasZeroFixedSideTarget(candidate.edge) && candidatesByGap[layerNo].filter(({ edge }) => edge.targetId === candidate.edge.targetId).length > candidatesByGap[layerNo].filter(({ edge }) => edge.sourceId === candidate.edge.sourceId).length);
|
|
2673
2751
|
const preservedGap = (existingGapByLayer[layerNo] ?? input.spacing.layer) - (routesNearTarget ? edgeEdgeSpacing : 0);
|
|
2674
|
-
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2752
|
+
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 || [...selfLoopsByNodeId].some(([id, loops]) => (flowLayerByNodeId.get(id) === layerNo || flowLayerByNodeId.get(id) === layerNo + 1) && loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2675
2753
|
nextStart = bounds.end + gapSpacing;
|
|
2676
2754
|
}
|
|
2677
2755
|
implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
@@ -2935,7 +3013,7 @@ function routeEdges(style) {
|
|
|
2935
3013
|
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
2936
3014
|
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
2937
3015
|
};
|
|
2938
|
-
const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide || hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide);
|
|
3016
|
+
const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (source.id !== target.id || input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true) && (hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide || hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide);
|
|
2939
3017
|
if (input.settings.feedbackEdges === true && reversedEdge || fixedSideFeedback) {
|
|
2940
3018
|
if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
|
|
2941
3019
|
const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
|
|
@@ -3066,6 +3144,7 @@ function routeEdges(style) {
|
|
|
3066
3144
|
continue;
|
|
3067
3145
|
}
|
|
3068
3146
|
if (source.id === target.id) {
|
|
3147
|
+
if (style === "ORTHOGONAL" && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true) outsideFeedbackEdgeIds.add(edge.id);
|
|
3069
3148
|
const loops = selfLoopsByNodeId.get(source.id) ?? [edge];
|
|
3070
3149
|
const loopIndex = loops.indexOf(edge);
|
|
3071
3150
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
@@ -3093,7 +3172,7 @@ function routeEdges(style) {
|
|
|
3093
3172
|
});
|
|
3094
3173
|
const sameSideLoopIndex = sameSideLoops.findIndex((candidate) => candidate.id === edge.id);
|
|
3095
3174
|
const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
3096
|
-
const trackDistance = spacing + sameSideLoops.slice(0, Math.max(0, sameSideLoopIndex)).reduce((extent$1, candidate) => extent$1 + (side === "EAST" || side === "WEST" ? candidate.width ?? 0 : candidate.height ?? 0) + labelSpacing, 0);
|
|
3175
|
+
const trackDistance = spacing + sameSideLoops.slice(0, Math.max(0, sameSideLoopIndex)).reduce((extent$1, candidate) => extent$1 + (side === "EAST" || side === "WEST" ? candidate.width ?? 0 : candidate.height ?? 0) + labelSpacing, 0) + ((side === "NORTH" || side === "SOUTH") && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true ? (edge.height ?? 0) / 2 + labelSpacing : 0);
|
|
3097
3176
|
if (side === "EAST" || side === "WEST") {
|
|
3098
3177
|
const nodeRects = [...placement.rectByNodeId.values()];
|
|
3099
3178
|
const minimumNodeX = Math.min(...nodeRects.map((rect) => rect.x));
|
|
@@ -3146,7 +3225,6 @@ function routeEdges(style) {
|
|
|
3146
3225
|
const denominator = countOnSide * 2 + 1;
|
|
3147
3226
|
const sequenced = ordering === "SEQUENCED";
|
|
3148
3227
|
const nestingIndex = ordering === "REVERSE_STACKED" ? countOnSide - indexOnSide - 1 : indexOnSide;
|
|
3149
|
-
const trackIndex = sequenced ? 1 : nestingIndex + 1;
|
|
3150
3228
|
let startRatio;
|
|
3151
3229
|
let endRatio;
|
|
3152
3230
|
if (sequenced) if (side === "NORTH") {
|
|
@@ -3163,7 +3241,8 @@ function routeEdges(style) {
|
|
|
3163
3241
|
startRatio = (countOnSide + nestingIndex + 1) / denominator;
|
|
3164
3242
|
endRatio = (countOnSide - nestingIndex) / denominator;
|
|
3165
3243
|
}
|
|
3166
|
-
const
|
|
3244
|
+
const distance = loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering);
|
|
3245
|
+
const y = side === "NORTH" ? sourceRect.y - distance : sourceRect.y + sourceRect.height + distance;
|
|
3167
3246
|
const start$2 = {
|
|
3168
3247
|
x: sourceRect.x + sourceRect.width * startRatio,
|
|
3169
3248
|
y: side === "NORTH" ? sourceRect.y : sourceRect.y + sourceRect.height
|
|
@@ -3192,7 +3271,8 @@ function routeEdges(style) {
|
|
|
3192
3271
|
const secondRatio = sequenced ? 1 / denominator : 2 / denominator;
|
|
3193
3272
|
const startRatio = side === "EAST" ? firstRatio : secondRatio;
|
|
3194
3273
|
const endRatio = side === "EAST" ? secondRatio : firstRatio;
|
|
3195
|
-
const
|
|
3274
|
+
const distance = input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true ? loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering) : spacing * (indexOnSide + 1);
|
|
3275
|
+
const x = side === "EAST" ? sourceRect.x + sourceRect.width + distance : sourceRect.x - distance;
|
|
3196
3276
|
const start$2 = {
|
|
3197
3277
|
x: side === "EAST" ? sourceRect.x + sourceRect.width : sourceRect.x,
|
|
3198
3278
|
y: sourceRect.y + sourceRect.height * startRatio
|
|
@@ -3699,6 +3779,77 @@ function getPolylineMidpoint(points) {
|
|
|
3699
3779
|
};
|
|
3700
3780
|
}
|
|
3701
3781
|
|
|
3782
|
+
//#endregion
|
|
3783
|
+
//#region src/layered/separate-exterior-labels.ts
|
|
3784
|
+
/** Separates movable inline labels and their exterior orthogonal route tracks. */
|
|
3785
|
+
function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings }) {
|
|
3786
|
+
const horizontalFlow = direction === "left" || direction === "right";
|
|
3787
|
+
const nodeCrossStart = Math.min(...nodeRects.map((rect) => horizontalFlow ? rect.y : rect.x));
|
|
3788
|
+
const nodeCrossEnd = Math.max(...nodeRects.map((rect) => horizontalFlow ? rect.y + rect.height : rect.x + rect.width));
|
|
3789
|
+
const flowStart = (rect) => horizontalFlow ? rect.x : rect.y;
|
|
3790
|
+
const flowEnd = (rect) => horizontalFlow ? rect.x + rect.width : rect.y + rect.height;
|
|
3791
|
+
const crossStart = (rect) => horizontalFlow ? rect.y : rect.x;
|
|
3792
|
+
const crossEnd = (rect) => horizontalFlow ? rect.y + rect.height : rect.x + rect.width;
|
|
3793
|
+
const overlaps = (left, right) => flowStart(left) < flowEnd(right) && flowEnd(left) > flowStart(right) && crossStart(left) < crossEnd(right) && crossEnd(left) > crossStart(right);
|
|
3794
|
+
const labelRectByEdgeId = new Map(edges.filter((edge) => edge.width > 0 && edge.height > 0).map((edge) => [String(edge.id), {
|
|
3795
|
+
x: edge.x,
|
|
3796
|
+
y: edge.y,
|
|
3797
|
+
width: edge.width,
|
|
3798
|
+
height: edge.height
|
|
3799
|
+
}]));
|
|
3800
|
+
const movableLabels = edges.flatMap((edge) => {
|
|
3801
|
+
const labelRect = labelRectByEdgeId.get(String(edge.id));
|
|
3802
|
+
const labelSettings = settings(edge);
|
|
3803
|
+
if (!labelRect || !labelSettings.inline || labelSettings.placement !== "CENTER") return [];
|
|
3804
|
+
const exteriorTrack = edge.points.flatMap((point, index) => {
|
|
3805
|
+
const next = edge.points[index + 1];
|
|
3806
|
+
if (!next) return [];
|
|
3807
|
+
const isFlowSegment = horizontalFlow ? point.y === next.y && point.x !== next.x : point.x === next.x && point.y !== next.y;
|
|
3808
|
+
const trackCross = horizontalFlow ? point.y : point.x;
|
|
3809
|
+
return isFlowSegment && (trackCross < nodeCrossStart || trackCross > nodeCrossEnd) ? [{
|
|
3810
|
+
cross: trackCross,
|
|
3811
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y)
|
|
3812
|
+
}] : [];
|
|
3813
|
+
}).sort((left, right) => right.length - left.length)[0];
|
|
3814
|
+
if (!exteriorTrack) return [];
|
|
3815
|
+
return [{
|
|
3816
|
+
edge,
|
|
3817
|
+
exteriorTrack,
|
|
3818
|
+
labelRect,
|
|
3819
|
+
lowSide: exteriorTrack.cross < (nodeCrossStart + nodeCrossEnd) / 2
|
|
3820
|
+
}];
|
|
3821
|
+
});
|
|
3822
|
+
movableLabels.sort((left, right) => {
|
|
3823
|
+
if (left.lowSide !== right.lowSide) return left.lowSide ? -1 : 1;
|
|
3824
|
+
return (left.lowSide ? crossStart(left.labelRect) - crossStart(right.labelRect) : crossStart(right.labelRect) - crossStart(left.labelRect)) || flowStart(left.labelRect) - flowStart(right.labelRect) || (String(left.edge.id) < String(right.edge.id) ? -1 : String(left.edge.id) > String(right.edge.id) ? 1 : 0);
|
|
3825
|
+
});
|
|
3826
|
+
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
3827
|
+
const obstacles = [...nodeRects, ...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) => edgeId === String(edge.id) ? [] : [rect])];
|
|
3828
|
+
let totalDelta = 0;
|
|
3829
|
+
while (true) {
|
|
3830
|
+
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
3831
|
+
if (blockers.length === 0) break;
|
|
3832
|
+
const delta = (lowSide ? Math.min(...blockers.map(crossStart)) - spacing - (horizontalFlow ? edge.height : edge.width) : Math.max(...blockers.map(crossEnd)) + spacing) - crossStart(labelRect);
|
|
3833
|
+
totalDelta += delta;
|
|
3834
|
+
if (horizontalFlow) labelRect.y += delta;
|
|
3835
|
+
else labelRect.x += delta;
|
|
3836
|
+
}
|
|
3837
|
+
if (totalDelta === 0) continue;
|
|
3838
|
+
if (horizontalFlow) edge.y += totalDelta;
|
|
3839
|
+
else edge.x += totalDelta;
|
|
3840
|
+
edge.points = edge.points.map((point) => {
|
|
3841
|
+
if ((horizontalFlow ? point.y : point.x) !== exteriorTrack.cross) return point;
|
|
3842
|
+
return horizontalFlow ? {
|
|
3843
|
+
...point,
|
|
3844
|
+
y: point.y + totalDelta
|
|
3845
|
+
} : {
|
|
3846
|
+
...point,
|
|
3847
|
+
x: point.x + totalDelta
|
|
3848
|
+
};
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3702
3853
|
//#endregion
|
|
3703
3854
|
//#region src/layered/network-simplex.ts
|
|
3704
3855
|
function connectedEdges(node) {
|
|
@@ -8691,30 +8842,37 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8691
8842
|
}
|
|
8692
8843
|
{
|
|
8693
8844
|
const horizontal = direction === "right" || direction === "left";
|
|
8845
|
+
const targetsBySource = /* @__PURE__ */ new Map();
|
|
8846
|
+
for (const edge of graph.edges) {
|
|
8847
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
8848
|
+
const targets = targetsBySource.get(edge.sourceId) ?? /* @__PURE__ */ new Set();
|
|
8849
|
+
targets.add(edge.targetId);
|
|
8850
|
+
targetsBySource.set(edge.sourceId, targets);
|
|
8851
|
+
}
|
|
8694
8852
|
for (const edge of graph.edges) {
|
|
8695
8853
|
if (edge.sourcePort === void 0) continue;
|
|
8696
8854
|
const source = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
8697
8855
|
const targetRect = mutableRects.get(edge.targetId);
|
|
8698
8856
|
const sourceRect = mutableRects.get(edge.sourceId);
|
|
8699
|
-
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
8857
|
+
const port = source?.ports?.find((candidate$1) => candidate$1.name === edge.sourcePort);
|
|
8700
8858
|
if (!source || !sourceRect || !targetRect || !port) continue;
|
|
8701
8859
|
const side = input.portSettings?.(port, source)?.["port.side"];
|
|
8702
|
-
if (
|
|
8860
|
+
if (edge.sourceId === edge.targetId || (targetsBySource.get(edge.sourceId)?.size ?? 0) > 1) continue;
|
|
8861
|
+
const candidate = horizontal && side === "NORTH" ? {
|
|
8703
8862
|
...targetRect,
|
|
8704
8863
|
y: sourceRect.y - (port.height ?? 0) - targetRect.height
|
|
8705
|
-
}
|
|
8706
|
-
else if (horizontal && side === "SOUTH") mutableRects.set(edge.targetId, {
|
|
8864
|
+
} : horizontal && side === "SOUTH" ? {
|
|
8707
8865
|
...targetRect,
|
|
8708
8866
|
y: sourceRect.y + sourceRect.height + (port.height ?? 0)
|
|
8709
|
-
}
|
|
8710
|
-
else if (!horizontal && side === "WEST") mutableRects.set(edge.targetId, {
|
|
8867
|
+
} : !horizontal && side === "WEST" ? {
|
|
8711
8868
|
...targetRect,
|
|
8712
8869
|
x: sourceRect.x - (port.width ?? 0) - targetRect.width
|
|
8713
|
-
}
|
|
8714
|
-
else if (!horizontal && side === "EAST") mutableRects.set(edge.targetId, {
|
|
8870
|
+
} : !horizontal && side === "EAST" ? {
|
|
8715
8871
|
...targetRect,
|
|
8716
8872
|
x: sourceRect.x + sourceRect.width + (port.width ?? 0)
|
|
8717
|
-
}
|
|
8873
|
+
} : void 0;
|
|
8874
|
+
if (!candidate || [...mutableRects].some(([id, other]) => id !== edge.targetId && other.width > 0 && other.height > 0 && candidate.x < other.x + other.width && candidate.x + candidate.width > other.x && candidate.y < other.y + other.height && candidate.y + candidate.height > other.y)) continue;
|
|
8875
|
+
mutableRects.set(edge.targetId, candidate);
|
|
8718
8876
|
}
|
|
8719
8877
|
}
|
|
8720
8878
|
measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation));
|
|
@@ -8743,7 +8901,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8743
8901
|
const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
|
|
8744
8902
|
const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
|
|
8745
8903
|
let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
|
|
8746
|
-
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
8904
|
+
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes, graph.edges));
|
|
8747
8905
|
const antiparallelLabelPositions = /* @__PURE__ */ new Map();
|
|
8748
8906
|
const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
|
|
8749
8907
|
const parallelLabelPositions = /* @__PURE__ */ new Map();
|
|
@@ -9248,10 +9406,10 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9248
9406
|
const constraints = node ? options.nodeSettings?.(node)?.portConstraints : void 0;
|
|
9249
9407
|
return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
|
|
9250
9408
|
};
|
|
9251
|
-
const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
|
|
9409
|
+
const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && edge.sourceId !== edge.targetId && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
|
|
9252
9410
|
const [beforeFlowRect, afterFlowRect] = sourceRect && targetRect ? horizontal ? sourceRect.x <= targetRect.x ? [sourceRect, targetRect] : [targetRect, sourceRect] : sourceRect.y <= targetRect.y ? [sourceRect, targetRect] : [targetRect, sourceRect] : [void 0, void 0];
|
|
9253
9411
|
const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
|
|
9254
|
-
const
|
|
9412
|
+
const outerAntiparallelLabelPosition = (() => {
|
|
9255
9413
|
if (!outerAntiparallelLabelIds.has(edge.id)) return void 0;
|
|
9256
9414
|
const edgeNodeSpacing$1 = Number(options.settings?.["spacing.edgeNode"] ?? 10);
|
|
9257
9415
|
if (horizontal) {
|
|
@@ -9276,9 +9434,14 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9276
9434
|
x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing$1,
|
|
9277
9435
|
y: y$1
|
|
9278
9436
|
};
|
|
9279
|
-
})()
|
|
9280
|
-
const
|
|
9281
|
-
const
|
|
9437
|
+
})();
|
|
9438
|
+
const centerFlowPosition = options.settings?.["compaction.postCompaction.strategy"] !== "EDGE_LENGTH" && labelDummyRect === void 0 && edgeRouting === "ORTHOGONAL" && inlineLabel && labelPlacement === "CENTER" && edge.sourceId !== edge.targetId && beforeFlowRect && afterFlowRect && (horizontal ? afterFlowRect.x - beforeFlowRect.x - beforeFlowRect.width >= width : afterFlowRect.y - beforeFlowRect.y - beforeFlowRect.height >= height) ? horizontal ? (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2 : (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2 - edgeThickness / 2 : void 0;
|
|
9439
|
+
const explicitLabelPosition = (edgeRouting === "ORTHOGONAL" && labelPlacement === "CENTER" && inlineLabel && edge.sourceId === edge.targetId && points.length === 4 ? {
|
|
9440
|
+
x: (points[1].x + points[2].x - width) / 2,
|
|
9441
|
+
y: (points[1].y + points[2].y - height) / 2
|
|
9442
|
+
} : void 0) ?? outerAntiparallelLabelPosition ?? antiparallelLabelPosition ?? parallelLabelPositions.get(edge.id);
|
|
9443
|
+
const x = explicitLabelPosition ? explicitLabelPosition.x : flexibleFeedbackLabel ? horizontal ? (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2 : targetRect.x + (targetRect.width - width) / 2 : centerFlowPosition !== void 0 && horizontal ? centerFlowPosition : labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
|
|
9444
|
+
const y = explicitLabelPosition ? explicitLabelPosition.y : flexibleFeedbackLabel ? horizontal ? targetRect.y + (targetRect.height - height) / 2 : (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2 : centerFlowPosition !== void 0 && !horizontal ? centerFlowPosition : labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
|
|
9282
9445
|
return {
|
|
9283
9446
|
...edge,
|
|
9284
9447
|
x,
|
|
@@ -9289,6 +9452,19 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9289
9452
|
routing: edgeRouting === "POLYLINE" ? "polyline" : edgeRouting === "SPLINES" ? "splines" : "orthogonal"
|
|
9290
9453
|
};
|
|
9291
9454
|
});
|
|
9455
|
+
if (edgeRouting === "ORTHOGONAL") separateExteriorLabels({
|
|
9456
|
+
edges,
|
|
9457
|
+
nodeRects: feedbackNodeRects,
|
|
9458
|
+
direction,
|
|
9459
|
+
spacing: Number(options.settings?.["spacing.edgeEdge"] ?? 10),
|
|
9460
|
+
settings: (edge) => {
|
|
9461
|
+
const edgeSettings = options.edgeSettings?.(edge);
|
|
9462
|
+
return {
|
|
9463
|
+
inline: edgeSettings?.["edgeLabels.inline"] === true,
|
|
9464
|
+
placement: edgeSettings?.["edgeLabels.placement"] ?? "CENTER"
|
|
9465
|
+
};
|
|
9466
|
+
}
|
|
9467
|
+
});
|
|
9292
9468
|
return {
|
|
9293
9469
|
...graph,
|
|
9294
9470
|
direction,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-
|
|
1
|
+
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-CZs9YWVx.mjs";
|
|
2
2
|
import { getNodeSize } from "@statelyai/graph/layout";
|
|
3
3
|
|
|
4
4
|
//#region src/fixed.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/layout",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Extensible, graph-native layout algorithms for @statelyai/graph",
|
|
5
5
|
"license": "EPL-2.0 OR GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@changesets/cli": "3.0.1",
|
|
48
48
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
49
49
|
"@codemirror/lint": "^6.9.7",
|
|
50
|
+
"@statelyai/layout-pre-exterior-label-separation": "npm:@statelyai/layout@0.0.4",
|
|
50
51
|
"@types/node": "^25.0.3",
|
|
51
52
|
"@types/react": "^19.2.18",
|
|
52
53
|
"@types/react-dom": "^19.2.4",
|
package/src/elkjs/index.ts
CHANGED
|
@@ -510,6 +510,7 @@ export default class ELK {
|
|
|
510
510
|
if (
|
|
511
511
|
algorithm === "layered" &&
|
|
512
512
|
graph_.edges.some((edge) => {
|
|
513
|
+
if (edge.sourceId === edge.targetId) return false;
|
|
513
514
|
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
514
515
|
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
515
516
|
return (
|
|
@@ -722,10 +723,15 @@ export default class ELK {
|
|
|
722
723
|
}
|
|
723
724
|
const edgeNodeSpacing = getNumberOption(layoutOptions, "spacing.edgeNodeBetweenLayers") ?? 10;
|
|
724
725
|
for (const restoration of hierarchyRestorations) {
|
|
725
|
-
const
|
|
726
|
+
const nativeEdge = laidOut.edges.find((edge) => edge.id === String(restoration.edge.id));
|
|
727
|
+
const route = nativeEdge?.points;
|
|
726
728
|
if (!route || route.length < 2) continue;
|
|
727
729
|
const start = route[0]!;
|
|
728
730
|
const end = route.at(-1)!;
|
|
731
|
+
// Decomposed ancestor/descendant edges are native self-loops. Preserve
|
|
732
|
+
// their reserved exterior route and label placement instead of treating
|
|
733
|
+
// equal endpoint coordinates as a straight inter-node connection.
|
|
734
|
+
if (nativeEdge.sourceId === nativeEdge.targetId) continue;
|
|
729
735
|
if (Math.abs(cross(start) - cross(end)) < 1e-9) {
|
|
730
736
|
route.splice(1, route.length - 2);
|
|
731
737
|
continue;
|
|
@@ -1824,3 +1830,11 @@ function placePortLabels(node: ElkNode, globalOptions: Readonly<Record<string, u
|
|
|
1824
1830
|
}
|
|
1825
1831
|
}
|
|
1826
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
export { compileStatechartLayout, layoutStatechart, scoreStatechartLayout } from "./statechart";
|
|
1835
|
+
export type {
|
|
1836
|
+
StatechartScope,
|
|
1837
|
+
StatechartLayoutOptions,
|
|
1838
|
+
StatechartLayoutScore,
|
|
1839
|
+
StatechartLayoutPlan,
|
|
1840
|
+
} from "./statechart";
|