@statelyai/layout 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +264 -0
- package/NOTICE.md +10 -0
- package/README.md +140 -0
- package/dist/elkjs/index.d.mts +97 -0
- package/dist/elkjs/index.mjs +412 -0
- package/dist/index-v0P1Ake8.d.mts +149 -0
- package/dist/index.d.mts +77 -0
- package/dist/index.mjs +139 -0
- package/dist/layered/index.d.mts +2 -0
- package/dist/layered/index.mjs +3 -0
- package/dist/layered-ByNCZQgJ.mjs +439 -0
- package/dist/spore-cihb_Aht.mjs +487 -0
- package/package.json +61 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { d as UnsupportedLayoutError, i as breakCyclesWithDepthFirstSearch, l as routeEdgesOrthogonally, n as layeredAlgorithm, o as minimizeCrossingsWithBarycenter, r as assignLayersByLongestPath, s as placeNodesInLayers, t as getLayeredLayout, u as LayoutError } from "./layered-ByNCZQgJ.mjs";
|
|
2
|
+
import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-cihb_Aht.mjs";
|
|
3
|
+
import { getGraphIssues } from "@statelyai/graph";
|
|
4
|
+
|
|
5
|
+
//#region src/layout.ts
|
|
6
|
+
const algorithms = new Map([
|
|
7
|
+
["box", boxAlgorithm],
|
|
8
|
+
["layered", layeredAlgorithm],
|
|
9
|
+
["fixed", fixedAlgorithm],
|
|
10
|
+
["rectpacking", rectanglePackingAlgorithm],
|
|
11
|
+
["random", randomAlgorithm],
|
|
12
|
+
["sporeCompaction", sporeCompactionAlgorithm],
|
|
13
|
+
["sporeOverlap", sporeOverlapRemovalAlgorithm]
|
|
14
|
+
]);
|
|
15
|
+
function supportsScope(algorithm, scope) {
|
|
16
|
+
switch (scope.mode) {
|
|
17
|
+
case "full": return algorithm.capabilities.full;
|
|
18
|
+
case "incremental": return algorithm.capabilities.incremental;
|
|
19
|
+
case "partial": return algorithm.capabilities.partial;
|
|
20
|
+
case "route-only": return algorithm.capabilities.routeOnly;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function samePoints(a, b) {
|
|
24
|
+
if (a === b) return true;
|
|
25
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
26
|
+
return a.every((point, index) => {
|
|
27
|
+
const other = b[index];
|
|
28
|
+
return other !== void 0 && point.x === other.x && point.y === other.y;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function getLayoutPatches(input, output) {
|
|
32
|
+
const patches = [];
|
|
33
|
+
const outputNodes = new Map(output.nodes.map((node) => [node.id, node]));
|
|
34
|
+
const outputEdges = new Map(output.edges.map((edge) => [edge.id, edge]));
|
|
35
|
+
for (const node of input.nodes) {
|
|
36
|
+
const next = outputNodes.get(node.id);
|
|
37
|
+
if (!next) continue;
|
|
38
|
+
if (node.x !== next.x || node.y !== next.y || node.width !== next.width || node.height !== next.height || node.ports !== next.ports) patches.push({
|
|
39
|
+
op: "updateNode",
|
|
40
|
+
id: node.id,
|
|
41
|
+
data: {
|
|
42
|
+
x: next.x,
|
|
43
|
+
y: next.y,
|
|
44
|
+
width: next.width,
|
|
45
|
+
height: next.height,
|
|
46
|
+
...next.ports === void 0 ? {} : { ports: next.ports }
|
|
47
|
+
},
|
|
48
|
+
description: "Apply layout geometry"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const edge of input.edges) {
|
|
52
|
+
const next = outputEdges.get(edge.id);
|
|
53
|
+
if (!next) continue;
|
|
54
|
+
if (edge.x !== next.x || edge.y !== next.y || edge.width !== next.width || edge.height !== next.height || edge.routing !== next.routing || !samePoints(edge.points, next.points)) patches.push({
|
|
55
|
+
op: "updateEdge",
|
|
56
|
+
id: edge.id,
|
|
57
|
+
data: {
|
|
58
|
+
x: next.x,
|
|
59
|
+
y: next.y,
|
|
60
|
+
width: next.width,
|
|
61
|
+
height: next.height,
|
|
62
|
+
...next.routing === void 0 ? {} : { routing: next.routing },
|
|
63
|
+
...next.points === void 0 ? {} : { points: next.points }
|
|
64
|
+
},
|
|
65
|
+
description: "Apply layout geometry"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return patches;
|
|
69
|
+
}
|
|
70
|
+
/** Register or replace a layout algorithm for subsequent `getLayout` calls. */
|
|
71
|
+
function registerLayoutAlgorithm(algorithm) {
|
|
72
|
+
const previous = algorithms.get(algorithm.id);
|
|
73
|
+
algorithms.set(algorithm.id, algorithm);
|
|
74
|
+
return () => {
|
|
75
|
+
if (previous) algorithms.set(algorithm.id, previous);
|
|
76
|
+
else algorithms.delete(algorithm.id);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function getLayoutAlgorithm(id) {
|
|
80
|
+
return algorithms.get(id);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run a registered or inline algorithm against an `@statelyai/graph` graph.
|
|
84
|
+
* The input is not mutated.
|
|
85
|
+
*/
|
|
86
|
+
async function getLayout(request) {
|
|
87
|
+
const startedAt = performance.now();
|
|
88
|
+
const scope = request.scope ?? { mode: "full" };
|
|
89
|
+
const diagnostics = [];
|
|
90
|
+
const phases = [];
|
|
91
|
+
const algorithm = typeof request.algorithm === "object" ? request.algorithm : getLayoutAlgorithm(request.algorithm ?? "layered");
|
|
92
|
+
if (!algorithm) throw new LayoutError(`Unknown layout algorithm: ${request.algorithm ?? "layered"}`, "UNKNOWN_ALGORITHM");
|
|
93
|
+
if (!supportsScope(algorithm, scope)) throw new UnsupportedLayoutError(`${algorithm.id} does not support ${scope.mode} layout yet`);
|
|
94
|
+
const issues = getGraphIssues(request.graph);
|
|
95
|
+
if (issues.length > 0) throw new LayoutError(issues.map((issue) => issue.message).join("; "), "INVALID_GRAPH");
|
|
96
|
+
const context = {
|
|
97
|
+
scope,
|
|
98
|
+
diagnostics,
|
|
99
|
+
...request.signal === void 0 ? {} : { signal: request.signal },
|
|
100
|
+
measurePhase(id, run) {
|
|
101
|
+
const phaseStartedAt = performance.now();
|
|
102
|
+
const record = () => {
|
|
103
|
+
phases.push({
|
|
104
|
+
id,
|
|
105
|
+
durationMs: performance.now() - phaseStartedAt
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const result = run();
|
|
110
|
+
if (result instanceof Promise) return result.finally(record);
|
|
111
|
+
record();
|
|
112
|
+
return result;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
record();
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
throwIfAborted() {
|
|
119
|
+
if (request.signal?.aborted) throw request.signal.reason ?? /* @__PURE__ */ new Error("Layout aborted");
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
context.throwIfAborted();
|
|
123
|
+
const graph = await algorithm.layout(request.graph, request.options, context);
|
|
124
|
+
context.throwIfAborted();
|
|
125
|
+
return {
|
|
126
|
+
graph,
|
|
127
|
+
patches: getLayoutPatches(request.graph, graph),
|
|
128
|
+
diagnostics,
|
|
129
|
+
metrics: {
|
|
130
|
+
durationMs: performance.now() - startedAt,
|
|
131
|
+
nodeCount: graph.nodes.length,
|
|
132
|
+
edgeCount: graph.edges.length,
|
|
133
|
+
phases
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
export { LayoutError, UnsupportedLayoutError, assignLayersByLongestPath, boxAlgorithm, breakCyclesWithDepthFirstSearch, fixedAlgorithm, getBoxLayout, getFixedLayout, getLayeredLayout, getLayout, getLayoutAlgorithm, getRandomLayout, getRectanglePackingLayout, getSporeCompactionLayout, getSporeOverlapRemovalLayout, layeredAlgorithm, minimizeCrossingsWithBarycenter, placeNodesInLayers, randomAlgorithm, rectanglePackingAlgorithm, registerLayoutAlgorithm, routeEdgesOrthogonally, sporeCompactionAlgorithm, sporeOverlapRemovalAlgorithm };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as NodeSize, S as NodePlacer, _ as LayeredPhaseInput, a as minimizeCrossingsWithBarycenter, b as LayoutPadding, c as AcyclicOrientation, d as EdgeRouter, f as EdgeRoutes, g as LayeredLayoutOptions, h as LayerOrder, i as breakCyclesWithDepthFirstSearch, l as CrossingMinimizer, m as LayerAssignment, n as layeredAlgorithm, o as placeNodesInLayers, p as LayerAssigner, r as assignLayersByLongestPath, s as routeEdgesOrthogonally, t as getLayeredLayout, u as CycleBreaker, v as LayeredSpacing, x as NodePlacement, y as LayeredStrategies } from "../index-v0P1Ake8.mjs";
|
|
2
|
+
export { AcyclicOrientation, CrossingMinimizer, CycleBreaker, EdgeRouter, EdgeRoutes, LayerAssigner, LayerAssignment, LayerOrder, LayeredLayoutOptions, LayeredPhaseInput, LayeredSpacing, LayeredStrategies, LayoutPadding, NodePlacement, NodePlacer, NodeSize, assignLayersByLongestPath, breakCyclesWithDepthFirstSearch, getLayeredLayout, layeredAlgorithm, minimizeCrossingsWithBarycenter, placeNodesInLayers, routeEdgesOrthogonally };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { i as breakCyclesWithDepthFirstSearch, l as routeEdgesOrthogonally, n as layeredAlgorithm, o as minimizeCrossingsWithBarycenter, r as assignLayersByLongestPath, s as placeNodesInLayers, t as getLayeredLayout } from "../layered-ByNCZQgJ.mjs";
|
|
2
|
+
|
|
3
|
+
export { assignLayersByLongestPath, breakCyclesWithDepthFirstSearch, getLayeredLayout, layeredAlgorithm, minimizeCrossingsWithBarycenter, placeNodesInLayers, routeEdgesOrthogonally };
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var LayoutError = class extends Error {
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "LayoutError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var UnsupportedLayoutError = class extends LayoutError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, "UNSUPPORTED_LAYOUT");
|
|
12
|
+
this.name = "UnsupportedLayoutError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/layered/strategies.ts
|
|
18
|
+
function getOrientedEndpoints(edge, orientation) {
|
|
19
|
+
return orientation.reversedEdgeIds.has(edge.id) ? [edge.targetId, edge.sourceId] : [edge.sourceId, edge.targetId];
|
|
20
|
+
}
|
|
21
|
+
const breakCyclesWithDepthFirstSearch = (input) => {
|
|
22
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const node of input.graph.nodes) outgoing.set(node.id, []);
|
|
24
|
+
for (const edge of input.graph.edges) outgoing.get(edge.sourceId)?.push(edge);
|
|
25
|
+
const state = /* @__PURE__ */ new Map();
|
|
26
|
+
const reversedEdgeIds = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const node of input.graph.nodes) {
|
|
28
|
+
if (state.get(node.id) !== void 0) continue;
|
|
29
|
+
state.set(node.id, "active");
|
|
30
|
+
const stack = [{
|
|
31
|
+
nodeId: node.id,
|
|
32
|
+
edgeIndex: 0
|
|
33
|
+
}];
|
|
34
|
+
while (stack.length > 0) {
|
|
35
|
+
const frame = stack.at(-1);
|
|
36
|
+
if (!frame) break;
|
|
37
|
+
const edge = (outgoing.get(frame.nodeId) ?? [])[frame.edgeIndex];
|
|
38
|
+
if (!edge) {
|
|
39
|
+
state.set(frame.nodeId, "done");
|
|
40
|
+
stack.pop();
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
frame.edgeIndex++;
|
|
44
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
45
|
+
const targetState = state.get(edge.targetId);
|
|
46
|
+
if (targetState === "active") reversedEdgeIds.add(edge.id);
|
|
47
|
+
else if (targetState === void 0) {
|
|
48
|
+
state.set(edge.targetId, "active");
|
|
49
|
+
stack.push({
|
|
50
|
+
nodeId: edge.targetId,
|
|
51
|
+
edgeIndex: 0
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { reversedEdgeIds };
|
|
57
|
+
};
|
|
58
|
+
const assignLayersByLongestPath = (input, orientation) => {
|
|
59
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
60
|
+
const successors = /* @__PURE__ */ new Map();
|
|
61
|
+
const layerByNodeId = /* @__PURE__ */ new Map();
|
|
62
|
+
for (const node of input.graph.nodes) {
|
|
63
|
+
indegree.set(node.id, 0);
|
|
64
|
+
successors.set(node.id, []);
|
|
65
|
+
layerByNodeId.set(node.id, input.constrainedLayerByNodeId.get(node.id) ?? 0);
|
|
66
|
+
}
|
|
67
|
+
for (const edge of input.graph.edges) {
|
|
68
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
69
|
+
if (sourceId === targetId) continue;
|
|
70
|
+
const sourceConstraint = input.constrainedLayerByNodeId.get(sourceId);
|
|
71
|
+
const targetConstraint = input.constrainedLayerByNodeId.get(targetId);
|
|
72
|
+
if (sourceConstraint !== void 0 && targetConstraint !== void 0 && targetConstraint <= sourceConstraint) throw new LayoutError(`Layer constraints conflict on edge ${edge.id}`, "UNSATISFIABLE_CONSTRAINTS");
|
|
73
|
+
successors.get(sourceId)?.push(targetId);
|
|
74
|
+
indegree.set(targetId, (indegree.get(targetId) ?? 0) + 1);
|
|
75
|
+
}
|
|
76
|
+
const queue = input.graph.nodes.filter((node) => indegree.get(node.id) === 0).map((node) => node.id);
|
|
77
|
+
for (let index = 0; index < queue.length; index++) {
|
|
78
|
+
const sourceId = queue[index];
|
|
79
|
+
if (sourceId === void 0) continue;
|
|
80
|
+
const sourceLayer = layerByNodeId.get(sourceId) ?? 0;
|
|
81
|
+
for (const targetId of successors.get(sourceId) ?? []) {
|
|
82
|
+
const targetConstraint = input.constrainedLayerByNodeId.get(targetId);
|
|
83
|
+
if (targetConstraint !== void 0 && targetConstraint < sourceLayer + 1) throw new LayoutError(`Layer constraint conflicts at node ${targetId}`, "UNSATISFIABLE_CONSTRAINTS");
|
|
84
|
+
layerByNodeId.set(targetId, targetConstraint ?? Math.max(layerByNodeId.get(targetId) ?? 0, sourceLayer + 1));
|
|
85
|
+
const nextIndegree = (indegree.get(targetId) ?? 1) - 1;
|
|
86
|
+
indegree.set(targetId, nextIndegree);
|
|
87
|
+
if (nextIndegree === 0) queue.push(targetId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { layerByNodeId };
|
|
91
|
+
};
|
|
92
|
+
function sortLayerByBarycenter(layer, adjacentLayer, neighbors) {
|
|
93
|
+
const adjacentIndex = new Map(adjacentLayer.map((nodeId, index) => [nodeId, index]));
|
|
94
|
+
const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index]));
|
|
95
|
+
layer.sort((a, b) => {
|
|
96
|
+
const barycenter = (nodeId) => {
|
|
97
|
+
const positions = (neighbors.get(nodeId) ?? []).map((id) => adjacentIndex.get(id)).filter((value) => value !== void 0);
|
|
98
|
+
if (positions.length === 0) return originalIndex.get(nodeId) ?? 0;
|
|
99
|
+
return positions.reduce((sum, value) => sum + value, 0) / positions.length;
|
|
100
|
+
};
|
|
101
|
+
return barycenter(a) - barycenter(b) || (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function minimizeCrossingsWithBarycenter(sweeps = 4) {
|
|
105
|
+
return (input, orientation, assignment) => {
|
|
106
|
+
let maximumLayer = 0;
|
|
107
|
+
for (const layer of assignment.layerByNodeId.values()) maximumLayer = Math.max(maximumLayer, layer);
|
|
108
|
+
const layerCount = maximumLayer + 1;
|
|
109
|
+
const layers = Array.from({ length: layerCount }, () => []);
|
|
110
|
+
for (const node of input.graph.nodes) layers[assignment.layerByNodeId.get(node.id) ?? 0]?.push(node.id);
|
|
111
|
+
const predecessors = /* @__PURE__ */ new Map();
|
|
112
|
+
const successors = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const node of input.graph.nodes) {
|
|
114
|
+
predecessors.set(node.id, []);
|
|
115
|
+
successors.set(node.id, []);
|
|
116
|
+
}
|
|
117
|
+
for (const edge of input.graph.edges) {
|
|
118
|
+
const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
|
|
119
|
+
if (sourceId === targetId) continue;
|
|
120
|
+
successors.get(sourceId)?.push(targetId);
|
|
121
|
+
predecessors.get(targetId)?.push(sourceId);
|
|
122
|
+
}
|
|
123
|
+
for (let sweep = 0; sweep < sweeps; sweep++) {
|
|
124
|
+
for (let layer = 1; layer < layers.length; layer++) {
|
|
125
|
+
const current = layers[layer];
|
|
126
|
+
const previous = layers[layer - 1];
|
|
127
|
+
if (current && previous) sortLayerByBarycenter(current, previous, predecessors);
|
|
128
|
+
}
|
|
129
|
+
for (let layer = layers.length - 2; layer >= 0; layer--) {
|
|
130
|
+
const current = layers[layer];
|
|
131
|
+
const next = layers[layer + 1];
|
|
132
|
+
if (current && next) sortLayerByBarycenter(current, next, successors);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { layers };
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function sumWithSpacing(ids, input, axis) {
|
|
139
|
+
return ids.reduce((total, id, index) => total + (input.sizes.get(id)?.[axis] ?? 0) + (index === 0 ? 0 : input.spacing.node), 0);
|
|
140
|
+
}
|
|
141
|
+
const placeNodesInLayers = (input, order) => {
|
|
142
|
+
const horizontal = input.direction === "left" || input.direction === "right";
|
|
143
|
+
const layerFlowSizes = order.layers.map((layer) => Math.max(0, ...layer.map((id) => horizontal ? input.sizes.get(id)?.width ?? 0 : input.sizes.get(id)?.height ?? 0)));
|
|
144
|
+
const layerCrossSizes = order.layers.map((layer) => sumWithSpacing(layer, input, horizontal ? "height" : "width"));
|
|
145
|
+
let maxCrossSize = 0;
|
|
146
|
+
for (const size of layerCrossSizes) maxCrossSize = Math.max(maxCrossSize, size);
|
|
147
|
+
const rectByNodeId = /* @__PURE__ */ new Map();
|
|
148
|
+
let flow = horizontal ? input.padding.left : input.padding.top;
|
|
149
|
+
order.layers.forEach((layer, layerIndex) => {
|
|
150
|
+
let cross = (horizontal ? input.padding.top : input.padding.left) + (maxCrossSize - (layerCrossSizes[layerIndex] ?? 0)) / 2;
|
|
151
|
+
for (const id of layer) {
|
|
152
|
+
const size = input.sizes.get(id) ?? {
|
|
153
|
+
width: 0,
|
|
154
|
+
height: 0
|
|
155
|
+
};
|
|
156
|
+
const rect = horizontal ? {
|
|
157
|
+
x: flow,
|
|
158
|
+
y: cross,
|
|
159
|
+
...size
|
|
160
|
+
} : {
|
|
161
|
+
x: cross,
|
|
162
|
+
y: flow,
|
|
163
|
+
...size
|
|
164
|
+
};
|
|
165
|
+
rectByNodeId.set(id, rect);
|
|
166
|
+
cross += (horizontal ? size.height : size.width) + input.spacing.node;
|
|
167
|
+
}
|
|
168
|
+
flow += (layerFlowSizes[layerIndex] ?? 0) + input.spacing.layer;
|
|
169
|
+
});
|
|
170
|
+
if (input.direction === "up" || input.direction === "left") {
|
|
171
|
+
let contentEnd = 0;
|
|
172
|
+
for (const rect of rectByNodeId.values()) contentEnd = Math.max(contentEnd, horizontal ? rect.x + rect.width : rect.y + rect.height);
|
|
173
|
+
const leadingPadding = horizontal ? input.padding.left : input.padding.top;
|
|
174
|
+
for (const [id, rect] of rectByNodeId) rectByNodeId.set(id, horizontal ? {
|
|
175
|
+
...rect,
|
|
176
|
+
x: contentEnd - rect.x + leadingPadding - rect.width
|
|
177
|
+
} : {
|
|
178
|
+
...rect,
|
|
179
|
+
y: contentEnd - rect.y + leadingPadding - rect.height
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return { rectByNodeId };
|
|
183
|
+
};
|
|
184
|
+
function getPortPoint(node, portName, rect, fallback, direction) {
|
|
185
|
+
if (portName === void 0) return fallback;
|
|
186
|
+
const port = placePorts(node.ports, rect, direction)?.find((candidate) => candidate.name === portName);
|
|
187
|
+
if (port?.x === void 0 || port.y === void 0) return fallback;
|
|
188
|
+
return {
|
|
189
|
+
x: rect.x + port.x + (port.width ?? 0) / 2,
|
|
190
|
+
y: rect.y + port.y + (port.height ?? 0) / 2
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function removeDuplicatePoints(points) {
|
|
194
|
+
return points.filter((point, index) => index === 0 || point.x !== points[index - 1]?.x || point.y !== points[index - 1]?.y);
|
|
195
|
+
}
|
|
196
|
+
const routeEdgesOrthogonally = (input, _orientation, placement) => {
|
|
197
|
+
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
198
|
+
const pointsByEdgeId = /* @__PURE__ */ new Map();
|
|
199
|
+
const horizontal = input.direction === "left" || input.direction === "right";
|
|
200
|
+
const reverse = input.direction === "up" || input.direction === "left";
|
|
201
|
+
for (const edge of input.graph.edges) {
|
|
202
|
+
const source = nodeById.get(edge.sourceId);
|
|
203
|
+
const target = nodeById.get(edge.targetId);
|
|
204
|
+
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
205
|
+
const targetRect = placement.rectByNodeId.get(edge.targetId);
|
|
206
|
+
if (!source || !target || !sourceRect || !targetRect) continue;
|
|
207
|
+
if (source.id === target.id) {
|
|
208
|
+
const x = sourceRect.x + sourceRect.width;
|
|
209
|
+
const y = sourceRect.y + sourceRect.height / 2;
|
|
210
|
+
pointsByEdgeId.set(edge.id, [
|
|
211
|
+
{
|
|
212
|
+
x,
|
|
213
|
+
y
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
x: x + 24,
|
|
217
|
+
y
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
x: x + 24,
|
|
221
|
+
y: y - 24
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
x,
|
|
225
|
+
y: y - 24
|
|
226
|
+
}
|
|
227
|
+
]);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const sourceFallback = horizontal ? {
|
|
231
|
+
x: sourceRect.x + (reverse ? 0 : sourceRect.width),
|
|
232
|
+
y: sourceRect.y + sourceRect.height / 2
|
|
233
|
+
} : {
|
|
234
|
+
x: sourceRect.x + sourceRect.width / 2,
|
|
235
|
+
y: sourceRect.y + (reverse ? 0 : sourceRect.height)
|
|
236
|
+
};
|
|
237
|
+
const targetFallback = horizontal ? {
|
|
238
|
+
x: targetRect.x + (reverse ? targetRect.width : 0),
|
|
239
|
+
y: targetRect.y + targetRect.height / 2
|
|
240
|
+
} : {
|
|
241
|
+
x: targetRect.x + targetRect.width / 2,
|
|
242
|
+
y: targetRect.y + (reverse ? targetRect.height : 0)
|
|
243
|
+
};
|
|
244
|
+
const start = getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction);
|
|
245
|
+
const end = getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction);
|
|
246
|
+
const middle = horizontal ? [{
|
|
247
|
+
x: (start.x + end.x) / 2,
|
|
248
|
+
y: start.y
|
|
249
|
+
}, {
|
|
250
|
+
x: (start.x + end.x) / 2,
|
|
251
|
+
y: end.y
|
|
252
|
+
}] : [{
|
|
253
|
+
x: start.x,
|
|
254
|
+
y: (start.y + end.y) / 2
|
|
255
|
+
}, {
|
|
256
|
+
x: end.x,
|
|
257
|
+
y: (start.y + end.y) / 2
|
|
258
|
+
}];
|
|
259
|
+
pointsByEdgeId.set(edge.id, removeDuplicatePoints([
|
|
260
|
+
start,
|
|
261
|
+
...middle,
|
|
262
|
+
end
|
|
263
|
+
]));
|
|
264
|
+
}
|
|
265
|
+
return { pointsByEdgeId };
|
|
266
|
+
};
|
|
267
|
+
function placePorts(ports, rect, direction) {
|
|
268
|
+
if (!ports) return void 0;
|
|
269
|
+
const horizontal = direction === "left" || direction === "right";
|
|
270
|
+
const reverse = direction === "up" || direction === "left";
|
|
271
|
+
return ports.map((port, index) => {
|
|
272
|
+
const size = {
|
|
273
|
+
width: port.width ?? 8,
|
|
274
|
+
height: port.height ?? 8
|
|
275
|
+
};
|
|
276
|
+
if (port.x !== void 0 && port.y !== void 0) return {
|
|
277
|
+
...port,
|
|
278
|
+
...size
|
|
279
|
+
};
|
|
280
|
+
const ratio = (index + 1) / (ports.length + 1);
|
|
281
|
+
const outgoing = port.direction !== "in";
|
|
282
|
+
const farSide = reverse ? !outgoing : outgoing;
|
|
283
|
+
return {
|
|
284
|
+
...port,
|
|
285
|
+
...size,
|
|
286
|
+
x: horizontal ? farSide ? rect.width - size.width / 2 : -size.width / 2 : ratio * rect.width - size.width / 2,
|
|
287
|
+
y: horizontal ? ratio * rect.height - size.height / 2 : farSide ? rect.height - size.height / 2 : -size.height / 2
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function getPolylineMidpoint(points) {
|
|
292
|
+
if (points.length === 0) return {
|
|
293
|
+
x: 0,
|
|
294
|
+
y: 0
|
|
295
|
+
};
|
|
296
|
+
let total = 0;
|
|
297
|
+
const lengths = [];
|
|
298
|
+
for (let index = 1; index < points.length; index++) {
|
|
299
|
+
const previous = points[index - 1];
|
|
300
|
+
const point = points[index];
|
|
301
|
+
if (!previous || !point) continue;
|
|
302
|
+
const length = Math.hypot(point.x - previous.x, point.y - previous.y);
|
|
303
|
+
lengths.push(length);
|
|
304
|
+
total += length;
|
|
305
|
+
}
|
|
306
|
+
let remaining = total / 2;
|
|
307
|
+
for (let index = 1; index < points.length; index++) {
|
|
308
|
+
const previous = points[index - 1];
|
|
309
|
+
const point = points[index];
|
|
310
|
+
const length = lengths[index - 1] ?? 0;
|
|
311
|
+
if (!previous || !point) continue;
|
|
312
|
+
if (remaining <= length) {
|
|
313
|
+
const ratio = length === 0 ? 0 : remaining / length;
|
|
314
|
+
return {
|
|
315
|
+
x: previous.x + (point.x - previous.x) * ratio,
|
|
316
|
+
y: previous.y + (point.y - previous.y) * ratio
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
remaining -= length;
|
|
320
|
+
}
|
|
321
|
+
return points.at(-1) ?? {
|
|
322
|
+
x: 0,
|
|
323
|
+
y: 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/layered/index.ts
|
|
329
|
+
const DEFAULT_NODE_SIZE = {
|
|
330
|
+
width: 100,
|
|
331
|
+
height: 50
|
|
332
|
+
};
|
|
333
|
+
function getNodeSize(node, options) {
|
|
334
|
+
const measured = options.measure?.(node);
|
|
335
|
+
if (measured) return measured;
|
|
336
|
+
return {
|
|
337
|
+
width: node.width !== void 0 && node.width > 0 ? node.width : DEFAULT_NODE_SIZE.width,
|
|
338
|
+
height: node.height !== void 0 && node.height > 0 ? node.height : DEFAULT_NODE_SIZE.height
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function assertFlatGraph(graph) {
|
|
342
|
+
const compoundNodeIds = graph.nodes.filter((node) => node.parentId != null).map((node) => node.id);
|
|
343
|
+
if (compoundNodeIds.length > 0) throw new UnsupportedLayoutError(`The first layered milestone supports flat graphs only; nested nodes: ${compoundNodeIds.join(", ")}`);
|
|
344
|
+
}
|
|
345
|
+
function runLayeredPipeline(graph, options, context) {
|
|
346
|
+
assertFlatGraph(graph);
|
|
347
|
+
const direction = options.direction ?? graph.direction ?? "down";
|
|
348
|
+
const padding = typeof options.padding === "number" ? {
|
|
349
|
+
top: options.padding,
|
|
350
|
+
right: options.padding,
|
|
351
|
+
bottom: options.padding,
|
|
352
|
+
left: options.padding
|
|
353
|
+
} : {
|
|
354
|
+
top: options.padding?.top ?? 0,
|
|
355
|
+
right: options.padding?.right ?? 0,
|
|
356
|
+
bottom: options.padding?.bottom ?? 0,
|
|
357
|
+
left: options.padding?.left ?? 0
|
|
358
|
+
};
|
|
359
|
+
const input = {
|
|
360
|
+
graph,
|
|
361
|
+
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
|
|
362
|
+
direction,
|
|
363
|
+
spacing: {
|
|
364
|
+
node: options.spacing?.node ?? 40,
|
|
365
|
+
layer: options.spacing?.layer ?? 60
|
|
366
|
+
},
|
|
367
|
+
padding,
|
|
368
|
+
constrainedLayerByNodeId: new Map(graph.nodes.flatMap((node) => {
|
|
369
|
+
const layer = options.constraints?.layer?.(node);
|
|
370
|
+
return layer === void 0 ? [] : [[node.id, layer]];
|
|
371
|
+
}))
|
|
372
|
+
};
|
|
373
|
+
const measure = (id, run) => {
|
|
374
|
+
context?.throwIfAborted();
|
|
375
|
+
return context ? context.measurePhase(id, run) : run();
|
|
376
|
+
};
|
|
377
|
+
const orientation = measure("cycle-breaking", () => (options.strategies?.breakCycles ?? breakCyclesWithDepthFirstSearch)(input));
|
|
378
|
+
const assignment = measure("layer-assignment", () => (options.strategies?.assignLayers ?? assignLayersByLongestPath)(input, orientation));
|
|
379
|
+
const order = measure("crossing-minimization", () => (options.strategies?.minimizeCrossings ?? minimizeCrossingsWithBarycenter(options.crossingSweeps))(input, orientation, assignment));
|
|
380
|
+
const placement = measure("node-placement", () => (options.strategies?.placeNodes ?? placeNodesInLayers)(input, order));
|
|
381
|
+
const routes = measure("edge-routing", () => (options.strategies?.routeEdges ?? routeEdgesOrthogonally)(input, orientation, placement));
|
|
382
|
+
const nodes = graph.nodes.map((node) => {
|
|
383
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
384
|
+
if (!rect) throw new Error(`Node placement missing for ${node.id}`);
|
|
385
|
+
const ports = placePorts(node.ports, rect, direction);
|
|
386
|
+
return {
|
|
387
|
+
...node,
|
|
388
|
+
...rect,
|
|
389
|
+
...ports === void 0 ? {} : { ports }
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
const edges = graph.edges.map((edge) => {
|
|
393
|
+
const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
|
|
394
|
+
const midpoint = getPolylineMidpoint(points);
|
|
395
|
+
const width = edge.width ?? 0;
|
|
396
|
+
const height = edge.height ?? 0;
|
|
397
|
+
return {
|
|
398
|
+
...edge,
|
|
399
|
+
x: midpoint.x - width / 2,
|
|
400
|
+
y: midpoint.y - height / 2,
|
|
401
|
+
width,
|
|
402
|
+
height,
|
|
403
|
+
points,
|
|
404
|
+
routing: "orthogonal"
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
...graph,
|
|
409
|
+
direction,
|
|
410
|
+
nodes,
|
|
411
|
+
edges
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Deterministic native layered layout for an `@statelyai/graph` graph.
|
|
416
|
+
*
|
|
417
|
+
* This initial vertical slice supports flat graphs, cycles, ports, self-loops,
|
|
418
|
+
* four directions, custom phase strategies, and orthogonal routes.
|
|
419
|
+
*/
|
|
420
|
+
function getLayeredLayout(graph, options = {}) {
|
|
421
|
+
return runLayeredPipeline(graph, options);
|
|
422
|
+
}
|
|
423
|
+
const layeredAlgorithm = {
|
|
424
|
+
id: "layered",
|
|
425
|
+
capabilities: {
|
|
426
|
+
full: true,
|
|
427
|
+
incremental: false,
|
|
428
|
+
partial: false,
|
|
429
|
+
routeOnly: false,
|
|
430
|
+
hierarchy: false,
|
|
431
|
+
ports: true
|
|
432
|
+
},
|
|
433
|
+
layout(graph, options, context) {
|
|
434
|
+
return runLayeredPipeline(graph, options ?? {}, context);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
export { getPolylineMidpoint as a, placePorts as c, UnsupportedLayoutError as d, breakCyclesWithDepthFirstSearch as i, routeEdgesOrthogonally as l, layeredAlgorithm as n, minimizeCrossingsWithBarycenter as o, assignLayersByLongestPath as r, placeNodesInLayers as s, getLayeredLayout as t, LayoutError as u };
|