@statelyai/layout 0.0.2 → 0.0.3

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.
@@ -1,5 +1,5 @@
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-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";
3
3
  import { createGraph } from "@statelyai/graph";
4
4
 
5
5
  //#region src/elkjs/index.ts
@@ -573,8 +573,9 @@ function applyNodeMicroLayout(graph, globalOptions) {
573
573
  const configuredSizeOptions = getOption(node.layoutOptions ?? {}, "nodeSize.options");
574
574
  const sizeOptions = new Set(configuredSizeOptions === void 0 ? ["DEFAULT_MINIMUM_SIZE"] : String(configuredSizeOptions).split(/[\s,;]+/).filter(Boolean));
575
575
  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;
576
+ const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
577
+ let width = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.width ?? 0 : 0;
578
+ let height = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.height ?? 0 : 0;
578
579
  let insideHorizontalInset = 0;
579
580
  let insideVerticalInset = 0;
580
581
  const insideLabelCells = /* @__PURE__ */ new Map();
@@ -599,8 +600,8 @@ function applyNodeMicroLayout(graph, globalOptions) {
599
600
  }
600
601
  width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
601
602
  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;
603
+ if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
604
+ if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
604
605
  }
605
606
  if (constraints.includes("NODE_LABELS")) {
606
607
  for (const label of node.labels ?? []) {
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-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";
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-f_3mmGHr.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,186 @@ 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 parallelLabelPositions = /* @__PURE__ */ new Map();
8749
+ const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
8750
+ const horizontalAntiparallelFlow = direction === "left" || direction === "right";
8751
+ 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") {
8752
+ const neighbors = new Map(graph.nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
8753
+ for (const edge of graph.edges) {
8754
+ if (edge.sourceId === edge.targetId) continue;
8755
+ neighbors.get(edge.sourceId)?.add(edge.targetId);
8756
+ neighbors.get(edge.targetId)?.add(edge.sourceId);
8757
+ }
8758
+ const visited = /* @__PURE__ */ new Set();
8759
+ for (const node of graph.nodes) {
8760
+ if (visited.has(node.id)) continue;
8761
+ const component = [];
8762
+ const pending = [node.id];
8763
+ while (pending.length > 0) {
8764
+ const id = pending.pop();
8765
+ if (visited.has(id)) continue;
8766
+ visited.add(id);
8767
+ const member = graph.nodes.find((candidate) => candidate.id === id);
8768
+ if (member) component.push(member);
8769
+ for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
8770
+ }
8771
+ if (component.length < 2) continue;
8772
+ const componentIds = new Set(component.map((member) => member.id));
8773
+ if (!graph.edges.some((edge) => componentIds.has(edge.sourceId) && componentIds.has(edge.targetId) && expanded.orientation.reversedEdgeIds.has(edge.id))) continue;
8774
+ const directedPairCounts = /* @__PURE__ */ new Map();
8775
+ for (const edge of graph.edges) {
8776
+ if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
8777
+ const key = `${edge.sourceId}\0${edge.targetId}`;
8778
+ directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
8779
+ }
8780
+ if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
8781
+ if (component.some((member) => {
8782
+ const constraints = options.nodeSettings?.(member)?.portConstraints;
8783
+ return constraints !== void 0 && constraints !== "UNDEFINED" && constraints !== "FREE";
8784
+ })) continue;
8785
+ const layerCounts = /* @__PURE__ */ new Map();
8786
+ for (const member of component) {
8787
+ const layer = expanded.assignment.layerByNodeId.get(member.id);
8788
+ if (layer === void 0) continue;
8789
+ layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
8790
+ }
8791
+ if ([...layerCounts.values()].some((count) => count !== 1)) continue;
8792
+ const rects = component.flatMap((member) => {
8793
+ const rect = mutableRects.get(member.id);
8794
+ return rect ? [{
8795
+ member,
8796
+ rect
8797
+ }] : [];
8798
+ });
8799
+ if (rects.length !== component.length) continue;
8800
+ 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;
8801
+ for (const { member, rect } of rects) {
8802
+ const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
8803
+ const nextCross = horizontalAntiparallelFlow ? center - rect.height / 2 : center - rect.width / 2;
8804
+ postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
8805
+ mutableRects.set(member.id, horizontalAntiparallelFlow ? {
8806
+ ...rect,
8807
+ y: nextCross
8808
+ } : {
8809
+ ...rect,
8810
+ x: nextCross
8811
+ });
8812
+ }
8813
+ }
8814
+ }
8815
+ {
8816
+ const horizontal = horizontalAntiparallelFlow;
8817
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
8818
+ const pairEdges = /* @__PURE__ */ new Map();
8819
+ for (const edge of graph.edges) {
8820
+ if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) continue;
8821
+ const key = [edge.sourceId, edge.targetId].sort().join("\0");
8822
+ const pair = pairEdges.get(key) ?? [];
8823
+ pair.push(edge);
8824
+ pairEdges.set(key, pair);
8825
+ }
8826
+ for (const pair of pairEdges.values()) {
8827
+ if (pair.length !== 2 || pair[0].sourceId !== pair[1].targetId || pair[0].targetId !== pair[1].sourceId || pair.some((edge) => {
8828
+ const settings = options.edgeSettings?.(edge);
8829
+ return (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true;
8830
+ })) continue;
8831
+ const firstNode = nodesById.get(pair[0].sourceId);
8832
+ const secondNode = nodesById.get(pair[0].targetId);
8833
+ const firstRect = placement.rectByNodeId.get(pair[0].sourceId);
8834
+ const secondRect = placement.rectByNodeId.get(pair[0].targetId);
8835
+ if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
8836
+ 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 hasFlexiblePorts = (node) => {
8839
+ const constraints = options.nodeSettings?.(node)?.portConstraints;
8840
+ return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
8841
+ };
8842
+ if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
8843
+ const firstBeforeSecond = horizontal ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
8844
+ const beforeRect = firstBeforeSecond ? firstRect : secondRect;
8845
+ const afterRect = firstBeforeSecond ? secondRect : firstRect;
8846
+ const forward = pair.find((edge) => firstBeforeSecond ? edge.sourceId === pair[0].sourceId : edge.sourceId === pair[0].targetId);
8847
+ if (!forward?.sourcePort || !forward.targetPort) continue;
8848
+ let cross = Math.min(horizontal ? firstRect.y : firstRect.x, horizontal ? secondRect.y : secondRect.x);
8849
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
8850
+ for (const edge of pair) {
8851
+ const width = edge.width ?? 0;
8852
+ const height = edge.height ?? 0;
8853
+ antiparallelLabelPositions.set(edge.id, horizontal ? {
8854
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
8855
+ y: cross
8856
+ } : {
8857
+ x: cross,
8858
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
8859
+ });
8860
+ cross += (horizontal ? height : width) + edgeSpacing;
8861
+ }
8862
+ const forwardLabel = antiparallelLabelPositions.get(forward.id);
8863
+ const forwardCross = horizontal ? Math.round(forwardLabel.y + (forward.height ?? 0) / 2) : Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
8864
+ const alignPort = (node, portName) => {
8865
+ const rect = placement.rectByNodeId.get(node.id);
8866
+ const port = node.ports?.find((candidate) => candidate.name === portName);
8867
+ if (!rect || !port) return;
8868
+ const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
8869
+ ...options.settings,
8870
+ ...options.nodeSettings?.(node)
8871
+ }, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
8872
+ if (!placedPort) return;
8873
+ const nextCross = forwardCross - (horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2);
8874
+ const currentCross = horizontal ? rect.y : rect.x;
8875
+ postCompactionNodeCrossDeltas.set(node.id, (postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross);
8876
+ mutableRects.set(node.id, horizontal ? {
8877
+ ...rect,
8878
+ y: nextCross
8879
+ } : {
8880
+ ...rect,
8881
+ x: nextCross
8882
+ });
8883
+ };
8884
+ alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
8885
+ alignPort(nodesById.get(forward.targetId), forward.targetPort);
8886
+ }
8887
+ }
8888
+ if (postCompactionNodeCrossDeltas.size > 0) {
8889
+ const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
8890
+ for (const edge of graph.edges) {
8891
+ const points = [...pointsByEdgeId.get(edge.id) ?? []];
8892
+ if (points.length === 0) continue;
8893
+ const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
8894
+ const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
8895
+ const shiftEndpoint = (endpointIndex, adjacentIndex, delta) => {
8896
+ if (delta === 0) return;
8897
+ const endpoint = points[endpointIndex];
8898
+ const adjacent = points[adjacentIndex];
8899
+ if (!endpoint) return;
8900
+ const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
8901
+ points[endpointIndex] = horizontalAntiparallelFlow ? {
8902
+ ...endpoint,
8903
+ y: endpoint.y + delta
8904
+ } : {
8905
+ ...endpoint,
8906
+ x: endpoint.x + delta
8907
+ };
8908
+ if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontalAntiparallelFlow ? {
8909
+ ...adjacent,
8910
+ y: adjacent.y + delta
8911
+ } : {
8912
+ ...adjacent,
8913
+ x: adjacent.x + delta
8914
+ };
8915
+ };
8916
+ shiftEndpoint(0, 1, sourceDelta);
8917
+ shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
8918
+ pointsByEdgeId.set(edge.id, points);
8919
+ }
8920
+ expandedRoutes = {
8921
+ ...expandedRoutes,
8922
+ pointsByEdgeId
8923
+ };
8924
+ }
8704
8925
  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
8926
  if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
8706
8927
  const horizontal = direction === "right" || direction === "left";
@@ -8832,6 +9053,84 @@ function runLayeredPipeline(graph, options, context) {
8832
9053
  };
8833
9054
  routes.pointsByEdgeId.set(edge.id, points);
8834
9055
  }
9056
+ for (const pair of labeledEdgesByPair.values()) {
9057
+ if (pair.length < 2) continue;
9058
+ const sourceRect = mutableRects.get(pair[0].sourceId);
9059
+ const targetRect = mutableRects.get(pair[0].targetId);
9060
+ if (!sourceRect || !targetRect) continue;
9061
+ const horizontal = direction === "left" || direction === "right";
9062
+ const beforeRect = horizontal ? sourceRect.x <= targetRect.x ? sourceRect : targetRect : sourceRect.y <= targetRect.y ? sourceRect : targetRect;
9063
+ const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
9064
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
9065
+ let cross = horizontal ? padding.top : padding.left;
9066
+ const orderedPair = options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES" ? pair : [...pair].reverse();
9067
+ for (const edge of orderedPair) {
9068
+ const width = edge.width ?? 0;
9069
+ const height = edge.height ?? 0;
9070
+ parallelLabelPositions.set(edge.id, horizontal ? {
9071
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
9072
+ y: cross
9073
+ } : {
9074
+ x: cross,
9075
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
9076
+ });
9077
+ cross += (horizontal ? height : width) + edgeSpacing;
9078
+ }
9079
+ const anchorEdge = orderedPair[0];
9080
+ const anchorLabel = parallelLabelPositions.get(anchorEdge.id);
9081
+ const desiredCross = Math.round(horizontal ? anchorLabel.y + (anchorEdge.height ?? 0) / 2 : anchorLabel.x + (anchorEdge.width ?? 0) / 2);
9082
+ const alignEndpoint = (nodeId, portName) => {
9083
+ const node = graph.nodes.find((candidate) => candidate.id === nodeId);
9084
+ const rect = mutableRects.get(nodeId);
9085
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
9086
+ if (!node || !rect || !port) return;
9087
+ const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
9088
+ ...options.settings,
9089
+ ...options.nodeSettings?.(node)
9090
+ }, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
9091
+ if (!placedPort) return;
9092
+ const localCross = horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
9093
+ const currentCross = horizontal ? rect.y : rect.x;
9094
+ const nextCross = desiredCross - localCross;
9095
+ const delta = nextCross - currentCross;
9096
+ mutableRects.set(nodeId, horizontal ? {
9097
+ ...rect,
9098
+ y: nextCross
9099
+ } : {
9100
+ ...rect,
9101
+ x: nextCross
9102
+ });
9103
+ for (const edge of graph.edges) {
9104
+ const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
9105
+ if (points.length === 0) continue;
9106
+ const index = edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : void 0;
9107
+ if (index === void 0) continue;
9108
+ const endpoint = points[index];
9109
+ const originalCross = horizontal ? endpoint.y : endpoint.x;
9110
+ points[index] = horizontal ? {
9111
+ ...endpoint,
9112
+ y: endpoint.y + delta
9113
+ } : {
9114
+ ...endpoint,
9115
+ x: endpoint.x + delta
9116
+ };
9117
+ const adjacentIndex = index === 0 ? 1 : points.length - 2;
9118
+ const adjacent = points[adjacentIndex];
9119
+ if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontal ? {
9120
+ ...adjacent,
9121
+ y: adjacent.y + delta
9122
+ } : {
9123
+ ...adjacent,
9124
+ x: adjacent.x + delta
9125
+ };
9126
+ routes.pointsByEdgeId.set(edge.id, points);
9127
+ }
9128
+ };
9129
+ if (anchorEdge.sourcePort && anchorEdge.targetPort) {
9130
+ alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
9131
+ alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
9132
+ }
9133
+ }
8835
9134
  const routedPortAnchors = /* @__PURE__ */ new Map();
8836
9135
  for (const edge of graph.edges) {
8837
9136
  const points = routes.pointsByEdgeId.get(edge.id);
@@ -8846,7 +9145,7 @@ function runLayeredPipeline(graph, options, context) {
8846
9145
  let ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
8847
9146
  ...options.settings,
8848
9147
  ...options.nodeSettings?.(node)
8849
- });
9148
+ }, (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port));
8850
9149
  if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") ports = ports?.map((port) => {
8851
9150
  if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
8852
9151
  const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
@@ -8921,8 +9220,9 @@ function runLayeredPipeline(graph, options, context) {
8921
9220
  }] : [];
8922
9221
  }).sort((left, right) => right.length - left.length)[0] : void 0;
8923
9222
  const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
9223
+ const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
8924
9224
  const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
8925
- const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
9225
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
8926
9226
  const sourcePortSide = sourceNode && sourcePort ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"] : void 0;
8927
9227
  const targetPortSide = sourceNode && targetPort ? options.portSettings?.(targetPort, sourceNode)?.["port.side"] : void 0;
8928
9228
  const horizontalFeedbackTrack = edge.sourceId === edge.targetId && (sourcePortSide === "EAST" || sourcePortSide === "WEST") && targetPortSide === sourcePortSide || (horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1) ? horizontalFeedbackCandidate : void 0;
@@ -8931,8 +9231,17 @@ function runLayeredPipeline(graph, options, context) {
8931
9231
  const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
8932
9232
  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
9233
  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;
9234
+ const sourceRect = placement.rectByNodeId.get(edge.sourceId);
9235
+ const targetRect = placement.rectByNodeId.get(edge.targetId);
9236
+ const hasFlexiblePorts = (node) => {
9237
+ const constraints = node ? options.nodeSettings?.(node)?.portConstraints : void 0;
9238
+ return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
9239
+ };
9240
+ const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
9241
+ 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);
9243
+ 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
+ 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
9245
  return {
8937
9246
  ...edge,
8938
9247
  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-f_3mmGHr.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.2",
3
+ "version": "0.0.3",
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": {
@@ -27,6 +27,10 @@
27
27
  "types": "./src/elkjs/index.ts",
28
28
  "default": "./dist/elkjs/index.mjs"
29
29
  },
30
+ "./lib/elk.bundled.js": {
31
+ "types": "./src/elkjs/index.ts",
32
+ "default": "./dist/elkjs/index.mjs"
33
+ },
30
34
  "./layered": {
31
35
  "types": "./src/layered/index.ts",
32
36
  "default": "./dist/layered/index.mjs"
@@ -824,8 +824,11 @@ function applyNodeMicroLayout(
824
824
  constraints.includes("PORT_LABELS") &&
825
825
  !constraints.includes("NODE_LABELS") &&
826
826
  !constraints.includes("MINIMUM_SIZE");
827
- let width = effectivelyFixedPortLabelSize ? (node.width ?? 0) : 0;
828
- let height = effectivelyFixedPortLabelSize ? (node.height ?? 0) : 0;
827
+ const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
828
+ let width =
829
+ effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? (node.width ?? 0) : 0;
830
+ let height =
831
+ effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? (node.height ?? 0) : 0;
829
832
  let insideHorizontalInset = 0;
830
833
  let insideVerticalInset = 0;
831
834
  const insideLabelCells = new Map<string, { width: number; height: number }>();
@@ -845,8 +848,12 @@ function applyNodeMicroLayout(
845
848
  }
846
849
  width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
847
850
  height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
848
- if (sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
849
- if (sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
851
+ if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) {
852
+ width = 0;
853
+ }
854
+ if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) {
855
+ height = 0;
856
+ }
850
857
  }
851
858
  if (constraints.includes("NODE_LABELS")) {
852
859
  for (const label of node.labels ?? []) {
@@ -29,6 +29,7 @@ import {
29
29
  breakCyclesWithModelOrderBreadthFirstSearch,
30
30
  breakCyclesWithDepthFirstSearch,
31
31
  getPolylineMidpoint,
32
+ getOrientedPortDirection,
32
33
  minimizeCrossingsWithBarycenter,
33
34
  minimizeCrossingsWithMedian,
34
35
  minimizeCrossingsInteractively,
@@ -1332,6 +1333,37 @@ function runLayeredPipeline<N, E, G, P>(
1332
1333
  left: options.padding?.left ?? 12,
1333
1334
  };
1334
1335
  const switchedSideByPort = new Map<string, "NORTH" | "SOUTH" | "WEST" | "EAST">();
1336
+ const parallelPortIndexByKey = new Map<string, number>();
1337
+ const labeledEdgesByPair = new Map<string, GraphEdge[]>();
1338
+ for (const edge of graph.edges as GraphEdge[]) {
1339
+ const settings = options.edgeSettings?.(edge);
1340
+ if (
1341
+ edge.sourcePort === undefined ||
1342
+ edge.targetPort === undefined ||
1343
+ edge.sourceId === edge.targetId ||
1344
+ (edge.width ?? 0) <= 0 ||
1345
+ (edge.height ?? 0) <= 0 ||
1346
+ (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
1347
+ settings?.["edgeLabels.inline"] !== true
1348
+ ) {
1349
+ continue;
1350
+ }
1351
+ const key = `${edge.sourceId}\0${edge.targetId}`;
1352
+ const pair = labeledEdgesByPair.get(key) ?? [];
1353
+ pair.push(edge);
1354
+ labeledEdgesByPair.set(key, pair);
1355
+ }
1356
+ for (const pair of labeledEdgesByPair.values()) {
1357
+ if (pair.length < 2) continue;
1358
+ for (const [index, edge] of pair.entries()) {
1359
+ const portIndex =
1360
+ options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
1361
+ ? index
1362
+ : pair.length - index - 1;
1363
+ parallelPortIndexByKey.set(`${edge.sourceId}\0${edge.sourcePort}`, portIndex);
1364
+ parallelPortIndexByKey.set(`${edge.targetId}\0${edge.targetPort}`, portIndex);
1365
+ }
1366
+ }
1335
1367
  const input: LayeredPhaseInput = {
1336
1368
  graph: graph as Graph<unknown, unknown, unknown, unknown>,
1337
1369
  sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
@@ -1352,6 +1384,9 @@ function runLayeredPipeline<N, E, G, P>(
1352
1384
  ...(options.edgeSettings === undefined ? {} : { edgeSettings: options.edgeSettings }),
1353
1385
  portSettings: (port, node) => ({
1354
1386
  ...options.portSettings?.(port, node),
1387
+ ...(parallelPortIndexByKey.has(`${node.id}\0${port.name}`)
1388
+ ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) }
1389
+ : {}),
1355
1390
  ...(switchedSideByPort.has(`${node.id}\0${port.name}`)
1356
1391
  ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) }
1357
1392
  : {}),
@@ -1660,7 +1695,7 @@ function runLayeredPipeline<N, E, G, P>(
1660
1695
  }
1661
1696
  }
1662
1697
  measure("port-margin-normalization", () =>
1663
- normalizePlacementForPortExtents(expanded.input, placement, order),
1698
+ normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation),
1664
1699
  );
1665
1700
  {
1666
1701
  const horizontal = direction === "left" || direction === "right";
@@ -1720,10 +1755,257 @@ function runLayeredPipeline<N, E, G, P>(
1720
1755
  : edgeRouting === "SPLINES"
1721
1756
  ? routeEdgesWithSplines
1722
1757
  : routeEdgesOrthogonally);
1723
- const expandedRoutes = measure("edge-routing", () =>
1758
+ let expandedRoutes = measure("edge-routing", () =>
1724
1759
  edgeRouter(expanded.input, expanded.orientation, placement),
1725
1760
  );
1726
1761
  measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
1762
+ const antiparallelLabelPositions = new Map<string, Point>();
1763
+ const parallelLabelPositions = new Map<string, Point>();
1764
+ const postCompactionNodeCrossDeltas = new Map<string, number>();
1765
+ const horizontalAntiparallelFlow = direction === "left" || direction === "right";
1766
+ const usesVizInteractivePlacementProfile =
1767
+ options.strategies?.routeEdges === undefined &&
1768
+ options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" &&
1769
+ options.settings?.["layering.strategy"] === "INTERACTIVE" &&
1770
+ options.settings?.["crossingMinimization.forceNodeModelOrder"] === true &&
1771
+ options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" &&
1772
+ options.settings?.["nodePlacement.favorStraightEdges"] === true &&
1773
+ options.settings?.["compaction.postCompaction.strategy"] === "LEFT" &&
1774
+ options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE";
1775
+ if (usesVizInteractivePlacementProfile) {
1776
+ const neighbors = new Map(graph.nodes.map((node) => [node.id, new Set<string>()]));
1777
+ for (const edge of graph.edges) {
1778
+ if (edge.sourceId === edge.targetId) continue;
1779
+ neighbors.get(edge.sourceId)?.add(edge.targetId);
1780
+ neighbors.get(edge.targetId)?.add(edge.sourceId);
1781
+ }
1782
+ const visited = new Set<string>();
1783
+ for (const node of graph.nodes) {
1784
+ if (visited.has(node.id)) continue;
1785
+ const component: GraphNode[] = [];
1786
+ const pending = [node.id];
1787
+ while (pending.length > 0) {
1788
+ const id = pending.pop()!;
1789
+ if (visited.has(id)) continue;
1790
+ visited.add(id);
1791
+ const member = graph.nodes.find((candidate) => candidate.id === id);
1792
+ if (member) component.push(member);
1793
+ for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
1794
+ }
1795
+ if (component.length < 2) continue;
1796
+ const componentIds = new Set(component.map((member) => member.id));
1797
+ if (
1798
+ !graph.edges.some(
1799
+ (edge) =>
1800
+ componentIds.has(edge.sourceId) &&
1801
+ componentIds.has(edge.targetId) &&
1802
+ expanded.orientation.reversedEdgeIds.has(edge.id),
1803
+ )
1804
+ ) {
1805
+ continue;
1806
+ }
1807
+ const directedPairCounts = new Map<string, number>();
1808
+ for (const edge of graph.edges) {
1809
+ if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
1810
+ const key = `${edge.sourceId}\0${edge.targetId}`;
1811
+ directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
1812
+ }
1813
+ if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
1814
+ if (
1815
+ component.some((member) => {
1816
+ const constraints = options.nodeSettings?.(member)?.portConstraints;
1817
+ return constraints !== undefined && constraints !== "UNDEFINED" && constraints !== "FREE";
1818
+ })
1819
+ ) {
1820
+ continue;
1821
+ }
1822
+ const layerCounts = new Map<number, number>();
1823
+ for (const member of component) {
1824
+ const layer = expanded.assignment.layerByNodeId.get(member.id);
1825
+ if (layer === undefined) continue;
1826
+ layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
1827
+ }
1828
+ if ([...layerCounts.values()].some((count) => count !== 1)) continue;
1829
+ const rects = component.flatMap((member) => {
1830
+ const rect = mutableRects.get(member.id);
1831
+ return rect ? [{ member, rect }] : [];
1832
+ });
1833
+ if (rects.length !== component.length) continue;
1834
+ const minimumCross = Math.min(
1835
+ ...rects.map(({ rect }) => (horizontalAntiparallelFlow ? rect.y : rect.x)),
1836
+ );
1837
+ const maximumCross = Math.max(
1838
+ ...rects.map(({ rect }) =>
1839
+ horizontalAntiparallelFlow ? rect.y + rect.height : rect.x + rect.width,
1840
+ ),
1841
+ );
1842
+ const center = (minimumCross + maximumCross) / 2;
1843
+ for (const { member, rect } of rects) {
1844
+ const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
1845
+ const nextCross = horizontalAntiparallelFlow
1846
+ ? center - rect.height / 2
1847
+ : center - rect.width / 2;
1848
+ postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
1849
+ mutableRects.set(
1850
+ member.id,
1851
+ horizontalAntiparallelFlow ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
1852
+ );
1853
+ }
1854
+ }
1855
+ }
1856
+ {
1857
+ const horizontal = horizontalAntiparallelFlow;
1858
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
1859
+ const pairEdges = new Map<string, GraphEdge[]>();
1860
+ for (const edge of graph.edges) {
1861
+ if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) {
1862
+ continue;
1863
+ }
1864
+ const key = [edge.sourceId, edge.targetId].sort().join("\0");
1865
+ const pair = pairEdges.get(key) ?? [];
1866
+ pair.push(edge);
1867
+ pairEdges.set(key, pair);
1868
+ }
1869
+ for (const pair of pairEdges.values()) {
1870
+ if (
1871
+ pair.length !== 2 ||
1872
+ pair[0]!.sourceId !== pair[1]!.targetId ||
1873
+ pair[0]!.targetId !== pair[1]!.sourceId ||
1874
+ pair.some((edge) => {
1875
+ const settings = options.edgeSettings?.(edge);
1876
+ return (
1877
+ (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
1878
+ settings?.["edgeLabels.inline"] !== true
1879
+ );
1880
+ })
1881
+ ) {
1882
+ continue;
1883
+ }
1884
+ const firstNode = nodesById.get(pair[0]!.sourceId);
1885
+ const secondNode = nodesById.get(pair[0]!.targetId);
1886
+ const firstRect = placement.rectByNodeId.get(pair[0]!.sourceId);
1887
+ const secondRect = placement.rectByNodeId.get(pair[0]!.targetId);
1888
+ if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
1889
+ const nonSelfNeighbors = (nodeId: string) =>
1890
+ new Set(
1891
+ graph.edges.flatMap((edge) =>
1892
+ edge.sourceId === edge.targetId
1893
+ ? []
1894
+ : edge.sourceId === nodeId
1895
+ ? [edge.targetId]
1896
+ : edge.targetId === nodeId
1897
+ ? [edge.sourceId]
1898
+ : [],
1899
+ ),
1900
+ );
1901
+ if (nonSelfNeighbors(firstNode.id).size !== 1 || nonSelfNeighbors(secondNode.id).size !== 1) {
1902
+ continue;
1903
+ }
1904
+ const hasFlexiblePorts = (node: GraphNode): boolean => {
1905
+ const constraints = options.nodeSettings?.(node)?.portConstraints;
1906
+ return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
1907
+ };
1908
+ if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
1909
+
1910
+ const firstBeforeSecond = horizontal
1911
+ ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2
1912
+ : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
1913
+ const beforeRect = firstBeforeSecond ? firstRect : secondRect;
1914
+ const afterRect = firstBeforeSecond ? secondRect : firstRect;
1915
+ const forward = pair.find((edge) =>
1916
+ firstBeforeSecond
1917
+ ? edge.sourceId === pair[0]!.sourceId
1918
+ : edge.sourceId === pair[0]!.targetId,
1919
+ );
1920
+ if (!forward?.sourcePort || !forward.targetPort) continue;
1921
+
1922
+ let cross = Math.min(
1923
+ horizontal ? firstRect.y : firstRect.x,
1924
+ horizontal ? secondRect.y : secondRect.x,
1925
+ );
1926
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
1927
+ for (const edge of pair) {
1928
+ const width = edge.width ?? 0;
1929
+ const height = edge.height ?? 0;
1930
+ antiparallelLabelPositions.set(
1931
+ 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
+ },
1941
+ );
1942
+ cross += (horizontal ? height : width) + edgeSpacing;
1943
+ }
1944
+
1945
+ const forwardLabel = antiparallelLabelPositions.get(forward.id)!;
1946
+ const forwardCross = horizontal
1947
+ ? Math.round(forwardLabel.y + (forward.height ?? 0) / 2)
1948
+ : Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
1949
+ const alignPort = (node: GraphNode, portName: string): void => {
1950
+ const rect = placement.rectByNodeId.get(node.id);
1951
+ const port = node.ports?.find((candidate) => candidate.name === portName);
1952
+ if (!rect || !port) return;
1953
+ const placedPort = placePorts(
1954
+ node.ports,
1955
+ rect,
1956
+ direction,
1957
+ (candidate) => input.portSettings?.(candidate, node),
1958
+ { ...options.settings, ...options.nodeSettings?.(node) },
1959
+ (candidate) =>
1960
+ getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
1961
+ )?.find((candidate) => candidate.name === portName);
1962
+ if (!placedPort) return;
1963
+ const localCross = horizontal
1964
+ ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
1965
+ : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
1966
+ const nextCross = forwardCross - localCross;
1967
+ const currentCross = horizontal ? rect.y : rect.x;
1968
+ postCompactionNodeCrossDeltas.set(
1969
+ node.id,
1970
+ (postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross,
1971
+ );
1972
+ mutableRects.set(
1973
+ node.id,
1974
+ horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
1975
+ );
1976
+ };
1977
+ alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
1978
+ alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
1979
+ }
1980
+ }
1981
+ if (postCompactionNodeCrossDeltas.size > 0) {
1982
+ const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
1983
+ for (const edge of graph.edges) {
1984
+ const points = [...(pointsByEdgeId.get(edge.id) ?? [])];
1985
+ if (points.length === 0) continue;
1986
+ const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
1987
+ const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
1988
+ const shiftEndpoint = (endpointIndex: number, adjacentIndex: number, delta: number): void => {
1989
+ if (delta === 0) return;
1990
+ const endpoint = points[endpointIndex];
1991
+ const adjacent = points[adjacentIndex];
1992
+ if (!endpoint) return;
1993
+ const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
1994
+ points[endpointIndex] = horizontalAntiparallelFlow
1995
+ ? { ...endpoint, y: endpoint.y + delta }
1996
+ : { ...endpoint, x: endpoint.x + delta };
1997
+ if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) {
1998
+ points[adjacentIndex] = horizontalAntiparallelFlow
1999
+ ? { ...adjacent, y: adjacent.y + delta }
2000
+ : { ...adjacent, x: adjacent.x + delta };
2001
+ }
2002
+ };
2003
+ shiftEndpoint(0, 1, sourceDelta);
2004
+ shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
2005
+ pointsByEdgeId.set(edge.id, points);
2006
+ }
2007
+ expandedRoutes = { ...expandedRoutes, pointsByEdgeId };
2008
+ }
1727
2009
  let routes = measure("long-edge-joining", () =>
1728
2010
  joinLongEdgeRoutes(
1729
2011
  expandedRoutes,
@@ -1877,6 +2159,99 @@ function runLayeredPipeline<N, E, G, P>(
1877
2159
  : { ...end, y: end.y + delta };
1878
2160
  (routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
1879
2161
  }
2162
+ for (const pair of labeledEdgesByPair.values()) {
2163
+ if (pair.length < 2) continue;
2164
+ const sourceRect = mutableRects.get(pair[0]!.sourceId);
2165
+ const targetRect = mutableRects.get(pair[0]!.targetId);
2166
+ if (!sourceRect || !targetRect) continue;
2167
+ const horizontal = direction === "left" || direction === "right";
2168
+ const beforeRect = horizontal
2169
+ ? sourceRect.x <= targetRect.x
2170
+ ? sourceRect
2171
+ : targetRect
2172
+ : sourceRect.y <= targetRect.y
2173
+ ? sourceRect
2174
+ : targetRect;
2175
+ const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
2176
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
2177
+ let cross = horizontal ? padding.top : padding.left;
2178
+ const orderedPair =
2179
+ options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
2180
+ ? pair
2181
+ : [...pair].reverse();
2182
+ for (const edge of orderedPair) {
2183
+ const width = edge.width ?? 0;
2184
+ const height = edge.height ?? 0;
2185
+ parallelLabelPositions.set(
2186
+ edge.id,
2187
+ horizontal
2188
+ ? {
2189
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
2190
+ y: cross,
2191
+ }
2192
+ : {
2193
+ x: cross,
2194
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
2195
+ },
2196
+ );
2197
+ cross += (horizontal ? height : width) + edgeSpacing;
2198
+ }
2199
+
2200
+ const anchorEdge = orderedPair[0]!;
2201
+ const anchorLabel = parallelLabelPositions.get(anchorEdge.id)!;
2202
+ const desiredCross = Math.round(
2203
+ horizontal
2204
+ ? anchorLabel.y + (anchorEdge.height ?? 0) / 2
2205
+ : anchorLabel.x + (anchorEdge.width ?? 0) / 2,
2206
+ );
2207
+ const alignEndpoint = (nodeId: string, portName: string): void => {
2208
+ const node = graph.nodes.find((candidate) => candidate.id === nodeId);
2209
+ const rect = mutableRects.get(nodeId);
2210
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
2211
+ if (!node || !rect || !port) return;
2212
+ const placedPort = placePorts(
2213
+ node.ports,
2214
+ rect,
2215
+ direction,
2216
+ (candidate) => input.portSettings?.(candidate, node),
2217
+ { ...options.settings, ...options.nodeSettings?.(node) },
2218
+ (candidate) =>
2219
+ getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
2220
+ )?.find((candidate) => candidate.name === portName);
2221
+ if (!placedPort) return;
2222
+ const localCross = horizontal
2223
+ ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
2224
+ : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
2225
+ const currentCross = horizontal ? rect.y : rect.x;
2226
+ const nextCross = desiredCross - localCross;
2227
+ const delta = nextCross - currentCross;
2228
+ mutableRects.set(nodeId, horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross });
2229
+ for (const edge of graph.edges) {
2230
+ const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
2231
+ if (points.length === 0) continue;
2232
+ const index =
2233
+ edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : undefined;
2234
+ if (index === undefined) continue;
2235
+ const endpoint = points[index]!;
2236
+ const originalCross = horizontal ? endpoint.y : endpoint.x;
2237
+ points[index] = horizontal
2238
+ ? { ...endpoint, y: endpoint.y + delta }
2239
+ : { ...endpoint, x: endpoint.x + delta };
2240
+ const adjacentIndex = index === 0 ? 1 : points.length - 2;
2241
+ const adjacent = points[adjacentIndex];
2242
+ if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) {
2243
+ points[adjacentIndex] = horizontal
2244
+ ? { ...adjacent, y: adjacent.y + delta }
2245
+ : { ...adjacent, x: adjacent.x + delta };
2246
+ }
2247
+ (routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
2248
+ }
2249
+ };
2250
+ if (anchorEdge.sourcePort && anchorEdge.targetPort) {
2251
+ alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
2252
+ alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
2253
+ }
2254
+ }
1880
2255
  const routedPortAnchors = new Map<string, Point>();
1881
2256
  for (const edge of graph.edges) {
1882
2257
  const points = routes.pointsByEdgeId.get(edge.id);
@@ -1914,6 +2289,7 @@ function runLayeredPipeline<N, E, G, P>(
1914
2289
  direction,
1915
2290
  (port) => input.portSettings?.(port, node),
1916
2291
  { ...options.settings, ...options.nodeSettings?.(node) },
2292
+ (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port),
1917
2293
  );
1918
2294
  if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") {
1919
2295
  ports = ports?.map((port) => {
@@ -2024,8 +2400,9 @@ function runLayeredPipeline<N, E, G, P>(
2024
2400
  .sort((left, right) => right.length - left.length)[0]
2025
2401
  : undefined;
2026
2402
  const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
2403
+ const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
2027
2404
  const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
2028
- const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
2405
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
2029
2406
  const sourcePortSide =
2030
2407
  sourceNode && sourcePort
2031
2408
  ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"]
@@ -2081,10 +2458,50 @@ function runLayeredPipeline<N, E, G, P>(
2081
2458
  : (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) +
2082
2459
  labelSpacing +
2083
2460
  Math.round(edgeThickness / 2);
2084
- const x =
2085
- labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
2086
- const y =
2087
- labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
2461
+ const sourceRect = placement.rectByNodeId.get(edge.sourceId);
2462
+ const targetRect = placement.rectByNodeId.get(edge.targetId);
2463
+ const hasFlexiblePorts = (node: GraphNode | undefined): boolean => {
2464
+ const constraints = node ? options.nodeSettings?.(node)?.portConstraints : undefined;
2465
+ return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
2466
+ };
2467
+ const flexibleFeedbackLabel =
2468
+ labelPlacement === "CENTER" &&
2469
+ inlineLabel &&
2470
+ expanded.orientation.reversedEdgeIds.has(edge.id) &&
2471
+ hasFlexiblePorts(sourceNode) &&
2472
+ hasFlexiblePorts(targetNode) &&
2473
+ sourceRect !== undefined &&
2474
+ targetRect !== undefined;
2475
+ const [beforeFlowRect, afterFlowRect] =
2476
+ sourceRect && targetRect
2477
+ ? horizontal
2478
+ ? sourceRect.x <= targetRect.x
2479
+ ? [sourceRect, targetRect]
2480
+ : [targetRect, sourceRect]
2481
+ : sourceRect.y <= targetRect.y
2482
+ ? [sourceRect, targetRect]
2483
+ : [targetRect, sourceRect]
2484
+ : [undefined, undefined];
2485
+ const explicitLabelPosition =
2486
+ antiparallelLabelPositions.get(edge.id) ?? parallelLabelPositions.get(edge.id);
2487
+ const x = explicitLabelPosition
2488
+ ? explicitLabelPosition.x
2489
+ : flexibleFeedbackLabel
2490
+ ? horizontal
2491
+ ? (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2
2492
+ : targetRect.x + (targetRect.width - width) / 2
2493
+ : labelPlacement === "CENTER" && horizontal && labelDummyRect
2494
+ ? labelDummyRect.x
2495
+ : routeX;
2496
+ const y = explicitLabelPosition
2497
+ ? explicitLabelPosition.y
2498
+ : flexibleFeedbackLabel
2499
+ ? horizontal
2500
+ ? targetRect.y + (targetRect.height - height) / 2
2501
+ : (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2
2502
+ : labelPlacement === "CENTER" && !horizontal && labelDummyRect
2503
+ ? labelDummyRect.y
2504
+ : routeY;
2088
2505
  return {
2089
2506
  ...edge,
2090
2507
  x,
@@ -71,14 +71,15 @@ export function splitLongEdges(
71
71
  source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : undefined;
72
72
  const targetSide =
73
73
  target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : undefined;
74
+ const hasFixedPortSide = (node: GraphNode | undefined): boolean => {
75
+ if (!node) return false;
76
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
77
+ return constraints !== "UNDEFINED" && constraints !== "FREE";
78
+ };
74
79
  const fixedSideFeedback =
75
80
  sourceLayer > targetLayer &&
76
- ((source !== undefined &&
77
- input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" &&
78
- sourceSide === forwardSourceSide) ||
79
- (target !== undefined &&
80
- input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" &&
81
- targetSide === forwardTargetSide));
81
+ ((hasFixedPortSide(source) && sourceSide === forwardSourceSide) ||
82
+ (hasFixedPortSide(target) && targetSide === forwardTargetSide));
82
83
  if (
83
84
  span <= 1 ||
84
85
  edge.sourceId === edge.targetId ||
@@ -30,6 +30,37 @@ function getOrientedEndpoints(
30
30
  : [edge.sourceId, edge.targetId];
31
31
  }
32
32
 
33
+ export function getOrientedPortDirection(
34
+ input: LayeredPhaseInput,
35
+ orientation: AcyclicOrientation,
36
+ node: GraphNode,
37
+ port: GraphPort,
38
+ ): GraphPort["direction"] {
39
+ const configuredSide = input.portSettings?.(port, node)?.["port.side"];
40
+ if (
41
+ configuredSide !== "NORTH" &&
42
+ configuredSide !== "SOUTH" &&
43
+ configuredSide !== "WEST" &&
44
+ configuredSide !== "EAST"
45
+ ) {
46
+ return port.direction;
47
+ }
48
+ let incoming = false;
49
+ let outgoing = false;
50
+ for (const edge of input.graph.edges) {
51
+ const reversed = orientation.reversedEdgeIds.has(edge.id);
52
+ if (edge.sourceId === node.id && edge.sourcePort === port.name) {
53
+ if (reversed) incoming = true;
54
+ else outgoing = true;
55
+ }
56
+ if (edge.targetId === node.id && edge.targetPort === port.name) {
57
+ if (reversed) outgoing = true;
58
+ else incoming = true;
59
+ }
60
+ }
61
+ return incoming && outgoing ? "inout" : outgoing ? "out" : incoming ? "in" : port.direction;
62
+ }
63
+
33
64
  function cycleModelOrder(input: LayeredPhaseInput): Map<string, number> {
34
65
  const enforced =
35
66
  input.settings["considerModelOrder.groupModelOrder.cbGroupOrderStrategy"] === "ENFORCED";
@@ -4312,10 +4343,15 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
4312
4343
  source.id === target.id &&
4313
4344
  feedbackSourcePortSide !== undefined &&
4314
4345
  feedbackSourcePortSide === feedbackTargetPortSide;
4346
+ const hasFixedPortSide = (node: GraphNode): boolean => {
4347
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
4348
+ return constraints !== "UNDEFINED" && constraints !== "FREE";
4349
+ };
4315
4350
  const fixedSideFeedback =
4316
4351
  style === "ORTHOGONAL" &&
4317
4352
  !sameSideSelfLoop &&
4318
- (feedbackSourcePortSide === sourceAwaySide || feedbackTargetPortSide === targetAwaySide);
4353
+ ((hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide) ||
4354
+ (hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide));
4319
4355
  if ((input.settings.feedbackEdges === true && reversedEdge) || fixedSideFeedback) {
4320
4356
  if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
4321
4357
  const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
@@ -4769,12 +4805,32 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
4769
4805
  (candidate) =>
4770
4806
  candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort,
4771
4807
  ).length === 1;
4772
- const start = sourceFixedSide
4808
+ const flexibleFeedback =
4809
+ reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
4810
+ const start = flexibleFeedback
4773
4811
  ? sourceFallback
4774
- : getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
4775
- const end = targetFixedSide
4812
+ : sourceFixedSide
4813
+ ? sourceFallback
4814
+ : getPortPoint(
4815
+ source,
4816
+ edge.sourcePort,
4817
+ sourceRect,
4818
+ sourceFallback,
4819
+ input.direction,
4820
+ input,
4821
+ );
4822
+ const end = flexibleFeedback
4776
4823
  ? targetFallback
4777
- : getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
4824
+ : targetFixedSide
4825
+ ? targetFallback
4826
+ : getPortPoint(
4827
+ target,
4828
+ edge.targetPort,
4829
+ targetRect,
4830
+ targetFallback,
4831
+ input.direction,
4832
+ input,
4833
+ );
4778
4834
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
4779
4835
  const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
4780
4836
  const earlierLayer = Math.min(sourceLayer, targetLayer);
@@ -4955,6 +5011,7 @@ export function placePorts<P>(
4955
5011
  direction: LayeredPhaseInput["direction"],
4956
5012
  portSettings?: (port: GraphPort<P>) => ElkLayeredOptionValueByName | undefined,
4957
5013
  nodeSettings?: ElkLayeredOptionValueByName,
5014
+ portDirection?: (port: GraphPort<P>) => GraphPort<P>["direction"],
4958
5015
  ): GraphPort<P>[] | undefined {
4959
5016
  if (!ports) return undefined;
4960
5017
  const horizontal = direction === "left" || direction === "right";
@@ -4981,7 +5038,7 @@ export function placePorts<P>(
4981
5038
  sideByPort.set(port, configured);
4982
5039
  continue;
4983
5040
  }
4984
- const outgoing = port.direction === "out";
5041
+ const outgoing = (portDirection?.(port) ?? port.direction) === "out";
4985
5042
  const normalFarSide = reverse ? !outgoing : outgoing;
4986
5043
  const farSide = sideFixed && configured === "UNDEFINED" ? !normalFarSide : normalFarSide;
4987
5044
  sideByPort.set(port, horizontal ? (farSide ? "EAST" : "WEST") : farSide ? "SOUTH" : "NORTH");
@@ -5221,6 +5278,7 @@ export function normalizePlacementForPortExtents(
5221
5278
  input: LayeredPhaseInput,
5222
5279
  placement: NodePlacement,
5223
5280
  order: LayerOrder,
5281
+ orientation: AcyclicOrientation,
5224
5282
  ): void {
5225
5283
  const horizontal = input.direction === "left" || input.direction === "right";
5226
5284
  const physicalLayers = order.layers
@@ -5267,6 +5325,7 @@ export function normalizePlacementForPortExtents(
5267
5325
  input.direction,
5268
5326
  (port) => input.portSettings?.(port, node),
5269
5327
  { ...input.settings, ...input.nodeSettings?.(node) },
5328
+ (port) => getOrientedPortDirection(input, orientation, node, port),
5270
5329
  );
5271
5330
  for (const port of ports ?? []) {
5272
5331
  const extent = horizontal
@@ -5297,6 +5356,7 @@ export function normalizePlacementForPortExtents(
5297
5356
  input.direction,
5298
5357
  (port) => input.portSettings?.(port, node),
5299
5358
  { ...input.settings, ...input.nodeSettings?.(node) },
5359
+ (port) => getOrientedPortDirection(input, orientation, node, port),
5300
5360
  );
5301
5361
  for (const port of ports ?? []) {
5302
5362
  minimumX = Math.min(minimumX, rect.x + (port.x ?? 0));