@statelyai/layout 0.0.3 → 0.0.4

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 CHANGED
@@ -9,12 +9,15 @@ return `VisualGraph`; positions remain node fields and routes remain
9
9
 
10
10
  ## Status
11
11
 
12
+ <!-- layered compatibility coverage from test/oracle*.test.ts -->
13
+
12
14
  The native layered implementation covers the complete 152-option elkjs 0.11.1
13
15
  layered inventory with one simplified typed name per ELK option. Flat and
14
16
  compound graphs, cross-hierarchy edges, ports, labels, self-loops, wrapping,
15
17
  four directions, constraints, and replaceable phases are differential-tested
16
- against elkjs. Partial, incremental, and route-only layout remain explicit
17
- unimplemented capabilities.
18
+ against elkjs, including a curated complex state-machine corpus in both primary
19
+ layout orientations. Partial, incremental, and route-only layout remain
20
+ explicit unimplemented capabilities.
18
21
 
19
22
  ## Install
20
23
 
@@ -1,8 +1,11 @@
1
- import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-f_3mmGHr.mjs";
2
- import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-BfTduMDc.mjs";
1
+ import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-Bp8MkxWK.mjs";
2
+ import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-ePWz2LtF.mjs";
3
3
  import { createGraph } from "@statelyai/graph";
4
4
 
5
5
  //#region src/elkjs/index.ts
6
+ function isLayoutEdgeLabel(label) {
7
+ return getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true && (Boolean(label.text) || label.width !== void 0 || label.height !== void 0);
8
+ }
6
9
  var ELK = class {
7
10
  #options;
8
11
  #algorithmIds;
@@ -897,7 +900,7 @@ function toGraph(root, globalOptions = {}) {
897
900
  if (isInsideSelfLoop(root, edge)) return [];
898
901
  const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
899
902
  const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
900
- const labels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
903
+ const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
901
904
  const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
902
905
  const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
903
906
  const labelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelLabelSpacing;
@@ -1000,7 +1003,7 @@ function applyLayout(root, graph, padding, layoutOptions) {
1000
1003
  label.y ??= 0;
1001
1004
  continue;
1002
1005
  }
1003
- if (!label.text) {
1006
+ if (!isLayoutEdgeLabel(label)) {
1004
1007
  label.x ??= 0;
1005
1008
  label.y ??= 0;
1006
1009
  continue;
@@ -1026,7 +1029,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1026
1029
  minimumY = Math.min(minimumY, node.y ?? 0, ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0)), ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0))));
1027
1030
  }
1028
1031
  for (const edge of root.edges ?? []) {
1029
- const laidOutLabels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
1032
+ const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
1030
1033
  minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
1031
1034
  minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
1032
1035
  if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
@@ -1055,7 +1058,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1055
1058
  point.x += shiftX;
1056
1059
  point.y += shiftY;
1057
1060
  }
1058
- for (const label of (edge.labels ?? []).filter((candidate) => Boolean(candidate.text))) {
1061
+ for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
1059
1062
  label.x = (label.x ?? 0) + shiftX;
1060
1063
  label.y = (label.y ?? 0) + shiftY;
1061
1064
  }
@@ -1091,7 +1094,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1091
1094
  ...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
1092
1095
  ...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
1093
1096
  ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
1094
- ]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true).map((label) => (label.x ?? 0) + (label.width ?? 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
1097
+ ]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.x ?? 0) + (label.width ?? 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
1095
1098
  section.startPoint.x,
1096
1099
  ...(section.bendPoints ?? []).map((point) => point.x),
1097
1100
  section.endPoint.x
@@ -1101,7 +1104,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1101
1104
  ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
1102
1105
  ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
1103
1106
  ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
1104
- ]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true).map((label) => (label.y ?? 0) + (label.height ?? 0) + (String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") === "CENTER" ? 1 : 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
1107
+ ]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.y ?? 0) + (label.height ?? 0) + (String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") === "CENTER" ? 1 : 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
1105
1108
  section.startPoint.y,
1106
1109
  ...(section.bendPoints ?? []).map((point) => point.y),
1107
1110
  section.endPoint.y
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-f_3mmGHr.mjs";
2
- import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-BfTduMDc.mjs";
1
+ import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-Bp8MkxWK.mjs";
2
+ import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-ePWz2LtF.mjs";
3
3
  import { getGraphIssues } from "@statelyai/graph";
4
4
 
5
5
  //#region src/layout.ts
@@ -1,3 +1,3 @@
1
- import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-f_3mmGHr.mjs";
1
+ import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-Bp8MkxWK.mjs";
2
2
 
3
3
  export { assignLayersByBreadthFirstModelOrder, assignLayersByDepthFirstModelOrder, assignLayersByLongestPath, assignLayersByLongestPathToSink, assignLayersInteractively, assignLayersWithCoffmanGraham, assignLayersWithMinWidth, assignLayersWithNetworkSimplex, assignLayersWithStretchWidth, breakCyclesByModelOrder, breakCyclesByStronglyConnectedConnectivity, breakCyclesByStronglyConnectedNodeType, breakCyclesGreedily, breakCyclesGreedilyByModelOrder, breakCyclesInteractively, breakCyclesWithDepthFirstSearch, breakCyclesWithModelOrderBreadthFirstSearch, breakCyclesWithModelOrderDepthFirstSearch, elkLayeredEnumValues, elkLayeredOptionDefinitions, fromElkLayeredOptionId, getLayeredLayout, layeredAlgorithm, minimizeCrossingsInteractively, minimizeCrossingsWithBarycenter, minimizeCrossingsWithMedian, minimizeCrossingsWithModelOrder, placeNodesInLayers, placeNodesInteractively, placeNodesWithBrandesKoepf, placeNodesWithLinearSegments, placeNodesWithNetworkSimplex, routeEdgesOrthogonally, routeEdgesWithPolylines, routeEdgesWithSplines, toElkLayeredOptions };
@@ -8745,6 +8745,7 @@ function runLayeredPipeline(graph, options, context) {
8745
8745
  let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8746
8746
  measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
8747
8747
  const antiparallelLabelPositions = /* @__PURE__ */ new Map();
8748
+ const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
8748
8749
  const parallelLabelPositions = /* @__PURE__ */ new Map();
8749
8750
  const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
8750
8751
  const horizontalAntiparallelFlow = direction === "left" || direction === "right";
@@ -8834,12 +8835,13 @@ function runLayeredPipeline(graph, options, context) {
8834
8835
  const secondRect = placement.rectByNodeId.get(pair[0].targetId);
8835
8836
  if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
8836
8837
  const nonSelfNeighbors = (nodeId) => new Set(graph.edges.flatMap((edge) => edge.sourceId === edge.targetId ? [] : edge.sourceId === nodeId ? [edge.targetId] : edge.targetId === nodeId ? [edge.sourceId] : []));
8837
- if (nonSelfNeighbors(firstNode.id).size !== 1 || nonSelfNeighbors(secondNode.id).size !== 1) continue;
8838
+ const isolatedAntiparallelPair = nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
8838
8839
  const hasFlexiblePorts = (node) => {
8839
8840
  const constraints = options.nodeSettings?.(node)?.portConstraints;
8840
8841
  return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
8841
8842
  };
8842
- if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
8843
+ const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
8844
+ if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
8843
8845
  const firstBeforeSecond = horizontal ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
8844
8846
  const beforeRect = firstBeforeSecond ? firstRect : secondRect;
8845
8847
  const afterRect = firstBeforeSecond ? secondRect : firstRect;
@@ -8850,13 +8852,20 @@ function runLayeredPipeline(graph, options, context) {
8850
8852
  for (const edge of pair) {
8851
8853
  const width = edge.width ?? 0;
8852
8854
  const height = edge.height ?? 0;
8853
- antiparallelLabelPositions.set(edge.id, horizontal ? {
8855
+ antiparallelLabelPositions.set(edge.id, isolatedAntiparallelPair ? horizontal ? {
8854
8856
  x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
8855
8857
  y: cross
8856
8858
  } : {
8857
8859
  x: cross,
8858
8860
  y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
8861
+ } : horizontal ? {
8862
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
8863
+ y: edge === forward ? (firstRect.y + firstRect.height / 2 + secondRect.y + secondRect.height / 2) / 2 - height / 2 : Math.max(firstRect.y + firstRect.height, secondRect.y + secondRect.height) + edgeSpacing
8864
+ } : {
8865
+ x: edge === forward ? (firstRect.x + firstRect.width / 2 + secondRect.x + secondRect.width / 2) / 2 - width / 2 : Math.max(firstRect.x + firstRect.width, secondRect.x + secondRect.width) + edgeSpacing,
8866
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
8859
8867
  });
8868
+ if (!isolatedAntiparallelPair && edge !== forward) outerAntiparallelLabelIds.add(edge.id);
8860
8869
  cross += (horizontal ? height : width) + edgeSpacing;
8861
8870
  }
8862
8871
  const forwardLabel = antiparallelLabelPositions.get(forward.id);
@@ -8881,8 +8890,10 @@ function runLayeredPipeline(graph, options, context) {
8881
8890
  x: nextCross
8882
8891
  });
8883
8892
  };
8884
- alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
8885
- alignPort(nodesById.get(forward.targetId), forward.targetPort);
8893
+ if (isolatedAntiparallelPair) {
8894
+ alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
8895
+ alignPort(nodesById.get(forward.targetId), forward.targetPort);
8896
+ }
8886
8897
  }
8887
8898
  }
8888
8899
  if (postCompactionNodeCrossDeltas.size > 0) {
@@ -9239,7 +9250,33 @@ function runLayeredPipeline(graph, options, context) {
9239
9250
  };
9240
9251
  const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
9241
9252
  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];
9242
- const explicitLabelPosition = antiparallelLabelPositions.get(edge.id) ?? parallelLabelPositions.get(edge.id);
9253
+ const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
9254
+ const explicitLabelPosition = (() => {
9255
+ if (!outerAntiparallelLabelIds.has(edge.id)) return void 0;
9256
+ const edgeNodeSpacing$1 = Number(options.settings?.["spacing.edgeNode"] ?? 10);
9257
+ if (horizontal) {
9258
+ const x$1 = (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2;
9259
+ const blockers$1 = [
9260
+ sourceRect,
9261
+ targetRect,
9262
+ ...[...placement.rectByNodeId.values()].filter((rect) => rect.x < x$1 + width && rect.x + rect.width > x$1)
9263
+ ];
9264
+ return {
9265
+ x: x$1,
9266
+ y: Math.max(...blockers$1.map((rect) => rect.y + rect.height)) + edgeNodeSpacing$1
9267
+ };
9268
+ }
9269
+ const y$1 = (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2;
9270
+ const blockers = [
9271
+ sourceRect,
9272
+ targetRect,
9273
+ ...[...placement.rectByNodeId.values()].filter((rect) => rect.y < y$1 + height && rect.y + rect.height > y$1)
9274
+ ];
9275
+ return {
9276
+ x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing$1,
9277
+ y: y$1
9278
+ };
9279
+ })() ?? antiparallelLabelPosition ?? parallelLabelPositions.get(edge.id);
9243
9280
  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;
9244
9281
  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;
9245
9282
  return {
@@ -1,4 +1,4 @@
1
- import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-f_3mmGHr.mjs";
1
+ import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-Bp8MkxWK.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",
3
+ "version": "0.0.4",
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": {
@@ -14,6 +14,7 @@ import type {
14
14
  ElkConstructorArguments,
15
15
  ElkLayoutAlgorithmDescription,
16
16
  ElkEdge,
17
+ ElkLabel,
17
18
  ElkLayoutArguments,
18
19
  ElkLayoutCategoryDescription,
19
20
  ElkLayoutOptionDescription,
@@ -25,6 +26,13 @@ import type {
25
26
  LaidOutElkNode,
26
27
  } from "./types";
27
28
 
29
+ function isLayoutEdgeLabel(label: ElkLabel): boolean {
30
+ return (
31
+ getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true &&
32
+ (Boolean(label.text) || label.width !== undefined || label.height !== undefined)
33
+ );
34
+ }
35
+
28
36
  export type {
29
37
  ElkConstructorArguments,
30
38
  ElkCommonDescription,
@@ -1264,10 +1272,7 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
1264
1272
  if (isInsideSelfLoop(root, edge)) return [];
1265
1273
  const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
1266
1274
  const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
1267
- const labels = (edge.labels ?? []).filter(
1268
- (label) =>
1269
- Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
1270
- );
1275
+ const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
1271
1276
  const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
1272
1277
  const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
1273
1278
  const labelHeight =
@@ -1387,7 +1392,7 @@ function applyLayout(
1387
1392
  label.y ??= 0;
1388
1393
  continue;
1389
1394
  }
1390
- if (!label.text) {
1395
+ if (!isLayoutEdgeLabel(label)) {
1391
1396
  label.x ??= 0;
1392
1397
  label.y ??= 0;
1393
1398
  continue;
@@ -1443,10 +1448,7 @@ function normalizeElkGraphBounds(
1443
1448
  );
1444
1449
  }
1445
1450
  for (const edge of root.edges ?? []) {
1446
- const laidOutLabels = (edge.labels ?? []).filter(
1447
- (label) =>
1448
- Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
1449
- );
1451
+ const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
1450
1452
  minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
1451
1453
  minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
1452
1454
  if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
@@ -1473,7 +1475,7 @@ function normalizeElkGraphBounds(
1473
1475
  point.y += shiftY;
1474
1476
  }
1475
1477
  }
1476
- for (const label of (edge.labels ?? []).filter((candidate) => Boolean(candidate.text))) {
1478
+ for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
1477
1479
  label.x = (label.x ?? 0) + shiftX;
1478
1480
  label.y = (label.y ?? 0) + shiftY;
1479
1481
  }
@@ -1578,11 +1580,7 @@ function normalizeElkGraphBounds(
1578
1580
  ]),
1579
1581
  ...(root.edges ?? []).flatMap((edge) =>
1580
1582
  (edge.labels ?? [])
1581
- .filter(
1582
- (label) =>
1583
- Boolean(label.text) &&
1584
- getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
1585
- )
1583
+ .filter(isLayoutEdgeLabel)
1586
1584
  .map((label) => (label.x ?? 0) + (label.width ?? 0)),
1587
1585
  ),
1588
1586
  ...(root.edges ?? []).flatMap((edge) =>
@@ -1617,11 +1615,7 @@ function normalizeElkGraphBounds(
1617
1615
  ]),
1618
1616
  ...(root.edges ?? []).flatMap((edge) =>
1619
1617
  (edge.labels ?? [])
1620
- .filter(
1621
- (label) =>
1622
- Boolean(label.text) &&
1623
- getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
1624
- )
1618
+ .filter(isLayoutEdgeLabel)
1625
1619
  .map(
1626
1620
  (label) =>
1627
1621
  (label.y ?? 0) +
@@ -1760,6 +1760,7 @@ function runLayeredPipeline<N, E, G, P>(
1760
1760
  );
1761
1761
  measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
1762
1762
  const antiparallelLabelPositions = new Map<string, Point>();
1763
+ const outerAntiparallelLabelIds = new Set<string>();
1763
1764
  const parallelLabelPositions = new Map<string, Point>();
1764
1765
  const postCompactionNodeCrossDeltas = new Map<string, number>();
1765
1766
  const horizontalAntiparallelFlow = direction === "left" || direction === "right";
@@ -1898,14 +1899,14 @@ function runLayeredPipeline<N, E, G, P>(
1898
1899
  : [],
1899
1900
  ),
1900
1901
  );
1901
- if (nonSelfNeighbors(firstNode.id).size !== 1 || nonSelfNeighbors(secondNode.id).size !== 1) {
1902
- continue;
1903
- }
1902
+ const isolatedAntiparallelPair =
1903
+ nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
1904
1904
  const hasFlexiblePorts = (node: GraphNode): boolean => {
1905
1905
  const constraints = options.nodeSettings?.(node)?.portConstraints;
1906
1906
  return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
1907
1907
  };
1908
- if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
1908
+ const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
1909
+ if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
1909
1910
 
1910
1911
  const firstBeforeSecond = horizontal
1911
1912
  ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2
@@ -1929,16 +1930,44 @@ function runLayeredPipeline<N, E, G, P>(
1929
1930
  const height = edge.height ?? 0;
1930
1931
  antiparallelLabelPositions.set(
1931
1932
  edge.id,
1932
- horizontal
1933
- ? {
1934
- x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
1935
- y: cross,
1936
- }
1937
- : {
1938
- x: cross,
1939
- y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
1940
- },
1933
+ isolatedAntiparallelPair
1934
+ ? horizontal
1935
+ ? {
1936
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
1937
+ y: cross,
1938
+ }
1939
+ : {
1940
+ x: cross,
1941
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
1942
+ }
1943
+ : horizontal
1944
+ ? {
1945
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
1946
+ y:
1947
+ edge === forward
1948
+ ? (firstRect.y +
1949
+ firstRect.height / 2 +
1950
+ secondRect.y +
1951
+ secondRect.height / 2) /
1952
+ 2 -
1953
+ height / 2
1954
+ : Math.max(firstRect.y + firstRect.height, secondRect.y + secondRect.height) +
1955
+ edgeSpacing,
1956
+ }
1957
+ : {
1958
+ x:
1959
+ edge === forward
1960
+ ? (firstRect.x + firstRect.width / 2 + secondRect.x + secondRect.width / 2) /
1961
+ 2 -
1962
+ width / 2
1963
+ : Math.max(firstRect.x + firstRect.width, secondRect.x + secondRect.width) +
1964
+ edgeSpacing,
1965
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
1966
+ },
1941
1967
  );
1968
+ if (!isolatedAntiparallelPair && edge !== forward) {
1969
+ outerAntiparallelLabelIds.add(edge.id);
1970
+ }
1942
1971
  cross += (horizontal ? height : width) + edgeSpacing;
1943
1972
  }
1944
1973
 
@@ -1974,8 +2003,10 @@ function runLayeredPipeline<N, E, G, P>(
1974
2003
  horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
1975
2004
  );
1976
2005
  };
1977
- alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
1978
- alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
2006
+ if (isolatedAntiparallelPair) {
2007
+ alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
2008
+ alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
2009
+ }
1979
2010
  }
1980
2011
  }
1981
2012
  if (postCompactionNodeCrossDeltas.size > 0) {
@@ -2482,8 +2513,41 @@ function runLayeredPipeline<N, E, G, P>(
2482
2513
  ? [sourceRect, targetRect]
2483
2514
  : [targetRect, sourceRect]
2484
2515
  : [undefined, undefined];
2516
+ const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
2517
+ const outerAntiparallelLabelPosition = (() => {
2518
+ if (!outerAntiparallelLabelIds.has(edge.id)) return undefined;
2519
+ const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNode"] ?? 10);
2520
+ if (horizontal) {
2521
+ const x = (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2;
2522
+ const blockers = [
2523
+ sourceRect!,
2524
+ targetRect!,
2525
+ ...[...placement.rectByNodeId.values()].filter(
2526
+ (rect) => rect.x < x + width && rect.x + rect.width > x,
2527
+ ),
2528
+ ];
2529
+ return {
2530
+ x,
2531
+ y: Math.max(...blockers.map((rect) => rect.y + rect.height)) + edgeNodeSpacing,
2532
+ };
2533
+ }
2534
+ const y = (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2;
2535
+ const blockers = [
2536
+ sourceRect!,
2537
+ targetRect!,
2538
+ ...[...placement.rectByNodeId.values()].filter(
2539
+ (rect) => rect.y < y + height && rect.y + rect.height > y,
2540
+ ),
2541
+ ];
2542
+ return {
2543
+ x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing,
2544
+ y,
2545
+ };
2546
+ })();
2485
2547
  const explicitLabelPosition =
2486
- antiparallelLabelPositions.get(edge.id) ?? parallelLabelPositions.get(edge.id);
2548
+ outerAntiparallelLabelPosition ??
2549
+ antiparallelLabelPosition ??
2550
+ parallelLabelPositions.get(edge.id);
2487
2551
  const x = explicitLabelPosition
2488
2552
  ? explicitLabelPosition.x
2489
2553
  : flexibleFeedbackLabel