@statelyai/layout 0.3.0 → 0.4.1
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-BBfKN4Dw.mjs +3075 -0
- package/dist/elkjs-DNfXDPup.cjs +3104 -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-B2buJqMf.cjs → layered-5RxQhryb.cjs} +11184 -2932
- package/dist/{layered-DGFTeQSl.mjs → layered-DSesbctm.mjs} +12059 -3903
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-CLFySz3R.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-GpBIX6ng.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 +1476 -449
- package/src/elkjs/layout-quality.ts +267 -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 +233 -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 +79 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +875 -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 +1044 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +154 -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 +2124 -886
- 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, type CrossingGraph } 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,
|
|
@@ -79,6 +124,24 @@ function cycleModelOrder(input: LayeredPhaseInput): Map<string, number> {
|
|
|
79
124
|
);
|
|
80
125
|
}
|
|
81
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Internal setting: how crossing minimization models ports and tracks.
|
|
129
|
+
* `mixedPorts` (default true) counts a port carrying both directions as two
|
|
130
|
+
* ports and groups its neighbours by direction. `separateSharedTracks`
|
|
131
|
+
* (default false) counts a hyperedge joining two or more ports on each side
|
|
132
|
+
* of its gap edge by edge, since routing never lets edges run opposite ways
|
|
133
|
+
* along one track. A `probe` records whether either choice can change the
|
|
134
|
+
* order. Each helps on average but steers the sweeps into worse orders on
|
|
135
|
+
* some graphs, so the elkjs facade also lays such graphs out with the
|
|
136
|
+
* alternatives and keeps the best result.
|
|
137
|
+
*/
|
|
138
|
+
export const CROSSING_MODEL = "internal.crossingModel";
|
|
139
|
+
export interface CrossingModel {
|
|
140
|
+
mixedPorts?: boolean;
|
|
141
|
+
separateSharedTracks?: boolean;
|
|
142
|
+
probe?: { mixedPorts: boolean; sharedTracks: boolean };
|
|
143
|
+
}
|
|
144
|
+
|
|
82
145
|
export const breakCyclesWithDepthFirstSearch: CycleBreaker = (input) => {
|
|
83
146
|
const outgoing = new Map<string, GraphEdge[]>();
|
|
84
147
|
for (const node of input.graph.nodes) outgoing.set(node.id, []);
|
|
@@ -243,6 +306,7 @@ function breakCyclesWithGreedyHeuristic(
|
|
|
243
306
|
if (source === undefined || target === undefined) continue;
|
|
244
307
|
if ((marks[source] ?? 0) > (marks[target] ?? 0)) reversedEdgeIds.add(edge.id);
|
|
245
308
|
}
|
|
309
|
+
recordCycleRandom(input, random);
|
|
246
310
|
return { reversedEdgeIds };
|
|
247
311
|
}
|
|
248
312
|
|
|
@@ -252,9 +316,39 @@ export const breakCyclesGreedily: CycleBreaker = (input) =>
|
|
|
252
316
|
export const breakCyclesGreedilyByModelOrder: CycleBreaker = (input) =>
|
|
253
317
|
breakCyclesWithGreedyHeuristic(input, "model-order");
|
|
254
318
|
|
|
255
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* A zero-size port on a FIXED_SIDE node used by exactly one edge has no
|
|
321
|
+
* physical extent, so its terminal takes a slot in its side's shared endpoint order.
|
|
322
|
+
*/
|
|
323
|
+
export function usesSharedSideSlot(
|
|
324
|
+
input: LayeredPhaseInput,
|
|
325
|
+
node: GraphNode,
|
|
326
|
+
portName: string | undefined,
|
|
327
|
+
): boolean {
|
|
328
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
329
|
+
return (
|
|
330
|
+
port !== undefined &&
|
|
331
|
+
(port.width ?? 8) === 0 &&
|
|
332
|
+
(port.height ?? 8) === 0 &&
|
|
333
|
+
input.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE" &&
|
|
334
|
+
input.graph.edges.filter(
|
|
335
|
+
(edge) =>
|
|
336
|
+
(edge.sourceId === node.id && edge.sourcePort === portName) ||
|
|
337
|
+
(edge.targetId === node.id && edge.targetPort === portName),
|
|
338
|
+
).length === 1
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** ELK MODEL_ORDER: layer-constrained nodes (e.g. external port dummies) order before/after the rest. */
|
|
256
343
|
export const breakCyclesByModelOrder: CycleBreaker = (input) => {
|
|
257
344
|
const order = cycleModelOrder(input);
|
|
345
|
+
const offset = Math.max(1, input.graph.nodes.length);
|
|
346
|
+
const shift = { FIRST_SEPARATE: -2, FIRST: -1, LAST: 1, LAST_SEPARATE: 2 } as const;
|
|
347
|
+
for (const node of input.graph.nodes) {
|
|
348
|
+
const constraint = input.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
349
|
+
const factor = shift[constraint as keyof typeof shift];
|
|
350
|
+
if (factor) order.set(node.id, factor * offset + order.get(node.id)!);
|
|
351
|
+
}
|
|
258
352
|
return {
|
|
259
353
|
reversedEdgeIds: new Set(
|
|
260
354
|
input.graph.edges
|
|
@@ -668,83 +762,6 @@ export function applyLayerConstraints(
|
|
|
668
762
|
};
|
|
669
763
|
}
|
|
670
764
|
|
|
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
765
|
/** Keep activated ELK partitions in ascending, contiguous layer blocks. */
|
|
749
766
|
export function applyPartitions(
|
|
750
767
|
input: LayeredPhaseInput,
|
|
@@ -819,14 +836,7 @@ export function applyLayerConstraintOrder(input: LayeredPhaseInput, order: Layer
|
|
|
819
836
|
...order,
|
|
820
837
|
layers: order.layers.map((layer) => {
|
|
821
838
|
const unconstrained = layer.filter((id) => !constrained.has(id));
|
|
822
|
-
|
|
823
|
-
input.settings.mergeEdges === true
|
|
824
|
-
? [
|
|
825
|
-
...unconstrained.filter((id) => id.startsWith("__layout_dummy:")),
|
|
826
|
-
...unconstrained.filter((id) => !id.startsWith("__layout_dummy:")),
|
|
827
|
-
]
|
|
828
|
-
: unconstrained;
|
|
829
|
-
return [...mergedOrder, ...layer.filter((id) => constrained.has(id))];
|
|
839
|
+
return [...unconstrained, ...layer.filter((id) => constrained.has(id))];
|
|
830
840
|
}),
|
|
831
841
|
};
|
|
832
842
|
}
|
|
@@ -911,56 +921,112 @@ export function applyGreedySwitch(
|
|
|
911
921
|
): LayerOrder {
|
|
912
922
|
if (input.settings["crossingMinimization.strategy"] === "INTERACTIVE") return order;
|
|
913
923
|
if (input.settings["crossingMinimization.semiInteractive"] === true) return order;
|
|
924
|
+
const type = input.settings["crossingMinimization.greedySwitch.type"] ?? "TWO_SIDED";
|
|
925
|
+
const threshold = input.settings["crossingMinimization.greedySwitch.activationThreshold"] ?? 40;
|
|
926
|
+
const originalNodeCount = input.graph.nodes.filter(
|
|
927
|
+
(node) => !node.id.startsWith("__layout_dummy:"),
|
|
928
|
+
).length;
|
|
929
|
+
if (type === "OFF" || (threshold !== 0 && threshold <= originalNodeCount)) return order;
|
|
930
|
+
if (order.layers.length === 0 || (order.layers.length === 1 && order.layers[0]?.length === 1))
|
|
931
|
+
return order;
|
|
932
|
+
const random = phaseRandomByInput.get(input) ?? crossingRandom(input);
|
|
933
|
+
// ELK initializes another layer-sweep processor for greedy switching. It
|
|
934
|
+
// consumes a saved seed, chooses a distributor for ONE_SIDED, then draws
|
|
935
|
+
// the sweep direction. Routing continues with this same graph RNG state.
|
|
936
|
+
random.nextLong();
|
|
937
|
+
if (type === "ONE_SIDED") random.nextBoolean();
|
|
938
|
+
let forward = random.nextBoolean();
|
|
939
|
+
phaseRandomByInput.set(input, random);
|
|
914
940
|
const modelOrderStrategy = input.settings["considerModelOrder.strategy"] ?? "NONE";
|
|
915
941
|
if (
|
|
916
942
|
(modelOrderStrategy === "NODES_AND_EDGES" || modelOrderStrategy === "PREFER_NODES") &&
|
|
917
|
-
(input.settings["
|
|
918
|
-
Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0) >= 1)
|
|
943
|
+
Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0) >= 1
|
|
919
944
|
) {
|
|
920
945
|
return order;
|
|
921
946
|
}
|
|
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
947
|
const layers = order.layers.map((layer) => [...layer]);
|
|
928
948
|
const layerByNodeId = new Map<string, number>();
|
|
929
949
|
for (const [layerIndex, layer] of layers.entries()) {
|
|
930
950
|
for (const id of layer) layerByNodeId.set(id, layerIndex);
|
|
931
951
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
952
|
+
const greedyGraph =
|
|
953
|
+
order.inputPortOrderByNodeId && order.outputPortOrderByNodeId
|
|
954
|
+
? crossingGraph(
|
|
955
|
+
input,
|
|
956
|
+
orientation,
|
|
957
|
+
layers,
|
|
958
|
+
order.inputPortOrderByNodeId,
|
|
959
|
+
order.outputPortOrderByNodeId,
|
|
960
|
+
)
|
|
961
|
+
: undefined;
|
|
962
|
+
const greedyNodes = new Map(greedyGraph?.layers.flat().map((node) => [node.id, node]));
|
|
963
|
+
if (greedyGraph) assertNorthSouthPortSides(greedyGraph);
|
|
964
|
+
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
965
|
+
const portPositions = (layer: readonly string[], incoming: boolean) => {
|
|
966
|
+
const positions = new Map<string, number>();
|
|
967
|
+
let consumed = 0;
|
|
968
|
+
for (const nodeId of layer) {
|
|
969
|
+
const selected = (
|
|
970
|
+
incoming ? order.inputPortOrderByNodeId : order.outputPortOrderByNodeId
|
|
971
|
+
)?.get(nodeId);
|
|
972
|
+
const edgeIds =
|
|
973
|
+
selected ??
|
|
974
|
+
input.graph.edges
|
|
975
|
+
.filter((edge) => {
|
|
976
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
977
|
+
return source !== target && (incoming ? target : source) === nodeId;
|
|
978
|
+
})
|
|
979
|
+
.map((edge) => edge.id);
|
|
980
|
+
// WEST ports occur clockwise bottom-to-top; greedy crossing counters
|
|
981
|
+
// enumerate both neighboring faces in physical top-to-bottom order.
|
|
982
|
+
const physical = incoming ? [...edgeIds].reverse() : edgeIds;
|
|
983
|
+
const shared = new Map<string, number>();
|
|
984
|
+
for (const edgeId of physical) {
|
|
985
|
+
const edge = edgeById.get(edgeId)!;
|
|
986
|
+
const reversed = orientation.reversedEdgeIds.has(edgeId);
|
|
987
|
+
const port = incoming !== reversed ? edge.targetPort : edge.sourcePort;
|
|
988
|
+
if (port !== undefined && shared.has(port)) {
|
|
989
|
+
positions.set(edgeId, shared.get(port)!);
|
|
990
|
+
} else {
|
|
991
|
+
positions.set(edgeId, consumed);
|
|
992
|
+
if (port !== undefined) shared.set(port, consumed);
|
|
993
|
+
consumed++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Preserve the old node-level fallback for custom minimizers that do
|
|
997
|
+
// not provide selected port orders.
|
|
998
|
+
if (!selected) {
|
|
999
|
+
for (const edgeId of edgeIds) positions.set(edgeId, consumed);
|
|
1000
|
+
consumed++;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return positions;
|
|
1004
|
+
};
|
|
940
1005
|
const countBoundary = (boundary: number): number => {
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
);
|
|
1006
|
+
const sourcePositions = portPositions(layers[boundary] ?? [], false);
|
|
1007
|
+
const targetPositions = portPositions(layers[boundary + 1] ?? [], true);
|
|
1008
|
+
const edges = input.graph.edges.filter((edge) => {
|
|
1009
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
1010
|
+
return layerByNodeId.get(source) === boundary && layerByNodeId.get(target) === boundary + 1;
|
|
1011
|
+
});
|
|
948
1012
|
let crossings = 0;
|
|
949
1013
|
for (let left = 0; left < edges.length; left++) {
|
|
950
|
-
const leftEdge = edges[left]!;
|
|
951
1014
|
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
1015
|
const sourceDifference =
|
|
955
|
-
|
|
1016
|
+
sourcePositions.get(edges[left]!.id)! - sourcePositions.get(edges[right]!.id)!;
|
|
956
1017
|
const targetDifference =
|
|
957
|
-
|
|
1018
|
+
targetPositions.get(edges[left]!.id)! - targetPositions.get(edges[right]!.id)!;
|
|
958
1019
|
if (sourceDifference * targetDifference < 0) crossings++;
|
|
959
1020
|
}
|
|
960
1021
|
}
|
|
961
1022
|
return crossings;
|
|
962
1023
|
};
|
|
963
1024
|
const countForLayer = (layerIndex: number, oneSidedBoundary?: number) => {
|
|
1025
|
+
// Greedy swapping counts edge pairs, rather than the sweep's hyperedge estimate.
|
|
1026
|
+
if (oneSidedBoundary === undefined && greedyGraph) {
|
|
1027
|
+
greedyGraph.layers = layers.map((layer) => layer.map((id) => greedyNodes.get(id)!));
|
|
1028
|
+
return countAllCrossings(greedyGraph, "edges").total;
|
|
1029
|
+
}
|
|
964
1030
|
if (oneSidedBoundary !== undefined) return countBoundary(oneSidedBoundary);
|
|
965
1031
|
return (
|
|
966
1032
|
(layerIndex > 0 ? countBoundary(layerIndex - 1) : 0) +
|
|
@@ -975,6 +1041,31 @@ export function applyGreedySwitch(
|
|
|
975
1041
|
for (const layerIndex of indices) {
|
|
976
1042
|
const layer = layers[layerIndex]!;
|
|
977
1043
|
let improved: boolean;
|
|
1044
|
+
if (greedyGraph) {
|
|
1045
|
+
// ELK decides each switch from local two-node crossing counts.
|
|
1046
|
+
const units = getCrossingUnits(input);
|
|
1047
|
+
greedyGraph.layers = layers.map((ids) => ids.map((id) => greedyNodes.get(id)!));
|
|
1048
|
+
const decider = new GreedySwitchDecider(
|
|
1049
|
+
greedyGraph,
|
|
1050
|
+
layerIndex,
|
|
1051
|
+
type === "ONE_SIDED" ? (forward ? "WEST" : "EAST") : undefined,
|
|
1052
|
+
);
|
|
1053
|
+
do {
|
|
1054
|
+
improved = false;
|
|
1055
|
+
for (let index = 0; index + 1 < layer.length; index++) {
|
|
1056
|
+
const upper = greedyNodes.get(layer[index]!)!,
|
|
1057
|
+
lower = greedyNodes.get(layer[index + 1]!)!;
|
|
1058
|
+
if (decider.preventsSwitch(upper, lower, units?.successors ?? new Map())) continue;
|
|
1059
|
+
const [current, switched] = decider.crossings(upper, lower);
|
|
1060
|
+
if (current <= switched) continue;
|
|
1061
|
+
decider.notifySwitch(upper, lower);
|
|
1062
|
+
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
1063
|
+
improved = true;
|
|
1064
|
+
changed = true;
|
|
1065
|
+
}
|
|
1066
|
+
} while (improved && layerIndex !== indices[0]);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
978
1069
|
do {
|
|
979
1070
|
improved = false;
|
|
980
1071
|
for (let index = 0; index + 1 < layer.length; index++) {
|
|
@@ -986,6 +1077,8 @@ export function applyGreedySwitch(
|
|
|
986
1077
|
) {
|
|
987
1078
|
continue;
|
|
988
1079
|
}
|
|
1080
|
+
const units = getCrossingUnits(input);
|
|
1081
|
+
if (units && !canSwapCrossingUnits(layer[index]!, layer[index + 1]!, units)) continue;
|
|
989
1082
|
const before = countForLayer(layerIndex, oneSidedBoundary);
|
|
990
1083
|
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
991
1084
|
const after = countForLayer(layerIndex, oneSidedBoundary);
|
|
@@ -996,13 +1089,56 @@ export function applyGreedySwitch(
|
|
|
996
1089
|
[layer[index], layer[index + 1]] = [layer[index + 1]!, layer[index]!];
|
|
997
1090
|
}
|
|
998
1091
|
}
|
|
999
|
-
|
|
1092
|
+
// ELK scans the starting layer once, then converges each later layer.
|
|
1093
|
+
// Repeating the start immediately changes the following layers' local optimum.
|
|
1094
|
+
} while (improved && layerIndex !== indices[0]);
|
|
1000
1095
|
}
|
|
1001
1096
|
return changed;
|
|
1002
1097
|
};
|
|
1003
|
-
|
|
1004
|
-
|
|
1098
|
+
const trace = (crossings: number) =>
|
|
1099
|
+
traceLayeredPhase(() => ({
|
|
1100
|
+
kind: "sweep",
|
|
1101
|
+
scope: input.graph.id,
|
|
1102
|
+
attempt: -1,
|
|
1103
|
+
forward,
|
|
1104
|
+
firstSweep: false,
|
|
1105
|
+
crossings,
|
|
1106
|
+
layers,
|
|
1107
|
+
}));
|
|
1108
|
+
if (type === "ONE_SIDED") {
|
|
1109
|
+
// ONE_SIDED does not always improve (opposite sweeps can undo each other),
|
|
1110
|
+
// so ELK's minimizeCrossingsWithCounter alternates sweeps only while the
|
|
1111
|
+
// total crossing count strictly drops, keeping the order before the last sweep.
|
|
1112
|
+
const countAll = () => {
|
|
1113
|
+
if (greedyGraph) return countForLayer(0);
|
|
1114
|
+
let total = 0;
|
|
1115
|
+
for (let boundary = 0; boundary + 1 < layers.length; boundary++)
|
|
1116
|
+
total += countBoundary(boundary);
|
|
1117
|
+
return total;
|
|
1118
|
+
};
|
|
1119
|
+
sweep(forward);
|
|
1120
|
+
let crossings = countAll();
|
|
1121
|
+
trace(crossings);
|
|
1122
|
+
while (crossings > 0) {
|
|
1123
|
+
const previous = layers.map((layer) => [...layer]);
|
|
1124
|
+
forward = !forward;
|
|
1125
|
+
sweep(forward);
|
|
1126
|
+
const next = countAll();
|
|
1127
|
+
trace(next);
|
|
1128
|
+
if (next >= crossings) {
|
|
1129
|
+
for (const [index, layer] of previous.entries()) layers[index] = layer;
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
crossings = next;
|
|
1133
|
+
}
|
|
1134
|
+
return { ...order, layers };
|
|
1005
1135
|
}
|
|
1136
|
+
let improved: boolean;
|
|
1137
|
+
do {
|
|
1138
|
+
improved = sweep(forward);
|
|
1139
|
+
trace(-1);
|
|
1140
|
+
forward = !forward;
|
|
1141
|
+
} while (improved);
|
|
1006
1142
|
return { ...order, layers };
|
|
1007
1143
|
}
|
|
1008
1144
|
|
|
@@ -1053,7 +1189,7 @@ export function applyForcedModelOrder(
|
|
|
1053
1189
|
order: LayerOrder,
|
|
1054
1190
|
): LayerOrder {
|
|
1055
1191
|
const strategy = input.settings["considerModelOrder.strategy"] ?? "NONE";
|
|
1056
|
-
if (strategy === "NONE") return order;
|
|
1192
|
+
if (strategy === "NONE" || modelSweepInputs.has(input)) return order;
|
|
1057
1193
|
const forceNodeOrder =
|
|
1058
1194
|
input.settings["crossingMinimization.forceNodeModelOrder"] === true &&
|
|
1059
1195
|
(strategy === "NODES_AND_EDGES" || strategy === "PREFER_NODES");
|
|
@@ -1191,10 +1327,48 @@ export function applyForcedModelOrder(
|
|
|
1191
1327
|
}
|
|
1192
1328
|
}
|
|
1193
1329
|
}
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
|
|
1197
|
-
|
|
1330
|
+
// Node reordering invalidates flexible physical-port ranks. Reconcile them
|
|
1331
|
+
// using the same distributor and rank convention as the preceding sweep;
|
|
1332
|
+
// retain fixed boundary orders for the canonical greedy crossing counter.
|
|
1333
|
+
const incoming = clonePortOrders(order.inputPortOrderByNodeId);
|
|
1334
|
+
const outgoing = clonePortOrders(order.outputPortOrderByNodeId);
|
|
1335
|
+
const graph = crossingGraph(input, orientation, layers, incoming, outgoing);
|
|
1336
|
+
const distributor = new CanonicalPortDistributor(graph);
|
|
1337
|
+
const fixedOrder = new Set(
|
|
1338
|
+
input.graph.nodes
|
|
1339
|
+
.filter((node) => {
|
|
1340
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
1341
|
+
return (
|
|
1342
|
+
constraint === "FIXED_ORDER" || constraint === "FIXED_POS" || constraint === "FIXED_RATIO"
|
|
1343
|
+
);
|
|
1344
|
+
})
|
|
1345
|
+
.map((node) => node.id),
|
|
1346
|
+
);
|
|
1347
|
+
for (let layer = layers.length - 1; layer >= 0; layer--) {
|
|
1348
|
+
distributor.distribute(graph, layer, false, {
|
|
1349
|
+
nodeRelative: nodeRelativePortRanksByInput.get(input) ?? true,
|
|
1350
|
+
fixedOrder,
|
|
1351
|
+
hierarchical: new Set(),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
const edges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
1355
|
+
const endpoints = new Map(edges.map((edge, index) => [edge.id, graph.edges[index]!]));
|
|
1356
|
+
for (const node of graph.layers.flat()) {
|
|
1357
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
1358
|
+
for (const [orders, source] of [
|
|
1359
|
+
[outgoing, true],
|
|
1360
|
+
[incoming, false],
|
|
1361
|
+
] as const) {
|
|
1362
|
+
orders
|
|
1363
|
+
.get(node.id)
|
|
1364
|
+
?.sort(
|
|
1365
|
+
(left, right) =>
|
|
1366
|
+
ordinal.get(endpoints.get(left)![source ? "source" : "target"])! -
|
|
1367
|
+
ordinal.get(endpoints.get(right)![source ? "source" : "target"])!,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return { ...order, layers, inputPortOrderByNodeId: incoming, outputPortOrderByNodeId: outgoing };
|
|
1198
1372
|
}
|
|
1199
1373
|
|
|
1200
1374
|
/** ELK LONGEST_PATH: align sinks on the final layer. */
|
|
@@ -1607,10 +1781,25 @@ function sortLayerByAdjacentPosition(
|
|
|
1607
1781
|
preOrdered = true,
|
|
1608
1782
|
sourceUnknownPlacement = true,
|
|
1609
1783
|
perturbSummedWeight = true,
|
|
1784
|
+
units?: CrossingUnits,
|
|
1785
|
+
sameLayerNeighbors: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
1786
|
+
orderedVisits?: ReadonlyMap<string, readonly (number | string)[]>,
|
|
1787
|
+
forcedCompare?: (left: string, right: string) => number,
|
|
1610
1788
|
): void {
|
|
1611
1789
|
const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index] as const));
|
|
1612
1790
|
const adjacentPosition = new Map<string, number | undefined>();
|
|
1613
|
-
|
|
1791
|
+
if (units && statistic === "mean") {
|
|
1792
|
+
for (const [id, score] of associatedBarycenters(
|
|
1793
|
+
layer,
|
|
1794
|
+
ranksByNodeId,
|
|
1795
|
+
sameLayerNeighbors,
|
|
1796
|
+
units.associates,
|
|
1797
|
+
random,
|
|
1798
|
+
orderedVisits,
|
|
1799
|
+
))
|
|
1800
|
+
adjacentPosition.set(id, score);
|
|
1801
|
+
}
|
|
1802
|
+
for (const nodeId of units && statistic === "mean" ? [] : layer) {
|
|
1614
1803
|
const positions = [...(ranksByNodeId.get(nodeId) ?? [])].sort((left, right) => left - right);
|
|
1615
1804
|
if (positions.length === 0) {
|
|
1616
1805
|
adjacentPosition.set(nodeId, undefined);
|
|
@@ -1671,174 +1860,374 @@ function sortLayerByAdjacentPosition(
|
|
|
1671
1860
|
}
|
|
1672
1861
|
}
|
|
1673
1862
|
|
|
1674
|
-
|
|
1863
|
+
const compare = (a: string, b: string) => {
|
|
1675
1864
|
return (
|
|
1865
|
+
forcedCompare?.(a, b) ||
|
|
1676
1866
|
(adjacentPosition.get(a) ?? 0) - (adjacentPosition.get(b) ?? 0) ||
|
|
1677
1867
|
(originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0)
|
|
1678
1868
|
);
|
|
1679
|
-
}
|
|
1869
|
+
};
|
|
1870
|
+
if (forcedCompare)
|
|
1871
|
+
insertionSort(
|
|
1872
|
+
layer,
|
|
1873
|
+
transitiveComparator((id: string) => id, compare),
|
|
1874
|
+
);
|
|
1875
|
+
else layer.sort(compare);
|
|
1876
|
+
if (units && !forcedCompare)
|
|
1877
|
+
layer.splice(0, layer.length, ...resolveCrossingConstraints(layer, adjacentPosition, units));
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
type LayerSweepVisitor = (
|
|
1881
|
+
layerIndex: number,
|
|
1882
|
+
nodeIds: readonly string[],
|
|
1883
|
+
forward: boolean,
|
|
1884
|
+
firstSweep: boolean,
|
|
1885
|
+
) => void;
|
|
1886
|
+
|
|
1887
|
+
/** @internal */
|
|
1888
|
+
export interface LayerSweepSession {
|
|
1889
|
+
/** Scope graph id, for development tracing. */
|
|
1890
|
+
readonly scope: string;
|
|
1891
|
+
readonly random: JavaRandom;
|
|
1892
|
+
readonly attempts: number;
|
|
1893
|
+
readonly restoreRejectedSweep: boolean;
|
|
1894
|
+
readonly usesInitialModelOrder?: boolean;
|
|
1895
|
+
/** Clear the scope flag after its first counter-based sweep. */
|
|
1896
|
+
finishInitialOrderAttempt?(): void;
|
|
1897
|
+
useRandom(random: JavaRandom): void;
|
|
1898
|
+
lockPortOrder(nodeId: string): void;
|
|
1899
|
+
markHierarchicalNode(nodeId: string): void;
|
|
1900
|
+
minimize(): LayerOrder;
|
|
1901
|
+
shuffleFirstLayer(forward: boolean): void;
|
|
1902
|
+
sweep(forward: boolean, firstSweep: boolean, visitLayer?: LayerSweepVisitor): void;
|
|
1903
|
+
countCrossings(): number;
|
|
1904
|
+
snapshot(): LayerOrder;
|
|
1905
|
+
restore(order: LayerOrder): void;
|
|
1906
|
+
finish(order: LayerOrder): LayerOrder;
|
|
1907
|
+
/** Live current layers; callers must not mutate them. */
|
|
1908
|
+
currentLayers(): readonly (readonly string[])[];
|
|
1909
|
+
/** Current canonical port ids of one node, as {@link snapshot} records them. */
|
|
1910
|
+
physicalPortOrder(nodeId: string): string[];
|
|
1911
|
+
/** Canonical crossing graph in the current order; callers must not mutate it. */
|
|
1912
|
+
crossingGraph(): CrossingGraph;
|
|
1913
|
+
/** Live incident edge orders of one node; callers must not mutate them. */
|
|
1914
|
+
edgeOrders(nodeId: string): { incoming: readonly string[]; outgoing: readonly string[] };
|
|
1915
|
+
/** Same as restoring a snapshot that differs only in one layer. */
|
|
1916
|
+
replaceLayer(index: number, nodeIds: readonly string[]): void;
|
|
1917
|
+
/** Same as restoring a snapshot that differs only in one node's port and edge orders. */
|
|
1918
|
+
reorderNodePorts(
|
|
1919
|
+
nodeId: string,
|
|
1920
|
+
physical: readonly string[],
|
|
1921
|
+
incoming: readonly string[],
|
|
1922
|
+
outgoing: readonly string[],
|
|
1923
|
+
): void;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function clonePortOrders(orders?: ReadonlyMap<string, readonly string[]>): Map<string, string[]> {
|
|
1927
|
+
return new Map([...(orders ?? [])].map(([id, edges]) => [id, [...edges]]));
|
|
1680
1928
|
}
|
|
1681
1929
|
|
|
1682
1930
|
function minimizeCrossingsWithLayerSweep(
|
|
1683
1931
|
statistic: "mean" | "median",
|
|
1684
|
-
sweeps =
|
|
1932
|
+
sweeps = 7,
|
|
1685
1933
|
): CrossingMinimizer {
|
|
1686
|
-
return (input, orientation, assignment) =>
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1934
|
+
return (input, orientation, assignment) =>
|
|
1935
|
+
createLayerSweepSession(input, orientation, assignment, statistic, sweeps).minimize();
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/** Internal mutable sweep state shared by standalone and hierarchy coordination. */
|
|
1939
|
+
export function createLayerSweepSession(
|
|
1940
|
+
input: LayeredPhaseInput,
|
|
1941
|
+
orientation: AcyclicOrientation,
|
|
1942
|
+
assignment: LayerAssignment,
|
|
1943
|
+
statistic: "mean" | "median" = "mean",
|
|
1944
|
+
sweeps = 7,
|
|
1945
|
+
initialization: { resetSeed?: boolean } = {},
|
|
1946
|
+
): LayerSweepSession {
|
|
1947
|
+
const exactPortSweep =
|
|
1948
|
+
(input.settings["wrapping.strategy"] ?? "OFF") === "OFF" &&
|
|
1949
|
+
(input.settings["layerUnzipping.strategy"] ?? "NONE") === "NONE";
|
|
1950
|
+
let maximumLayer = 0;
|
|
1951
|
+
for (const layer of assignment.layerByNodeId.values()) {
|
|
1952
|
+
maximumLayer = Math.max(maximumLayer, layer);
|
|
1953
|
+
}
|
|
1954
|
+
const layerCount = maximumLayer + 1;
|
|
1955
|
+
const layers = Array.from({ length: layerCount }, () => [] as string[]);
|
|
1956
|
+
const initialNodes = assignment.seedOrder
|
|
1957
|
+
? completeSeedOrder(input, assignment)
|
|
1958
|
+
: (input.settings["layering.strategy"] ?? "NETWORK_SIMPLEX") === "NETWORK_SIMPLEX"
|
|
1959
|
+
? networkSimplexComponentOrder(input)
|
|
1960
|
+
: input.graph.nodes.map((node) => node.id);
|
|
1961
|
+
for (const nodeId of initialNodes) {
|
|
1962
|
+
layers[assignment.layerByNodeId.get(nodeId) ?? 0]?.push(nodeId);
|
|
1963
|
+
}
|
|
1964
|
+
const units = getCrossingUnits(input);
|
|
1965
|
+
// Edges, orientation and layer assignment stay fixed for the session.
|
|
1966
|
+
const sameLayerNeighborsByDirection = new Map<boolean, Map<string, string[]>>();
|
|
1967
|
+
const sameLayerNeighbors = (forward: boolean) => {
|
|
1968
|
+
let cached = sameLayerNeighborsByDirection.get(forward);
|
|
1969
|
+
if (!cached)
|
|
1970
|
+
sameLayerNeighborsByDirection.set(forward, (cached = findSameLayerNeighbors(forward)));
|
|
1971
|
+
return cached;
|
|
1972
|
+
};
|
|
1973
|
+
const findSameLayerNeighbors = (forward: boolean) => {
|
|
1974
|
+
const neighbors = new Map<string, string[]>();
|
|
1975
|
+
for (const edge of input.graph.edges) {
|
|
1976
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
1977
|
+
if (
|
|
1978
|
+
source !== target &&
|
|
1979
|
+
assignment.layerByNodeId.get(source) === assignment.layerByNodeId.get(target)
|
|
1980
|
+
) {
|
|
1981
|
+
const free = forward ? target : source,
|
|
1982
|
+
fixed = forward ? source : target;
|
|
1983
|
+
neighbors.set(free, [...(neighbors.get(free) ?? []), fixed]);
|
|
1984
|
+
}
|
|
1695
1985
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1986
|
+
return neighbors;
|
|
1987
|
+
};
|
|
1988
|
+
const inputPortOrder = new Map(input.graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
1989
|
+
const outputPortOrder = new Map(input.graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
1990
|
+
for (const edge of input.graph.edges) {
|
|
1991
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
1992
|
+
// ELK detaches self-loop edges before crossing minimization, then restores
|
|
1993
|
+
// them afterward. Their connectivity must not consume sweep port ranks.
|
|
1994
|
+
if (sourceId === targetId) continue;
|
|
1995
|
+
outputPortOrder.get(sourceId)?.push(edge.id);
|
|
1996
|
+
inputPortOrder.get(targetId)?.push(edge.id);
|
|
1997
|
+
}
|
|
1998
|
+
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
1999
|
+
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
2000
|
+
// ELK lists a compound's boundary ports in creation order: edges on them
|
|
2001
|
+
// take their slots in that order, in model order within a port. Authored
|
|
2002
|
+
// ports already arrive in ELK's sorted order and keep their edge order.
|
|
2003
|
+
const boundaryPortIndex = (nodeId: string, edgeId: string, incoming: boolean) => {
|
|
2004
|
+
const edge = edgeById.get(edgeId)!;
|
|
2005
|
+
const name =
|
|
2006
|
+
incoming !== orientation.reversedEdgeIds.has(edgeId) ? edge.targetPort : edge.sourcePort;
|
|
2007
|
+
if (!name?.startsWith("__native_hierarchy")) return -1;
|
|
2008
|
+
return nodeById.get(nodeId)!.ports?.findIndex((port) => port.name === name) ?? -1;
|
|
2009
|
+
};
|
|
2010
|
+
for (const [orders, incoming] of [
|
|
2011
|
+
[outputPortOrder, false],
|
|
2012
|
+
[inputPortOrder, true],
|
|
2013
|
+
] as const)
|
|
2014
|
+
for (const [nodeId, edgeIds] of orders) {
|
|
2015
|
+
if (edgeIds.length < 2) continue;
|
|
2016
|
+
const slots = edgeIds.flatMap((id, slot) =>
|
|
2017
|
+
boundaryPortIndex(nodeId, id, incoming) < 0 ? [] : [slot],
|
|
2018
|
+
);
|
|
2019
|
+
const sorted = slots
|
|
2020
|
+
.map((slot) => edgeIds[slot]!)
|
|
2021
|
+
.sort(
|
|
2022
|
+
(a, b) => boundaryPortIndex(nodeId, a, incoming) - boundaryPortIndex(nodeId, b, incoming),
|
|
2023
|
+
);
|
|
2024
|
+
slots.forEach((slot, index) => (edgeIds[slot] = sorted[index]!));
|
|
1705
2025
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2026
|
+
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
2027
|
+
const hierarchicalNodes = new Set<string>();
|
|
2028
|
+
const fixedOrderNodes = new Set(
|
|
2029
|
+
input.graph.nodes
|
|
2030
|
+
.filter((node) => {
|
|
2031
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
2032
|
+
return !!node.ports?.length && (constraint === "FIXED_POS" || constraint === "FIXED_RATIO");
|
|
2033
|
+
})
|
|
2034
|
+
.map((node) => node.id),
|
|
2035
|
+
);
|
|
2036
|
+
const fixedPortRank = (nodeId: string, edgeId: string, incoming: boolean): number => {
|
|
2037
|
+
const node = nodeById.get(nodeId)!;
|
|
2038
|
+
const edge = edgeById.get(edgeId)!;
|
|
2039
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
2040
|
+
const portName = incoming !== reversed ? edge.targetPort : edge.sourcePort;
|
|
2041
|
+
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
2042
|
+
const horizontal = input.direction === "right" || input.direction === "left";
|
|
2043
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
2044
|
+
const size = input.sizes.get(nodeId)!;
|
|
2045
|
+
const width = horizontal ? size.width : size.height;
|
|
2046
|
+
const height = horizontal ? size.height : size.width;
|
|
2047
|
+
const x = (port?.x ?? size.width / 2) + (port?.width ?? 0) / 2;
|
|
2048
|
+
const y = (port?.y ?? size.height / 2) + (port?.height ?? 0) / 2;
|
|
2049
|
+
const flow = reverse ? width - (horizontal ? x : y) : horizontal ? x : y;
|
|
2050
|
+
const cross = horizontal ? y : x;
|
|
2051
|
+
const side = port && input.portSettings?.(port, node)?.["port.side"];
|
|
2052
|
+
const flowSide = horizontal ? (reverse ? "WEST" : "EAST") : reverse ? "NORTH" : "SOUTH";
|
|
2053
|
+
const backSide = horizontal ? (reverse ? "EAST" : "WEST") : reverse ? "SOUTH" : "NORTH";
|
|
2054
|
+
const upperSide = horizontal ? "NORTH" : "WEST";
|
|
2055
|
+
// Clockwise boundary order in the canonical rightward coordinate system.
|
|
2056
|
+
if (side === upperSide) return flow;
|
|
2057
|
+
if (side === flowSide || (!side && !incoming)) return width + cross;
|
|
2058
|
+
if (side === backSide || (!side && incoming)) return 2 * width + 2 * height - cross;
|
|
2059
|
+
return 2 * width + height - flow;
|
|
2060
|
+
};
|
|
2061
|
+
// Port distribution may reorder flexible ports, never fixed physical positions.
|
|
2062
|
+
for (const nodeId of fixedOrderNodes) {
|
|
2063
|
+
outputPortOrder
|
|
2064
|
+
.get(nodeId)
|
|
2065
|
+
?.sort((a, b) => fixedPortRank(nodeId, a, false) - fixedPortRank(nodeId, b, false));
|
|
2066
|
+
inputPortOrder
|
|
2067
|
+
.get(nodeId)
|
|
2068
|
+
?.sort((a, b) => fixedPortRank(nodeId, a, true) - fixedPortRank(nodeId, b, true));
|
|
2069
|
+
}
|
|
2070
|
+
if (exactPortSweep) {
|
|
2071
|
+
for (const [nodeId, edgeIds] of inputPortOrder) {
|
|
2072
|
+
if (fixedOrderNodes.has(nodeId)) continue;
|
|
2073
|
+
if (edgeIds.length >= 4) {
|
|
2074
|
+
edgeIds.splice(0, edgeIds.length - 1, ...edgeIds.slice(0, -1).reverse());
|
|
2075
|
+
}
|
|
1712
2076
|
}
|
|
2077
|
+
}
|
|
2078
|
+
const buildCrossingGraph = createCrossingGraphBuilder(input);
|
|
2079
|
+
const crossingModel: CrossingModel =
|
|
2080
|
+
(input.settings as { [CROSSING_MODEL]?: CrossingModel })[CROSSING_MODEL] ?? {};
|
|
2081
|
+
const mixedPorts = crossingModel.mixedPorts !== false,
|
|
2082
|
+
probe = crossingModel.probe;
|
|
2083
|
+
const countCrossings = (candidateLayers: readonly (readonly string[])[]): number => {
|
|
1713
2084
|
if (exactPortSweep) {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2085
|
+
const graph = buildCrossingGraph(
|
|
2086
|
+
orientation,
|
|
2087
|
+
candidateLayers,
|
|
2088
|
+
inputPortOrder,
|
|
2089
|
+
outputPortOrder,
|
|
2090
|
+
);
|
|
2091
|
+
// Edge orders rank a side's ports per direction; a fixed-side port with
|
|
2092
|
+
// a reversed edge mixes both, so count the sweep's physical port order.
|
|
2093
|
+
for (const layer of graph.layers)
|
|
2094
|
+
for (const node of layer) {
|
|
2095
|
+
const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : undefined;
|
|
2096
|
+
if (physical?.length !== node.ports.length) continue;
|
|
2097
|
+
const slot = new Map(physical.map((port, index) => [port.id, index]));
|
|
2098
|
+
if (node.ports.every((port) => slot.has(port.id)))
|
|
2099
|
+
node.ports.sort((a, b) => slot.get(a.id)! - slot.get(b.id)!);
|
|
1717
2100
|
}
|
|
2101
|
+
const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
|
|
2102
|
+
const score = countAllCrossings(
|
|
2103
|
+
split,
|
|
2104
|
+
crossingModel.separateSharedTracks ? "separated" : "hyperedges",
|
|
2105
|
+
);
|
|
2106
|
+
if (probe) {
|
|
2107
|
+
if (split !== graph) probe.mixedPorts = true;
|
|
2108
|
+
if (score.sharedTracks) probe.sharedTracks = true;
|
|
1718
2109
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
input.graph.edges[right]!,
|
|
1735
|
-
orientation,
|
|
1736
|
-
);
|
|
1737
|
-
if (leftSource === rightSource || leftTarget === rightTarget) continue;
|
|
1738
|
-
const sourceDifference =
|
|
1739
|
-
(positions.get(leftSource) ?? 0) - (positions.get(rightSource) ?? 0);
|
|
1740
|
-
const targetDifference =
|
|
1741
|
-
(positions.get(leftTarget) ?? 0) - (positions.get(rightTarget) ?? 0);
|
|
1742
|
-
if (sourceDifference * targetDifference < 0) crossings++;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
return crossings;
|
|
2110
|
+
let total = score.total;
|
|
2111
|
+
const nodeInfluence = Number(
|
|
2112
|
+
input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0,
|
|
2113
|
+
);
|
|
2114
|
+
// ELK 0.11.1 tests node influence twice when selecting the weighted counter.
|
|
2115
|
+
// Port influence alone therefore does not enable its model-order objective.
|
|
2116
|
+
if (
|
|
2117
|
+
(input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE" &&
|
|
2118
|
+
nodeInfluence !== 0
|
|
2119
|
+
) {
|
|
2120
|
+
const changes = countModelOrderChanges(input, orientation, graph);
|
|
2121
|
+
total +=
|
|
2122
|
+
nodeInfluence * changes.nodeChanges +
|
|
2123
|
+
Number(input.settings["considerModelOrder.crossingCounterPortInfluence"] ?? 0) *
|
|
2124
|
+
changes.portChanges;
|
|
1746
2125
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2126
|
+
return total;
|
|
2127
|
+
}
|
|
2128
|
+
const positions = new Map<string, number>();
|
|
2129
|
+
for (const layer of candidateLayers) {
|
|
2130
|
+
for (const [index, id] of layer.entries()) positions.set(id, index);
|
|
2131
|
+
}
|
|
2132
|
+
let crossings = 0;
|
|
2133
|
+
for (let left = 0; left < input.graph.edges.length; left++) {
|
|
2134
|
+
const [leftSource, leftTarget] = getOrientedEndpoints(input.graph.edges[left]!, orientation);
|
|
2135
|
+
for (let right = left + 1; right < input.graph.edges.length; right++) {
|
|
2136
|
+
const [rightSource, rightTarget] = getOrientedEndpoints(
|
|
2137
|
+
input.graph.edges[right]!,
|
|
2138
|
+
orientation,
|
|
2139
|
+
);
|
|
2140
|
+
if (leftSource === rightSource || leftTarget === rightTarget) continue;
|
|
2141
|
+
const sourceDifference =
|
|
2142
|
+
(positions.get(leftSource) ?? 0) - (positions.get(rightSource) ?? 0);
|
|
2143
|
+
const targetDifference =
|
|
2144
|
+
(positions.get(leftTarget) ?? 0) - (positions.get(rightTarget) ?? 0);
|
|
2145
|
+
if (sourceDifference * targetDifference < 0) crossings++;
|
|
1752
2146
|
}
|
|
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
|
-
|
|
2147
|
+
}
|
|
2148
|
+
return crossings;
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
const sharedRandom = crossingRandom(input);
|
|
2152
|
+
const randomSeed = initialization.resetSeed === false ? undefined : sharedRandom.nextLong();
|
|
2153
|
+
const portDistributorUsesNodeRelativeRanks = sharedRandom.nextBoolean();
|
|
2154
|
+
const nodeRelativePortRanks = portDistributorUsesNodeRelativeRanks;
|
|
2155
|
+
nodeRelativePortRanksByInput.set(input, nodeRelativePortRanks);
|
|
2156
|
+
// ELK treats the median heuristic as deterministic, so it keeps using the
|
|
2157
|
+
// graph's shared RNG after port-distributor selection. Barycenter resets to
|
|
2158
|
+
// the saved seed while comparing randomized layouts.
|
|
2159
|
+
let random =
|
|
2160
|
+
statistic === "median" || randomSeed === undefined ? sharedRandom : new JavaRandom(randomSeed);
|
|
2161
|
+
const thoroughness = Math.max(1, input.settings.thoroughness ?? sweeps ?? 7);
|
|
2162
|
+
let working = layers.map((layer) => [...layer]);
|
|
2163
|
+
const usesInitialModelOrder =
|
|
2164
|
+
statistic === "mean" && (input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE";
|
|
2165
|
+
// Preserved model order makes unknown barycenters interpolate rather than
|
|
2166
|
+
// consume random floats during the first attempt. Child scopes keep their
|
|
2167
|
+
// own flag until their counter-based attempt finishes.
|
|
2168
|
+
let initialOrderAttempt = usesInitialModelOrder;
|
|
2169
|
+
const sourceUnknownPlacement = true;
|
|
2170
|
+
const forcedCompare =
|
|
2171
|
+
usesInitialModelOrder && input.settings["crossingMinimization.forceNodeModelOrder"] === true
|
|
2172
|
+
? normalModelComparator(input)
|
|
2173
|
+
: undefined;
|
|
2174
|
+
const usePortRanks = true;
|
|
2175
|
+
const medianWeights = new Map<string, number>();
|
|
2176
|
+
const listedEdgeIds = new Map<string, string>();
|
|
2177
|
+
const listedEdgeId = (edgeId: string) => {
|
|
2178
|
+
let listed = listedEdgeIds.get(edgeId);
|
|
2179
|
+
if (listed === undefined)
|
|
2180
|
+
listedEdgeIds.set(edgeId, (listed = edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "")));
|
|
2181
|
+
return listed;
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
const adjacentRanks = (
|
|
2185
|
+
fixedLayer: readonly string[],
|
|
2186
|
+
freeLayer: readonly string[],
|
|
2187
|
+
forward: boolean,
|
|
2188
|
+
): { ranks: Map<string, number[]>; visits: Map<string, (number | string)[]> } => {
|
|
2189
|
+
const edgeRanks = new Map<string, number>();
|
|
2190
|
+
const visits = new Map(freeLayer.map((id) => [id, [] as (number | string)[]]));
|
|
2191
|
+
const freeIds = new Set(freeLayer);
|
|
2192
|
+
const ranks = new Map(freeLayer.map((id) => [id, [] as number[]]));
|
|
2193
|
+
if (!usePortRanks) {
|
|
2194
|
+
const fixedPosition = new Map(fixedLayer.map((id, index) => [id, index]));
|
|
2195
|
+
for (const edge of input.graph.edges) {
|
|
2196
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
2197
|
+
if (forward && fixedPosition.has(sourceId) && freeIds.has(targetId)) {
|
|
2198
|
+
ranks.get(targetId)?.push(fixedPosition.get(sourceId)!);
|
|
2199
|
+
} else if (!forward && fixedPosition.has(targetId) && freeIds.has(sourceId)) {
|
|
2200
|
+
ranks.get(sourceId)?.push(fixedPosition.get(targetId)!);
|
|
1799
2201
|
}
|
|
1800
2202
|
}
|
|
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
|
-
|
|
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
|
-
}
|
|
2203
|
+
return { ranks, visits };
|
|
2204
|
+
}
|
|
2205
|
+
if (exactPortSweep) {
|
|
2206
|
+
const fixedIndex = assignment.layerByNodeId.get(fixedLayer[0] ?? "");
|
|
2207
|
+
if (fixedIndex !== undefined) {
|
|
2208
|
+
const nodes = fixedLayer.map((id) => canonicalNodes.get(id)!);
|
|
2209
|
+
canonicalGraph.layers[fixedIndex] = nodes;
|
|
2210
|
+
canonicalDistributor.calculatePortRanks(
|
|
2211
|
+
canonicalGraph,
|
|
2212
|
+
nodes,
|
|
2213
|
+
!forward,
|
|
2214
|
+
nodeRelativePortRanks,
|
|
2215
|
+
);
|
|
2216
|
+
// The fixed layer's edges toward the free layer, in edge order.
|
|
2217
|
+
const byEnd = forward ? canonicalEdgesBySource : canonicalEdgesByTarget;
|
|
2218
|
+
const indices = fixedLayer.flatMap((id) => byEnd.get(id) ?? []).sort((a, b) => a - b);
|
|
2219
|
+
for (const index of indices) {
|
|
2220
|
+
const edge = canonicalEdges[index]!;
|
|
2221
|
+
const [source, target] = canonicalOrientedEndpoints[index]!;
|
|
2222
|
+
if (!freeIds.has(forward ? target : source)) continue;
|
|
2223
|
+
const endpoint = canonicalEndpoints.get(edge.id)!;
|
|
2224
|
+
const rank =
|
|
2225
|
+
canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target]!;
|
|
2226
|
+
ranks.get(forward ? target : source)!.push(rank);
|
|
2227
|
+
edgeRanks.set(edge.id, rank);
|
|
1839
2228
|
}
|
|
1840
|
-
return ranks;
|
|
1841
2229
|
}
|
|
2230
|
+
} else {
|
|
1842
2231
|
let rankSum = 0;
|
|
1843
2232
|
for (const fixedId of fixedLayer) {
|
|
1844
2233
|
const groups = new Map<string, { edges: GraphEdge[]; portOrder: number }>();
|
|
@@ -1886,188 +2275,343 @@ function minimizeCrossingsWithLayerSweep(
|
|
|
1886
2275
|
for (const edge of group.edges) {
|
|
1887
2276
|
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
1888
2277
|
ranks.get(forward ? targetId : sourceId)?.push(rank);
|
|
2278
|
+
edgeRanks.set(edge.id, rank);
|
|
1889
2279
|
}
|
|
1890
2280
|
}
|
|
1891
2281
|
rankSum += nodeRelativePortRanks ? 1 : count;
|
|
1892
2282
|
}
|
|
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
|
-
});
|
|
2283
|
+
}
|
|
2284
|
+
// Visit each free node's ports in their current canonical order, while
|
|
2285
|
+
// preserving incident-edge order within a shared port. Fixed-layer ranks
|
|
2286
|
+
// and same-layer recursion must not be separated into independent sums.
|
|
2287
|
+
for (const id of freeLayer) {
|
|
2288
|
+
const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
|
|
2289
|
+
const groups = new Map<string, string[]>();
|
|
2290
|
+
for (const edgeId of order) {
|
|
2291
|
+
const edge = edgeById.get(edgeId)!;
|
|
2292
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
2293
|
+
const name = forward !== reversed ? edge.targetPort : edge.sourcePort;
|
|
2294
|
+
const key = name ?? `__implicit:${edgeId}`;
|
|
2295
|
+
const group = groups.get(key);
|
|
2296
|
+
if (group) group.push(edgeId);
|
|
2297
|
+
else groups.set(key, [edgeId]);
|
|
2298
|
+
}
|
|
2299
|
+
for (const group of groups.values()) {
|
|
2300
|
+
// Port sorting reorders ports, not the edges on one port; those keep
|
|
2301
|
+
// ELK's edge list order. Expanded segments keep their listed edge id.
|
|
2302
|
+
const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
|
|
2303
|
+
const rank = (edgeId: string) => listed?.get(listedEdgeId(edgeId));
|
|
2304
|
+
group.sort(
|
|
2305
|
+
(a, b) =>
|
|
2306
|
+
(rank(a) !== undefined && rank(b) !== undefined ? rank(a)! - rank(b)! : 0) ||
|
|
2307
|
+
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2308
|
+
);
|
|
2309
|
+
for (const edgeId of group) {
|
|
2310
|
+
const edge = edgeById.get(edgeId)!;
|
|
2311
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2312
|
+
const neighbor = forward ? source : target;
|
|
2313
|
+
const rank = edgeRanks.get(edgeId);
|
|
2314
|
+
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2315
|
+
visits.get(id)!.push(neighbor);
|
|
2316
|
+
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
1936
2317
|
}
|
|
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
2318
|
}
|
|
1949
|
-
}
|
|
2319
|
+
}
|
|
2320
|
+
return { ranks, visits };
|
|
2321
|
+
};
|
|
1950
2322
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2323
|
+
let canonicalGraph = buildCrossingGraph(orientation, working, inputPortOrder, outputPortOrder);
|
|
2324
|
+
if (usesInitialModelOrder) {
|
|
2325
|
+
if (!restoreInitialModelOrder(input, canonicalGraph))
|
|
2326
|
+
sortInitialModelOrder(input, orientation, canonicalGraph);
|
|
2327
|
+
working = canonicalGraph.layers.map((layer) => layer.map((node) => node.id));
|
|
2328
|
+
const edges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
2329
|
+
const endpoints = new Map(edges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]));
|
|
2330
|
+
for (const node of canonicalGraph.layers.flat()) {
|
|
2331
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2332
|
+
for (const [orders, source] of [
|
|
2333
|
+
[outputPortOrder, true],
|
|
2334
|
+
[inputPortOrder, false],
|
|
2335
|
+
] as const) {
|
|
2336
|
+
orders
|
|
2337
|
+
.get(node.id)
|
|
2338
|
+
?.sort(
|
|
2339
|
+
(a, b) =>
|
|
2340
|
+
ordinal.get(endpoints.get(a)![source ? "source" : "target"])! -
|
|
2341
|
+
ordinal.get(endpoints.get(b)![source ? "source" : "target"])!,
|
|
2342
|
+
);
|
|
1963
2343
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2344
|
+
}
|
|
2345
|
+
modelSweepInputs.add(input);
|
|
2346
|
+
}
|
|
2347
|
+
let canonicalNodes = new Map(canonicalGraph.layers.flat().map((node) => [node.id, node]));
|
|
2348
|
+
const canonicalDistributor = new CanonicalPortDistributor(canonicalGraph);
|
|
2349
|
+
const canonicalEdges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
|
|
2350
|
+
const canonicalReversed = canonicalEdges.map((edge) => orientation.reversedEdgeIds.has(edge.id));
|
|
2351
|
+
const canonicalOrientedEndpoints = canonicalEdges.map((edge) =>
|
|
2352
|
+
getOrientedEndpoints(edge, orientation),
|
|
2353
|
+
);
|
|
2354
|
+
const canonicalEdgesBySource = new Map<string, number[]>(),
|
|
2355
|
+
canonicalEdgesByTarget = new Map<string, number[]>();
|
|
2356
|
+
canonicalOrientedEndpoints.forEach(([source, target], index) => {
|
|
2357
|
+
for (const [byEnd, id] of [
|
|
2358
|
+
[canonicalEdgesBySource, source],
|
|
2359
|
+
[canonicalEdgesByTarget, target],
|
|
2360
|
+
] as const) {
|
|
2361
|
+
const list = byEnd.get(id);
|
|
2362
|
+
if (list) list.push(index);
|
|
2363
|
+
else byEnd.set(id, [index]);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2366
|
+
const canonicalEndpoints = new Map(
|
|
2367
|
+
canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]),
|
|
2368
|
+
);
|
|
2369
|
+
const canonicalFixedOrder = new Set(
|
|
2370
|
+
input.graph.nodes
|
|
2371
|
+
.filter((node) => {
|
|
2372
|
+
const constraint = input.nodeSettings?.(node)?.portConstraints;
|
|
2373
|
+
return (
|
|
2374
|
+
constraint === "FIXED_ORDER" || constraint === "FIXED_POS" || constraint === "FIXED_RATIO"
|
|
2375
|
+
);
|
|
2376
|
+
})
|
|
2377
|
+
.map((node) => node.id),
|
|
2378
|
+
);
|
|
2379
|
+
// Every canonically fixed node plus each port order locked by a child scope.
|
|
2380
|
+
const fixedPortOrders = new Set(canonicalFixedOrder);
|
|
2381
|
+
const distributePorts = (layerIndex: number, forward: boolean) => {
|
|
2382
|
+
const touched = [layerIndex, layerIndex + (forward ? -1 : 1)].filter(
|
|
2383
|
+
(index) => index >= 0 && index < working.length,
|
|
2384
|
+
);
|
|
2385
|
+
for (const index of touched)
|
|
2386
|
+
canonicalGraph.layers[index] = working[index]!.map((id) => canonicalNodes.get(id)!);
|
|
2387
|
+
canonicalDistributor.distribute(canonicalGraph, layerIndex, forward, {
|
|
2388
|
+
nodeRelative: nodeRelativePortRanks,
|
|
2389
|
+
fixedOrder: fixedPortOrders,
|
|
2390
|
+
hierarchical: hierarchicalNodes,
|
|
2391
|
+
});
|
|
2392
|
+
for (const index of touched)
|
|
2393
|
+
for (const node of canonicalGraph.layers[index]!) {
|
|
2394
|
+
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2395
|
+
for (const [orders, source] of [
|
|
2396
|
+
[outputPortOrder, true],
|
|
2397
|
+
[inputPortOrder, false],
|
|
2398
|
+
] as const)
|
|
2399
|
+
orders
|
|
2400
|
+
.get(node.id)
|
|
2401
|
+
?.sort(
|
|
2402
|
+
(left, right) =>
|
|
2403
|
+
ordinal.get(canonicalEndpoints.get(left)![source ? "source" : "target"])! -
|
|
2404
|
+
ordinal.get(canonicalEndpoints.get(right)![source ? "source" : "target"])!,
|
|
1986
2405
|
);
|
|
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
|
-
};
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2034
2408
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2409
|
+
const shuffleFirstLayer = (forward: boolean) => {
|
|
2410
|
+
const firstLayerIndex = forward ? 0 : Math.max(0, working.length - 1);
|
|
2411
|
+
const firstLayerWeights = new Map(
|
|
2412
|
+
(working[firstLayerIndex] ?? []).map((id) => [id, random.nextDouble()]),
|
|
2413
|
+
);
|
|
2414
|
+
const compare = (left: string, right: string) =>
|
|
2415
|
+
forcedCompare?.(left, right) ||
|
|
2416
|
+
(firstLayerWeights.get(left) ?? 0) - (firstLayerWeights.get(right) ?? 0);
|
|
2417
|
+
const first = working[firstLayerIndex];
|
|
2418
|
+
if (first) {
|
|
2419
|
+
if (forcedCompare)
|
|
2420
|
+
insertionSort(
|
|
2421
|
+
first,
|
|
2422
|
+
transitiveComparator((id: string) => id, compare),
|
|
2423
|
+
);
|
|
2424
|
+
else first.sort(compare);
|
|
2425
|
+
}
|
|
2426
|
+
if (units && !forcedCompare && working[firstLayerIndex])
|
|
2427
|
+
working[firstLayerIndex] = resolveCrossingConstraints(
|
|
2428
|
+
working[firstLayerIndex]!,
|
|
2429
|
+
firstLayerWeights,
|
|
2430
|
+
units,
|
|
2043
2431
|
);
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2432
|
+
if (statistic === "median") {
|
|
2433
|
+
working[firstLayerIndex]?.forEach((id, index) => medianWeights.set(id, index + 1));
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
// Non-loop edges by endpoint, for grouping a mixed port's neighbours.
|
|
2437
|
+
const edgesByNode = new Map<string, GraphEdge[]>();
|
|
2438
|
+
for (const edge of input.graph.edges) {
|
|
2439
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
2440
|
+
for (const id of [edge.sourceId, edge.targetId]) {
|
|
2441
|
+
const list = edgesByNode.get(id);
|
|
2442
|
+
if (list) list.push(edge);
|
|
2443
|
+
else edgesByNode.set(id, [edge]);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* A port carrying both directions gives each direction its own track and
|
|
2448
|
+
* attachment, so its edges are no longer interchangeable: neighbours of one
|
|
2449
|
+
* direction must not interleave with the other's, or the separated routes
|
|
2450
|
+
* cross. Barycenters tie (up to noise) for nodes reached only through one
|
|
2451
|
+
* port; within the slots those nodes occupy, put forward edges before
|
|
2452
|
+
* reversed ones. Both ends of a chain see the same reversal, so the order
|
|
2453
|
+
* holds along it.
|
|
2454
|
+
*/
|
|
2455
|
+
const constrained = new Set([
|
|
2456
|
+
...(units?.units.keys() ?? []),
|
|
2457
|
+
...[...(units?.successors ?? [])].flatMap(([id, after]) => [id, ...after]),
|
|
2458
|
+
]);
|
|
2459
|
+
const groupMixedPortDirections = (
|
|
2460
|
+
free: string[],
|
|
2461
|
+
fixed: readonly string[],
|
|
2462
|
+
beyond: readonly string[] = [],
|
|
2463
|
+
) => {
|
|
2464
|
+
if (!mixedPorts) return;
|
|
2465
|
+
const fixedIds = new Set(fixed);
|
|
2466
|
+
const groups = new Map<string, Array<{ id: string; reversed: boolean }>>();
|
|
2467
|
+
for (const id of free) {
|
|
2468
|
+
if (constrained.has(id)) continue;
|
|
2469
|
+
let only: GraphEdge | undefined,
|
|
2470
|
+
count = 0;
|
|
2471
|
+
for (const edge of edgesByNode.get(id) ?? [])
|
|
2472
|
+
if (fixedIds.has(edge.sourceId === id ? edge.targetId : edge.sourceId)) {
|
|
2473
|
+
only = edge;
|
|
2474
|
+
count++;
|
|
2052
2475
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2476
|
+
if (count !== 1) continue;
|
|
2477
|
+
const atSource = only!.sourceId !== id;
|
|
2478
|
+
const name = atSource ? only!.sourcePort : only!.targetPort;
|
|
2479
|
+
if (name === undefined) continue;
|
|
2480
|
+
const key = JSON.stringify([atSource ? only!.sourceId : only!.targetId, name]);
|
|
2481
|
+
const group = groups.get(key) ?? [];
|
|
2482
|
+
group.push({ id, reversed: orientation.reversedEdgeIds.has(only!.id) });
|
|
2483
|
+
groups.set(key, group);
|
|
2484
|
+
}
|
|
2485
|
+
if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed)))
|
|
2486
|
+
return;
|
|
2487
|
+
if (probe) probe.mixedPorts = true;
|
|
2488
|
+
const index = new Map(free.map((id, i) => [id, i]));
|
|
2489
|
+
// The direction whose nodes lead further toward the start of the layer
|
|
2490
|
+
// beyond (from the previous sweep) goes first; forward edges by default.
|
|
2491
|
+
const far = new Map(beyond.map((id, i) => [id, i]));
|
|
2492
|
+
const reach = (members: ReadonlyArray<{ id: string }>) => {
|
|
2493
|
+
let sum = 0,
|
|
2494
|
+
count = 0;
|
|
2495
|
+
for (const { id } of members)
|
|
2496
|
+
for (const edge of edgesByNode.get(id) ?? []) {
|
|
2497
|
+
const at = far.get(edge.sourceId === id ? edge.targetId : edge.sourceId);
|
|
2498
|
+
if (at === undefined) continue;
|
|
2499
|
+
sum += at;
|
|
2500
|
+
count++;
|
|
2501
|
+
}
|
|
2502
|
+
return count ? sum / count : undefined;
|
|
2503
|
+
};
|
|
2504
|
+
for (const group of groups.values()) {
|
|
2505
|
+
if (!group.some((m) => m.reversed) || group.every((m) => m.reversed)) continue;
|
|
2506
|
+
const forward = reach(group.filter((m) => !m.reversed)),
|
|
2507
|
+
backward = reach(group.filter((m) => m.reversed));
|
|
2508
|
+
const reversedFirst =
|
|
2509
|
+
forward === undefined || backward === undefined ? false : backward < forward;
|
|
2510
|
+
const slots = group.map((m) => index.get(m.id)!).sort((a, b) => a - b);
|
|
2511
|
+
const members = group
|
|
2512
|
+
.sort((a, b) => index.get(a.id)! - index.get(b.id)!)
|
|
2513
|
+
.sort((a, b) => (reversedFirst ? -1 : 1) * (Number(a.reversed) - Number(b.reversed)));
|
|
2514
|
+
for (const [i, slot] of slots.entries()) free[slot] = members[i]!.id;
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
const sweep = (isForward: boolean, firstSweep: boolean, visitLayer?: LayerSweepVisitor) => {
|
|
2518
|
+
const firstLayer = isForward ? 0 : Math.max(0, working.length - 1);
|
|
2519
|
+
if (exactPortSweep) distributePorts(firstLayer, isForward);
|
|
2520
|
+
visitLayer?.(firstLayer, [...(working[firstLayer] ?? [])], isForward, firstSweep);
|
|
2521
|
+
const sortWithMedianWeights = (current: string[], reference: readonly string[]) => {
|
|
2522
|
+
const referenceIds = new Set(reference);
|
|
2523
|
+
const originalIndex = new Map(current.map((id, index) => [id, index]));
|
|
2524
|
+
for (const id of current) {
|
|
2525
|
+
const connectedWeights = input.graph.edges
|
|
2526
|
+
.flatMap((edge) => {
|
|
2527
|
+
if (edge.sourceId === id && referenceIds.has(edge.targetId)) {
|
|
2528
|
+
return [medianWeights.get(edge.targetId) ?? 0];
|
|
2529
|
+
}
|
|
2530
|
+
if (edge.targetId === id && referenceIds.has(edge.sourceId)) {
|
|
2531
|
+
return [medianWeights.get(edge.sourceId) ?? 0];
|
|
2532
|
+
}
|
|
2533
|
+
return [];
|
|
2534
|
+
})
|
|
2535
|
+
.sort((left, right) => left - right);
|
|
2536
|
+
medianWeights.set(
|
|
2537
|
+
id,
|
|
2538
|
+
connectedWeights.length > 0
|
|
2539
|
+
? connectedWeights[Math.floor(connectedWeights.length / 2)]!
|
|
2540
|
+
: Number.MAX_VALUE / 2,
|
|
2060
2541
|
);
|
|
2061
2542
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2543
|
+
current.sort(
|
|
2544
|
+
(left, right) =>
|
|
2545
|
+
(medianWeights.get(left) ?? 0) - (medianWeights.get(right) ?? 0) ||
|
|
2546
|
+
(originalIndex.get(left) ?? 0) - (originalIndex.get(right) ?? 0),
|
|
2547
|
+
);
|
|
2548
|
+
};
|
|
2549
|
+
if (isForward) {
|
|
2550
|
+
for (let layer = 1; layer < working.length; layer++) {
|
|
2551
|
+
const current = working[layer];
|
|
2552
|
+
const previous = working[layer - 1];
|
|
2553
|
+
if (current && previous) {
|
|
2554
|
+
if (statistic === "median") sortWithMedianWeights(current, previous);
|
|
2555
|
+
else {
|
|
2556
|
+
const adjacent = adjacentRanks(previous, current, true);
|
|
2557
|
+
sortLayerByAdjacentPosition(
|
|
2558
|
+
current,
|
|
2559
|
+
adjacent.ranks,
|
|
2560
|
+
statistic,
|
|
2561
|
+
random,
|
|
2562
|
+
!firstSweep ||
|
|
2563
|
+
initialOrderAttempt ||
|
|
2564
|
+
canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT",
|
|
2565
|
+
sourceUnknownPlacement,
|
|
2566
|
+
true,
|
|
2567
|
+
units,
|
|
2568
|
+
sameLayerNeighbors(isForward),
|
|
2569
|
+
adjacent.visits,
|
|
2570
|
+
forcedCompare,
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
groupMixedPortDirections(current, previous, working[layer + 1]);
|
|
2574
|
+
if (exactPortSweep) distributePorts(layer, true);
|
|
2575
|
+
visitLayer?.(layer, [...current], isForward, firstSweep);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
} else {
|
|
2579
|
+
for (let layer = working.length - 2; layer >= 0; layer--) {
|
|
2580
|
+
const current = working[layer];
|
|
2581
|
+
const next = working[layer + 1];
|
|
2582
|
+
if (current && next) {
|
|
2583
|
+
if (statistic === "median") sortWithMedianWeights(current, next);
|
|
2584
|
+
else {
|
|
2585
|
+
const adjacent = adjacentRanks(next, current, false);
|
|
2586
|
+
sortLayerByAdjacentPosition(
|
|
2587
|
+
current,
|
|
2588
|
+
adjacent.ranks,
|
|
2589
|
+
statistic,
|
|
2590
|
+
random,
|
|
2591
|
+
!firstSweep ||
|
|
2592
|
+
initialOrderAttempt ||
|
|
2593
|
+
canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT",
|
|
2594
|
+
sourceUnknownPlacement,
|
|
2595
|
+
true,
|
|
2596
|
+
units,
|
|
2597
|
+
sameLayerNeighbors(isForward),
|
|
2598
|
+
adjacent.visits,
|
|
2599
|
+
forcedCompare,
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
groupMixedPortDirections(current, next, working[layer - 1]);
|
|
2603
|
+
if (exactPortSweep) distributePorts(layer, false);
|
|
2604
|
+
visitLayer?.(layer, [...current], isForward, firstSweep);
|
|
2605
|
+
}
|
|
2068
2606
|
}
|
|
2069
2607
|
}
|
|
2608
|
+
};
|
|
2070
2609
|
|
|
2610
|
+
const attempts = statistic === "median" ? 1 : thoroughness;
|
|
2611
|
+
const finish = (order: LayerOrder): LayerOrder => {
|
|
2612
|
+
const bestLayers = order.layers.map((layer) => [...layer]);
|
|
2613
|
+
const bestInputPortOrder = clonePortOrders(order.inputPortOrderByNodeId);
|
|
2614
|
+
const bestOutputPortOrder = clonePortOrders(order.outputPortOrderByNodeId);
|
|
2071
2615
|
if (statistic === "median" && input.direction === "left") {
|
|
2072
2616
|
const edgeOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
2073
2617
|
for (const layer of bestLayers) {
|
|
@@ -2117,41 +2661,100 @@ function minimizeCrossingsWithLayerSweep(
|
|
|
2117
2661
|
phaseRandomByInput.set(input, random);
|
|
2118
2662
|
return exactPortSweep
|
|
2119
2663
|
? {
|
|
2664
|
+
physicalPortOrderByNodeId: clonePortOrders(order.physicalPortOrderByNodeId),
|
|
2120
2665
|
layers: bestLayers,
|
|
2121
2666
|
inputPortOrderByNodeId: bestInputPortOrder,
|
|
2122
2667
|
outputPortOrderByNodeId: bestOutputPortOrder,
|
|
2123
|
-
}
|
|
2124
|
-
: { layers: bestLayers };
|
|
2125
|
-
};
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
if (visited.has(node.id)) continue;
|
|
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);
|
|
2668
|
+
}
|
|
2669
|
+
: { layers: bestLayers };
|
|
2670
|
+
};
|
|
2671
|
+
const restore = (order: LayerOrder) => {
|
|
2672
|
+
working = order.layers.map((layer) => [...layer]);
|
|
2673
|
+
inputPortOrder.clear();
|
|
2674
|
+
outputPortOrder.clear();
|
|
2675
|
+
for (const [id, edges] of clonePortOrders(order.inputPortOrderByNodeId))
|
|
2676
|
+
inputPortOrder.set(id, edges);
|
|
2677
|
+
for (const [id, edges] of clonePortOrders(order.outputPortOrderByNodeId))
|
|
2678
|
+
outputPortOrder.set(id, edges);
|
|
2679
|
+
canonicalGraph = buildCrossingGraph(orientation, working, inputPortOrder, outputPortOrder);
|
|
2680
|
+
canonicalNodes = new Map(canonicalGraph.layers.flat().map((node) => [node.id, node]));
|
|
2681
|
+
for (const [id, ports] of order.physicalPortOrderByNodeId ?? []) {
|
|
2682
|
+
const node = canonicalNodes.get(id);
|
|
2683
|
+
if (node) node.ports.sort((a, b) => ports.indexOf(a.id) - ports.indexOf(b.id));
|
|
2152
2684
|
}
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2685
|
+
};
|
|
2686
|
+
traceLayeredPhase(() => ({
|
|
2687
|
+
kind: "initial-order",
|
|
2688
|
+
scope: input.graph.id,
|
|
2689
|
+
layers: working.map((layer) => [...layer]),
|
|
2690
|
+
}));
|
|
2691
|
+
const session: LayerSweepSession = {
|
|
2692
|
+
scope: input.graph.id,
|
|
2693
|
+
get random() {
|
|
2694
|
+
return random;
|
|
2695
|
+
},
|
|
2696
|
+
attempts,
|
|
2697
|
+
usesInitialModelOrder,
|
|
2698
|
+
finishInitialOrderAttempt: () => {
|
|
2699
|
+
initialOrderAttempt = false;
|
|
2700
|
+
},
|
|
2701
|
+
restoreRejectedSweep: !exactPortSweep,
|
|
2702
|
+
lockPortOrder: (id) => {
|
|
2703
|
+
fixedPortOrders.add(id);
|
|
2704
|
+
},
|
|
2705
|
+
markHierarchicalNode: (id) => {
|
|
2706
|
+
hierarchicalNodes.add(id);
|
|
2707
|
+
},
|
|
2708
|
+
useRandom: (source) => {
|
|
2709
|
+
random = source;
|
|
2710
|
+
},
|
|
2711
|
+
minimize: () => {
|
|
2712
|
+
const root = { session, childrenByNodeId: new Map(), useBottomUp: true };
|
|
2713
|
+
return minimizeHierarchyCrossings(root).get(root)!;
|
|
2714
|
+
},
|
|
2715
|
+
shuffleFirstLayer,
|
|
2716
|
+
sweep,
|
|
2717
|
+
countCrossings: () => countCrossings(working),
|
|
2718
|
+
snapshot: () => ({
|
|
2719
|
+
physicalPortOrderByNodeId: new Map(
|
|
2720
|
+
[...canonicalNodes].map(([id, node]) => [id, node.ports.map((port) => port.id)]),
|
|
2721
|
+
),
|
|
2722
|
+
layers: working.map((layer) => [...layer]),
|
|
2723
|
+
inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
|
|
2724
|
+
outputPortOrderByNodeId: clonePortOrders(outputPortOrder),
|
|
2725
|
+
}),
|
|
2726
|
+
restore,
|
|
2727
|
+
finish,
|
|
2728
|
+
currentLayers: () => working,
|
|
2729
|
+
crossingGraph: () => ({
|
|
2730
|
+
layers: working.map((layer) => layer.map((id) => canonicalNodes.get(id)!)),
|
|
2731
|
+
edges: canonicalGraph.edges,
|
|
2732
|
+
}),
|
|
2733
|
+
physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
|
|
2734
|
+
edgeOrders: (id) => ({
|
|
2735
|
+
incoming: inputPortOrder.get(id) ?? [],
|
|
2736
|
+
outgoing: outputPortOrder.get(id) ?? [],
|
|
2737
|
+
}),
|
|
2738
|
+
// A full restore would rebuild an equivalent canonical graph: ports keep
|
|
2739
|
+
// their recorded physical order and layers are re-read before distribution.
|
|
2740
|
+
replaceLayer: (index, ids) => {
|
|
2741
|
+
working[index] = [...ids];
|
|
2742
|
+
},
|
|
2743
|
+
reorderNodePorts: (id, physical, incoming, outgoing) => {
|
|
2744
|
+
inputPortOrder.set(id, [...incoming]);
|
|
2745
|
+
outputPortOrder.set(id, [...outgoing]);
|
|
2746
|
+
const node = canonicalNodes.get(id);
|
|
2747
|
+
if (!node) return;
|
|
2748
|
+
const slot = new Map(physical.map((port, index) => [port, index]));
|
|
2749
|
+
node.ports.sort((a, b) => (slot.get(a.id) ?? -1) - (slot.get(b.id) ?? -1));
|
|
2750
|
+
},
|
|
2751
|
+
};
|
|
2752
|
+
return session;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
/** NetworkSimplexLayerer processes the largest undirected component first. */
|
|
2756
|
+
function networkSimplexComponentOrder(input: LayeredPhaseInput): string[] {
|
|
2757
|
+
return networkSimplexComponents(input).flat();
|
|
2155
2758
|
}
|
|
2156
2759
|
|
|
2157
2760
|
/** Keep model order within each component while placing connected work before isolated nodes. */
|
|
@@ -2193,7 +2796,11 @@ export function minimizeCrossingsWithMedian(sweeps = 7): CrossingMinimizer {
|
|
|
2193
2796
|
}
|
|
2194
2797
|
|
|
2195
2798
|
function layersFromAssignment(input: LayeredPhaseInput, assignment: LayerAssignment): string[][] {
|
|
2196
|
-
const maximumLayer = Math.max(
|
|
2799
|
+
const maximumLayer = Math.max(
|
|
2800
|
+
0,
|
|
2801
|
+
(assignment.layerCount ?? 1) - 1,
|
|
2802
|
+
...assignment.layerByNodeId.values(),
|
|
2803
|
+
);
|
|
2197
2804
|
const layers = Array.from({ length: maximumLayer + 1 }, () => [] as string[]);
|
|
2198
2805
|
for (const node of input.graph.nodes) {
|
|
2199
2806
|
layers[assignment.layerByNodeId.get(node.id) ?? 0]?.push(node.id);
|
|
@@ -2215,7 +2822,11 @@ export const minimizeCrossingsWithModelOrder: CrossingMinimizer = (
|
|
|
2215
2822
|
_orientation,
|
|
2216
2823
|
assignment,
|
|
2217
2824
|
) => {
|
|
2218
|
-
const maximumLayer = Math.max(
|
|
2825
|
+
const maximumLayer = Math.max(
|
|
2826
|
+
0,
|
|
2827
|
+
(assignment.layerCount ?? 1) - 1,
|
|
2828
|
+
...assignment.layerByNodeId.values(),
|
|
2829
|
+
);
|
|
2219
2830
|
const layers = Array.from({ length: maximumLayer + 1 }, () => [] as string[]);
|
|
2220
2831
|
const layeringStrategy = input.settings["layering.strategy"] ?? "NETWORK_SIMPLEX";
|
|
2221
2832
|
const nodeOrder = assignment.seedOrder
|
|
@@ -2410,6 +3021,19 @@ export function applyPostCompaction(
|
|
|
2410
3021
|
const constraintStrategy = input.settings["compaction.postCompaction.constraints"] ?? "SCANLINE";
|
|
2411
3022
|
void constraintStrategy;
|
|
2412
3023
|
if (strategy === "NONE") return placement;
|
|
3024
|
+
if (
|
|
3025
|
+
strategy === "EDGE_LENGTH" ||
|
|
3026
|
+
((input.settings.edgeRouting ?? "ORTHOGONAL") === "ORTHOGONAL" &&
|
|
3027
|
+
["LEFT", "RIGHT", "LEFT_RIGHT_CONSTRAINT_LOCKING", "LEFT_RIGHT_CONNECTION_LOCKING"].includes(
|
|
3028
|
+
String(strategy),
|
|
3029
|
+
))
|
|
3030
|
+
)
|
|
3031
|
+
return applyGroupedEdgeLengthCompaction(
|
|
3032
|
+
input,
|
|
3033
|
+
placement,
|
|
3034
|
+
routes,
|
|
3035
|
+
getPlacementOrientation(input),
|
|
3036
|
+
);
|
|
2413
3037
|
const rects = placement.rectByNodeId as Map<string, EntityRect>;
|
|
2414
3038
|
const originalEndpointsByEdgeId = new Map(
|
|
2415
3039
|
[...(routes?.pointsByEdgeId ?? [])].flatMap(([edgeId, points]) => {
|
|
@@ -2485,8 +3109,7 @@ export function applyPostCompaction(
|
|
|
2485
3109
|
// Compaction must preserve the space reserved for an inline center label,
|
|
2486
3110
|
// even when its endpoint rectangles do not intersect on the cross axis.
|
|
2487
3111
|
const inlineLabelSpaceByPair = new Map<string, number>();
|
|
2488
|
-
|
|
2489
|
-
if (strategy !== "EDGE_LENGTH" && (input.direction === "left" || input.direction === "right")) {
|
|
3112
|
+
if (input.direction === "left" || input.direction === "right") {
|
|
2490
3113
|
for (const edge of labelEdges) {
|
|
2491
3114
|
const settings = input.edgeSettings?.(edge);
|
|
2492
3115
|
if (
|
|
@@ -2508,7 +3131,16 @@ export function applyPostCompaction(
|
|
|
2508
3131
|
: 0;
|
|
2509
3132
|
const horizontalSpacing = (left: ConstraintNode, right: ConstraintNode): number =>
|
|
2510
3133
|
left.kind === "node" && right.kind === "node"
|
|
2511
|
-
? Math.max(
|
|
3134
|
+
? Math.max(
|
|
3135
|
+
input.spacing.node,
|
|
3136
|
+
(left.nodeId?.startsWith("__layout_dummy:label:") &&
|
|
3137
|
+
!right.nodeId?.startsWith("__layout_dummy:")) ||
|
|
3138
|
+
(right.nodeId?.startsWith("__layout_dummy:label:") &&
|
|
3139
|
+
!left.nodeId?.startsWith("__layout_dummy:"))
|
|
3140
|
+
? input.spacing.layer
|
|
3141
|
+
: 0,
|
|
3142
|
+
labelSpace(left, right),
|
|
3143
|
+
)
|
|
2512
3144
|
: left.kind === "segment" && right.kind === "segment" && left.edgeId === right.edgeId
|
|
2513
3145
|
? 0
|
|
2514
3146
|
: Number(
|
|
@@ -2619,90 +3251,8 @@ export function applyPostCompaction(
|
|
|
2619
3251
|
);
|
|
2620
3252
|
} else {
|
|
2621
3253
|
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
3254
|
}
|
|
3255
|
+
|
|
2706
3256
|
if (input.direction === "right" && centerLabeledEdgeIds.size > 0) {
|
|
2707
3257
|
const itemByNodeId = new Map(
|
|
2708
3258
|
compactables.flatMap((item) =>
|
|
@@ -2715,7 +3265,7 @@ export function applyPostCompaction(
|
|
|
2715
3265
|
// A late target nudge would invalidate its outgoing label constraints.
|
|
2716
3266
|
if (
|
|
2717
3267
|
!centerLabeledEdgeIds.has(edge.id) ||
|
|
2718
|
-
|
|
3268
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
2719
3269
|
)
|
|
2720
3270
|
continue;
|
|
2721
3271
|
const source = itemByNodeId.get(edge.sourceId);
|
|
@@ -2870,6 +3420,7 @@ function nodeFlowOffset(
|
|
|
2870
3420
|
layerFlowSize: number,
|
|
2871
3421
|
nodeFlowSize: number,
|
|
2872
3422
|
layerByNodeId: ReadonlyMap<string, number>,
|
|
3423
|
+
margins?: { leading: number; trailing: number; maxLeading: number; maxTrailing: number },
|
|
2873
3424
|
): number {
|
|
2874
3425
|
const node = input.graph.nodes.find((candidate) => candidate.id === id);
|
|
2875
3426
|
const alignment = node
|
|
@@ -2880,33 +3431,87 @@ function nodeFlowOffset(
|
|
|
2880
3431
|
else if (alignment === "RIGHT" || alignment === "BOTTOM") ratio = 1;
|
|
2881
3432
|
else if (alignment === "CENTER") ratio = 0.5;
|
|
2882
3433
|
else {
|
|
2883
|
-
|
|
2884
|
-
|
|
3434
|
+
const incoming = new Set<string>();
|
|
3435
|
+
const outgoing = new Set<string>();
|
|
2885
3436
|
const ownLayer = layerByNodeId.get(id) ?? 0;
|
|
3437
|
+
const orientation = getPlacementOrientation(input);
|
|
2886
3438
|
for (const edge of input.graph.edges) {
|
|
2887
3439
|
if (edge.sourceId !== id && edge.targetId !== id) continue;
|
|
2888
|
-
|
|
3440
|
+
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3441
|
+
const source = edge.sourceId === id;
|
|
3442
|
+
const portName = source ? edge.sourcePort : edge.targetPort;
|
|
3443
|
+
const key = portName === undefined ? `edge:${edge.id}` : `port:${portName}`;
|
|
3444
|
+
const otherId = source ? edge.targetId : edge.sourceId;
|
|
2889
3445
|
const otherLayer = layerByNodeId.get(otherId) ?? ownLayer;
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3446
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3447
|
+
const isOutgoing = orientation
|
|
3448
|
+
? source !== orientation.reversedEdgeIds.has(edge.id)
|
|
3449
|
+
: otherLayer !== ownLayer
|
|
3450
|
+
? otherLayer > ownLayer
|
|
3451
|
+
: id.startsWith("__layout_dummy:") && port?.direction !== "inout"
|
|
3452
|
+
? port?.direction === "out"
|
|
3453
|
+
: source;
|
|
3454
|
+
(isOutgoing ? outgoing : incoming).add(key);
|
|
2894
3455
|
}
|
|
2895
|
-
ratio =
|
|
3456
|
+
ratio =
|
|
3457
|
+
incoming.size + outgoing.size === 0 ? 0.5 : outgoing.size / (incoming.size + outgoing.size);
|
|
3458
|
+
}
|
|
3459
|
+
let position = (layerFlowSize - nodeFlowSize) * ratio;
|
|
3460
|
+
if (margins) {
|
|
3461
|
+
if (ratio > 0.5) position -= margins.maxTrailing * 2 * (ratio - 0.5);
|
|
3462
|
+
else if (ratio < 0.5) position += margins.maxLeading * 2 * (0.5 - ratio);
|
|
3463
|
+
position = Math.max(position, margins.leading);
|
|
3464
|
+
position = Math.min(position, layerFlowSize - margins.trailing - nodeFlowSize);
|
|
2896
3465
|
}
|
|
2897
|
-
return
|
|
3466
|
+
return position;
|
|
2898
3467
|
}
|
|
2899
3468
|
|
|
2900
3469
|
export const placeNodesInLayers: NodePlacer = (input, order) => {
|
|
2901
3470
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
3471
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
3472
|
+
const orientation = getPlacementOrientation(input);
|
|
3473
|
+
const loopMargins = preparedLoopEnvelopes(input);
|
|
3474
|
+
const flowMargins = new Map(
|
|
3475
|
+
input.graph.nodes.map((node) => {
|
|
3476
|
+
const size = input.sizes.get(node.id) ?? { width: 0, height: 0 };
|
|
3477
|
+
const ports = placePorts(
|
|
3478
|
+
node.ports,
|
|
3479
|
+
{ x: 0, y: 0, ...size },
|
|
3480
|
+
input.direction,
|
|
3481
|
+
(port) => input.portSettings?.(port, node),
|
|
3482
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
3483
|
+
orientation
|
|
3484
|
+
? (port) => getOrientedPortDirection(input, orientation, node, port)
|
|
3485
|
+
: undefined,
|
|
3486
|
+
);
|
|
3487
|
+
let low = 0,
|
|
3488
|
+
high = 0;
|
|
3489
|
+
for (const port of ports ?? []) {
|
|
3490
|
+
const start = horizontal ? (port.x ?? 0) : (port.y ?? 0);
|
|
3491
|
+
const end = start + (horizontal ? (port.width ?? 0) : (port.height ?? 0));
|
|
3492
|
+
low = Math.max(low, -start);
|
|
3493
|
+
high = Math.max(high, end - (horizontal ? size.width : size.height));
|
|
3494
|
+
}
|
|
3495
|
+
return [
|
|
3496
|
+
node.id,
|
|
3497
|
+
{
|
|
3498
|
+
leading: Math.max(reverse ? high : low, loopMargins?.get(node.id)?.flowBefore ?? 0),
|
|
3499
|
+
trailing: Math.max(reverse ? low : high, loopMargins?.get(node.id)?.flowAfter ?? 0),
|
|
3500
|
+
},
|
|
3501
|
+
] as const;
|
|
3502
|
+
}),
|
|
3503
|
+
);
|
|
2902
3504
|
const layerByNodeId = new Map(
|
|
2903
3505
|
order.layers.flatMap((layer, layerIndex) => layer.map((id) => [id, layerIndex] as const)),
|
|
2904
3506
|
);
|
|
2905
3507
|
const layerFlowSizes = order.layers.map((layer) => {
|
|
2906
3508
|
const size = Math.max(
|
|
2907
3509
|
0,
|
|
2908
|
-
...layer.map(
|
|
2909
|
-
|
|
3510
|
+
...layer.map(
|
|
3511
|
+
(id) =>
|
|
3512
|
+
(horizontal ? (input.sizes.get(id)?.width ?? 0) : (input.sizes.get(id)?.height ?? 0)) +
|
|
3513
|
+
(flowMargins.get(id)?.leading ?? 0) +
|
|
3514
|
+
(flowMargins.get(id)?.trailing ?? 0),
|
|
2910
3515
|
),
|
|
2911
3516
|
);
|
|
2912
3517
|
return size === 0 &&
|
|
@@ -2941,6 +3546,12 @@ export const placeNodesInLayers: NodePlacer = (input, order) => {
|
|
|
2941
3546
|
layerFlowSizes[layerIndex] ?? 0,
|
|
2942
3547
|
horizontal ? size.width : size.height,
|
|
2943
3548
|
layerByNodeId,
|
|
3549
|
+
{
|
|
3550
|
+
leading: flowMargins.get(id)?.leading ?? 0,
|
|
3551
|
+
trailing: flowMargins.get(id)?.trailing ?? 0,
|
|
3552
|
+
maxLeading: Math.max(0, ...layer.map((id) => flowMargins.get(id)?.leading ?? 0)),
|
|
3553
|
+
maxTrailing: Math.max(0, ...layer.map((id) => flowMargins.get(id)?.trailing ?? 0)),
|
|
3554
|
+
},
|
|
2944
3555
|
);
|
|
2945
3556
|
const rect = horizontal
|
|
2946
3557
|
? { x: centeredFlow, y: cross, ...size }
|
|
@@ -3073,22 +3684,42 @@ export const placeNodesInteractively: NodePlacer = (input, order) => {
|
|
|
3073
3684
|
return { rectByNodeId };
|
|
3074
3685
|
};
|
|
3075
3686
|
|
|
3076
|
-
function getPortPoint(
|
|
3687
|
+
export function getPortPoint(
|
|
3077
3688
|
node: GraphNode,
|
|
3078
3689
|
portName: string | undefined,
|
|
3079
3690
|
rect: EntityRect,
|
|
3080
3691
|
fallback: Point,
|
|
3081
3692
|
direction: LayeredPhaseInput["direction"],
|
|
3082
3693
|
input: LayeredPhaseInput,
|
|
3694
|
+
/** Reuses each node's placed ports across calls while settings stay fixed. */
|
|
3695
|
+
placedPorts?: Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>,
|
|
3083
3696
|
): Point {
|
|
3084
3697
|
if (portName === undefined) return fallback;
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3698
|
+
const cached = placedPorts?.get(node);
|
|
3699
|
+
let ports: GraphPort[] | undefined;
|
|
3700
|
+
if (
|
|
3701
|
+
cached &&
|
|
3702
|
+
cached.rect.x === rect.x &&
|
|
3703
|
+
cached.rect.y === rect.y &&
|
|
3704
|
+
cached.rect.width === rect.width &&
|
|
3705
|
+
cached.rect.height === rect.height
|
|
3706
|
+
)
|
|
3707
|
+
ports = cached.ports;
|
|
3708
|
+
else {
|
|
3709
|
+
const orientation = getPlacementOrientation(input);
|
|
3710
|
+
ports = placePorts(
|
|
3711
|
+
node.ports,
|
|
3712
|
+
rect,
|
|
3713
|
+
direction,
|
|
3714
|
+
(candidate) => input.portSettings?.(candidate, node),
|
|
3715
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
3716
|
+
orientation
|
|
3717
|
+
? (candidate) => getOrientedPortDirection(input, orientation, node, candidate)
|
|
3718
|
+
: undefined,
|
|
3719
|
+
);
|
|
3720
|
+
placedPorts?.set(node, { rect: { ...rect }, ports });
|
|
3721
|
+
}
|
|
3722
|
+
const port = ports?.find((candidate) => candidate.name === portName);
|
|
3092
3723
|
if (port?.x === undefined || port.y === undefined) return fallback;
|
|
3093
3724
|
const settings = input.portSettings?.(port, node);
|
|
3094
3725
|
const configuredAnchor = settings?.["port.anchor"] as { x?: number; y?: number } | undefined;
|
|
@@ -3102,7 +3733,31 @@ function getPortPoint(
|
|
|
3102
3733
|
};
|
|
3103
3734
|
}
|
|
3104
3735
|
|
|
3105
|
-
|
|
3736
|
+
/**
|
|
3737
|
+
* Drop interior bends where an orthogonal route doubles back along one line,
|
|
3738
|
+
* so it never retraces itself. Terminal segments keep their port direction.
|
|
3739
|
+
*/
|
|
3740
|
+
export function removeRouteSpurs(points: readonly Point[]): readonly Point[] {
|
|
3741
|
+
const equal = (left: number, right: number) => Math.abs(left - right) < 1e-9;
|
|
3742
|
+
const route = points.filter(
|
|
3743
|
+
(point, index) =>
|
|
3744
|
+
index === 0 || !equal(point.x, points[index - 1]!.x) || !equal(point.y, points[index - 1]!.y),
|
|
3745
|
+
);
|
|
3746
|
+
let changed = false;
|
|
3747
|
+
for (let index = 2; index < route.length - 2; index++) {
|
|
3748
|
+
const [previous, middle, next] = [route[index - 1]!, route[index]!, route[index + 1]!];
|
|
3749
|
+
const sameX = equal(previous.x, middle.x) && equal(middle.x, next.x),
|
|
3750
|
+
sameY = equal(previous.y, middle.y) && equal(middle.y, next.y);
|
|
3751
|
+
const axis = sameX ? "y" : sameY ? "x" : undefined;
|
|
3752
|
+
if (!axis || (middle[axis] - previous[axis]) * (next[axis] - middle[axis]) >= 0) continue;
|
|
3753
|
+
route.splice(index, 1);
|
|
3754
|
+
changed = true;
|
|
3755
|
+
index = Math.max(1, index - 2);
|
|
3756
|
+
}
|
|
3757
|
+
return changed ? route : points;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
export function simplifyRoute(points: readonly Point[]): Point[] {
|
|
3106
3761
|
const equal = (left: number, right: number) => Math.abs(left - right) < 1e-9;
|
|
3107
3762
|
const simplified: Point[] = [];
|
|
3108
3763
|
for (const point of points) {
|
|
@@ -3135,6 +3790,7 @@ function implicitEdgeEndpoints(
|
|
|
3135
3790
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
3136
3791
|
const groups = new Map<string, Array<{ edge: GraphEdge; endpoint: "source" | "target" }>>();
|
|
3137
3792
|
for (const edge of input.graph.edges) {
|
|
3793
|
+
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3138
3794
|
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
3139
3795
|
const targetRect = placement.rectByNodeId.get(edge.targetId);
|
|
3140
3796
|
if (!sourceRect || !targetRect) continue;
|
|
@@ -3142,7 +3798,16 @@ function implicitEdgeEndpoints(
|
|
|
3142
3798
|
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
3143
3799
|
const modelOrderPromotion =
|
|
3144
3800
|
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3145
|
-
const
|
|
3801
|
+
const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
|
|
3802
|
+
const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
|
|
3803
|
+
// A zero-gap boundary can coincide with the node's flow origin.
|
|
3804
|
+
const forward =
|
|
3805
|
+
modelOrderPromotion ||
|
|
3806
|
+
(sourceFlow === targetFlow && (sourceBoundary || targetBoundary)
|
|
3807
|
+
? sourceBoundary
|
|
3808
|
+
? sourceBoundary.port.side === "EAST" || sourceBoundary.port.side === "SOUTH"
|
|
3809
|
+
: targetBoundary!.port.side === "WEST" || targetBoundary!.port.side === "NORTH"
|
|
3810
|
+
: sourceFlow <= targetFlow);
|
|
3146
3811
|
const feedback =
|
|
3147
3812
|
input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
|
|
3148
3813
|
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
@@ -3167,11 +3832,25 @@ function implicitEdgeEndpoints(
|
|
|
3167
3832
|
? "after"
|
|
3168
3833
|
: undefined;
|
|
3169
3834
|
};
|
|
3835
|
+
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3836
|
+
const node = nodeById.get(nodeId);
|
|
3837
|
+
const constraints = node && input.nodeSettings?.(node)?.portConstraints;
|
|
3838
|
+
if (
|
|
3839
|
+
portName !== undefined ||
|
|
3840
|
+
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3841
|
+
)
|
|
3842
|
+
return undefined;
|
|
3843
|
+
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3844
|
+
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3845
|
+
return source !== directionReversed ? "after" : "before";
|
|
3846
|
+
};
|
|
3170
3847
|
const sourceSide =
|
|
3171
3848
|
endpointSide(edge.sourceId, edge.sourcePort) ??
|
|
3849
|
+
fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ??
|
|
3172
3850
|
(feedback ? (directionReversed ? "before" : "after") : forward ? "after" : "before");
|
|
3173
3851
|
const targetSide =
|
|
3174
3852
|
endpointSide(edge.targetId, edge.targetPort) ??
|
|
3853
|
+
fixedImplicitSide(edge.targetId, edge.targetPort, false) ??
|
|
3175
3854
|
(feedback ? (directionReversed ? "after" : "before") : forward ? "before" : "after");
|
|
3176
3855
|
const sourceKey = `${edge.sourceId}:${sourceSide}`;
|
|
3177
3856
|
const targetKey = `${edge.targetId}:${targetSide}`;
|
|
@@ -3185,6 +3864,32 @@ function implicitEdgeEndpoints(
|
|
|
3185
3864
|
|
|
3186
3865
|
const result = new Map<string, { source: Point; target: Point }>();
|
|
3187
3866
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
3867
|
+
// First positions in swept port orders, as `indexOf` finds them.
|
|
3868
|
+
const orderIndexes = new Map<readonly string[], Map<string, number>>();
|
|
3869
|
+
const orderIndex = (list: readonly string[], id: string) => {
|
|
3870
|
+
let positions = orderIndexes.get(list);
|
|
3871
|
+
if (!positions) {
|
|
3872
|
+
positions = new Map();
|
|
3873
|
+
for (const [index, item] of list.entries())
|
|
3874
|
+
if (!positions.has(item)) positions.set(item, index);
|
|
3875
|
+
orderIndexes.set(list, positions);
|
|
3876
|
+
}
|
|
3877
|
+
return positions.get(id) ?? -1;
|
|
3878
|
+
};
|
|
3879
|
+
const placedPorts = new Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>();
|
|
3880
|
+
const portSideCounts = new Map<GraphNode, Map<unknown, number>>();
|
|
3881
|
+
const portSideCount = (node: GraphNode, side: unknown): number => {
|
|
3882
|
+
let counts = portSideCounts.get(node);
|
|
3883
|
+
if (!counts) {
|
|
3884
|
+
counts = new Map();
|
|
3885
|
+
for (const port of node.ports ?? []) {
|
|
3886
|
+
const portSide = input.portSettings?.(port, node)?.["port.side"];
|
|
3887
|
+
counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
|
|
3888
|
+
}
|
|
3889
|
+
portSideCounts.set(node, counts);
|
|
3890
|
+
}
|
|
3891
|
+
return counts.get(side) ?? 0;
|
|
3892
|
+
};
|
|
3188
3893
|
for (const [key, entries] of groups) {
|
|
3189
3894
|
const split = key.lastIndexOf(":");
|
|
3190
3895
|
const nodeId = key.slice(0, split);
|
|
@@ -3235,9 +3940,23 @@ function implicitEdgeEndpoints(
|
|
|
3235
3940
|
unzipping ||
|
|
3236
3941
|
interactiveTargetOrder ||
|
|
3237
3942
|
verticalNoneTargetOrder ||
|
|
3238
|
-
|
|
3239
|
-
|
|
3943
|
+
crossingStrategy === "LAYER_SWEEP" ||
|
|
3944
|
+
crossingStrategy === "MEDIAN_LAYER_SWEEP";
|
|
3945
|
+
const sweptOrder = getPlacementOrder(input);
|
|
3240
3946
|
entries.sort((left, right) => {
|
|
3947
|
+
const leftIncoming =
|
|
3948
|
+
(left.endpoint === "target") !== (orientation?.reversedEdgeIds.has(left.edge.id) ?? false);
|
|
3949
|
+
const rightIncoming =
|
|
3950
|
+
(right.endpoint === "target") !==
|
|
3951
|
+
(orientation?.reversedEdgeIds.has(right.edge.id) ?? false);
|
|
3952
|
+
if (leftIncoming === rightIncoming) {
|
|
3953
|
+
const selected = (
|
|
3954
|
+
leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId
|
|
3955
|
+
)?.get(nodeId);
|
|
3956
|
+
const a = selected ? orderIndex(selected, left.edge.id) : -1,
|
|
3957
|
+
b = selected ? orderIndex(selected, right.edge.id) : -1;
|
|
3958
|
+
if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
|
|
3959
|
+
}
|
|
3241
3960
|
if (interactiveLeftSourceOrder) {
|
|
3242
3961
|
const leftExtreme = interactiveLeftSourceOrder[0] === left.edge.id;
|
|
3243
3962
|
const rightExtreme = interactiveLeftSourceOrder[0] === right.edge.id;
|
|
@@ -3377,6 +4096,32 @@ function implicitEdgeEndpoints(
|
|
|
3377
4096
|
? { ...point, y: Math.round(point.y) }
|
|
3378
4097
|
: { ...point, x: Math.round(point.x) };
|
|
3379
4098
|
}
|
|
4099
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
4100
|
+
const explicitPort = node?.ports?.find((port) => port.name === portName);
|
|
4101
|
+
const physicalSide =
|
|
4102
|
+
node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.side"] : undefined;
|
|
4103
|
+
const singleFixedSidePort =
|
|
4104
|
+
constraints === "FIXED_SIDE" &&
|
|
4105
|
+
physicalSide !== undefined &&
|
|
4106
|
+
node?.ports !== undefined &&
|
|
4107
|
+
portSideCount(node, physicalSide) === 1;
|
|
4108
|
+
// Movable port ordering comes from the layer sweep above. Fixed coordinates
|
|
4109
|
+
// and authored anchors must replace the synthesized node-origin fallback.
|
|
4110
|
+
const authoredAnchor =
|
|
4111
|
+
node && explicitPort
|
|
4112
|
+
? input.portSettings?.(explicitPort, node)?.["port.anchor"]
|
|
4113
|
+
: undefined;
|
|
4114
|
+
if (
|
|
4115
|
+
portName !== undefined &&
|
|
4116
|
+
node &&
|
|
4117
|
+
(constraints === "FIXED_POS" ||
|
|
4118
|
+
constraints === "FIXED_RATIO" ||
|
|
4119
|
+
constraints === "FIXED_ORDER" ||
|
|
4120
|
+
singleFixedSidePort ||
|
|
4121
|
+
authoredAnchor !== undefined)
|
|
4122
|
+
) {
|
|
4123
|
+
point = getPortPoint(node, portName, rect, point, input.direction, input, placedPorts);
|
|
4124
|
+
}
|
|
3380
4125
|
const pair = result.get(edge.id) ?? { source: point, target: point };
|
|
3381
4126
|
pair[endpoint] = point;
|
|
3382
4127
|
result.set(edge.id, pair);
|
|
@@ -3400,6 +4145,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3400
4145
|
);
|
|
3401
4146
|
};
|
|
3402
4147
|
const pointsByEdgeId = new Map<string, readonly Point[]>();
|
|
4148
|
+
const junctionGroups: OrthogonalJunctionGroup[] = [];
|
|
4149
|
+
const physicalOrder = new Map(
|
|
4150
|
+
[
|
|
4151
|
+
...input.graph.edges
|
|
4152
|
+
.filter((edge) => !orientation.reversedEdgeIds.has(edge.id))
|
|
4153
|
+
.map((edge) => edge.id),
|
|
4154
|
+
...orientation.reversedEdgeIds,
|
|
4155
|
+
].map((id, index) => [id, index]),
|
|
4156
|
+
);
|
|
4157
|
+
|
|
3403
4158
|
const splineNubControlsByEdgeId = new Map<string, readonly Point[]>();
|
|
3404
4159
|
const outsideFeedbackEdgeIds = new Set<string>();
|
|
3405
4160
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
@@ -3417,6 +4172,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3417
4172
|
(mutableRects.get(leftId)?.x ?? 0) - (mutableRects.get(rightId)?.x ?? 0),
|
|
3418
4173
|
);
|
|
3419
4174
|
for (const [id, loops] of selfLoopEntriesByX) {
|
|
4175
|
+
if (hasPlacementLoopEnvelope(placement, id)) continue;
|
|
3420
4176
|
const rect = mutableRects.get(id);
|
|
3421
4177
|
const node = nodeById.get(id);
|
|
3422
4178
|
if (!rect || !node) continue;
|
|
@@ -3479,14 +4235,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3479
4235
|
edge: GraphEdge,
|
|
3480
4236
|
side: string,
|
|
3481
4237
|
ordering: string,
|
|
4238
|
+
canonical = false,
|
|
3482
4239
|
): number => {
|
|
3483
|
-
const
|
|
4240
|
+
const crossSide = side === "NORTH" || side === "SOUTH";
|
|
4241
|
+
const axis = crossSide === (!canonical || horizontal) ? "height" : "width";
|
|
3484
4242
|
const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
|
|
3485
4243
|
const index = ordered.indexOf(edge);
|
|
3486
4244
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
3487
4245
|
const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
3488
4246
|
const size = (candidate: GraphEdge) =>
|
|
3489
|
-
input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true
|
|
4247
|
+
canonical || input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true
|
|
3490
4248
|
? (candidate[axis] ?? 0)
|
|
3491
4249
|
: 0;
|
|
3492
4250
|
return (
|
|
@@ -3498,11 +4256,19 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3498
4256
|
extent + (size(candidate) > 0 ? size(candidate) + labelSpacing : 0),
|
|
3499
4257
|
0,
|
|
3500
4258
|
) +
|
|
3501
|
-
(size(edge) > 0
|
|
4259
|
+
(size(edge) > 0 && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
4260
|
+
? size(edge) / 2 + labelSpacing
|
|
4261
|
+
: 0)
|
|
3502
4262
|
);
|
|
3503
4263
|
};
|
|
4264
|
+
const hasFixedLoopPorts = (node: GraphNode): boolean => {
|
|
4265
|
+
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4266
|
+
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4267
|
+
};
|
|
3504
4268
|
const northReserveByLayer = new Map<number, number>();
|
|
3505
|
-
for (const [id,
|
|
4269
|
+
for (const [id, nodeLoops] of selfLoopsByNodeId) {
|
|
4270
|
+
const loops = nodeLoops.filter(() => !hasPlacementLoopEnvelope(placement, id));
|
|
4271
|
+
if (loops.length === 0) continue;
|
|
3506
4272
|
const rect = mutableRects.get(id);
|
|
3507
4273
|
if (!rect) continue;
|
|
3508
4274
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
@@ -3518,8 +4284,36 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3518
4284
|
y: rect.y + spacing + splineOffset,
|
|
3519
4285
|
});
|
|
3520
4286
|
} else {
|
|
3521
|
-
const
|
|
3522
|
-
|
|
4287
|
+
const northLoops = loops.filter((edge, index) => {
|
|
4288
|
+
if (
|
|
4289
|
+
style === "ORTHOGONAL" &&
|
|
4290
|
+
node &&
|
|
4291
|
+
edge.sourcePort !== undefined &&
|
|
4292
|
+
edge.targetPort !== undefined &&
|
|
4293
|
+
hasFixedLoopPorts(node)
|
|
4294
|
+
) {
|
|
4295
|
+
const sourcePort = node.ports?.find((port) => port.name === edge.sourcePort);
|
|
4296
|
+
const targetPort = node.ports?.find((port) => port.name === edge.targetPort);
|
|
4297
|
+
return (
|
|
4298
|
+
(sourcePort && input.portSettings?.(sourcePort, node)?.["port.side"] === "NORTH") ||
|
|
4299
|
+
(targetPort && input.portSettings?.(targetPort, node)?.["port.side"] === "NORTH")
|
|
4300
|
+
);
|
|
4301
|
+
}
|
|
4302
|
+
if (
|
|
4303
|
+
style === "ORTHOGONAL" &&
|
|
4304
|
+
node &&
|
|
4305
|
+
(edge.sourcePort === undefined || edge.targetPort === undefined) &&
|
|
4306
|
+
String(nodeSettings?.portConstraints ?? "FREE") !== "FREE" &&
|
|
4307
|
+
String(nodeSettings?.portConstraints ?? "UNDEFINED") !== "UNDEFINED"
|
|
4308
|
+
) {
|
|
4309
|
+
const port = node.ports?.find(
|
|
4310
|
+
(candidate) => candidate.name === (edge.sourcePort ?? edge.targetPort),
|
|
4311
|
+
);
|
|
4312
|
+
if (port) return input.portSettings?.(port, node)?.["port.side"] === "NORTH";
|
|
4313
|
+
}
|
|
4314
|
+
return distribution !== "NORTH_SOUTH" || index % 2 === 0;
|
|
4315
|
+
});
|
|
4316
|
+
const sideLoopCount = northLoops.length;
|
|
3523
4317
|
const reserve =
|
|
3524
4318
|
(ordering === "SEQUENCED" ? Math.min(1, sideLoopCount) : sideLoopCount) * spacing +
|
|
3525
4319
|
splineOffset;
|
|
@@ -3644,6 +4438,50 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3644
4438
|
}
|
|
3645
4439
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
3646
4440
|
}
|
|
4441
|
+
const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
|
|
4442
|
+
const labelFlowLayers = new Set(
|
|
4443
|
+
[...flowLayerByNodeId]
|
|
4444
|
+
.filter(([id]) => id.startsWith("__layout_dummy:label:"))
|
|
4445
|
+
.map(([, layer]) => layer),
|
|
4446
|
+
);
|
|
4447
|
+
|
|
4448
|
+
// Routing advances past the occupied layer, including protruding ports.
|
|
4449
|
+
// Assign layers from bodies first so port envelopes cannot merge ranks.
|
|
4450
|
+
for (const [id, rect] of placement.rectByNodeId) {
|
|
4451
|
+
const node = nodeById.get(id);
|
|
4452
|
+
const bounds = flowLayers[flowLayerByNodeId.get(id) ?? -1];
|
|
4453
|
+
if (!node || !bounds) continue;
|
|
4454
|
+
const envelope = hasPlacementLoopEnvelope(placement, id)
|
|
4455
|
+
? preparedLoopEnvelopes(input)?.get(id)
|
|
4456
|
+
: undefined;
|
|
4457
|
+
if (envelope) {
|
|
4458
|
+
const negative = input.direction === "left" || input.direction === "up";
|
|
4459
|
+
const start = horizontal ? rect.x : rect.y;
|
|
4460
|
+
const size = horizontal ? rect.width : rect.height;
|
|
4461
|
+
bounds.start = Math.min(
|
|
4462
|
+
bounds.start,
|
|
4463
|
+
start - (negative ? envelope.flowAfter : envelope.flowBefore),
|
|
4464
|
+
);
|
|
4465
|
+
bounds.end = Math.max(
|
|
4466
|
+
bounds.end,
|
|
4467
|
+
start + size + (negative ? envelope.flowBefore : envelope.flowAfter),
|
|
4468
|
+
);
|
|
4469
|
+
}
|
|
4470
|
+
const ports = placePorts(
|
|
4471
|
+
node.ports,
|
|
4472
|
+
rect,
|
|
4473
|
+
input.direction,
|
|
4474
|
+
(port) => input.portSettings?.(port, node),
|
|
4475
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
4476
|
+
(port) => getOrientedPortDirection(input, orientation, node, port),
|
|
4477
|
+
);
|
|
4478
|
+
for (const port of ports ?? []) {
|
|
4479
|
+
const start = horizontal ? rect.x + (port.x ?? 0) : rect.y + (port.y ?? 0);
|
|
4480
|
+
const end = start + (horizontal ? (port.width ?? 0) : (port.height ?? 0));
|
|
4481
|
+
bounds.start = Math.min(bounds.start, start);
|
|
4482
|
+
bounds.end = Math.max(bounds.end, end);
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
3647
4485
|
|
|
3648
4486
|
const increasing = input.direction === "right" || input.direction === "down";
|
|
3649
4487
|
const flowPortProtrusion = (edge: GraphEdge, endpoint: "source" | "target"): number => {
|
|
@@ -3711,7 +4549,9 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3711
4549
|
const requiredGap =
|
|
3712
4550
|
labelFlowSize +
|
|
3713
4551
|
Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) +
|
|
3714
|
-
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing)
|
|
4552
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing) -
|
|
4553
|
+
flowPortProtrusion(edge, "source") -
|
|
4554
|
+
flowPortProtrusion(edge, "target");
|
|
3715
4555
|
const gap = Math.min(sourceLayer, targetLayer);
|
|
3716
4556
|
const availableGap = flowLayers[gap + 1]!.start - flowLayers[gap]!.end;
|
|
3717
4557
|
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
@@ -3743,26 +4583,84 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3743
4583
|
if (labelShift !== 0) implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
3744
4584
|
|
|
3745
4585
|
const orthogonalTrackByEdgeId = new Map<string, number>();
|
|
4586
|
+
// Cross-axis shift of an edge's attachment at a port carrying both directions.
|
|
4587
|
+
const attachmentShiftByEdgeId = new Map<string, { source?: number; target?: number }>();
|
|
4588
|
+
const attached = (edge: GraphEdge, end: "source" | "target", point: Point): Point => {
|
|
4589
|
+
const shift = attachmentShiftByEdgeId.get(edge.id)?.[end];
|
|
4590
|
+
if (shift === undefined) return point;
|
|
4591
|
+
return horizontal ? { x: point.x, y: point.y + shift } : { x: point.x + shift, y: point.y };
|
|
4592
|
+
};
|
|
4593
|
+
// Tracks given to edges that routing saw as straight, by cross extent.
|
|
4594
|
+
const fallbackTracks: Array<{ track: number; lo: number; hi: number; sign: number }> = [];
|
|
4595
|
+
/**
|
|
4596
|
+
* The gap's middle for an edge routing saw as straight. When its ends
|
|
4597
|
+
* moved apart since, the middle can be the track of an edge heading the
|
|
4598
|
+
* other way: take the nearest track free of those instead.
|
|
4599
|
+
*/
|
|
4600
|
+
const freeGapTrack = (edgeId: string, from: number, to: number, a: Point, b: Point) => {
|
|
4601
|
+
const middle = (from + to) / 2;
|
|
4602
|
+
if (style !== "ORTHOGONAL") return middle;
|
|
4603
|
+
const cross = (p: Point) => (horizontal ? p.y : p.x);
|
|
4604
|
+
const lo = Math.min(cross(a), cross(b)),
|
|
4605
|
+
hi = Math.max(cross(a), cross(b)),
|
|
4606
|
+
sign = Math.sign(cross(b) - cross(a));
|
|
4607
|
+
if (hi - lo < 1e-6) return middle;
|
|
4608
|
+
const spans = [
|
|
4609
|
+
...fallbackTracks,
|
|
4610
|
+
...[...orthogonalTrackByEdgeId].flatMap(([id, track]) => {
|
|
4611
|
+
const ends = implicitEndpoints.get(id);
|
|
4612
|
+
if (id === edgeId || !ends) return [];
|
|
4613
|
+
const values = [cross(ends.source), cross(ends.target)];
|
|
4614
|
+
return [
|
|
4615
|
+
{
|
|
4616
|
+
track,
|
|
4617
|
+
lo: Math.min(...values),
|
|
4618
|
+
hi: Math.max(...values),
|
|
4619
|
+
sign: Math.sign(values[1]! - values[0]!),
|
|
4620
|
+
},
|
|
4621
|
+
];
|
|
4622
|
+
}),
|
|
4623
|
+
];
|
|
4624
|
+
const spacing = Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10) / 2;
|
|
4625
|
+
const free = (track: number) =>
|
|
4626
|
+
!spans.some(
|
|
4627
|
+
(span) =>
|
|
4628
|
+
span.sign === -sign &&
|
|
4629
|
+
Math.abs(span.track - track) < 1e-6 &&
|
|
4630
|
+
Math.min(span.hi, hi) - Math.max(span.lo, lo) > 1e-6,
|
|
4631
|
+
);
|
|
4632
|
+
let chosen = middle;
|
|
4633
|
+
for (
|
|
4634
|
+
let step = 1;
|
|
4635
|
+
!free(chosen) && step <= 2 * Math.floor((to - from) / 2 / spacing);
|
|
4636
|
+
step++
|
|
4637
|
+
) {
|
|
4638
|
+
const candidate = middle + (step % 2 ? -1 : 1) * Math.ceil(step / 2) * spacing;
|
|
4639
|
+
if (candidate > from && candidate < to && free(candidate)) chosen = candidate;
|
|
4640
|
+
}
|
|
4641
|
+
fallbackTracks.push({ track: chosen, lo, hi, sign });
|
|
4642
|
+
return chosen;
|
|
4643
|
+
};
|
|
3746
4644
|
const orthogonalDetourByEdgeId = new Map<
|
|
3747
4645
|
string,
|
|
3748
4646
|
{ firstTrack: number; secondTrack: number; crossover: number }
|
|
3749
4647
|
>();
|
|
3750
4648
|
if (
|
|
3751
4649
|
style === "ORTHOGONAL" &&
|
|
3752
|
-
input.settings.mergeEdges !== true &&
|
|
3753
4650
|
(input.settings["wrapping.strategy"] ?? "OFF") === "OFF" &&
|
|
3754
|
-
input.settings.hierarchyHandling !== "INCLUDE_CHILDREN" &&
|
|
3755
4651
|
(input.settings["layerUnzipping.strategy"] ?? "NONE") === "NONE"
|
|
3756
4652
|
) {
|
|
3757
4653
|
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
3758
4654
|
const edgeEdgeSpacing = Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10);
|
|
3759
|
-
const candidatesByGap =
|
|
4655
|
+
const candidatesByGap = Array.from(
|
|
4656
|
+
{ length: flowLayers.length + 1 },
|
|
3760
4657
|
() =>
|
|
3761
4658
|
[] as Array<{
|
|
3762
4659
|
edge: GraphEdge;
|
|
3763
4660
|
sourceCross: number;
|
|
3764
4661
|
targetCross: number;
|
|
3765
4662
|
straight: boolean;
|
|
4663
|
+
sameLayerPortSide?: "source" | "target";
|
|
3766
4664
|
slot?: number;
|
|
3767
4665
|
secondSlot?: number;
|
|
3768
4666
|
crossover?: number;
|
|
@@ -3776,30 +4674,78 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3776
4674
|
endpoints === undefined ||
|
|
3777
4675
|
sourceLayer === undefined ||
|
|
3778
4676
|
targetLayer === undefined ||
|
|
3779
|
-
Math.abs(sourceLayer - targetLayer)
|
|
4677
|
+
Math.abs(sourceLayer - targetLayer) > 1
|
|
3780
4678
|
) {
|
|
3781
4679
|
continue;
|
|
3782
4680
|
}
|
|
4681
|
+
const sameLayer = sourceLayer === targetLayer;
|
|
4682
|
+
let positiveFace = true;
|
|
4683
|
+
if (sameLayer) {
|
|
4684
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
4685
|
+
const positiveSide = horizontal ? "EAST" : "SOUTH";
|
|
4686
|
+
const face = (id: string, name: string | undefined) => {
|
|
4687
|
+
const node = nodeById.get(id)!;
|
|
4688
|
+
const port = node.ports?.find((port) => port.name === name);
|
|
4689
|
+
if (port) return input.portSettings?.(port, node)?.["port.side"];
|
|
4690
|
+
// A merged hyperedge dummy keeps an inverted dummy's in-layer
|
|
4691
|
+
// edge, whose dummy ports face backward (input) or forward (output).
|
|
4692
|
+
if (!id.startsWith("__layout_dummy:") || (name !== "input" && name !== "output"))
|
|
4693
|
+
return undefined;
|
|
4694
|
+
const forward = { right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" }[
|
|
4695
|
+
input.direction
|
|
4696
|
+
]!;
|
|
4697
|
+
const backward = { EAST: "WEST", WEST: "EAST", SOUTH: "NORTH", NORTH: "SOUTH" }[
|
|
4698
|
+
forward
|
|
4699
|
+
]!;
|
|
4700
|
+
return name === "output" ? forward : backward;
|
|
4701
|
+
};
|
|
4702
|
+
const negativeSide = horizontal ? "WEST" : "NORTH";
|
|
4703
|
+
const sourceFace = face(edge.sourceId, edge.sourcePort);
|
|
4704
|
+
if (
|
|
4705
|
+
sourceFace !== face(edge.targetId, edge.targetPort) ||
|
|
4706
|
+
(sourceFace !== positiveSide && sourceFace !== negativeSide)
|
|
4707
|
+
)
|
|
4708
|
+
continue;
|
|
4709
|
+
positiveFace = sourceFace === positiveSide;
|
|
4710
|
+
}
|
|
3783
4711
|
const graphSourceCross = horizontal ? endpoints.source.y : endpoints.source.x;
|
|
3784
4712
|
const graphTargetCross = horizontal ? endpoints.target.y : endpoints.target.x;
|
|
3785
|
-
const sourceBeforeTarget =
|
|
4713
|
+
const sourceBeforeTarget = sameLayer
|
|
4714
|
+
? !orientation.reversedEdgeIds.has(edge.id)
|
|
4715
|
+
: sourceLayer < targetLayer;
|
|
3786
4716
|
const sourceCross = sourceBeforeTarget ? graphSourceCross : graphTargetCross;
|
|
3787
4717
|
const targetCross = sourceBeforeTarget ? graphTargetCross : graphSourceCross;
|
|
3788
|
-
|
|
4718
|
+
const gap = sameLayer
|
|
4719
|
+
? sourceLayer - (positiveFace ? 0 : 1)
|
|
4720
|
+
: Math.min(sourceLayer, targetLayer);
|
|
4721
|
+
candidatesByGap[gap + 1]?.push({
|
|
3789
4722
|
edge,
|
|
4723
|
+
...(sameLayer
|
|
4724
|
+
? {
|
|
4725
|
+
sameLayerPortSide:
|
|
4726
|
+
increasing === positiveFace ? ("source" as const) : ("target" as const),
|
|
4727
|
+
}
|
|
4728
|
+
: {}),
|
|
3790
4729
|
sourceCross,
|
|
3791
4730
|
targetCross,
|
|
3792
4731
|
straight: Math.abs(sourceCross - targetCross) < 1e-3,
|
|
3793
4732
|
});
|
|
3794
4733
|
}
|
|
3795
4734
|
|
|
4735
|
+
const routingPlacement = routingCoordinates(placement);
|
|
4736
|
+
const rawEndpoints = implicitEdgeEndpoints(input, routingPlacement, orientation);
|
|
3796
4737
|
const slotsByGap = candidatesByGap.map((candidates) => {
|
|
3797
4738
|
if (candidates.length === 0) return 0;
|
|
3798
4739
|
// ELK creates hyperedge segments by walking layer nodes and their ports,
|
|
3799
4740
|
// so dependency-cycle tie breaking observes cross-axis order.
|
|
3800
4741
|
candidates.sort(
|
|
3801
4742
|
(left, right) =>
|
|
3802
|
-
|
|
4743
|
+
(increasing
|
|
4744
|
+
? left.sourceCross - right.sourceCross
|
|
4745
|
+
: left.targetCross - right.targetCross) ||
|
|
4746
|
+
(increasing
|
|
4747
|
+
? left.targetCross - right.targetCross
|
|
4748
|
+
: left.sourceCross - right.sourceCross),
|
|
3803
4749
|
);
|
|
3804
4750
|
const minimumDifference = (values: readonly number[]) => {
|
|
3805
4751
|
const distinct = [...new Set(values)].sort((left, right) => left - right);
|
|
@@ -3809,261 +4755,291 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3809
4755
|
}
|
|
3810
4756
|
return minimum;
|
|
3811
4757
|
};
|
|
3812
|
-
const
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4758
|
+
const ports: OrthogonalPort[] = [];
|
|
4759
|
+
const byPort = new Map<string, OrthogonalPort>();
|
|
4760
|
+
const portByCandidate = new Map<
|
|
4761
|
+
(typeof candidates)[number],
|
|
4762
|
+
{ source: string; target: string }
|
|
4763
|
+
>();
|
|
4764
|
+
const physicalPort = (
|
|
4765
|
+
candidate: (typeof candidates)[number],
|
|
4766
|
+
side: "source" | "target",
|
|
4767
|
+
) => {
|
|
4768
|
+
const edge = candidate.edge;
|
|
4769
|
+
const sourceIsBefore =
|
|
4770
|
+
flowLayerByNodeId.get(edge.sourceId)! < flowLayerByNodeId.get(edge.targetId)!;
|
|
4771
|
+
const graphSource = candidate.sameLayerPortSide
|
|
4772
|
+
? (side === "source") !== orientation.reversedEdgeIds.has(edge.id)
|
|
4773
|
+
: (side === "source") === (sourceIsBefore === increasing);
|
|
4774
|
+
const nodeId = graphSource ? edge.sourceId : edge.targetId;
|
|
4775
|
+
const name = graphSource ? edge.sourcePort : edge.targetPort;
|
|
4776
|
+
return { graphSource, nodeId, name };
|
|
3830
4777
|
};
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
4778
|
+
// A port whose edges leave and enter it in this gap gives each
|
|
4779
|
+
// direction its own hypersegment: opposite directions never share a track.
|
|
4780
|
+
const directionsByPort = new Map<string, Set<boolean>>();
|
|
4781
|
+
for (const candidate of candidates)
|
|
4782
|
+
for (const side of ["source", "target"] as const) {
|
|
4783
|
+
const { graphSource, nodeId, name } = physicalPort(candidate, side);
|
|
4784
|
+
if (name === undefined || isMergedHyperedgeDummy(input, nodeId)) continue;
|
|
4785
|
+
const key = JSON.stringify([nodeId, name]);
|
|
4786
|
+
let directions = directionsByPort.get(key);
|
|
4787
|
+
if (!directions) directionsByPort.set(key, (directions = new Set()));
|
|
4788
|
+
directions.add(graphSource);
|
|
4789
|
+
}
|
|
4790
|
+
// The cross coordinate where a candidate meets this gap on `side`.
|
|
4791
|
+
const endPosition = (candidate: (typeof candidates)[number], side: "source" | "target") => {
|
|
4792
|
+
const position = candidate.sameLayerPortSide
|
|
4793
|
+
? side === "source"
|
|
4794
|
+
? candidate.sourceCross
|
|
4795
|
+
: candidate.targetCross
|
|
4796
|
+
: (side === "source") === increasing
|
|
4797
|
+
? candidate.sourceCross
|
|
4798
|
+
: candidate.targetCross;
|
|
4799
|
+
const endpoints = rawEndpoints.get(candidate.edge.id);
|
|
4800
|
+
const sourceBefore =
|
|
4801
|
+
flowLayerByNodeId.get(candidate.edge.sourceId)! <
|
|
4802
|
+
flowLayerByNodeId.get(candidate.edge.targetId)!;
|
|
4803
|
+
const rawPoint =
|
|
4804
|
+
endpoints &&
|
|
4805
|
+
((
|
|
4806
|
+
candidate.sameLayerPortSide
|
|
4807
|
+
? (side === "source") !== orientation.reversedEdgeIds.has(candidate.edge.id)
|
|
4808
|
+
: (side === "source") === (sourceBefore === increasing)
|
|
4809
|
+
)
|
|
4810
|
+
? endpoints.source
|
|
4811
|
+
: endpoints.target);
|
|
4812
|
+
return rawPoint ? (horizontal ? rawPoint.y : rawPoint.x) : position;
|
|
3835
4813
|
};
|
|
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
|
-
|
|
4814
|
+
/**
|
|
4815
|
+
* At a port carrying both directions, the edges fan out from separate
|
|
4816
|
+
* attachments. Ordered by where their far ends lie (back along the
|
|
4817
|
+
* port's own boundary on its near side first, then across the gap,
|
|
4818
|
+
* then back along the boundary on its far side), the edges nest
|
|
4819
|
+
* without crossing; consecutive edges of one direction share an
|
|
4820
|
+
* attachment and a hypersegment. Routing sees each attachment where
|
|
4821
|
+
* it will be, so the track order respects the split.
|
|
4822
|
+
*/
|
|
4823
|
+
const runByEnd = new Map<string, number>();
|
|
4824
|
+
const runShift = new Map<string, number>();
|
|
4825
|
+
if (style === "ORTHOGONAL")
|
|
4826
|
+
for (const [key, directions] of directionsByPort) {
|
|
4827
|
+
if (directions.size < 2) continue;
|
|
4828
|
+
const [nodeId, name] = JSON.parse(key) as [string, string];
|
|
4829
|
+
const ends = candidates.flatMap((candidate, index) =>
|
|
4830
|
+
(["source", "target"] as const).flatMap((side) => {
|
|
4831
|
+
const port = physicalPort(candidate, side);
|
|
4832
|
+
if (port.nodeId !== nodeId || port.name !== name) return [];
|
|
4833
|
+
const other = side === "source" ? "target" : "source";
|
|
4834
|
+
return [
|
|
4835
|
+
{
|
|
4836
|
+
end: `${index}:${side}`,
|
|
4837
|
+
graphSource: port.graphSource,
|
|
4838
|
+
at: endPosition(candidate, side),
|
|
4839
|
+
far: endPosition(candidate, other),
|
|
4840
|
+
near: candidate.sameLayerPortSide !== undefined,
|
|
4841
|
+
},
|
|
4842
|
+
];
|
|
4843
|
+
}),
|
|
4844
|
+
);
|
|
4845
|
+
const at = ends[0]!.at;
|
|
4846
|
+
if (ends.some((end) => Math.abs(end.at - at) > 1e-6)) continue;
|
|
4847
|
+
const group = (end: (typeof ends)[number]) => (!end.near ? 1 : end.far < at ? 0 : 2);
|
|
4848
|
+
const sorted = [...ends].sort(
|
|
4849
|
+
(a, b) =>
|
|
4850
|
+
group(a) - group(b) ||
|
|
4851
|
+
(group(a) === 1 ? a.far - b.far : b.far - a.far) ||
|
|
4852
|
+
Number(a.graphSource) - Number(b.graphSource),
|
|
4853
|
+
);
|
|
4854
|
+
let runs = 0;
|
|
4855
|
+
for (const [index, end] of sorted.entries()) {
|
|
4856
|
+
if (index && end.graphSource !== sorted[index - 1]!.graphSource) runs++;
|
|
4857
|
+
runByEnd.set(end.end, runs);
|
|
3871
4858
|
}
|
|
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
|
-
}
|
|
4859
|
+
const count = runs + 1;
|
|
4860
|
+
// A run holding an edge straight across the gap stays in place.
|
|
4861
|
+
const straight = new Set(
|
|
4862
|
+
sorted
|
|
4863
|
+
.filter((end) => !end.near && Math.abs(end.far - at) < 1e-3)
|
|
4864
|
+
.map((end) => runByEnd.get(end.end)!),
|
|
4865
|
+
);
|
|
4866
|
+
const anchor = straight.size === 1 ? [...straight][0]! : (count - 1) / 2;
|
|
4867
|
+
const rect = routingPlacement.rectByNodeId.get(nodeId);
|
|
4868
|
+
const low = rect ? (horizontal ? rect.y : rect.x) : -Infinity,
|
|
4869
|
+
high = rect ? low + (horizontal ? rect.height : rect.width) : Infinity;
|
|
4870
|
+
// Attachments stay on the node's side, closer together when it is short.
|
|
4871
|
+
let step = edgeEdgeSpacing;
|
|
4872
|
+
for (const run of [0, count - 1]) {
|
|
4873
|
+
const offset = run - anchor;
|
|
4874
|
+
if (offset < 0) step = Math.min(step, (at - low) / -offset);
|
|
4875
|
+
if (offset > 0) step = Math.min(step, (high - at) / offset);
|
|
3892
4876
|
}
|
|
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;
|
|
4877
|
+
if (!(step >= 2)) {
|
|
4878
|
+
for (const end of ends) runByEnd.delete(end.end);
|
|
4879
|
+
continue;
|
|
3918
4880
|
}
|
|
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
|
-
}
|
|
4881
|
+
for (let run = 0; run < count; run++)
|
|
4882
|
+
runShift.set(JSON.stringify(["port", nodeId, name, run]), (run - anchor) * step);
|
|
4883
|
+
for (const end of ends) {
|
|
4884
|
+
const index = end.end.split(":")[0]!;
|
|
4885
|
+
const shift = (runByEnd.get(end.end)! - anchor) * step;
|
|
4886
|
+
if (Math.abs(shift) < 1e-9) continue;
|
|
4887
|
+
const id = candidates[Number(index)]!.edge.id;
|
|
4888
|
+
const record = attachmentShiftByEdgeId.get(id) ?? {};
|
|
4889
|
+
record[end.graphSource ? "source" : "target"] = shift;
|
|
4890
|
+
attachmentShiftByEdgeId.set(id, record);
|
|
3936
4891
|
}
|
|
3937
4892
|
}
|
|
3938
|
-
|
|
3939
|
-
const
|
|
3940
|
-
const
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4893
|
+
const portKey = (candidate: (typeof candidates)[number], side: "source" | "target") => {
|
|
4894
|
+
const edge = candidate.edge;
|
|
4895
|
+
const { graphSource, nodeId, name } = physicalPort(candidate, side);
|
|
4896
|
+
const node = nodeById.get(nodeId)!;
|
|
4897
|
+
if (isMergedHyperedgeDummy(input, nodeId))
|
|
4898
|
+
return JSON.stringify(["hyperedge", nodeId, candidate.sameLayerPortSide ?? side]);
|
|
4899
|
+
const run = runByEnd.get(`${candidates.indexOf(candidate)}:${side}`);
|
|
4900
|
+
if (name !== undefined && run !== undefined)
|
|
4901
|
+
return JSON.stringify(["port", nodeId, name, run]);
|
|
4902
|
+
if (name !== undefined)
|
|
4903
|
+
return directionsByPort.get(JSON.stringify([nodeId, name]))!.size > 1
|
|
4904
|
+
? JSON.stringify(["port", nodeId, name, graphSource])
|
|
4905
|
+
: JSON.stringify(["port", nodeId, name]);
|
|
4906
|
+
if (input.settings.mergeEdges === true || input.nodeSettings?.(node)?.hypernode === true)
|
|
4907
|
+
return JSON.stringify(["implicit", nodeId, candidate.sameLayerPortSide ?? side]);
|
|
4908
|
+
return JSON.stringify(["edge", edge.id, side]);
|
|
4909
|
+
};
|
|
4910
|
+
for (const side of ["source", "target"] as const)
|
|
4911
|
+
for (const candidate of candidates) {
|
|
4912
|
+
const key = portKey(candidate, side);
|
|
4913
|
+
if (!byPort.has(key)) {
|
|
4914
|
+
const port = {
|
|
4915
|
+
id: key,
|
|
4916
|
+
side: candidate.sameLayerPortSide ?? side,
|
|
4917
|
+
position: endPosition(candidate, side) + (runShift.get(key) ?? 0),
|
|
4918
|
+
};
|
|
4919
|
+
byPort.set(key, port);
|
|
4920
|
+
ports.push(port);
|
|
3959
4921
|
}
|
|
3960
4922
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
4923
|
+
for (const candidate of candidates)
|
|
4924
|
+
portByCandidate.set(candidate, {
|
|
4925
|
+
source: portKey(candidate, "source"),
|
|
4926
|
+
target: portKey(candidate, "target"),
|
|
4927
|
+
});
|
|
4928
|
+
const edgeLists = getCrossingUnits(input)?.edgeListRanks;
|
|
4929
|
+
const listedId = (edgeId: string) => edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "");
|
|
4930
|
+
const grouped = createOrthogonalHypersegments(
|
|
4931
|
+
ports,
|
|
4932
|
+
candidates.map((candidate) => portByCandidate.get(candidate)!),
|
|
4933
|
+
(index, list) => {
|
|
4934
|
+
const { edge } = candidates[index]!;
|
|
4935
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
4936
|
+
// The port side that lists this connection: its source lists it as outgoing.
|
|
4937
|
+
const node = (list === "outgoing") !== reversed ? edge.sourceId : edge.targetId;
|
|
4938
|
+
return edgeLists?.[list].get(node)?.get(listedId(edge.id));
|
|
4939
|
+
},
|
|
4940
|
+
);
|
|
4941
|
+
const criticalThreshold =
|
|
4942
|
+
0.2 *
|
|
4943
|
+
Math.min(
|
|
4944
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.incoming)),
|
|
4945
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.outgoing)),
|
|
3963
4946
|
);
|
|
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
|
-
|
|
4947
|
+
const random =
|
|
4948
|
+
phaseRandomByInput.get(input) ?? new JavaRandom(input.settings.randomSeed ?? 1);
|
|
4949
|
+
const result = routeOrthogonalSegments(
|
|
4950
|
+
grouped.segments,
|
|
4951
|
+
0.5 * edgeEdgeSpacing,
|
|
4952
|
+
criticalThreshold,
|
|
4953
|
+
random,
|
|
4954
|
+
);
|
|
4955
|
+
const junctionGroup: (typeof junctionGroups)[number] = [];
|
|
4956
|
+
for (const group of grouped.segments) {
|
|
4957
|
+
for (const port of group.ports) {
|
|
4958
|
+
const outgoing = candidates.filter(
|
|
4959
|
+
(candidate) => portByCandidate.get(candidate)!.source === port,
|
|
4960
|
+
);
|
|
4961
|
+
// Merging adjacent helpers concatenates their physical incident
|
|
4962
|
+
// edges in retained layer order; root edge order loses that state.
|
|
4963
|
+
const listedOutgoing = getCrossingUnits(input)?.edgeListRanks?.outgoing;
|
|
4964
|
+
const listed = (edgeId: string) => edgeId.replace(/(::(segment|inverted):\d+:*)+$/, "");
|
|
4965
|
+
outgoing.sort((a, b) => {
|
|
4966
|
+
// ELK visits a port's outgoing edges in list order, which records
|
|
4967
|
+
// reversal history; edges from dummies keep the fallbacks below.
|
|
4968
|
+
const sourceOf = (candidate: (typeof outgoing)[number]) =>
|
|
4969
|
+
orientation.reversedEdgeIds.has(candidate.edge.id)
|
|
4970
|
+
? candidate.edge.targetId
|
|
4971
|
+
: candidate.edge.sourceId;
|
|
4972
|
+
const ranks =
|
|
4973
|
+
sourceOf(a) === sourceOf(b) ? listedOutgoing?.get(sourceOf(a)) : undefined;
|
|
4974
|
+
const ra = ranks?.get(listed(a.edge.id)),
|
|
4975
|
+
rb = ranks?.get(listed(b.edge.id));
|
|
4976
|
+
if (ra !== undefined && rb !== undefined) return ra - rb;
|
|
4977
|
+
// Merging appends a lower dummy's edges after the kept dummy's.
|
|
4978
|
+
if (sourceOf(a) === sourceOf(b) && isMergedHyperedgeDummy(input, sourceOf(a))) {
|
|
4979
|
+
const merged = getPlacementOrder(input)?.outputPortOrderByNodeId?.get(sourceOf(a));
|
|
4980
|
+
const ma = merged?.indexOf(a.edge.id) ?? -1,
|
|
4981
|
+
mb = merged?.indexOf(b.edge.id) ?? -1;
|
|
4982
|
+
if (ma >= 0 && mb >= 0) return ma - mb;
|
|
4983
|
+
}
|
|
4984
|
+
// Reversing incoming edges appends them after retained outgoing
|
|
4985
|
+
// edges on the physical port, regardless of original model order.
|
|
4986
|
+
const reverseOrder =
|
|
4987
|
+
Number(orientation.reversedEdgeIds.has(a.edge.id)) -
|
|
4988
|
+
Number(orientation.reversedEdgeIds.has(b.edge.id));
|
|
4989
|
+
if (reverseOrder) return reverseOrder;
|
|
4990
|
+
const source = orientation.reversedEdgeIds.has(a.edge.id)
|
|
4991
|
+
? a.edge.targetId
|
|
4992
|
+
: a.edge.sourceId;
|
|
4993
|
+
const selected = getPlacementOrder(input)?.outputPortOrderByNodeId?.get(source);
|
|
4994
|
+
const aa = selected?.indexOf(a.edge.id) ?? -1;
|
|
4995
|
+
const bb = selected?.indexOf(b.edge.id) ?? -1;
|
|
4996
|
+
return aa >= 0 && bb >= 0
|
|
4997
|
+
? aa - bb
|
|
4998
|
+
: physicalOrder.get(a.edge.id)! - physicalOrder.get(b.edge.id)!;
|
|
4999
|
+
});
|
|
5000
|
+
for (const candidate of outgoing) {
|
|
5001
|
+
const segment = result.segments[grouped.segmentByPort.get(port)!]!;
|
|
5002
|
+
const reversed = orientation.reversedEdgeIds.has(candidate.edge.id);
|
|
5003
|
+
junctionGroup.push({
|
|
5004
|
+
edgeId: candidate.edge.id,
|
|
5005
|
+
reversed,
|
|
5006
|
+
segment,
|
|
5007
|
+
sourceValue: byPort.get(portByCandidate.get(candidate)!.source)!.position,
|
|
5008
|
+
targetValue: byPort.get(portByCandidate.get(candidate)!.target)!.position,
|
|
5009
|
+
partner:
|
|
5010
|
+
segment.partner === undefined ? undefined : result.segments[segment.partner],
|
|
5011
|
+
});
|
|
4028
5012
|
}
|
|
4029
5013
|
}
|
|
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
5014
|
}
|
|
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
|
-
}
|
|
5015
|
+
junctionGroups.push(junctionGroup);
|
|
5016
|
+
let maximumSlot = -1;
|
|
5017
|
+
for (const segment of result.segments) {
|
|
5018
|
+
if (Math.abs(segment.start - segment.end) >= 1e-3)
|
|
5019
|
+
maximumSlot = Math.max(maximumSlot, segment.slot);
|
|
4054
5020
|
}
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
5021
|
+
for (const candidate of candidates) {
|
|
5022
|
+
const segment =
|
|
5023
|
+
result.segments[grouped.segmentByPort.get(portByCandidate.get(candidate)!.source)!]!;
|
|
5024
|
+
candidate.straight = Math.abs(segment.start - segment.end) < 1e-3;
|
|
5025
|
+
candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
|
|
5026
|
+
if (segment.partner !== undefined) {
|
|
5027
|
+
// The route leaves the graph source on the half holding that end.
|
|
5028
|
+
const partner = result.segments[segment.partner]!;
|
|
5029
|
+
const [first, second] = physicalPort(candidate, "source").graphSource
|
|
5030
|
+
? [segment, partner]
|
|
5031
|
+
: [partner, segment];
|
|
5032
|
+
candidate.slot = increasing ? first.slot : maximumSlot - first.slot;
|
|
5033
|
+
candidate.secondSlot = increasing ? second.slot : maximumSlot - second.slot;
|
|
5034
|
+
const sourcePort = portByCandidate.get(candidate)!.source;
|
|
5035
|
+
const offset =
|
|
5036
|
+
(increasing ? candidate.sourceCross : candidate.targetCross) -
|
|
5037
|
+
byPort.get(sourcePort)!.position +
|
|
5038
|
+
(runShift.get(sourcePort) ?? 0);
|
|
5039
|
+
candidate.crossover = segment.outgoing[0]! + offset;
|
|
4064
5040
|
}
|
|
4065
5041
|
}
|
|
4066
|
-
return
|
|
5042
|
+
return maximumSlot + 1;
|
|
4067
5043
|
});
|
|
4068
5044
|
|
|
4069
5045
|
const existingGapByLayer = flowLayers
|
|
@@ -4080,7 +5056,26 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4080
5056
|
input.nodeSettings?.(node)?.["nodePlacement.networkSimplex.nodeFlexibility"] !==
|
|
4081
5057
|
"NONE",
|
|
4082
5058
|
));
|
|
4083
|
-
|
|
5059
|
+
const leadingSlots = slotsByGap[0] ?? 0;
|
|
5060
|
+
const leadingWidth =
|
|
5061
|
+
leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
|
|
5062
|
+
let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
|
|
5063
|
+
// ELK omits ordinary node spacing beside an external-port-only layer.
|
|
5064
|
+
const externalFlowLayers = new Set(
|
|
5065
|
+
flowLayers.flatMap((_, layer) => {
|
|
5066
|
+
const ids = [...flowLayerByNodeId].filter(([, index]) => index === layer);
|
|
5067
|
+
return ids.length &&
|
|
5068
|
+
ids.every(([id]) => {
|
|
5069
|
+
const node = nodeById.get(id);
|
|
5070
|
+
const side = node && externalPortDummyOf(node)?.side;
|
|
5071
|
+
return horizontal
|
|
5072
|
+
? side === "WEST" || side === "EAST"
|
|
5073
|
+
: side === "NORTH" || side === "SOUTH";
|
|
5074
|
+
})
|
|
5075
|
+
? [layer]
|
|
5076
|
+
: [];
|
|
5077
|
+
}),
|
|
5078
|
+
);
|
|
4084
5079
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
4085
5080
|
const shift = nextStart - bounds.start;
|
|
4086
5081
|
for (const [id, rect] of placement.rectByNodeId) {
|
|
@@ -4093,23 +5088,44 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4093
5088
|
const size = bounds.end - bounds.start;
|
|
4094
5089
|
bounds.start = nextStart;
|
|
4095
5090
|
bounds.end = nextStart + size;
|
|
4096
|
-
const slots = slotsByGap[layerNo] ?? 0;
|
|
4097
|
-
const routesNearTarget = candidatesByGap[layerNo]?.some(
|
|
5091
|
+
const slots = slotsByGap[layerNo + 1] ?? 0;
|
|
5092
|
+
const routesNearTarget = candidatesByGap[layerNo + 1]?.some(
|
|
4098
5093
|
(candidate) =>
|
|
4099
5094
|
hasZeroFixedSideTarget(candidate.edge) &&
|
|
4100
|
-
candidatesByGap[layerNo]!.filter(
|
|
5095
|
+
candidatesByGap[layerNo + 1]!.filter(
|
|
4101
5096
|
({ edge }) => edge.targetId === candidate.edge.targetId,
|
|
4102
5097
|
).length >
|
|
4103
|
-
candidatesByGap[layerNo]!.filter(
|
|
5098
|
+
candidatesByGap[layerNo + 1]!.filter(
|
|
4104
5099
|
({ edge }) => edge.sourceId === candidate.edge.sourceId,
|
|
4105
5100
|
).length,
|
|
4106
5101
|
);
|
|
4107
5102
|
const preservedGap =
|
|
4108
5103
|
(existingGapByLayer[layerNo] ?? input.spacing.layer) -
|
|
4109
5104
|
(routesNearTarget ? edgeEdgeSpacing : 0);
|
|
5105
|
+
const labelAdjacent = labelFlowLayers.has(layerNo) || labelFlowLayers.has(layerNo + 1);
|
|
5106
|
+
const portMargin =
|
|
5107
|
+
flowLayers[layerNo]!.end -
|
|
5108
|
+
flowLayers[layerNo]!.start -
|
|
5109
|
+
(bodyFlowLayers[layerNo]!.end - bodyFlowLayers[layerNo]!.start) +
|
|
5110
|
+
(flowLayers[layerNo + 1]
|
|
5111
|
+
? flowLayers[layerNo + 1]!.end -
|
|
5112
|
+
flowLayers[layerNo + 1]!.start -
|
|
5113
|
+
(bodyFlowLayers[layerNo + 1]!.end - bodyFlowLayers[layerNo + 1]!.start)
|
|
5114
|
+
: 0);
|
|
5115
|
+
const externalBoundary =
|
|
5116
|
+
externalFlowLayers.has(layerNo) || externalFlowLayers.has(layerNo + 1);
|
|
5117
|
+
const minimumGap = externalBoundary
|
|
5118
|
+
? 0
|
|
5119
|
+
: labelAdjacent
|
|
5120
|
+
? Math.max(edgeNodeSpacing, input.spacing.layer - portMargin)
|
|
5121
|
+
: input.spacing.layer;
|
|
4110
5122
|
const gapSpacing =
|
|
4111
5123
|
slots === 0
|
|
4112
|
-
?
|
|
5124
|
+
? externalBoundary
|
|
5125
|
+
? 0
|
|
5126
|
+
: labelAdjacent
|
|
5127
|
+
? minimumGap
|
|
5128
|
+
: (existingGapByLayer[layerNo] ?? input.spacing.layer)
|
|
4113
5129
|
: Math.max(
|
|
4114
5130
|
preservesNodeFlexibilityGap ||
|
|
4115
5131
|
(labelExtraByGap[layerNo] ?? 0) > 0 ||
|
|
@@ -4123,14 +5139,22 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4123
5139
|
),
|
|
4124
5140
|
)
|
|
4125
5141
|
? preservedGap
|
|
4126
|
-
:
|
|
5142
|
+
: minimumGap,
|
|
5143
|
+
// LABEL nodes also need clearance beyond the final routing track.
|
|
4127
5144
|
2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing,
|
|
4128
5145
|
);
|
|
4129
5146
|
nextStart = bounds.end + gapSpacing;
|
|
4130
5147
|
}
|
|
4131
5148
|
implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
4132
5149
|
|
|
4133
|
-
for (const [
|
|
5150
|
+
for (const [boundary, candidates] of candidatesByGap.entries()) {
|
|
5151
|
+
const gap = boundary - 1;
|
|
5152
|
+
const firstBoundaryTrack =
|
|
5153
|
+
gap < 0
|
|
5154
|
+
? (flowLayers[0]?.start ?? 0) -
|
|
5155
|
+
edgeNodeSpacing -
|
|
5156
|
+
Math.max(0, leadingSlots - 1) * edgeEdgeSpacing
|
|
5157
|
+
: (flowLayers[gap]?.end ?? 0) + edgeNodeSpacing;
|
|
4134
5158
|
for (const candidate of candidates) {
|
|
4135
5159
|
if (candidate.straight) continue;
|
|
4136
5160
|
const routeNearTarget =
|
|
@@ -4142,16 +5166,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4142
5166
|
? (flowLayers[gap + 1]?.start ?? 0) -
|
|
4143
5167
|
edgeNodeSpacing -
|
|
4144
5168
|
(candidate.slot ?? 0) * edgeEdgeSpacing
|
|
4145
|
-
: (
|
|
4146
|
-
edgeNodeSpacing +
|
|
4147
|
-
(candidate.slot ?? 0) * edgeEdgeSpacing;
|
|
5169
|
+
: firstBoundaryTrack + (candidate.slot ?? 0) * edgeEdgeSpacing;
|
|
4148
5170
|
if (candidate.secondSlot !== undefined && candidate.crossover !== undefined) {
|
|
4149
5171
|
orthogonalDetourByEdgeId.set(candidate.edge.id, {
|
|
4150
5172
|
firstTrack,
|
|
4151
|
-
secondTrack:
|
|
4152
|
-
(flowLayers[gap]?.end ?? 0) +
|
|
4153
|
-
edgeNodeSpacing +
|
|
4154
|
-
candidate.secondSlot * edgeEdgeSpacing,
|
|
5173
|
+
secondTrack: firstBoundaryTrack + candidate.secondSlot * edgeEdgeSpacing,
|
|
4155
5174
|
crossover: candidate.crossover,
|
|
4156
5175
|
});
|
|
4157
5176
|
} else {
|
|
@@ -4543,18 +5562,101 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4543
5562
|
const feedbackTargetPortSide = feedbackTargetPort
|
|
4544
5563
|
? input.portSettings?.(feedbackTargetPort, target)?.["port.side"]
|
|
4545
5564
|
: undefined;
|
|
5565
|
+
// A merged hyperedge dummy keeps an inverted dummy's in-layer edge,
|
|
5566
|
+
// whose dummy ports face backward (input) or forward (output).
|
|
5567
|
+
const mergedFace = (id: string, name: string | undefined) => {
|
|
5568
|
+
if (!isMergedHyperedgeDummy(input, id) || (name !== "input" && name !== "output"))
|
|
5569
|
+
return undefined;
|
|
5570
|
+
const forward = { right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" }[
|
|
5571
|
+
input.direction
|
|
5572
|
+
]!;
|
|
5573
|
+
const backward = { EAST: "WEST", WEST: "EAST", SOUTH: "NORTH", NORTH: "SOUTH" }[forward]!;
|
|
5574
|
+
return name === "output" ? forward : backward;
|
|
5575
|
+
};
|
|
5576
|
+
const inLayerSourceSide = feedbackSourcePortSide ?? mergedFace(source.id, edge.sourcePort);
|
|
5577
|
+
const inLayerTargetSide = feedbackTargetPortSide ?? mergedFace(target.id, edge.targetPort);
|
|
5578
|
+
// Inverted-port dummies keep the turn in the node's own layer. Both
|
|
5579
|
+
// terminals face the same exterior corridor; this is not an outer
|
|
5580
|
+
// feedback loop around the entire graph.
|
|
5581
|
+
if (
|
|
5582
|
+
style === "ORTHOGONAL" &&
|
|
5583
|
+
(source.id.startsWith("__layout_dummy:inverted:") ||
|
|
5584
|
+
target.id.startsWith("__layout_dummy:inverted:") ||
|
|
5585
|
+
isMergedHyperedgeDummy(input, source.id) ||
|
|
5586
|
+
isMergedHyperedgeDummy(input, target.id)) &&
|
|
5587
|
+
flowLayerByNodeId.get(source.id) === flowLayerByNodeId.get(target.id) &&
|
|
5588
|
+
inLayerSourceSide === inLayerTargetSide &&
|
|
5589
|
+
inLayerSourceSide !== undefined
|
|
5590
|
+
) {
|
|
5591
|
+
const sourceFallback = implicitEndpoints.get(edge.id)?.source ?? {
|
|
5592
|
+
x: sourceRect.x,
|
|
5593
|
+
y: sourceRect.y,
|
|
5594
|
+
};
|
|
5595
|
+
const targetFallback = implicitEndpoints.get(edge.id)?.target ?? {
|
|
5596
|
+
x: targetRect.x,
|
|
5597
|
+
y: targetRect.y,
|
|
5598
|
+
};
|
|
5599
|
+
const start = attached(
|
|
5600
|
+
edge,
|
|
5601
|
+
"source",
|
|
5602
|
+
getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction, input),
|
|
5603
|
+
);
|
|
5604
|
+
const end = attached(
|
|
5605
|
+
edge,
|
|
5606
|
+
"target",
|
|
5607
|
+
getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction, input),
|
|
5608
|
+
);
|
|
5609
|
+
if (reversedEdge) outsideFeedbackEdgeIds.add(edge.id);
|
|
5610
|
+
const side = inLayerSourceSide;
|
|
5611
|
+
const spacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
5612
|
+
// Clear the whole occupied layer, including ports and self-loop
|
|
5613
|
+
// reservations; endpoint anchors alone omit those margins.
|
|
5614
|
+
const occupied = flowLayers[flowLayerByNodeId.get(source.id) ?? -1];
|
|
5615
|
+
const track =
|
|
5616
|
+
orthogonalTrackByEdgeId.get(edge.id) ??
|
|
5617
|
+
(side === "WEST"
|
|
5618
|
+
? (horizontal
|
|
5619
|
+
? (occupied?.start ?? Math.min(start.x, end.x))
|
|
5620
|
+
: Math.min(start.x, end.x)) - spacing
|
|
5621
|
+
: side === "EAST"
|
|
5622
|
+
? (horizontal
|
|
5623
|
+
? (occupied?.end ?? Math.max(start.x, end.x))
|
|
5624
|
+
: Math.max(start.x, end.x)) + spacing
|
|
5625
|
+
: side === "NORTH"
|
|
5626
|
+
? (!horizontal
|
|
5627
|
+
? (occupied?.start ?? Math.min(start.y, end.y))
|
|
5628
|
+
: Math.min(start.y, end.y)) - spacing
|
|
5629
|
+
: (!horizontal
|
|
5630
|
+
? (occupied?.end ?? Math.max(start.y, end.y))
|
|
5631
|
+
: Math.max(start.y, end.y)) + spacing);
|
|
5632
|
+
pointsByEdgeId.set(
|
|
5633
|
+
edge.id,
|
|
5634
|
+
simplifyRoute(
|
|
5635
|
+
side === "WEST" || side === "EAST"
|
|
5636
|
+
? [start, { x: track, y: start.y }, { x: track, y: end.y }, end]
|
|
5637
|
+
: [start, { x: start.x, y: track }, { x: end.x, y: track }, end],
|
|
5638
|
+
),
|
|
5639
|
+
);
|
|
5640
|
+
continue;
|
|
5641
|
+
}
|
|
5642
|
+
const sourceFlow = horizontal ? sourceRect.x : sourceRect.y;
|
|
5643
|
+
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
5644
|
+
const sourceBeforeTarget =
|
|
5645
|
+
sourceFlow < targetFlow ||
|
|
5646
|
+
(sourceFlow === targetFlow &&
|
|
5647
|
+
(flowLayerByNodeId.get(source.id) ?? 0) < (flowLayerByNodeId.get(target.id) ?? 0));
|
|
4546
5648
|
const sourceAwaySide = horizontal
|
|
4547
|
-
?
|
|
5649
|
+
? sourceBeforeTarget
|
|
4548
5650
|
? "WEST"
|
|
4549
5651
|
: "EAST"
|
|
4550
|
-
:
|
|
5652
|
+
: sourceBeforeTarget
|
|
4551
5653
|
? "NORTH"
|
|
4552
5654
|
: "SOUTH";
|
|
4553
5655
|
const targetAwaySide = horizontal
|
|
4554
|
-
?
|
|
5656
|
+
? sourceBeforeTarget
|
|
4555
5657
|
? "EAST"
|
|
4556
5658
|
: "WEST"
|
|
4557
|
-
:
|
|
5659
|
+
: sourceBeforeTarget
|
|
4558
5660
|
? "SOUTH"
|
|
4559
5661
|
: "NORTH";
|
|
4560
5662
|
const sameSideSelfLoop =
|
|
@@ -4565,6 +5667,63 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4565
5667
|
const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
|
|
4566
5668
|
return constraints !== "UNDEFINED" && constraints !== "FREE";
|
|
4567
5669
|
};
|
|
5670
|
+
if (
|
|
5671
|
+
style === "ORTHOGONAL" &&
|
|
5672
|
+
source.id === target.id &&
|
|
5673
|
+
hasFixedPortSide(source) &&
|
|
5674
|
+
(feedbackSourcePortSide !== undefined || feedbackTargetPortSide !== undefined) &&
|
|
5675
|
+
(edge.sourcePort === undefined ||
|
|
5676
|
+
edge.targetPort === undefined ||
|
|
5677
|
+
!sameSideSelfLoop ||
|
|
5678
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true)
|
|
5679
|
+
) {
|
|
5680
|
+
const endpoints = implicitEndpoints.get(edge.id)!;
|
|
5681
|
+
// Like other edges, a single-edge zero-size fixed-side port takes its
|
|
5682
|
+
// slot in the side's shared endpoint order, so it cannot coincide
|
|
5683
|
+
// with a sibling port that already uses that order.
|
|
5684
|
+
const terminal = (endpoint: "source" | "target"): Point => {
|
|
5685
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
5686
|
+
const fallback = endpoints[endpoint];
|
|
5687
|
+
const port = source.ports?.find((candidate) => candidate.name === portName);
|
|
5688
|
+
const side = port ? input.portSettings?.(port, source)?.["port.side"] : undefined;
|
|
5689
|
+
const onSide =
|
|
5690
|
+
(side === "EAST" && Math.abs(fallback.x - sourceRect.x - sourceRect.width) < 1e-9) ||
|
|
5691
|
+
(side === "WEST" && Math.abs(fallback.x - sourceRect.x) < 1e-9) ||
|
|
5692
|
+
(side === "SOUTH" && Math.abs(fallback.y - sourceRect.y - sourceRect.height) < 1e-9) ||
|
|
5693
|
+
(side === "NORTH" && Math.abs(fallback.y - sourceRect.y) < 1e-9);
|
|
5694
|
+
return onSide && usesSharedSideSlot(input, source, portName)
|
|
5695
|
+
? fallback
|
|
5696
|
+
: getPortPoint(source, portName, sourceRect, fallback, input.direction, input);
|
|
5697
|
+
};
|
|
5698
|
+
const start = terminal("source");
|
|
5699
|
+
const end = terminal("target");
|
|
5700
|
+
const outputSide = ({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[
|
|
5701
|
+
input.direction
|
|
5702
|
+
];
|
|
5703
|
+
const inputSide = ({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[
|
|
5704
|
+
input.direction
|
|
5705
|
+
];
|
|
5706
|
+
const loops = selfLoopsByNodeId.get(source.id) ?? [edge];
|
|
5707
|
+
const distance =
|
|
5708
|
+
Number(input.settings["spacing.nodeSelfLoop"] ?? 10) *
|
|
5709
|
+
(selfLoopTracks(loops).get(edge.id)! + 1);
|
|
5710
|
+
pointsByEdgeId.set(
|
|
5711
|
+
edge.id,
|
|
5712
|
+
(sameSideSelfLoop ? (points: Point[]) => points : simplifyRoute)(
|
|
5713
|
+
routeFixedSelfLoop(
|
|
5714
|
+
sourceRect,
|
|
5715
|
+
start,
|
|
5716
|
+
end,
|
|
5717
|
+
fixedSelfLoopSide(feedbackSourcePortSide) ?? outputSide,
|
|
5718
|
+
fixedSelfLoopSide(feedbackTargetPortSide) ?? inputSide,
|
|
5719
|
+
distance,
|
|
5720
|
+
input.direction,
|
|
5721
|
+
),
|
|
5722
|
+
),
|
|
5723
|
+
);
|
|
5724
|
+
outsideFeedbackEdgeIds.add(edge.id);
|
|
5725
|
+
continue;
|
|
5726
|
+
}
|
|
4568
5727
|
const fixedSideFeedback =
|
|
4569
5728
|
style === "ORTHOGONAL" &&
|
|
4570
5729
|
!sameSideSelfLoop &&
|
|
@@ -4850,6 +6009,31 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4850
6009
|
outsideFeedbackEdgeIds.add(edge.id);
|
|
4851
6010
|
continue;
|
|
4852
6011
|
}
|
|
6012
|
+
const movable =
|
|
6013
|
+
hasMovableLoopPorts(input, edge) &&
|
|
6014
|
+
hasPlacementLoopEnvelope(placement, source.id) &&
|
|
6015
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true;
|
|
6016
|
+
// Route unconstrained loops in the canonical rightward system, then
|
|
6017
|
+
// apply the same direction transform as the layer pipeline.
|
|
6018
|
+
const loopRect = movable
|
|
6019
|
+
? {
|
|
6020
|
+
x: 0,
|
|
6021
|
+
y: 0,
|
|
6022
|
+
width: horizontal ? sourceRect.width : sourceRect.height,
|
|
6023
|
+
height: horizontal ? sourceRect.height : sourceRect.width,
|
|
6024
|
+
}
|
|
6025
|
+
: sourceRect;
|
|
6026
|
+
const publishLoop = (points: readonly Point[]) => {
|
|
6027
|
+
const physical = !movable
|
|
6028
|
+
? points
|
|
6029
|
+
: points.map((point) => {
|
|
6030
|
+
const flow = reverse ? loopRect.width - point.x : point.x;
|
|
6031
|
+
return horizontal
|
|
6032
|
+
? { x: sourceRect.x + flow, y: sourceRect.y + point.y }
|
|
6033
|
+
: { x: sourceRect.x + point.y, y: sourceRect.y + flow };
|
|
6034
|
+
});
|
|
6035
|
+
pointsByEdgeId.set(edge.id, physical);
|
|
6036
|
+
};
|
|
4853
6037
|
const routeHorizontalSide = (
|
|
4854
6038
|
side: "NORTH" | "SOUTH",
|
|
4855
6039
|
indexOnSide: number,
|
|
@@ -4880,17 +6064,15 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4880
6064
|
const sideLoops = loops.filter(
|
|
4881
6065
|
(candidate, i) => loopSide(distribution, i, candidate) === side,
|
|
4882
6066
|
);
|
|
4883
|
-
const distance = loopDistance(sideLoops, edge, side, ordering);
|
|
6067
|
+
const distance = loopDistance(sideLoops, edge, side, ordering, movable);
|
|
4884
6068
|
const y =
|
|
4885
|
-
side === "NORTH"
|
|
4886
|
-
? sourceRect.y - distance
|
|
4887
|
-
: sourceRect.y + sourceRect.height + distance;
|
|
6069
|
+
side === "NORTH" ? loopRect.y - distance : loopRect.y + loopRect.height + distance;
|
|
4888
6070
|
const start = {
|
|
4889
|
-
x:
|
|
4890
|
-
y: side === "NORTH" ?
|
|
6071
|
+
x: loopRect.x + loopRect.width * startRatio,
|
|
6072
|
+
y: side === "NORTH" ? loopRect.y : loopRect.y + loopRect.height,
|
|
4891
6073
|
};
|
|
4892
6074
|
const end = {
|
|
4893
|
-
x:
|
|
6075
|
+
x: loopRect.x + loopRect.width * endRatio,
|
|
4894
6076
|
y: start.y,
|
|
4895
6077
|
};
|
|
4896
6078
|
return [start, { x: start.x, y }, { x: end.x, y }, end];
|
|
@@ -4907,43 +6089,41 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4907
6089
|
const startRatio = side === "EAST" ? firstRatio : secondRatio;
|
|
4908
6090
|
const endRatio = side === "EAST" ? secondRatio : firstRatio;
|
|
4909
6091
|
const distance =
|
|
4910
|
-
input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
6092
|
+
movable || input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
4911
6093
|
? loopDistance(
|
|
4912
6094
|
loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side),
|
|
4913
6095
|
edge,
|
|
4914
6096
|
side,
|
|
4915
6097
|
ordering,
|
|
6098
|
+
movable,
|
|
4916
6099
|
)
|
|
4917
6100
|
: spacing * (indexOnSide + 1);
|
|
4918
6101
|
const x =
|
|
4919
|
-
side === "EAST"
|
|
4920
|
-
? sourceRect.x + sourceRect.width + distance
|
|
4921
|
-
: sourceRect.x - distance;
|
|
6102
|
+
side === "EAST" ? loopRect.x + loopRect.width + distance : loopRect.x - distance;
|
|
4922
6103
|
const start = {
|
|
4923
|
-
x: side === "EAST" ?
|
|
4924
|
-
y:
|
|
6104
|
+
x: side === "EAST" ? loopRect.x + loopRect.width : loopRect.x,
|
|
6105
|
+
y: loopRect.y + loopRect.height * startRatio,
|
|
4925
6106
|
};
|
|
4926
|
-
const end = { x: start.x, y:
|
|
6107
|
+
const end = { x: start.x, y: loopRect.y + loopRect.height * endRatio };
|
|
4927
6108
|
return [start, { x, y: start.y }, { x, y: end.y }, end];
|
|
4928
6109
|
};
|
|
4929
6110
|
if (distribution === "NORTH_SOUTH") {
|
|
4930
6111
|
const side = loopIndex % 2 === 0 ? "NORTH" : "SOUTH";
|
|
4931
6112
|
const indexOnSide = Math.floor(loopIndex / 2);
|
|
4932
6113
|
const countOnSide = Math.ceil((loops.length - (side === "SOUTH" ? 1 : 0)) / 2);
|
|
4933
|
-
|
|
6114
|
+
publishLoop(routeHorizontalSide(side, indexOnSide, countOnSide));
|
|
4934
6115
|
} else if (distribution === "EQUALLY") {
|
|
4935
6116
|
const sides = ["NORTH", "SOUTH", "EAST", "WEST"] as const;
|
|
4936
6117
|
const side = sides[loopIndex % sides.length]!;
|
|
4937
6118
|
const indexOnSide = Math.floor(loopIndex / sides.length);
|
|
4938
6119
|
const countOnSide = Math.ceil((loops.length - sides.indexOf(side)) / sides.length);
|
|
4939
|
-
|
|
4940
|
-
edge.id,
|
|
6120
|
+
publishLoop(
|
|
4941
6121
|
side === "NORTH" || side === "SOUTH"
|
|
4942
6122
|
? routeHorizontalSide(side, indexOnSide, countOnSide)
|
|
4943
6123
|
: routeVerticalSide(side, indexOnSide, countOnSide),
|
|
4944
6124
|
);
|
|
4945
6125
|
} else {
|
|
4946
|
-
|
|
6126
|
+
publishLoop(routeHorizontalSide("NORTH", loopIndex, loops.length));
|
|
4947
6127
|
}
|
|
4948
6128
|
continue;
|
|
4949
6129
|
}
|
|
@@ -5047,7 +6227,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5047
6227
|
).length === 1;
|
|
5048
6228
|
const flexibleFeedback =
|
|
5049
6229
|
reversedEdge && !hasFixedPortSide(source) && !hasFixedPortSide(target);
|
|
5050
|
-
const
|
|
6230
|
+
const unshiftedStart = flexibleFeedback
|
|
5051
6231
|
? sourceFallback
|
|
5052
6232
|
: sourceFixedSide
|
|
5053
6233
|
? sourceFallback
|
|
@@ -5059,7 +6239,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5059
6239
|
input.direction,
|
|
5060
6240
|
input,
|
|
5061
6241
|
);
|
|
5062
|
-
const
|
|
6242
|
+
const unshiftedEnd = flexibleFeedback
|
|
5063
6243
|
? targetFallback
|
|
5064
6244
|
: targetFixedSide
|
|
5065
6245
|
? targetFallback
|
|
@@ -5071,19 +6251,23 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5071
6251
|
input.direction,
|
|
5072
6252
|
input,
|
|
5073
6253
|
);
|
|
6254
|
+
const start = attached(edge, "source", unshiftedStart);
|
|
6255
|
+
const end = attached(edge, "target", unshiftedEnd);
|
|
5074
6256
|
const sourceLayer = flowLayerByNodeId.get(edge.sourceId) ?? 0;
|
|
5075
6257
|
const targetLayer = flowLayerByNodeId.get(edge.targetId) ?? 0;
|
|
5076
6258
|
const earlierLayer = Math.min(sourceLayer, targetLayer);
|
|
5077
6259
|
const laterLayer = Math.max(sourceLayer, targetLayer);
|
|
5078
6260
|
const earlier = flowLayers[earlierLayer];
|
|
5079
6261
|
const later = flowLayers[laterLayer];
|
|
6262
|
+
// Inverted-port links clear the occupied layer, including protruding
|
|
6263
|
+
// ports and routing reservations, rather than only the two node bodies.
|
|
5080
6264
|
const sameLayerTrack =
|
|
5081
6265
|
sourceLayer === targetLayer
|
|
5082
|
-
?
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
6266
|
+
? (flowLayers[sourceLayer]?.end ??
|
|
6267
|
+
(horizontal
|
|
6268
|
+
? Math.max(sourceRect.x + sourceRect.width, targetRect.x + targetRect.width)
|
|
6269
|
+
: Math.max(sourceRect.y + sourceRect.height, targetRect.y + targetRect.height))) +
|
|
6270
|
+
Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10)
|
|
5087
6271
|
: undefined;
|
|
5088
6272
|
const track =
|
|
5089
6273
|
orthogonalTrackByEdgeId.get(edge.id) ??
|
|
@@ -5091,7 +6275,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5091
6275
|
splineTrackByEdgeId.get(edge.id) ??
|
|
5092
6276
|
sameLayerTrack ??
|
|
5093
6277
|
(earlier && later && earlierLayer !== laterLayer
|
|
5094
|
-
? (earlier.end
|
|
6278
|
+
? freeGapTrack(edge.id, earlier.end, later.start, start, end)
|
|
5095
6279
|
: horizontal
|
|
5096
6280
|
? (start.x + end.x) / 2
|
|
5097
6281
|
: (start.y + end.y) / 2);
|
|
@@ -5237,7 +6421,26 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5237
6421
|
);
|
|
5238
6422
|
}
|
|
5239
6423
|
|
|
5240
|
-
|
|
6424
|
+
const junctionPointsByEdgeId =
|
|
6425
|
+
style === "ORTHOGONAL"
|
|
6426
|
+
? orthogonalJunctionPoints(
|
|
6427
|
+
input,
|
|
6428
|
+
orientation,
|
|
6429
|
+
implicitEdgeEndpoints(input, routingCoordinates(placement), orientation),
|
|
6430
|
+
flowLayerByNodeId,
|
|
6431
|
+
junctionGroups,
|
|
6432
|
+
pointsByEdgeId,
|
|
6433
|
+
orthogonalTrackByEdgeId,
|
|
6434
|
+
orthogonalDetourByEdgeId,
|
|
6435
|
+
(node, port) => getOrientedPortDirection(input, orientation, node, port),
|
|
6436
|
+
)
|
|
6437
|
+
: undefined;
|
|
6438
|
+
return {
|
|
6439
|
+
pointsByEdgeId,
|
|
6440
|
+
splineNubControlsByEdgeId,
|
|
6441
|
+
outsideFeedbackEdgeIds,
|
|
6442
|
+
...(style === "ORTHOGONAL" ? { junctionPointsByEdgeId } : {}),
|
|
6443
|
+
};
|
|
5241
6444
|
};
|
|
5242
6445
|
}
|
|
5243
6446
|
|
|
@@ -5329,11 +6532,26 @@ export function placePorts<P>(
|
|
|
5329
6532
|
return { ...port, ...size, x: port.x, y: port.y };
|
|
5330
6533
|
}
|
|
5331
6534
|
const side = sideByPort.get(port)!;
|
|
6535
|
+
const borderOffset = Number(portSettings?.(port)?.["port.borderOffset"] ?? 0);
|
|
5332
6536
|
return {
|
|
5333
6537
|
...port,
|
|
5334
6538
|
...size,
|
|
5335
|
-
x:
|
|
5336
|
-
|
|
6539
|
+
x:
|
|
6540
|
+
side === "EAST"
|
|
6541
|
+
? rect.width + borderOffset
|
|
6542
|
+
: side === "WEST"
|
|
6543
|
+
? constraints === "FIXED_POS" && direction === "left" && size.width === 0
|
|
6544
|
+
? 0 - borderOffset
|
|
6545
|
+
: -size.width - borderOffset
|
|
6546
|
+
: port.x,
|
|
6547
|
+
y:
|
|
6548
|
+
side === "SOUTH"
|
|
6549
|
+
? rect.height + borderOffset
|
|
6550
|
+
: side === "NORTH"
|
|
6551
|
+
? constraints === "FIXED_POS" && direction === "up" && size.height === 0
|
|
6552
|
+
? 0 - borderOffset
|
|
6553
|
+
: -size.height - borderOffset
|
|
6554
|
+
: port.y,
|
|
5337
6555
|
};
|
|
5338
6556
|
}
|
|
5339
6557
|
const side = sideByPort.get(port)!;
|
|
@@ -5549,8 +6767,8 @@ export function normalizePlacementForPortExtents(
|
|
|
5549
6767
|
}
|
|
5550
6768
|
const nextLayer = physicalLayers[layerIndex + 1];
|
|
5551
6769
|
if (!nextLayer) continue;
|
|
5552
|
-
let
|
|
5553
|
-
let
|
|
6770
|
+
let trailingBoundary = Number.NEGATIVE_INFINITY;
|
|
6771
|
+
let leadingBoundary = Number.POSITIVE_INFINITY;
|
|
5554
6772
|
for (const [ids, trailing] of [
|
|
5555
6773
|
[layer, true],
|
|
5556
6774
|
[nextLayer, false],
|
|
@@ -5559,6 +6777,10 @@ export function normalizePlacementForPortExtents(
|
|
|
5559
6777
|
const node = input.graph.nodes.find((candidate) => candidate.id === id);
|
|
5560
6778
|
const rect = placement.rectByNodeId.get(id);
|
|
5561
6779
|
if (!node || !rect) continue;
|
|
6780
|
+
const bodyStart = horizontal ? rect.x : rect.y;
|
|
6781
|
+
const bodySize = horizontal ? rect.width : rect.height;
|
|
6782
|
+
if (trailing) trailingBoundary = Math.max(trailingBoundary, bodyStart + bodySize);
|
|
6783
|
+
else leadingBoundary = Math.min(leadingBoundary, bodyStart);
|
|
5562
6784
|
const ports = placePorts(
|
|
5563
6785
|
node.ports,
|
|
5564
6786
|
rect,
|
|
@@ -5575,21 +6797,37 @@ export function normalizePlacementForPortExtents(
|
|
|
5575
6797
|
: trailing
|
|
5576
6798
|
? Math.max(0, (port.y ?? 0) + (port.height ?? 0) - rect.height)
|
|
5577
6799
|
: Math.max(0, -(port.y ?? 0));
|
|
5578
|
-
if (trailing)
|
|
5579
|
-
|
|
6800
|
+
if (trailing)
|
|
6801
|
+
trailingBoundary = Math.max(trailingBoundary, bodyStart + bodySize + extent);
|
|
6802
|
+
else leadingBoundary = Math.min(leadingBoundary, bodyStart - extent);
|
|
5580
6803
|
}
|
|
5581
6804
|
}
|
|
5582
6805
|
}
|
|
5583
|
-
accumulatedShift +=
|
|
6806
|
+
accumulatedShift += Math.max(
|
|
6807
|
+
0,
|
|
6808
|
+
input.spacing.layer - (leadingBoundary + accumulatedShift - trailingBoundary),
|
|
6809
|
+
);
|
|
5584
6810
|
}
|
|
5585
6811
|
|
|
5586
6812
|
let minimumX = Number.POSITIVE_INFINITY;
|
|
5587
6813
|
let minimumY = Number.POSITIVE_INFINITY;
|
|
6814
|
+
const envelopes = loopEnvelopes(input);
|
|
6815
|
+
const reverse = input.direction === "left" || input.direction === "up";
|
|
5588
6816
|
for (const node of input.graph.nodes) {
|
|
5589
6817
|
const rect = placement.rectByNodeId.get(node.id);
|
|
5590
6818
|
if (!rect) continue;
|
|
5591
|
-
|
|
5592
|
-
|
|
6819
|
+
const envelope = hasPlacementLoopEnvelope(placement, node.id)
|
|
6820
|
+
? envelopes.get(node.id)
|
|
6821
|
+
: undefined;
|
|
6822
|
+
const flowBefore = reverse ? envelope?.flowAfter : envelope?.flowBefore;
|
|
6823
|
+
minimumX = Math.min(
|
|
6824
|
+
minimumX,
|
|
6825
|
+
rect.x - (horizontal ? (flowBefore ?? 0) : (envelope?.before ?? 0)),
|
|
6826
|
+
);
|
|
6827
|
+
minimumY = Math.min(
|
|
6828
|
+
minimumY,
|
|
6829
|
+
rect.y - (horizontal ? (envelope?.before ?? 0) : (flowBefore ?? 0)),
|
|
6830
|
+
);
|
|
5593
6831
|
const ports = placePorts(
|
|
5594
6832
|
node.ports,
|
|
5595
6833
|
rect,
|