@statelyai/layout 0.0.5 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.add(edge.id);
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 horizontalSpacing = (left, right) => left.kind === "node" && right.kind === "node" ? input.spacing.node : 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
+ 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 y = side === "NORTH" ? sourceRect.y - spacing * trackIndex : sourceRect.y + sourceRect.height + spacing * trackIndex;
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 x = side === "EAST" ? sourceRect.x + sourceRect.width + spacing * (indexOnSide + 1) : sourceRect.x - spacing * (indexOnSide + 1);
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
@@ -8762,30 +8842,37 @@ function runLayeredPipeline(graph, options, context) {
8762
8842
  }
8763
8843
  {
8764
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
+ }
8765
8852
  for (const edge of graph.edges) {
8766
8853
  if (edge.sourcePort === void 0) continue;
8767
8854
  const source = graph.nodes.find((node) => node.id === edge.sourceId);
8768
8855
  const targetRect = mutableRects.get(edge.targetId);
8769
8856
  const sourceRect = mutableRects.get(edge.sourceId);
8770
- const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
8857
+ const port = source?.ports?.find((candidate$1) => candidate$1.name === edge.sourcePort);
8771
8858
  if (!source || !sourceRect || !targetRect || !port) continue;
8772
8859
  const side = input.portSettings?.(port, source)?.["port.side"];
8773
- if (horizontal && side === "NORTH") mutableRects.set(edge.targetId, {
8860
+ if (edge.sourceId === edge.targetId || (targetsBySource.get(edge.sourceId)?.size ?? 0) > 1) continue;
8861
+ const candidate = horizontal && side === "NORTH" ? {
8774
8862
  ...targetRect,
8775
8863
  y: sourceRect.y - (port.height ?? 0) - targetRect.height
8776
- });
8777
- else if (horizontal && side === "SOUTH") mutableRects.set(edge.targetId, {
8864
+ } : horizontal && side === "SOUTH" ? {
8778
8865
  ...targetRect,
8779
8866
  y: sourceRect.y + sourceRect.height + (port.height ?? 0)
8780
- });
8781
- else if (!horizontal && side === "WEST") mutableRects.set(edge.targetId, {
8867
+ } : !horizontal && side === "WEST" ? {
8782
8868
  ...targetRect,
8783
8869
  x: sourceRect.x - (port.width ?? 0) - targetRect.width
8784
- });
8785
- else if (!horizontal && side === "EAST") mutableRects.set(edge.targetId, {
8870
+ } : !horizontal && side === "EAST" ? {
8786
8871
  ...targetRect,
8787
8872
  x: sourceRect.x + sourceRect.width + (port.width ?? 0)
8788
- });
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);
8789
8876
  }
8790
8877
  }
8791
8878
  measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation));
@@ -8814,7 +8901,7 @@ function runLayeredPipeline(graph, options, context) {
8814
8901
  const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
8815
8902
  const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
8816
8903
  let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8817
- measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
8904
+ measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes, graph.edges));
8818
8905
  const antiparallelLabelPositions = /* @__PURE__ */ new Map();
8819
8906
  const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
8820
8907
  const parallelLabelPositions = /* @__PURE__ */ new Map();
@@ -9319,10 +9406,10 @@ function runLayeredPipeline(graph, options, context) {
9319
9406
  const constraints = node ? options.nodeSettings?.(node)?.portConstraints : void 0;
9320
9407
  return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
9321
9408
  };
9322
- 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;
9323
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];
9324
9411
  const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
9325
- const explicitLabelPosition = (() => {
9412
+ const outerAntiparallelLabelPosition = (() => {
9326
9413
  if (!outerAntiparallelLabelIds.has(edge.id)) return void 0;
9327
9414
  const edgeNodeSpacing$1 = Number(options.settings?.["spacing.edgeNode"] ?? 10);
9328
9415
  if (horizontal) {
@@ -9347,9 +9434,14 @@ function runLayeredPipeline(graph, options, context) {
9347
9434
  x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing$1,
9348
9435
  y: y$1
9349
9436
  };
9350
- })() ?? antiparallelLabelPosition ?? parallelLabelPositions.get(edge.id);
9351
- const x = explicitLabelPosition ? explicitLabelPosition.x : flexibleFeedbackLabel ? horizontal ? (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2 : targetRect.x + (targetRect.width - width) / 2 : labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
9352
- const y = explicitLabelPosition ? explicitLabelPosition.y : flexibleFeedbackLabel ? horizontal ? targetRect.y + (targetRect.height - height) / 2 : (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2 : labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
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;
9353
9445
  return {
9354
9446
  ...edge,
9355
9447
  x,
@@ -1,4 +1,4 @@
1
- import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-BYhTymAk.mjs";
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.5",
3
+ "version": "0.1.1",
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": {
@@ -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 route = laidOut.edges.find((edge) => edge.id === String(restoration.edge.id))?.points;
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";