@statelyai/layout 0.0.1 → 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.
Files changed (42) hide show
  1. package/README.md +1 -3
  2. package/dist/elkjs/index.mjs +15 -11
  3. package/dist/{index-D2RodsZY.d.mts → index-8xYkohbz.d.mts} +2 -0
  4. package/dist/index.d.mts +1 -1
  5. package/dist/index.mjs +2 -2
  6. package/dist/layered/index.d.mts +1 -1
  7. package/dist/layered/index.mjs +1 -1
  8. package/dist/{layered-Dd868WZY.mjs → layered-f_3mmGHr.mjs} +717 -62
  9. package/dist/{spore-fTgSoRLP.mjs → spore-BfTduMDc.mjs} +1 -1
  10. package/package.json +20 -6
  11. package/src/box.ts +135 -0
  12. package/src/elkjs/index.ts +1832 -0
  13. package/src/elkjs/types.ts +103 -0
  14. package/src/errors.ts +16 -0
  15. package/src/fixed.ts +80 -0
  16. package/src/index.ts +92 -0
  17. package/src/java-random.ts +46 -0
  18. package/src/layered/bk-node-placement.ts +715 -0
  19. package/src/layered/elk-enum-values.ts +171 -0
  20. package/src/layered/elk-options.generated.ts +315 -0
  21. package/src/layered/elk-options.ts +98 -0
  22. package/src/layered/flexible-ports.ts +11 -0
  23. package/src/layered/high-degree.ts +127 -0
  24. package/src/layered/index.ts +2555 -0
  25. package/src/layered/layer-unzipping.ts +217 -0
  26. package/src/layered/linear-segments-node-placement.ts +447 -0
  27. package/src/layered/long-edges.ts +406 -0
  28. package/src/layered/min-width.ts +159 -0
  29. package/src/layered/multi-edge-wrapping.ts +460 -0
  30. package/src/layered/network-simplex-node-placement.ts +500 -0
  31. package/src/layered/network-simplex.ts +346 -0
  32. package/src/layered/node-promotion.ts +197 -0
  33. package/src/layered/spacing.ts +37 -0
  34. package/src/layered/spline-bezier.ts +102 -0
  35. package/src/layered/strategies.ts +5403 -0
  36. package/src/layered/stretch-width.ts +136 -0
  37. package/src/layered/types.ts +111 -0
  38. package/src/layout.ts +202 -0
  39. package/src/packing.ts +74 -0
  40. package/src/random.ts +142 -0
  41. package/src/spore.ts +103 -0
  42. package/src/types.ts +84 -0
@@ -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);
@@ -1721,6 +1735,14 @@ function applyPostCompaction(input, placement, routes) {
1721
1735
  input.settings["compaction.postCompaction.constraints"];
1722
1736
  if (strategy === "NONE") return placement;
1723
1737
  const rects = placement.rectByNodeId;
1738
+ const originalEndpointsByEdgeId = new Map([...routes?.pointsByEdgeId ?? []].flatMap(([edgeId, points]) => {
1739
+ const start = points[0];
1740
+ const end = points.at(-1);
1741
+ return start && end ? [[edgeId, {
1742
+ start: { ...start },
1743
+ end: { ...end }
1744
+ }]] : [];
1745
+ }));
1724
1746
  const compactables = [...rects].map(([id, rect]) => {
1725
1747
  const longEdgeDummy = id.startsWith("__layout_dummy:") && rect.width === 0 && rect.height === 0;
1726
1748
  const incidentEdge = longEdgeDummy ? input.graph.edges.find((edge) => edge.sourceId === id || edge.targetId === id) : void 0;
@@ -1736,6 +1758,7 @@ function applyPostCompaction(input, placement, routes) {
1736
1758
  };
1737
1759
  });
1738
1760
  if (routes) for (const [edgeId, readonlyPoints] of routes.pointsByEdgeId) {
1761
+ if (routes.outsideFeedbackEdgeIds?.has(edgeId)) continue;
1739
1762
  const points = readonlyPoints;
1740
1763
  for (let index = 0; index + 1 < points.length; index++) {
1741
1764
  const first = points[index];
@@ -1756,7 +1779,10 @@ function applyPostCompaction(input, placement, routes) {
1756
1779
  }
1757
1780
  const verticalSpacing = (left, right) => left.kind === "node" && right.kind === "node" ? input.spacing.node : left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId ? 1 : Number(input.settings[left.kind === "segment" && right.kind === "segment" ? "spacing.edgeEdge" : "spacing.edgeNode"] ?? 10);
1758
1781
  const horizontalSpacing = (left, right) => left.kind === "node" && right.kind === "node" ? input.spacing.node : left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId ? 0 : Number(input.settings[left.kind === "segment" && right.kind === "segment" ? "spacing.edgeEdge" : "spacing.edgeNode"] ?? 10);
1782
+ const centerLabeledEdgeIds = new Set(input.graph.edges.filter((edge) => (edge.width ?? 0) > 0 && (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER").map((edge) => edge.id));
1783
+ const labelFlowLocked = new Set(compactables.filter((item) => item.kind === "segment" && item.edgeId !== void 0 && centerLabeledEdgeIds.has(item.edgeId)).map((item) => item.id));
1759
1784
  const compact$1 = (direction, locked = /* @__PURE__ */ new Set()) => {
1785
+ locked = new Set([...labelFlowLocked, ...locked]);
1760
1786
  const nodes = compactables.map((item) => ({
1761
1787
  ...item,
1762
1788
  x: direction === "RIGHT" ? -item.x - item.width : item.x,
@@ -1823,6 +1849,7 @@ function applyPostCompaction(input, placement, routes) {
1823
1849
  for (const node of input.graph.nodes) {
1824
1850
  const targets = outgoing.get(node.id) ?? [];
1825
1851
  if (targets.length <= (incomingDegree.get(node.id) ?? 0) || targets.length === 0) continue;
1852
+ if (input.graph.edges.some((edge) => edge.sourceId === node.id && centerLabeledEdgeIds.has(edge.id))) continue;
1826
1853
  const item = itemByNodeId.get(node.id);
1827
1854
  if (!item) continue;
1828
1855
  const upper = Math.min(...targets.map((targetId) => (itemByNodeId.get(targetId)?.x ?? item.x) - item.width - input.spacing.node));
@@ -1830,6 +1857,49 @@ function applyPostCompaction(input, placement, routes) {
1830
1857
  }
1831
1858
  }
1832
1859
  }
1860
+ if (strategy === "EDGE_LENGTH" && input.settings["nodePlacement.favorStraightEdges"] === true && (input.direction === "down" || input.direction === "up")) {
1861
+ const itemByNodeId = new Map(compactables.flatMap((item) => item.kind === "node" && item.nodeId ? [[item.nodeId, item]] : []));
1862
+ const incomingDegree = new Map(input.graph.nodes.map((node) => [node.id, 0]));
1863
+ for (const edge of input.graph.edges) if (edge.sourceId !== edge.targetId) incomingDegree.set(edge.targetId, (incomingDegree.get(edge.targetId) ?? 0) + 1);
1864
+ for (const edge of input.graph.edges) {
1865
+ if (edge.sourceId === edge.targetId || (incomingDegree.get(edge.targetId) ?? 0) !== 1) continue;
1866
+ const endpoints$2 = originalEndpointsByEdgeId.get(edge.id);
1867
+ const source = itemByNodeId.get(edge.sourceId);
1868
+ const target = itemByNodeId.get(edge.targetId);
1869
+ if (!endpoints$2 || !source || !target) continue;
1870
+ if (!(input.direction === "down" ? endpoints$2.start.y < endpoints$2.end.y : endpoints$2.start.y > endpoints$2.end.y)) continue;
1871
+ const desiredTargetX = source.x + (endpoints$2.start.x - source.originalX) - (endpoints$2.end.x - target.originalX);
1872
+ if (compactables.some((candidate) => {
1873
+ if (candidate.kind !== "node" || candidate === target) return false;
1874
+ if (!(candidate.y < target.y + target.height && candidate.y + candidate.height > target.y)) return false;
1875
+ const spacing = input.spacing.node;
1876
+ return desiredTargetX < candidate.x + candidate.width + spacing && desiredTargetX + target.width + spacing > candidate.x;
1877
+ })) continue;
1878
+ target.x = desiredTargetX;
1879
+ const alignedX = source.x + (endpoints$2.start.x - source.originalX);
1880
+ for (const segment of compactables) if (segment.kind === "segment" && segment.edgeId === edge.id) segment.x = alignedX;
1881
+ }
1882
+ }
1883
+ if (input.direction === "right" && centerLabeledEdgeIds.size > 0) {
1884
+ const itemByNodeId = new Map(compactables.flatMap((item) => item.kind === "node" && item.nodeId ? [[item.nodeId, item]] : []));
1885
+ const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
1886
+ for (const edge of input.graph.edges) {
1887
+ if (!centerLabeledEdgeIds.has(edge.id)) continue;
1888
+ const source = itemByNodeId.get(edge.sourceId);
1889
+ const target = itemByNodeId.get(edge.targetId);
1890
+ const originalEndpoints = originalEndpointsByEdgeId.get(edge.id);
1891
+ if (!source || !target || !originalEndpoints || originalEndpoints.start.x >= originalEndpoints.end.x) continue;
1892
+ const tracks = compactables.filter((item) => item.kind === "segment" && item.edgeId === edge.id);
1893
+ const track = tracks.length > 0 ? Math.max(...tracks.map((item) => item.x)) : void 0;
1894
+ const targetNode = input.graph.nodes.find((node) => node.id === edge.targetId);
1895
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
1896
+ if (track !== void 0 && targetNode !== void 0 && targetPort !== void 0 && (targetPort.width ?? 8) === 0 && (targetPort.height ?? 8) === 0 && input.nodeSettings?.(targetNode)?.portConstraints === "FIXED_SIDE" && input.graph.edges.filter((candidate) => candidate.targetId === edge.targetId).length > input.graph.edges.filter((candidate) => candidate.sourceId === edge.sourceId).length) source.x = Math.max(source.x, track - edgeNodeSpacing - (edge.width ?? 0) - input.spacing.layer - source.width);
1897
+ else {
1898
+ const routeStart = track !== void 0 ? track + edgeNodeSpacing : source.x + source.width + input.spacing.layer;
1899
+ target.x = Math.max(target.x, routeStart + (edge.width ?? 0) + input.spacing.layer);
1900
+ }
1901
+ }
1902
+ }
1833
1903
  const offset = input.padding.left - Math.min(...compactables.map((item) => item.x));
1834
1904
  for (const item of compactables) item.x += offset;
1835
1905
  const nodeDeltaById = /* @__PURE__ */ new Map();
@@ -1846,14 +1916,71 @@ function applyPostCompaction(input, placement, routes) {
1846
1916
  item.points[1].x += delta;
1847
1917
  }
1848
1918
  if (routes) {
1919
+ const edgeRouting = input.settings.edgeRouting ?? "ORTHOGONAL";
1920
+ const compactedSplineControls = routes.splineNubControlsByEdgeId ? new Map(routes.splineNubControlsByEdgeId) : void 0;
1849
1921
  const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
1850
1922
  for (const [edgeId, readonlyPoints] of routes.pointsByEdgeId) {
1851
- const points = readonlyPoints;
1923
+ const points = [...readonlyPoints];
1852
1924
  const edge = edgeById.get(edgeId);
1853
1925
  if (!edge || points.length === 0) continue;
1854
- points[0].x += nodeDeltaById.get(edge.sourceId) ?? 0;
1855
- points.at(-1).x += nodeDeltaById.get(edge.targetId) ?? 0;
1926
+ const originalEndpoints = originalEndpointsByEdgeId.get(edgeId);
1927
+ if (originalEndpoints) {
1928
+ points[0] = {
1929
+ ...originalEndpoints.start,
1930
+ x: originalEndpoints.start.x + (nodeDeltaById.get(edge.sourceId) ?? 0)
1931
+ };
1932
+ points[points.length - 1] = {
1933
+ ...originalEndpoints.end,
1934
+ x: originalEndpoints.end.x + (nodeDeltaById.get(edge.targetId) ?? 0)
1935
+ };
1936
+ if (routes.outsideFeedbackEdgeIds?.has(edgeId)) {
1937
+ const horizontal = input.direction === "left" || input.direction === "right";
1938
+ const startDelta = points[0].x - originalEndpoints.start.x;
1939
+ const endDelta = points.at(-1).x - originalEndpoints.end.x;
1940
+ if (horizontal && points.length >= 6) {
1941
+ for (const index of [1, 2]) points[index] = {
1942
+ ...points[index],
1943
+ x: points[index].x + startDelta
1944
+ };
1945
+ for (const index of [points.length - 3, points.length - 2]) points[index] = {
1946
+ ...points[index],
1947
+ x: points[index].x + endDelta
1948
+ };
1949
+ } else if (!horizontal && points.length >= 4) {
1950
+ points[1] = {
1951
+ ...points[1],
1952
+ x: points[1].x + startDelta
1953
+ };
1954
+ points[points.length - 2] = {
1955
+ ...points[points.length - 2],
1956
+ x: points[points.length - 2].x + endDelta
1957
+ };
1958
+ }
1959
+ }
1960
+ }
1961
+ let compactedPoints = points;
1962
+ if (edgeRouting === "ORTHOGONAL") {
1963
+ const horizontal = input.direction === "left" || input.direction === "right";
1964
+ const orthogonal = [];
1965
+ for (const [index, point] of points.entries()) {
1966
+ const previous = orthogonal.at(-1);
1967
+ if (previous && Math.abs(previous.x - point.x) > 1e-9 && Math.abs(previous.y - point.y) > 1e-9) {
1968
+ const approachingTarget = index === points.length - 1;
1969
+ orthogonal.push(horizontal === approachingTarget ? {
1970
+ x: previous.x,
1971
+ y: point.y
1972
+ } : {
1973
+ x: point.x,
1974
+ y: previous.y
1975
+ });
1976
+ }
1977
+ orthogonal.push(point);
1978
+ }
1979
+ compactedPoints = simplifyRoute$1(orthogonal);
1980
+ } else if (edgeRouting === "SPLINES") compactedSplineControls?.delete(edgeId);
1981
+ routes.pointsByEdgeId.set(edgeId, compactedPoints);
1856
1982
  }
1983
+ if (compactedSplineControls) routes.splineNubControlsByEdgeId = compactedSplineControls;
1857
1984
  }
1858
1985
  return placement;
1859
1986
  }
@@ -2046,8 +2173,16 @@ function implicitEdgeEndpoints(input, placement, orientation) {
2046
2173
  const forward = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT" || sourceFlow <= targetFlow;
2047
2174
  const feedback = input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
2048
2175
  const directionReversed = input.direction === "left" || input.direction === "up";
2049
- const sourceSide = feedback ? directionReversed ? "before" : "after" : forward ? "after" : "before";
2050
- const targetSide = feedback ? directionReversed ? "after" : "before" : forward ? "before" : "after";
2176
+ const endpointSide = (nodeId, portName) => {
2177
+ const node = nodeById.get(nodeId);
2178
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
2179
+ const constraints = node ? input.nodeSettings?.(node)?.portConstraints : void 0;
2180
+ if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
2181
+ const configured = input.portSettings?.(port, node)?.["port.side"];
2182
+ return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
2183
+ };
2184
+ const sourceSide = endpointSide(edge.sourceId, edge.sourcePort) ?? (feedback ? directionReversed ? "before" : "after" : forward ? "after" : "before");
2185
+ const targetSide = endpointSide(edge.targetId, edge.targetPort) ?? (feedback ? directionReversed ? "after" : "before" : forward ? "before" : "after");
2051
2186
  const sourceKey = `${edge.sourceId}:${sourceSide}`;
2052
2187
  const targetKey = `${edge.targetId}:${targetSide}`;
2053
2188
  const sourceGroup = groups.get(sourceKey) ?? [];
@@ -2169,8 +2304,14 @@ function implicitEdgeEndpoints(input, placement, orientation) {
2169
2304
  function routeEdges(style) {
2170
2305
  return (input, orientation, placement) => {
2171
2306
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
2307
+ const hasZeroFixedSideTarget = (edge) => {
2308
+ const target = nodeById.get(edge.targetId);
2309
+ const port = target?.ports?.find((candidate) => candidate.name === edge.targetPort);
2310
+ return target !== void 0 && port !== void 0 && (port.width ?? 8) === 0 && (port.height ?? 8) === 0 && input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE";
2311
+ };
2172
2312
  const pointsByEdgeId = /* @__PURE__ */ new Map();
2173
2313
  const splineNubControlsByEdgeId = /* @__PURE__ */ new Map();
2314
+ const outsideFeedbackEdgeIds = /* @__PURE__ */ new Set();
2174
2315
  const horizontal = input.direction === "left" || input.direction === "right";
2175
2316
  const reverse = input.direction === "up" || input.direction === "left";
2176
2317
  const mutableRects = placement.rectByNodeId;
@@ -2181,6 +2322,35 @@ function routeEdges(style) {
2181
2322
  loops.push(edge);
2182
2323
  selfLoopsByNodeId.set(edge.sourceId, loops);
2183
2324
  }
2325
+ const selfLoopEntriesByX = [...selfLoopsByNodeId].sort(([leftId], [rightId]) => (mutableRects.get(leftId)?.x ?? 0) - (mutableRects.get(rightId)?.x ?? 0));
2326
+ for (const [id, loops] of selfLoopEntriesByX) {
2327
+ const rect = mutableRects.get(id);
2328
+ const node = nodeById.get(id);
2329
+ if (!rect || !node) continue;
2330
+ const eastLoops = loops.filter((edge) => {
2331
+ const sourcePort = node.ports?.find((port) => port.name === edge.sourcePort);
2332
+ const targetPort = node.ports?.find((port) => port.name === edge.targetPort);
2333
+ return sourcePort !== void 0 && targetPort !== void 0 && input.portSettings?.(sourcePort, node)?.["port.side"] === "EAST" && input.portSettings?.(targetPort, node)?.["port.side"] === "EAST";
2334
+ });
2335
+ if (eastLoops.length === 0) continue;
2336
+ const selfLoopSpacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
2337
+ const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
2338
+ const exteriorWidth = selfLoopSpacing + eastLoops.reduce((sum, edge) => sum + (edge.width ?? 0) + labelSpacing, 1);
2339
+ const nodeRight = rect.x + rect.width;
2340
+ const rightCandidates = [...mutableRects.entries()].filter(([candidateId, candidateRect]) => candidateId !== id && candidateRect.x >= nodeRight - 1e-9);
2341
+ if (rightCandidates.length === 0) continue;
2342
+ const nextX = Math.min(...rightCandidates.map(([, candidateRect]) => candidateRect.x));
2343
+ const shift = nodeRight + exteriorWidth + input.spacing.node - nextX;
2344
+ if (shift <= 0) continue;
2345
+ for (const [candidateId, candidateRect] of mutableRects) {
2346
+ if (candidateId === id || candidateRect.x < nextX - 1e-9) continue;
2347
+ mutableRects.set(candidateId, {
2348
+ ...candidateRect,
2349
+ x: candidateRect.x + shift
2350
+ });
2351
+ }
2352
+ }
2353
+ const northReserveByLayer = /* @__PURE__ */ new Map();
2184
2354
  for (const [id, loops] of selfLoopsByNodeId) {
2185
2355
  const rect = mutableRects.get(id);
2186
2356
  if (!rect) continue;
@@ -2198,12 +2368,17 @@ function routeEdges(style) {
2198
2368
  else {
2199
2369
  const sideLoopCount = distribution === "NORTH_SOUTH" ? Math.ceil(loops.length / 2) : loops.length;
2200
2370
  const reserve = (ordering === "SEQUENCED" ? Math.min(1, sideLoopCount) : sideLoopCount) * spacing + splineOffset;
2201
- mutableRects.set(id, {
2202
- ...rect,
2203
- y: rect.y + reserve
2204
- });
2371
+ northReserveByLayer.set(rect.y, Math.max(northReserveByLayer.get(rect.y) ?? 0, reserve));
2205
2372
  }
2206
2373
  }
2374
+ const northReserves = [...northReserveByLayer].sort(([left], [right]) => left - right);
2375
+ for (const [candidateId, candidateRect] of mutableRects) {
2376
+ const reserve = northReserves.reduce((total, [layerY, layerReserve]) => candidateRect.y + 1e-9 >= layerY ? total + layerReserve : total, 0);
2377
+ if (reserve > 0) mutableRects.set(candidateId, {
2378
+ ...candidateRect,
2379
+ y: candidateRect.y + reserve
2380
+ });
2381
+ }
2207
2382
  const edgeLabelSideSelection = input.settings["edgeLabels.sideSelection"] ?? "SMART_DOWN";
2208
2383
  if (edgeLabelSideSelection === "ALWAYS_UP" || edgeLabelSideSelection === "SMART_UP" || edgeLabelSideSelection === "DIRECTION_UP") {
2209
2384
  let crossShift = 0;
@@ -2243,12 +2418,13 @@ function routeEdges(style) {
2243
2418
  }
2244
2419
  const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
2245
2420
  for (const edge of input.graph.edges) {
2246
- if ((edge.width ?? 0) <= 0) continue;
2421
+ const labelFlowSize = horizontal ? edge.width ?? 0 : edge.height ?? 0;
2422
+ if (labelFlowSize <= 0) continue;
2247
2423
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
2248
2424
  const targetLayer = flowLayerByNodeId.get(edge.targetId);
2249
2425
  if (sourceLayer === void 0 || targetLayer === void 0) continue;
2250
2426
  if (Math.abs(sourceLayer - targetLayer) !== 1) continue;
2251
- const extra = (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER" ? (edge.width ?? 0) + input.spacing.layer : (edge.width ?? 0) + Number(input.settings["spacing.edgeLabel"] ?? 2);
2427
+ const extra = (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER" ? labelFlowSize + input.spacing.layer : labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
2252
2428
  const gap = Math.min(sourceLayer, targetLayer);
2253
2429
  labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
2254
2430
  }
@@ -2493,13 +2669,15 @@ function routeEdges(style) {
2493
2669
  bounds.start = nextStart;
2494
2670
  bounds.end = nextStart + size;
2495
2671
  const slots = slotsByGap[layerNo] ?? 0;
2496
- const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap ? existingGapByLayer[layerNo] ?? input.spacing.layer : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
2672
+ const routesNearTarget = candidatesByGap[layerNo]?.some((candidate) => hasZeroFixedSideTarget(candidate.edge) && candidatesByGap[layerNo].filter(({ edge }) => edge.targetId === candidate.edge.targetId).length > candidatesByGap[layerNo].filter(({ edge }) => edge.sourceId === candidate.edge.sourceId).length);
2673
+ const preservedGap = (existingGapByLayer[layerNo] ?? input.spacing.layer) - (routesNearTarget ? edgeEdgeSpacing : 0);
2674
+ const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
2497
2675
  nextStart = bounds.end + gapSpacing;
2498
2676
  }
2499
2677
  implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
2500
2678
  for (const [gap, candidates] of candidatesByGap.entries()) for (const candidate of candidates) {
2501
2679
  if (candidate.straight) continue;
2502
- const firstTrack = (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing + (candidate.slot ?? 0) * edgeEdgeSpacing;
2680
+ const firstTrack = input.direction === "right" && hasZeroFixedSideTarget(candidate.edge) && candidates.filter(({ edge }) => edge.targetId === candidate.edge.targetId).length > candidates.filter(({ edge }) => edge.sourceId === candidate.edge.sourceId).length ? (flowLayers[gap + 1]?.start ?? 0) - edgeNodeSpacing - (candidate.slot ?? 0) * edgeEdgeSpacing : (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing + (candidate.slot ?? 0) * edgeEdgeSpacing;
2503
2681
  if (candidate.secondSlot !== void 0 && candidate.crossover !== void 0) orthogonalDetourByEdgeId.set(candidate.edge.id, {
2504
2682
  firstTrack,
2505
2683
  secondTrack: (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing + candidate.secondSlot * edgeEdgeSpacing,
@@ -2663,7 +2841,7 @@ function routeEdges(style) {
2663
2841
  }] : [];
2664
2842
  });
2665
2843
  const incoming = segments.map(() => 0);
2666
- for (const dependency of acyclicDependencies) incoming[dependency.target]++;
2844
+ for (const dependency of acyclicDependencies) incoming[dependency.target] = (incoming[dependency.target] ?? 0) + 1;
2667
2845
  const queue = incoming.flatMap((count, index) => count === 0 ? [index] : []);
2668
2846
  const rank = segments.map(() => 0);
2669
2847
  for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
@@ -2745,67 +2923,92 @@ function routeEdges(style) {
2745
2923
  const sourceRect = placement.rectByNodeId.get(edge.sourceId);
2746
2924
  const targetRect = placement.rectByNodeId.get(edge.targetId);
2747
2925
  if (!source || !target || !sourceRect || !targetRect) continue;
2748
- if (input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id)) {
2749
- const spacing = Number(input.settings["spacing.edgeNode"] ?? 10);
2926
+ const reversedEdge = orientation.reversedEdgeIds.has(edge.id);
2927
+ const feedbackSourcePort = source.ports?.find((port) => port.name === edge.sourcePort);
2928
+ const feedbackTargetPort = target.ports?.find((port) => port.name === edge.targetPort);
2929
+ const feedbackSourcePortSide = feedbackSourcePort ? input.portSettings?.(feedbackSourcePort, source)?.["port.side"] : void 0;
2930
+ const feedbackTargetPortSide = feedbackTargetPort ? input.portSettings?.(feedbackTargetPort, target)?.["port.side"] : void 0;
2931
+ const sourceAwaySide = horizontal ? sourceRect.x < targetRect.x ? "WEST" : "EAST" : sourceRect.y < targetRect.y ? "NORTH" : "SOUTH";
2932
+ const targetAwaySide = horizontal ? sourceRect.x < targetRect.x ? "EAST" : "WEST" : sourceRect.y < targetRect.y ? "SOUTH" : "NORTH";
2933
+ const sameSideSelfLoop = source.id === target.id && feedbackSourcePortSide !== void 0 && feedbackSourcePortSide === feedbackTargetPortSide;
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);
2939
+ if (input.settings.feedbackEdges === true && reversedEdge || fixedSideFeedback) {
2940
+ if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
2941
+ const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
2942
+ const flowSpacing = fixedSideFeedback ? Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10) + (source.id === target.id ? Number(input.settings["spacing.nodeSelfLoop"] ?? 10) : 0) : crossSpacing;
2943
+ const sourceFallback$1 = implicitEndpoints.get(edge.id)?.source ?? {
2944
+ x: sourceRect.x + sourceRect.width / 2,
2945
+ y: sourceRect.y + sourceRect.height / 2
2946
+ };
2947
+ const targetFallback$1 = implicitEndpoints.get(edge.id)?.target ?? {
2948
+ x: targetRect.x + targetRect.width / 2,
2949
+ y: targetRect.y + targetRect.height / 2
2950
+ };
2951
+ const start$1 = fixedSideFeedback ? getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback$1, input.direction, input) : sourceFallback$1;
2952
+ const end$1 = fixedSideFeedback ? getPortPoint(target, edge.targetPort, targetRect, targetFallback$1, input.direction, input) : targetFallback$1;
2750
2953
  if (horizontal) {
2751
- const sign = reverse ? -1 : 1;
2752
- const start$1 = {
2753
- x: sign > 0 ? sourceRect.x + sourceRect.width : sourceRect.x,
2754
- y: sourceRect.y + sourceRect.height / 2
2755
- };
2756
- const end$1 = {
2757
- x: sign > 0 ? targetRect.x : targetRect.x + targetRect.width,
2758
- y: targetRect.y + targetRect.height / 2
2759
- };
2760
- const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.y + rect.height)) + spacing;
2954
+ const sign = fixedSideFeedback ? Math.sign(sourceRect.x - targetRect.x) || 1 : reverse ? -1 : 1;
2955
+ const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.y + rect.height)) + crossSpacing + (fixedSideFeedback ? .5 : 0);
2956
+ const preserveCenters = fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
2761
2957
  pointsByEdgeId.set(edge.id, [
2762
2958
  start$1,
2763
2959
  {
2764
- x: start$1.x + sign * spacing,
2960
+ x: start$1.x + sign * flowSpacing,
2765
2961
  y: start$1.y
2766
2962
  },
2767
2963
  {
2768
- x: start$1.x + sign * spacing,
2964
+ x: start$1.x + sign * flowSpacing,
2769
2965
  y: outerCross
2770
2966
  },
2967
+ ...preserveCenters ? [{
2968
+ x: sourceRect.x + sourceRect.width / 2,
2969
+ y: outerCross
2970
+ }, {
2971
+ x: targetRect.x + targetRect.width / 2,
2972
+ y: outerCross
2973
+ }] : [],
2771
2974
  {
2772
- x: end$1.x - sign * spacing,
2975
+ x: end$1.x - sign * flowSpacing,
2773
2976
  y: outerCross
2774
2977
  },
2775
2978
  {
2776
- x: end$1.x - sign * spacing,
2979
+ x: end$1.x - sign * flowSpacing,
2777
2980
  y: end$1.y
2778
2981
  },
2779
2982
  end$1
2780
2983
  ]);
2781
2984
  } else {
2782
- const sign = reverse ? -1 : 1;
2783
- const start$1 = {
2784
- x: sourceRect.x + sourceRect.width / 2,
2785
- y: sign > 0 ? sourceRect.y + sourceRect.height : sourceRect.y
2786
- };
2787
- const end$1 = {
2788
- x: targetRect.x + targetRect.width / 2,
2789
- y: sign > 0 ? targetRect.y : targetRect.y + targetRect.height
2790
- };
2791
- const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.x + rect.width)) + spacing;
2985
+ const sign = fixedSideFeedback ? Math.sign(sourceRect.y - targetRect.y) || 1 : reverse ? -1 : 1;
2986
+ const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.x + rect.width)) + crossSpacing + (fixedSideFeedback ? .5 : 0);
2987
+ const preserveCenters = fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
2792
2988
  pointsByEdgeId.set(edge.id, [
2793
2989
  start$1,
2794
2990
  {
2795
2991
  x: start$1.x,
2796
- y: start$1.y + sign * spacing
2992
+ y: start$1.y + sign * flowSpacing
2797
2993
  },
2798
2994
  {
2799
2995
  x: outerCross,
2800
- y: start$1.y + sign * spacing
2996
+ y: start$1.y + sign * flowSpacing
2801
2997
  },
2998
+ ...preserveCenters ? [{
2999
+ x: outerCross,
3000
+ y: sourceRect.y + sourceRect.height / 2
3001
+ }, {
3002
+ x: outerCross,
3003
+ y: targetRect.y + targetRect.height / 2
3004
+ }] : [],
2802
3005
  {
2803
3006
  x: outerCross,
2804
- y: end$1.y - sign * spacing
3007
+ y: end$1.y - sign * flowSpacing
2805
3008
  },
2806
3009
  {
2807
3010
  x: end$1.x,
2808
- y: end$1.y - sign * spacing
3011
+ y: end$1.y - sign * flowSpacing
2809
3012
  },
2810
3013
  end$1
2811
3014
  ]);
@@ -2870,6 +3073,75 @@ function routeEdges(style) {
2870
3073
  const distribution = nodeSettings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
2871
3074
  const ordering = nodeSettings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
2872
3075
  if (style === "ORTHOGONAL") {
3076
+ if (sameSideSelfLoop) {
3077
+ const endpoints$2 = implicitEndpoints.get(edge.id);
3078
+ const start$2 = getPortPoint(source, edge.sourcePort, sourceRect, endpoints$2?.source ?? {
3079
+ x: sourceRect.x + sourceRect.width / 2,
3080
+ y: sourceRect.y + sourceRect.height / 2
3081
+ }, input.direction, input);
3082
+ const end$2 = getPortPoint(target, edge.targetPort, targetRect, endpoints$2?.target ?? {
3083
+ x: targetRect.x + targetRect.width / 2,
3084
+ y: targetRect.y + targetRect.height / 2
3085
+ }, input.direction, input);
3086
+ const side = feedbackSourcePortSide;
3087
+ const sameSideLoops = loops.filter((candidate) => {
3088
+ const candidateSourcePort = source.ports?.find((port) => port.name === candidate.sourcePort);
3089
+ const candidateTargetPort = source.ports?.find((port) => port.name === candidate.targetPort);
3090
+ const candidateSourceSide = candidateSourcePort ? input.portSettings?.(candidateSourcePort, source)?.["port.side"] : void 0;
3091
+ const candidateTargetSide = candidateTargetPort ? input.portSettings?.(candidateTargetPort, source)?.["port.side"] : void 0;
3092
+ return candidateSourceSide === side && candidateTargetSide === side;
3093
+ });
3094
+ const sameSideLoopIndex = sameSideLoops.findIndex((candidate) => candidate.id === edge.id);
3095
+ const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
3096
+ const trackDistance = spacing + sameSideLoops.slice(0, Math.max(0, sameSideLoopIndex)).reduce((extent$1, candidate) => extent$1 + (side === "EAST" || side === "WEST" ? candidate.width ?? 0 : candidate.height ?? 0) + labelSpacing, 0);
3097
+ if (side === "EAST" || side === "WEST") {
3098
+ const nodeRects = [...placement.rectByNodeId.values()];
3099
+ const minimumNodeX = Math.min(...nodeRects.map((rect) => rect.x));
3100
+ const maximumNodeX = Math.max(...nodeRects.map((rect) => rect.x + rect.width));
3101
+ const maximumNodeY = Math.max(...nodeRects.map((rect) => rect.y + rect.height));
3102
+ const precedingLabelHeight = sameSideLoops.slice(0, Math.max(0, sameSideLoopIndex)).reduce((extent$1, candidate) => extent$1 + (candidate.height ?? 0) + labelSpacing, 0);
3103
+ const nearTrack = side === "EAST" ? sourceRect.x + sourceRect.width + spacing : sourceRect.x - spacing;
3104
+ const farTrack = side === "EAST" ? maximumNodeX + trackDistance + (edge.width ?? 0) : minimumNodeX - trackDistance - (edge.width ?? 0);
3105
+ const exteriorY = maximumNodeY + spacing + precedingLabelHeight + (edge.height ?? 0) / 2 + .5;
3106
+ pointsByEdgeId.set(edge.id, [
3107
+ start$2,
3108
+ {
3109
+ x: nearTrack,
3110
+ y: start$2.y
3111
+ },
3112
+ {
3113
+ x: nearTrack,
3114
+ y: exteriorY
3115
+ },
3116
+ {
3117
+ x: farTrack,
3118
+ y: exteriorY
3119
+ },
3120
+ {
3121
+ x: farTrack,
3122
+ y: end$2.y
3123
+ },
3124
+ end$2
3125
+ ]);
3126
+ outsideFeedbackEdgeIds.add(edge.id);
3127
+ continue;
3128
+ }
3129
+ const track$1 = side === "SOUTH" ? sourceRect.y + sourceRect.height + trackDistance : sourceRect.y - trackDistance;
3130
+ pointsByEdgeId.set(edge.id, [
3131
+ start$2,
3132
+ {
3133
+ x: start$2.x,
3134
+ y: track$1
3135
+ },
3136
+ {
3137
+ x: end$2.x,
3138
+ y: track$1
3139
+ },
3140
+ end$2
3141
+ ]);
3142
+ outsideFeedbackEdgeIds.add(edge.id);
3143
+ continue;
3144
+ }
2873
3145
  const routeHorizontalSide = (side, indexOnSide, countOnSide) => {
2874
3146
  const denominator = countOnSide * 2 + 1;
2875
3147
  const sequenced = ordering === "SEQUENCED";
@@ -3035,8 +3307,16 @@ function routeEdges(style) {
3035
3307
  x: targetRect.x + targetRect.width / 2,
3036
3308
  y: targetRect.y + (reverse ? targetRect.height : 0)
3037
3309
  });
3038
- const start = getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
3039
- const end = getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
3310
+ const sourcePort = source.ports?.find((port) => port.name === edge.sourcePort);
3311
+ const targetPort = target.ports?.find((port) => port.name === edge.targetPort);
3312
+ const sourcePortSide = sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
3313
+ const targetPortSide = targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : void 0;
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;
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;
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;
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);
3040
3320
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
3041
3321
  const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
3042
3322
  const earlierLayer = Math.min(sourceLayer, targetLayer);
@@ -3181,14 +3461,15 @@ function routeEdges(style) {
3181
3461
  }
3182
3462
  return {
3183
3463
  pointsByEdgeId,
3184
- splineNubControlsByEdgeId
3464
+ splineNubControlsByEdgeId,
3465
+ outsideFeedbackEdgeIds
3185
3466
  };
3186
3467
  };
3187
3468
  }
3188
3469
  const routeEdgesOrthogonally = routeEdges("ORTHOGONAL");
3189
3470
  const routeEdgesWithPolylines = routeEdges("POLYLINE");
3190
3471
  const routeEdgesWithSplines = routeEdges("SPLINES");
3191
- function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3472
+ function placePorts(ports, rect, direction, portSettings, nodeSettings, portDirection) {
3192
3473
  if (!ports) return void 0;
3193
3474
  const horizontal = direction === "left" || direction === "right";
3194
3475
  const reverse = direction === "up" || direction === "left";
@@ -3202,7 +3483,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3202
3483
  sideByPort.set(port, configured);
3203
3484
  continue;
3204
3485
  }
3205
- const outgoing = port.direction === "out";
3486
+ const outgoing = (portDirection?.(port) ?? port.direction) === "out";
3206
3487
  const normalFarSide = reverse ? !outgoing : outgoing;
3207
3488
  const farSide = sideFixed && configured === "UNDEFINED" ? !normalFarSide : normalFarSide;
3208
3489
  sideByPort.set(port, horizontal ? farSide ? "EAST" : "WEST" : farSide ? "SOUTH" : "NORTH");
@@ -3312,7 +3593,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
3312
3593
  });
3313
3594
  }
3314
3595
  /** Account for port extents in ELK's graph-padding normalization. */
3315
- function normalizePlacementForPortExtents(input, placement, order) {
3596
+ function normalizePlacementForPortExtents(input, placement, order, orientation) {
3316
3597
  const horizontal = input.direction === "left" || input.direction === "right";
3317
3598
  const physicalLayers = order.layers.map((layer) => layer.flatMap((id) => placement.rectByNodeId.has(id) ? [id] : [])).filter((layer) => layer.length > 0).sort((left, right) => {
3318
3599
  const leftRect = placement.rectByNodeId.get(left[0]);
@@ -3347,7 +3628,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
3347
3628
  const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
3348
3629
  ...input.settings,
3349
3630
  ...input.nodeSettings?.(node)
3350
- });
3631
+ }, (port) => getOrientedPortDirection(input, orientation, node, port));
3351
3632
  for (const port of ports ?? []) {
3352
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));
3353
3634
  if (trailing) trailingExtent = Math.max(trailingExtent, extent$1);
@@ -3366,7 +3647,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
3366
3647
  const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
3367
3648
  ...input.settings,
3368
3649
  ...input.nodeSettings?.(node)
3369
- });
3650
+ }, (port) => getOrientedPortDirection(input, orientation, node, port));
3370
3651
  for (const port of ports ?? []) {
3371
3652
  minimumX = Math.min(minimumX, rect.x + (port.x ?? 0));
3372
3653
  minimumY = Math.min(minimumY, rect.y + (port.y ?? 0));
@@ -3616,7 +3897,7 @@ function normalizeAndBalance(nodes, previousLayerCounts) {
3616
3897
  }
3617
3898
  return filling;
3618
3899
  }
3619
- function runNetworkSimplex(nodes, edges, iterationLimit, previousLayerCounts, balance = true) {
3900
+ function runNetworkSimplex(nodes, _edges, iterationLimit, previousLayerCounts, balance = true) {
3620
3901
  const orderedEdges = nodes.flatMap((node) => node.outgoing);
3621
3902
  orderedEdges.forEach((edge, index) => edge.order = index);
3622
3903
  assignInitialLayers(nodes);
@@ -3890,10 +4171,26 @@ function splitLongEdges(input, orientation, assignment) {
3890
4171
  const originalEdgeBySegmentId = /* @__PURE__ */ new Map();
3891
4172
  const usedNodeIds = new Set(nodes.map((node) => node.id));
3892
4173
  const originalNodeIds = new Set(usedNodeIds);
4174
+ const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4175
+ const forwardSourceSide = input.direction === "right" ? "EAST" : input.direction === "left" ? "WEST" : input.direction === "down" ? "SOUTH" : "NORTH";
4176
+ const forwardTargetSide = input.direction === "right" ? "WEST" : input.direction === "left" ? "EAST" : input.direction === "down" ? "NORTH" : "SOUTH";
3893
4177
  for (const edge of input.graph.edges) {
3894
4178
  const sourceLayer = layerByNodeId.get(edge.sourceId) ?? 0;
3895
4179
  const targetLayer = layerByNodeId.get(edge.targetId) ?? 0;
3896
- if (Math.abs(targetLayer - sourceLayer) <= 1 || edge.sourceId === edge.targetId || input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id)) {
4180
+ const span = Math.abs(targetLayer - sourceLayer);
4181
+ const source = nodeById.get(edge.sourceId);
4182
+ const target = nodeById.get(edge.targetId);
4183
+ const sourcePort = source?.ports?.find((port) => port.name === edge.sourcePort);
4184
+ const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
4185
+ const sourceSide = source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
4186
+ const targetSide = target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : void 0;
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);
4193
+ if (span <= 1 || edge.sourceId === edge.targetId || input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id) || fixedSideFeedback) {
3897
4194
  edges.push(edge);
3898
4195
  originalEdgeBySegmentId.set(edge.id, edge);
3899
4196
  if (orientation.reversedEdgeIds.has(edge.id)) reversedEdgeIds.add(edge.id);
@@ -4055,7 +4352,9 @@ function joinLongEdgeRoutes(routes, segmentIdsByEdgeId, preserveInternalDuplicat
4055
4352
  return result;
4056
4353
  };
4057
4354
  const pointsByEdgeId = /* @__PURE__ */ new Map();
4355
+ const outsideFeedbackEdgeIds = /* @__PURE__ */ new Set();
4058
4356
  for (const [edgeId, segmentIds] of segmentIdsByEdgeId) {
4357
+ if (segmentIds.some((segmentId) => routes.outsideFeedbackEdgeIds?.has(segmentId))) outsideFeedbackEdgeIds.add(edgeId);
4059
4358
  if (convertLongSplines && segmentIds.length > 1) {
4060
4359
  const segments = segmentIds.map((segmentId) => routes.pointsByEdgeId.get(segmentId) ?? []).filter((points$1) => points$1.length >= 2);
4061
4360
  if (segments.length > 1) {
@@ -4128,7 +4427,10 @@ function joinLongEdgeRoutes(routes, segmentIdsByEdgeId, preserveInternalDuplicat
4128
4427
  } else for (const segmentId of segmentIds) appendPoints(points, routes.pointsByEdgeId.get(segmentId) ?? [], preserveInternalDuplicates || segmentIds.length === 1);
4129
4428
  pointsByEdgeId.set(edgeId, preserveInternalDuplicates || segmentIds.length === 1 ? points : simplify(points));
4130
4429
  }
4131
- return { pointsByEdgeId };
4430
+ return {
4431
+ pointsByEdgeId,
4432
+ outsideFeedbackEdgeIds
4433
+ };
4132
4434
  }
4133
4435
 
4134
4436
  //#endregion
@@ -8181,6 +8483,24 @@ function runLayeredPipeline(graph, options, context) {
8181
8483
  left: options.padding?.left ?? 12
8182
8484
  };
8183
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
+ }
8184
8504
  const input = {
8185
8505
  graph,
8186
8506
  sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
@@ -8199,6 +8519,7 @@ function runLayeredPipeline(graph, options, context) {
8199
8519
  ...options.edgeSettings === void 0 ? {} : { edgeSettings: options.edgeSettings },
8200
8520
  portSettings: (port, node) => ({
8201
8521
  ...options.portSettings?.(port, node),
8522
+ ...parallelPortIndexByKey.has(`${node.id}\0${port.name}`) ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) } : {},
8202
8523
  ...switchedSideByPort.has(`${node.id}\0${port.name}`) ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) } : {}
8203
8524
  })
8204
8525
  };
@@ -8396,7 +8717,7 @@ function runLayeredPipeline(graph, options, context) {
8396
8717
  });
8397
8718
  }
8398
8719
  }
8399
- measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order));
8720
+ measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation));
8400
8721
  {
8401
8722
  const horizontal = direction === "left" || direction === "right";
8402
8723
  const crossCenter = (nodeId) => {
@@ -8421,8 +8742,186 @@ function runLayeredPipeline(graph, options, context) {
8421
8742
  }
8422
8743
  const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
8423
8744
  const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
8424
- const expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8745
+ let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
8425
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
+ }
8426
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)));
8427
8926
  if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
8428
8927
  const horizontal = direction === "right" || direction === "left";
@@ -8554,12 +9053,115 @@ function runLayeredPipeline(graph, options, context) {
8554
9053
  };
8555
9054
  routes.pointsByEdgeId.set(edge.id, points);
8556
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
+ }
9134
+ const routedPortAnchors = /* @__PURE__ */ new Map();
9135
+ for (const edge of graph.edges) {
9136
+ const points = routes.pointsByEdgeId.get(edge.id);
9137
+ const first = points?.[0];
9138
+ const last = points?.at(-1);
9139
+ if (edge.sourcePort !== void 0 && first && graph.edges.filter((candidate) => candidate.sourceId === edge.sourceId && candidate.sourcePort === edge.sourcePort).length === 1) routedPortAnchors.set(`${edge.sourceId}\0${edge.sourcePort}`, first);
9140
+ if (edge.targetPort !== void 0 && last && graph.edges.filter((candidate) => candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort).length === 1) routedPortAnchors.set(`${edge.targetId}\0${edge.targetPort}`, last);
9141
+ }
8557
9142
  const nodes = graph.nodes.map((node) => {
8558
9143
  const rect = placement.rectByNodeId.get(node.id);
8559
9144
  if (!rect) throw new Error(`Node placement missing for ${node.id}`);
8560
- const ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
9145
+ let ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
8561
9146
  ...options.settings,
8562
9147
  ...options.nodeSettings?.(node)
9148
+ }, (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port));
9149
+ if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") ports = ports?.map((port) => {
9150
+ if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
9151
+ const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
9152
+ if (!anchor || port.x === void 0 || port.y === void 0) return port;
9153
+ const configuredAnchor = (input.portSettings?.(port, node))?.["port.anchor"];
9154
+ const width = port.width ?? 0;
9155
+ const height = port.height ?? 0;
9156
+ const defaultAnchorX = port.x >= rect.width ? width : port.x + width <= 0 ? 0 : width / 2;
9157
+ const defaultAnchorY = port.y >= rect.height ? height : port.y + height <= 0 ? 0 : height / 2;
9158
+ const x = anchor.x - rect.x - (configuredAnchor?.x ?? defaultAnchorX);
9159
+ const y = anchor.y - rect.y - (configuredAnchor?.y ?? defaultAnchorY);
9160
+ return {
9161
+ ...port,
9162
+ x: x === 0 && Object.is(port.x, -0) ? port.x : x,
9163
+ y: y === 0 && Object.is(port.y, -0) ? port.y : y
9164
+ };
8563
9165
  });
8564
9166
  return {
8565
9167
  ...node,
@@ -8567,6 +9169,14 @@ function runLayeredPipeline(graph, options, context) {
8567
9169
  ...ports === void 0 ? {} : { ports }
8568
9170
  };
8569
9171
  });
9172
+ const feedbackNodeRects = graph.nodes.flatMap((node) => {
9173
+ const rect = placement.rectByNodeId.get(node.id);
9174
+ return rect ? [rect] : [];
9175
+ });
9176
+ const minimumFeedbackNodeX = Math.min(...feedbackNodeRects.map((rect) => rect.x));
9177
+ const maximumFeedbackNodeX = Math.max(...feedbackNodeRects.map((rect) => rect.x + rect.width));
9178
+ const minimumFeedbackNodeY = Math.min(...feedbackNodeRects.map((rect) => rect.y));
9179
+ const maximumFeedbackNodeY = Math.max(...feedbackNodeRects.map((rect) => rect.y + rect.height));
8570
9180
  const edges = graph.edges.map((edge) => {
8571
9181
  const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
8572
9182
  const midpoint = getPolylineMidpoint(points);
@@ -8582,11 +9192,56 @@ function runLayeredPipeline(graph, options, context) {
8582
9192
  const labelDummyRect = placement.rectByNodeId.get(expanded.labelDummyIdByEdgeId.get(edge.id) ?? "");
8583
9193
  const edgeLabelSideSelection = options.settings?.["edgeLabels.sideSelection"] ?? "SMART_DOWN";
8584
9194
  const placeLabelUp = edgeLabelSideSelection === "ALWAYS_UP" || edgeLabelSideSelection === "SMART_UP" || edgeLabelSideSelection === "DIRECTION_UP";
8585
- const routeX = labelPlacement === "TAIL" ? firstPoint.x + labelSpacing : labelPlacement === "HEAD" ? lastPoint.x - width - labelSpacing : midpoint.x - width / 2;
8586
- const routeY = 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);
8587
9195
  const horizontal = direction === "left" || direction === "right";
8588
- const x = labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
8589
- const y = labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
9196
+ const verticalTrack = horizontal ? points.find((point, index) => {
9197
+ const next = points[index + 1];
9198
+ return next !== void 0 && point.x === next.x && point.y !== next.y;
9199
+ }) : void 0;
9200
+ const secondPoint = points[1];
9201
+ const beforeLastPoint = points.at(-2);
9202
+ const flowDelta = horizontal ? lastPoint.x - firstPoint.x : lastPoint.y - firstPoint.y;
9203
+ const firstLeadDelta = secondPoint ? horizontal ? secondPoint.x - firstPoint.x : secondPoint.y - firstPoint.y : 0;
9204
+ const lastLeadDelta = beforeLastPoint ? horizontal ? lastPoint.x - beforeLastPoint.x : lastPoint.y - beforeLastPoint.y : 0;
9205
+ const outsideFeedback = routes.outsideFeedbackEdgeIds?.has(edge.id) === true || secondPoint !== void 0 && beforeLastPoint !== void 0 && flowDelta !== 0 && firstLeadDelta * flowDelta < 0 && lastLeadDelta * flowDelta < 0;
9206
+ const horizontalFeedbackCandidate = outsideFeedback ? points.flatMap((point, index) => {
9207
+ const next = points[index + 1];
9208
+ return next !== void 0 && point.y === next.y && (point.y < minimumFeedbackNodeY || point.y > maximumFeedbackNodeY) ? [{
9209
+ start: point,
9210
+ end: next,
9211
+ length: Math.abs(next.x - point.x)
9212
+ }] : [];
9213
+ }).sort((left, right) => right.length - left.length)[0] : void 0;
9214
+ const verticalFeedbackCandidate = outsideFeedback ? points.flatMap((point, index) => {
9215
+ const next = points[index + 1];
9216
+ return next !== void 0 && point.x === next.x && (point.x < minimumFeedbackNodeX || point.x > maximumFeedbackNodeX) ? [{
9217
+ start: point,
9218
+ end: next,
9219
+ length: Math.abs(next.y - point.y)
9220
+ }] : [];
9221
+ }).sort((left, right) => right.length - left.length)[0] : void 0;
9222
+ const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
9223
+ const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
9224
+ const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
9225
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
9226
+ const sourcePortSide = sourceNode && sourcePort ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"] : void 0;
9227
+ const targetPortSide = sourceNode && targetPort ? options.portSettings?.(targetPort, sourceNode)?.["port.side"] : void 0;
9228
+ const horizontalFeedbackTrack = edge.sourceId === edge.targetId && (sourcePortSide === "EAST" || sourcePortSide === "WEST") && targetPortSide === sourcePortSide || (horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1) ? horizontalFeedbackCandidate : void 0;
9229
+ const verticalFeedbackTrack = horizontalFeedbackTrack ? void 0 : verticalFeedbackCandidate;
9230
+ const trackNearTarget = verticalTrack !== void 0 && Math.abs(lastPoint.x - verticalTrack.x) < Math.abs(verticalTrack.x - firstPoint.x);
9231
+ const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
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;
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);
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;
8590
9245
  return {
8591
9246
  ...edge,
8592
9247
  x,