@statelyai/layout 0.3.0 → 0.4.0
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/NOTICE.md +4 -2
- package/README.md +150 -11
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +2 -0
- package/dist/elkjs/index.d.mts +2 -0
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
- package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
- package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
- package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
- package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +2 -1
- package/dist/routing/index.d.cts +2 -2
- package/dist/routing/index.d.mts +2 -2
- package/dist/routing/index.mjs +2 -2
- package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
- package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
- package/package.json +8 -2
- package/src/authoring/coordinates.ts +28 -8
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1392 -449
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +2 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +35 -7
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +886 -161
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +13 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +32 -0
- package/src/routing/algorithms.ts +433 -21
- package/src/routing/coordination.ts +8 -3
- package/src/routing/index.ts +1 -0
- package/src/routing/search.ts +93 -11
- package/dist/elkjs-DaHMISnQ.mjs +0 -1677
- package/dist/elkjs-dMghiiWW.cjs +0 -1706
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sortInitialModelOrder,
|
|
3
|
+
restoreInitialModelOrder,
|
|
4
|
+
countModelOrderChanges,
|
|
5
|
+
normalModelComparator,
|
|
6
|
+
transitiveComparator,
|
|
7
|
+
} from "./initial-model-order";
|
|
8
|
+
import { insertionSort } from "./gwt-sort";
|
|
9
|
+
import { CanonicalPortDistributor } from "./port-distributor";
|
|
10
|
+
import { countAllCrossings } from "./crossing-counter";
|
|
11
|
+
import {
|
|
12
|
+
createCrossingGraphBuilder,
|
|
13
|
+
crossingGraph,
|
|
14
|
+
splitMixedCrossingPorts,
|
|
15
|
+
} from "./crossing-graph";
|
|
16
|
+
import { isMergedHyperedgeDummy } from "./hyperedge-dummy-merger";
|
|
17
|
+
import { externalPortDummyOf } from "./external-port-dummy";
|
|
18
|
+
import { routingCoordinates } from "./routing-coordinates";
|
|
19
|
+
import {
|
|
20
|
+
hasMovableLoopPorts,
|
|
21
|
+
hasPlacementLoopEnvelope,
|
|
22
|
+
loopEnvelopes,
|
|
23
|
+
preparedLoopEnvelopes,
|
|
24
|
+
} from "./loop-envelopes";
|
|
25
|
+
import { routeFixedSelfLoop, fixedSelfLoopSide, selfLoopTracks } from "./fixed-self-loop";
|
|
26
|
+
import { networkSimplexComponents } from "./network-simplex";
|
|
27
|
+
import { minimizeHierarchyCrossings } from "./hierarchy-crossing";
|
|
28
|
+
import { orthogonalJunctionPoints, type OrthogonalJunctionGroup } from "./orthogonal-junctions";
|
|
29
|
+
import { routeOrthogonalSegments } from "./orthogonal-segments";
|
|
30
|
+
import { createOrthogonalHypersegments, type OrthogonalPort } from "./orthogonal-hypersegments";
|
|
31
|
+
import {
|
|
32
|
+
associatedBarycenters,
|
|
33
|
+
resolveCrossingConstraints,
|
|
34
|
+
getCrossingUnits,
|
|
35
|
+
canSwapCrossingUnits,
|
|
36
|
+
type CrossingUnits,
|
|
37
|
+
} from "./crossing-constraints";
|
|
38
|
+
import { applyGroupedEdgeLengthCompaction } from "./grouped-compaction";
|
|
39
|
+
import { getPlacementOrientation, getPlacementOrder } from "./placement-orientation";
|
|
40
|
+
import { recordCycleRandom, crossingRandom, phaseRandomByInput } from "./cycle-random";
|
|
1
41
|
import type { EntityRect, GraphEdge, GraphNode, GraphPort, Point } from "@statelyai/graph";
|
|
2
42
|
import { LayoutError } from "../errors";
|
|
3
43
|
import { JavaRandom } from "../java-random";
|
|
@@ -15,11 +55,16 @@ import type {
|
|
|
15
55
|
NodePlacer,
|
|
16
56
|
} from "./types";
|
|
17
57
|
|
|
18
|
-
const phaseRandomByInput = new WeakMap<LayeredPhaseInput, JavaRandom>();
|
|
19
58
|
import { nodeNodeSpacing } from "./spacing";
|
|
20
59
|
import type { ElkLayeredOptionValueByName } from "./elk-options";
|
|
21
60
|
import { conservativeSpline } from "./spline-bezier";
|
|
22
61
|
import { getFlexiblePortPosition } from "./flexible-ports";
|
|
62
|
+
import { traceLayeredPhase } from "../internal/layered-trace";
|
|
63
|
+
import { assertNorthSouthPortSides, GreedySwitchDecider } from "./greedy-switch-decider";
|
|
64
|
+
|
|
65
|
+
const modelSweepInputs = new WeakSet<LayeredPhaseInput>();
|
|
66
|
+
|
|
67
|
+
const nodeRelativePortRanksByInput = new WeakMap<LayeredPhaseInput, boolean>();
|
|
23
68
|
|
|
24
69
|
function getOrientedEndpoints(
|
|
25
70
|
edge: GraphEdge,
|
|
@@ -243,6 +288,7 @@ function breakCyclesWithGreedyHeuristic(
|
|
|
243
288
|
if (source === undefined || target === undefined) continue;
|
|
244
289
|
if ((marks[source] ?? 0) > (marks[target] ?? 0)) reversedEdgeIds.add(edge.id);
|
|
245
290
|
}
|
|
291
|
+
recordCycleRandom(input, random);
|
|
246
292
|
return { reversedEdgeIds };
|
|
247
293
|
}
|
|
248
294
|
|
|
@@ -252,9 +298,39 @@ export const breakCyclesGreedily: CycleBreaker = (input) =>
|
|
|
252
298
|
export const breakCyclesGreedilyByModelOrder: CycleBreaker = (input) =>
|
|
253
299
|
breakCyclesWithGreedyHeuristic(input, "model-order");
|
|
254
300
|
|
|
255
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* A zero-size port on a FIXED_SIDE node used by exactly one edge has no
|
|
303
|
+
* physical extent, so its terminal takes a slot in its side's shared endpoint order.
|
|
304
|
+
*/
|
|
305
|
+
export function usesSharedSideSlot(
|
|
306
|
+
input: LayeredPhaseInput,
|
|
307
|
+
node: GraphNode,
|
|
308
|
+
portName: string | undefined,
|
|
309
|
+
): boolean {
|
|
310
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
311
|
+
return (
|
|
312
|
+
port !== undefined &&
|
|
313
|
+
(port.width ?? 8) === 0 &&
|
|
314
|
+
(port.height ?? 8) === 0 &&
|
|
315
|
+
input.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE" &&
|
|
316
|
+
input.graph.edges.filter(
|
|
317
|
+
(edge) =>
|
|
318
|
+
(edge.sourceId === node.id && edge.sourcePort === portName) ||
|
|
319
|
+
(edge.targetId === node.id && edge.targetPort === portName),
|
|
320
|
+
).length === 1
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** ELK MODEL_ORDER: layer-constrained nodes (e.g. external port dummies) order before/after the rest. */
|
|
256
325
|
export const breakCyclesByModelOrder: CycleBreaker = (input) => {
|
|
257
326
|
const order = cycleModelOrder(input);
|
|
327
|
+
const offset = Math.max(1, input.graph.nodes.length);
|
|
328
|
+
const shift = { FIRST_SEPARATE: -2, FIRST: -1, LAST: 1, LAST_SEPARATE: 2 } as const;
|
|
329
|
+
for (const node of input.graph.nodes) {
|
|
330
|
+
const constraint = input.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
331
|
+
const factor = shift[constraint as keyof typeof shift];
|
|
332
|
+
if (factor) order.set(node.id, factor * offset + order.get(node.id)!);
|
|
333
|
+
}
|
|
258
334
|
return {
|
|
259
335
|
reversedEdgeIds: new Set(
|
|
260
336
|
input.graph.edges
|
|
@@ -668,83 +744,6 @@ export function applyLayerConstraints(
|
|
|
668
744
|
};
|
|
669
745
|
}
|
|
670
746
|
|
|
671
|
-
/** Orient constrained outer nodes toward the graph interior before layering. */
|
|
672
|
-
export function applyLayerConstraintOrientation(
|
|
673
|
-
input: LayeredPhaseInput,
|
|
674
|
-
orientation: AcyclicOrientation,
|
|
675
|
-
): AcyclicOrientation {
|
|
676
|
-
if (
|
|
677
|
-
input.nodeSettings === undefined &&
|
|
678
|
-
!input.graph.nodes.some((node) => (node.ports?.length ?? 0) > 0)
|
|
679
|
-
) {
|
|
680
|
-
return orientation;
|
|
681
|
-
}
|
|
682
|
-
const reversedEdgeIds = new Set(orientation.reversedEdgeIds);
|
|
683
|
-
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
684
|
-
const constraintByNodeId = new Map(
|
|
685
|
-
input.graph.nodes.map((node) => [
|
|
686
|
-
node.id,
|
|
687
|
-
input.nodeSettings?.(node)?.["layering.layerConstraint"],
|
|
688
|
-
]),
|
|
689
|
-
);
|
|
690
|
-
for (const edge of input.graph.edges) {
|
|
691
|
-
const reversed = reversedEdgeIds.has(edge.id);
|
|
692
|
-
const sourceId = reversed ? edge.targetId : edge.sourceId;
|
|
693
|
-
const targetId = reversed ? edge.sourceId : edge.targetId;
|
|
694
|
-
const sourceConstraint = constraintByNodeId.get(sourceId);
|
|
695
|
-
const targetConstraint = constraintByNodeId.get(targetId);
|
|
696
|
-
const shouldReverse =
|
|
697
|
-
targetConstraint === "FIRST" ||
|
|
698
|
-
targetConstraint === "FIRST_SEPARATE" ||
|
|
699
|
-
sourceConstraint === "LAST" ||
|
|
700
|
-
sourceConstraint === "LAST_SEPARATE";
|
|
701
|
-
if (shouldReverse) {
|
|
702
|
-
if (reversed) reversedEdgeIds.delete(edge.id);
|
|
703
|
-
else reversedEdgeIds.add(edge.id);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const oppositeFlowSide =
|
|
707
|
-
input.direction === "right"
|
|
708
|
-
? "WEST"
|
|
709
|
-
: input.direction === "left"
|
|
710
|
-
? "EAST"
|
|
711
|
-
: input.direction === "down"
|
|
712
|
-
? "NORTH"
|
|
713
|
-
: "SOUTH";
|
|
714
|
-
const forwardFlowSide =
|
|
715
|
-
input.direction === "right"
|
|
716
|
-
? "EAST"
|
|
717
|
-
: input.direction === "left"
|
|
718
|
-
? "WEST"
|
|
719
|
-
: input.direction === "down"
|
|
720
|
-
? "SOUTH"
|
|
721
|
-
: "NORTH";
|
|
722
|
-
for (const edge of input.graph.edges) {
|
|
723
|
-
const source = nodeById.get(edge.sourceId);
|
|
724
|
-
const target = nodeById.get(edge.targetId);
|
|
725
|
-
const sourcePort = source?.ports?.find((port) => port.name === edge.sourcePort);
|
|
726
|
-
const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
|
|
727
|
-
const sourceSide = sourcePort
|
|
728
|
-
? input.portSettings?.(sourcePort, source!)?.["port.side"]
|
|
729
|
-
: undefined;
|
|
730
|
-
const targetSide = targetPort
|
|
731
|
-
? input.portSettings?.(targetPort, target!)?.["port.side"]
|
|
732
|
-
: undefined;
|
|
733
|
-
const reverseForPort =
|
|
734
|
-
sourceSide === oppositeFlowSide ||
|
|
735
|
-
(sourceSide === "UNDEFINED" &&
|
|
736
|
-
input.nodeSettings?.(source!)?.portConstraints === "FIXED_SIDE") ||
|
|
737
|
-
targetSide === forwardFlowSide ||
|
|
738
|
-
(targetSide === "UNDEFINED" &&
|
|
739
|
-
input.nodeSettings?.(target!)?.portConstraints === "FIXED_SIDE");
|
|
740
|
-
if (!reverseForPort) continue;
|
|
741
|
-
if (reversedEdgeIds.has(edge.id)) reversedEdgeIds.delete(edge.id);
|
|
742
|
-
else reversedEdgeIds.add(edge.id);
|
|
743
|
-
}
|
|
744
|
-
// Port-side preferences can reintroduce cycles after the cycle breaker.
|
|
745
|
-
return { reversedEdgeIds: addDepthFirstBackEdges(input, reversedEdgeIds, "model") };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
747
|
/** Keep activated ELK partitions in ascending, contiguous layer blocks. */
|
|
749
748
|
export function applyPartitions(
|
|
750
749
|
input: LayeredPhaseInput,
|
|
@@ -911,56 +910,112 @@ export function applyGreedySwitch(
|
|
|
911
910
|
): LayerOrder {
|
|
912
911
|
if (input.settings["crossingMinimization.strategy"] === "INTERACTIVE") return order;
|
|
913
912
|
if (input.settings["crossingMinimization.semiInteractive"] === true) return order;
|
|
913
|
+
const type = input.settings["crossingMinimization.greedySwitch.type"] ?? "TWO_SIDED";
|
|
914
|
+
const threshold = input.settings["crossingMinimization.greedySwitch.activationThreshold"] ?? 40;
|
|
915
|
+
const originalNodeCount = input.graph.nodes.filter(
|
|
916
|
+
(node) => !node.id.startsWith("__layout_dummy:"),
|
|
917
|
+
).length;
|
|
918
|
+
if (type === "OFF" || (threshold !== 0 && threshold <= originalNodeCount)) return order;
|
|
919
|
+
if (order.layers.length === 0 || (order.layers.length === 1 && order.layers[0]?.length === 1))
|
|
920
|
+
return order;
|
|
921
|
+
const random = phaseRandomByInput.get(input) ?? crossingRandom(input);
|
|
922
|
+
// ELK initializes another layer-sweep processor for greedy switching. It
|
|
923
|
+
// consumes a saved seed, chooses a distributor for ONE_SIDED, then draws
|
|
924
|
+
// the sweep direction. Routing continues with this same graph RNG state.
|
|
925
|
+
random.nextLong();
|
|
926
|
+
if (type === "ONE_SIDED") random.nextBoolean();
|
|
927
|
+
let forward = random.nextBoolean();
|
|
928
|
+
phaseRandomByInput.set(input, random);
|
|
914
929
|
const modelOrderStrategy = input.settings["considerModelOrder.strategy"] ?? "NONE";
|
|
915
930
|
if (
|
|
916
931
|
(modelOrderStrategy === "NODES_AND_EDGES" || modelOrderStrategy === "PREFER_NODES") &&
|
|
917
|
-
(input.settings["
|
|
918
|
-
Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0) >= 1)
|
|
932
|
+
Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0) >= 1
|
|
919
933
|
) {
|
|
920
934
|
return order;
|
|
921
935
|
}
|
|
922
|
-
const type = input.settings["crossingMinimization.greedySwitch.type"] ?? "TWO_SIDED";
|
|
923
|
-
const threshold = input.settings["crossingMinimization.greedySwitch.activationThreshold"] ?? 40;
|
|
924
|
-
if (type === "OFF" || (threshold !== 0 && threshold <= input.graph.nodes.length)) {
|
|
925
|
-
return order;
|
|
926
|
-
}
|
|
927
936
|
const layers = order.layers.map((layer) => [...layer]);
|
|
928
937
|
const layerByNodeId = new Map<string, number>();
|
|
929
938
|
for (const [layerIndex, layer] of layers.entries()) {
|
|
930
939
|
for (const id of layer) layerByNodeId.set(id, layerIndex);
|
|
931
940
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
941
|
+
const greedyGraph =
|
|
942
|
+
order.inputPortOrderByNodeId && order.outputPortOrderByNodeId
|
|
943
|
+
? crossingGraph(
|
|
944
|
+
input,
|
|
945
|
+
orientation,
|
|
946
|
+
layers,
|
|
947
|
+
order.inputPortOrderByNodeId,
|
|
948
|
+
order.outputPortOrderByNodeId,
|
|
949
|
+
)
|
|
950
|
+
: undefined;
|
|
951
|
+
const greedyNodes = new Map(greedyGraph?.layers.flat().map((node) => [node.id, node]));
|
|
952
|
+
if (greedyGraph) assertNorthSouthPortSides(greedyGraph);
|
|
953
|
+
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
954
|
+
const portPositions = (layer: readonly string[], incoming: boolean) => {
|
|
955
|
+
const positions = new Map<string, number>();
|
|
956
|
+
let consumed = 0;
|
|
957
|
+
for (const nodeId of layer) {
|
|
958
|
+
const selected = (
|
|
959
|
+
incoming ? order.inputPortOrderByNodeId : order.outputPortOrderByNodeId
|
|
960
|
+
)?.get(nodeId);
|
|
961
|
+
const edgeIds =
|
|
962
|
+
selected ??
|
|
963
|
+
input.graph.edges
|
|
964
|
+
.filter((edge) => {
|
|
965
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
966
|
+
return source !== target && (incoming ? target : source) === nodeId;
|
|
967
|
+
})
|
|
968
|
+
.map((edge) => edge.id);
|
|
969
|
+
// WEST ports occur clockwise bottom-to-top; greedy crossing counters
|
|
970
|
+
// enumerate both neighboring faces in physical top-to-bottom order.
|
|
971
|
+
const physical = incoming ? [...edgeIds].reverse() : edgeIds;
|
|
972
|
+
const shared = new Map<string, number>();
|
|
973
|
+
for (const edgeId of physical) {
|
|
974
|
+
const edge = edgeById.get(edgeId)!;
|
|
975
|
+
const reversed = orientation.reversedEdgeIds.has(edgeId);
|
|
976
|
+
const port = incoming !== reversed ? edge.targetPort : edge.sourcePort;
|
|
977
|
+
if (port !== undefined && shared.has(port)) {
|
|
978
|
+
positions.set(edgeId, shared.get(port)!);
|
|
979
|
+
} else {
|
|
980
|
+
positions.set(edgeId, consumed);
|
|
981
|
+
if (port !== undefined) shared.set(port, consumed);
|
|
982
|
+
consumed++;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Preserve the old node-level fallback for custom minimizers that do
|
|
986
|
+
// not provide selected port orders.
|
|
987
|
+
if (!selected) {
|
|
988
|
+
for (const edgeId of edgeIds) positions.set(edgeId, consumed);
|
|
989
|
+
consumed++;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return positions;
|
|
993
|
+
};
|
|
940
994
|
const countBoundary = (boundary: number): number => {
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
);
|
|
995
|
+
const sourcePositions = portPositions(layers[boundary] ?? [], false);
|
|
996
|
+
const targetPositions = portPositions(layers[boundary + 1] ?? [], true);
|
|
997
|
+
const edges = input.graph.edges.filter((edge) => {
|
|
998
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
999
|
+
return layerByNodeId.get(source) === boundary && layerByNodeId.get(target) === boundary + 1;
|
|
1000
|
+
});
|
|
948
1001
|
let crossings = 0;
|
|
949
1002
|
for (let left = 0; left < edges.length; left++) {
|
|
950
|
-
const leftEdge = edges[left]!;
|
|
951
1003
|
for (let right = left + 1; right < edges.length; right++) {
|
|
952
|
-
const rightEdge = edges[right]!;
|
|
953
|
-
if (leftEdge[0] === rightEdge[0] || leftEdge[1] === rightEdge[1]) continue;
|
|
954
1004
|
const sourceDifference =
|
|
955
|
-
|
|
1005
|
+
sourcePositions.get(edges[left]!.id)! - sourcePositions.get(edges[right]!.id)!;
|
|
956
1006
|
const targetDifference =
|
|
957
|
-
|
|
1007
|
+
targetPositions.get(edges[left]!.id)! - targetPositions.get(edges[right]!.id)!;
|
|
958
1008
|
if (sourceDifference * targetDifference < 0) crossings++;
|
|
959
1009
|
}
|
|
960
1010
|
}
|
|
961
1011
|
return crossings;
|
|
962
1012
|
};
|
|
963
1013
|
const countForLayer = (layerIndex: number, oneSidedBoundary?: number) => {
|
|
1014
|
+
// Greedy swapping counts edge pairs, rather than the sweep's hyperedge estimate.
|
|
1015
|
+
if (oneSidedBoundary === undefined && greedyGraph) {
|
|
1016
|
+
greedyGraph.layers = layers.map((layer) => layer.map((id) => greedyNodes.get(id)!));
|
|
1017
|
+
return countAllCrossings(greedyGraph, "edges").total;
|
|
1018
|
+
}
|
|
964
1019
|
if (oneSidedBoundary !== undefined) return countBoundary(oneSidedBoundary);
|
|
965
1020
|
return (
|
|
966
1021
|
(layerIndex > 0 ? countBoundary(layerIndex - 1) : 0) +
|
|
@@ -975,6 +1030,31 @@ export function applyGreedySwitch(
|
|
|
975
1030
|
for (const layerIndex of indices) {
|
|
976
1031
|
const layer = layers[layerIndex]!;
|
|
977
1032
|
let improved: boolean;
|
|
1033
|
+
if (greedyGraph) {
|
|
1034
|
+
// ELK decides each switch from local two-node crossing counts.
|
|
1035
|
+
const units = getCrossingUnits(input);
|
|
1036
|
+
greedyGraph.layers = layers.map((ids) => ids.map((id) => greedyNodes.get(id)!));
|
|
1037
|
+
const decider = new GreedySwitchDecider(
|
|
1038
|
+
greedyGraph,
|
|
1039
|
+
layerIndex,
|
|
1040
|
+
type === "ONE_SIDED" ? (forward ? "WEST" : "EAST") : undefined,
|
|
1041
|
+
);
|
|
1042
|
+
do {
|
|
1043
|
+
improved = false;
|
|
1044
|
+
for (let index = 0; index + 1 < layer.length; index++) {
|
|
1045
|
+
const upper = greedyNodes.get(layer[index]!)!,
|
|
1046
|
+
lower = greedyNodes.get(layer[index + 1]!)!;
|
|
1047
|
+
if (decider.preventsSwitch(upper, lower, units?.successors ?? new Map())) continue;
|
|
1048
|
+
const [current, switched] = decider.crossings(upper, lower);
|
|
1049
|
+
if (current <= switched) continue;
|
|
1050
|
+
decider.notifySwitch(upper, lower);
|
|
1051
|
+
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
1052
|
+
improved = true;
|
|
1053
|
+
changed = true;
|
|
1054
|
+
}
|
|
1055
|
+
} while (improved && layerIndex !== indices[0]);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
978
1058
|
do {
|
|
979
1059
|
improved = false;
|
|
980
1060
|
for (let index = 0; index + 1 < layer.length; index++) {
|
|
@@ -986,6 +1066,8 @@ export function applyGreedySwitch(
|
|
|
986
1066
|
) {
|
|
987
1067
|
continue;
|
|
988
1068
|
}
|
|
1069
|
+
const units = getCrossingUnits(input);
|
|
1070
|
+
if (units && !canSwapCrossingUnits(layer[index]!, layer[index + 1]!, units)) continue;
|
|
989
1071
|
const before = countForLayer(layerIndex, oneSidedBoundary);
|
|
990
1072
|
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
991
1073
|
const after = countForLayer(layerIndex, oneSidedBoundary);
|
|
@@ -996,13 +1078,56 @@ export function applyGreedySwitch(
|
|
|
996
1078
|
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
997
1079
|
}
|
|
998
1080
|
}
|
|
999
|
-
|
|
1081
|
+
// ELK scans the starting layer once, then converges each later layer.
|
|
1082
|
+
// Repeating the start immediately changes the following layers' local optimum.
|
|
1083
|
+
} while (improved && layerIndex !== indices[0]);
|
|
1000
1084
|
}
|
|
1001
1085
|
return changed;
|
|
1002
1086
|
};
|
|
1003
|
-
|
|
1004
|
-
|
|
1087
|
+
const trace = (crossings: number) =>
|
|
1088
|
+
traceLayeredPhase(() => ({
|
|
1089
|
+
kind: "sweep",
|
|
1090
|
+
scope: input.graph.id,
|
|
1091
|
+
attempt: -1,
|
|
1092
|
+
forward,
|
|
1093
|
+
firstSweep: false,
|
|
1094
|
+
crossings,
|
|
1095
|
+
layers,
|
|
1096
|
+
}));
|
|
1097
|
+
if (type === "ONE_SIDED") {
|
|
1098
|
+
// ONE_SIDED does not always improve (opposite sweeps can undo each other),
|
|
1099
|
+
// so ELK's minimizeCrossingsWithCounter alternates sweeps only while the
|
|
1100
|
+
// total crossing count strictly drops, keeping the order before the last sweep.
|
|
1101
|
+
const countAll = () => {
|
|
1102
|
+
if (greedyGraph) return countForLayer(0);
|
|
1103
|
+
let total = 0;
|
|
1104
|
+
for (let boundary = 0; boundary + 1 < layers.length; boundary++)
|
|
1105
|
+
total += countBoundary(boundary);
|
|
1106
|
+
return total;
|
|
1107
|
+
};
|
|
1108
|
+
sweep(forward);
|
|
1109
|
+
let crossings = countAll();
|
|
1110
|
+
trace(crossings);
|
|
1111
|
+
while (crossings > 0) {
|
|
1112
|
+
const previous = layers.map((layer) => [...layer]);
|
|
1113
|
+
forward = !forward;
|
|
1114
|
+
sweep(forward);
|
|
1115
|
+
const next = countAll();
|
|
1116
|
+
trace(next);
|
|
1117
|
+
if (next >= crossings) {
|
|
1118
|
+
for (const [index, layer] of previous.entries()) layers[index] = layer;
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
crossings = next;
|
|
1122
|
+
}
|
|
1123
|
+
return { ...order, layers };
|
|
1005
1124
|
}
|
|
1125
|
+
let improved: boolean;
|
|
1126
|
+
do {
|
|
1127
|
+
improved = sweep(forward);
|
|
1128
|
+
trace(-1);
|
|
1129
|
+
forward = !forward;
|
|
1130
|
+
} while (improved);
|
|
1006
1131
|
return { ...order, layers };
|
|
1007
1132
|
}
|
|
1008
1133
|
|
|
@@ -1053,7 +1178,7 @@ export function applyForcedModelOrder(
|
|
|
1053
1178
|
order: LayerOrder,
|
|
1054
1179
|
): LayerOrder {
|
|
1055
1180
|
const strategy = input.settings["considerModelOrder.strategy"] ?? "NONE";
|
|
1056
|
-
if (strategy === "NONE") return order;
|
|
1181
|
+
if (strategy === "NONE" || modelSweepInputs.has(input)) return order;
|
|
1057
1182
|
const forceNodeOrder =
|
|
1058
1183
|
input.settings["crossingMinimization.forceNodeModelOrder"] === true &&
|
|
1059
1184
|
(strategy === "NODES_AND_EDGES" || strategy === "PREFER_NODES");
|
|
@@ -1191,10 +1316,48 @@ export function applyForcedModelOrder(
|
|
|
1191
1316
|
}
|
|
1192
1317
|
}
|
|
1193
1318
|
}
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
|
|
1197
|
-
|
|
1319
|
+
// Node reordering invalidates flexible physical-port ranks. Reconcile them
|
|
1320
|
+
// using the same distributor and rank convention as the preceding sweep;
|
|
1321
|
+
// retain fixed boundary orders for the canonical greedy crossing counter.
|
|
1322
|
+
const incoming = clonePortOrders(order.inputPortOrderByNodeId);
|
|
1323
|
+
const outgoing = clonePortOrders(order.outputPortOrderByNodeId);
|
|
1324
|
+
const graph = crossingGraph(input, orientation, layers, incoming, outgoing);
|
|
1325
|
+
const distributor = new CanonicalPortDistributor(graph);
|
|
1326
|
+
const fixedOrder = new Set(
|
|
1327
|
+
input.graph.nodes
|
|
1328
|
+
.filter((node) => {
|
|
1329
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
1330
|
+
return (
|
|
1331
|
+
constraint === "FIXED_ORDER" || constraint === "FIXED_POS" || constraint === "FIXED_RATIO"
|
|
1332
|
+
);
|
|
1333
|
+
})
|
|
1334
|
+
.map((node) => node.id),
|
|
1335
|
+
);
|
|
1336
|
+
for (let layer = layers.length - 1; layer >= 0; layer--) {
|
|
1337
|
+
distributor.distribute(graph, layer, false, {
|
|
1338
|
+
nodeRelative: nodeRelativePortRanksByInput.get(input) ?? true,
|
|
1339
|
+
fixedOrder,
|
|
1340
|
+
hierarchical: new Set(),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const edges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
1344
|
+
const endpoints = new Map(edges.map((edge, index) => [edge.id, graph.edges[index]!]));
|
|
1345
|
+
for (const node of graph.layers.flat()) {
|
|
1346
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
1347
|
+
for (const [orders, source] of [
|
|
1348
|
+
[outgoing, true],
|
|
1349
|
+
[incoming, false],
|
|
1350
|
+
] as const) {
|
|
1351
|
+
orders
|
|
1352
|
+
.get(node.id)
|
|
1353
|
+
?.sort(
|
|
1354
|
+
(left, right) =>
|
|
1355
|
+
ordinal.get(endpoints.get(left)![source ? "source" : "target"])! -
|
|
1356
|
+
ordinal.get(endpoints.get(right)![source ? "source" : "target"])!,
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return { ...order, layers, inputPortOrderByNodeId: incoming, outputPortOrderByNodeId: outgoing };
|
|
1198
1361
|
}
|
|
1199
1362
|
|
|
1200
1363
|
/** ELK LONGEST_PATH: align sinks on the final layer. */
|
|
@@ -1607,10 +1770,25 @@ function sortLayerByAdjacentPosition(
|
|
|
1607
1770
|
preOrdered = true,
|
|
1608
1771
|
sourceUnknownPlacement = true,
|
|
1609
1772
|
perturbSummedWeight = true,
|
|
1773
|
+
units?: CrossingUnits,
|
|
1774
|
+
sameLayerNeighbors: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
1775
|
+
orderedVisits?: ReadonlyMap<string, readonly (number | string)[]>,
|
|
1776
|
+
forcedCompare?: (left: string, right: string) => number,
|
|
1610
1777
|
): void {
|
|
1611
1778
|
const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index] as const));
|
|
1612
1779
|
const adjacentPosition = new Map<string, number | undefined>();
|
|
1613
|
-
|
|
1780
|
+
if (units && statistic === "mean") {
|
|
1781
|
+
for (const [id, score] of associatedBarycenters(
|
|
1782
|
+
layer,
|
|
1783
|
+
ranksByNodeId,
|
|
1784
|
+
sameLayerNeighbors,
|
|
1785
|
+
units.associates,
|
|
1786
|
+
random,
|
|
1787
|
+
orderedVisits,
|
|
1788
|
+
))
|
|
1789
|
+
adjacentPosition.set(id, score);
|
|
1790
|
+
}
|
|
1791
|
+
for (const nodeId of units && statistic === "mean" ? [] : layer) {
|
|
1614
1792
|
const positions = [...(ranksByNodeId.get(nodeId) ?? [])].sort((left, right) => left - right);
|
|
1615
1793
|
if (positions.length === 0) {
|
|
1616
1794
|
adjacentPosition.set(nodeId, undefined);
|
|
@@ -1671,174 +1849,322 @@ function sortLayerByAdjacentPosition(
|
|
|
1671
1849
|
}
|
|
1672
1850
|
}
|
|
1673
1851
|
|
|
1674
|
-
|
|
1852
|
+
const compare = (a: string, b: string) => {
|
|
1675
1853
|
return (
|
|
1854
|
+
forcedCompare?.(a, b) ||
|
|
1676
1855
|
(adjacentPosition.get(a) ?? 0) - (adjacentPosition.get(b) ?? 0) ||
|
|
1677
1856
|
(originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0)
|
|
1678
1857
|
);
|
|
1679
|
-
}
|
|
1858
|
+
};
|
|
1859
|
+
if (forcedCompare)
|
|
1860
|
+
insertionSort(
|
|
1861
|
+
layer,
|
|
1862
|
+
transitiveComparator((id: string) => id, compare),
|
|
1863
|
+
);
|
|
1864
|
+
else layer.sort(compare);
|
|
1865
|
+
if (units && !forcedCompare)
|
|
1866
|
+
layer.splice(0, layer.length, ...resolveCrossingConstraints(layer, adjacentPosition, units));
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
type LayerSweepVisitor = (
|
|
1870
|
+
layerIndex: number,
|
|
1871
|
+
nodeIds: readonly string[],
|
|
1872
|
+
forward: boolean,
|
|
1873
|
+
firstSweep: boolean,
|
|
1874
|
+
) => void;
|
|
1875
|
+
|
|
1876
|
+
/** @internal */
|
|
1877
|
+
export interface LayerSweepSession {
|
|
1878
|
+
/** Scope graph id, for development tracing. */
|
|
1879
|
+
readonly scope: string;
|
|
1880
|
+
readonly random: JavaRandom;
|
|
1881
|
+
readonly attempts: number;
|
|
1882
|
+
readonly restoreRejectedSweep: boolean;
|
|
1883
|
+
readonly usesInitialModelOrder?: boolean;
|
|
1884
|
+
/** Clear the scope flag after its first counter-based sweep. */
|
|
1885
|
+
finishInitialOrderAttempt?(): void;
|
|
1886
|
+
useRandom(random: JavaRandom): void;
|
|
1887
|
+
lockPortOrder(nodeId: string): void;
|
|
1888
|
+
markHierarchicalNode(nodeId: string): void;
|
|
1889
|
+
minimize(): LayerOrder;
|
|
1890
|
+
shuffleFirstLayer(forward: boolean): void;
|
|
1891
|
+
sweep(forward: boolean, firstSweep: boolean, visitLayer?: LayerSweepVisitor): void;
|
|
1892
|
+
countCrossings(): number;
|
|
1893
|
+
snapshot(): LayerOrder;
|
|
1894
|
+
restore(order: LayerOrder): void;
|
|
1895
|
+
finish(order: LayerOrder): LayerOrder;
|
|
1896
|
+
/** Live current layers; callers must not mutate them. */
|
|
1897
|
+
currentLayers(): readonly (readonly string[])[];
|
|
1898
|
+
/** Current canonical port ids of one node, as {@link snapshot} records them. */
|
|
1899
|
+
physicalPortOrder(nodeId: string): string[];
|
|
1900
|
+
/** Live incident edge orders of one node; callers must not mutate them. */
|
|
1901
|
+
edgeOrders(nodeId: string): { incoming: readonly string[]; outgoing: readonly string[] };
|
|
1902
|
+
/** Same as restoring a snapshot that differs only in one layer. */
|
|
1903
|
+
replaceLayer(index: number, nodeIds: readonly string[]): void;
|
|
1904
|
+
/** Same as restoring a snapshot that differs only in one node's port and edge orders. */
|
|
1905
|
+
reorderNodePorts(
|
|
1906
|
+
nodeId: string,
|
|
1907
|
+
physical: readonly string[],
|
|
1908
|
+
incoming: readonly string[],
|
|
1909
|
+
outgoing: readonly string[],
|
|
1910
|
+
): void;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function clonePortOrders(orders?: ReadonlyMap<string, readonly string[]>): Map<string, string[]> {
|
|
1914
|
+
return new Map([...(orders ?? [])].map(([id, edges]) => [id, [...edges]]));
|
|
1680
1915
|
}
|
|
1681
1916
|
|
|
1682
1917
|
function minimizeCrossingsWithLayerSweep(
|
|
1683
1918
|
statistic: "mean" | "median",
|
|
1684
|
-
sweeps =
|
|
1919
|
+
sweeps = 7,
|
|
1685
1920
|
): CrossingMinimizer {
|
|
1686
|
-
return (input, orientation, assignment) =>
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1921
|
+
return (input, orientation, assignment) =>
|
|
1922
|
+
createLayerSweepSession(input, orientation, assignment, statistic, sweeps).minimize();
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/** Internal mutable sweep state shared by standalone and hierarchy coordination. */
|
|
1926
|
+
export function createLayerSweepSession(
|
|
1927
|
+
input: LayeredPhaseInput,
|
|
1928
|
+
orientation: AcyclicOrientation,
|
|
1929
|
+
assignment: LayerAssignment,
|
|
1930
|
+
statistic: "mean" | "median" = "mean",
|
|
1931
|
+
sweeps = 7,
|
|
1932
|
+
initialization: { resetSeed?: boolean } = {},
|
|
1933
|
+
): LayerSweepSession {
|
|
1934
|
+
const exactPortSweep =
|
|
1935
|
+
(input.settings["wrapping.strategy"] ?? "OFF") === "OFF" &&
|
|
1936
|
+
(input.settings["layerUnzipping.strategy"] ?? "NONE") === "NONE";
|
|
1937
|
+
let maximumLayer = 0;
|
|
1938
|
+
for (const layer of assignment.layerByNodeId.values()) {
|
|
1939
|
+
maximumLayer = Math.max(maximumLayer, layer);
|
|
1940
|
+
}
|
|
1941
|
+
const layerCount = maximumLayer + 1;
|
|
1942
|
+
const layers = Array.from({ length: layerCount }, () => [] as string[]);
|
|
1943
|
+
const initialNodes = assignment.seedOrder
|
|
1944
|
+
? completeSeedOrder(input, assignment)
|
|
1945
|
+
: (input.settings["layering.strategy"] ?? "NETWORK_SIMPLEX") === "NETWORK_SIMPLEX"
|
|
1946
|
+
? networkSimplexComponentOrder(input)
|
|
1947
|
+
: input.graph.nodes.map((node) => node.id);
|
|
1948
|
+
for (const nodeId of initialNodes) {
|
|
1949
|
+
layers[assignment.layerByNodeId.get(nodeId) ?? 0]?.push(nodeId);
|
|
1950
|
+
}
|
|
1951
|
+
const units = getCrossingUnits(input);
|
|
1952
|
+
// Edges, orientation and layer assignment stay fixed for the session.
|
|
1953
|
+
const sameLayerNeighborsByDirection = new Map<boolean, Map<string, string[]>>();
|
|
1954
|
+
const sameLayerNeighbors = (forward: boolean) => {
|
|
1955
|
+
let cached = sameLayerNeighborsByDirection.get(forward);
|
|
1956
|
+
if (!cached)
|
|
1957
|
+
sameLayerNeighborsByDirection.set(forward, (cached = findSameLayerNeighbors(forward)));
|
|
1958
|
+
return cached;
|
|
1959
|
+
};
|
|
1960
|
+
const findSameLayerNeighbors = (forward: boolean) => {
|
|
1961
|
+
const neighbors = new Map<string, string[]>();
|
|
1708
1962
|
for (const edge of input.graph.edges) {
|
|
1709
|
-
const [
|
|
1710
|
-
|
|
1711
|
-
|
|
1963
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
1964
|
+
if (
|
|
1965
|
+
source !== target &&
|
|
1966
|
+
assignment.layerByNodeId.get(source) === assignment.layerByNodeId.get(target)
|
|
1967
|
+
) {
|
|
1968
|
+
const free = forward ? target : source,
|
|
1969
|
+
fixed = forward ? source : target;
|
|
1970
|
+
neighbors.set(free, [...(neighbors.get(free) ?? []), fixed]);
|
|
1971
|
+
}
|
|
1712
1972
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1973
|
+
return neighbors;
|
|
1974
|
+
};
|
|
1975
|
+
const inputPortOrder = new Map(input.graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
1976
|
+
const outputPortOrder = new Map(input.graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
1977
|
+
for (const edge of input.graph.edges) {
|
|
1978
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
1979
|
+
// ELK detaches self-loop edges before crossing minimization, then restores
|
|
1980
|
+
// them afterward. Their connectivity must not consume sweep port ranks.
|
|
1981
|
+
if (sourceId === targetId) continue;
|
|
1982
|
+
outputPortOrder.get(sourceId)?.push(edge.id);
|
|
1983
|
+
inputPortOrder.get(targetId)?.push(edge.id);
|
|
1984
|
+
}
|
|
1985
|
+
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
1986
|
+
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
1987
|
+
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
1988
|
+
const hierarchicalNodes = new Set<string>();
|
|
1989
|
+
const fixedOrderNodes = new Set(
|
|
1990
|
+
input.graph.nodes
|
|
1991
|
+
.filter((node) => {
|
|
1992
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
1993
|
+
return !!node.ports?.length && (constraint === "FIXED_POS" || constraint === "FIXED_RATIO");
|
|
1994
|
+
})
|
|
1995
|
+
.map((node) => node.id),
|
|
1996
|
+
);
|
|
1997
|
+
const fixedPortRank = (nodeId: string, edgeId: string, incoming: boolean): number => {
|
|
1998
|
+
const node = nodeById.get(nodeId)!;
|
|
1999
|
+
const edge = edgeById.get(edgeId)!;
|
|
2000
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
2001
|
+
const portName = incoming !== reversed ? edge.targetPort : edge.sourcePort;
|
|
2002
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
2003
|
+
const horizontal = input.direction === "right" || input.direction === "left";
|
|
2004
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
2005
|
+
const size = input.sizes.get(nodeId)!;
|
|
2006
|
+
const width = horizontal ? size.width : size.height;
|
|
2007
|
+
const height = horizontal ? size.height : size.width;
|
|
2008
|
+
const x = (port?.x ?? size.width / 2) + (port?.width ?? 0) / 2;
|
|
2009
|
+
const y = (port?.y ?? size.height / 2) + (port?.height ?? 0) / 2;
|
|
2010
|
+
const flow = reverse ? width - (horizontal ? x : y) : horizontal ? x : y;
|
|
2011
|
+
const cross = horizontal ? y : x;
|
|
2012
|
+
const side = port && input.portSettings?.(port, node)?.["port.side"];
|
|
2013
|
+
const flowSide = horizontal ? (reverse ? "WEST" : "EAST") : reverse ? "NORTH" : "SOUTH";
|
|
2014
|
+
const backSide = horizontal ? (reverse ? "EAST" : "WEST") : reverse ? "SOUTH" : "NORTH";
|
|
2015
|
+
const upperSide = horizontal ? "NORTH" : "WEST";
|
|
2016
|
+
// Clockwise boundary order in the canonical rightward coordinate system.
|
|
2017
|
+
if (side === upperSide) return flow;
|
|
2018
|
+
if (side === flowSide || (!side && !incoming)) return width + cross;
|
|
2019
|
+
if (side === backSide || (!side && incoming)) return 2 * width + 2 * height - cross;
|
|
2020
|
+
return 2 * width + height - flow;
|
|
2021
|
+
};
|
|
2022
|
+
// Port distribution may reorder flexible ports, never fixed physical positions.
|
|
2023
|
+
for (const nodeId of fixedOrderNodes) {
|
|
2024
|
+
outputPortOrder
|
|
2025
|
+
.get(nodeId)
|
|
2026
|
+
?.sort((a, b) => fixedPortRank(nodeId, a, false) - fixedPortRank(nodeId, b, false));
|
|
2027
|
+
inputPortOrder
|
|
2028
|
+
.get(nodeId)
|
|
2029
|
+
?.sort((a, b) => fixedPortRank(nodeId, a, true) - fixedPortRank(nodeId, b, true));
|
|
2030
|
+
}
|
|
2031
|
+
if (exactPortSweep) {
|
|
2032
|
+
for (const [nodeId, edgeIds] of inputPortOrder) {
|
|
2033
|
+
if (fixedOrderNodes.has(nodeId)) continue;
|
|
2034
|
+
if (edgeIds.length >= 4) {
|
|
2035
|
+
edgeIds.splice(0, edgeIds.length - 1, ...edgeIds.slice(0, -1).reverse());
|
|
1718
2036
|
}
|
|
1719
2037
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
return crossings;
|
|
2038
|
+
}
|
|
2039
|
+
const buildCrossingGraph = createCrossingGraphBuilder(input);
|
|
2040
|
+
const countCrossings = (candidateLayers: readonly (readonly string[])[]): number => {
|
|
2041
|
+
if (exactPortSweep) {
|
|
2042
|
+
const graph = buildCrossingGraph(
|
|
2043
|
+
orientation,
|
|
2044
|
+
candidateLayers,
|
|
2045
|
+
inputPortOrder,
|
|
2046
|
+
outputPortOrder,
|
|
2047
|
+
);
|
|
2048
|
+
let total = countAllCrossings(splitMixedCrossingPorts(graph, canonicalReversed)).total;
|
|
2049
|
+
const nodeInfluence = Number(
|
|
2050
|
+
input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0,
|
|
2051
|
+
);
|
|
2052
|
+
// ELK 0.11.1 tests node influence twice when selecting the weighted counter.
|
|
2053
|
+
// Port influence alone therefore does not enable its model-order objective.
|
|
2054
|
+
if (
|
|
2055
|
+
(input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE" &&
|
|
2056
|
+
nodeInfluence !== 0
|
|
2057
|
+
) {
|
|
2058
|
+
const changes = countModelOrderChanges(input, orientation, graph);
|
|
2059
|
+
total +=
|
|
2060
|
+
nodeInfluence * changes.nodeChanges +
|
|
2061
|
+
Number(input.settings["considerModelOrder.crossingCounterPortInfluence"] ?? 0) *
|
|
2062
|
+
changes.portChanges;
|
|
1746
2063
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2064
|
+
return total;
|
|
2065
|
+
}
|
|
2066
|
+
const positions = new Map<string, number>();
|
|
2067
|
+
for (const layer of candidateLayers) {
|
|
2068
|
+
for (const [index, id] of layer.entries()) positions.set(id, index);
|
|
2069
|
+
}
|
|
2070
|
+
let crossings = 0;
|
|
2071
|
+
for (let left = 0; left < input.graph.edges.length; left++) {
|
|
2072
|
+
const [leftSource, leftTarget] = getOrientedEndpoints(input.graph.edges[left]!, orientation);
|
|
2073
|
+
for (let right = left + 1; right < input.graph.edges.length; right++) {
|
|
2074
|
+
const [rightSource, rightTarget] = getOrientedEndpoints(
|
|
2075
|
+
input.graph.edges[right]!,
|
|
2076
|
+
orientation,
|
|
2077
|
+
);
|
|
2078
|
+
if (leftSource === rightSource || leftTarget === rightTarget) continue;
|
|
2079
|
+
const sourceDifference =
|
|
2080
|
+
(positions.get(leftSource) ?? 0) - (positions.get(rightSource) ?? 0);
|
|
2081
|
+
const targetDifference =
|
|
2082
|
+
(positions.get(leftTarget) ?? 0) - (positions.get(rightTarget) ?? 0);
|
|
2083
|
+
if (sourceDifference * targetDifference < 0) crossings++;
|
|
1752
2084
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2085
|
+
}
|
|
2086
|
+
return crossings;
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
const sharedRandom = crossingRandom(input);
|
|
2090
|
+
const randomSeed = initialization.resetSeed === false ? undefined : sharedRandom.nextLong();
|
|
2091
|
+
const portDistributorUsesNodeRelativeRanks = sharedRandom.nextBoolean();
|
|
2092
|
+
const nodeRelativePortRanks = portDistributorUsesNodeRelativeRanks;
|
|
2093
|
+
nodeRelativePortRanksByInput.set(input, nodeRelativePortRanks);
|
|
2094
|
+
// ELK treats the median heuristic as deterministic, so it keeps using the
|
|
2095
|
+
// graph's shared RNG after port-distributor selection. Barycenter resets to
|
|
2096
|
+
// the saved seed while comparing randomized layouts.
|
|
2097
|
+
let random =
|
|
2098
|
+
statistic === "median" || randomSeed === undefined ? sharedRandom : new JavaRandom(randomSeed);
|
|
2099
|
+
const thoroughness = Math.max(1, input.settings.thoroughness ?? sweeps ?? 7);
|
|
2100
|
+
let working = layers.map((layer) => [...layer]);
|
|
2101
|
+
const usesInitialModelOrder =
|
|
2102
|
+
statistic === "mean" && (input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE";
|
|
2103
|
+
// Preserved model order makes unknown barycenters interpolate rather than
|
|
2104
|
+
// consume random floats during the first attempt. Child scopes keep their
|
|
2105
|
+
// own flag until their counter-based attempt finishes.
|
|
2106
|
+
let initialOrderAttempt = usesInitialModelOrder;
|
|
2107
|
+
const sourceUnknownPlacement = true;
|
|
2108
|
+
const forcedCompare =
|
|
2109
|
+
usesInitialModelOrder && input.settings["crossingMinimization.forceNodeModelOrder"] === true
|
|
2110
|
+
? normalModelComparator(input)
|
|
2111
|
+
: undefined;
|
|
2112
|
+
const usePortRanks = true;
|
|
2113
|
+
const medianWeights = new Map<string, number>();
|
|
2114
|
+
const listedEdgeIds = new Map<string, string>();
|
|
2115
|
+
const listedEdgeId = (edgeId: string) => {
|
|
2116
|
+
let listed = listedEdgeIds.get(edgeId);
|
|
2117
|
+
if (listed === undefined)
|
|
2118
|
+
listedEdgeIds.set(edgeId, (listed = edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "")));
|
|
2119
|
+
return listed;
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
const adjacentRanks = (
|
|
2123
|
+
fixedLayer: readonly string[],
|
|
2124
|
+
freeLayer: readonly string[],
|
|
2125
|
+
forward: boolean,
|
|
2126
|
+
): { ranks: Map<string, number[]>; visits: Map<string, (number | string)[]> } => {
|
|
2127
|
+
const edgeRanks = new Map<string, number>();
|
|
2128
|
+
const visits = new Map(freeLayer.map((id) => [id, [] as (number | string)[]]));
|
|
2129
|
+
const freeIds = new Set(freeLayer);
|
|
2130
|
+
const ranks = new Map(freeLayer.map((id) => [id, [] as number[]]));
|
|
2131
|
+
if (!usePortRanks) {
|
|
2132
|
+
const fixedPosition = new Map(fixedLayer.map((id, index) => [id, index]));
|
|
2133
|
+
for (const edge of input.graph.edges) {
|
|
2134
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
2135
|
+
if (forward && fixedPosition.has(sourceId) && freeIds.has(targetId)) {
|
|
2136
|
+
ranks.get(targetId)?.push(fixedPosition.get(sourceId)!);
|
|
2137
|
+
} else if (!forward && fixedPosition.has(targetId) && freeIds.has(sourceId)) {
|
|
2138
|
+
ranks.get(sourceId)?.push(fixedPosition.get(targetId)!);
|
|
1799
2139
|
}
|
|
1800
2140
|
}
|
|
1801
|
-
return
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
freeLayer: readonly string[],
|
|
1826
|
-
forward: boolean,
|
|
1827
|
-
): Map<string, number[]> => {
|
|
1828
|
-
const freeIds = new Set(freeLayer);
|
|
1829
|
-
const ranks = new Map(freeLayer.map((id) => [id, [] as number[]]));
|
|
1830
|
-
if (!usePortRanks) {
|
|
1831
|
-
const fixedPosition = new Map(fixedLayer.map((id, index) => [id, index]));
|
|
1832
|
-
for (const edge of input.graph.edges) {
|
|
1833
|
-
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
1834
|
-
if (forward && fixedPosition.has(sourceId) && freeIds.has(targetId)) {
|
|
1835
|
-
ranks.get(targetId)?.push(fixedPosition.get(sourceId)!);
|
|
1836
|
-
} else if (!forward && fixedPosition.has(targetId) && freeIds.has(sourceId)) {
|
|
1837
|
-
ranks.get(sourceId)?.push(fixedPosition.get(targetId)!);
|
|
1838
|
-
}
|
|
2141
|
+
return { ranks, visits };
|
|
2142
|
+
}
|
|
2143
|
+
if (exactPortSweep) {
|
|
2144
|
+
const fixedIndex = assignment.layerByNodeId.get(fixedLayer[0] ?? "");
|
|
2145
|
+
if (fixedIndex !== undefined) {
|
|
2146
|
+
const nodes = fixedLayer.map((id) => canonicalNodes.get(id)!);
|
|
2147
|
+
canonicalGraph.layers[fixedIndex] = nodes;
|
|
2148
|
+
canonicalDistributor.calculatePortRanks(
|
|
2149
|
+
canonicalGraph,
|
|
2150
|
+
nodes,
|
|
2151
|
+
!forward,
|
|
2152
|
+
nodeRelativePortRanks,
|
|
2153
|
+
);
|
|
2154
|
+
const fixedIds = new Set(fixedLayer);
|
|
2155
|
+
for (let index = 0; index < canonicalEdges.length; index++) {
|
|
2156
|
+
const edge = canonicalEdges[index]!;
|
|
2157
|
+
const [source, target] = canonicalOrientedEndpoints[index]!;
|
|
2158
|
+
if (!fixedIds.has(forward ? source : target) || !freeIds.has(forward ? target : source))
|
|
2159
|
+
continue;
|
|
2160
|
+
const endpoint = canonicalEndpoints.get(edge.id)!;
|
|
2161
|
+
const rank =
|
|
2162
|
+
canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target]!;
|
|
2163
|
+
ranks.get(forward ? target : source)!.push(rank);
|
|
2164
|
+
edgeRanks.set(edge.id, rank);
|
|
1839
2165
|
}
|
|
1840
|
-
return ranks;
|
|
1841
2166
|
}
|
|
2167
|
+
} else {
|
|
1842
2168
|
let rankSum = 0;
|
|
1843
2169
|
for (const fixedId of fixedLayer) {
|
|
1844
2170
|
const groups = new Map<string, { edges: GraphEdge[]; portOrder: number }>();
|
|
@@ -1886,188 +2212,329 @@ function minimizeCrossingsWithLayerSweep(
|
|
|
1886
2212
|
for (const edge of group.edges) {
|
|
1887
2213
|
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
1888
2214
|
ranks.get(forward ? targetId : sourceId)?.push(rank);
|
|
2215
|
+
edgeRanks.set(edge.id, rank);
|
|
1889
2216
|
}
|
|
1890
2217
|
}
|
|
1891
2218
|
rankSum += nodeRelativePortRanks ? 1 : count;
|
|
1892
2219
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
orders: Map<string, string[]>,
|
|
1928
|
-
oppositeRanks: ReadonlyMap<string, number>,
|
|
1929
|
-
reverse: boolean,
|
|
1930
|
-
) => {
|
|
1931
|
-
for (const nodeId of nodeIds) {
|
|
1932
|
-
orders.get(nodeId)?.sort((leftId, rightId) => {
|
|
1933
|
-
const difference = (oppositeRanks.get(leftId) ?? 0) - (oppositeRanks.get(rightId) ?? 0);
|
|
1934
|
-
return reverse ? -difference : difference;
|
|
1935
|
-
});
|
|
2220
|
+
}
|
|
2221
|
+
// Visit each free node's ports in their current canonical order, while
|
|
2222
|
+
// preserving incident-edge order within a shared port. Fixed-layer ranks
|
|
2223
|
+
// and same-layer recursion must not be separated into independent sums.
|
|
2224
|
+
for (const id of freeLayer) {
|
|
2225
|
+
const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
|
|
2226
|
+
const groups = new Map<string, string[]>();
|
|
2227
|
+
for (const edgeId of order) {
|
|
2228
|
+
const edge = edgeById.get(edgeId)!;
|
|
2229
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
2230
|
+
const name = forward !== reversed ? edge.targetPort : edge.sourcePort;
|
|
2231
|
+
const key = name ?? `__implicit:${edgeId}`;
|
|
2232
|
+
const group = groups.get(key);
|
|
2233
|
+
if (group) group.push(edgeId);
|
|
2234
|
+
else groups.set(key, [edgeId]);
|
|
2235
|
+
}
|
|
2236
|
+
for (const group of groups.values()) {
|
|
2237
|
+
// Port sorting reorders ports, not the edges on one port; those keep
|
|
2238
|
+
// ELK's edge list order. Expanded segments keep their listed edge id.
|
|
2239
|
+
const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
|
|
2240
|
+
const rank = (edgeId: string) => listed?.get(listedEdgeId(edgeId));
|
|
2241
|
+
group.sort(
|
|
2242
|
+
(a, b) =>
|
|
2243
|
+
(rank(a) !== undefined && rank(b) !== undefined ? rank(a)! - rank(b)! : 0) ||
|
|
2244
|
+
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2245
|
+
);
|
|
2246
|
+
for (const edgeId of group) {
|
|
2247
|
+
const edge = edgeById.get(edgeId)!;
|
|
2248
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2249
|
+
const neighbor = forward ? source : target;
|
|
2250
|
+
const rank = edgeRanks.get(edgeId);
|
|
2251
|
+
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2252
|
+
visits.get(id)!.push(neighbor);
|
|
2253
|
+
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
1936
2254
|
}
|
|
1937
|
-
};
|
|
1938
|
-
if (forward) {
|
|
1939
|
-
const fixedRanks = calculateRanks(fixedLayer, outputPortOrder, false);
|
|
1940
|
-
reorder(freeLayer, inputPortOrder, fixedRanks, true);
|
|
1941
|
-
const freeRanks = calculateRanks(freeLayer, inputPortOrder, true);
|
|
1942
|
-
reorder(fixedLayer, outputPortOrder, freeRanks, false);
|
|
1943
|
-
} else {
|
|
1944
|
-
const fixedRanks = calculateRanks(fixedLayer, inputPortOrder, true);
|
|
1945
|
-
reorder(freeLayer, outputPortOrder, fixedRanks, false);
|
|
1946
|
-
const freeRanks = calculateRanks(freeLayer, outputPortOrder, false);
|
|
1947
|
-
reorder(fixedLayer, inputPortOrder, freeRanks, true);
|
|
1948
2255
|
}
|
|
1949
|
-
}
|
|
2256
|
+
}
|
|
2257
|
+
return { ranks, visits };
|
|
2258
|
+
};
|
|
1950
2259
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2260
|
+
let canonicalGraph = buildCrossingGraph(orientation, working, inputPortOrder, outputPortOrder);
|
|
2261
|
+
if (usesInitialModelOrder) {
|
|
2262
|
+
if (!restoreInitialModelOrder(input, canonicalGraph))
|
|
2263
|
+
sortInitialModelOrder(input, orientation, canonicalGraph);
|
|
2264
|
+
working = canonicalGraph.layers.map((layer) => layer.map((node) => node.id));
|
|
2265
|
+
const edges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
2266
|
+
const endpoints = new Map(edges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]));
|
|
2267
|
+
for (const node of canonicalGraph.layers.flat()) {
|
|
2268
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2269
|
+
for (const [orders, source] of [
|
|
2270
|
+
[outputPortOrder, true],
|
|
2271
|
+
[inputPortOrder, false],
|
|
2272
|
+
] as const) {
|
|
2273
|
+
orders
|
|
2274
|
+
.get(node.id)
|
|
2275
|
+
?.sort(
|
|
2276
|
+
(a, b) =>
|
|
2277
|
+
ordinal.get(endpoints.get(a)![source ? "source" : "target"])! -
|
|
2278
|
+
ordinal.get(endpoints.get(b)![source ? "source" : "target"])!,
|
|
2279
|
+
);
|
|
1963
2280
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2281
|
+
}
|
|
2282
|
+
modelSweepInputs.add(input);
|
|
2283
|
+
}
|
|
2284
|
+
let canonicalNodes = new Map(canonicalGraph.layers.flat().map((node) => [node.id, node]));
|
|
2285
|
+
const canonicalDistributor = new CanonicalPortDistributor(canonicalGraph);
|
|
2286
|
+
const canonicalEdges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
2287
|
+
const canonicalReversed = canonicalEdges.map((edge) => orientation.reversedEdgeIds.has(edge.id));
|
|
2288
|
+
const canonicalOrientedEndpoints = canonicalEdges.map((edge) =>
|
|
2289
|
+
getOrientedEndpoints(edge, orientation),
|
|
2290
|
+
);
|
|
2291
|
+
const canonicalEndpoints = new Map(
|
|
2292
|
+
canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]),
|
|
2293
|
+
);
|
|
2294
|
+
const canonicalFixedOrder = new Set(
|
|
2295
|
+
input.graph.nodes
|
|
2296
|
+
.filter((node) => {
|
|
2297
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
2298
|
+
return (
|
|
2299
|
+
constraint === "FIXED_ORDER" || constraint === "FIXED_POS" || constraint === "FIXED_RATIO"
|
|
2300
|
+
);
|
|
2301
|
+
})
|
|
2302
|
+
.map((node) => node.id),
|
|
2303
|
+
);
|
|
2304
|
+
// Every canonically fixed node plus each port order locked by a child scope.
|
|
2305
|
+
const fixedPortOrders = new Set(canonicalFixedOrder);
|
|
2306
|
+
const distributePorts = (layerIndex: number, forward: boolean) => {
|
|
2307
|
+
const touched = [layerIndex, layerIndex + (forward ? -1 : 1)].filter(
|
|
2308
|
+
(index) => index >= 0 && index < working.length,
|
|
2309
|
+
);
|
|
2310
|
+
for (const index of touched)
|
|
2311
|
+
canonicalGraph.layers[index] = working[index]!.map((id) => canonicalNodes.get(id)!);
|
|
2312
|
+
canonicalDistributor.distribute(canonicalGraph, layerIndex, forward, {
|
|
2313
|
+
nodeRelative: nodeRelativePortRanks,
|
|
2314
|
+
fixedOrder: fixedPortOrders,
|
|
2315
|
+
hierarchical: hierarchicalNodes,
|
|
2316
|
+
});
|
|
2317
|
+
for (const index of touched)
|
|
2318
|
+
for (const node of canonicalGraph.layers[index]!) {
|
|
2319
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2320
|
+
for (const [orders, source] of [
|
|
2321
|
+
[outputPortOrder, true],
|
|
2322
|
+
[inputPortOrder, false],
|
|
2323
|
+
] as const)
|
|
2324
|
+
orders
|
|
2325
|
+
.get(node.id)
|
|
2326
|
+
?.sort(
|
|
2327
|
+
(left, right) =>
|
|
2328
|
+
ordinal.get(canonicalEndpoints.get(left)![source ? "source" : "target"])! -
|
|
2329
|
+
ordinal.get(canonicalEndpoints.get(right)![source ? "source" : "target"])!,
|
|
1986
2330
|
);
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
(left, right) =>
|
|
1990
|
-
(medianWeights.get(left) ?? 0) - (medianWeights.get(right) ?? 0) ||
|
|
1991
|
-
(originalIndex.get(left) ?? 0) - (originalIndex.get(right) ?? 0),
|
|
1992
|
-
);
|
|
1993
|
-
};
|
|
1994
|
-
if (isForward) {
|
|
1995
|
-
for (let layer = 1; layer < working.length; layer++) {
|
|
1996
|
-
const current = working[layer];
|
|
1997
|
-
const previous = working[layer - 1];
|
|
1998
|
-
if (current && previous) {
|
|
1999
|
-
if (statistic === "median") sortWithMedianWeights(current, previous);
|
|
2000
|
-
else
|
|
2001
|
-
sortLayerByAdjacentPosition(
|
|
2002
|
-
current,
|
|
2003
|
-
adjacentRanks(previous, current, true),
|
|
2004
|
-
statistic,
|
|
2005
|
-
random,
|
|
2006
|
-
!firstSweep,
|
|
2007
|
-
sourceUnknownPlacement,
|
|
2008
|
-
true,
|
|
2009
|
-
);
|
|
2010
|
-
if (exactPortSweep) distributePorts(previous, current, true);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
} else {
|
|
2014
|
-
for (let layer = working.length - 2; layer >= 0; layer--) {
|
|
2015
|
-
const current = working[layer];
|
|
2016
|
-
const next = working[layer + 1];
|
|
2017
|
-
if (current && next) {
|
|
2018
|
-
if (statistic === "median") sortWithMedianWeights(current, next);
|
|
2019
|
-
else
|
|
2020
|
-
sortLayerByAdjacentPosition(
|
|
2021
|
-
current,
|
|
2022
|
-
adjacentRanks(next, current, false),
|
|
2023
|
-
statistic,
|
|
2024
|
-
random,
|
|
2025
|
-
!firstSweep,
|
|
2026
|
-
sourceUnknownPlacement,
|
|
2027
|
-
true,
|
|
2028
|
-
);
|
|
2029
|
-
if (exactPortSweep) distributePorts(next, current, false);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
};
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2034
2333
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2334
|
+
const shuffleFirstLayer = (forward: boolean) => {
|
|
2335
|
+
const firstLayerIndex = forward ? 0 : Math.max(0, working.length - 1);
|
|
2336
|
+
const firstLayerWeights = new Map(
|
|
2337
|
+
(working[firstLayerIndex] ?? []).map((id) => [id, random.nextDouble()]),
|
|
2338
|
+
);
|
|
2339
|
+
const compare = (left: string, right: string) =>
|
|
2340
|
+
forcedCompare?.(left, right) ||
|
|
2341
|
+
(firstLayerWeights.get(left) ?? 0) - (firstLayerWeights.get(right) ?? 0);
|
|
2342
|
+
const first = working[firstLayerIndex];
|
|
2343
|
+
if (first) {
|
|
2344
|
+
if (forcedCompare)
|
|
2345
|
+
insertionSort(
|
|
2346
|
+
first,
|
|
2347
|
+
transitiveComparator((id: string) => id, compare),
|
|
2348
|
+
);
|
|
2349
|
+
else first.sort(compare);
|
|
2350
|
+
}
|
|
2351
|
+
if (units && !forcedCompare && working[firstLayerIndex])
|
|
2352
|
+
working[firstLayerIndex] = resolveCrossingConstraints(
|
|
2353
|
+
working[firstLayerIndex]!,
|
|
2354
|
+
firstLayerWeights,
|
|
2355
|
+
units,
|
|
2043
2356
|
);
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2357
|
+
if (statistic === "median") {
|
|
2358
|
+
working[firstLayerIndex]?.forEach((id, index) => medianWeights.set(id, index + 1));
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
// Non-loop edges by endpoint, for grouping a mixed port's neighbours.
|
|
2362
|
+
const edgesByNode = new Map<string, GraphEdge[]>();
|
|
2363
|
+
for (const edge of input.graph.edges) {
|
|
2364
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
2365
|
+
for (const id of [edge.sourceId, edge.targetId]) {
|
|
2366
|
+
const list = edgesByNode.get(id);
|
|
2367
|
+
if (list) list.push(edge);
|
|
2368
|
+
else edgesByNode.set(id, [edge]);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* A port carrying both directions gives each direction its own track and
|
|
2373
|
+
* attachment, so its edges are no longer interchangeable: neighbours of one
|
|
2374
|
+
* direction must not interleave with the other's, or the separated routes
|
|
2375
|
+
* cross. Barycenters tie (up to noise) for nodes reached only through one
|
|
2376
|
+
* port; within the slots those nodes occupy, put forward edges before
|
|
2377
|
+
* reversed ones. Both ends of a chain see the same reversal, so the order
|
|
2378
|
+
* holds along it.
|
|
2379
|
+
*/
|
|
2380
|
+
const constrained = new Set([
|
|
2381
|
+
...(units?.units.keys() ?? []),
|
|
2382
|
+
...[...(units?.successors ?? [])].flatMap(([id, after]) => [id, ...after]),
|
|
2383
|
+
]);
|
|
2384
|
+
const groupMixedPortDirections = (
|
|
2385
|
+
free: string[],
|
|
2386
|
+
fixed: readonly string[],
|
|
2387
|
+
beyond: readonly string[] = [],
|
|
2388
|
+
) => {
|
|
2389
|
+
const fixedIds = new Set(fixed);
|
|
2390
|
+
const groups = new Map<string, Array<{ id: string; reversed: boolean }>>();
|
|
2391
|
+
for (const id of free) {
|
|
2392
|
+
if (constrained.has(id)) continue;
|
|
2393
|
+
let only: GraphEdge | undefined,
|
|
2394
|
+
count = 0;
|
|
2395
|
+
for (const edge of edgesByNode.get(id) ?? [])
|
|
2396
|
+
if (fixedIds.has(edge.sourceId === id ? edge.targetId : edge.sourceId)) {
|
|
2397
|
+
only = edge;
|
|
2398
|
+
count++;
|
|
2052
2399
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2400
|
+
if (count !== 1) continue;
|
|
2401
|
+
const atSource = only!.sourceId !== id;
|
|
2402
|
+
const name = atSource ? only!.sourcePort : only!.targetPort;
|
|
2403
|
+
if (name === undefined) continue;
|
|
2404
|
+
const key = JSON.stringify([atSource ? only!.sourceId : only!.targetId, name]);
|
|
2405
|
+
const group = groups.get(key) ?? [];
|
|
2406
|
+
group.push({ id, reversed: orientation.reversedEdgeIds.has(only!.id) });
|
|
2407
|
+
groups.set(key, group);
|
|
2408
|
+
}
|
|
2409
|
+
if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed)))
|
|
2410
|
+
return;
|
|
2411
|
+
const index = new Map(free.map((id, i) => [id, i]));
|
|
2412
|
+
// The direction whose nodes lead further toward the start of the layer
|
|
2413
|
+
// beyond (from the previous sweep) goes first; forward edges by default.
|
|
2414
|
+
const far = new Map(beyond.map((id, i) => [id, i]));
|
|
2415
|
+
const reach = (members: ReadonlyArray<{ id: string }>) => {
|
|
2416
|
+
let sum = 0,
|
|
2417
|
+
count = 0;
|
|
2418
|
+
for (const { id } of members)
|
|
2419
|
+
for (const edge of edgesByNode.get(id) ?? []) {
|
|
2420
|
+
const at = far.get(edge.sourceId === id ? edge.targetId : edge.sourceId);
|
|
2421
|
+
if (at === undefined) continue;
|
|
2422
|
+
sum += at;
|
|
2423
|
+
count++;
|
|
2424
|
+
}
|
|
2425
|
+
return count ? sum / count : undefined;
|
|
2426
|
+
};
|
|
2427
|
+
for (const group of groups.values()) {
|
|
2428
|
+
if (!group.some((m) => m.reversed) || group.every((m) => m.reversed)) continue;
|
|
2429
|
+
const forward = reach(group.filter((m) => !m.reversed)),
|
|
2430
|
+
backward = reach(group.filter((m) => m.reversed));
|
|
2431
|
+
const reversedFirst =
|
|
2432
|
+
forward === undefined || backward === undefined ? false : backward < forward;
|
|
2433
|
+
const slots = group.map((m) => index.get(m.id)!).sort((a, b) => a - b);
|
|
2434
|
+
const members = group
|
|
2435
|
+
.sort((a, b) => index.get(a.id)! - index.get(b.id)!)
|
|
2436
|
+
.sort((a, b) => (reversedFirst ? -1 : 1) * (Number(a.reversed) - Number(b.reversed)));
|
|
2437
|
+
for (const [i, slot] of slots.entries()) free[slot] = members[i]!.id;
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
const sweep = (isForward: boolean, firstSweep: boolean, visitLayer?: LayerSweepVisitor) => {
|
|
2441
|
+
const firstLayer = isForward ? 0 : Math.max(0, working.length - 1);
|
|
2442
|
+
if (exactPortSweep) distributePorts(firstLayer, isForward);
|
|
2443
|
+
visitLayer?.(firstLayer, [...(working[firstLayer] ?? [])], isForward, firstSweep);
|
|
2444
|
+
const sortWithMedianWeights = (current: string[], reference: readonly string[]) => {
|
|
2445
|
+
const referenceIds = new Set(reference);
|
|
2446
|
+
const originalIndex = new Map(current.map((id, index) => [id, index]));
|
|
2447
|
+
for (const id of current) {
|
|
2448
|
+
const connectedWeights = input.graph.edges
|
|
2449
|
+
.flatMap((edge) => {
|
|
2450
|
+
if (edge.sourceId === id && referenceIds.has(edge.targetId)) {
|
|
2451
|
+
return [medianWeights.get(edge.targetId) ?? 0];
|
|
2452
|
+
}
|
|
2453
|
+
if (edge.targetId === id && referenceIds.has(edge.sourceId)) {
|
|
2454
|
+
return [medianWeights.get(edge.sourceId) ?? 0];
|
|
2455
|
+
}
|
|
2456
|
+
return [];
|
|
2457
|
+
})
|
|
2458
|
+
.sort((left, right) => left - right);
|
|
2459
|
+
medianWeights.set(
|
|
2460
|
+
id,
|
|
2461
|
+
connectedWeights.length > 0
|
|
2462
|
+
? connectedWeights[Math.floor(connectedWeights.length / 2)]!
|
|
2463
|
+
: Number.MAX_VALUE / 2,
|
|
2060
2464
|
);
|
|
2061
2465
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2466
|
+
current.sort(
|
|
2467
|
+
(left, right) =>
|
|
2468
|
+
(medianWeights.get(left) ?? 0) - (medianWeights.get(right) ?? 0) ||
|
|
2469
|
+
(originalIndex.get(left) ?? 0) - (originalIndex.get(right) ?? 0),
|
|
2470
|
+
);
|
|
2471
|
+
};
|
|
2472
|
+
if (isForward) {
|
|
2473
|
+
for (let layer = 1; layer < working.length; layer++) {
|
|
2474
|
+
const current = working[layer];
|
|
2475
|
+
const previous = working[layer - 1];
|
|
2476
|
+
if (current && previous) {
|
|
2477
|
+
if (statistic === "median") sortWithMedianWeights(current, previous);
|
|
2478
|
+
else {
|
|
2479
|
+
const adjacent = adjacentRanks(previous, current, true);
|
|
2480
|
+
sortLayerByAdjacentPosition(
|
|
2481
|
+
current,
|
|
2482
|
+
adjacent.ranks,
|
|
2483
|
+
statistic,
|
|
2484
|
+
random,
|
|
2485
|
+
!firstSweep ||
|
|
2486
|
+
initialOrderAttempt ||
|
|
2487
|
+
canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT",
|
|
2488
|
+
sourceUnknownPlacement,
|
|
2489
|
+
true,
|
|
2490
|
+
units,
|
|
2491
|
+
sameLayerNeighbors(isForward),
|
|
2492
|
+
adjacent.visits,
|
|
2493
|
+
forcedCompare,
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
groupMixedPortDirections(current, previous, working[layer + 1]);
|
|
2497
|
+
if (exactPortSweep) distributePorts(layer, true);
|
|
2498
|
+
visitLayer?.(layer, [...current], isForward, firstSweep);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
} else {
|
|
2502
|
+
for (let layer = working.length - 2; layer >= 0; layer--) {
|
|
2503
|
+
const current = working[layer];
|
|
2504
|
+
const next = working[layer + 1];
|
|
2505
|
+
if (current && next) {
|
|
2506
|
+
if (statistic === "median") sortWithMedianWeights(current, next);
|
|
2507
|
+
else {
|
|
2508
|
+
const adjacent = adjacentRanks(next, current, false);
|
|
2509
|
+
sortLayerByAdjacentPosition(
|
|
2510
|
+
current,
|
|
2511
|
+
adjacent.ranks,
|
|
2512
|
+
statistic,
|
|
2513
|
+
random,
|
|
2514
|
+
!firstSweep ||
|
|
2515
|
+
initialOrderAttempt ||
|
|
2516
|
+
canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT",
|
|
2517
|
+
sourceUnknownPlacement,
|
|
2518
|
+
true,
|
|
2519
|
+
units,
|
|
2520
|
+
sameLayerNeighbors(isForward),
|
|
2521
|
+
adjacent.visits,
|
|
2522
|
+
forcedCompare,
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
groupMixedPortDirections(current, next, working[layer - 1]);
|
|
2526
|
+
if (exactPortSweep) distributePorts(layer, false);
|
|
2527
|
+
visitLayer?.(layer, [...current], isForward, firstSweep);
|
|
2528
|
+
}
|
|
2068
2529
|
}
|
|
2069
2530
|
}
|
|
2531
|
+
};
|
|
2070
2532
|
|
|
2533
|
+
const attempts = statistic === "median" ? 1 : thoroughness;
|
|
2534
|
+
const finish = (order: LayerOrder): LayerOrder => {
|
|
2535
|
+
const bestLayers = order.layers.map((layer) => [...layer]);
|
|
2536
|
+
const bestInputPortOrder = clonePortOrders(order.inputPortOrderByNodeId);
|
|
2537
|
+
const bestOutputPortOrder = clonePortOrders(order.outputPortOrderByNodeId);
|
|
2071
2538
|
if (statistic === "median" && input.direction === "left") {
|
|
2072
2539
|
const edgeOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
2073
2540
|
for (const layer of bestLayers) {
|
|
@@ -2117,41 +2584,96 @@ function minimizeCrossingsWithLayerSweep(
|
|
|
2117
2584
|
phaseRandomByInput.set(input, random);
|
|
2118
2585
|
return exactPortSweep
|
|
2119
2586
|
? {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const component: string[] = [];
|
|
2141
|
-
const visit = (id: string): void => {
|
|
2142
|
-
if (visited.has(id)) return;
|
|
2143
|
-
visited.add(id);
|
|
2144
|
-
component.push(id);
|
|
2145
|
-
for (const neighbor of neighbors.get(id) ?? []) visit(neighbor);
|
|
2146
|
-
};
|
|
2147
|
-
visit(node.id);
|
|
2148
|
-
if (components.length === 0 || components[0]!.length < component.length) {
|
|
2149
|
-
components.unshift(component);
|
|
2150
|
-
} else {
|
|
2151
|
-
components.push(component);
|
|
2587
|
+
physicalPortOrderByNodeId: clonePortOrders(order.physicalPortOrderByNodeId),
|
|
2588
|
+
layers: bestLayers,
|
|
2589
|
+
inputPortOrderByNodeId: bestInputPortOrder,
|
|
2590
|
+
outputPortOrderByNodeId: bestOutputPortOrder,
|
|
2591
|
+
}
|
|
2592
|
+
: { layers: bestLayers };
|
|
2593
|
+
};
|
|
2594
|
+
const restore = (order: LayerOrder) => {
|
|
2595
|
+
working = order.layers.map((layer) => [...layer]);
|
|
2596
|
+
inputPortOrder.clear();
|
|
2597
|
+
outputPortOrder.clear();
|
|
2598
|
+
for (const [id, edges] of clonePortOrders(order.inputPortOrderByNodeId))
|
|
2599
|
+
inputPortOrder.set(id, edges);
|
|
2600
|
+
for (const [id, edges] of clonePortOrders(order.outputPortOrderByNodeId))
|
|
2601
|
+
outputPortOrder.set(id, edges);
|
|
2602
|
+
canonicalGraph = buildCrossingGraph(orientation, working, inputPortOrder, outputPortOrder);
|
|
2603
|
+
canonicalNodes = new Map(canonicalGraph.layers.flat().map((node) => [node.id, node]));
|
|
2604
|
+
for (const [id, ports] of order.physicalPortOrderByNodeId ?? []) {
|
|
2605
|
+
const node = canonicalNodes.get(id);
|
|
2606
|
+
if (node) node.ports.sort((a, b) => ports.indexOf(a.id) - ports.indexOf(b.id));
|
|
2152
2607
|
}
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2608
|
+
};
|
|
2609
|
+
traceLayeredPhase(() => ({
|
|
2610
|
+
kind: "initial-order",
|
|
2611
|
+
scope: input.graph.id,
|
|
2612
|
+
layers: working.map((layer) => [...layer]),
|
|
2613
|
+
}));
|
|
2614
|
+
const session: LayerSweepSession = {
|
|
2615
|
+
scope: input.graph.id,
|
|
2616
|
+
get random() {
|
|
2617
|
+
return random;
|
|
2618
|
+
},
|
|
2619
|
+
attempts,
|
|
2620
|
+
usesInitialModelOrder,
|
|
2621
|
+
finishInitialOrderAttempt: () => {
|
|
2622
|
+
initialOrderAttempt = false;
|
|
2623
|
+
},
|
|
2624
|
+
restoreRejectedSweep: !exactPortSweep,
|
|
2625
|
+
lockPortOrder: (id) => {
|
|
2626
|
+
fixedPortOrders.add(id);
|
|
2627
|
+
},
|
|
2628
|
+
markHierarchicalNode: (id) => {
|
|
2629
|
+
hierarchicalNodes.add(id);
|
|
2630
|
+
},
|
|
2631
|
+
useRandom: (source) => {
|
|
2632
|
+
random = source;
|
|
2633
|
+
},
|
|
2634
|
+
minimize: () => {
|
|
2635
|
+
const root = { session, childrenByNodeId: new Map(), useBottomUp: true };
|
|
2636
|
+
return minimizeHierarchyCrossings(root).get(root)!;
|
|
2637
|
+
},
|
|
2638
|
+
shuffleFirstLayer,
|
|
2639
|
+
sweep,
|
|
2640
|
+
countCrossings: () => countCrossings(working),
|
|
2641
|
+
snapshot: () => ({
|
|
2642
|
+
physicalPortOrderByNodeId: new Map(
|
|
2643
|
+
[...canonicalNodes].map(([id, node]) => [id, node.ports.map((port) => port.id)]),
|
|
2644
|
+
),
|
|
2645
|
+
layers: working.map((layer) => [...layer]),
|
|
2646
|
+
inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
|
|
2647
|
+
outputPortOrderByNodeId: clonePortOrders(outputPortOrder),
|
|
2648
|
+
}),
|
|
2649
|
+
restore,
|
|
2650
|
+
finish,
|
|
2651
|
+
currentLayers: () => working,
|
|
2652
|
+
physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
|
|
2653
|
+
edgeOrders: (id) => ({
|
|
2654
|
+
incoming: inputPortOrder.get(id) ?? [],
|
|
2655
|
+
outgoing: outputPortOrder.get(id) ?? [],
|
|
2656
|
+
}),
|
|
2657
|
+
// A full restore would rebuild an equivalent canonical graph: ports keep
|
|
2658
|
+
// their recorded physical order and layers are re-read before distribution.
|
|
2659
|
+
replaceLayer: (index, ids) => {
|
|
2660
|
+
working[index] = [...ids];
|
|
2661
|
+
},
|
|
2662
|
+
reorderNodePorts: (id, physical, incoming, outgoing) => {
|
|
2663
|
+
inputPortOrder.set(id, [...incoming]);
|
|
2664
|
+
outputPortOrder.set(id, [...outgoing]);
|
|
2665
|
+
const node = canonicalNodes.get(id);
|
|
2666
|
+
if (!node) return;
|
|
2667
|
+
const slot = new Map(physical.map((port, index) => [port, index]));
|
|
2668
|
+
node.ports.sort((a, b) => (slot.get(a.id) ?? -1) - (slot.get(b.id) ?? -1));
|
|
2669
|
+
},
|
|
2670
|
+
};
|
|
2671
|
+
return session;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
/** NetworkSimplexLayerer processes the largest undirected component first. */
|
|
2675
|
+
function networkSimplexComponentOrder(input: LayeredPhaseInput): string[] {
|
|
2676
|
+
return networkSimplexComponents(input).flat();
|
|
2155
2677
|
}
|
|
2156
2678
|
|
|
2157
2679
|
/** Keep model order within each component while placing connected work before isolated nodes. */
|
|
@@ -2193,7 +2715,11 @@ export function minimizeCrossingsWithMedian(sweeps = 7): CrossingMinimizer {
|
|
|
2193
2715
|
}
|
|
2194
2716
|
|
|
2195
2717
|
function layersFromAssignment(input: LayeredPhaseInput, assignment: LayerAssignment): string[][] {
|
|
2196
|
-
const maximumLayer = Math.max(
|
|
2718
|
+
const maximumLayer = Math.max(
|
|
2719
|
+
0,
|
|
2720
|
+
(assignment.layerCount ?? 1) - 1,
|
|
2721
|
+
...assignment.layerByNodeId.values(),
|
|
2722
|
+
);
|
|
2197
2723
|
const layers = Array.from({ length: maximumLayer + 1 }, () => [] as string[]);
|
|
2198
2724
|
for (const node of input.graph.nodes) {
|
|
2199
2725
|
layers[assignment.layerByNodeId.get(node.id) ?? 0]?.push(node.id);
|
|
@@ -2215,7 +2741,11 @@ export const minimizeCrossingsWithModelOrder: CrossingMinimizer = (
|
|
|
2215
2741
|
_orientation,
|
|
2216
2742
|
assignment,
|
|
2217
2743
|
) => {
|
|
2218
|
-
const maximumLayer = Math.max(
|
|
2744
|
+
const maximumLayer = Math.max(
|
|
2745
|
+
0,
|
|
2746
|
+
(assignment.layerCount ?? 1) - 1,
|
|
2747
|
+
...assignment.layerByNodeId.values(),
|
|
2748
|
+
);
|
|
2219
2749
|
const layers = Array.from({ length: maximumLayer + 1 }, () => [] as string[]);
|
|
2220
2750
|
const layeringStrategy = input.settings["layering.strategy"] ?? "NETWORK_SIMPLEX";
|
|
2221
2751
|
const nodeOrder = assignment.seedOrder
|
|
@@ -2410,6 +2940,19 @@ export function applyPostCompaction(
|
|
|
2410
2940
|
const constraintStrategy = input.settings["compaction.postCompaction.constraints"] ?? "SCANLINE";
|
|
2411
2941
|
void constraintStrategy;
|
|
2412
2942
|
if (strategy === "NONE") return placement;
|
|
2943
|
+
if (
|
|
2944
|
+
strategy === "EDGE_LENGTH" ||
|
|
2945
|
+
((input.settings.edgeRouting ?? "ORTHOGONAL") === "ORTHOGONAL" &&
|
|
2946
|
+
["LEFT", "RIGHT", "LEFT_RIGHT_CONSTRAINT_LOCKING", "LEFT_RIGHT_CONNECTION_LOCKING"].includes(
|
|
2947
|
+
String(strategy),
|
|
2948
|
+
))
|
|
2949
|
+
)
|
|
2950
|
+
return applyGroupedEdgeLengthCompaction(
|
|
2951
|
+
input,
|
|
2952
|
+
placement,
|
|
2953
|
+
routes,
|
|
2954
|
+
getPlacementOrientation(input),
|
|
2955
|
+
);
|
|
2413
2956
|
const rects = placement.rectByNodeId as Map<string, EntityRect>;
|
|
2414
2957
|
const originalEndpointsByEdgeId = new Map(
|
|
2415
2958
|
[...(routes?.pointsByEdgeId ?? [])].flatMap(([edgeId, points]) => {
|
|
@@ -2485,8 +3028,7 @@ export function applyPostCompaction(
|
|
|
2485
3028
|
// Compaction must preserve the space reserved for an inline center label,
|
|
2486
3029
|
// even when its endpoint rectangles do not intersect on the cross axis.
|
|
2487
3030
|
const inlineLabelSpaceByPair = new Map<string, number>();
|
|
2488
|
-
|
|
2489
|
-
if (strategy !== "EDGE_LENGTH" && (input.direction === "left" || input.direction === "right")) {
|
|
3031
|
+
if (input.direction === "left" || input.direction === "right") {
|
|
2490
3032
|
for (const edge of labelEdges) {
|
|
2491
3033
|
const settings = input.edgeSettings?.(edge);
|
|
2492
3034
|
if (
|
|
@@ -2508,7 +3050,16 @@ export function applyPostCompaction(
|
|
|
2508
3050
|
: 0;
|
|
2509
3051
|
const horizontalSpacing = (left: ConstraintNode, right: ConstraintNode): number =>
|
|
2510
3052
|
left.kind === "node" && right.kind === "node"
|
|
2511
|
-
? Math.max(
|
|
3053
|
+
? Math.max(
|
|
3054
|
+
input.spacing.node,
|
|
3055
|
+
(left.nodeId?.startsWith("__layout_dummy:label:") &&
|
|
3056
|
+
!right.nodeId?.startsWith("__layout_dummy:")) ||
|
|
3057
|
+
(right.nodeId?.startsWith("__layout_dummy:label:") &&
|
|
3058
|
+
!left.nodeId?.startsWith("__layout_dummy:"))
|
|
3059
|
+
? input.spacing.layer
|
|
3060
|
+
: 0,
|
|
3061
|
+
labelSpace(left, right),
|
|
3062
|
+
)
|
|
2512
3063
|
: left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId
|
|
2513
3064
|
? 0
|
|
2514
3065
|
: Number(
|
|
@@ -2619,90 +3170,8 @@ export function applyPostCompaction(
|
|
|
2619
3170
|
);
|
|
2620
3171
|
} else {
|
|
2621
3172
|
compact("LEFT");
|
|
2622
|
-
if (strategy === "EDGE_LENGTH") {
|
|
2623
|
-
const itemByNodeId = new Map(
|
|
2624
|
-
compactables.flatMap((item) =>
|
|
2625
|
-
item.kind === "node" && item.nodeId ? [[item.nodeId, item] as const] : [],
|
|
2626
|
-
),
|
|
2627
|
-
);
|
|
2628
|
-
const incomingDegree = new Map(input.graph.nodes.map((node) => [node.id, 0]));
|
|
2629
|
-
const outgoing = new Map(input.graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
2630
|
-
for (const edge of input.graph.edges) {
|
|
2631
|
-
outgoing.get(edge.sourceId)?.push(edge.targetId);
|
|
2632
|
-
incomingDegree.set(edge.targetId, (incomingDegree.get(edge.targetId) ?? 0) + 1);
|
|
2633
|
-
}
|
|
2634
|
-
for (const node of input.graph.nodes) {
|
|
2635
|
-
const targets = outgoing.get(node.id) ?? [];
|
|
2636
|
-
if (targets.length <= (incomingDegree.get(node.id) ?? 0) || targets.length === 0) continue;
|
|
2637
|
-
if (
|
|
2638
|
-
input.graph.edges.some(
|
|
2639
|
-
(edge) => edge.sourceId === node.id && centerLabeledEdgeIds.has(edge.id),
|
|
2640
|
-
)
|
|
2641
|
-
) {
|
|
2642
|
-
continue;
|
|
2643
|
-
}
|
|
2644
|
-
const item = itemByNodeId.get(node.id);
|
|
2645
|
-
if (!item) continue;
|
|
2646
|
-
const upper = Math.min(
|
|
2647
|
-
...targets.map(
|
|
2648
|
-
(targetId) =>
|
|
2649
|
-
(itemByNodeId.get(targetId)?.x ?? item.x) - item.width - input.spacing.node,
|
|
2650
|
-
),
|
|
2651
|
-
);
|
|
2652
|
-
item.x = Math.max(item.x, upper);
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
if (
|
|
2657
|
-
strategy === "EDGE_LENGTH" &&
|
|
2658
|
-
input.settings["nodePlacement.favorStraightEdges"] === true &&
|
|
2659
|
-
(input.direction === "down" || input.direction === "up")
|
|
2660
|
-
) {
|
|
2661
|
-
const itemByNodeId = new Map(
|
|
2662
|
-
compactables.flatMap((item) =>
|
|
2663
|
-
item.kind === "node" && item.nodeId ? [[item.nodeId, item] as const] : [],
|
|
2664
|
-
),
|
|
2665
|
-
);
|
|
2666
|
-
const incomingDegree = new Map(input.graph.nodes.map((node) => [node.id, 0]));
|
|
2667
|
-
for (const edge of input.graph.edges) {
|
|
2668
|
-
if (edge.sourceId !== edge.targetId) {
|
|
2669
|
-
incomingDegree.set(edge.targetId, (incomingDegree.get(edge.targetId) ?? 0) + 1);
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
for (const edge of input.graph.edges) {
|
|
2673
|
-
if (edge.sourceId === edge.targetId || (incomingDegree.get(edge.targetId) ?? 0) !== 1) {
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
const endpoints = originalEndpointsByEdgeId.get(edge.id);
|
|
2677
|
-
const source = itemByNodeId.get(edge.sourceId);
|
|
2678
|
-
const target = itemByNodeId.get(edge.targetId);
|
|
2679
|
-
if (!endpoints || !source || !target) continue;
|
|
2680
|
-
const forward =
|
|
2681
|
-
input.direction === "down"
|
|
2682
|
-
? endpoints.start.y < endpoints.end.y
|
|
2683
|
-
: endpoints.start.y > endpoints.end.y;
|
|
2684
|
-
if (!forward) continue;
|
|
2685
|
-
const desiredTargetX =
|
|
2686
|
-
source.x + (endpoints.start.x - source.originalX) - (endpoints.end.x - target.originalX);
|
|
2687
|
-
const collides = compactables.some((candidate) => {
|
|
2688
|
-
if (candidate.kind !== "node" || candidate === target) return false;
|
|
2689
|
-
const flowOverlap =
|
|
2690
|
-
candidate.y < target.y + target.height && candidate.y + candidate.height > target.y;
|
|
2691
|
-
if (!flowOverlap) return false;
|
|
2692
|
-
const spacing = input.spacing.node;
|
|
2693
|
-
return (
|
|
2694
|
-
desiredTargetX < candidate.x + candidate.width + spacing &&
|
|
2695
|
-
desiredTargetX + target.width + spacing > candidate.x
|
|
2696
|
-
);
|
|
2697
|
-
});
|
|
2698
|
-
if (collides) continue;
|
|
2699
|
-
target.x = desiredTargetX;
|
|
2700
|
-
const alignedX = source.x + (endpoints.start.x - source.originalX);
|
|
2701
|
-
for (const segment of compactables) {
|
|
2702
|
-
if (segment.kind === "segment" && segment.edgeId === edge.id) segment.x = alignedX;
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
3173
|
}
|
|
3174
|
+
|
|
2706
3175
|
if (input.direction === "right" && centerLabeledEdgeIds.size > 0) {
|
|
2707
3176
|
const itemByNodeId = new Map(
|
|
2708
3177
|
compactables.flatMap((item) =>
|
|
@@ -2715,7 +3184,7 @@ export function applyPostCompaction(
|
|
|
2715
3184
|
// A late target nudge would invalidate its outgoing label constraints.
|
|
2716
3185
|
if (
|
|
2717
3186
|
!centerLabeledEdgeIds.has(edge.id) ||
|
|
2718
|
-
|
|
3187
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
2719
3188
|
)
|
|
2720
3189
|
continue;
|
|
2721
3190
|
const source = itemByNodeId.get(edge.sourceId);
|
|
@@ -2870,6 +3339,7 @@ function nodeFlowOffset(
|
|
|
2870
3339
|
layerFlowSize: number,
|
|
2871
3340
|
nodeFlowSize: number,
|
|
2872
3341
|
layerByNodeId: ReadonlyMap<string, number>,
|
|
3342
|
+
margins?: { leading: number; trailing: number; maxLeading: number; maxTrailing: number },
|
|
2873
3343
|
): number {
|
|
2874
3344
|
const node = input.graph.nodes.find((candidate) => candidate.id === id);
|
|
2875
3345
|
const alignment = node
|
|
@@ -2880,33 +3350,87 @@ function nodeFlowOffset(
|
|
|
2880
3350
|
else if (alignment === "RIGHT" || alignment === "BOTTOM") ratio = 1;
|
|
2881
3351
|
else if (alignment === "CENTER") ratio = 0.5;
|
|
2882
3352
|
else {
|
|
2883
|
-
|
|
2884
|
-
|
|
3353
|
+
const incoming = new Set<string>();
|
|
3354
|
+
const outgoing = new Set<string>();
|
|
2885
3355
|
const ownLayer = layerByNodeId.get(id) ?? 0;
|
|
3356
|
+
const orientation = getPlacementOrientation(input);
|
|
2886
3357
|
for (const edge of input.graph.edges) {
|
|
2887
3358
|
if (edge.sourceId !== id && edge.targetId !== id) continue;
|
|
2888
|
-
|
|
3359
|
+
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3360
|
+
const source = edge.sourceId === id;
|
|
3361
|
+
const portName = source ? edge.sourcePort : edge.targetPort;
|
|
3362
|
+
const key = portName === undefined ? `edge:${edge.id}` : `port:${portName}`;
|
|
3363
|
+
const otherId = source ? edge.targetId : edge.sourceId;
|
|
2889
3364
|
const otherLayer = layerByNodeId.get(otherId) ?? ownLayer;
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3365
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3366
|
+
const isOutgoing = orientation
|
|
3367
|
+
? source !== orientation.reversedEdgeIds.has(edge.id)
|
|
3368
|
+
: otherLayer !== ownLayer
|
|
3369
|
+
? otherLayer > ownLayer
|
|
3370
|
+
: id.startsWith("__layout_dummy:") && port?.direction !== "inout"
|
|
3371
|
+
? port?.direction === "out"
|
|
3372
|
+
: source;
|
|
3373
|
+
(isOutgoing ? outgoing : incoming).add(key);
|
|
2894
3374
|
}
|
|
2895
|
-
ratio =
|
|
3375
|
+
ratio =
|
|
3376
|
+
incoming.size + outgoing.size === 0 ? 0.5 : outgoing.size / (incoming.size + outgoing.size);
|
|
3377
|
+
}
|
|
3378
|
+
let position = (layerFlowSize - nodeFlowSize) * ratio;
|
|
3379
|
+
if (margins) {
|
|
3380
|
+
if (ratio > 0.5) position -= margins.maxTrailing * 2 * (ratio - 0.5);
|
|
3381
|
+
else if (ratio < 0.5) position += margins.maxLeading * 2 * (0.5 - ratio);
|
|
3382
|
+
position = Math.max(position, margins.leading);
|
|
3383
|
+
position = Math.min(position, layerFlowSize - margins.trailing - nodeFlowSize);
|
|
2896
3384
|
}
|
|
2897
|
-
return
|
|
3385
|
+
return position;
|
|
2898
3386
|
}
|
|
2899
3387
|
|
|
2900
3388
|
export const placeNodesInLayers: NodePlacer = (input, order) => {
|
|
2901
3389
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
3390
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
3391
|
+
const orientation = getPlacementOrientation(input);
|
|
3392
|
+
const loopMargins = preparedLoopEnvelopes(input);
|
|
3393
|
+
const flowMargins = new Map(
|
|
3394
|
+
input.graph.nodes.map((node) => {
|
|
3395
|
+
const size = input.sizes.get(node.id) ?? { width: 0, height: 0 };
|
|
3396
|
+
const ports = placePorts(
|
|
3397
|
+
node.ports,
|
|
3398
|
+
{ x: 0, y: 0, ...size },
|
|
3399
|
+
input.direction,
|
|
3400
|
+
(port) => input.portSettings?.(port, node),
|
|
3401
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
3402
|
+
orientation
|
|
3403
|
+
? (port) => getOrientedPortDirection(input, orientation, node, port)
|
|
3404
|
+
: undefined,
|
|
3405
|
+
);
|
|
3406
|
+
let low = 0,
|
|
3407
|
+
high = 0;
|
|
3408
|
+
for (const port of ports ?? []) {
|
|
3409
|
+
const start = horizontal ? (port.x ?? 0) : (port.y ?? 0);
|
|
3410
|
+
const end = start + (horizontal ? (port.width ?? 0) : (port.height ?? 0));
|
|
3411
|
+
low = Math.max(low, -start);
|
|
3412
|
+
high = Math.max(high, end - (horizontal ? size.width : size.height));
|
|
3413
|
+
}
|
|
3414
|
+
return [
|
|
3415
|
+
node.id,
|
|
3416
|
+
{
|
|
3417
|
+
leading: Math.max(reverse ? high : low, loopMargins?.get(node.id)?.flowBefore ?? 0),
|
|
3418
|
+
trailing: Math.max(reverse ? low : high, loopMargins?.get(node.id)?.flowAfter ?? 0),
|
|
3419
|
+
},
|
|
3420
|
+
] as const;
|
|
3421
|
+
}),
|
|
3422
|
+
);
|
|
2902
3423
|
const layerByNodeId = new Map(
|
|
2903
3424
|
order.layers.flatMap((layer, layerIndex) => layer.map((id) => [id, layerIndex] as const)),
|
|
2904
3425
|
);
|
|
2905
3426
|
const layerFlowSizes = order.layers.map((layer) => {
|
|
2906
3427
|
const size = Math.max(
|
|
2907
3428
|
0,
|
|
2908
|
-
...layer.map(
|
|
2909
|
-
|
|
3429
|
+
...layer.map(
|
|
3430
|
+
(id) =>
|
|
3431
|
+
(horizontal ? (input.sizes.get(id)?.width ?? 0) : (input.sizes.get(id)?.height ?? 0)) +
|
|
3432
|
+
(flowMargins.get(id)?.leading ?? 0) +
|
|
3433
|
+
(flowMargins.get(id)?.trailing ?? 0),
|
|
2910
3434
|
),
|
|
2911
3435
|
);
|
|
2912
3436
|
return size === 0 &&
|
|
@@ -2941,6 +3465,12 @@ export const placeNodesInLayers: NodePlacer = (input, order) => {
|
|
|
2941
3465
|
layerFlowSizes[layerIndex] ?? 0,
|
|
2942
3466
|
horizontal ? size.width : size.height,
|
|
2943
3467
|
layerByNodeId,
|
|
3468
|
+
{
|
|
3469
|
+
leading: flowMargins.get(id)?.leading ?? 0,
|
|
3470
|
+
trailing: flowMargins.get(id)?.trailing ?? 0,
|
|
3471
|
+
maxLeading: Math.max(0, ...layer.map((id) => flowMargins.get(id)?.leading ?? 0)),
|
|
3472
|
+
maxTrailing: Math.max(0, ...layer.map((id) => flowMargins.get(id)?.trailing ?? 0)),
|
|
3473
|
+
},
|
|
2944
3474
|
);
|
|
2945
3475
|
const rect = horizontal
|
|
2946
3476
|
? { x: centeredFlow, y: cross, ...size }
|
|
@@ -3073,22 +3603,42 @@ export const placeNodesInteractively: NodePlacer = (input, order) => {
|
|
|
3073
3603
|
return { rectByNodeId };
|
|
3074
3604
|
};
|
|
3075
3605
|
|
|
3076
|
-
function getPortPoint(
|
|
3606
|
+
export function getPortPoint(
|
|
3077
3607
|
node: GraphNode,
|
|
3078
3608
|
portName: string | undefined,
|
|
3079
3609
|
rect: EntityRect,
|
|
3080
3610
|
fallback: Point,
|
|
3081
3611
|
direction: LayeredPhaseInput["direction"],
|
|
3082
3612
|
input: LayeredPhaseInput,
|
|
3613
|
+
/** Reuses each node's placed ports across calls while settings stay fixed. */
|
|
3614
|
+
placedPorts?: Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>,
|
|
3083
3615
|
): Point {
|
|
3084
3616
|
if (portName === undefined) return fallback;
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3617
|
+
const cached = placedPorts?.get(node);
|
|
3618
|
+
let ports: GraphPort[] | undefined;
|
|
3619
|
+
if (
|
|
3620
|
+
cached &&
|
|
3621
|
+
cached.rect.x === rect.x &&
|
|
3622
|
+
cached.rect.y === rect.y &&
|
|
3623
|
+
cached.rect.width === rect.width &&
|
|
3624
|
+
cached.rect.height === rect.height
|
|
3625
|
+
)
|
|
3626
|
+
ports = cached.ports;
|
|
3627
|
+
else {
|
|
3628
|
+
const orientation = getPlacementOrientation(input);
|
|
3629
|
+
ports = placePorts(
|
|
3630
|
+
node.ports,
|
|
3631
|
+
rect,
|
|
3632
|
+
direction,
|
|
3633
|
+
(candidate) => input.portSettings?.(candidate, node),
|
|
3634
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
3635
|
+
orientation
|
|
3636
|
+
? (candidate) => getOrientedPortDirection(input, orientation, node, candidate)
|
|
3637
|
+
: undefined,
|
|
3638
|
+
);
|
|
3639
|
+
placedPorts?.set(node, { rect: { ...rect }, ports });
|
|
3640
|
+
}
|
|
3641
|
+
const port = ports?.find((candidate) => candidate.name === portName);
|
|
3092
3642
|
if (port?.x === undefined || port.y === undefined) return fallback;
|
|
3093
3643
|
const settings = input.portSettings?.(port, node);
|
|
3094
3644
|
const configuredAnchor = settings?.["port.anchor"] as { x?: number; y?: number } | undefined;
|
|
@@ -3102,7 +3652,31 @@ function getPortPoint(
|
|
|
3102
3652
|
};
|
|
3103
3653
|
}
|
|
3104
3654
|
|
|
3105
|
-
|
|
3655
|
+
/**
|
|
3656
|
+
* Drop interior bends where an orthogonal route doubles back along one line,
|
|
3657
|
+
* so it never retraces itself. Terminal segments keep their port direction.
|
|
3658
|
+
*/
|
|
3659
|
+
export function removeRouteSpurs(points: readonly Point[]): readonly Point[] {
|
|
3660
|
+
const equal = (left: number, right: number) => Math.abs(left - right) < 1e-9;
|
|
3661
|
+
const route = points.filter(
|
|
3662
|
+
(point, index) =>
|
|
3663
|
+
index === 0 || !equal(point.x, points[index - 1]!.x) || !equal(point.y, points[index - 1]!.y),
|
|
3664
|
+
);
|
|
3665
|
+
let changed = false;
|
|
3666
|
+
for (let index = 2; index < route.length - 2; index++) {
|
|
3667
|
+
const [previous, middle, next] = [route[index - 1]!, route[index]!, route[index + 1]!];
|
|
3668
|
+
const sameX = equal(previous.x, middle.x) && equal(middle.x, next.x),
|
|
3669
|
+
sameY = equal(previous.y, middle.y) && equal(middle.y, next.y);
|
|
3670
|
+
const axis = sameX ? "y" : sameY ? "x" : undefined;
|
|
3671
|
+
if (!axis || (middle[axis] - previous[axis]) * (next[axis] - middle[axis]) >= 0) continue;
|
|
3672
|
+
route.splice(index, 1);
|
|
3673
|
+
changed = true;
|
|
3674
|
+
index = Math.max(1, index - 2);
|
|
3675
|
+
}
|
|
3676
|
+
return changed ? route : points;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
export function simplifyRoute(points: readonly Point[]): Point[] {
|
|
3106
3680
|
const equal = (left: number, right: number) => Math.abs(left - right) < 1e-9;
|
|
3107
3681
|
const simplified: Point[] = [];
|
|
3108
3682
|
for (const point of points) {
|
|
@@ -3135,6 +3709,7 @@ function implicitEdgeEndpoints(
|
|
|
3135
3709
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
3136
3710
|
const groups = new Map<string, Array<{ edge: GraphEdge; endpoint: "source" | "target" }>>();
|
|
3137
3711
|
for (const edge of input.graph.edges) {
|
|
3712
|
+
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3138
3713
|
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
3139
3714
|
const targetRect = placement.rectByNodeId.get(edge.targetId);
|
|
3140
3715
|
if (!sourceRect || !targetRect) continue;
|
|
@@ -3142,7 +3717,16 @@ function implicitEdgeEndpoints(
|
|
|
3142
3717
|
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
3143
3718
|
const modelOrderPromotion =
|
|
3144
3719
|
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3145
|
-
const
|
|
3720
|
+
const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
|
|
3721
|
+
const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
|
|
3722
|
+
// A zero-gap boundary can coincide with the node's flow origin.
|
|
3723
|
+
const forward =
|
|
3724
|
+
modelOrderPromotion ||
|
|
3725
|
+
(sourceFlow === targetFlow && (sourceBoundary || targetBoundary)
|
|
3726
|
+
? sourceBoundary
|
|
3727
|
+
? sourceBoundary.port.side === "EAST" || sourceBoundary.port.side === "SOUTH"
|
|
3728
|
+
: targetBoundary!.port.side === "WEST" || targetBoundary!.port.side === "NORTH"
|
|
3729
|
+
: sourceFlow <= targetFlow);
|
|
3146
3730
|
const feedback =
|
|
3147
3731
|
input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
|
|
3148
3732
|
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
@@ -3167,11 +3751,25 @@ function implicitEdgeEndpoints(
|
|
|
3167
3751
|
? "after"
|
|
3168
3752
|
: undefined;
|
|
3169
3753
|
};
|
|
3754
|
+
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3755
|
+
const node = nodeById.get(nodeId);
|
|
3756
|
+
const constraints = node && input.nodeSettings?.(node)?.portConstraints;
|
|
3757
|
+
if (
|
|
3758
|
+
portName !== undefined ||
|
|
3759
|
+
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3760
|
+
)
|
|
3761
|
+
return undefined;
|
|
3762
|
+
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3763
|
+
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3764
|
+
return source !== directionReversed ? "after" : "before";
|
|
3765
|
+
};
|
|
3170
3766
|
const sourceSide =
|
|
3171
3767
|
endpointSide(edge.sourceId, edge.sourcePort) ??
|
|
3768
|
+
fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ??
|
|
3172
3769
|
(feedback ? (directionReversed ? "before" : "after") : forward ? "after" : "before");
|
|
3173
3770
|
const targetSide =
|
|
3174
3771
|
endpointSide(edge.targetId, edge.targetPort) ??
|
|
3772
|
+
fixedImplicitSide(edge.targetId, edge.targetPort, false) ??
|
|
3175
3773
|
(feedback ? (directionReversed ? "after" : "before") : forward ? "before" : "after");
|
|
3176
3774
|
const sourceKey = `${edge.sourceId}:${sourceSide}`;
|
|
3177
3775
|
const targetKey = `${edge.targetId}:${targetSide}`;
|
|
@@ -3185,6 +3783,20 @@ function implicitEdgeEndpoints(
|
|
|
3185
3783
|
|
|
3186
3784
|
const result = new Map<string, { source: Point; target: Point }>();
|
|
3187
3785
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
3786
|
+
const placedPorts = new Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>();
|
|
3787
|
+
const portSideCounts = new Map<GraphNode, Map<unknown, number>>();
|
|
3788
|
+
const portSideCount = (node: GraphNode, side: unknown): number => {
|
|
3789
|
+
let counts = portSideCounts.get(node);
|
|
3790
|
+
if (!counts) {
|
|
3791
|
+
counts = new Map();
|
|
3792
|
+
for (const port of node.ports ?? []) {
|
|
3793
|
+
const portSide = input.portSettings?.(port, node)?.["port.side"];
|
|
3794
|
+
counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
|
|
3795
|
+
}
|
|
3796
|
+
portSideCounts.set(node, counts);
|
|
3797
|
+
}
|
|
3798
|
+
return counts.get(side) ?? 0;
|
|
3799
|
+
};
|
|
3188
3800
|
for (const [key, entries] of groups) {
|
|
3189
3801
|
const split = key.lastIndexOf(":");
|
|
3190
3802
|
const nodeId = key.slice(0, split);
|
|
@@ -3235,9 +3847,23 @@ function implicitEdgeEndpoints(
|
|
|
3235
3847
|
unzipping ||
|
|
3236
3848
|
interactiveTargetOrder ||
|
|
3237
3849
|
verticalNoneTargetOrder ||
|
|
3238
|
-
|
|
3239
|
-
|
|
3850
|
+
crossingStrategy === "LAYER_SWEEP" ||
|
|
3851
|
+
crossingStrategy === "MEDIAN_LAYER_SWEEP";
|
|
3852
|
+
const sweptOrder = getPlacementOrder(input);
|
|
3240
3853
|
entries.sort((left, right) => {
|
|
3854
|
+
const leftIncoming =
|
|
3855
|
+
(left.endpoint === "target") !== (orientation?.reversedEdgeIds.has(left.edge.id) ?? false);
|
|
3856
|
+
const rightIncoming =
|
|
3857
|
+
(right.endpoint === "target") !==
|
|
3858
|
+
(orientation?.reversedEdgeIds.has(right.edge.id) ?? false);
|
|
3859
|
+
if (leftIncoming === rightIncoming) {
|
|
3860
|
+
const selected = (
|
|
3861
|
+
leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId
|
|
3862
|
+
)?.get(nodeId);
|
|
3863
|
+
const a = selected?.indexOf(left.edge.id) ?? -1,
|
|
3864
|
+
b = selected?.indexOf(right.edge.id) ?? -1;
|
|
3865
|
+
if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
|
|
3866
|
+
}
|
|
3241
3867
|
if (interactiveLeftSourceOrder) {
|
|
3242
3868
|
const leftExtreme = interactiveLeftSourceOrder[0] === left.edge.id;
|
|
3243
3869
|
const rightExtreme = interactiveLeftSourceOrder[0] === right.edge.id;
|
|
@@ -3377,6 +4003,32 @@ function implicitEdgeEndpoints(
|
|
|
3377
4003
|
? { ...point, y: Math.round(point.y) }
|
|
3378
4004
|
: { ...point, x: Math.round(point.x) };
|
|
3379
4005
|
}
|
|
4006
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
4007
|
+
const explicitPort = node?.ports?.find((port) => port.name === portName);
|
|
4008
|
+
const physicalSide =
|
|
4009
|
+
node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.side"] : undefined;
|
|
4010
|
+
const singleFixedSidePort =
|
|
4011
|
+
constraints === "FIXED_SIDE" &&
|
|
4012
|
+
physicalSide !== undefined &&
|
|
4013
|
+
node?.ports !== undefined &&
|
|
4014
|
+
portSideCount(node, physicalSide) === 1;
|
|
4015
|
+
// Movable port ordering comes from the layer sweep above. Fixed coordinates
|
|
4016
|
+
// and authored anchors must replace the synthesized node-origin fallback.
|
|
4017
|
+
const authoredAnchor =
|
|
4018
|
+
node && explicitPort
|
|
4019
|
+
? input.portSettings?.(explicitPort, node)?.["port.anchor"]
|
|
4020
|
+
: undefined;
|
|
4021
|
+
if (
|
|
4022
|
+
portName !== undefined &&
|
|
4023
|
+
node &&
|
|
4024
|
+
(constraints === "FIXED_POS" ||
|
|
4025
|
+
constraints === "FIXED_RATIO" ||
|
|
4026
|
+
constraints === "FIXED_ORDER" ||
|
|
4027
|
+
singleFixedSidePort ||
|
|
4028
|
+
authoredAnchor !== undefined)
|
|
4029
|
+
) {
|
|
4030
|
+
point = getPortPoint(node, portName, rect, point, input.direction, input, placedPorts);
|
|
4031
|
+
}
|
|
3380
4032
|
const pair = result.get(edge.id) ?? { source: point, target: point };
|
|
3381
4033
|
pair[endpoint] = point;
|
|
3382
4034
|
result.set(edge.id, pair);
|
|
@@ -3400,6 +4052,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3400
4052
|
);
|
|
3401
4053
|
};
|
|
3402
4054
|
const pointsByEdgeId = new Map<string, readonly Point[]>();
|
|
4055
|
+
const junctionGroups: OrthogonalJunctionGroup[] = [];
|
|
4056
|
+
const physicalOrder = new Map(
|
|
4057
|
+
[
|
|
4058
|
+
...input.graph.edges
|
|
4059
|
+
.filter((edge) => !orientation.reversedEdgeIds.has(edge.id))
|
|
4060
|
+
.map((edge) => edge.id),
|
|
4061
|
+
...orientation.reversedEdgeIds,
|
|
4062
|
+
].map((id, index) => [id, index]),
|
|
4063
|
+
);
|
|
4064
|
+
|
|
3403
4065
|
const splineNubControlsByEdgeId = new Map<string, readonly Point[]>();
|
|
3404
4066
|
const outsideFeedbackEdgeIds = new Set<string>();
|
|
3405
4067
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
@@ -3417,6 +4079,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3417
4079
|
(mutableRects.get(leftId)?.x ?? 0) - (mutableRects.get(rightId)?.x ?? 0),
|
|
3418
4080
|
);
|
|
3419
4081
|
for (const [id, loops] of selfLoopEntriesByX) {
|
|
4082
|
+
if (hasPlacementLoopEnvelope(placement, id)) continue;
|
|
3420
4083
|
const rect = mutableRects.get(id);
|
|
3421
4084
|
const node = nodeById.get(id);
|
|
3422
4085
|
if (!rect || !node) continue;
|
|
@@ -3479,14 +4142,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3479
4142
|
edge: GraphEdge,
|
|
3480
4143
|
side: string,
|
|
3481
4144
|
ordering: string,
|
|
4145
|
+
canonical = false,
|
|
3482
4146
|
): number => {
|
|
3483
|
-
const
|
|
4147
|
+
const crossSide = side === "NORTH" || side === "SOUTH";
|
|
4148
|
+
const axis = crossSide === (!canonical || horizontal) ? "height" : "width";
|
|
3484
4149
|
const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
|
|
3485
4150
|
const index = ordered.indexOf(edge);
|
|
3486
4151
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
3487
4152
|
const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
3488
4153
|
const size = (candidate: GraphEdge) =>
|
|
3489
|
-
input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true
|
|
4154
|
+
canonical || input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true
|
|
3490
4155
|
? (candidate[axis] ?? 0)
|
|
3491
4156
|
: 0;
|
|
3492
4157
|
return (
|
|
@@ -3498,11 +4163,19 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3498
4163
|
extent + (size(candidate) > 0 ? size(candidate) + labelSpacing : 0),
|
|
3499
4164
|
0,
|
|
3500
4165
|
) +
|
|
3501
|
-
(size(edge) > 0
|
|
4166
|
+
(size(edge) > 0 && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
4167
|
+
? size(edge) / 2 + labelSpacing
|
|
4168
|
+
: 0)
|
|
3502
4169
|
);
|
|
3503
4170
|
};
|
|
4171
|
+
const hasFixedLoopPorts = (node: GraphNode): boolean => {
|
|
4172
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4173
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4174
|
+
};
|
|
3504
4175
|
const northReserveByLayer = new Map<number, number>();
|
|
3505
|
-
for (const [id,
|
|
4176
|
+
for (const [id, nodeLoops] of selfLoopsByNodeId) {
|
|
4177
|
+
const loops = nodeLoops.filter(() => !hasPlacementLoopEnvelope(placement, id));
|
|
4178
|
+
if (loops.length === 0) continue;
|
|
3506
4179
|
const rect = mutableRects.get(id);
|
|
3507
4180
|
if (!rect) continue;
|
|
3508
4181
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
@@ -3518,8 +4191,36 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3518
4191
|
y: rect.y + spacing + splineOffset,
|
|
3519
4192
|
});
|
|
3520
4193
|
} else {
|
|
3521
|
-
const
|
|
3522
|
-
|
|
4194
|
+
const northLoops = loops.filter((edge, index) => {
|
|
4195
|
+
if (
|
|
4196
|
+
style === "ORTHOGONAL" &&
|
|
4197
|
+
node &&
|
|
4198
|
+
edge.sourcePort !== undefined &&
|
|
4199
|
+
edge.targetPort !== undefined &&
|
|
4200
|
+
hasFixedLoopPorts(node)
|
|
4201
|
+
) {
|
|
4202
|
+
const sourcePort = node.ports?.find((port) => port.name === edge.sourcePort);
|
|
4203
|
+
const targetPort = node.ports?.find((port) => port.name === edge.targetPort);
|
|
4204
|
+
return (
|
|
4205
|
+
(sourcePort && input.portSettings?.(sourcePort, node)?.["port.side"] === "NORTH") ||
|
|
4206
|
+
(targetPort && input.portSettings?.(targetPort, node)?.["port.side"] === "NORTH")
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
4209
|
+
if (
|
|
4210
|
+
style === "ORTHOGONAL" &&
|
|
4211
|
+
node &&
|
|
4212
|
+
(edge.sourcePort === undefined || edge.targetPort === undefined) &&
|
|
4213
|
+
String(nodeSettings?.portConstraints ?? "FREE") !== "FREE" &&
|
|
4214
|
+
String(nodeSettings?.portConstraints ?? "UNDEFINED") !== "UNDEFINED"
|
|
4215
|
+
) {
|
|
4216
|
+
const port = node.ports?.find(
|
|
4217
|
+
(candidate) => candidate.name === (edge.sourcePort ?? edge.targetPort),
|
|
4218
|
+
);
|
|
4219
|
+
if (port) return input.portSettings?.(port, node)?.["port.side"] === "NORTH";
|
|
4220
|
+
}
|
|
4221
|
+
return distribution !== "NORTH_SOUTH" || index % 2 === 0;
|
|
4222
|
+
});
|
|
4223
|
+
const sideLoopCount = northLoops.length;
|
|
3523
4224
|
const reserve =
|
|
3524
4225
|
(ordering === "SEQUENCED" ? Math.min(1, sideLoopCount) : sideLoopCount) * spacing +
|
|
3525
4226
|
splineOffset;
|
|
@@ -3644,6 +4345,50 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3644
4345
|
}
|
|
3645
4346
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
3646
4347
|
}
|
|
4348
|
+
const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
|
|
4349
|
+
const labelFlowLayers = new Set(
|
|
4350
|
+
[...flowLayerByNodeId]
|
|
4351
|
+
.filter(([id]) => id.startsWith("__layout_dummy:label:"))
|
|
4352
|
+
.map(([, layer]) => layer),
|
|
4353
|
+
);
|
|
4354
|
+
|
|
4355
|
+
// Routing advances past the occupied layer, including protruding ports.
|
|
4356
|
+
// Assign layers from bodies first so port envelopes cannot merge ranks.
|
|
4357
|
+
for (const [id, rect] of placement.rectByNodeId) {
|
|
4358
|
+
const node = nodeById.get(id);
|
|
4359
|
+
const bounds = flowLayers[flowLayerByNodeId.get(id) ?? -1];
|
|
4360
|
+
if (!node || !bounds) continue;
|
|
4361
|
+
const envelope = hasPlacementLoopEnvelope(placement, id)
|
|
4362
|
+
? preparedLoopEnvelopes(input)?.get(id)
|
|
4363
|
+
: undefined;
|
|
4364
|
+
if (envelope) {
|
|
4365
|
+
const negative = input.direction === "left" || input.direction === "up";
|
|
4366
|
+
const start = horizontal ? rect.x : rect.y;
|
|
4367
|
+
const size = horizontal ? rect.width : rect.height;
|
|
4368
|
+
bounds.start = Math.min(
|
|
4369
|
+
bounds.start,
|
|
4370
|
+
start - (negative ? envelope.flowAfter : envelope.flowBefore),
|
|
4371
|
+
);
|
|
4372
|
+
bounds.end = Math.max(
|
|
4373
|
+
bounds.end,
|
|
4374
|
+
start + size + (negative ? envelope.flowBefore : envelope.flowAfter),
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
const ports = placePorts(
|
|
4378
|
+
node.ports,
|
|
4379
|
+
rect,
|
|
4380
|
+
input.direction,
|
|
4381
|
+
(port) => input.portSettings?.(port, node),
|
|
4382
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
4383
|
+
(port) => getOrientedPortDirection(input, orientation, node, port),
|
|
4384
|
+
);
|
|
4385
|
+
for (const port of ports ?? []) {
|
|
4386
|
+
const start = horizontal ? rect.x + (port.x ?? 0) : rect.y + (port.y ?? 0);
|
|
4387
|
+
const end = start + (horizontal ? (port.width ?? 0) : (port.height ?? 0));
|
|
4388
|
+
bounds.start = Math.min(bounds.start, start);
|
|
4389
|
+
bounds.end = Math.max(bounds.end, end);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
3647
4392
|
|
|
3648
4393
|
const increasing = input.direction === "right" || input.direction === "down";
|
|
3649
4394
|
const flowPortProtrusion = (edge: GraphEdge, endpoint: "source" | "target"): number => {
|
|
@@ -3711,7 +4456,9 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3711
4456
|
const requiredGap =
|
|
3712
4457
|
labelFlowSize +
|
|
3713
4458
|
Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) +
|
|
3714
|
-
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing)
|
|
4459
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing) -
|
|
4460
|
+
flowPortProtrusion(edge, "source") -
|
|
4461
|
+
flowPortProtrusion(edge, "target");
|
|
3715
4462
|
const gap = Math.min(sourceLayer, targetLayer);
|
|
3716
4463
|
const availableGap = flowLayers[gap + 1]!.start - flowLayers[gap]!.end;
|
|
3717
4464
|
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
@@ -3743,26 +4490,84 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3743
4490
|
if (labelShift !== 0) implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
3744
4491
|
|
|
3745
4492
|
const orthogonalTrackByEdgeId = new Map<string, number>();
|
|
4493
|
+
// Cross-axis shift of an edge's attachment at a port carrying both directions.
|
|
4494
|
+
const attachmentShiftByEdgeId = new Map<string, { source?: number; target?: number }>();
|
|
4495
|
+
const attached = (edge: GraphEdge, end: "source" | "target", point: Point): Point => {
|
|
4496
|
+
const shift = attachmentShiftByEdgeId.get(edge.id)?.[end];
|
|
4497
|
+
if (shift === undefined) return point;
|
|
4498
|
+
return horizontal ? { x: point.x, y: point.y + shift } : { x: point.x + shift, y: point.y };
|
|
4499
|
+
};
|
|
4500
|
+
// Tracks given to edges that routing saw as straight, by cross extent.
|
|
4501
|
+
const fallbackTracks: Array<{ track: number; lo: number; hi: number; sign: number }> = [];
|
|
4502
|
+
/**
|
|
4503
|
+
* The gap's middle for an edge routing saw as straight. When its ends
|
|
4504
|
+
* moved apart since, the middle can be the track of an edge heading the
|
|
4505
|
+
* other way: take the nearest track free of those instead.
|
|
4506
|
+
*/
|
|
4507
|
+
const freeGapTrack = (edgeId: string, from: number, to: number, a: Point, b: Point) => {
|
|
4508
|
+
const middle = (from + to) / 2;
|
|
4509
|
+
if (style !== "ORTHOGONAL") return middle;
|
|
4510
|
+
const cross = (p: Point) => (horizontal ? p.y : p.x);
|
|
4511
|
+
const lo = Math.min(cross(a), cross(b)),
|
|
4512
|
+
hi = Math.max(cross(a), cross(b)),
|
|
4513
|
+
sign = Math.sign(cross(b) - cross(a));
|
|
4514
|
+
if (hi - lo < 1e-6) return middle;
|
|
4515
|
+
const spans = [
|
|
4516
|
+
...fallbackTracks,
|
|
4517
|
+
...[...orthogonalTrackByEdgeId].flatMap(([id, track]) => {
|
|
4518
|
+
const ends = implicitEndpoints.get(id);
|
|
4519
|
+
if (id === edgeId || !ends) return [];
|
|
4520
|
+
const values = [cross(ends.source), cross(ends.target)];
|
|
4521
|
+
return [
|
|
4522
|
+
{
|
|
4523
|
+
track,
|
|
4524
|
+
lo: Math.min(...values),
|
|
4525
|
+
hi: Math.max(...values),
|
|
4526
|
+
sign: Math.sign(values[1]! - values[0]!),
|
|
4527
|
+
},
|
|
4528
|
+
];
|
|
4529
|
+
}),
|
|
4530
|
+
];
|
|
4531
|
+
const spacing = Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10) / 2;
|
|
4532
|
+
const free = (track: number) =>
|
|
4533
|
+
!spans.some(
|
|
4534
|
+
(span) =>
|
|
4535
|
+
span.sign === -sign &&
|
|
4536
|
+
Math.abs(span.track - track) < 1e-6 &&
|
|
4537
|
+
Math.min(span.hi, hi) - Math.max(span.lo, lo) > 1e-6,
|
|
4538
|
+
);
|
|
4539
|
+
let chosen = middle;
|
|
4540
|
+
for (
|
|
4541
|
+
let step = 1;
|
|
4542
|
+
!free(chosen) && step <= 2 * Math.floor((to - from) / 2 / spacing);
|
|
4543
|
+
step++
|
|
4544
|
+
) {
|
|
4545
|
+
const candidate = middle + (step % 2 ? -1 : 1) * Math.ceil(step / 2) * spacing;
|
|
4546
|
+
if (candidate > from && candidate < to && free(candidate)) chosen = candidate;
|
|
4547
|
+
}
|
|
4548
|
+
fallbackTracks.push({ track: chosen, lo, hi, sign });
|
|
4549
|
+
return chosen;
|
|
4550
|
+
};
|
|
3746
4551
|
const orthogonalDetourByEdgeId = new Map<
|
|
3747
4552
|
string,
|
|
3748
4553
|
{ firstTrack: number; secondTrack: number; crossover: number }
|
|
3749
4554
|
>();
|
|
3750
4555
|
if (
|
|
3751
4556
|
style === "ORTHOGONAL" &&
|
|
3752
|
-
input.settings.mergeEdges !== true &&
|
|
3753
4557
|
(input.settings["wrapping.strategy"] ?? "OFF") === "OFF" &&
|
|
3754
|
-
input.settings.hierarchyHandling !== "INCLUDE_CHILDREN" &&
|
|
3755
4558
|
(input.settings["layerUnzipping.strategy"] ?? "NONE") === "NONE"
|
|
3756
4559
|
) {
|
|
3757
4560
|
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
3758
4561
|
const edgeEdgeSpacing = Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10);
|
|
3759
|
-
const candidatesByGap =
|
|
4562
|
+
const candidatesByGap = Array.from(
|
|
4563
|
+
{ length: flowLayers.length + 1 },
|
|
3760
4564
|
() =>
|
|
3761
4565
|
[] as Array<{
|
|
3762
4566
|
edge: GraphEdge;
|
|
3763
4567
|
sourceCross: number;
|
|
3764
4568
|
targetCross: number;
|
|
3765
4569
|
straight: boolean;
|
|
4570
|
+
sameLayerPortSide?: "source" | "target";
|
|
3766
4571
|
slot?: number;
|
|
3767
4572
|
secondSlot?: number;
|
|
3768
4573
|
crossover?: number;
|
|
@@ -3776,30 +4581,78 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3776
4581
|
endpoints === undefined ||
|
|
3777
4582
|
sourceLayer === undefined ||
|
|
3778
4583
|
targetLayer === undefined ||
|
|
3779
|
-
Math.abs(sourceLayer - targetLayer)
|
|
4584
|
+
Math.abs(sourceLayer - targetLayer) > 1
|
|
3780
4585
|
) {
|
|
3781
4586
|
continue;
|
|
3782
4587
|
}
|
|
4588
|
+
const sameLayer = sourceLayer === targetLayer;
|
|
4589
|
+
let positiveFace = true;
|
|
4590
|
+
if (sameLayer) {
|
|
4591
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
4592
|
+
const positiveSide = horizontal ? "EAST" : "SOUTH";
|
|
4593
|
+
const face = (id: string, name: string | undefined) => {
|
|
4594
|
+
const node = nodeById.get(id)!;
|
|
4595
|
+
const port = node.ports?.find((port) => port.name === name);
|
|
4596
|
+
if (port) return input.portSettings?.(port, node)?.["port.side"];
|
|
4597
|
+
// A merged hyperedge dummy keeps an inverted dummy's in-layer
|
|
4598
|
+
// edge, whose dummy ports face backward (input) or forward (output).
|
|
4599
|
+
if (!id.startsWith("__layout_dummy:") || (name !== "input" && name !== "output"))
|
|
4600
|
+
return undefined;
|
|
4601
|
+
const forward = { right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" }[
|
|
4602
|
+
input.direction
|
|
4603
|
+
]!;
|
|
4604
|
+
const backward = { EAST: "WEST", WEST: "EAST", SOUTH: "NORTH", NORTH: "SOUTH" }[
|
|
4605
|
+
forward
|
|
4606
|
+
]!;
|
|
4607
|
+
return name === "output" ? forward : backward;
|
|
4608
|
+
};
|
|
4609
|
+
const negativeSide = horizontal ? "WEST" : "NORTH";
|
|
4610
|
+
const sourceFace = face(edge.sourceId, edge.sourcePort);
|
|
4611
|
+
if (
|
|
4612
|
+
sourceFace !== face(edge.targetId, edge.targetPort) ||
|
|
4613
|
+
(sourceFace !== positiveSide && sourceFace !== negativeSide)
|
|
4614
|
+
)
|
|
4615
|
+
continue;
|
|
4616
|
+
positiveFace = sourceFace === positiveSide;
|
|
4617
|
+
}
|
|
3783
4618
|
const graphSourceCross = horizontal ? endpoints.source.y : endpoints.source.x;
|
|
3784
4619
|
const graphTargetCross = horizontal ? endpoints.target.y : endpoints.target.x;
|
|
3785
|
-
const sourceBeforeTarget =
|
|
4620
|
+
const sourceBeforeTarget = sameLayer
|
|
4621
|
+
? !orientation.reversedEdgeIds.has(edge.id)
|
|
4622
|
+
: sourceLayer < targetLayer;
|
|
3786
4623
|
const sourceCross = sourceBeforeTarget ? graphSourceCross : graphTargetCross;
|
|
3787
4624
|
const targetCross = sourceBeforeTarget ? graphTargetCross : graphSourceCross;
|
|
3788
|
-
|
|
4625
|
+
const gap = sameLayer
|
|
4626
|
+
? sourceLayer - (positiveFace ? 0 : 1)
|
|
4627
|
+
: Math.min(sourceLayer, targetLayer);
|
|
4628
|
+
candidatesByGap[gap + 1]?.push({
|
|
3789
4629
|
edge,
|
|
4630
|
+
...(sameLayer
|
|
4631
|
+
? {
|
|
4632
|
+
sameLayerPortSide:
|
|
4633
|
+
increasing === positiveFace ? ("source" as const) : ("target" as const),
|
|
4634
|
+
}
|
|
4635
|
+
: {}),
|
|
3790
4636
|
sourceCross,
|
|
3791
4637
|
targetCross,
|
|
3792
4638
|
straight: Math.abs(sourceCross - targetCross) < 1e-3,
|
|
3793
4639
|
});
|
|
3794
4640
|
}
|
|
3795
4641
|
|
|
4642
|
+
const routingPlacement = routingCoordinates(placement);
|
|
4643
|
+
const rawEndpoints = implicitEdgeEndpoints(input, routingPlacement, orientation);
|
|
3796
4644
|
const slotsByGap = candidatesByGap.map((candidates) => {
|
|
3797
4645
|
if (candidates.length === 0) return 0;
|
|
3798
4646
|
// ELK creates hyperedge segments by walking layer nodes and their ports,
|
|
3799
4647
|
// so dependency-cycle tie breaking observes cross-axis order.
|
|
3800
4648
|
candidates.sort(
|
|
3801
4649
|
(left, right) =>
|
|
3802
|
-
|
|
4650
|
+
(increasing
|
|
4651
|
+
? left.sourceCross - right.sourceCross
|
|
4652
|
+
: left.targetCross - right.targetCross) ||
|
|
4653
|
+
(increasing
|
|
4654
|
+
? left.targetCross - right.targetCross
|
|
4655
|
+
: left.sourceCross - right.sourceCross),
|
|
3803
4656
|
);
|
|
3804
4657
|
const minimumDifference = (values: readonly number[]) => {
|
|
3805
4658
|
const distinct = [...new Set(values)].sort((left, right) => left - right);
|
|
@@ -3809,261 +4662,287 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3809
4662
|
}
|
|
3810
4663
|
return minimum;
|
|
3811
4664
|
};
|
|
3812
|
-
const
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4665
|
+
const ports: OrthogonalPort[] = [];
|
|
4666
|
+
const byPort = new Map<string, OrthogonalPort>();
|
|
4667
|
+
const portByCandidate = new Map<
|
|
4668
|
+
(typeof candidates)[number],
|
|
4669
|
+
{ source: string; target: string }
|
|
4670
|
+
>();
|
|
4671
|
+
const physicalPort = (
|
|
4672
|
+
candidate: (typeof candidates)[number],
|
|
4673
|
+
side: "source" | "target",
|
|
4674
|
+
) => {
|
|
4675
|
+
const edge = candidate.edge;
|
|
4676
|
+
const sourceIsBefore =
|
|
4677
|
+
flowLayerByNodeId.get(edge.sourceId)! < flowLayerByNodeId.get(edge.targetId)!;
|
|
4678
|
+
const graphSource = candidate.sameLayerPortSide
|
|
4679
|
+
? (side === "source") !== orientation.reversedEdgeIds.has(edge.id)
|
|
4680
|
+
: (side === "source") === (sourceIsBefore === increasing);
|
|
4681
|
+
const nodeId = graphSource ? edge.sourceId : edge.targetId;
|
|
4682
|
+
const name = graphSource ? edge.sourcePort : edge.targetPort;
|
|
4683
|
+
return { graphSource, nodeId, name };
|
|
3830
4684
|
};
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
4685
|
+
// A port whose edges leave and enter it in this gap gives each
|
|
4686
|
+
// direction its own hypersegment: opposite directions never share a track.
|
|
4687
|
+
const directionsByPort = new Map<string, Set<boolean>>();
|
|
4688
|
+
for (const candidate of candidates)
|
|
4689
|
+
for (const side of ["source", "target"] as const) {
|
|
4690
|
+
const { graphSource, nodeId, name } = physicalPort(candidate, side);
|
|
4691
|
+
if (name === undefined || isMergedHyperedgeDummy(input, nodeId)) continue;
|
|
4692
|
+
const key = JSON.stringify([nodeId, name]);
|
|
4693
|
+
let directions = directionsByPort.get(key);
|
|
4694
|
+
if (!directions) directionsByPort.set(key, (directions = new Set()));
|
|
4695
|
+
directions.add(graphSource);
|
|
4696
|
+
}
|
|
4697
|
+
// The cross coordinate where a candidate meets this gap on `side`.
|
|
4698
|
+
const endPosition = (candidate: (typeof candidates)[number], side: "source" | "target") => {
|
|
4699
|
+
const position = candidate.sameLayerPortSide
|
|
4700
|
+
? side === "source"
|
|
4701
|
+
? candidate.sourceCross
|
|
4702
|
+
: candidate.targetCross
|
|
4703
|
+
: (side === "source") === increasing
|
|
4704
|
+
? candidate.sourceCross
|
|
4705
|
+
: candidate.targetCross;
|
|
4706
|
+
const endpoints = rawEndpoints.get(candidate.edge.id);
|
|
4707
|
+
const sourceBefore =
|
|
4708
|
+
flowLayerByNodeId.get(candidate.edge.sourceId)! <
|
|
4709
|
+
flowLayerByNodeId.get(candidate.edge.targetId)!;
|
|
4710
|
+
const rawPoint =
|
|
4711
|
+
endpoints &&
|
|
4712
|
+
((
|
|
4713
|
+
candidate.sameLayerPortSide
|
|
4714
|
+
? (side === "source") !== orientation.reversedEdgeIds.has(candidate.edge.id)
|
|
4715
|
+
: (side === "source") === (sourceBefore === increasing)
|
|
4716
|
+
)
|
|
4717
|
+
? endpoints.source
|
|
4718
|
+
: endpoints.target);
|
|
4719
|
+
return rawPoint ? (horizontal ? rawPoint.y : rawPoint.x) : position;
|
|
3835
4720
|
};
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
if (
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
4721
|
+
/**
|
|
4722
|
+
* At a port carrying both directions, the edges fan out from separate
|
|
4723
|
+
* attachments. Ordered by where their far ends lie (back along the
|
|
4724
|
+
* port's own boundary on its near side first, then across the gap,
|
|
4725
|
+
* then back along the boundary on its far side), the edges nest
|
|
4726
|
+
* without crossing; consecutive edges of one direction share an
|
|
4727
|
+
* attachment and a hypersegment. Routing sees each attachment where
|
|
4728
|
+
* it will be, so the track order respects the split.
|
|
4729
|
+
*/
|
|
4730
|
+
const runByEnd = new Map<string, number>();
|
|
4731
|
+
const runShift = new Map<string, number>();
|
|
4732
|
+
if (style === "ORTHOGONAL")
|
|
4733
|
+
for (const [key, directions] of directionsByPort) {
|
|
4734
|
+
if (directions.size < 2) continue;
|
|
4735
|
+
const [nodeId, name] = JSON.parse(key) as [string, string];
|
|
4736
|
+
const ends = candidates.flatMap((candidate, index) =>
|
|
4737
|
+
(["source", "target"] as const).flatMap((side) => {
|
|
4738
|
+
const port = physicalPort(candidate, side);
|
|
4739
|
+
if (port.nodeId !== nodeId || port.name !== name) return [];
|
|
4740
|
+
const other = side === "source" ? "target" : "source";
|
|
4741
|
+
return [
|
|
4742
|
+
{
|
|
4743
|
+
end: `${index}:${side}`,
|
|
4744
|
+
graphSource: port.graphSource,
|
|
4745
|
+
at: endPosition(candidate, side),
|
|
4746
|
+
far: endPosition(candidate, other),
|
|
4747
|
+
near: candidate.sameLayerPortSide !== undefined,
|
|
4748
|
+
},
|
|
4749
|
+
];
|
|
4750
|
+
}),
|
|
4751
|
+
);
|
|
4752
|
+
const at = ends[0]!.at;
|
|
4753
|
+
if (ends.some((end) => Math.abs(end.at - at) > 1e-6)) continue;
|
|
4754
|
+
const group = (end: (typeof ends)[number]) => (!end.near ? 1 : end.far < at ? 0 : 2);
|
|
4755
|
+
const sorted = [...ends].sort(
|
|
4756
|
+
(a, b) =>
|
|
4757
|
+
group(a) - group(b) ||
|
|
4758
|
+
(group(a) === 1 ? a.far - b.far : b.far - a.far) ||
|
|
4759
|
+
Number(a.graphSource) - Number(b.graphSource),
|
|
4760
|
+
);
|
|
4761
|
+
let runs = 0;
|
|
4762
|
+
for (const [index, end] of sorted.entries()) {
|
|
4763
|
+
if (index && end.graphSource !== sorted[index - 1]!.graphSource) runs++;
|
|
4764
|
+
runByEnd.set(end.end, runs);
|
|
3871
4765
|
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
)
|
|
3888
|
-
|
|
3889
|
-
dependencies[ordinary]?.add(dummy);
|
|
3890
|
-
dependencyWeights[ordinary]?.set(dummy, 1);
|
|
3891
|
-
}
|
|
4766
|
+
const count = runs + 1;
|
|
4767
|
+
// A run holding an edge straight across the gap stays in place.
|
|
4768
|
+
const straight = new Set(
|
|
4769
|
+
sorted
|
|
4770
|
+
.filter((end) => !end.near && Math.abs(end.far - at) < 1e-3)
|
|
4771
|
+
.map((end) => runByEnd.get(end.end)!),
|
|
4772
|
+
);
|
|
4773
|
+
const anchor = straight.size === 1 ? [...straight][0]! : (count - 1) / 2;
|
|
4774
|
+
const rect = routingPlacement.rectByNodeId.get(nodeId);
|
|
4775
|
+
const low = rect ? (horizontal ? rect.y : rect.x) : -Infinity,
|
|
4776
|
+
high = rect ? low + (horizontal ? rect.height : rect.width) : Infinity;
|
|
4777
|
+
// Attachments stay on the node's side, closer together when it is short.
|
|
4778
|
+
let step = edgeEdgeSpacing;
|
|
4779
|
+
for (const run of [0, count - 1]) {
|
|
4780
|
+
const offset = run - anchor;
|
|
4781
|
+
if (offset < 0) step = Math.min(step, (at - low) / -offset);
|
|
4782
|
+
if (offset > 0) step = Math.min(step, (high - at) / offset);
|
|
3892
4783
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
const nonStraight = candidates.filter(({ straight }) => !straight);
|
|
3897
|
-
let splitCycle = false;
|
|
3898
|
-
for (let left = 0; left < candidates.length - 1 && !splitCycle; left++) {
|
|
3899
|
-
for (let right = left + 1; right < candidates.length; right++) {
|
|
3900
|
-
if (
|
|
3901
|
-
dependencies[left]?.has(right) &&
|
|
3902
|
-
dependencies[right]?.has(left) &&
|
|
3903
|
-
criticalDependencies.has(`${left}:${right}`) &&
|
|
3904
|
-
criticalDependencies.has(`${right}:${left}`)
|
|
3905
|
-
) {
|
|
3906
|
-
const random =
|
|
3907
|
-
phaseRandomByInput.get(input) ?? new JavaRandom(input.settings.randomSeed ?? 1);
|
|
3908
|
-
const detour = random.nextInt(2) === 0 ? candidates[right]! : candidates[left]!;
|
|
3909
|
-
const central = detour === candidates[left] ? candidates[right]! : candidates[left]!;
|
|
3910
|
-
detour.slot = 0;
|
|
3911
|
-
detour.secondSlot = 2;
|
|
3912
|
-
detour.crossover = (detour.sourceCross + central.sourceCross) / 2;
|
|
3913
|
-
central.slot = 1;
|
|
3914
|
-
removeDependency(left, right);
|
|
3915
|
-
removeDependency(right, left);
|
|
3916
|
-
splitCycle = true;
|
|
3917
|
-
break;
|
|
4784
|
+
if (!(step >= 2)) {
|
|
4785
|
+
for (const end of ends) runByEnd.delete(end.end);
|
|
4786
|
+
continue;
|
|
3918
4787
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
!criticalDependencies.has(`${right}:${left}`)
|
|
3930
|
-
) {
|
|
3931
|
-
const lowerSource =
|
|
3932
|
-
candidates[left]!.sourceCross < candidates[right]!.sourceCross ? left : right;
|
|
3933
|
-
const higherSource = lowerSource === left ? right : left;
|
|
3934
|
-
removeDependency(lowerSource, higherSource);
|
|
3935
|
-
}
|
|
4788
|
+
for (let run = 0; run < count; run++)
|
|
4789
|
+
runShift.set(JSON.stringify(["port", nodeId, name, run]), (run - anchor) * step);
|
|
4790
|
+
for (const end of ends) {
|
|
4791
|
+
const index = end.end.split(":")[0]!;
|
|
4792
|
+
const shift = (runByEnd.get(end.end)! - anchor) * step;
|
|
4793
|
+
if (Math.abs(shift) < 1e-9) continue;
|
|
4794
|
+
const id = candidates[Number(index)]!.edge.id;
|
|
4795
|
+
const record = attachmentShiftByEdgeId.get(id) ?? {};
|
|
4796
|
+
record[end.graphSource ? "source" : "target"] = shift;
|
|
4797
|
+
attachmentShiftByEdgeId.set(id, record);
|
|
3936
4798
|
}
|
|
3937
4799
|
}
|
|
3938
|
-
|
|
3939
|
-
const
|
|
3940
|
-
const
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4800
|
+
const portKey = (candidate: (typeof candidates)[number], side: "source" | "target") => {
|
|
4801
|
+
const edge = candidate.edge;
|
|
4802
|
+
const { graphSource, nodeId, name } = physicalPort(candidate, side);
|
|
4803
|
+
const node = nodeById.get(nodeId)!;
|
|
4804
|
+
if (isMergedHyperedgeDummy(input, nodeId))
|
|
4805
|
+
return JSON.stringify(["hyperedge", nodeId, candidate.sameLayerPortSide ?? side]);
|
|
4806
|
+
const run = runByEnd.get(`${candidates.indexOf(candidate)}:${side}`);
|
|
4807
|
+
if (name !== undefined && run !== undefined)
|
|
4808
|
+
return JSON.stringify(["port", nodeId, name, run]);
|
|
4809
|
+
if (name !== undefined)
|
|
4810
|
+
return directionsByPort.get(JSON.stringify([nodeId, name]))!.size > 1
|
|
4811
|
+
? JSON.stringify(["port", nodeId, name, graphSource])
|
|
4812
|
+
: JSON.stringify(["port", nodeId, name]);
|
|
4813
|
+
if (input.settings.mergeEdges === true || input.nodeSettings?.(node)?.hypernode === true)
|
|
4814
|
+
return JSON.stringify(["implicit", nodeId, candidate.sameLayerPortSide ?? side]);
|
|
4815
|
+
return JSON.stringify(["edge", edge.id, side]);
|
|
4816
|
+
};
|
|
4817
|
+
for (const side of ["source", "target"] as const)
|
|
4818
|
+
for (const candidate of candidates) {
|
|
4819
|
+
const key = portKey(candidate, side);
|
|
4820
|
+
if (!byPort.has(key)) {
|
|
4821
|
+
const port = {
|
|
4822
|
+
id: key,
|
|
4823
|
+
side: candidate.sameLayerPortSide ?? side,
|
|
4824
|
+
position: endPosition(candidate, side) + (runShift.get(key) ?? 0),
|
|
4825
|
+
};
|
|
4826
|
+
byPort.set(key, port);
|
|
4827
|
+
ports.push(port);
|
|
3959
4828
|
}
|
|
3960
4829
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
4830
|
+
for (const candidate of candidates)
|
|
4831
|
+
portByCandidate.set(candidate, {
|
|
4832
|
+
source: portKey(candidate, "source"),
|
|
4833
|
+
target: portKey(candidate, "target"),
|
|
4834
|
+
});
|
|
4835
|
+
const edgeLists = getCrossingUnits(input)?.edgeListRanks;
|
|
4836
|
+
const listedId = (edgeId: string) => edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "");
|
|
4837
|
+
const grouped = createOrthogonalHypersegments(
|
|
4838
|
+
ports,
|
|
4839
|
+
candidates.map((candidate) => portByCandidate.get(candidate)!),
|
|
4840
|
+
(index, list) => {
|
|
4841
|
+
const { edge } = candidates[index]!;
|
|
4842
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
4843
|
+
// The port side that lists this connection: its source lists it as outgoing.
|
|
4844
|
+
const node = (list === "outgoing") !== reversed ? edge.sourceId : edge.targetId;
|
|
4845
|
+
return edgeLists?.[list].get(node)?.get(listedId(edge.id));
|
|
4846
|
+
},
|
|
4847
|
+
);
|
|
4848
|
+
const criticalThreshold =
|
|
4849
|
+
0.2 *
|
|
4850
|
+
Math.min(
|
|
4851
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.incoming)),
|
|
4852
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.outgoing)),
|
|
3963
4853
|
);
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
const
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4854
|
+
const random =
|
|
4855
|
+
phaseRandomByInput.get(input) ?? new JavaRandom(input.settings.randomSeed ?? 1);
|
|
4856
|
+
const result = routeOrthogonalSegments(
|
|
4857
|
+
grouped.segments,
|
|
4858
|
+
0.5 * edgeEdgeSpacing,
|
|
4859
|
+
criticalThreshold,
|
|
4860
|
+
random,
|
|
4861
|
+
);
|
|
4862
|
+
const junctionGroup: (typeof junctionGroups)[number] = [];
|
|
4863
|
+
for (const group of grouped.segments) {
|
|
4864
|
+
for (const port of group.ports) {
|
|
4865
|
+
const outgoing = candidates.filter(
|
|
4866
|
+
(candidate) => portByCandidate.get(candidate)!.source === port,
|
|
4867
|
+
);
|
|
4868
|
+
// Merging adjacent helpers concatenates their physical incident
|
|
4869
|
+
// edges in retained layer order; root edge order loses that state.
|
|
4870
|
+
const listedOutgoing = getCrossingUnits(input)?.edgeListRanks?.outgoing;
|
|
4871
|
+
const listed = (edgeId: string) => edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "");
|
|
4872
|
+
outgoing.sort((a, b) => {
|
|
4873
|
+
// ELK visits a port's outgoing edges in list order, which records
|
|
4874
|
+
// reversal history; edges from dummies keep the fallbacks below.
|
|
4875
|
+
const sourceOf = (candidate: (typeof outgoing)[number]) =>
|
|
4876
|
+
orientation.reversedEdgeIds.has(candidate.edge.id)
|
|
4877
|
+
? candidate.edge.targetId
|
|
4878
|
+
: candidate.edge.sourceId;
|
|
4879
|
+
const ranks =
|
|
4880
|
+
sourceOf(a) === sourceOf(b) ? listedOutgoing?.get(sourceOf(a)) : undefined;
|
|
4881
|
+
const ra = ranks?.get(listed(a.edge.id)),
|
|
4882
|
+
rb = ranks?.get(listed(b.edge.id));
|
|
4883
|
+
if (ra !== undefined && rb !== undefined) return ra - rb;
|
|
4884
|
+
// Merging appends a lower dummy's edges after the kept dummy's.
|
|
4885
|
+
if (sourceOf(a) === sourceOf(b) && isMergedHyperedgeDummy(input, sourceOf(a))) {
|
|
4886
|
+
const merged = getPlacementOrder(input)?.outputPortOrderByNodeId?.get(sourceOf(a));
|
|
4887
|
+
const ma = merged?.indexOf(a.edge.id) ?? -1,
|
|
4888
|
+
mb = merged?.indexOf(b.edge.id) ?? -1;
|
|
4889
|
+
if (ma >= 0 && mb >= 0) return ma - mb;
|
|
4890
|
+
}
|
|
4891
|
+
// Reversing incoming edges appends them after retained outgoing
|
|
4892
|
+
// edges on the physical port, regardless of original model order.
|
|
4893
|
+
const reverseOrder =
|
|
4894
|
+
Number(orientation.reversedEdgeIds.has(a.edge.id)) -
|
|
4895
|
+
Number(orientation.reversedEdgeIds.has(b.edge.id));
|
|
4896
|
+
if (reverseOrder) return reverseOrder;
|
|
4897
|
+
const source = orientation.reversedEdgeIds.has(a.edge.id)
|
|
4898
|
+
? a.edge.targetId
|
|
4899
|
+
: a.edge.sourceId;
|
|
4900
|
+
const selected = getPlacementOrder(input)?.outputPortOrderByNodeId?.get(source);
|
|
4901
|
+
const aa = selected?.indexOf(a.edge.id) ?? -1;
|
|
4902
|
+
const bb = selected?.indexOf(b.edge.id) ?? -1;
|
|
4903
|
+
return aa >= 0 && bb >= 0
|
|
4904
|
+
? aa - bb
|
|
4905
|
+
: physicalOrder.get(a.edge.id)! - physicalOrder.get(b.edge.id)!;
|
|
4906
|
+
});
|
|
4907
|
+
for (const candidate of outgoing) {
|
|
4908
|
+
const segment = result.segments[grouped.segmentByPort.get(port)!]!;
|
|
4909
|
+
const reversed = orientation.reversedEdgeIds.has(candidate.edge.id);
|
|
4910
|
+
junctionGroup.push({
|
|
4911
|
+
edgeId: candidate.edge.id,
|
|
4912
|
+
reversed,
|
|
4913
|
+
segment,
|
|
4914
|
+
sourceValue: byPort.get(portByCandidate.get(candidate)!.source)!.position,
|
|
4915
|
+
targetValue: byPort.get(portByCandidate.get(candidate)!.target)!.position,
|
|
4916
|
+
partner:
|
|
4917
|
+
segment.partner === undefined ? undefined : result.segments[segment.partner],
|
|
4918
|
+
});
|
|
4028
4919
|
}
|
|
4029
4920
|
}
|
|
4030
|
-
for (const [source, target, weight] of reversed)
|
|
4031
|
-
addDependency(source, target, false, weight);
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
const incoming = candidates.map(() => 0);
|
|
4035
|
-
for (const targets of dependencies) {
|
|
4036
|
-
for (const target of targets) incoming[target] = (incoming[target] ?? 0) + 1;
|
|
4037
4921
|
}
|
|
4038
|
-
|
|
4039
|
-
let maximumSlot =
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
|
|
4044
|
-
const source = queue[queueIndex]!;
|
|
4045
|
-
const sourceSlot = candidates[source]?.slot ?? 0;
|
|
4046
|
-
for (const target of dependencies[source] ?? []) {
|
|
4047
|
-
const targetCandidate = candidates[target];
|
|
4048
|
-
if (!targetCandidate) continue;
|
|
4049
|
-
targetCandidate.slot = Math.max(targetCandidate.slot ?? 0, sourceSlot + 1);
|
|
4050
|
-
maximumSlot = Math.max(maximumSlot, targetCandidate.slot);
|
|
4051
|
-
incoming[target]!--;
|
|
4052
|
-
if (incoming[target] === 0) queue.push(target);
|
|
4053
|
-
}
|
|
4922
|
+
junctionGroups.push(junctionGroup);
|
|
4923
|
+
let maximumSlot = -1;
|
|
4924
|
+
for (const segment of result.segments) {
|
|
4925
|
+
if (Math.abs(segment.start - segment.end) >= 1e-3)
|
|
4926
|
+
maximumSlot = Math.max(maximumSlot, segment.slot);
|
|
4054
4927
|
}
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
candidate.slot = maximumSlot;
|
|
4928
|
+
for (const candidate of candidates) {
|
|
4929
|
+
const segment =
|
|
4930
|
+
result.segments[grouped.segmentByPort.get(portByCandidate.get(candidate)!.source)!]!;
|
|
4931
|
+
candidate.straight = Math.abs(segment.start - segment.end) < 1e-3;
|
|
4932
|
+
candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
|
|
4933
|
+
if (segment.partner !== undefined) {
|
|
4934
|
+
const partner = result.segments[segment.partner]!;
|
|
4935
|
+
candidate.secondSlot = increasing ? partner.slot : maximumSlot - segment.slot;
|
|
4936
|
+
if (!increasing) candidate.slot = maximumSlot - partner.slot;
|
|
4937
|
+
const sourcePort = portByCandidate.get(candidate)!.source;
|
|
4938
|
+
const offset =
|
|
4939
|
+
(increasing ? candidate.sourceCross : candidate.targetCross) -
|
|
4940
|
+
byPort.get(sourcePort)!.position +
|
|
4941
|
+
(runShift.get(sourcePort) ?? 0);
|
|
4942
|
+
candidate.crossover = segment.outgoing[0]! + offset;
|
|
4064
4943
|
}
|
|
4065
4944
|
}
|
|
4066
|
-
return
|
|
4945
|
+
return maximumSlot + 1;
|
|
4067
4946
|
});
|
|
4068
4947
|
|
|
4069
4948
|
const existingGapByLayer = flowLayers
|
|
@@ -4080,7 +4959,26 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4080
4959
|
input.nodeSettings?.(node)?.["nodePlacement.networkSimplex.nodeFlexibility"] !==
|
|
4081
4960
|
"NONE",
|
|
4082
4961
|
));
|
|
4083
|
-
|
|
4962
|
+
const leadingSlots = slotsByGap[0] ?? 0;
|
|
4963
|
+
const leadingWidth =
|
|
4964
|
+
leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
|
|
4965
|
+
let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
|
|
4966
|
+
// ELK omits ordinary node spacing beside an external-port-only layer.
|
|
4967
|
+
const externalFlowLayers = new Set(
|
|
4968
|
+
flowLayers.flatMap((_, layer) => {
|
|
4969
|
+
const ids = [...flowLayerByNodeId].filter(([, index]) => index === layer);
|
|
4970
|
+
return ids.length &&
|
|
4971
|
+
ids.every(([id]) => {
|
|
4972
|
+
const node = nodeById.get(id);
|
|
4973
|
+
const side = node && externalPortDummyOf(node)?.side;
|
|
4974
|
+
return horizontal
|
|
4975
|
+
? side === "WEST" || side === "EAST"
|
|
4976
|
+
: side === "NORTH" || side === "SOUTH";
|
|
4977
|
+
})
|
|
4978
|
+
? [layer]
|
|
4979
|
+
: [];
|
|
4980
|
+
}),
|
|
4981
|
+
);
|
|
4084
4982
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
4085
4983
|
const shift = nextStart - bounds.start;
|
|
4086
4984
|
for (const [id, rect] of placement.rectByNodeId) {
|
|
@@ -4093,23 +4991,44 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4093
4991
|
const size = bounds.end - bounds.start;
|
|
4094
4992
|
bounds.start = nextStart;
|
|
4095
4993
|
bounds.end = nextStart + size;
|
|
4096
|
-
const slots = slotsByGap[layerNo] ?? 0;
|
|
4097
|
-
const routesNearTarget = candidatesByGap[layerNo]?.some(
|
|
4994
|
+
const slots = slotsByGap[layerNo + 1] ?? 0;
|
|
4995
|
+
const routesNearTarget = candidatesByGap[layerNo + 1]?.some(
|
|
4098
4996
|
(candidate) =>
|
|
4099
4997
|
hasZeroFixedSideTarget(candidate.edge) &&
|
|
4100
|
-
candidatesByGap[layerNo]!.filter(
|
|
4998
|
+
candidatesByGap[layerNo + 1]!.filter(
|
|
4101
4999
|
({ edge }) => edge.targetId === candidate.edge.targetId,
|
|
4102
5000
|
).length >
|
|
4103
|
-
candidatesByGap[layerNo]!.filter(
|
|
5001
|
+
candidatesByGap[layerNo + 1]!.filter(
|
|
4104
5002
|
({ edge }) => edge.sourceId === candidate.edge.sourceId,
|
|
4105
5003
|
).length,
|
|
4106
5004
|
);
|
|
4107
5005
|
const preservedGap =
|
|
4108
5006
|
(existingGapByLayer[layerNo] ?? input.spacing.layer) -
|
|
4109
5007
|
(routesNearTarget ? edgeEdgeSpacing : 0);
|
|
5008
|
+
const labelAdjacent = labelFlowLayers.has(layerNo) || labelFlowLayers.has(layerNo + 1);
|
|
5009
|
+
const portMargin =
|
|
5010
|
+
flowLayers[layerNo]!.end -
|
|
5011
|
+
flowLayers[layerNo]!.start -
|
|
5012
|
+
(bodyFlowLayers[layerNo]!.end - bodyFlowLayers[layerNo]!.start) +
|
|
5013
|
+
(flowLayers[layerNo + 1]
|
|
5014
|
+
? flowLayers[layerNo + 1]!.end -
|
|
5015
|
+
flowLayers[layerNo + 1]!.start -
|
|
5016
|
+
(bodyFlowLayers[layerNo + 1]!.end - bodyFlowLayers[layerNo + 1]!.start)
|
|
5017
|
+
: 0);
|
|
5018
|
+
const externalBoundary =
|
|
5019
|
+
externalFlowLayers.has(layerNo) || externalFlowLayers.has(layerNo + 1);
|
|
5020
|
+
const minimumGap = externalBoundary
|
|
5021
|
+
? 0
|
|
5022
|
+
: labelAdjacent
|
|
5023
|
+
? Math.max(edgeNodeSpacing, input.spacing.layer - portMargin)
|
|
5024
|
+
: input.spacing.layer;
|
|
4110
5025
|
const gapSpacing =
|
|
4111
5026
|
slots === 0
|
|
4112
|
-
?
|
|
5027
|
+
? externalBoundary
|
|
5028
|
+
? 0
|
|
5029
|
+
: labelAdjacent
|
|
5030
|
+
? minimumGap
|
|
5031
|
+
: (existingGapByLayer[layerNo] ?? input.spacing.layer)
|
|
4113
5032
|
: Math.max(
|
|
4114
5033
|
preservesNodeFlexibilityGap ||
|
|
4115
5034
|
(labelExtraByGap[layerNo] ?? 0) > 0 ||
|
|
@@ -4123,14 +5042,22 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4123
5042
|
),
|
|
4124
5043
|
)
|
|
4125
5044
|
? preservedGap
|
|
4126
|
-
:
|
|
5045
|
+
: minimumGap,
|
|
5046
|
+
// LABEL nodes also need clearance beyond the final routing track.
|
|
4127
5047
|
2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing,
|
|
4128
5048
|
);
|
|
4129
5049
|
nextStart = bounds.end + gapSpacing;
|
|
4130
5050
|
}
|
|
4131
5051
|
implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
4132
5052
|
|
|
4133
|
-
for (const [
|
|
5053
|
+
for (const [boundary, candidates] of candidatesByGap.entries()) {
|
|
5054
|
+
const gap = boundary - 1;
|
|
5055
|
+
const firstBoundaryTrack =
|
|
5056
|
+
gap < 0
|
|
5057
|
+
? (flowLayers[0]?.start ?? 0) -
|
|
5058
|
+
edgeNodeSpacing -
|
|
5059
|
+
Math.max(0, leadingSlots - 1) * edgeEdgeSpacing
|
|
5060
|
+
: (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing;
|
|
4134
5061
|
for (const candidate of candidates) {
|
|
4135
5062
|
if (candidate.straight) continue;
|
|
4136
5063
|
const routeNearTarget =
|
|
@@ -4142,16 +5069,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4142
5069
|
? (flowLayers[gap + 1]?.start ?? 0) -
|
|
4143
5070
|
edgeNodeSpacing -
|
|
4144
5071
|
(candidate.slot ?? 0) * edgeEdgeSpacing
|
|
4145
|
-
: (
|
|
4146
|
-
edgeNodeSpacing +
|
|
4147
|
-
(candidate.slot ?? 0) * edgeEdgeSpacing;
|
|
5072
|
+
: firstBoundaryTrack + (candidate.slot ?? 0) * edgeEdgeSpacing;
|
|
4148
5073
|
if (candidate.secondSlot !== undefined && candidate.crossover !== undefined) {
|
|
4149
5074
|
orthogonalDetourByEdgeId.set(candidate.edge.id, {
|
|
4150
5075
|
firstTrack,
|
|
4151
|
-
secondTrack:
|
|
4152
|
-
(flowLayers[gap]?.end ?? 0) +
|
|
4153
|
-
edgeNodeSpacing +
|
|
4154
|
-
candidate.secondSlot * edgeEdgeSpacing,
|
|
5076
|
+
secondTrack: firstBoundaryTrack + candidate.secondSlot * edgeEdgeSpacing,
|
|
4155
5077
|
crossover: candidate.crossover,
|
|
4156
5078
|
});
|
|
4157
5079
|
} else {
|
|
@@ -4543,18 +5465,101 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4543
5465
|
const feedbackTargetPortSide = feedbackTargetPort
|
|
4544
5466
|
? input.portSettings?.(feedbackTargetPort, target)?.["port.side"]
|
|
4545
5467
|
: undefined;
|
|
5468
|
+
// A merged hyperedge dummy keeps an inverted dummy's in-layer edge,
|
|
5469
|
+
// whose dummy ports face backward (input) or forward (output).
|
|
5470
|
+
const mergedFace = (id: string, name: string | undefined) => {
|
|
5471
|
+
if (!isMergedHyperedgeDummy(input, id) || (name !== "input" && name !== "output"))
|
|
5472
|
+
return undefined;
|
|
5473
|
+
const forward = { right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" }[
|
|
5474
|
+
input.direction
|
|
5475
|
+
]!;
|
|
5476
|
+
const backward = { EAST: "WEST", WEST: "EAST", SOUTH: "NORTH", NORTH: "SOUTH" }[forward]!;
|
|
5477
|
+
return name === "output" ? forward : backward;
|
|
5478
|
+
};
|
|
5479
|
+
const inLayerSourceSide = feedbackSourcePortSide ?? mergedFace(source.id, edge.sourcePort);
|
|
5480
|
+
const inLayerTargetSide = feedbackTargetPortSide ?? mergedFace(target.id, edge.targetPort);
|
|
5481
|
+
// Inverted-port dummies keep the turn in the node's own layer. Both
|
|
5482
|
+
// terminals face the same exterior corridor; this is not an outer
|
|
5483
|
+
// feedback loop around the entire graph.
|
|
5484
|
+
if (
|
|
5485
|
+
style === "ORTHOGONAL" &&
|
|
5486
|
+
(source.id.startsWith("__layout_dummy:inverted:") ||
|
|
5487
|
+
target.id.startsWith("__layout_dummy:inverted:") ||
|
|
5488
|
+
isMergedHyperedgeDummy(input, source.id) ||
|
|
5489
|
+
isMergedHyperedgeDummy(input, target.id)) &&
|
|
5490
|
+
flowLayerByNodeId.get(source.id) === flowLayerByNodeId.get(target.id) &&
|
|
5491
|
+
inLayerSourceSide === inLayerTargetSide &&
|
|
5492
|
+
inLayerSourceSide !== undefined
|
|
5493
|
+
) {
|
|
5494
|
+
const sourceFallback = implicitEndpoints.get(edge.id)?.source ?? {
|
|
5495
|
+
x: sourceRect.x,
|
|
5496
|
+
y: sourceRect.y,
|
|
5497
|
+
};
|
|
5498
|
+
const targetFallback = implicitEndpoints.get(edge.id)?.target ?? {
|
|
5499
|
+
x: targetRect.x,
|
|
5500
|
+
y: targetRect.y,
|
|
5501
|
+
};
|
|
5502
|
+
const start = attached(
|
|
5503
|
+
edge,
|
|
5504
|
+
"source",
|
|
5505
|
+
getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input),
|
|
5506
|
+
);
|
|
5507
|
+
const end = attached(
|
|
5508
|
+
edge,
|
|
5509
|
+
"target",
|
|
5510
|
+
getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input),
|
|
5511
|
+
);
|
|
5512
|
+
if (reversedEdge) outsideFeedbackEdgeIds.add(edge.id);
|
|
5513
|
+
const side = inLayerSourceSide;
|
|
5514
|
+
const spacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
5515
|
+
// Clear the whole occupied layer, including ports and self-loop
|
|
5516
|
+
// reservations; endpoint anchors alone omit those margins.
|
|
5517
|
+
const occupied = flowLayers[flowLayerByNodeId.get(source.id) ?? -1];
|
|
5518
|
+
const track =
|
|
5519
|
+
orthogonalTrackByEdgeId.get(edge.id) ??
|
|
5520
|
+
(side === "WEST"
|
|
5521
|
+
? (horizontal
|
|
5522
|
+
? (occupied?.start ?? Math.min(start.x, end.x))
|
|
5523
|
+
: Math.min(start.x, end.x)) - spacing
|
|
5524
|
+
: side === "EAST"
|
|
5525
|
+
? (horizontal
|
|
5526
|
+
? (occupied?.end ?? Math.max(start.x, end.x))
|
|
5527
|
+
: Math.max(start.x, end.x)) + spacing
|
|
5528
|
+
: side === "NORTH"
|
|
5529
|
+
? (!horizontal
|
|
5530
|
+
? (occupied?.start ?? Math.min(start.y, end.y))
|
|
5531
|
+
: Math.min(start.y, end.y)) - spacing
|
|
5532
|
+
: (!horizontal
|
|
5533
|
+
? (occupied?.end ?? Math.max(start.y, end.y))
|
|
5534
|
+
: Math.max(start.y, end.y)) + spacing);
|
|
5535
|
+
pointsByEdgeId.set(
|
|
5536
|
+
edge.id,
|
|
5537
|
+
simplifyRoute(
|
|
5538
|
+
side === "WEST" || side === "EAST"
|
|
5539
|
+
? [start, { x: track, y: start.y }, { x: track, y: end.y }, end]
|
|
5540
|
+
: [start, { x: start.x, y: track }, { x: end.x, y: track }, end],
|
|
5541
|
+
),
|
|
5542
|
+
);
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
const sourceFlow = horizontal ? sourceRect.x : sourceRect.y;
|
|
5546
|
+
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
5547
|
+
const sourceBeforeTarget =
|
|
5548
|
+
sourceFlow < targetFlow ||
|
|
5549
|
+
(sourceFlow === targetFlow &&
|
|
5550
|
+
(flowLayerByNodeId.get(source.id) ?? 0) < (flowLayerByNodeId.get(target.id) ?? 0));
|
|
4546
5551
|
const sourceAwaySide = horizontal
|
|
4547
|
-
?
|
|
5552
|
+
? sourceBeforeTarget
|
|
4548
5553
|
? "WEST"
|
|
4549
5554
|
: "EAST"
|
|
4550
|
-
:
|
|
5555
|
+
: sourceBeforeTarget
|
|
4551
5556
|
? "NORTH"
|
|
4552
5557
|
: "SOUTH";
|
|
4553
5558
|
const targetAwaySide = horizontal
|
|
4554
|
-
?
|
|
5559
|
+
? sourceBeforeTarget
|
|
4555
5560
|
? "EAST"
|
|
4556
5561
|
: "WEST"
|
|
4557
|
-
:
|
|
5562
|
+
: sourceBeforeTarget
|
|
4558
5563
|
? "SOUTH"
|
|
4559
5564
|
: "NORTH";
|
|
4560
5565
|
const sameSideSelfLoop =
|
|
@@ -4565,6 +5570,63 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4565
5570
|
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4566
5571
|
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4567
5572
|
};
|
|
5573
|
+
if (
|
|
5574
|
+
style === "ORTHOGONAL" &&
|
|
5575
|
+
source.id === target.id &&
|
|
5576
|
+
hasFixedPortSide(source) &&
|
|
5577
|
+
(feedbackSourcePortSide !== undefined || feedbackTargetPortSide !== undefined) &&
|
|
5578
|
+
(edge.sourcePort === undefined ||
|
|
5579
|
+
edge.targetPort === undefined ||
|
|
5580
|
+
!sameSideSelfLoop ||
|
|
5581
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true)
|
|
5582
|
+
) {
|
|
5583
|
+
const endpoints = implicitEndpoints.get(edge.id)!;
|
|
5584
|
+
// Like other edges, a single-edge zero-size fixed-side port takes its
|
|
5585
|
+
// slot in the side's shared endpoint order, so it cannot coincide
|
|
5586
|
+
// with a sibling port that already uses that order.
|
|
5587
|
+
const terminal = (endpoint: "source" | "target"): Point => {
|
|
5588
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
5589
|
+
const fallback = endpoints[endpoint];
|
|
5590
|
+
const port = source.ports?.find((candidate) => candidate.name === portName);
|
|
5591
|
+
const side = port ? input.portSettings?.(port, source)?.["port.side"] : undefined;
|
|
5592
|
+
const onSide =
|
|
5593
|
+
(side === "EAST" && Math.abs(fallback.x - sourceRect.x - sourceRect.width) < 1e-9) ||
|
|
5594
|
+
(side === "WEST" && Math.abs(fallback.x - sourceRect.x) < 1e-9) ||
|
|
5595
|
+
(side === "SOUTH" && Math.abs(fallback.y - sourceRect.y - sourceRect.height) < 1e-9) ||
|
|
5596
|
+
(side === "NORTH" && Math.abs(fallback.y - sourceRect.y) < 1e-9);
|
|
5597
|
+
return onSide && usesSharedSideSlot(input, source, portName)
|
|
5598
|
+
? fallback
|
|
5599
|
+
: getPortPoint(source, portName, sourceRect, fallback, input.direction, input);
|
|
5600
|
+
};
|
|
5601
|
+
const start = terminal("source");
|
|
5602
|
+
const end = terminal("target");
|
|
5603
|
+
const outputSide = ({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[
|
|
5604
|
+
input.direction
|
|
5605
|
+
];
|
|
5606
|
+
const inputSide = ({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[
|
|
5607
|
+
input.direction
|
|
5608
|
+
];
|
|
5609
|
+
const loops = selfLoopsByNodeId.get(source.id) ?? [edge];
|
|
5610
|
+
const distance =
|
|
5611
|
+
Number(input.settings["spacing.nodeSelfLoop"] ?? 10) *
|
|
5612
|
+
(selfLoopTracks(loops).get(edge.id)! + 1);
|
|
5613
|
+
pointsByEdgeId.set(
|
|
5614
|
+
edge.id,
|
|
5615
|
+
(sameSideSelfLoop ? (points: Point[]) => points : simplifyRoute)(
|
|
5616
|
+
routeFixedSelfLoop(
|
|
5617
|
+
sourceRect,
|
|
5618
|
+
start,
|
|
5619
|
+
end,
|
|
5620
|
+
fixedSelfLoopSide(feedbackSourcePortSide) ?? outputSide,
|
|
5621
|
+
fixedSelfLoopSide(feedbackTargetPortSide) ?? inputSide,
|
|
5622
|
+
distance,
|
|
5623
|
+
input.direction,
|
|
5624
|
+
),
|
|
5625
|
+
),
|
|
5626
|
+
);
|
|
5627
|
+
outsideFeedbackEdgeIds.add(edge.id);
|
|
5628
|
+
continue;
|
|
5629
|
+
}
|
|
4568
5630
|
const fixedSideFeedback =
|
|
4569
5631
|
style === "ORTHOGONAL" &&
|
|
4570
5632
|
!sameSideSelfLoop &&
|
|
@@ -4850,6 +5912,31 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4850
5912
|
outsideFeedbackEdgeIds.add(edge.id);
|
|
4851
5913
|
continue;
|
|
4852
5914
|
}
|
|
5915
|
+
const movable =
|
|
5916
|
+
hasMovableLoopPorts(input, edge) &&
|
|
5917
|
+
hasPlacementLoopEnvelope(placement, source.id) &&
|
|
5918
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true;
|
|
5919
|
+
// Route unconstrained loops in the canonical rightward system, then
|
|
5920
|
+
// apply the same direction transform as the layer pipeline.
|
|
5921
|
+
const loopRect = movable
|
|
5922
|
+
? {
|
|
5923
|
+
x: 0,
|
|
5924
|
+
y: 0,
|
|
5925
|
+
width: horizontal ? sourceRect.width : sourceRect.height,
|
|
5926
|
+
height: horizontal ? sourceRect.height : sourceRect.width,
|
|
5927
|
+
}
|
|
5928
|
+
: sourceRect;
|
|
5929
|
+
const publishLoop = (points: readonly Point[]) => {
|
|
5930
|
+
const physical = !movable
|
|
5931
|
+
? points
|
|
5932
|
+
: points.map((point) => {
|
|
5933
|
+
const flow = reverse ? loopRect.width - point.x : point.x;
|
|
5934
|
+
return horizontal
|
|
5935
|
+
? { x: sourceRect.x + flow, y: sourceRect.y + point.y }
|
|
5936
|
+
: { x: sourceRect.x + point.y, y: sourceRect.y + flow };
|
|
5937
|
+
});
|
|
5938
|
+
pointsByEdgeId.set(edge.id, physical);
|
|
5939
|
+
};
|
|
4853
5940
|
const routeHorizontalSide = (
|
|
4854
5941
|
side: "NORTH" | "SOUTH",
|
|
4855
5942
|
indexOnSide: number,
|
|
@@ -4880,17 +5967,15 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4880
5967
|
const sideLoops = loops.filter(
|
|
4881
5968
|
(candidate, i) => loopSide(distribution, i, candidate) === side,
|
|
4882
5969
|
);
|
|
4883
|
-
const distance = loopDistance(sideLoops, edge, side, ordering);
|
|
5970
|
+
const distance = loopDistance(sideLoops, edge, side, ordering, movable);
|
|
4884
5971
|
const y =
|
|
4885
|
-
side === "NORTH"
|
|
4886
|
-
? sourceRect.y - distance
|
|
4887
|
-
: sourceRect.y + sourceRect.height + distance;
|
|
5972
|
+
side === "NORTH" ? loopRect.y - distance : loopRect.y + loopRect.height + distance;
|
|
4888
5973
|
const start = {
|
|
4889
|
-
x:
|
|
4890
|
-
y: side === "NORTH" ?
|
|
5974
|
+
x: loopRect.x + loopRect.width * startRatio,
|
|
5975
|
+
y: side === "NORTH" ? loopRect.y : loopRect.y + loopRect.height,
|
|
4891
5976
|
};
|
|
4892
5977
|
const end = {
|
|
4893
|
-
x:
|
|
5978
|
+
x: loopRect.x + loopRect.width * endRatio,
|
|
4894
5979
|
y: start.y,
|
|
4895
5980
|
};
|
|
4896
5981
|
return [start, { x: start.x, y }, { x: end.x, y }, end];
|
|
@@ -4907,43 +5992,41 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4907
5992
|
const startRatio = side === "EAST" ? firstRatio : secondRatio;
|
|
4908
5993
|
const endRatio = side === "EAST" ? secondRatio : firstRatio;
|
|
4909
5994
|
const distance =
|
|
4910
|
-
input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
5995
|
+
movable || input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
4911
5996
|
? loopDistance(
|
|
4912
5997
|
loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side),
|
|
4913
5998
|
edge,
|
|
4914
5999
|
side,
|
|
4915
6000
|
ordering,
|
|
6001
|
+
movable,
|
|
4916
6002
|
)
|
|
4917
6003
|
: spacing * (indexOnSide + 1);
|
|
4918
6004
|
const x =
|
|
4919
|
-
side === "EAST"
|
|
4920
|
-
? sourceRect.x + sourceRect.width + distance
|
|
4921
|
-
: sourceRect.x - distance;
|
|
6005
|
+
side === "EAST" ? loopRect.x + loopRect.width + distance : loopRect.x - distance;
|
|
4922
6006
|
const start = {
|
|
4923
|
-
x: side === "EAST" ?
|
|
4924
|
-
y:
|
|
6007
|
+
x: side === "EAST" ? loopRect.x + loopRect.width : loopRect.x,
|
|
6008
|
+
y: loopRect.y + loopRect.height * startRatio,
|
|
4925
6009
|
};
|
|
4926
|
-
const end = { x: start.x, y:
|
|
6010
|
+
const end = { x: start.x, y: loopRect.y + loopRect.height * endRatio };
|
|
4927
6011
|
return [start, { x, y: start.y }, { x, y: end.y }, end];
|
|
4928
6012
|
};
|
|
4929
6013
|
if (distribution === "NORTH_SOUTH") {
|
|
4930
6014
|
const side = loopIndex % 2 === 0 ? "NORTH" : "SOUTH";
|
|
4931
6015
|
const indexOnSide = Math.floor(loopIndex / 2);
|
|
4932
6016
|
const countOnSide = Math.ceil((loops.length - (side === "SOUTH" ? 1 : 0)) / 2);
|
|
4933
|
-
|
|
6017
|
+
publishLoop(routeHorizontalSide(side, indexOnSide, countOnSide));
|
|
4934
6018
|
} else if (distribution === "EQUALLY") {
|
|
4935
6019
|
const sides = ["NORTH", "SOUTH", "EAST", "WEST"] as const;
|
|
4936
6020
|
const side = sides[loopIndex % sides.length]!;
|
|
4937
6021
|
const indexOnSide = Math.floor(loopIndex / sides.length);
|
|
4938
6022
|
const countOnSide = Math.ceil((loops.length - sides.indexOf(side)) / sides.length);
|
|
4939
|
-
|
|
4940
|
-
edge.id,
|
|
6023
|
+
publishLoop(
|
|
4941
6024
|
side === "NORTH" || side === "SOUTH"
|
|
4942
6025
|
? routeHorizontalSide(side, indexOnSide, countOnSide)
|
|
4943
6026
|
: routeVerticalSide(side, indexOnSide, countOnSide),
|
|
4944
6027
|
);
|
|
4945
6028
|
} else {
|
|
4946
|
-
|
|
6029
|
+
publishLoop(routeHorizontalSide("NORTH", loopIndex, loops.length));
|
|
4947
6030
|
}
|
|
4948
6031
|
continue;
|
|
4949
6032
|
}
|
|
@@ -5047,7 +6130,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5047
6130
|
).length === 1;
|
|
5048
6131
|
const flexibleFeedback =
|
|
5049
6132
|
reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
|
|
5050
|
-
const
|
|
6133
|
+
const unshiftedStart = flexibleFeedback
|
|
5051
6134
|
? sourceFallback
|
|
5052
6135
|
: sourceFixedSide
|
|
5053
6136
|
? sourceFallback
|
|
@@ -5059,7 +6142,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5059
6142
|
input.direction,
|
|
5060
6143
|
input,
|
|
5061
6144
|
);
|
|
5062
|
-
const
|
|
6145
|
+
const unshiftedEnd = flexibleFeedback
|
|
5063
6146
|
? targetFallback
|
|
5064
6147
|
: targetFixedSide
|
|
5065
6148
|
? targetFallback
|
|
@@ -5071,19 +6154,23 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5071
6154
|
input.direction,
|
|
5072
6155
|
input,
|
|
5073
6156
|
);
|
|
6157
|
+
const start = attached(edge, "source", unshiftedStart);
|
|
6158
|
+
const end = attached(edge, "target", unshiftedEnd);
|
|
5074
6159
|
const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
|
|
5075
6160
|
const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
|
|
5076
6161
|
const earlierLayer = Math.min(sourceLayer, targetLayer);
|
|
5077
6162
|
const laterLayer = Math.max(sourceLayer, targetLayer);
|
|
5078
6163
|
const earlier = flowLayers[earlierLayer];
|
|
5079
6164
|
const later = flowLayers[laterLayer];
|
|
6165
|
+
// Inverted-port links clear the occupied layer, including protruding
|
|
6166
|
+
// ports and routing reservations, rather than only the two node bodies.
|
|
5080
6167
|
const sameLayerTrack =
|
|
5081
6168
|
sourceLayer === targetLayer
|
|
5082
|
-
?
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
6169
|
+
? (flowLayers[sourceLayer]?.end ??
|
|
6170
|
+
(horizontal
|
|
6171
|
+
? Math.max(sourceRect.x + sourceRect.width, targetRect.x + targetRect.width)
|
|
6172
|
+
: Math.max(sourceRect.y + sourceRect.height, targetRect.y + targetRect.height))) +
|
|
6173
|
+
Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10)
|
|
5087
6174
|
: undefined;
|
|
5088
6175
|
const track =
|
|
5089
6176
|
orthogonalTrackByEdgeId.get(edge.id) ??
|
|
@@ -5091,7 +6178,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5091
6178
|
splineTrackByEdgeId.get(edge.id) ??
|
|
5092
6179
|
sameLayerTrack ??
|
|
5093
6180
|
(earlier && later && earlierLayer !== laterLayer
|
|
5094
|
-
? (earlier.end
|
|
6181
|
+
? freeGapTrack(edge.id, earlier.end, later.start, start, end)
|
|
5095
6182
|
: horizontal
|
|
5096
6183
|
? (start.x + end.x) / 2
|
|
5097
6184
|
: (start.y + end.y) / 2);
|
|
@@ -5237,7 +6324,26 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5237
6324
|
);
|
|
5238
6325
|
}
|
|
5239
6326
|
|
|
5240
|
-
|
|
6327
|
+
const junctionPointsByEdgeId =
|
|
6328
|
+
style === "ORTHOGONAL"
|
|
6329
|
+
? orthogonalJunctionPoints(
|
|
6330
|
+
input,
|
|
6331
|
+
orientation,
|
|
6332
|
+
implicitEdgeEndpoints(input, routingCoordinates(placement), orientation),
|
|
6333
|
+
flowLayerByNodeId,
|
|
6334
|
+
junctionGroups,
|
|
6335
|
+
pointsByEdgeId,
|
|
6336
|
+
orthogonalTrackByEdgeId,
|
|
6337
|
+
orthogonalDetourByEdgeId,
|
|
6338
|
+
(node, port) => getOrientedPortDirection(input, orientation, node, port),
|
|
6339
|
+
)
|
|
6340
|
+
: undefined;
|
|
6341
|
+
return {
|
|
6342
|
+
pointsByEdgeId,
|
|
6343
|
+
splineNubControlsByEdgeId,
|
|
6344
|
+
outsideFeedbackEdgeIds,
|
|
6345
|
+
...(style === "ORTHOGONAL" ? { junctionPointsByEdgeId } : {}),
|
|
6346
|
+
};
|
|
5241
6347
|
};
|
|
5242
6348
|
}
|
|
5243
6349
|
|
|
@@ -5329,11 +6435,26 @@ export function placePorts<P>(
|
|
|
5329
6435
|
return { ...port, ...size, x: port.x, y: port.y };
|
|
5330
6436
|
}
|
|
5331
6437
|
const side = sideByPort.get(port)!;
|
|
6438
|
+
const borderOffset = Number(portSettings?.(port)?.["port.borderOffset"] ?? 0);
|
|
5332
6439
|
return {
|
|
5333
6440
|
...port,
|
|
5334
6441
|
...size,
|
|
5335
|
-
x:
|
|
5336
|
-
|
|
6442
|
+
x:
|
|
6443
|
+
side === "EAST"
|
|
6444
|
+
? rect.width + borderOffset
|
|
6445
|
+
: side === "WEST"
|
|
6446
|
+
? constraints === "FIXED_POS" && direction === "left" && size.width === 0
|
|
6447
|
+
? 0 - borderOffset
|
|
6448
|
+
: -size.width - borderOffset
|
|
6449
|
+
: port.x,
|
|
6450
|
+
y:
|
|
6451
|
+
side === "SOUTH"
|
|
6452
|
+
? rect.height + borderOffset
|
|
6453
|
+
: side === "NORTH"
|
|
6454
|
+
? constraints === "FIXED_POS" && direction === "up" && size.height === 0
|
|
6455
|
+
? 0 - borderOffset
|
|
6456
|
+
: -size.height - borderOffset
|
|
6457
|
+
: port.y,
|
|
5337
6458
|
};
|
|
5338
6459
|
}
|
|
5339
6460
|
const side = sideByPort.get(port)!;
|
|
@@ -5549,8 +6670,8 @@ export function normalizePlacementForPortExtents(
|
|
|
5549
6670
|
}
|
|
5550
6671
|
const nextLayer = physicalLayers[layerIndex + 1];
|
|
5551
6672
|
if (!nextLayer) continue;
|
|
5552
|
-
let
|
|
5553
|
-
let
|
|
6673
|
+
let trailingBoundary = Number.NEGATIVE_INFINITY;
|
|
6674
|
+
let leadingBoundary = Number.POSITIVE_INFINITY;
|
|
5554
6675
|
for (const [ids, trailing] of [
|
|
5555
6676
|
[layer, true],
|
|
5556
6677
|
[nextLayer, false],
|
|
@@ -5559,6 +6680,10 @@ export function normalizePlacementForPortExtents(
|
|
|
5559
6680
|
const node = input.graph.nodes.find((candidate) => candidate.id === id);
|
|
5560
6681
|
const rect = placement.rectByNodeId.get(id);
|
|
5561
6682
|
if (!node || !rect) continue;
|
|
6683
|
+
const bodyStart = horizontal ? rect.x : rect.y;
|
|
6684
|
+
const bodySize = horizontal ? rect.width : rect.height;
|
|
6685
|
+
if (trailing) trailingBoundary = Math.max(trailingBoundary, bodyStart + bodySize);
|
|
6686
|
+
else leadingBoundary = Math.min(leadingBoundary, bodyStart);
|
|
5562
6687
|
const ports = placePorts(
|
|
5563
6688
|
node.ports,
|
|
5564
6689
|
rect,
|
|
@@ -5575,21 +6700,37 @@ export function normalizePlacementForPortExtents(
|
|
|
5575
6700
|
: trailing
|
|
5576
6701
|
? Math.max(0, (port.y ?? 0) + (port.height ?? 0) - rect.height)
|
|
5577
6702
|
: Math.max(0, -(port.y ?? 0));
|
|
5578
|
-
if (trailing)
|
|
5579
|
-
|
|
6703
|
+
if (trailing)
|
|
6704
|
+
trailingBoundary = Math.max(trailingBoundary, bodyStart + bodySize + extent);
|
|
6705
|
+
else leadingBoundary = Math.min(leadingBoundary, bodyStart - extent);
|
|
5580
6706
|
}
|
|
5581
6707
|
}
|
|
5582
6708
|
}
|
|
5583
|
-
accumulatedShift +=
|
|
6709
|
+
accumulatedShift += Math.max(
|
|
6710
|
+
0,
|
|
6711
|
+
input.spacing.layer - (leadingBoundary + accumulatedShift - trailingBoundary),
|
|
6712
|
+
);
|
|
5584
6713
|
}
|
|
5585
6714
|
|
|
5586
6715
|
let minimumX = Number.POSITIVE_INFINITY;
|
|
5587
6716
|
let minimumY = Number.POSITIVE_INFINITY;
|
|
6717
|
+
const envelopes = loopEnvelopes(input);
|
|
6718
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
5588
6719
|
for (const node of input.graph.nodes) {
|
|
5589
6720
|
const rect = placement.rectByNodeId.get(node.id);
|
|
5590
6721
|
if (!rect) continue;
|
|
5591
|
-
|
|
5592
|
-
|
|
6722
|
+
const envelope = hasPlacementLoopEnvelope(placement, node.id)
|
|
6723
|
+
? envelopes.get(node.id)
|
|
6724
|
+
: undefined;
|
|
6725
|
+
const flowBefore = reverse ? envelope?.flowAfter : envelope?.flowBefore;
|
|
6726
|
+
minimumX = Math.min(
|
|
6727
|
+
minimumX,
|
|
6728
|
+
rect.x - (horizontal ? (flowBefore ?? 0) : (envelope?.before ?? 0)),
|
|
6729
|
+
);
|
|
6730
|
+
minimumY = Math.min(
|
|
6731
|
+
minimumY,
|
|
6732
|
+
rect.y - (horizontal ? (envelope?.before ?? 0) : (flowBefore ?? 0)),
|
|
6733
|
+
);
|
|
5593
6734
|
const ports = placePorts(
|
|
5594
6735
|
node.ports,
|
|
5595
6736
|
rect,
|