@statelyai/layout 0.0.2 → 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-QwJn2gb2.mjs";
2
- import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-D15xIQKj.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;
@@ -573,8 +576,9 @@ function applyNodeMicroLayout(graph, globalOptions) {
573
576
  const configuredSizeOptions = getOption(node.layoutOptions ?? {}, "nodeSize.options");
574
577
  const sizeOptions = new Set(configuredSizeOptions === void 0 ? ["DEFAULT_MINIMUM_SIZE"] : String(configuredSizeOptions).split(/[\s,;]+/).filter(Boolean));
575
578
  const effectivelyFixedPortLabelSize = constraints.includes("PORT_LABELS") && !constraints.includes("NODE_LABELS") && !constraints.includes("MINIMUM_SIZE");
576
- let width = effectivelyFixedPortLabelSize ? node.width ?? 0 : 0;
577
- let height = effectivelyFixedPortLabelSize ? node.height ?? 0 : 0;
579
+ const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
580
+ let width = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.width ?? 0 : 0;
581
+ let height = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.height ?? 0 : 0;
578
582
  let insideHorizontalInset = 0;
579
583
  let insideVerticalInset = 0;
580
584
  const insideLabelCells = /* @__PURE__ */ new Map();
@@ -599,8 +603,8 @@ function applyNodeMicroLayout(graph, globalOptions) {
599
603
  }
600
604
  width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
601
605
  height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
602
- if (sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
603
- if (sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
606
+ if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
607
+ if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
604
608
  }
605
609
  if (constraints.includes("NODE_LABELS")) {
606
610
  for (const label of node.labels ?? []) {
@@ -896,7 +900,7 @@ function toGraph(root, globalOptions = {}) {
896
900
  if (isInsideSelfLoop(root, edge)) return [];
897
901
  const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
898
902
  const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
899
- const labels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
903
+ const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
900
904
  const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
901
905
  const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
902
906
  const labelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelLabelSpacing;
@@ -999,7 +1003,7 @@ function applyLayout(root, graph, padding, layoutOptions) {
999
1003
  label.y ??= 0;
1000
1004
  continue;
1001
1005
  }
1002
- if (!label.text) {
1006
+ if (!isLayoutEdgeLabel(label)) {
1003
1007
  label.x ??= 0;
1004
1008
  label.y ??= 0;
1005
1009
  continue;
@@ -1025,7 +1029,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1025
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))));
1026
1030
  }
1027
1031
  for (const edge of root.edges ?? []) {
1028
- const laidOutLabels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
1032
+ const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
1029
1033
  minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
1030
1034
  minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
1031
1035
  if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
@@ -1054,7 +1058,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1054
1058
  point.x += shiftX;
1055
1059
  point.y += shiftY;
1056
1060
  }
1057
- for (const label of (edge.labels ?? []).filter((candidate) => Boolean(candidate.text))) {
1061
+ for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
1058
1062
  label.x = (label.x ?? 0) + shiftX;
1059
1063
  label.y = (label.y ?? 0) + shiftY;
1060
1064
  }
@@ -1090,7 +1094,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1090
1094
  ...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
1091
1095
  ...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
1092
1096
  ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
1093
- ]), ...(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) => [
1094
1098
  section.startPoint.x,
1095
1099
  ...(section.bendPoints ?? []).map((point) => point.x),
1096
1100
  section.endPoint.x
@@ -1100,7 +1104,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
1100
1104
  ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
1101
1105
  ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
1102
1106
  ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
1103
- ]), ...(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) => [
1104
1108
  section.startPoint.y,
1105
1109
  ...(section.bendPoints ?? []).map((point) => point.y),
1106
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-QwJn2gb2.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-D15xIQKj.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-QwJn2gb2.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 };
@@ -195,6 +195,20 @@ const phaseRandomByInput = /* @__PURE__ */ new WeakMap();
195
195
  function getOrientedEndpoints$1(edge, orientation) {
196
196
  return orientation.reversedEdgeIds.has(edge.id) ? [edge.targetId, edge.sourceId] : [edge.sourceId, edge.targetId];
197
197
  }
198
+ function getOrientedPortDirection(input, orientation, node, port) {
199
+ const configuredSide = input.portSettings?.(port, node)?.["port.side"];
200
+ if (configuredSide !== "NORTH" && configuredSide !== "SOUTH" && configuredSide !== "WEST" && configuredSide !== "EAST") return port.direction;
201
+ let incoming = false;
202
+ let outgoing = false;
203
+ for (const edge of input.graph.edges) {
204
+ const reversed = orientation.reversedEdgeIds.has(edge.id);
205
+ if (edge.sourceId === node.id && edge.sourcePort === port.name) if (reversed) incoming = true;
206
+ else outgoing = true;
207
+ if (edge.targetId === node.id && edge.targetPort === port.name) if (reversed) outgoing = true;
208
+ else incoming = true;
209
+ }
210
+ return incoming && outgoing ? "inout" : outgoing ? "out" : incoming ? "in" : port.direction;
211
+ }
198
212
  function cycleModelOrder(input) {
199
213
  const enforced = input.settings["considerModelOrder.groupModelOrder.cbGroupOrderStrategy"] === "ENFORCED";
200
214
  const count = Math.max(1, input.graph.nodes.length);
@@ -2917,7 +2931,11 @@ function routeEdges(style) {
2917
2931
  const sourceAwaySide = horizontal ? sourceRect.x < targetRect.x ? "WEST" : "EAST" : sourceRect.y < targetRect.y ? "NORTH" : "SOUTH";
2918
2932
  const targetAwaySide = horizontal ? sourceRect.x < targetRect.x ? "EAST" : "WEST" : sourceRect.y < targetRect.y ? "SOUTH" : "NORTH";
2919
2933
  const sameSideSelfLoop = source.id === target.id && feedbackSourcePortSide !== void 0 && feedbackSourcePortSide === feedbackTargetPortSide;
2920
- const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (feedbackSourcePortSide === sourceAwaySide || feedbackTargetPortSide === targetAwaySide);
2934
+ const hasFixedPortSide = (node) => {
2935
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
2936
+ return constraints !== "UNDEFINED" && constraints !== "FREE";
2937
+ };
2938
+ const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide || hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide);
2921
2939
  if (input.settings.feedbackEdges === true && reversedEdge || fixedSideFeedback) {
2922
2940
  if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
2923
2941
  const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
@@ -3296,8 +3314,9 @@ function routeEdges(style) {
3296
3314
  const fallbackMatchesPortSide = (fallback, rect, side) => side === void 0 || side === "UNDEFINED" || side === "EAST" && Math.abs(fallback.x - rect.x - rect.width) < 1e-9 || side === "WEST" && Math.abs(fallback.x - rect.x) < 1e-9 || side === "SOUTH" && Math.abs(fallback.y - rect.y - rect.height) < 1e-9 || side === "NORTH" && Math.abs(fallback.y - rect.y) < 1e-9;
3297
3315
  const sourceFixedSide = sourcePort !== void 0 && fallbackMatchesPortSide(sourceFallback, sourceRect, sourcePortSide) && (sourcePort.width ?? 8) === 0 && (sourcePort.height ?? 8) === 0 && input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" && input.graph.edges.filter((candidate) => candidate.sourceId === edge.sourceId && candidate.sourcePort === edge.sourcePort).length === 1;
3298
3316
  const targetFixedSide = targetPort !== void 0 && fallbackMatchesPortSide(targetFallback, targetRect, targetPortSide) && (targetPort.width ?? 8) === 0 && (targetPort.height ?? 8) === 0 && input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" && input.graph.edges.filter((candidate) => candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort).length === 1;
3299
- const start = sourceFixedSide ? sourceFallback : getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
3300
- const end = targetFixedSide ? targetFallback : getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
3317
+ const flexibleFeedback = reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
3318
+ const start = flexibleFeedback ? sourceFallback : sourceFixedSide ? sourceFallback : getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
3319
+ const end = flexibleFeedback ? targetFallback : targetFixedSide ? targetFallback : getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
3301
3320
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
3302
3321
  const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
3303
3322
  const earlierLayer = Math.min(sourceLayer, targetLayer);
@@ -3450,7 +3469,7 @@ function routeEdges(style) {
3450
3469
  const routeEdgesOrthogonally = routeEdges("ORTHOGONAL");
3451
3470
  const routeEdgesWithPolylines = routeEdges("POLYLINE");
3452
3471
  const routeEdgesWithSplines = routeEdges("SPLINES");
3453
- function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3472
+ function placePorts(ports, rect, direction, portSettings, nodeSettings, portDirection) {
3454
3473
  if (!ports) return void 0;
3455
3474
  const horizontal = direction === "left" || direction === "right";
3456
3475
  const reverse = direction === "up" || direction === "left";
@@ -3464,7 +3483,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3464
3483
  sideByPort.set(port, configured);
3465
3484
  continue;
3466
3485
  }
3467
- const outgoing = port.direction === "out";
3486
+ const outgoing = (portDirection?.(port) ?? port.direction) === "out";
3468
3487
  const normalFarSide = reverse ? !outgoing : outgoing;
3469
3488
  const farSide = sideFixed && configured === "UNDEFINED" ? !normalFarSide : normalFarSide;
3470
3489
  sideByPort.set(port, horizontal ? farSide ? "EAST" : "WEST" : farSide ? "SOUTH" : "NORTH");
@@ -3574,7 +3593,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3574
3593
  });
3575
3594
  }
3576
3595
  /** Account for port extents in ELK's graph-padding normalization. */
3577
- function normalizePlacementForPortExtents(input, placement, order) {
3596
+ function normalizePlacementForPortExtents(input, placement, order, orientation) {
3578
3597
  const horizontal = input.direction === "left" || input.direction === "right";
3579
3598
  const physicalLayers = order.layers.map((layer) => layer.flatMap((id) => placement.rectByNodeId.has(id) ? [id] : [])).filter((layer) => layer.length > 0).sort((left, right) => {
3580
3599
  const leftRect = placement.rectByNodeId.get(left[0]);
@@ -3609,7 +3628,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
3609
3628
  const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
3610
3629
  ...input.settings,
3611
3630
  ...input.nodeSettings?.(node)
3612
- });
3631
+ }, (port) => getOrientedPortDirection(input, orientation, node, port));
3613
3632
  for (const port of ports ?? []) {
3614
3633
  const extent$1 = horizontal ? trailing ? Math.max(0, (port.x ?? 0) + (port.width ?? 0) - rect.width) : Math.max(0, -(port.x ?? 0)) : trailing ? Math.max(0, (port.y ?? 0) + (port.height ?? 0) - rect.height) : Math.max(0, -(port.y ?? 0));
3615
3634
  if (trailing) trailingExtent = Math.max(trailingExtent, extent$1);
@@ -3628,7 +3647,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
3628
3647
  const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
3629
3648
  ...input.settings,
3630
3649
  ...input.nodeSettings?.(node)
3631
- });
3650
+ }, (port) => getOrientedPortDirection(input, orientation, node, port));
3632
3651
  for (const port of ports ?? []) {
3633
3652
  minimumX = Math.min(minimumX, rect.x + (port.x ?? 0));
3634
3653
  minimumY = Math.min(minimumY, rect.y + (port.y ?? 0));
@@ -4165,7 +4184,12 @@ function splitLongEdges(input, orientation, assignment) {
4165
4184
  const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
4166
4185
  const sourceSide = source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
4167
4186
  const targetSide = target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : void 0;
4168
- const fixedSideFeedback = sourceLayer > targetLayer && (source !== void 0 && input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" && sourceSide === forwardSourceSide || target !== void 0 && input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" && targetSide === forwardTargetSide);
4187
+ const hasFixedPortSide = (node) => {
4188
+ if (!node) return false;
4189
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
4190
+ return constraints !== "UNDEFINED" && constraints !== "FREE";
4191
+ };
4192
+ const fixedSideFeedback = sourceLayer > targetLayer && (hasFixedPortSide(source) && sourceSide === forwardSourceSide || hasFixedPortSide(target) && targetSide === forwardTargetSide);
4169
4193
  if (span <= 1 || edge.sourceId === edge.targetId || input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id) || fixedSideFeedback) {
4170
4194
  edges.push(edge);
4171
4195
  originalEdgeBySegmentId.set(edge.id, edge);
@@ -8459,6 +8483,24 @@ function runLayeredPipeline(graph, options, context) {
8459
8483
  left: options.padding?.left ?? 12
8460
8484
  };
8461
8485
  const switchedSideByPort = /* @__PURE__ */ new Map();
8486
+ const parallelPortIndexByKey = /* @__PURE__ */ new Map();
8487
+ const labeledEdgesByPair = /* @__PURE__ */ new Map();
8488
+ for (const edge of graph.edges) {
8489
+ const settings = options.edgeSettings?.(edge);
8490
+ if (edge.sourcePort === void 0 || edge.targetPort === void 0 || edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0 || (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true) continue;
8491
+ const key = `${edge.sourceId}\0${edge.targetId}`;
8492
+ const pair = labeledEdgesByPair.get(key) ?? [];
8493
+ pair.push(edge);
8494
+ labeledEdgesByPair.set(key, pair);
8495
+ }
8496
+ for (const pair of labeledEdgesByPair.values()) {
8497
+ if (pair.length < 2) continue;
8498
+ for (const [index, edge] of pair.entries()) {
8499
+ const portIndex = options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES" ? index : pair.length - index - 1;
8500
+ parallelPortIndexByKey.set(`${edge.sourceId}\0${edge.sourcePort}`, portIndex);
8501
+ parallelPortIndexByKey.set(`${edge.targetId}\0${edge.targetPort}`, portIndex);
8502
+ }
8503
+ }
8462
8504
  const input = {
8463
8505
  graph,
8464
8506
  sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
@@ -8477,6 +8519,7 @@ function runLayeredPipeline(graph, options, context) {
8477
8519
  ...options.edgeSettings === void 0 ? {} : { edgeSettings: options.edgeSettings },
8478
8520
  portSettings: (port, node) => ({
8479
8521
  ...options.portSettings?.(port, node),
8522
+ ...parallelPortIndexByKey.has(`${node.id}\0${port.name}`) ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) } : {},
8480
8523
  ...switchedSideByPort.has(`${node.id}\0${port.name}`) ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) } : {}
8481
8524
  })
8482
8525
  };
@@ -8674,7 +8717,7 @@ function runLayeredPipeline(graph, options, context) {
8674
8717
  });
8675
8718
  }
8676
8719
  }
8677
- measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order));
8720
+ measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation));
8678
8721
  {
8679
8722
  const horizontal = direction === "left" || direction === "right";
8680
8723
  const crossCenter = (nodeId) => {
@@ -8699,8 +8742,197 @@ function runLayeredPipeline(graph, options, context) {
8699
8742
  }
8700
8743
  const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
8701
8744
  const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
8702
- const expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8745
+ let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8703
8746
  measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
8747
+ const antiparallelLabelPositions = /* @__PURE__ */ new Map();
8748
+ const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
8749
+ const parallelLabelPositions = /* @__PURE__ */ new Map();
8750
+ const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
8751
+ const horizontalAntiparallelFlow = direction === "left" || direction === "right";
8752
+ if (options.strategies?.routeEdges === void 0 && options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" && options.settings?.["layering.strategy"] === "INTERACTIVE" && options.settings?.["crossingMinimization.forceNodeModelOrder"] === true && options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" && options.settings?.["nodePlacement.favorStraightEdges"] === true && options.settings?.["compaction.postCompaction.strategy"] === "LEFT" && options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE") {
8753
+ const neighbors = new Map(graph.nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
8754
+ for (const edge of graph.edges) {
8755
+ if (edge.sourceId === edge.targetId) continue;
8756
+ neighbors.get(edge.sourceId)?.add(edge.targetId);
8757
+ neighbors.get(edge.targetId)?.add(edge.sourceId);
8758
+ }
8759
+ const visited = /* @__PURE__ */ new Set();
8760
+ for (const node of graph.nodes) {
8761
+ if (visited.has(node.id)) continue;
8762
+ const component = [];
8763
+ const pending = [node.id];
8764
+ while (pending.length > 0) {
8765
+ const id = pending.pop();
8766
+ if (visited.has(id)) continue;
8767
+ visited.add(id);
8768
+ const member = graph.nodes.find((candidate) => candidate.id === id);
8769
+ if (member) component.push(member);
8770
+ for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
8771
+ }
8772
+ if (component.length < 2) continue;
8773
+ const componentIds = new Set(component.map((member) => member.id));
8774
+ if (!graph.edges.some((edge) => componentIds.has(edge.sourceId) && componentIds.has(edge.targetId) && expanded.orientation.reversedEdgeIds.has(edge.id))) continue;
8775
+ const directedPairCounts = /* @__PURE__ */ new Map();
8776
+ for (const edge of graph.edges) {
8777
+ if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
8778
+ const key = `${edge.sourceId}\0${edge.targetId}`;
8779
+ directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
8780
+ }
8781
+ if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
8782
+ if (component.some((member) => {
8783
+ const constraints = options.nodeSettings?.(member)?.portConstraints;
8784
+ return constraints !== void 0 && constraints !== "UNDEFINED" && constraints !== "FREE";
8785
+ })) continue;
8786
+ const layerCounts = /* @__PURE__ */ new Map();
8787
+ for (const member of component) {
8788
+ const layer = expanded.assignment.layerByNodeId.get(member.id);
8789
+ if (layer === void 0) continue;
8790
+ layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
8791
+ }
8792
+ if ([...layerCounts.values()].some((count) => count !== 1)) continue;
8793
+ const rects = component.flatMap((member) => {
8794
+ const rect = mutableRects.get(member.id);
8795
+ return rect ? [{
8796
+ member,
8797
+ rect
8798
+ }] : [];
8799
+ });
8800
+ if (rects.length !== component.length) continue;
8801
+ const center = (Math.min(...rects.map(({ rect }) => horizontalAntiparallelFlow ? rect.y : rect.x)) + Math.max(...rects.map(({ rect }) => horizontalAntiparallelFlow ? rect.y + rect.height : rect.x + rect.width))) / 2;
8802
+ for (const { member, rect } of rects) {
8803
+ const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
8804
+ const nextCross = horizontalAntiparallelFlow ? center - rect.height / 2 : center - rect.width / 2;
8805
+ postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
8806
+ mutableRects.set(member.id, horizontalAntiparallelFlow ? {
8807
+ ...rect,
8808
+ y: nextCross
8809
+ } : {
8810
+ ...rect,
8811
+ x: nextCross
8812
+ });
8813
+ }
8814
+ }
8815
+ }
8816
+ {
8817
+ const horizontal = horizontalAntiparallelFlow;
8818
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
8819
+ const pairEdges = /* @__PURE__ */ new Map();
8820
+ for (const edge of graph.edges) {
8821
+ if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) continue;
8822
+ const key = [edge.sourceId, edge.targetId].sort().join("\0");
8823
+ const pair = pairEdges.get(key) ?? [];
8824
+ pair.push(edge);
8825
+ pairEdges.set(key, pair);
8826
+ }
8827
+ for (const pair of pairEdges.values()) {
8828
+ if (pair.length !== 2 || pair[0].sourceId !== pair[1].targetId || pair[0].targetId !== pair[1].sourceId || pair.some((edge) => {
8829
+ const settings = options.edgeSettings?.(edge);
8830
+ return (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true;
8831
+ })) continue;
8832
+ const firstNode = nodesById.get(pair[0].sourceId);
8833
+ const secondNode = nodesById.get(pair[0].targetId);
8834
+ const firstRect = placement.rectByNodeId.get(pair[0].sourceId);
8835
+ const secondRect = placement.rectByNodeId.get(pair[0].targetId);
8836
+ if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
8837
+ const nonSelfNeighbors = (nodeId) => new Set(graph.edges.flatMap((edge) => edge.sourceId === edge.targetId ? [] : edge.sourceId === nodeId ? [edge.targetId] : edge.targetId === nodeId ? [edge.sourceId] : []));
8838
+ const isolatedAntiparallelPair = nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
8839
+ const hasFlexiblePorts = (node) => {
8840
+ const constraints = options.nodeSettings?.(node)?.portConstraints;
8841
+ return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
8842
+ };
8843
+ const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
8844
+ if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
8845
+ const firstBeforeSecond = horizontal ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
8846
+ const beforeRect = firstBeforeSecond ? firstRect : secondRect;
8847
+ const afterRect = firstBeforeSecond ? secondRect : firstRect;
8848
+ const forward = pair.find((edge) => firstBeforeSecond ? edge.sourceId === pair[0].sourceId : edge.sourceId === pair[0].targetId);
8849
+ if (!forward?.sourcePort || !forward.targetPort) continue;
8850
+ let cross = Math.min(horizontal ? firstRect.y : firstRect.x, horizontal ? secondRect.y : secondRect.x);
8851
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
8852
+ for (const edge of pair) {
8853
+ const width = edge.width ?? 0;
8854
+ const height = edge.height ?? 0;
8855
+ antiparallelLabelPositions.set(edge.id, isolatedAntiparallelPair ? horizontal ? {
8856
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
8857
+ y: cross
8858
+ } : {
8859
+ x: cross,
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
8867
+ });
8868
+ if (!isolatedAntiparallelPair && edge !== forward) outerAntiparallelLabelIds.add(edge.id);
8869
+ cross += (horizontal ? height : width) + edgeSpacing;
8870
+ }
8871
+ const forwardLabel = antiparallelLabelPositions.get(forward.id);
8872
+ const forwardCross = horizontal ? Math.round(forwardLabel.y + (forward.height ?? 0) / 2) : Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
8873
+ const alignPort = (node, portName) => {
8874
+ const rect = placement.rectByNodeId.get(node.id);
8875
+ const port = node.ports?.find((candidate) => candidate.name === portName);
8876
+ if (!rect || !port) return;
8877
+ const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
8878
+ ...options.settings,
8879
+ ...options.nodeSettings?.(node)
8880
+ }, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
8881
+ if (!placedPort) return;
8882
+ const nextCross = forwardCross - (horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2);
8883
+ const currentCross = horizontal ? rect.y : rect.x;
8884
+ postCompactionNodeCrossDeltas.set(node.id, (postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross);
8885
+ mutableRects.set(node.id, horizontal ? {
8886
+ ...rect,
8887
+ y: nextCross
8888
+ } : {
8889
+ ...rect,
8890
+ x: nextCross
8891
+ });
8892
+ };
8893
+ if (isolatedAntiparallelPair) {
8894
+ alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
8895
+ alignPort(nodesById.get(forward.targetId), forward.targetPort);
8896
+ }
8897
+ }
8898
+ }
8899
+ if (postCompactionNodeCrossDeltas.size > 0) {
8900
+ const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
8901
+ for (const edge of graph.edges) {
8902
+ const points = [...pointsByEdgeId.get(edge.id) ?? []];
8903
+ if (points.length === 0) continue;
8904
+ const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
8905
+ const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
8906
+ const shiftEndpoint = (endpointIndex, adjacentIndex, delta) => {
8907
+ if (delta === 0) return;
8908
+ const endpoint = points[endpointIndex];
8909
+ const adjacent = points[adjacentIndex];
8910
+ if (!endpoint) return;
8911
+ const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
8912
+ points[endpointIndex] = horizontalAntiparallelFlow ? {
8913
+ ...endpoint,
8914
+ y: endpoint.y + delta
8915
+ } : {
8916
+ ...endpoint,
8917
+ x: endpoint.x + delta
8918
+ };
8919
+ if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontalAntiparallelFlow ? {
8920
+ ...adjacent,
8921
+ y: adjacent.y + delta
8922
+ } : {
8923
+ ...adjacent,
8924
+ x: adjacent.x + delta
8925
+ };
8926
+ };
8927
+ shiftEndpoint(0, 1, sourceDelta);
8928
+ shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
8929
+ pointsByEdgeId.set(edge.id, points);
8930
+ }
8931
+ expandedRoutes = {
8932
+ ...expandedRoutes,
8933
+ pointsByEdgeId
8934
+ };
8935
+ }
8704
8936
  let routes = measure("long-edge-joining", () => joinLongEdgeRoutes(expandedRoutes, expanded.segmentIdsByEdgeId, edgeRouting === "SPLINES" || options.settings?.unnecessaryBendpoints === true, edgeRouting === "SPLINES", Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10)));
8705
8937
  if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
8706
8938
  const horizontal = direction === "right" || direction === "left";
@@ -8832,6 +9064,84 @@ function runLayeredPipeline(graph, options, context) {
8832
9064
  };
8833
9065
  routes.pointsByEdgeId.set(edge.id, points);
8834
9066
  }
9067
+ for (const pair of labeledEdgesByPair.values()) {
9068
+ if (pair.length < 2) continue;
9069
+ const sourceRect = mutableRects.get(pair[0].sourceId);
9070
+ const targetRect = mutableRects.get(pair[0].targetId);
9071
+ if (!sourceRect || !targetRect) continue;
9072
+ const horizontal = direction === "left" || direction === "right";
9073
+ const beforeRect = horizontal ? sourceRect.x <= targetRect.x ? sourceRect : targetRect : sourceRect.y <= targetRect.y ? sourceRect : targetRect;
9074
+ const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
9075
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
9076
+ let cross = horizontal ? padding.top : padding.left;
9077
+ const orderedPair = options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES" ? pair : [...pair].reverse();
9078
+ for (const edge of orderedPair) {
9079
+ const width = edge.width ?? 0;
9080
+ const height = edge.height ?? 0;
9081
+ parallelLabelPositions.set(edge.id, horizontal ? {
9082
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
9083
+ y: cross
9084
+ } : {
9085
+ x: cross,
9086
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
9087
+ });
9088
+ cross += (horizontal ? height : width) + edgeSpacing;
9089
+ }
9090
+ const anchorEdge = orderedPair[0];
9091
+ const anchorLabel = parallelLabelPositions.get(anchorEdge.id);
9092
+ const desiredCross = Math.round(horizontal ? anchorLabel.y + (anchorEdge.height ?? 0) / 2 : anchorLabel.x + (anchorEdge.width ?? 0) / 2);
9093
+ const alignEndpoint = (nodeId, portName) => {
9094
+ const node = graph.nodes.find((candidate) => candidate.id === nodeId);
9095
+ const rect = mutableRects.get(nodeId);
9096
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
9097
+ if (!node || !rect || !port) return;
9098
+ const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
9099
+ ...options.settings,
9100
+ ...options.nodeSettings?.(node)
9101
+ }, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
9102
+ if (!placedPort) return;
9103
+ const localCross = horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
9104
+ const currentCross = horizontal ? rect.y : rect.x;
9105
+ const nextCross = desiredCross - localCross;
9106
+ const delta = nextCross - currentCross;
9107
+ mutableRects.set(nodeId, horizontal ? {
9108
+ ...rect,
9109
+ y: nextCross
9110
+ } : {
9111
+ ...rect,
9112
+ x: nextCross
9113
+ });
9114
+ for (const edge of graph.edges) {
9115
+ const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
9116
+ if (points.length === 0) continue;
9117
+ const index = edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : void 0;
9118
+ if (index === void 0) continue;
9119
+ const endpoint = points[index];
9120
+ const originalCross = horizontal ? endpoint.y : endpoint.x;
9121
+ points[index] = horizontal ? {
9122
+ ...endpoint,
9123
+ y: endpoint.y + delta
9124
+ } : {
9125
+ ...endpoint,
9126
+ x: endpoint.x + delta
9127
+ };
9128
+ const adjacentIndex = index === 0 ? 1 : points.length - 2;
9129
+ const adjacent = points[adjacentIndex];
9130
+ if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontal ? {
9131
+ ...adjacent,
9132
+ y: adjacent.y + delta
9133
+ } : {
9134
+ ...adjacent,
9135
+ x: adjacent.x + delta
9136
+ };
9137
+ routes.pointsByEdgeId.set(edge.id, points);
9138
+ }
9139
+ };
9140
+ if (anchorEdge.sourcePort && anchorEdge.targetPort) {
9141
+ alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
9142
+ alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
9143
+ }
9144
+ }
8835
9145
  const routedPortAnchors = /* @__PURE__ */ new Map();
8836
9146
  for (const edge of graph.edges) {
8837
9147
  const points = routes.pointsByEdgeId.get(edge.id);
@@ -8846,7 +9156,7 @@ function runLayeredPipeline(graph, options, context) {
8846
9156
  let ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
8847
9157
  ...options.settings,
8848
9158
  ...options.nodeSettings?.(node)
8849
- });
9159
+ }, (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port));
8850
9160
  if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") ports = ports?.map((port) => {
8851
9161
  if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
8852
9162
  const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
@@ -8921,8 +9231,9 @@ function runLayeredPipeline(graph, options, context) {
8921
9231
  }] : [];
8922
9232
  }).sort((left, right) => right.length - left.length)[0] : void 0;
8923
9233
  const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
9234
+ const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
8924
9235
  const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
8925
- const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
9236
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
8926
9237
  const sourcePortSide = sourceNode && sourcePort ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"] : void 0;
8927
9238
  const targetPortSide = sourceNode && targetPort ? options.portSettings?.(targetPort, sourceNode)?.["port.side"] : void 0;
8928
9239
  const horizontalFeedbackTrack = edge.sourceId === edge.targetId && (sourcePortSide === "EAST" || sourcePortSide === "WEST") && targetPortSide === sourcePortSide || (horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1) ? horizontalFeedbackCandidate : void 0;
@@ -8931,8 +9242,43 @@ function runLayeredPipeline(graph, options, context) {
8931
9242
  const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
8932
9243
  const routeX = labelPlacement === "TAIL" ? firstPoint.x + labelSpacing : labelPlacement === "HEAD" ? lastPoint.x - width - labelSpacing : inlineLabel && horizontalFeedbackTrack ? (horizontalFeedbackTrack.start.x + horizontalFeedbackTrack.end.x - width) / 2 : inlineLabel && verticalFeedbackTrack ? verticalFeedbackTrack.start.x > (minimumFeedbackNodeX + maximumFeedbackNodeX) / 2 ? verticalFeedbackTrack.start.x + labelSpacing + 1 : verticalFeedbackTrack.start.x - labelSpacing - width - 1 : inlineLabel && verticalTrack ? (direction === "right" ? trackNearTarget : !trackNearTarget) ? verticalTrack.x - edgeNodeSpacing - width : verticalTrack.x + edgeNodeSpacing : inlineLabel ? horizontal ? Math.floor(midpoint.x - width / 2) : Math.ceil(midpoint.x - width / 2) : midpoint.x - width / 2;
8933
9244
  const routeY = labelPlacement === "CENTER" && inlineLabel && horizontalFeedbackTrack ? horizontalFeedbackTrack.start.y - height / 2 - .5 : labelPlacement === "CENTER" && inlineLabel && verticalFeedbackTrack ? (verticalFeedbackTrack.start.y + verticalFeedbackTrack.end.y - height) / 2 : labelPlacement === "CENTER" && inlineLabel ? midpoint.y - height / 2 - .5 : labelPlacement === "CENTER" && placeLabelUp ? midpoint.y - height - labelSpacing - Math.round(edgeThickness / 2) : (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) + labelSpacing + Math.round(edgeThickness / 2);
8934
- const x = labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
8935
- const y = labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
9245
+ const sourceRect = placement.rectByNodeId.get(edge.sourceId);
9246
+ const targetRect = placement.rectByNodeId.get(edge.targetId);
9247
+ const hasFlexiblePorts = (node) => {
9248
+ const constraints = node ? options.nodeSettings?.(node)?.portConstraints : void 0;
9249
+ return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
9250
+ };
9251
+ const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
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];
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);
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;
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;
8936
9282
  return {
8937
9283
  ...edge,
8938
9284
  x,
@@ -1,4 +1,4 @@
1
- import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-QwJn2gb2.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