@statelyai/layout 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elkjs/index.mjs +7 -6
- package/dist/index.mjs +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-QwJn2gb2.mjs → layered-f_3mmGHr.mjs} +324 -15
- package/dist/{spore-D15xIQKj.mjs → spore-BfTduMDc.mjs} +1 -1
- package/package.json +5 -1
- package/src/elkjs/index.ts +11 -4
- package/src/layered/index.ts +424 -7
- package/src/layered/long-edges.ts +7 -6
- package/src/layered/strategies.ts +66 -6
package/dist/elkjs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-
|
|
2
|
-
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-
|
|
1
|
+
import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-f_3mmGHr.mjs";
|
|
2
|
+
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-BfTduMDc.mjs";
|
|
3
3
|
import { createGraph } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/elkjs/index.ts
|
|
@@ -573,8 +573,9 @@ function applyNodeMicroLayout(graph, globalOptions) {
|
|
|
573
573
|
const configuredSizeOptions = getOption(node.layoutOptions ?? {}, "nodeSize.options");
|
|
574
574
|
const sizeOptions = new Set(configuredSizeOptions === void 0 ? ["DEFAULT_MINIMUM_SIZE"] : String(configuredSizeOptions).split(/[\s,;]+/).filter(Boolean));
|
|
575
575
|
const effectivelyFixedPortLabelSize = constraints.includes("PORT_LABELS") && !constraints.includes("NODE_LABELS") && !constraints.includes("MINIMUM_SIZE");
|
|
576
|
-
|
|
577
|
-
let
|
|
576
|
+
const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
|
|
577
|
+
let width = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.width ?? 0 : 0;
|
|
578
|
+
let height = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.height ?? 0 : 0;
|
|
578
579
|
let insideHorizontalInset = 0;
|
|
579
580
|
let insideVerticalInset = 0;
|
|
580
581
|
const insideLabelCells = /* @__PURE__ */ new Map();
|
|
@@ -599,8 +600,8 @@ function applyNodeMicroLayout(graph, globalOptions) {
|
|
|
599
600
|
}
|
|
600
601
|
width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
|
|
601
602
|
height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
|
|
602
|
-
if (sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
|
|
603
|
-
if (sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
|
|
603
|
+
if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
|
|
604
|
+
if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
|
|
604
605
|
}
|
|
605
606
|
if (constraints.includes("NODE_LABELS")) {
|
|
606
607
|
for (const label of node.labels ?? []) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-
|
|
2
|
-
import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-
|
|
1
|
+
import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-f_3mmGHr.mjs";
|
|
2
|
+
import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-BfTduMDc.mjs";
|
|
3
3
|
import { getGraphIssues } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/layout.ts
|
package/dist/layered/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-
|
|
1
|
+
import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-f_3mmGHr.mjs";
|
|
2
2
|
|
|
3
3
|
export { assignLayersByBreadthFirstModelOrder, assignLayersByDepthFirstModelOrder, assignLayersByLongestPath, assignLayersByLongestPathToSink, assignLayersInteractively, assignLayersWithCoffmanGraham, assignLayersWithMinWidth, assignLayersWithNetworkSimplex, assignLayersWithStretchWidth, breakCyclesByModelOrder, breakCyclesByStronglyConnectedConnectivity, breakCyclesByStronglyConnectedNodeType, breakCyclesGreedily, breakCyclesGreedilyByModelOrder, breakCyclesInteractively, breakCyclesWithDepthFirstSearch, breakCyclesWithModelOrderBreadthFirstSearch, breakCyclesWithModelOrderDepthFirstSearch, elkLayeredEnumValues, elkLayeredOptionDefinitions, fromElkLayeredOptionId, getLayeredLayout, layeredAlgorithm, minimizeCrossingsInteractively, minimizeCrossingsWithBarycenter, minimizeCrossingsWithMedian, minimizeCrossingsWithModelOrder, placeNodesInLayers, placeNodesInteractively, placeNodesWithBrandesKoepf, placeNodesWithLinearSegments, placeNodesWithNetworkSimplex, routeEdgesOrthogonally, routeEdgesWithPolylines, routeEdgesWithSplines, toElkLayeredOptions };
|
|
@@ -195,6 +195,20 @@ const phaseRandomByInput = /* @__PURE__ */ new WeakMap();
|
|
|
195
195
|
function getOrientedEndpoints$1(edge, orientation) {
|
|
196
196
|
return orientation.reversedEdgeIds.has(edge.id) ? [edge.targetId, edge.sourceId] : [edge.sourceId, edge.targetId];
|
|
197
197
|
}
|
|
198
|
+
function getOrientedPortDirection(input, orientation, node, port) {
|
|
199
|
+
const configuredSide = input.portSettings?.(port, node)?.["port.side"];
|
|
200
|
+
if (configuredSide !== "NORTH" && configuredSide !== "SOUTH" && configuredSide !== "WEST" && configuredSide !== "EAST") return port.direction;
|
|
201
|
+
let incoming = false;
|
|
202
|
+
let outgoing = false;
|
|
203
|
+
for (const edge of input.graph.edges) {
|
|
204
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
205
|
+
if (edge.sourceId === node.id && edge.sourcePort === port.name) if (reversed) incoming = true;
|
|
206
|
+
else outgoing = true;
|
|
207
|
+
if (edge.targetId === node.id && edge.targetPort === port.name) if (reversed) outgoing = true;
|
|
208
|
+
else incoming = true;
|
|
209
|
+
}
|
|
210
|
+
return incoming && outgoing ? "inout" : outgoing ? "out" : incoming ? "in" : port.direction;
|
|
211
|
+
}
|
|
198
212
|
function cycleModelOrder(input) {
|
|
199
213
|
const enforced = input.settings["considerModelOrder.groupModelOrder.cbGroupOrderStrategy"] === "ENFORCED";
|
|
200
214
|
const count = Math.max(1, input.graph.nodes.length);
|
|
@@ -2917,7 +2931,11 @@ function routeEdges(style) {
|
|
|
2917
2931
|
const sourceAwaySide = horizontal ? sourceRect.x < targetRect.x ? "WEST" : "EAST" : sourceRect.y < targetRect.y ? "NORTH" : "SOUTH";
|
|
2918
2932
|
const targetAwaySide = horizontal ? sourceRect.x < targetRect.x ? "EAST" : "WEST" : sourceRect.y < targetRect.y ? "SOUTH" : "NORTH";
|
|
2919
2933
|
const sameSideSelfLoop = source.id === target.id && feedbackSourcePortSide !== void 0 && feedbackSourcePortSide === feedbackTargetPortSide;
|
|
2920
|
-
const
|
|
2934
|
+
const hasFixedPortSide = (node) => {
|
|
2935
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
2936
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
2937
|
+
};
|
|
2938
|
+
const fixedSideFeedback = style === "ORTHOGONAL" && !sameSideSelfLoop && (hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide || hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide);
|
|
2921
2939
|
if (input.settings.feedbackEdges === true && reversedEdge || fixedSideFeedback) {
|
|
2922
2940
|
if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
|
|
2923
2941
|
const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
|
|
@@ -3296,8 +3314,9 @@ function routeEdges(style) {
|
|
|
3296
3314
|
const fallbackMatchesPortSide = (fallback, rect, side) => side === void 0 || side === "UNDEFINED" || side === "EAST" && Math.abs(fallback.x - rect.x - rect.width) < 1e-9 || side === "WEST" && Math.abs(fallback.x - rect.x) < 1e-9 || side === "SOUTH" && Math.abs(fallback.y - rect.y - rect.height) < 1e-9 || side === "NORTH" && Math.abs(fallback.y - rect.y) < 1e-9;
|
|
3297
3315
|
const sourceFixedSide = sourcePort !== void 0 && fallbackMatchesPortSide(sourceFallback, sourceRect, sourcePortSide) && (sourcePort.width ?? 8) === 0 && (sourcePort.height ?? 8) === 0 && input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" && input.graph.edges.filter((candidate) => candidate.sourceId === edge.sourceId && candidate.sourcePort === edge.sourcePort).length === 1;
|
|
3298
3316
|
const targetFixedSide = targetPort !== void 0 && fallbackMatchesPortSide(targetFallback, targetRect, targetPortSide) && (targetPort.width ?? 8) === 0 && (targetPort.height ?? 8) === 0 && input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" && input.graph.edges.filter((candidate) => candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort).length === 1;
|
|
3299
|
-
const
|
|
3300
|
-
const
|
|
3317
|
+
const flexibleFeedback = reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
|
|
3318
|
+
const start = flexibleFeedback ? sourceFallback : sourceFixedSide ? sourceFallback : getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input);
|
|
3319
|
+
const end = flexibleFeedback ? targetFallback : targetFixedSide ? targetFallback : getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input);
|
|
3301
3320
|
const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
|
|
3302
3321
|
const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
|
|
3303
3322
|
const earlierLayer = Math.min(sourceLayer, targetLayer);
|
|
@@ -3450,7 +3469,7 @@ function routeEdges(style) {
|
|
|
3450
3469
|
const routeEdgesOrthogonally = routeEdges("ORTHOGONAL");
|
|
3451
3470
|
const routeEdgesWithPolylines = routeEdges("POLYLINE");
|
|
3452
3471
|
const routeEdgesWithSplines = routeEdges("SPLINES");
|
|
3453
|
-
function placePorts(ports, rect, direction, portSettings, nodeSettings) {
|
|
3472
|
+
function placePorts(ports, rect, direction, portSettings, nodeSettings, portDirection) {
|
|
3454
3473
|
if (!ports) return void 0;
|
|
3455
3474
|
const horizontal = direction === "left" || direction === "right";
|
|
3456
3475
|
const reverse = direction === "up" || direction === "left";
|
|
@@ -3464,7 +3483,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
|
|
|
3464
3483
|
sideByPort.set(port, configured);
|
|
3465
3484
|
continue;
|
|
3466
3485
|
}
|
|
3467
|
-
const outgoing = port.direction === "out";
|
|
3486
|
+
const outgoing = (portDirection?.(port) ?? port.direction) === "out";
|
|
3468
3487
|
const normalFarSide = reverse ? !outgoing : outgoing;
|
|
3469
3488
|
const farSide = sideFixed && configured === "UNDEFINED" ? !normalFarSide : normalFarSide;
|
|
3470
3489
|
sideByPort.set(port, horizontal ? farSide ? "EAST" : "WEST" : farSide ? "SOUTH" : "NORTH");
|
|
@@ -3574,7 +3593,7 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings) {
|
|
|
3574
3593
|
});
|
|
3575
3594
|
}
|
|
3576
3595
|
/** Account for port extents in ELK's graph-padding normalization. */
|
|
3577
|
-
function normalizePlacementForPortExtents(input, placement, order) {
|
|
3596
|
+
function normalizePlacementForPortExtents(input, placement, order, orientation) {
|
|
3578
3597
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
3579
3598
|
const physicalLayers = order.layers.map((layer) => layer.flatMap((id) => placement.rectByNodeId.has(id) ? [id] : [])).filter((layer) => layer.length > 0).sort((left, right) => {
|
|
3580
3599
|
const leftRect = placement.rectByNodeId.get(left[0]);
|
|
@@ -3609,7 +3628,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
|
|
|
3609
3628
|
const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
|
|
3610
3629
|
...input.settings,
|
|
3611
3630
|
...input.nodeSettings?.(node)
|
|
3612
|
-
});
|
|
3631
|
+
}, (port) => getOrientedPortDirection(input, orientation, node, port));
|
|
3613
3632
|
for (const port of ports ?? []) {
|
|
3614
3633
|
const extent$1 = horizontal ? trailing ? Math.max(0, (port.x ?? 0) + (port.width ?? 0) - rect.width) : Math.max(0, -(port.x ?? 0)) : trailing ? Math.max(0, (port.y ?? 0) + (port.height ?? 0) - rect.height) : Math.max(0, -(port.y ?? 0));
|
|
3615
3634
|
if (trailing) trailingExtent = Math.max(trailingExtent, extent$1);
|
|
@@ -3628,7 +3647,7 @@ function normalizePlacementForPortExtents(input, placement, order) {
|
|
|
3628
3647
|
const ports = placePorts(node.ports, rect, input.direction, (port) => input.portSettings?.(port, node), {
|
|
3629
3648
|
...input.settings,
|
|
3630
3649
|
...input.nodeSettings?.(node)
|
|
3631
|
-
});
|
|
3650
|
+
}, (port) => getOrientedPortDirection(input, orientation, node, port));
|
|
3632
3651
|
for (const port of ports ?? []) {
|
|
3633
3652
|
minimumX = Math.min(minimumX, rect.x + (port.x ?? 0));
|
|
3634
3653
|
minimumY = Math.min(minimumY, rect.y + (port.y ?? 0));
|
|
@@ -4165,7 +4184,12 @@ function splitLongEdges(input, orientation, assignment) {
|
|
|
4165
4184
|
const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
|
|
4166
4185
|
const sourceSide = source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : void 0;
|
|
4167
4186
|
const targetSide = target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : void 0;
|
|
4168
|
-
const
|
|
4187
|
+
const hasFixedPortSide = (node) => {
|
|
4188
|
+
if (!node) return false;
|
|
4189
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4190
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4191
|
+
};
|
|
4192
|
+
const fixedSideFeedback = sourceLayer > targetLayer && (hasFixedPortSide(source) && sourceSide === forwardSourceSide || hasFixedPortSide(target) && targetSide === forwardTargetSide);
|
|
4169
4193
|
if (span <= 1 || edge.sourceId === edge.targetId || input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id) || fixedSideFeedback) {
|
|
4170
4194
|
edges.push(edge);
|
|
4171
4195
|
originalEdgeBySegmentId.set(edge.id, edge);
|
|
@@ -8459,6 +8483,24 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8459
8483
|
left: options.padding?.left ?? 12
|
|
8460
8484
|
};
|
|
8461
8485
|
const switchedSideByPort = /* @__PURE__ */ new Map();
|
|
8486
|
+
const parallelPortIndexByKey = /* @__PURE__ */ new Map();
|
|
8487
|
+
const labeledEdgesByPair = /* @__PURE__ */ new Map();
|
|
8488
|
+
for (const edge of graph.edges) {
|
|
8489
|
+
const settings = options.edgeSettings?.(edge);
|
|
8490
|
+
if (edge.sourcePort === void 0 || edge.targetPort === void 0 || edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0 || (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true) continue;
|
|
8491
|
+
const key = `${edge.sourceId}\0${edge.targetId}`;
|
|
8492
|
+
const pair = labeledEdgesByPair.get(key) ?? [];
|
|
8493
|
+
pair.push(edge);
|
|
8494
|
+
labeledEdgesByPair.set(key, pair);
|
|
8495
|
+
}
|
|
8496
|
+
for (const pair of labeledEdgesByPair.values()) {
|
|
8497
|
+
if (pair.length < 2) continue;
|
|
8498
|
+
for (const [index, edge] of pair.entries()) {
|
|
8499
|
+
const portIndex = options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES" ? index : pair.length - index - 1;
|
|
8500
|
+
parallelPortIndexByKey.set(`${edge.sourceId}\0${edge.sourcePort}`, portIndex);
|
|
8501
|
+
parallelPortIndexByKey.set(`${edge.targetId}\0${edge.targetPort}`, portIndex);
|
|
8502
|
+
}
|
|
8503
|
+
}
|
|
8462
8504
|
const input = {
|
|
8463
8505
|
graph,
|
|
8464
8506
|
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
|
|
@@ -8477,6 +8519,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8477
8519
|
...options.edgeSettings === void 0 ? {} : { edgeSettings: options.edgeSettings },
|
|
8478
8520
|
portSettings: (port, node) => ({
|
|
8479
8521
|
...options.portSettings?.(port, node),
|
|
8522
|
+
...parallelPortIndexByKey.has(`${node.id}\0${port.name}`) ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) } : {},
|
|
8480
8523
|
...switchedSideByPort.has(`${node.id}\0${port.name}`) ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) } : {}
|
|
8481
8524
|
})
|
|
8482
8525
|
};
|
|
@@ -8674,7 +8717,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8674
8717
|
});
|
|
8675
8718
|
}
|
|
8676
8719
|
}
|
|
8677
|
-
measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order));
|
|
8720
|
+
measure("port-margin-normalization", () => normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation));
|
|
8678
8721
|
{
|
|
8679
8722
|
const horizontal = direction === "left" || direction === "right";
|
|
8680
8723
|
const crossCenter = (nodeId) => {
|
|
@@ -8699,8 +8742,186 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8699
8742
|
}
|
|
8700
8743
|
const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
|
|
8701
8744
|
const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
|
|
8702
|
-
|
|
8745
|
+
let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
|
|
8703
8746
|
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
8747
|
+
const antiparallelLabelPositions = /* @__PURE__ */ new Map();
|
|
8748
|
+
const parallelLabelPositions = /* @__PURE__ */ new Map();
|
|
8749
|
+
const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
|
|
8750
|
+
const horizontalAntiparallelFlow = direction === "left" || direction === "right";
|
|
8751
|
+
if (options.strategies?.routeEdges === void 0 && options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" && options.settings?.["layering.strategy"] === "INTERACTIVE" && options.settings?.["crossingMinimization.forceNodeModelOrder"] === true && options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" && options.settings?.["nodePlacement.favorStraightEdges"] === true && options.settings?.["compaction.postCompaction.strategy"] === "LEFT" && options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE") {
|
|
8752
|
+
const neighbors = new Map(graph.nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
8753
|
+
for (const edge of graph.edges) {
|
|
8754
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
8755
|
+
neighbors.get(edge.sourceId)?.add(edge.targetId);
|
|
8756
|
+
neighbors.get(edge.targetId)?.add(edge.sourceId);
|
|
8757
|
+
}
|
|
8758
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8759
|
+
for (const node of graph.nodes) {
|
|
8760
|
+
if (visited.has(node.id)) continue;
|
|
8761
|
+
const component = [];
|
|
8762
|
+
const pending = [node.id];
|
|
8763
|
+
while (pending.length > 0) {
|
|
8764
|
+
const id = pending.pop();
|
|
8765
|
+
if (visited.has(id)) continue;
|
|
8766
|
+
visited.add(id);
|
|
8767
|
+
const member = graph.nodes.find((candidate) => candidate.id === id);
|
|
8768
|
+
if (member) component.push(member);
|
|
8769
|
+
for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
|
|
8770
|
+
}
|
|
8771
|
+
if (component.length < 2) continue;
|
|
8772
|
+
const componentIds = new Set(component.map((member) => member.id));
|
|
8773
|
+
if (!graph.edges.some((edge) => componentIds.has(edge.sourceId) && componentIds.has(edge.targetId) && expanded.orientation.reversedEdgeIds.has(edge.id))) continue;
|
|
8774
|
+
const directedPairCounts = /* @__PURE__ */ new Map();
|
|
8775
|
+
for (const edge of graph.edges) {
|
|
8776
|
+
if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
|
|
8777
|
+
const key = `${edge.sourceId}\0${edge.targetId}`;
|
|
8778
|
+
directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
|
|
8779
|
+
}
|
|
8780
|
+
if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
|
|
8781
|
+
if (component.some((member) => {
|
|
8782
|
+
const constraints = options.nodeSettings?.(member)?.portConstraints;
|
|
8783
|
+
return constraints !== void 0 && constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
8784
|
+
})) continue;
|
|
8785
|
+
const layerCounts = /* @__PURE__ */ new Map();
|
|
8786
|
+
for (const member of component) {
|
|
8787
|
+
const layer = expanded.assignment.layerByNodeId.get(member.id);
|
|
8788
|
+
if (layer === void 0) continue;
|
|
8789
|
+
layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
|
|
8790
|
+
}
|
|
8791
|
+
if ([...layerCounts.values()].some((count) => count !== 1)) continue;
|
|
8792
|
+
const rects = component.flatMap((member) => {
|
|
8793
|
+
const rect = mutableRects.get(member.id);
|
|
8794
|
+
return rect ? [{
|
|
8795
|
+
member,
|
|
8796
|
+
rect
|
|
8797
|
+
}] : [];
|
|
8798
|
+
});
|
|
8799
|
+
if (rects.length !== component.length) continue;
|
|
8800
|
+
const center = (Math.min(...rects.map(({ rect }) => horizontalAntiparallelFlow ? rect.y : rect.x)) + Math.max(...rects.map(({ rect }) => horizontalAntiparallelFlow ? rect.y + rect.height : rect.x + rect.width))) / 2;
|
|
8801
|
+
for (const { member, rect } of rects) {
|
|
8802
|
+
const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
|
|
8803
|
+
const nextCross = horizontalAntiparallelFlow ? center - rect.height / 2 : center - rect.width / 2;
|
|
8804
|
+
postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
|
|
8805
|
+
mutableRects.set(member.id, horizontalAntiparallelFlow ? {
|
|
8806
|
+
...rect,
|
|
8807
|
+
y: nextCross
|
|
8808
|
+
} : {
|
|
8809
|
+
...rect,
|
|
8810
|
+
x: nextCross
|
|
8811
|
+
});
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
{
|
|
8816
|
+
const horizontal = horizontalAntiparallelFlow;
|
|
8817
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
8818
|
+
const pairEdges = /* @__PURE__ */ new Map();
|
|
8819
|
+
for (const edge of graph.edges) {
|
|
8820
|
+
if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) continue;
|
|
8821
|
+
const key = [edge.sourceId, edge.targetId].sort().join("\0");
|
|
8822
|
+
const pair = pairEdges.get(key) ?? [];
|
|
8823
|
+
pair.push(edge);
|
|
8824
|
+
pairEdges.set(key, pair);
|
|
8825
|
+
}
|
|
8826
|
+
for (const pair of pairEdges.values()) {
|
|
8827
|
+
if (pair.length !== 2 || pair[0].sourceId !== pair[1].targetId || pair[0].targetId !== pair[1].sourceId || pair.some((edge) => {
|
|
8828
|
+
const settings = options.edgeSettings?.(edge);
|
|
8829
|
+
return (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true;
|
|
8830
|
+
})) continue;
|
|
8831
|
+
const firstNode = nodesById.get(pair[0].sourceId);
|
|
8832
|
+
const secondNode = nodesById.get(pair[0].targetId);
|
|
8833
|
+
const firstRect = placement.rectByNodeId.get(pair[0].sourceId);
|
|
8834
|
+
const secondRect = placement.rectByNodeId.get(pair[0].targetId);
|
|
8835
|
+
if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
|
|
8836
|
+
const nonSelfNeighbors = (nodeId) => new Set(graph.edges.flatMap((edge) => edge.sourceId === edge.targetId ? [] : edge.sourceId === nodeId ? [edge.targetId] : edge.targetId === nodeId ? [edge.sourceId] : []));
|
|
8837
|
+
if (nonSelfNeighbors(firstNode.id).size !== 1 || nonSelfNeighbors(secondNode.id).size !== 1) continue;
|
|
8838
|
+
const hasFlexiblePorts = (node) => {
|
|
8839
|
+
const constraints = options.nodeSettings?.(node)?.portConstraints;
|
|
8840
|
+
return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
|
|
8841
|
+
};
|
|
8842
|
+
if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
|
|
8843
|
+
const firstBeforeSecond = horizontal ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
|
|
8844
|
+
const beforeRect = firstBeforeSecond ? firstRect : secondRect;
|
|
8845
|
+
const afterRect = firstBeforeSecond ? secondRect : firstRect;
|
|
8846
|
+
const forward = pair.find((edge) => firstBeforeSecond ? edge.sourceId === pair[0].sourceId : edge.sourceId === pair[0].targetId);
|
|
8847
|
+
if (!forward?.sourcePort || !forward.targetPort) continue;
|
|
8848
|
+
let cross = Math.min(horizontal ? firstRect.y : firstRect.x, horizontal ? secondRect.y : secondRect.x);
|
|
8849
|
+
const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
|
|
8850
|
+
for (const edge of pair) {
|
|
8851
|
+
const width = edge.width ?? 0;
|
|
8852
|
+
const height = edge.height ?? 0;
|
|
8853
|
+
antiparallelLabelPositions.set(edge.id, horizontal ? {
|
|
8854
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
8855
|
+
y: cross
|
|
8856
|
+
} : {
|
|
8857
|
+
x: cross,
|
|
8858
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
|
|
8859
|
+
});
|
|
8860
|
+
cross += (horizontal ? height : width) + edgeSpacing;
|
|
8861
|
+
}
|
|
8862
|
+
const forwardLabel = antiparallelLabelPositions.get(forward.id);
|
|
8863
|
+
const forwardCross = horizontal ? Math.round(forwardLabel.y + (forward.height ?? 0) / 2) : Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
|
|
8864
|
+
const alignPort = (node, portName) => {
|
|
8865
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
8866
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
8867
|
+
if (!rect || !port) return;
|
|
8868
|
+
const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
|
|
8869
|
+
...options.settings,
|
|
8870
|
+
...options.nodeSettings?.(node)
|
|
8871
|
+
}, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
|
|
8872
|
+
if (!placedPort) return;
|
|
8873
|
+
const nextCross = forwardCross - (horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2);
|
|
8874
|
+
const currentCross = horizontal ? rect.y : rect.x;
|
|
8875
|
+
postCompactionNodeCrossDeltas.set(node.id, (postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross);
|
|
8876
|
+
mutableRects.set(node.id, horizontal ? {
|
|
8877
|
+
...rect,
|
|
8878
|
+
y: nextCross
|
|
8879
|
+
} : {
|
|
8880
|
+
...rect,
|
|
8881
|
+
x: nextCross
|
|
8882
|
+
});
|
|
8883
|
+
};
|
|
8884
|
+
alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
|
|
8885
|
+
alignPort(nodesById.get(forward.targetId), forward.targetPort);
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
8889
|
+
const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
|
|
8890
|
+
for (const edge of graph.edges) {
|
|
8891
|
+
const points = [...pointsByEdgeId.get(edge.id) ?? []];
|
|
8892
|
+
if (points.length === 0) continue;
|
|
8893
|
+
const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
|
|
8894
|
+
const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
|
|
8895
|
+
const shiftEndpoint = (endpointIndex, adjacentIndex, delta) => {
|
|
8896
|
+
if (delta === 0) return;
|
|
8897
|
+
const endpoint = points[endpointIndex];
|
|
8898
|
+
const adjacent = points[adjacentIndex];
|
|
8899
|
+
if (!endpoint) return;
|
|
8900
|
+
const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
|
|
8901
|
+
points[endpointIndex] = horizontalAntiparallelFlow ? {
|
|
8902
|
+
...endpoint,
|
|
8903
|
+
y: endpoint.y + delta
|
|
8904
|
+
} : {
|
|
8905
|
+
...endpoint,
|
|
8906
|
+
x: endpoint.x + delta
|
|
8907
|
+
};
|
|
8908
|
+
if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontalAntiparallelFlow ? {
|
|
8909
|
+
...adjacent,
|
|
8910
|
+
y: adjacent.y + delta
|
|
8911
|
+
} : {
|
|
8912
|
+
...adjacent,
|
|
8913
|
+
x: adjacent.x + delta
|
|
8914
|
+
};
|
|
8915
|
+
};
|
|
8916
|
+
shiftEndpoint(0, 1, sourceDelta);
|
|
8917
|
+
shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
|
|
8918
|
+
pointsByEdgeId.set(edge.id, points);
|
|
8919
|
+
}
|
|
8920
|
+
expandedRoutes = {
|
|
8921
|
+
...expandedRoutes,
|
|
8922
|
+
pointsByEdgeId
|
|
8923
|
+
};
|
|
8924
|
+
}
|
|
8704
8925
|
let routes = measure("long-edge-joining", () => joinLongEdgeRoutes(expandedRoutes, expanded.segmentIdsByEdgeId, edgeRouting === "SPLINES" || options.settings?.unnecessaryBendpoints === true, edgeRouting === "SPLINES", Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10)));
|
|
8705
8926
|
if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
|
|
8706
8927
|
const horizontal = direction === "right" || direction === "left";
|
|
@@ -8832,6 +9053,84 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8832
9053
|
};
|
|
8833
9054
|
routes.pointsByEdgeId.set(edge.id, points);
|
|
8834
9055
|
}
|
|
9056
|
+
for (const pair of labeledEdgesByPair.values()) {
|
|
9057
|
+
if (pair.length < 2) continue;
|
|
9058
|
+
const sourceRect = mutableRects.get(pair[0].sourceId);
|
|
9059
|
+
const targetRect = mutableRects.get(pair[0].targetId);
|
|
9060
|
+
if (!sourceRect || !targetRect) continue;
|
|
9061
|
+
const horizontal = direction === "left" || direction === "right";
|
|
9062
|
+
const beforeRect = horizontal ? sourceRect.x <= targetRect.x ? sourceRect : targetRect : sourceRect.y <= targetRect.y ? sourceRect : targetRect;
|
|
9063
|
+
const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
|
|
9064
|
+
const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
|
|
9065
|
+
let cross = horizontal ? padding.top : padding.left;
|
|
9066
|
+
const orderedPair = options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES" ? pair : [...pair].reverse();
|
|
9067
|
+
for (const edge of orderedPair) {
|
|
9068
|
+
const width = edge.width ?? 0;
|
|
9069
|
+
const height = edge.height ?? 0;
|
|
9070
|
+
parallelLabelPositions.set(edge.id, horizontal ? {
|
|
9071
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
9072
|
+
y: cross
|
|
9073
|
+
} : {
|
|
9074
|
+
x: cross,
|
|
9075
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
|
|
9076
|
+
});
|
|
9077
|
+
cross += (horizontal ? height : width) + edgeSpacing;
|
|
9078
|
+
}
|
|
9079
|
+
const anchorEdge = orderedPair[0];
|
|
9080
|
+
const anchorLabel = parallelLabelPositions.get(anchorEdge.id);
|
|
9081
|
+
const desiredCross = Math.round(horizontal ? anchorLabel.y + (anchorEdge.height ?? 0) / 2 : anchorLabel.x + (anchorEdge.width ?? 0) / 2);
|
|
9082
|
+
const alignEndpoint = (nodeId, portName) => {
|
|
9083
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
9084
|
+
const rect = mutableRects.get(nodeId);
|
|
9085
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
9086
|
+
if (!node || !rect || !port) return;
|
|
9087
|
+
const placedPort = placePorts(node.ports, rect, direction, (candidate) => input.portSettings?.(candidate, node), {
|
|
9088
|
+
...options.settings,
|
|
9089
|
+
...options.nodeSettings?.(node)
|
|
9090
|
+
}, (candidate) => getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate))?.find((candidate) => candidate.name === portName);
|
|
9091
|
+
if (!placedPort) return;
|
|
9092
|
+
const localCross = horizontal ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2 : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
|
|
9093
|
+
const currentCross = horizontal ? rect.y : rect.x;
|
|
9094
|
+
const nextCross = desiredCross - localCross;
|
|
9095
|
+
const delta = nextCross - currentCross;
|
|
9096
|
+
mutableRects.set(nodeId, horizontal ? {
|
|
9097
|
+
...rect,
|
|
9098
|
+
y: nextCross
|
|
9099
|
+
} : {
|
|
9100
|
+
...rect,
|
|
9101
|
+
x: nextCross
|
|
9102
|
+
});
|
|
9103
|
+
for (const edge of graph.edges) {
|
|
9104
|
+
const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
|
|
9105
|
+
if (points.length === 0) continue;
|
|
9106
|
+
const index = edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : void 0;
|
|
9107
|
+
if (index === void 0) continue;
|
|
9108
|
+
const endpoint = points[index];
|
|
9109
|
+
const originalCross = horizontal ? endpoint.y : endpoint.x;
|
|
9110
|
+
points[index] = horizontal ? {
|
|
9111
|
+
...endpoint,
|
|
9112
|
+
y: endpoint.y + delta
|
|
9113
|
+
} : {
|
|
9114
|
+
...endpoint,
|
|
9115
|
+
x: endpoint.x + delta
|
|
9116
|
+
};
|
|
9117
|
+
const adjacentIndex = index === 0 ? 1 : points.length - 2;
|
|
9118
|
+
const adjacent = points[adjacentIndex];
|
|
9119
|
+
if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) points[adjacentIndex] = horizontal ? {
|
|
9120
|
+
...adjacent,
|
|
9121
|
+
y: adjacent.y + delta
|
|
9122
|
+
} : {
|
|
9123
|
+
...adjacent,
|
|
9124
|
+
x: adjacent.x + delta
|
|
9125
|
+
};
|
|
9126
|
+
routes.pointsByEdgeId.set(edge.id, points);
|
|
9127
|
+
}
|
|
9128
|
+
};
|
|
9129
|
+
if (anchorEdge.sourcePort && anchorEdge.targetPort) {
|
|
9130
|
+
alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
|
|
9131
|
+
alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
8835
9134
|
const routedPortAnchors = /* @__PURE__ */ new Map();
|
|
8836
9135
|
for (const edge of graph.edges) {
|
|
8837
9136
|
const points = routes.pointsByEdgeId.get(edge.id);
|
|
@@ -8846,7 +9145,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8846
9145
|
let ports = placePorts(node.ports, rect, direction, (port) => input.portSettings?.(port, node), {
|
|
8847
9146
|
...options.settings,
|
|
8848
9147
|
...options.nodeSettings?.(node)
|
|
8849
|
-
});
|
|
9148
|
+
}, (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port));
|
|
8850
9149
|
if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") ports = ports?.map((port) => {
|
|
8851
9150
|
if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
|
|
8852
9151
|
const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
|
|
@@ -8921,8 +9220,9 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8921
9220
|
}] : [];
|
|
8922
9221
|
}).sort((left, right) => right.length - left.length)[0] : void 0;
|
|
8923
9222
|
const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
9223
|
+
const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
|
|
8924
9224
|
const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
|
|
8925
|
-
const targetPort =
|
|
9225
|
+
const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
|
|
8926
9226
|
const sourcePortSide = sourceNode && sourcePort ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"] : void 0;
|
|
8927
9227
|
const targetPortSide = sourceNode && targetPort ? options.portSettings?.(targetPort, sourceNode)?.["port.side"] : void 0;
|
|
8928
9228
|
const horizontalFeedbackTrack = edge.sourceId === edge.targetId && (sourcePortSide === "EAST" || sourcePortSide === "WEST") && targetPortSide === sourcePortSide || (horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1) ? horizontalFeedbackCandidate : void 0;
|
|
@@ -8931,8 +9231,17 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8931
9231
|
const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
8932
9232
|
const routeX = labelPlacement === "TAIL" ? firstPoint.x + labelSpacing : labelPlacement === "HEAD" ? lastPoint.x - width - labelSpacing : inlineLabel && horizontalFeedbackTrack ? (horizontalFeedbackTrack.start.x + horizontalFeedbackTrack.end.x - width) / 2 : inlineLabel && verticalFeedbackTrack ? verticalFeedbackTrack.start.x > (minimumFeedbackNodeX + maximumFeedbackNodeX) / 2 ? verticalFeedbackTrack.start.x + labelSpacing + 1 : verticalFeedbackTrack.start.x - labelSpacing - width - 1 : inlineLabel && verticalTrack ? (direction === "right" ? trackNearTarget : !trackNearTarget) ? verticalTrack.x - edgeNodeSpacing - width : verticalTrack.x + edgeNodeSpacing : inlineLabel ? horizontal ? Math.floor(midpoint.x - width / 2) : Math.ceil(midpoint.x - width / 2) : midpoint.x - width / 2;
|
|
8933
9233
|
const routeY = labelPlacement === "CENTER" && inlineLabel && horizontalFeedbackTrack ? horizontalFeedbackTrack.start.y - height / 2 - .5 : labelPlacement === "CENTER" && inlineLabel && verticalFeedbackTrack ? (verticalFeedbackTrack.start.y + verticalFeedbackTrack.end.y - height) / 2 : labelPlacement === "CENTER" && inlineLabel ? midpoint.y - height / 2 - .5 : labelPlacement === "CENTER" && placeLabelUp ? midpoint.y - height - labelSpacing - Math.round(edgeThickness / 2) : (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) + labelSpacing + Math.round(edgeThickness / 2);
|
|
8934
|
-
const
|
|
8935
|
-
const
|
|
9234
|
+
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
9235
|
+
const targetRect = placement.rectByNodeId.get(edge.targetId);
|
|
9236
|
+
const hasFlexiblePorts = (node) => {
|
|
9237
|
+
const constraints = node ? options.nodeSettings?.(node)?.portConstraints : void 0;
|
|
9238
|
+
return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
|
|
9239
|
+
};
|
|
9240
|
+
const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
|
|
9241
|
+
const [beforeFlowRect, afterFlowRect] = sourceRect && targetRect ? horizontal ? sourceRect.x <= targetRect.x ? [sourceRect, targetRect] : [targetRect, sourceRect] : sourceRect.y <= targetRect.y ? [sourceRect, targetRect] : [targetRect, sourceRect] : [void 0, void 0];
|
|
9242
|
+
const explicitLabelPosition = antiparallelLabelPositions.get(edge.id) ?? parallelLabelPositions.get(edge.id);
|
|
9243
|
+
const x = explicitLabelPosition ? explicitLabelPosition.x : flexibleFeedbackLabel ? horizontal ? (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2 : targetRect.x + (targetRect.width - width) / 2 : labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
|
|
9244
|
+
const y = explicitLabelPosition ? explicitLabelPosition.y : flexibleFeedbackLabel ? horizontal ? targetRect.y + (targetRect.height - height) / 2 : (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2 : labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
|
|
8936
9245
|
return {
|
|
8937
9246
|
...edge,
|
|
8938
9247
|
x,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-
|
|
1
|
+
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-f_3mmGHr.mjs";
|
|
2
2
|
import { getNodeSize } from "@statelyai/graph/layout";
|
|
3
3
|
|
|
4
4
|
//#region src/fixed.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/layout",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Extensible, graph-native layout algorithms for @statelyai/graph",
|
|
5
5
|
"license": "EPL-2.0 OR GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./src/elkjs/index.ts",
|
|
28
28
|
"default": "./dist/elkjs/index.mjs"
|
|
29
29
|
},
|
|
30
|
+
"./lib/elk.bundled.js": {
|
|
31
|
+
"types": "./src/elkjs/index.ts",
|
|
32
|
+
"default": "./dist/elkjs/index.mjs"
|
|
33
|
+
},
|
|
30
34
|
"./layered": {
|
|
31
35
|
"types": "./src/layered/index.ts",
|
|
32
36
|
"default": "./dist/layered/index.mjs"
|
package/src/elkjs/index.ts
CHANGED
|
@@ -824,8 +824,11 @@ function applyNodeMicroLayout(
|
|
|
824
824
|
constraints.includes("PORT_LABELS") &&
|
|
825
825
|
!constraints.includes("NODE_LABELS") &&
|
|
826
826
|
!constraints.includes("MINIMUM_SIZE");
|
|
827
|
-
|
|
828
|
-
let
|
|
827
|
+
const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
|
|
828
|
+
let width =
|
|
829
|
+
effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? (node.width ?? 0) : 0;
|
|
830
|
+
let height =
|
|
831
|
+
effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? (node.height ?? 0) : 0;
|
|
829
832
|
let insideHorizontalInset = 0;
|
|
830
833
|
let insideVerticalInset = 0;
|
|
831
834
|
const insideLabelCells = new Map<string, { width: number; height: number }>();
|
|
@@ -845,8 +848,12 @@ function applyNodeMicroLayout(
|
|
|
845
848
|
}
|
|
846
849
|
width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
|
|
847
850
|
height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
|
|
848
|
-
if (sideCounts.NORTH === 0 && sideCounts.SOUTH === 0)
|
|
849
|
-
|
|
851
|
+
if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) {
|
|
852
|
+
width = 0;
|
|
853
|
+
}
|
|
854
|
+
if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) {
|
|
855
|
+
height = 0;
|
|
856
|
+
}
|
|
850
857
|
}
|
|
851
858
|
if (constraints.includes("NODE_LABELS")) {
|
|
852
859
|
for (const label of node.labels ?? []) {
|
package/src/layered/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
breakCyclesWithModelOrderBreadthFirstSearch,
|
|
30
30
|
breakCyclesWithDepthFirstSearch,
|
|
31
31
|
getPolylineMidpoint,
|
|
32
|
+
getOrientedPortDirection,
|
|
32
33
|
minimizeCrossingsWithBarycenter,
|
|
33
34
|
minimizeCrossingsWithMedian,
|
|
34
35
|
minimizeCrossingsInteractively,
|
|
@@ -1332,6 +1333,37 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1332
1333
|
left: options.padding?.left ?? 12,
|
|
1333
1334
|
};
|
|
1334
1335
|
const switchedSideByPort = new Map<string, "NORTH" | "SOUTH" | "WEST" | "EAST">();
|
|
1336
|
+
const parallelPortIndexByKey = new Map<string, number>();
|
|
1337
|
+
const labeledEdgesByPair = new Map<string, GraphEdge[]>();
|
|
1338
|
+
for (const edge of graph.edges as GraphEdge[]) {
|
|
1339
|
+
const settings = options.edgeSettings?.(edge);
|
|
1340
|
+
if (
|
|
1341
|
+
edge.sourcePort === undefined ||
|
|
1342
|
+
edge.targetPort === undefined ||
|
|
1343
|
+
edge.sourceId === edge.targetId ||
|
|
1344
|
+
(edge.width ?? 0) <= 0 ||
|
|
1345
|
+
(edge.height ?? 0) <= 0 ||
|
|
1346
|
+
(settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
|
|
1347
|
+
settings?.["edgeLabels.inline"] !== true
|
|
1348
|
+
) {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
const key = `${edge.sourceId}\0${edge.targetId}`;
|
|
1352
|
+
const pair = labeledEdgesByPair.get(key) ?? [];
|
|
1353
|
+
pair.push(edge);
|
|
1354
|
+
labeledEdgesByPair.set(key, pair);
|
|
1355
|
+
}
|
|
1356
|
+
for (const pair of labeledEdgesByPair.values()) {
|
|
1357
|
+
if (pair.length < 2) continue;
|
|
1358
|
+
for (const [index, edge] of pair.entries()) {
|
|
1359
|
+
const portIndex =
|
|
1360
|
+
options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
|
|
1361
|
+
? index
|
|
1362
|
+
: pair.length - index - 1;
|
|
1363
|
+
parallelPortIndexByKey.set(`${edge.sourceId}\0${edge.sourcePort}`, portIndex);
|
|
1364
|
+
parallelPortIndexByKey.set(`${edge.targetId}\0${edge.targetPort}`, portIndex);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1335
1367
|
const input: LayeredPhaseInput = {
|
|
1336
1368
|
graph: graph as Graph<unknown, unknown, unknown, unknown>,
|
|
1337
1369
|
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
|
|
@@ -1352,6 +1384,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1352
1384
|
...(options.edgeSettings === undefined ? {} : { edgeSettings: options.edgeSettings }),
|
|
1353
1385
|
portSettings: (port, node) => ({
|
|
1354
1386
|
...options.portSettings?.(port, node),
|
|
1387
|
+
...(parallelPortIndexByKey.has(`${node.id}\0${port.name}`)
|
|
1388
|
+
? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) }
|
|
1389
|
+
: {}),
|
|
1355
1390
|
...(switchedSideByPort.has(`${node.id}\0${port.name}`)
|
|
1356
1391
|
? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) }
|
|
1357
1392
|
: {}),
|
|
@@ -1660,7 +1695,7 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1660
1695
|
}
|
|
1661
1696
|
}
|
|
1662
1697
|
measure("port-margin-normalization", () =>
|
|
1663
|
-
normalizePlacementForPortExtents(expanded.input, placement, order),
|
|
1698
|
+
normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation),
|
|
1664
1699
|
);
|
|
1665
1700
|
{
|
|
1666
1701
|
const horizontal = direction === "left" || direction === "right";
|
|
@@ -1720,10 +1755,257 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1720
1755
|
: edgeRouting === "SPLINES"
|
|
1721
1756
|
? routeEdgesWithSplines
|
|
1722
1757
|
: routeEdgesOrthogonally);
|
|
1723
|
-
|
|
1758
|
+
let expandedRoutes = measure("edge-routing", () =>
|
|
1724
1759
|
edgeRouter(expanded.input, expanded.orientation, placement),
|
|
1725
1760
|
);
|
|
1726
1761
|
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
1762
|
+
const antiparallelLabelPositions = new Map<string, Point>();
|
|
1763
|
+
const parallelLabelPositions = new Map<string, Point>();
|
|
1764
|
+
const postCompactionNodeCrossDeltas = new Map<string, number>();
|
|
1765
|
+
const horizontalAntiparallelFlow = direction === "left" || direction === "right";
|
|
1766
|
+
const usesVizInteractivePlacementProfile =
|
|
1767
|
+
options.strategies?.routeEdges === undefined &&
|
|
1768
|
+
options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" &&
|
|
1769
|
+
options.settings?.["layering.strategy"] === "INTERACTIVE" &&
|
|
1770
|
+
options.settings?.["crossingMinimization.forceNodeModelOrder"] === true &&
|
|
1771
|
+
options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" &&
|
|
1772
|
+
options.settings?.["nodePlacement.favorStraightEdges"] === true &&
|
|
1773
|
+
options.settings?.["compaction.postCompaction.strategy"] === "LEFT" &&
|
|
1774
|
+
options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE";
|
|
1775
|
+
if (usesVizInteractivePlacementProfile) {
|
|
1776
|
+
const neighbors = new Map(graph.nodes.map((node) => [node.id, new Set<string>()]));
|
|
1777
|
+
for (const edge of graph.edges) {
|
|
1778
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
1779
|
+
neighbors.get(edge.sourceId)?.add(edge.targetId);
|
|
1780
|
+
neighbors.get(edge.targetId)?.add(edge.sourceId);
|
|
1781
|
+
}
|
|
1782
|
+
const visited = new Set<string>();
|
|
1783
|
+
for (const node of graph.nodes) {
|
|
1784
|
+
if (visited.has(node.id)) continue;
|
|
1785
|
+
const component: GraphNode[] = [];
|
|
1786
|
+
const pending = [node.id];
|
|
1787
|
+
while (pending.length > 0) {
|
|
1788
|
+
const id = pending.pop()!;
|
|
1789
|
+
if (visited.has(id)) continue;
|
|
1790
|
+
visited.add(id);
|
|
1791
|
+
const member = graph.nodes.find((candidate) => candidate.id === id);
|
|
1792
|
+
if (member) component.push(member);
|
|
1793
|
+
for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
|
|
1794
|
+
}
|
|
1795
|
+
if (component.length < 2) continue;
|
|
1796
|
+
const componentIds = new Set(component.map((member) => member.id));
|
|
1797
|
+
if (
|
|
1798
|
+
!graph.edges.some(
|
|
1799
|
+
(edge) =>
|
|
1800
|
+
componentIds.has(edge.sourceId) &&
|
|
1801
|
+
componentIds.has(edge.targetId) &&
|
|
1802
|
+
expanded.orientation.reversedEdgeIds.has(edge.id),
|
|
1803
|
+
)
|
|
1804
|
+
) {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
const directedPairCounts = new Map<string, number>();
|
|
1808
|
+
for (const edge of graph.edges) {
|
|
1809
|
+
if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
|
|
1810
|
+
const key = `${edge.sourceId}\0${edge.targetId}`;
|
|
1811
|
+
directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
|
|
1812
|
+
}
|
|
1813
|
+
if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
|
|
1814
|
+
if (
|
|
1815
|
+
component.some((member) => {
|
|
1816
|
+
const constraints = options.nodeSettings?.(member)?.portConstraints;
|
|
1817
|
+
return constraints !== undefined && constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
1818
|
+
})
|
|
1819
|
+
) {
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
const layerCounts = new Map<number, number>();
|
|
1823
|
+
for (const member of component) {
|
|
1824
|
+
const layer = expanded.assignment.layerByNodeId.get(member.id);
|
|
1825
|
+
if (layer === undefined) continue;
|
|
1826
|
+
layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
|
|
1827
|
+
}
|
|
1828
|
+
if ([...layerCounts.values()].some((count) => count !== 1)) continue;
|
|
1829
|
+
const rects = component.flatMap((member) => {
|
|
1830
|
+
const rect = mutableRects.get(member.id);
|
|
1831
|
+
return rect ? [{ member, rect }] : [];
|
|
1832
|
+
});
|
|
1833
|
+
if (rects.length !== component.length) continue;
|
|
1834
|
+
const minimumCross = Math.min(
|
|
1835
|
+
...rects.map(({ rect }) => (horizontalAntiparallelFlow ? rect.y : rect.x)),
|
|
1836
|
+
);
|
|
1837
|
+
const maximumCross = Math.max(
|
|
1838
|
+
...rects.map(({ rect }) =>
|
|
1839
|
+
horizontalAntiparallelFlow ? rect.y + rect.height : rect.x + rect.width,
|
|
1840
|
+
),
|
|
1841
|
+
);
|
|
1842
|
+
const center = (minimumCross + maximumCross) / 2;
|
|
1843
|
+
for (const { member, rect } of rects) {
|
|
1844
|
+
const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
|
|
1845
|
+
const nextCross = horizontalAntiparallelFlow
|
|
1846
|
+
? center - rect.height / 2
|
|
1847
|
+
: center - rect.width / 2;
|
|
1848
|
+
postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
|
|
1849
|
+
mutableRects.set(
|
|
1850
|
+
member.id,
|
|
1851
|
+
horizontalAntiparallelFlow ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
{
|
|
1857
|
+
const horizontal = horizontalAntiparallelFlow;
|
|
1858
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1859
|
+
const pairEdges = new Map<string, GraphEdge[]>();
|
|
1860
|
+
for (const edge of graph.edges) {
|
|
1861
|
+
if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) {
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const key = [edge.sourceId, edge.targetId].sort().join("\0");
|
|
1865
|
+
const pair = pairEdges.get(key) ?? [];
|
|
1866
|
+
pair.push(edge);
|
|
1867
|
+
pairEdges.set(key, pair);
|
|
1868
|
+
}
|
|
1869
|
+
for (const pair of pairEdges.values()) {
|
|
1870
|
+
if (
|
|
1871
|
+
pair.length !== 2 ||
|
|
1872
|
+
pair[0]!.sourceId !== pair[1]!.targetId ||
|
|
1873
|
+
pair[0]!.targetId !== pair[1]!.sourceId ||
|
|
1874
|
+
pair.some((edge) => {
|
|
1875
|
+
const settings = options.edgeSettings?.(edge);
|
|
1876
|
+
return (
|
|
1877
|
+
(settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
|
|
1878
|
+
settings?.["edgeLabels.inline"] !== true
|
|
1879
|
+
);
|
|
1880
|
+
})
|
|
1881
|
+
) {
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const firstNode = nodesById.get(pair[0]!.sourceId);
|
|
1885
|
+
const secondNode = nodesById.get(pair[0]!.targetId);
|
|
1886
|
+
const firstRect = placement.rectByNodeId.get(pair[0]!.sourceId);
|
|
1887
|
+
const secondRect = placement.rectByNodeId.get(pair[0]!.targetId);
|
|
1888
|
+
if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
|
|
1889
|
+
const nonSelfNeighbors = (nodeId: string) =>
|
|
1890
|
+
new Set(
|
|
1891
|
+
graph.edges.flatMap((edge) =>
|
|
1892
|
+
edge.sourceId === edge.targetId
|
|
1893
|
+
? []
|
|
1894
|
+
: edge.sourceId === nodeId
|
|
1895
|
+
? [edge.targetId]
|
|
1896
|
+
: edge.targetId === nodeId
|
|
1897
|
+
? [edge.sourceId]
|
|
1898
|
+
: [],
|
|
1899
|
+
),
|
|
1900
|
+
);
|
|
1901
|
+
if (nonSelfNeighbors(firstNode.id).size !== 1 || nonSelfNeighbors(secondNode.id).size !== 1) {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
const hasFlexiblePorts = (node: GraphNode): boolean => {
|
|
1905
|
+
const constraints = options.nodeSettings?.(node)?.portConstraints;
|
|
1906
|
+
return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
|
|
1907
|
+
};
|
|
1908
|
+
if (!hasFlexiblePorts(firstNode) || !hasFlexiblePorts(secondNode)) continue;
|
|
1909
|
+
|
|
1910
|
+
const firstBeforeSecond = horizontal
|
|
1911
|
+
? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2
|
|
1912
|
+
: firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
|
|
1913
|
+
const beforeRect = firstBeforeSecond ? firstRect : secondRect;
|
|
1914
|
+
const afterRect = firstBeforeSecond ? secondRect : firstRect;
|
|
1915
|
+
const forward = pair.find((edge) =>
|
|
1916
|
+
firstBeforeSecond
|
|
1917
|
+
? edge.sourceId === pair[0]!.sourceId
|
|
1918
|
+
: edge.sourceId === pair[0]!.targetId,
|
|
1919
|
+
);
|
|
1920
|
+
if (!forward?.sourcePort || !forward.targetPort) continue;
|
|
1921
|
+
|
|
1922
|
+
let cross = Math.min(
|
|
1923
|
+
horizontal ? firstRect.y : firstRect.x,
|
|
1924
|
+
horizontal ? secondRect.y : secondRect.x,
|
|
1925
|
+
);
|
|
1926
|
+
const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
|
|
1927
|
+
for (const edge of pair) {
|
|
1928
|
+
const width = edge.width ?? 0;
|
|
1929
|
+
const height = edge.height ?? 0;
|
|
1930
|
+
antiparallelLabelPositions.set(
|
|
1931
|
+
edge.id,
|
|
1932
|
+
horizontal
|
|
1933
|
+
? {
|
|
1934
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
1935
|
+
y: cross,
|
|
1936
|
+
}
|
|
1937
|
+
: {
|
|
1938
|
+
x: cross,
|
|
1939
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
|
|
1940
|
+
},
|
|
1941
|
+
);
|
|
1942
|
+
cross += (horizontal ? height : width) + edgeSpacing;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const forwardLabel = antiparallelLabelPositions.get(forward.id)!;
|
|
1946
|
+
const forwardCross = horizontal
|
|
1947
|
+
? Math.round(forwardLabel.y + (forward.height ?? 0) / 2)
|
|
1948
|
+
: Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
|
|
1949
|
+
const alignPort = (node: GraphNode, portName: string): void => {
|
|
1950
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
1951
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
1952
|
+
if (!rect || !port) return;
|
|
1953
|
+
const placedPort = placePorts(
|
|
1954
|
+
node.ports,
|
|
1955
|
+
rect,
|
|
1956
|
+
direction,
|
|
1957
|
+
(candidate) => input.portSettings?.(candidate, node),
|
|
1958
|
+
{ ...options.settings, ...options.nodeSettings?.(node) },
|
|
1959
|
+
(candidate) =>
|
|
1960
|
+
getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
|
|
1961
|
+
)?.find((candidate) => candidate.name === portName);
|
|
1962
|
+
if (!placedPort) return;
|
|
1963
|
+
const localCross = horizontal
|
|
1964
|
+
? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
|
|
1965
|
+
: (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
|
|
1966
|
+
const nextCross = forwardCross - localCross;
|
|
1967
|
+
const currentCross = horizontal ? rect.y : rect.x;
|
|
1968
|
+
postCompactionNodeCrossDeltas.set(
|
|
1969
|
+
node.id,
|
|
1970
|
+
(postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross,
|
|
1971
|
+
);
|
|
1972
|
+
mutableRects.set(
|
|
1973
|
+
node.id,
|
|
1974
|
+
horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
|
|
1975
|
+
);
|
|
1976
|
+
};
|
|
1977
|
+
alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
|
|
1978
|
+
alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
1982
|
+
const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
|
|
1983
|
+
for (const edge of graph.edges) {
|
|
1984
|
+
const points = [...(pointsByEdgeId.get(edge.id) ?? [])];
|
|
1985
|
+
if (points.length === 0) continue;
|
|
1986
|
+
const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
|
|
1987
|
+
const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
|
|
1988
|
+
const shiftEndpoint = (endpointIndex: number, adjacentIndex: number, delta: number): void => {
|
|
1989
|
+
if (delta === 0) return;
|
|
1990
|
+
const endpoint = points[endpointIndex];
|
|
1991
|
+
const adjacent = points[adjacentIndex];
|
|
1992
|
+
if (!endpoint) return;
|
|
1993
|
+
const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
|
|
1994
|
+
points[endpointIndex] = horizontalAntiparallelFlow
|
|
1995
|
+
? { ...endpoint, y: endpoint.y + delta }
|
|
1996
|
+
: { ...endpoint, x: endpoint.x + delta };
|
|
1997
|
+
if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) {
|
|
1998
|
+
points[adjacentIndex] = horizontalAntiparallelFlow
|
|
1999
|
+
? { ...adjacent, y: adjacent.y + delta }
|
|
2000
|
+
: { ...adjacent, x: adjacent.x + delta };
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
shiftEndpoint(0, 1, sourceDelta);
|
|
2004
|
+
shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
|
|
2005
|
+
pointsByEdgeId.set(edge.id, points);
|
|
2006
|
+
}
|
|
2007
|
+
expandedRoutes = { ...expandedRoutes, pointsByEdgeId };
|
|
2008
|
+
}
|
|
1727
2009
|
let routes = measure("long-edge-joining", () =>
|
|
1728
2010
|
joinLongEdgeRoutes(
|
|
1729
2011
|
expandedRoutes,
|
|
@@ -1877,6 +2159,99 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1877
2159
|
: { ...end, y: end.y + delta };
|
|
1878
2160
|
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
|
|
1879
2161
|
}
|
|
2162
|
+
for (const pair of labeledEdgesByPair.values()) {
|
|
2163
|
+
if (pair.length < 2) continue;
|
|
2164
|
+
const sourceRect = mutableRects.get(pair[0]!.sourceId);
|
|
2165
|
+
const targetRect = mutableRects.get(pair[0]!.targetId);
|
|
2166
|
+
if (!sourceRect || !targetRect) continue;
|
|
2167
|
+
const horizontal = direction === "left" || direction === "right";
|
|
2168
|
+
const beforeRect = horizontal
|
|
2169
|
+
? sourceRect.x <= targetRect.x
|
|
2170
|
+
? sourceRect
|
|
2171
|
+
: targetRect
|
|
2172
|
+
: sourceRect.y <= targetRect.y
|
|
2173
|
+
? sourceRect
|
|
2174
|
+
: targetRect;
|
|
2175
|
+
const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
|
|
2176
|
+
const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
|
|
2177
|
+
let cross = horizontal ? padding.top : padding.left;
|
|
2178
|
+
const orderedPair =
|
|
2179
|
+
options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
|
|
2180
|
+
? pair
|
|
2181
|
+
: [...pair].reverse();
|
|
2182
|
+
for (const edge of orderedPair) {
|
|
2183
|
+
const width = edge.width ?? 0;
|
|
2184
|
+
const height = edge.height ?? 0;
|
|
2185
|
+
parallelLabelPositions.set(
|
|
2186
|
+
edge.id,
|
|
2187
|
+
horizontal
|
|
2188
|
+
? {
|
|
2189
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
2190
|
+
y: cross,
|
|
2191
|
+
}
|
|
2192
|
+
: {
|
|
2193
|
+
x: cross,
|
|
2194
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
|
|
2195
|
+
},
|
|
2196
|
+
);
|
|
2197
|
+
cross += (horizontal ? height : width) + edgeSpacing;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const anchorEdge = orderedPair[0]!;
|
|
2201
|
+
const anchorLabel = parallelLabelPositions.get(anchorEdge.id)!;
|
|
2202
|
+
const desiredCross = Math.round(
|
|
2203
|
+
horizontal
|
|
2204
|
+
? anchorLabel.y + (anchorEdge.height ?? 0) / 2
|
|
2205
|
+
: anchorLabel.x + (anchorEdge.width ?? 0) / 2,
|
|
2206
|
+
);
|
|
2207
|
+
const alignEndpoint = (nodeId: string, portName: string): void => {
|
|
2208
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
2209
|
+
const rect = mutableRects.get(nodeId);
|
|
2210
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
2211
|
+
if (!node || !rect || !port) return;
|
|
2212
|
+
const placedPort = placePorts(
|
|
2213
|
+
node.ports,
|
|
2214
|
+
rect,
|
|
2215
|
+
direction,
|
|
2216
|
+
(candidate) => input.portSettings?.(candidate, node),
|
|
2217
|
+
{ ...options.settings, ...options.nodeSettings?.(node) },
|
|
2218
|
+
(candidate) =>
|
|
2219
|
+
getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
|
|
2220
|
+
)?.find((candidate) => candidate.name === portName);
|
|
2221
|
+
if (!placedPort) return;
|
|
2222
|
+
const localCross = horizontal
|
|
2223
|
+
? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
|
|
2224
|
+
: (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
|
|
2225
|
+
const currentCross = horizontal ? rect.y : rect.x;
|
|
2226
|
+
const nextCross = desiredCross - localCross;
|
|
2227
|
+
const delta = nextCross - currentCross;
|
|
2228
|
+
mutableRects.set(nodeId, horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross });
|
|
2229
|
+
for (const edge of graph.edges) {
|
|
2230
|
+
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
2231
|
+
if (points.length === 0) continue;
|
|
2232
|
+
const index =
|
|
2233
|
+
edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : undefined;
|
|
2234
|
+
if (index === undefined) continue;
|
|
2235
|
+
const endpoint = points[index]!;
|
|
2236
|
+
const originalCross = horizontal ? endpoint.y : endpoint.x;
|
|
2237
|
+
points[index] = horizontal
|
|
2238
|
+
? { ...endpoint, y: endpoint.y + delta }
|
|
2239
|
+
: { ...endpoint, x: endpoint.x + delta };
|
|
2240
|
+
const adjacentIndex = index === 0 ? 1 : points.length - 2;
|
|
2241
|
+
const adjacent = points[adjacentIndex];
|
|
2242
|
+
if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) {
|
|
2243
|
+
points[adjacentIndex] = horizontal
|
|
2244
|
+
? { ...adjacent, y: adjacent.y + delta }
|
|
2245
|
+
: { ...adjacent, x: adjacent.x + delta };
|
|
2246
|
+
}
|
|
2247
|
+
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
if (anchorEdge.sourcePort && anchorEdge.targetPort) {
|
|
2251
|
+
alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
|
|
2252
|
+
alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
1880
2255
|
const routedPortAnchors = new Map<string, Point>();
|
|
1881
2256
|
for (const edge of graph.edges) {
|
|
1882
2257
|
const points = routes.pointsByEdgeId.get(edge.id);
|
|
@@ -1914,6 +2289,7 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1914
2289
|
direction,
|
|
1915
2290
|
(port) => input.portSettings?.(port, node),
|
|
1916
2291
|
{ ...options.settings, ...options.nodeSettings?.(node) },
|
|
2292
|
+
(port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port),
|
|
1917
2293
|
);
|
|
1918
2294
|
if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") {
|
|
1919
2295
|
ports = ports?.map((port) => {
|
|
@@ -2024,8 +2400,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2024
2400
|
.sort((left, right) => right.length - left.length)[0]
|
|
2025
2401
|
: undefined;
|
|
2026
2402
|
const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
2403
|
+
const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
|
|
2027
2404
|
const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
|
|
2028
|
-
const targetPort =
|
|
2405
|
+
const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
|
|
2029
2406
|
const sourcePortSide =
|
|
2030
2407
|
sourceNode && sourcePort
|
|
2031
2408
|
? options.portSettings?.(sourcePort, sourceNode)?.["port.side"]
|
|
@@ -2081,10 +2458,50 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2081
2458
|
: (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) +
|
|
2082
2459
|
labelSpacing +
|
|
2083
2460
|
Math.round(edgeThickness / 2);
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2461
|
+
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
2462
|
+
const targetRect = placement.rectByNodeId.get(edge.targetId);
|
|
2463
|
+
const hasFlexiblePorts = (node: GraphNode | undefined): boolean => {
|
|
2464
|
+
const constraints = node ? options.nodeSettings?.(node)?.portConstraints : undefined;
|
|
2465
|
+
return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
|
|
2466
|
+
};
|
|
2467
|
+
const flexibleFeedbackLabel =
|
|
2468
|
+
labelPlacement === "CENTER" &&
|
|
2469
|
+
inlineLabel &&
|
|
2470
|
+
expanded.orientation.reversedEdgeIds.has(edge.id) &&
|
|
2471
|
+
hasFlexiblePorts(sourceNode) &&
|
|
2472
|
+
hasFlexiblePorts(targetNode) &&
|
|
2473
|
+
sourceRect !== undefined &&
|
|
2474
|
+
targetRect !== undefined;
|
|
2475
|
+
const [beforeFlowRect, afterFlowRect] =
|
|
2476
|
+
sourceRect && targetRect
|
|
2477
|
+
? horizontal
|
|
2478
|
+
? sourceRect.x <= targetRect.x
|
|
2479
|
+
? [sourceRect, targetRect]
|
|
2480
|
+
: [targetRect, sourceRect]
|
|
2481
|
+
: sourceRect.y <= targetRect.y
|
|
2482
|
+
? [sourceRect, targetRect]
|
|
2483
|
+
: [targetRect, sourceRect]
|
|
2484
|
+
: [undefined, undefined];
|
|
2485
|
+
const explicitLabelPosition =
|
|
2486
|
+
antiparallelLabelPositions.get(edge.id) ?? parallelLabelPositions.get(edge.id);
|
|
2487
|
+
const x = explicitLabelPosition
|
|
2488
|
+
? explicitLabelPosition.x
|
|
2489
|
+
: flexibleFeedbackLabel
|
|
2490
|
+
? horizontal
|
|
2491
|
+
? (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2
|
|
2492
|
+
: targetRect.x + (targetRect.width - width) / 2
|
|
2493
|
+
: labelPlacement === "CENTER" && horizontal && labelDummyRect
|
|
2494
|
+
? labelDummyRect.x
|
|
2495
|
+
: routeX;
|
|
2496
|
+
const y = explicitLabelPosition
|
|
2497
|
+
? explicitLabelPosition.y
|
|
2498
|
+
: flexibleFeedbackLabel
|
|
2499
|
+
? horizontal
|
|
2500
|
+
? targetRect.y + (targetRect.height - height) / 2
|
|
2501
|
+
: (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2
|
|
2502
|
+
: labelPlacement === "CENTER" && !horizontal && labelDummyRect
|
|
2503
|
+
? labelDummyRect.y
|
|
2504
|
+
: routeY;
|
|
2088
2505
|
return {
|
|
2089
2506
|
...edge,
|
|
2090
2507
|
x,
|
|
@@ -71,14 +71,15 @@ export function splitLongEdges(
|
|
|
71
71
|
source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : undefined;
|
|
72
72
|
const targetSide =
|
|
73
73
|
target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : undefined;
|
|
74
|
+
const hasFixedPortSide = (node: GraphNode | undefined): boolean => {
|
|
75
|
+
if (!node) return false;
|
|
76
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
77
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
78
|
+
};
|
|
74
79
|
const fixedSideFeedback =
|
|
75
80
|
sourceLayer > targetLayer &&
|
|
76
|
-
((source
|
|
77
|
-
|
|
78
|
-
sourceSide === forwardSourceSide) ||
|
|
79
|
-
(target !== undefined &&
|
|
80
|
-
input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" &&
|
|
81
|
-
targetSide === forwardTargetSide));
|
|
81
|
+
((hasFixedPortSide(source) && sourceSide === forwardSourceSide) ||
|
|
82
|
+
(hasFixedPortSide(target) && targetSide === forwardTargetSide));
|
|
82
83
|
if (
|
|
83
84
|
span <= 1 ||
|
|
84
85
|
edge.sourceId === edge.targetId ||
|
|
@@ -30,6 +30,37 @@ function getOrientedEndpoints(
|
|
|
30
30
|
: [edge.sourceId, edge.targetId];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function getOrientedPortDirection(
|
|
34
|
+
input: LayeredPhaseInput,
|
|
35
|
+
orientation: AcyclicOrientation,
|
|
36
|
+
node: GraphNode,
|
|
37
|
+
port: GraphPort,
|
|
38
|
+
): GraphPort["direction"] {
|
|
39
|
+
const configuredSide = input.portSettings?.(port, node)?.["port.side"];
|
|
40
|
+
if (
|
|
41
|
+
configuredSide !== "NORTH" &&
|
|
42
|
+
configuredSide !== "SOUTH" &&
|
|
43
|
+
configuredSide !== "WEST" &&
|
|
44
|
+
configuredSide !== "EAST"
|
|
45
|
+
) {
|
|
46
|
+
return port.direction;
|
|
47
|
+
}
|
|
48
|
+
let incoming = false;
|
|
49
|
+
let outgoing = false;
|
|
50
|
+
for (const edge of input.graph.edges) {
|
|
51
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
52
|
+
if (edge.sourceId === node.id && edge.sourcePort === port.name) {
|
|
53
|
+
if (reversed) incoming = true;
|
|
54
|
+
else outgoing = true;
|
|
55
|
+
}
|
|
56
|
+
if (edge.targetId === node.id && edge.targetPort === port.name) {
|
|
57
|
+
if (reversed) outgoing = true;
|
|
58
|
+
else incoming = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return incoming && outgoing ? "inout" : outgoing ? "out" : incoming ? "in" : port.direction;
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
function cycleModelOrder(input: LayeredPhaseInput): Map<string, number> {
|
|
34
65
|
const enforced =
|
|
35
66
|
input.settings["considerModelOrder.groupModelOrder.cbGroupOrderStrategy"] === "ENFORCED";
|
|
@@ -4312,10 +4343,15 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4312
4343
|
source.id === target.id &&
|
|
4313
4344
|
feedbackSourcePortSide !== undefined &&
|
|
4314
4345
|
feedbackSourcePortSide === feedbackTargetPortSide;
|
|
4346
|
+
const hasFixedPortSide = (node: GraphNode): boolean => {
|
|
4347
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4348
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4349
|
+
};
|
|
4315
4350
|
const fixedSideFeedback =
|
|
4316
4351
|
style === "ORTHOGONAL" &&
|
|
4317
4352
|
!sameSideSelfLoop &&
|
|
4318
|
-
(feedbackSourcePortSide === sourceAwaySide ||
|
|
4353
|
+
((hasFixedPortSide(source) && feedbackSourcePortSide === sourceAwaySide) ||
|
|
4354
|
+
(hasFixedPortSide(target) && feedbackTargetPortSide === targetAwaySide));
|
|
4319
4355
|
if ((input.settings.feedbackEdges === true && reversedEdge) || fixedSideFeedback) {
|
|
4320
4356
|
if (fixedSideFeedback) outsideFeedbackEdgeIds.add(edge.id);
|
|
4321
4357
|
const crossSpacing = Number(input.settings["spacing.edgeNode"] ?? 10);
|
|
@@ -4769,12 +4805,32 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4769
4805
|
(candidate) =>
|
|
4770
4806
|
candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort,
|
|
4771
4807
|
).length === 1;
|
|
4772
|
-
const
|
|
4808
|
+
const flexibleFeedback =
|
|
4809
|
+
reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
|
|
4810
|
+
const start = flexibleFeedback
|
|
4773
4811
|
? sourceFallback
|
|
4774
|
-
:
|
|
4775
|
-
|
|
4812
|
+
: sourceFixedSide
|
|
4813
|
+
? sourceFallback
|
|
4814
|
+
: getPortPoint(
|
|
4815
|
+
source,
|
|
4816
|
+
edge.sourcePort,
|
|
4817
|
+
sourceRect,
|
|
4818
|
+
sourceFallback,
|
|
4819
|
+
input.direction,
|
|
4820
|
+
input,
|
|
4821
|
+
);
|
|
4822
|
+
const end = flexibleFeedback
|
|
4776
4823
|
? targetFallback
|
|
4777
|
-
:
|
|
4824
|
+
: targetFixedSide
|
|
4825
|
+
? targetFallback
|
|
4826
|
+
: getPortPoint(
|
|
4827
|
+
target,
|
|
4828
|
+
edge.targetPort,
|
|
4829
|
+
targetRect,
|
|
4830
|
+
targetFallback,
|
|
4831
|
+
input.direction,
|
|
4832
|
+
input,
|
|
4833
|
+
);
|
|
4778
4834
|
const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
|
|
4779
4835
|
const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
|
|
4780
4836
|
const earlierLayer = Math.min(sourceLayer, targetLayer);
|
|
@@ -4955,6 +5011,7 @@ export function placePorts<P>(
|
|
|
4955
5011
|
direction: LayeredPhaseInput["direction"],
|
|
4956
5012
|
portSettings?: (port: GraphPort<P>) => ElkLayeredOptionValueByName | undefined,
|
|
4957
5013
|
nodeSettings?: ElkLayeredOptionValueByName,
|
|
5014
|
+
portDirection?: (port: GraphPort<P>) => GraphPort<P>["direction"],
|
|
4958
5015
|
): GraphPort<P>[] | undefined {
|
|
4959
5016
|
if (!ports) return undefined;
|
|
4960
5017
|
const horizontal = direction === "left" || direction === "right";
|
|
@@ -4981,7 +5038,7 @@ export function placePorts<P>(
|
|
|
4981
5038
|
sideByPort.set(port, configured);
|
|
4982
5039
|
continue;
|
|
4983
5040
|
}
|
|
4984
|
-
const outgoing = port.direction === "out";
|
|
5041
|
+
const outgoing = (portDirection?.(port) ?? port.direction) === "out";
|
|
4985
5042
|
const normalFarSide = reverse ? !outgoing : outgoing;
|
|
4986
5043
|
const farSide = sideFixed && configured === "UNDEFINED" ? !normalFarSide : normalFarSide;
|
|
4987
5044
|
sideByPort.set(port, horizontal ? (farSide ? "EAST" : "WEST") : farSide ? "SOUTH" : "NORTH");
|
|
@@ -5221,6 +5278,7 @@ export function normalizePlacementForPortExtents(
|
|
|
5221
5278
|
input: LayeredPhaseInput,
|
|
5222
5279
|
placement: NodePlacement,
|
|
5223
5280
|
order: LayerOrder,
|
|
5281
|
+
orientation: AcyclicOrientation,
|
|
5224
5282
|
): void {
|
|
5225
5283
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
5226
5284
|
const physicalLayers = order.layers
|
|
@@ -5267,6 +5325,7 @@ export function normalizePlacementForPortExtents(
|
|
|
5267
5325
|
input.direction,
|
|
5268
5326
|
(port) => input.portSettings?.(port, node),
|
|
5269
5327
|
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
5328
|
+
(port) => getOrientedPortDirection(input, orientation, node, port),
|
|
5270
5329
|
);
|
|
5271
5330
|
for (const port of ports ?? []) {
|
|
5272
5331
|
const extent = horizontal
|
|
@@ -5297,6 +5356,7 @@ export function normalizePlacementForPortExtents(
|
|
|
5297
5356
|
input.direction,
|
|
5298
5357
|
(port) => input.portSettings?.(port, node),
|
|
5299
5358
|
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
5359
|
+
(port) => getOrientedPortDirection(input, orientation, node, port),
|
|
5300
5360
|
);
|
|
5301
5361
|
for (const port of ports ?? []) {
|
|
5302
5362
|
minimumX = Math.min(minimumX, rect.x + (port.x ?? 0));
|