@statelyai/layout 0.0.1 → 0.0.2

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 +10 -7
  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-QwJn2gb2.mjs} +399 -53
  9. package/dist/{spore-fTgSoRLP.mjs → spore-D15xIQKj.mjs} +1 -1
  10. package/package.json +16 -6
  11. package/src/box.ts +135 -0
  12. package/src/elkjs/index.ts +1825 -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 +2138 -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 +405 -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 +5343 -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
@@ -1721,6 +1721,14 @@ function applyPostCompaction(input, placement, routes) {
1721
1721
  input.settings["compaction.postCompaction.constraints"];
1722
1722
  if (strategy === "NONE") return placement;
1723
1723
  const rects = placement.rectByNodeId;
1724
+ const originalEndpointsByEdgeId = new Map([...routes?.pointsByEdgeId ?? []].flatMap(([edgeId, points]) => {
1725
+ const start = points[0];
1726
+ const end = points.at(-1);
1727
+ return start && end ? [[edgeId, {
1728
+ start: { ...start },
1729
+ end: { ...end }
1730
+ }]] : [];
1731
+ }));
1724
1732
  const compactables = [...rects].map(([id, rect]) => {
1725
1733
  const longEdgeDummy = id.startsWith("__layout_dummy:") && rect.width === 0 && rect.height === 0;
1726
1734
  const incidentEdge = longEdgeDummy ? input.graph.edges.find((edge) => edge.sourceId === id || edge.targetId === id) : void 0;
@@ -1736,6 +1744,7 @@ function applyPostCompaction(input, placement, routes) {
1736
1744
  };
1737
1745
  });
1738
1746
  if (routes) for (const [edgeId, readonlyPoints] of routes.pointsByEdgeId) {
1747
+ if (routes.outsideFeedbackEdgeIds?.has(edgeId)) continue;
1739
1748
  const points = readonlyPoints;
1740
1749
  for (let index = 0; index + 1 < points.length; index++) {
1741
1750
  const first = points[index];
@@ -1756,7 +1765,10 @@ function applyPostCompaction(input, placement, routes) {
1756
1765
  }
1757
1766
  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
1767
  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);
1768
+ const centerLabeledEdgeIds = new Set(input.graph.edges.filter((edge) => (edge.width ?? 0) > 0 && (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER").map((edge) => edge.id));
1769
+ const labelFlowLocked = new Set(compactables.filter((item) => item.kind === "segment" && item.edgeId !== void 0 && centerLabeledEdgeIds.has(item.edgeId)).map((item) => item.id));
1759
1770
  const compact$1 = (direction, locked = /* @__PURE__ */ new Set()) => {
1771
+ locked = new Set([...labelFlowLocked, ...locked]);
1760
1772
  const nodes = compactables.map((item) => ({
1761
1773
  ...item,
1762
1774
  x: direction === "RIGHT" ? -item.x - item.width : item.x,
@@ -1823,6 +1835,7 @@ function applyPostCompaction(input, placement, routes) {
1823
1835
  for (const node of input.graph.nodes) {
1824
1836
  const targets = outgoing.get(node.id) ?? [];
1825
1837
  if (targets.length <= (incomingDegree.get(node.id) ?? 0) || targets.length === 0) continue;
1838
+ if (input.graph.edges.some((edge) => edge.sourceId === node.id && centerLabeledEdgeIds.has(edge.id))) continue;
1826
1839
  const item = itemByNodeId.get(node.id);
1827
1840
  if (!item) continue;
1828
1841
  const upper = Math.min(...targets.map((targetId) => (itemByNodeId.get(targetId)?.x ?? item.x) - item.width - input.spacing.node));
@@ -1830,6 +1843,49 @@ function applyPostCompaction(input, placement, routes) {
1830
1843
  }
1831
1844
  }
1832
1845
  }
1846
+ if (strategy === "EDGE_LENGTH" && input.settings["nodePlacement.favorStraightEdges"] === true && (input.direction === "down" || input.direction === "up")) {
1847
+ const itemByNodeId = new Map(compactables.flatMap((item) => item.kind === "node" && item.nodeId ? [[item.nodeId, item]] : []));
1848
+ const incomingDegree = new Map(input.graph.nodes.map((node) => [node.id, 0]));
1849
+ for (const edge of input.graph.edges) if (edge.sourceId !== edge.targetId) incomingDegree.set(edge.targetId, (incomingDegree.get(edge.targetId) ?? 0) + 1);
1850
+ for (const edge of input.graph.edges) {
1851
+ if (edge.sourceId === edge.targetId || (incomingDegree.get(edge.targetId) ?? 0) !== 1) continue;
1852
+ const endpoints$2 = originalEndpointsByEdgeId.get(edge.id);
1853
+ const source = itemByNodeId.get(edge.sourceId);
1854
+ const target = itemByNodeId.get(edge.targetId);
1855
+ if (!endpoints$2 || !source || !target) continue;
1856
+ if (!(input.direction === "down" ? endpoints$2.start.y < endpoints$2.end.y : endpoints$2.start.y > endpoints$2.end.y)) continue;
1857
+ const desiredTargetX = source.x + (endpoints$2.start.x - source.originalX) - (endpoints$2.end.x - target.originalX);
1858
+ if (compactables.some((candidate) => {
1859
+ if (candidate.kind !== "node" || candidate === target) return false;
1860
+ if (!(candidate.y < target.y + target.height && candidate.y + candidate.height > target.y)) return false;
1861
+ const spacing = input.spacing.node;
1862
+ return desiredTargetX < candidate.x + candidate.width + spacing && desiredTargetX + target.width + spacing > candidate.x;
1863
+ })) continue;
1864
+ target.x = desiredTargetX;
1865
+ const alignedX = source.x + (endpoints$2.start.x - source.originalX);
1866
+ for (const segment of compactables) if (segment.kind === "segment" && segment.edgeId === edge.id) segment.x = alignedX;
1867
+ }
1868
+ }
1869
+ if (input.direction === "right" && centerLabeledEdgeIds.size > 0) {
1870
+ const itemByNodeId = new Map(compactables.flatMap((item) => item.kind === "node" && item.nodeId ? [[item.nodeId, item]] : []));
1871
+ const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
1872
+ for (const edge of input.graph.edges) {
1873
+ if (!centerLabeledEdgeIds.has(edge.id)) continue;
1874
+ const source = itemByNodeId.get(edge.sourceId);
1875
+ const target = itemByNodeId.get(edge.targetId);
1876
+ const originalEndpoints = originalEndpointsByEdgeId.get(edge.id);
1877
+ if (!source || !target || !originalEndpoints || originalEndpoints.start.x >= originalEndpoints.end.x) continue;
1878
+ const tracks = compactables.filter((item) => item.kind === "segment" && item.edgeId === edge.id);
1879
+ const track = tracks.length > 0 ? Math.max(...tracks.map((item) => item.x)) : void 0;
1880
+ const targetNode = input.graph.nodes.find((node) => node.id === edge.targetId);
1881
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
1882
+ 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);
1883
+ else {
1884
+ const routeStart = track !== void 0 ? track + edgeNodeSpacing : source.x + source.width + input.spacing.layer;
1885
+ target.x = Math.max(target.x, routeStart + (edge.width ?? 0) + input.spacing.layer);
1886
+ }
1887
+ }
1888
+ }
1833
1889
  const offset = input.padding.left - Math.min(...compactables.map((item) => item.x));
1834
1890
  for (const item of compactables) item.x += offset;
1835
1891
  const nodeDeltaById = /* @__PURE__ */ new Map();
@@ -1846,14 +1902,71 @@ function applyPostCompaction(input, placement, routes) {
1846
1902
  item.points[1].x += delta;
1847
1903
  }
1848
1904
  if (routes) {
1905
+ const edgeRouting = input.settings.edgeRouting ?? "ORTHOGONAL";
1906
+ const compactedSplineControls = routes.splineNubControlsByEdgeId ? new Map(routes.splineNubControlsByEdgeId) : void 0;
1849
1907
  const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
1850
1908
  for (const [edgeId, readonlyPoints] of routes.pointsByEdgeId) {
1851
- const points = readonlyPoints;
1909
+ const points = [...readonlyPoints];
1852
1910
  const edge = edgeById.get(edgeId);
1853
1911
  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;
1912
+ const originalEndpoints = originalEndpointsByEdgeId.get(edgeId);
1913
+ if (originalEndpoints) {
1914
+ points[0] = {
1915
+ ...originalEndpoints.start,
1916
+ x: originalEndpoints.start.x + (nodeDeltaById.get(edge.sourceId) ?? 0)
1917
+ };
1918
+ points[points.length - 1] = {
1919
+ ...originalEndpoints.end,
1920
+ x: originalEndpoints.end.x + (nodeDeltaById.get(edge.targetId) ?? 0)
1921
+ };
1922
+ if (routes.outsideFeedbackEdgeIds?.has(edgeId)) {
1923
+ const horizontal = input.direction === "left" || input.direction === "right";
1924
+ const startDelta = points[0].x - originalEndpoints.start.x;
1925
+ const endDelta = points.at(-1).x - originalEndpoints.end.x;
1926
+ if (horizontal && points.length >= 6) {
1927
+ for (const index of [1, 2]) points[index] = {
1928
+ ...points[index],
1929
+ x: points[index].x + startDelta
1930
+ };
1931
+ for (const index of [points.length - 3, points.length - 2]) points[index] = {
1932
+ ...points[index],
1933
+ x: points[index].x + endDelta
1934
+ };
1935
+ } else if (!horizontal && points.length >= 4) {
1936
+ points[1] = {
1937
+ ...points[1],
1938
+ x: points[1].x + startDelta
1939
+ };
1940
+ points[points.length - 2] = {
1941
+ ...points[points.length - 2],
1942
+ x: points[points.length - 2].x + endDelta
1943
+ };
1944
+ }
1945
+ }
1946
+ }
1947
+ let compactedPoints = points;
1948
+ if (edgeRouting === "ORTHOGONAL") {
1949
+ const horizontal = input.direction === "left" || input.direction === "right";
1950
+ const orthogonal = [];
1951
+ for (const [index, point] of points.entries()) {
1952
+ const previous = orthogonal.at(-1);
1953
+ if (previous && Math.abs(previous.x - point.x) > 1e-9 && Math.abs(previous.y - point.y) > 1e-9) {
1954
+ const approachingTarget = index === points.length - 1;
1955
+ orthogonal.push(horizontal === approachingTarget ? {
1956
+ x: previous.x,
1957
+ y: point.y
1958
+ } : {
1959
+ x: point.x,
1960
+ y: previous.y
1961
+ });
1962
+ }
1963
+ orthogonal.push(point);
1964
+ }
1965
+ compactedPoints = simplifyRoute$1(orthogonal);
1966
+ } else if (edgeRouting === "SPLINES") compactedSplineControls?.delete(edgeId);
1967
+ routes.pointsByEdgeId.set(edgeId, compactedPoints);
1856
1968
  }
1969
+ if (compactedSplineControls) routes.splineNubControlsByEdgeId = compactedSplineControls;
1857
1970
  }
1858
1971
  return placement;
1859
1972
  }
@@ -2046,8 +2159,16 @@ function implicitEdgeEndpoints(input, placement, orientation) {
2046
2159
  const forward = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT" || sourceFlow <= targetFlow;
2047
2160
  const feedback = input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
2048
2161
  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";
2162
+ const endpointSide = (nodeId, portName) => {
2163
+ const node = nodeById.get(nodeId);
2164
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
2165
+ const constraints = node ? input.nodeSettings?.(node)?.portConstraints : void 0;
2166
+ if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
2167
+ const configured = input.portSettings?.(port, node)?.["port.side"];
2168
+ return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
2169
+ };
2170
+ const sourceSide = endpointSide(edge.sourceId, edge.sourcePort) ?? (feedback ? directionReversed ? "before" : "after" : forward ? "after" : "before");
2171
+ const targetSide = endpointSide(edge.targetId, edge.targetPort) ?? (feedback ? directionReversed ? "after" : "before" : forward ? "before" : "after");
2051
2172
  const sourceKey = `${edge.sourceId}:${sourceSide}`;
2052
2173
  const targetKey = `${edge.targetId}:${targetSide}`;
2053
2174
  const sourceGroup = groups.get(sourceKey) ?? [];
@@ -2169,8 +2290,14 @@ function implicitEdgeEndpoints(input, placement, orientation) {
2169
2290
  function routeEdges(style) {
2170
2291
  return (input, orientation, placement) => {
2171
2292
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
2293
+ const hasZeroFixedSideTarget = (edge) => {
2294
+ const target = nodeById.get(edge.targetId);
2295
+ const port = target?.ports?.find((candidate) => candidate.name === edge.targetPort);
2296
+ return target !== void 0 && port !== void 0 && (port.width ?? 8) === 0 && (port.height ?? 8) === 0 && input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE";
2297
+ };
2172
2298
  const pointsByEdgeId = /* @__PURE__ */ new Map();
2173
2299
  const splineNubControlsByEdgeId = /* @__PURE__ */ new Map();
2300
+ const outsideFeedbackEdgeIds = /* @__PURE__ */ new Set();
2174
2301
  const horizontal = input.direction === "left" || input.direction === "right";
2175
2302
  const reverse = input.direction === "up" || input.direction === "left";
2176
2303
  const mutableRects = placement.rectByNodeId;
@@ -2181,6 +2308,35 @@ function routeEdges(style) {
2181
2308
  loops.push(edge);
2182
2309
  selfLoopsByNodeId.set(edge.sourceId, loops);
2183
2310
  }
2311
+ const selfLoopEntriesByX = [...selfLoopsByNodeId].sort(([leftId], [rightId]) => (mutableRects.get(leftId)?.x ?? 0) - (mutableRects.get(rightId)?.x ?? 0));
2312
+ for (const [id, loops] of selfLoopEntriesByX) {
2313
+ const rect = mutableRects.get(id);
2314
+ const node = nodeById.get(id);
2315
+ if (!rect || !node) continue;
2316
+ const eastLoops = loops.filter((edge) => {
2317
+ const sourcePort = node.ports?.find((port) => port.name === edge.sourcePort);
2318
+ const targetPort = node.ports?.find((port) => port.name === edge.targetPort);
2319
+ return sourcePort !== void 0 && targetPort !== void 0 && input.portSettings?.(sourcePort, node)?.["port.side"] === "EAST" && input.portSettings?.(targetPort, node)?.["port.side"] === "EAST";
2320
+ });
2321
+ if (eastLoops.length === 0) continue;
2322
+ const selfLoopSpacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
2323
+ const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
2324
+ const exteriorWidth = selfLoopSpacing + eastLoops.reduce((sum, edge) => sum + (edge.width ?? 0) + labelSpacing, 1);
2325
+ const nodeRight = rect.x + rect.width;
2326
+ const rightCandidates = [...mutableRects.entries()].filter(([candidateId, candidateRect]) => candidateId !== id && candidateRect.x >= nodeRight - 1e-9);
2327
+ if (rightCandidates.length === 0) continue;
2328
+ const nextX = Math.min(...rightCandidates.map(([, candidateRect]) => candidateRect.x));
2329
+ const shift = nodeRight + exteriorWidth + input.spacing.node - nextX;
2330
+ if (shift <= 0) continue;
2331
+ for (const [candidateId, candidateRect] of mutableRects) {
2332
+ if (candidateId === id || candidateRect.x < nextX - 1e-9) continue;
2333
+ mutableRects.set(candidateId, {
2334
+ ...candidateRect,
2335
+ x: candidateRect.x + shift
2336
+ });
2337
+ }
2338
+ }
2339
+ const northReserveByLayer = /* @__PURE__ */ new Map();
2184
2340
  for (const [id, loops] of selfLoopsByNodeId) {
2185
2341
  const rect = mutableRects.get(id);
2186
2342
  if (!rect) continue;
@@ -2198,12 +2354,17 @@ function routeEdges(style) {
2198
2354
  else {
2199
2355
  const sideLoopCount = distribution === "NORTH_SOUTH" ? Math.ceil(loops.length / 2) : loops.length;
2200
2356
  const reserve = (ordering === "SEQUENCED" ? Math.min(1, sideLoopCount) : sideLoopCount) * spacing + splineOffset;
2201
- mutableRects.set(id, {
2202
- ...rect,
2203
- y: rect.y + reserve
2204
- });
2357
+ northReserveByLayer.set(rect.y, Math.max(northReserveByLayer.get(rect.y) ?? 0, reserve));
2205
2358
  }
2206
2359
  }
2360
+ const northReserves = [...northReserveByLayer].sort(([left], [right]) => left - right);
2361
+ for (const [candidateId, candidateRect] of mutableRects) {
2362
+ const reserve = northReserves.reduce((total, [layerY, layerReserve]) => candidateRect.y + 1e-9 >= layerY ? total + layerReserve : total, 0);
2363
+ if (reserve > 0) mutableRects.set(candidateId, {
2364
+ ...candidateRect,
2365
+ y: candidateRect.y + reserve
2366
+ });
2367
+ }
2207
2368
  const edgeLabelSideSelection = input.settings["edgeLabels.sideSelection"] ?? "SMART_DOWN";
2208
2369
  if (edgeLabelSideSelection === "ALWAYS_UP" || edgeLabelSideSelection === "SMART_UP" || edgeLabelSideSelection === "DIRECTION_UP") {
2209
2370
  let crossShift = 0;
@@ -2243,12 +2404,13 @@ function routeEdges(style) {
2243
2404
  }
2244
2405
  const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
2245
2406
  for (const edge of input.graph.edges) {
2246
- if ((edge.width ?? 0) <= 0) continue;
2407
+ const labelFlowSize = horizontal ? edge.width ?? 0 : edge.height ?? 0;
2408
+ if (labelFlowSize <= 0) continue;
2247
2409
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
2248
2410
  const targetLayer = flowLayerByNodeId.get(edge.targetId);
2249
2411
  if (sourceLayer === void 0 || targetLayer === void 0) continue;
2250
2412
  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);
2413
+ const extra = (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER" ? labelFlowSize + input.spacing.layer : labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
2252
2414
  const gap = Math.min(sourceLayer, targetLayer);
2253
2415
  labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
2254
2416
  }
@@ -2493,13 +2655,15 @@ function routeEdges(style) {
2493
2655
  bounds.start = nextStart;
2494
2656
  bounds.end = nextStart + size;
2495
2657
  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);
2658
+ 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);
2659
+ const preservedGap = (existingGapByLayer[layerNo] ?? input.spacing.layer) - (routesNearTarget ? edgeEdgeSpacing : 0);
2660
+ 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
2661
  nextStart = bounds.end + gapSpacing;
2498
2662
  }
2499
2663
  implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
2500
2664
  for (const [gap, candidates] of candidatesByGap.entries()) for (const candidate of candidates) {
2501
2665
  if (candidate.straight) continue;
2502
- const firstTrack = (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing + (candidate.slot ?? 0) * edgeEdgeSpacing;
2666
+ 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
2667
  if (candidate.secondSlot !== void 0 && candidate.crossover !== void 0) orthogonalDetourByEdgeId.set(candidate.edge.id, {
2504
2668
  firstTrack,
2505
2669
  secondTrack: (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing + candidate.secondSlot * edgeEdgeSpacing,
@@ -2663,7 +2827,7 @@ function routeEdges(style) {
2663
2827
  }] : [];
2664
2828
  });
2665
2829
  const incoming = segments.map(() => 0);
2666
- for (const dependency of acyclicDependencies) incoming[dependency.target]++;
2830
+ for (const dependency of acyclicDependencies) incoming[dependency.target] = (incoming[dependency.target] ?? 0) + 1;
2667
2831
  const queue = incoming.flatMap((count, index) => count === 0 ? [index] : []);
2668
2832
  const rank = segments.map(() => 0);
2669
2833
  for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
@@ -2745,67 +2909,88 @@ function routeEdges(style) {
2745
2909
  const sourceRect = placement.rectByNodeId.get(edge.sourceId);
2746
2910
  const targetRect = placement.rectByNodeId.get(edge.targetId);
2747
2911
  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);
2912
+ const reversedEdge = orientation.reversedEdgeIds.has(edge.id);
2913
+ const feedbackSourcePort = source.ports?.find((port) => port.name === edge.sourcePort);
2914
+ const feedbackTargetPort = target.ports?.find((port) => port.name === edge.targetPort);
2915
+ const feedbackSourcePortSide = feedbackSourcePort ? input.portSettings?.(feedbackSourcePort, source)?.["port.side"] : void 0;
2916
+ const feedbackTargetPortSide = feedbackTargetPort ? input.portSettings?.(feedbackTargetPort, target)?.["port.side"] : void 0;
2917
+ const sourceAwaySide = horizontal ? sourceRect.x < targetRect.x ? "WEST" : "EAST" : sourceRect.y < targetRect.y ? "NORTH" : "SOUTH";
2918
+ const targetAwaySide = horizontal ? sourceRect.x < targetRect.x ? "EAST" : "WEST" : sourceRect.y < targetRect.y ? "SOUTH" : "NORTH";
2919
+ const sameSideSelfLoop = source.id === target.id && feedbackSourcePortSide !== void 0 && feedbackSourcePortSide === feedbackTargetPortSide;
2920
+ const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (feedbackSourcePortSide === sourceAwaySide || feedbackTargetPortSide === targetAwaySide);
2921
+ if (input.settings.feedbackEdges === true && reversedEdge || fixedSideFeedback) {
2922
+ if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
2923
+ const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
2924
+ const flowSpacing = fixedSideFeedback ? Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10) + (source.id === target.id ? Number(input.settings["spacing.nodeSelfLoop"] ?? 10) : 0) : crossSpacing;
2925
+ const sourceFallback$1 = implicitEndpoints.get(edge.id)?.source ?? {
2926
+ x: sourceRect.x + sourceRect.width / 2,
2927
+ y: sourceRect.y + sourceRect.height / 2
2928
+ };
2929
+ const targetFallback$1 = implicitEndpoints.get(edge.id)?.target ?? {
2930
+ x: targetRect.x + targetRect.width / 2,
2931
+ y: targetRect.y + targetRect.height / 2
2932
+ };
2933
+ const start$1 = fixedSideFeedback ? getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback$1, input.direction, input) : sourceFallback$1;
2934
+ const end$1 = fixedSideFeedback ? getPortPoint(target, edge.targetPort, targetRect, targetFallback$1, input.direction, input) : targetFallback$1;
2750
2935
  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;
2936
+ const sign = fixedSideFeedback ? Math.sign(sourceRect.x - targetRect.x) || 1 : reverse ? -1 : 1;
2937
+ const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.y + rect.height)) + crossSpacing + (fixedSideFeedback ? .5 : 0);
2938
+ const preserveCenters = fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
2761
2939
  pointsByEdgeId.set(edge.id, [
2762
2940
  start$1,
2763
2941
  {
2764
- x: start$1.x + sign * spacing,
2942
+ x: start$1.x + sign * flowSpacing,
2765
2943
  y: start$1.y
2766
2944
  },
2767
2945
  {
2768
- x: start$1.x + sign * spacing,
2946
+ x: start$1.x + sign * flowSpacing,
2769
2947
  y: outerCross
2770
2948
  },
2949
+ ...preserveCenters ? [{
2950
+ x: sourceRect.x + sourceRect.width / 2,
2951
+ y: outerCross
2952
+ }, {
2953
+ x: targetRect.x + targetRect.width / 2,
2954
+ y: outerCross
2955
+ }] : [],
2771
2956
  {
2772
- x: end$1.x - sign * spacing,
2957
+ x: end$1.x - sign * flowSpacing,
2773
2958
  y: outerCross
2774
2959
  },
2775
2960
  {
2776
- x: end$1.x - sign * spacing,
2961
+ x: end$1.x - sign * flowSpacing,
2777
2962
  y: end$1.y
2778
2963
  },
2779
2964
  end$1
2780
2965
  ]);
2781
2966
  } 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;
2967
+ const sign = fixedSideFeedback ? Math.sign(sourceRect.y - targetRect.y) || 1 : reverse ? -1 : 1;
2968
+ const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.x + rect.width)) + crossSpacing + (fixedSideFeedback ? .5 : 0);
2969
+ const preserveCenters = fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
2792
2970
  pointsByEdgeId.set(edge.id, [
2793
2971
  start$1,
2794
2972
  {
2795
2973
  x: start$1.x,
2796
- y: start$1.y + sign * spacing
2974
+ y: start$1.y + sign * flowSpacing
2797
2975
  },
2798
2976
  {
2799
2977
  x: outerCross,
2800
- y: start$1.y + sign * spacing
2978
+ y: start$1.y + sign * flowSpacing
2801
2979
  },
2980
+ ...preserveCenters ? [{
2981
+ x: outerCross,
2982
+ y: sourceRect.y + sourceRect.height / 2
2983
+ }, {
2984
+ x: outerCross,
2985
+ y: targetRect.y + targetRect.height / 2
2986
+ }] : [],
2802
2987
  {
2803
2988
  x: outerCross,
2804
- y: end$1.y - sign * spacing
2989
+ y: end$1.y - sign * flowSpacing
2805
2990
  },
2806
2991
  {
2807
2992
  x: end$1.x,
2808
- y: end$1.y - sign * spacing
2993
+ y: end$1.y - sign * flowSpacing
2809
2994
  },
2810
2995
  end$1
2811
2996
  ]);
@@ -2870,6 +3055,75 @@ function routeEdges(style) {
2870
3055
  const distribution = nodeSettings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
2871
3056
  const ordering = nodeSettings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
2872
3057
  if (style === "ORTHOGONAL") {
3058
+ if (sameSideSelfLoop) {
3059
+ const endpoints$2 = implicitEndpoints.get(edge.id);
3060
+ const start$2 = getPortPoint(source, edge.sourcePort, sourceRect, endpoints$2?.source ?? {
3061
+ x: sourceRect.x + sourceRect.width / 2,
3062
+ y: sourceRect.y + sourceRect.height / 2
3063
+ }, input.direction, input);
3064
+ const end$2 = getPortPoint(target, edge.targetPort, targetRect, endpoints$2?.target ?? {
3065
+ x: targetRect.x + targetRect.width / 2,
3066
+ y: targetRect.y + targetRect.height / 2
3067
+ }, input.direction, input);
3068
+ const side = feedbackSourcePortSide;
3069
+ const sameSideLoops = loops.filter((candidate) => {
3070
+ const candidateSourcePort = source.ports?.find((port) => port.name === candidate.sourcePort);
3071
+ const candidateTargetPort = source.ports?.find((port) => port.name === candidate.targetPort);
3072
+ const candidateSourceSide = candidateSourcePort ? input.portSettings?.(candidateSourcePort, source)?.["port.side"] : void 0;
3073
+ const candidateTargetSide = candidateTargetPort ? input.portSettings?.(candidateTargetPort, source)?.["port.side"] : void 0;
3074
+ return candidateSourceSide === side && candidateTargetSide === side;
3075
+ });
3076
+ const sameSideLoopIndex = sameSideLoops.findIndex((candidate) => candidate.id === edge.id);
3077
+ const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
3078
+ 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);
3079
+ if (side === "EAST" || side === "WEST") {
3080
+ const nodeRects = [...placement.rectByNodeId.values()];
3081
+ const minimumNodeX = Math.min(...nodeRects.map((rect) => rect.x));
3082
+ const maximumNodeX = Math.max(...nodeRects.map((rect) => rect.x + rect.width));
3083
+ const maximumNodeY = Math.max(...nodeRects.map((rect) => rect.y + rect.height));
3084
+ const precedingLabelHeight = sameSideLoops.slice(0, Math.max(0, sameSideLoopIndex)).reduce((extent$1, candidate) => extent$1 + (candidate.height ?? 0) + labelSpacing, 0);
3085
+ const nearTrack = side === "EAST" ? sourceRect.x + sourceRect.width + spacing : sourceRect.x - spacing;
3086
+ const farTrack = side === "EAST" ? maximumNodeX + trackDistance + (edge.width ?? 0) : minimumNodeX - trackDistance - (edge.width ?? 0);
3087
+ const exteriorY = maximumNodeY + spacing + precedingLabelHeight + (edge.height ?? 0) / 2 + .5;
3088
+ pointsByEdgeId.set(edge.id, [
3089
+ start$2,
3090
+ {
3091
+ x: nearTrack,
3092
+ y: start$2.y
3093
+ },
3094
+ {
3095
+ x: nearTrack,
3096
+ y: exteriorY
3097
+ },
3098
+ {
3099
+ x: farTrack,
3100
+ y: exteriorY
3101
+ },
3102
+ {
3103
+ x: farTrack,
3104
+ y: end$2.y
3105
+ },
3106
+ end$2
3107
+ ]);
3108
+ outsideFeedbackEdgeIds.add(edge.id);
3109
+ continue;
3110
+ }
3111
+ const track$1 = side === "SOUTH" ? sourceRect.y + sourceRect.height + trackDistance : sourceRect.y - trackDistance;
3112
+ pointsByEdgeId.set(edge.id, [
3113
+ start$2,
3114
+ {
3115
+ x: start$2.x,
3116
+ y: track$1
3117
+ },
3118
+ {
3119
+ x: end$2.x,
3120
+ y: track$1
3121
+ },
3122
+ end$2
3123
+ ]);
3124
+ outsideFeedbackEdgeIds.add(edge.id);
3125
+ continue;
3126
+ }
2873
3127
  const routeHorizontalSide = (side, indexOnSide, countOnSide) => {
2874
3128
  const denominator = countOnSide * 2 + 1;
2875
3129
  const sequenced = ordering === "SEQUENCED";
@@ -3035,8 +3289,15 @@ function routeEdges(style) {
3035
3289
  x: targetRect.x + targetRect.width / 2,
3036
3290
  y: targetRect.y + (reverse ? targetRect.height : 0)
3037
3291
  });
3038
- const start = getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
3039
- const end = getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
3292
+ const sourcePort = source.ports?.find((port) => port.name === edge.sourcePort);
3293
+ const targetPort = target.ports?.find((port) => port.name === edge.targetPort);
3294
+ const sourcePortSide = sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
3295
+ const targetPortSide = targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : void 0;
3296
+ 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
+ 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
+ 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);
3040
3301
  const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
3041
3302
  const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
3042
3303
  const earlierLayer = Math.min(sourceLayer, targetLayer);
@@ -3181,7 +3442,8 @@ function routeEdges(style) {
3181
3442
  }
3182
3443
  return {
3183
3444
  pointsByEdgeId,
3184
- splineNubControlsByEdgeId
3445
+ splineNubControlsByEdgeId,
3446
+ outsideFeedbackEdgeIds
3185
3447
  };
3186
3448
  };
3187
3449
  }
@@ -3616,7 +3878,7 @@ function normalizeAndBalance(nodes, previousLayerCounts) {
3616
3878
  }
3617
3879
  return filling;
3618
3880
  }
3619
- function runNetworkSimplex(nodes, edges, iterationLimit, previousLayerCounts, balance = true) {
3881
+ function runNetworkSimplex(nodes, _edges, iterationLimit, previousLayerCounts, balance = true) {
3620
3882
  const orderedEdges = nodes.flatMap((node) => node.outgoing);
3621
3883
  orderedEdges.forEach((edge, index) => edge.order = index);
3622
3884
  assignInitialLayers(nodes);
@@ -3890,10 +4152,21 @@ function splitLongEdges(input, orientation, assignment) {
3890
4152
  const originalEdgeBySegmentId = /* @__PURE__ */ new Map();
3891
4153
  const usedNodeIds = new Set(nodes.map((node) => node.id));
3892
4154
  const originalNodeIds = new Set(usedNodeIds);
4155
+ const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4156
+ const forwardSourceSide = input.direction === "right" ? "EAST" : input.direction === "left" ? "WEST" : input.direction === "down" ? "SOUTH" : "NORTH";
4157
+ const forwardTargetSide = input.direction === "right" ? "WEST" : input.direction === "left" ? "EAST" : input.direction === "down" ? "NORTH" : "SOUTH";
3893
4158
  for (const edge of input.graph.edges) {
3894
4159
  const sourceLayer = layerByNodeId.get(edge.sourceId) ?? 0;
3895
4160
  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)) {
4161
+ const span = Math.abs(targetLayer - sourceLayer);
4162
+ const source = nodeById.get(edge.sourceId);
4163
+ const target = nodeById.get(edge.targetId);
4164
+ const sourcePort = source?.ports?.find((port) => port.name === edge.sourcePort);
4165
+ const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
4166
+ const sourceSide = source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
4167
+ 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);
4169
+ if (span <= 1 || edge.sourceId === edge.targetId || input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id) || fixedSideFeedback) {
3897
4170
  edges.push(edge);
3898
4171
  originalEdgeBySegmentId.set(edge.id, edge);
3899
4172
  if (orientation.reversedEdgeIds.has(edge.id)) reversedEdgeIds.add(edge.id);
@@ -4055,7 +4328,9 @@ function joinLongEdgeRoutes(routes, segmentIdsByEdgeId, preserveInternalDuplicat
4055
4328
  return result;
4056
4329
  };
4057
4330
  const pointsByEdgeId = /* @__PURE__ */ new Map();
4331
+ const outsideFeedbackEdgeIds = /* @__PURE__ */ new Set();
4058
4332
  for (const [edgeId, segmentIds] of segmentIdsByEdgeId) {
4333
+ if (segmentIds.some((segmentId) => routes.outsideFeedbackEdgeIds?.has(segmentId))) outsideFeedbackEdgeIds.add(edgeId);
4059
4334
  if (convertLongSplines && segmentIds.length > 1) {
4060
4335
  const segments = segmentIds.map((segmentId) => routes.pointsByEdgeId.get(segmentId) ?? []).filter((points$1) => points$1.length >= 2);
4061
4336
  if (segments.length > 1) {
@@ -4128,7 +4403,10 @@ function joinLongEdgeRoutes(routes, segmentIdsByEdgeId, preserveInternalDuplicat
4128
4403
  } else for (const segmentId of segmentIds) appendPoints(points, routes.pointsByEdgeId.get(segmentId) ?? [], preserveInternalDuplicates || segmentIds.length === 1);
4129
4404
  pointsByEdgeId.set(edgeId, preserveInternalDuplicates || segmentIds.length === 1 ? points : simplify(points));
4130
4405
  }
4131
- return { pointsByEdgeId };
4406
+ return {
4407
+ pointsByEdgeId,
4408
+ outsideFeedbackEdgeIds
4409
+ };
4132
4410
  }
4133
4411
 
4134
4412
  //#endregion
@@ -8554,19 +8832,52 @@ function runLayeredPipeline(graph, options, context) {
8554
8832
  };
8555
8833
  routes.pointsByEdgeId.set(edge.id, points);
8556
8834
  }
8835
+ const routedPortAnchors = /* @__PURE__ */ new Map();
8836
+ for (const edge of graph.edges) {
8837
+ const points = routes.pointsByEdgeId.get(edge.id);
8838
+ const first = points?.[0];
8839
+ const last = points?.at(-1);
8840
+ 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);
8841
+ 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);
8842
+ }
8557
8843
  const nodes = graph.nodes.map((node) => {
8558
8844
  const rect = placement.rectByNodeId.get(node.id);
8559
8845
  if (!rect) throw new Error(`Node placement missing for ${node.id}`);
8560
- const ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
8846
+ let ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
8561
8847
  ...options.settings,
8562
8848
  ...options.nodeSettings?.(node)
8563
8849
  });
8850
+ if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") ports = ports?.map((port) => {
8851
+ if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
8852
+ const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
8853
+ if (!anchor || port.x === void 0 || port.y === void 0) return port;
8854
+ const configuredAnchor = (input.portSettings?.(port, node))?.["port.anchor"];
8855
+ const width = port.width ?? 0;
8856
+ const height = port.height ?? 0;
8857
+ const defaultAnchorX = port.x >= rect.width ? width : port.x + width <= 0 ? 0 : width / 2;
8858
+ const defaultAnchorY = port.y >= rect.height ? height : port.y + height <= 0 ? 0 : height / 2;
8859
+ const x = anchor.x - rect.x - (configuredAnchor?.x ?? defaultAnchorX);
8860
+ const y = anchor.y - rect.y - (configuredAnchor?.y ?? defaultAnchorY);
8861
+ return {
8862
+ ...port,
8863
+ x: x === 0 && Object.is(port.x, -0) ? port.x : x,
8864
+ y: y === 0 && Object.is(port.y, -0) ? port.y : y
8865
+ };
8866
+ });
8564
8867
  return {
8565
8868
  ...node,
8566
8869
  ...rect,
8567
8870
  ...ports === void 0 ? {} : { ports }
8568
8871
  };
8569
8872
  });
8873
+ const feedbackNodeRects = graph.nodes.flatMap((node) => {
8874
+ const rect = placement.rectByNodeId.get(node.id);
8875
+ return rect ? [rect] : [];
8876
+ });
8877
+ const minimumFeedbackNodeX = Math.min(...feedbackNodeRects.map((rect) => rect.x));
8878
+ const maximumFeedbackNodeX = Math.max(...feedbackNodeRects.map((rect) => rect.x + rect.width));
8879
+ const minimumFeedbackNodeY = Math.min(...feedbackNodeRects.map((rect) => rect.y));
8880
+ const maximumFeedbackNodeY = Math.max(...feedbackNodeRects.map((rect) => rect.y + rect.height));
8570
8881
  const edges = graph.edges.map((edge) => {
8571
8882
  const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
8572
8883
  const midpoint = getPolylineMidpoint(points);
@@ -8582,9 +8893,44 @@ function runLayeredPipeline(graph, options, context) {
8582
8893
  const labelDummyRect = placement.rectByNodeId.get(expanded.labelDummyIdByEdgeId.get(edge.id) ?? "");
8583
8894
  const edgeLabelSideSelection = options.settings?.["edgeLabels.sideSelection"] ?? "SMART_DOWN";
8584
8895
  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
8896
  const horizontal = direction === "left" || direction === "right";
8897
+ const verticalTrack = horizontal ? points.find((point, index) => {
8898
+ const next = points[index + 1];
8899
+ return next !== void 0 && point.x === next.x && point.y !== next.y;
8900
+ }) : void 0;
8901
+ const secondPoint = points[1];
8902
+ const beforeLastPoint = points.at(-2);
8903
+ const flowDelta = horizontal ? lastPoint.x - firstPoint.x : lastPoint.y - firstPoint.y;
8904
+ const firstLeadDelta = secondPoint ? horizontal ? secondPoint.x - firstPoint.x : secondPoint.y - firstPoint.y : 0;
8905
+ const lastLeadDelta = beforeLastPoint ? horizontal ? lastPoint.x - beforeLastPoint.x : lastPoint.y - beforeLastPoint.y : 0;
8906
+ const outsideFeedback = routes.outsideFeedbackEdgeIds?.has(edge.id) === true || secondPoint !== void 0 && beforeLastPoint !== void 0 && flowDelta !== 0 && firstLeadDelta * flowDelta < 0 && lastLeadDelta * flowDelta < 0;
8907
+ const horizontalFeedbackCandidate = outsideFeedback ? points.flatMap((point, index) => {
8908
+ const next = points[index + 1];
8909
+ return next !== void 0 && point.y === next.y && (point.y < minimumFeedbackNodeY || point.y > maximumFeedbackNodeY) ? [{
8910
+ start: point,
8911
+ end: next,
8912
+ length: Math.abs(next.x - point.x)
8913
+ }] : [];
8914
+ }).sort((left, right) => right.length - left.length)[0] : void 0;
8915
+ const verticalFeedbackCandidate = outsideFeedback ? points.flatMap((point, index) => {
8916
+ const next = points[index + 1];
8917
+ return next !== void 0 && point.x === next.x && (point.x < minimumFeedbackNodeX || point.x > maximumFeedbackNodeX) ? [{
8918
+ start: point,
8919
+ end: next,
8920
+ length: Math.abs(next.y - point.y)
8921
+ }] : [];
8922
+ }).sort((left, right) => right.length - left.length)[0] : void 0;
8923
+ const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
8924
+ const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
8925
+ const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
8926
+ const sourcePortSide = sourceNode && sourcePort ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"] : void 0;
8927
+ const targetPortSide = sourceNode && targetPort ? options.portSettings?.(targetPort, sourceNode)?.["port.side"] : void 0;
8928
+ const horizontalFeedbackTrack = edge.sourceId === edge.targetId && (sourcePortSide === "EAST" || sourcePortSide === "WEST") && targetPortSide === sourcePortSide || (horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1) ? horizontalFeedbackCandidate : void 0;
8929
+ const verticalFeedbackTrack = horizontalFeedbackTrack ? void 0 : verticalFeedbackCandidate;
8930
+ const trackNearTarget = verticalTrack !== void 0 && Math.abs(lastPoint.x - verticalTrack.x) < Math.abs(verticalTrack.x - firstPoint.x);
8931
+ const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
8932
+ 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
+ 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);
8588
8934
  const x = labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
8589
8935
  const y = labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
8590
8936
  return {