@statelyai/layout 0.0.0 → 0.0.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 +9 -1
- package/README.md +52 -25
- package/dist/elkjs/index.d.mts +3 -0
- package/dist/elkjs/index.mjs +901 -97
- package/dist/index-D2RodsZY.d.mts +1210 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/layered-Dd868WZY.mjs +8632 -0
- package/dist/{spore-cihb_Aht.mjs → spore-fTgSoRLP.mjs} +1 -31
- package/package.json +45 -22
- package/dist/index-v0P1Ake8.d.mts +0 -149
- package/dist/layered-ByNCZQgJ.mjs +0 -439
package/dist/elkjs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as getLayeredLayout } from "../layered-
|
|
2
|
-
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-
|
|
1
|
+
import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-Dd868WZY.mjs";
|
|
2
|
+
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-fTgSoRLP.mjs";
|
|
3
3
|
import { createGraph } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/elkjs/index.ts
|
|
@@ -20,7 +20,7 @@ var ELK = class {
|
|
|
20
20
|
}
|
|
21
21
|
async knownLayoutAlgorithms() {
|
|
22
22
|
return [...this.#algorithmIds].map((id) => ({
|
|
23
|
-
id,
|
|
23
|
+
id: id === "layered" ? "org.eclipse.elk.layered" : id,
|
|
24
24
|
name: {
|
|
25
25
|
layered: "Layered",
|
|
26
26
|
box: "Box",
|
|
@@ -31,12 +31,7 @@ var ELK = class {
|
|
|
31
31
|
sporeOverlap: "SPOrE Overlap Removal"
|
|
32
32
|
}[id],
|
|
33
33
|
category: id === "layered" ? "layered" : "other",
|
|
34
|
-
knownOptions: id === "layered" ? [
|
|
35
|
-
"elk.direction",
|
|
36
|
-
"elk.padding",
|
|
37
|
-
"elk.spacing.nodeNode",
|
|
38
|
-
"elk.layered.spacing.nodeNodeBetweenLayers"
|
|
39
|
-
] : id === "box" ? [
|
|
34
|
+
knownOptions: id === "layered" ? elkLayeredOptionDefinitions.map((definition) => definition.elkId) : id === "box" ? [
|
|
40
35
|
"padding",
|
|
41
36
|
"spacing.nodeNode",
|
|
42
37
|
"aspectRatio",
|
|
@@ -50,43 +45,16 @@ var ELK = class {
|
|
|
50
45
|
}));
|
|
51
46
|
}
|
|
52
47
|
async knownLayoutOptions() {
|
|
53
|
-
return [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "elk.padding",
|
|
66
|
-
name: "Padding",
|
|
67
|
-
type: "OBJECT"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "elk.aspectRatio",
|
|
71
|
-
name: "Aspect Ratio",
|
|
72
|
-
type: "DOUBLE"
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: "elk.randomSeed",
|
|
76
|
-
name: "Random Seed",
|
|
77
|
-
type: "INT"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "elk.spacing.nodeNode",
|
|
81
|
-
name: "Node Spacing",
|
|
82
|
-
type: "DOUBLE"
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "elk.layered.spacing.nodeNodeBetweenLayers",
|
|
86
|
-
name: "Layer Spacing",
|
|
87
|
-
type: "DOUBLE"
|
|
88
|
-
}
|
|
89
|
-
];
|
|
48
|
+
return [{
|
|
49
|
+
id: "org.eclipse.elk.algorithm",
|
|
50
|
+
name: "Layout Algorithm",
|
|
51
|
+
type: "STRING"
|
|
52
|
+
}, ...elkLayeredOptionDefinitions.map((definition) => ({
|
|
53
|
+
id: definition.elkId,
|
|
54
|
+
name: definition.name,
|
|
55
|
+
type: definition.type,
|
|
56
|
+
targets: [...definition.targets]
|
|
57
|
+
}))];
|
|
90
58
|
}
|
|
91
59
|
async knownLayoutCategories() {
|
|
92
60
|
return [{
|
|
@@ -115,33 +83,270 @@ var ELK = class {
|
|
|
115
83
|
...graph.properties,
|
|
116
84
|
...graph.layoutOptions
|
|
117
85
|
};
|
|
86
|
+
getOption(layoutOptions, "debugMode");
|
|
87
|
+
getOption(layoutOptions, "interactiveLayout");
|
|
88
|
+
getOption(layoutOptions, "layered.generatePositionAndLayerIds");
|
|
89
|
+
getOption(layoutOptions, "topdown.scaleFactor");
|
|
90
|
+
getOption(layoutOptions, "contentAlignment");
|
|
91
|
+
for (const child of graph.children ?? []) {
|
|
92
|
+
const childOptions = child.layoutOptions ?? {};
|
|
93
|
+
getOption(childOptions, "layered.layering.layerId");
|
|
94
|
+
getOption(childOptions, "layered.crossingMinimization.positionId");
|
|
95
|
+
getOption(childOptions, "layered.layering.layerChoiceConstraint");
|
|
96
|
+
getOption(childOptions, "layered.crossingMinimization.positionChoiceConstraint");
|
|
97
|
+
getOption(childOptions, "layered.crossingMinimization.inLayerPredOf");
|
|
98
|
+
getOption(childOptions, "layered.crossingMinimization.inLayerSuccOf");
|
|
99
|
+
getOption(childOptions, "topdown.scaleFactor");
|
|
100
|
+
getOption(childOptions, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
101
|
+
for (const port of child.ports ?? []) getOption(port.layoutOptions ?? {}, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
102
|
+
}
|
|
103
|
+
for (const edge of graph.edges ?? []) getOption(edge.layoutOptions ?? {}, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
118
104
|
const requestedAlgorithm = String(getOption(layoutOptions, "algorithm") ?? "layered");
|
|
119
105
|
const algorithm = requestedAlgorithm.replace(/^(?:org\.eclipse\.)?elk\./, "");
|
|
120
106
|
if (algorithm !== "layered" && algorithm !== "box" && algorithm !== "fixed" && algorithm !== "random" && algorithm !== "rectpacking" && algorithm !== "sporeCompaction" && algorithm !== "sporeOverlap") throw new Error(`org.eclipse.elk.core.UnsupportedConfigurationException: Layout algorithm '${requestedAlgorithm}' not found`);
|
|
121
107
|
const hasHierarchy = (graph.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
108
|
+
const insideSelfLoopBaseHeightByNodeId = /* @__PURE__ */ new Map();
|
|
109
|
+
const hierarchyRestorations = [];
|
|
110
|
+
const syntheticPortIds = /* @__PURE__ */ new Set();
|
|
111
|
+
const authoredPortsByCompound = /* @__PURE__ */ new Map();
|
|
112
|
+
const authoredOptionsByCompound = /* @__PURE__ */ new Map();
|
|
113
|
+
const hierarchyBoundaryCountByEdge = /* @__PURE__ */ new Map();
|
|
114
|
+
const originalHierarchyEndpoints = new Map((graph.edges ?? []).map((edge) => [edge, {
|
|
115
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
116
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
117
|
+
sources: edge.sources,
|
|
118
|
+
targets: edge.targets,
|
|
119
|
+
source: edge.source,
|
|
120
|
+
target: edge.target
|
|
121
|
+
}]));
|
|
122
|
+
let hasHierarchyCrossingEdges = false;
|
|
122
123
|
const hierarchyHandling = getOption(layoutOptions, "hierarchyHandling");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
124
|
+
const topdownLayout = getBooleanOption(layoutOptions, "topdownLayout") === true;
|
|
125
|
+
const separateHierarchy = hasHierarchy && hierarchyHandling !== void 0 && hierarchyHandling !== "INCLUDE_CHILDREN";
|
|
126
|
+
if (hasHierarchy && topdownLayout && hierarchyHandling === "INCLUDE_CHILDREN") throw new Error("org.eclipse.elk.core.UnsupportedConfigurationException: Topdown layout cannot be used together with hierarchy handling.");
|
|
127
|
+
if (separateHierarchy) {
|
|
128
|
+
const hasCrossHierarchyEdge = (container) => {
|
|
129
|
+
const directEndpointIds = /* @__PURE__ */ new Set();
|
|
130
|
+
for (const child of container.children ?? []) {
|
|
131
|
+
directEndpointIds.add(String(child.id));
|
|
132
|
+
for (const port of child.ports ?? []) directEndpointIds.add(String(port.id));
|
|
133
|
+
}
|
|
134
|
+
if ((container.edges ?? []).some((edge) => {
|
|
135
|
+
const sourceId = String(edge.sources?.[0] ?? edge.source);
|
|
136
|
+
const targetId = String(edge.targets?.[0] ?? edge.target);
|
|
137
|
+
return !directEndpointIds.has(sourceId) || !directEndpointIds.has(targetId);
|
|
138
|
+
})) return true;
|
|
139
|
+
return (container.children ?? []).some((child) => hasCrossHierarchyEdge(child));
|
|
140
|
+
};
|
|
141
|
+
if (hasCrossHierarchyEdge(graph)) throw new Error("org.eclipse.elk.core.UnsupportedGraphException: Hierarchical edges require INCLUDE_CHILDREN");
|
|
142
|
+
for (const child of graph.children ?? []) {
|
|
143
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
144
|
+
await this.layout(child, {
|
|
145
|
+
...arguments_,
|
|
146
|
+
layoutOptions: {
|
|
147
|
+
...arguments_.layoutOptions,
|
|
148
|
+
...child.layoutOptions,
|
|
149
|
+
hierarchyHandling
|
|
150
|
+
},
|
|
151
|
+
logging: false,
|
|
152
|
+
measureExecutionTime: false
|
|
153
|
+
});
|
|
154
|
+
}
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
if (hasHierarchy && topdownLayout) {
|
|
157
|
+
if (getOption(layoutOptions, "topdown.nodeType") === void 0) throw new Error(`${String(graph.id)} has not been assigned a top-down node type.`);
|
|
158
|
+
for (const child of graph.children ?? []) {
|
|
159
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
160
|
+
const childOptions = {
|
|
161
|
+
...layoutOptions,
|
|
162
|
+
...child.layoutOptions
|
|
163
|
+
};
|
|
164
|
+
const childPadding = parsePadding(getOption(childOptions, "padding"), 12);
|
|
165
|
+
const parallelNode = getOption(childOptions, "topdown.nodeType") === "PARALLEL_NODE";
|
|
166
|
+
const width = getNumberOption(childOptions, "topdown.hierarchicalNodeWidth") ?? (parallelNode ? 150 : 0);
|
|
167
|
+
const aspectRatio = getNumberOption(childOptions, "topdown.hierarchicalNodeAspectRatio") ?? (parallelNode ? 1.414 : 1 / Math.sqrt(2));
|
|
168
|
+
child.width = Math.max(child.width ?? 0, width + childPadding.left + childPadding.right);
|
|
169
|
+
child.height = Math.max(child.height ?? 0, width / aspectRatio + childPadding.top + childPadding.bottom);
|
|
170
|
+
}
|
|
171
|
+
} else if (hasHierarchy && !separateHierarchy) {
|
|
172
|
+
getNumberOption(layoutOptions, "layered.crossingMinimization.hierarchicalSweepiness");
|
|
173
|
+
for (const child of graph.children ?? []) {
|
|
174
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
175
|
+
const descendantById = /* @__PURE__ */ new Map();
|
|
176
|
+
const descendantOwnerByEndpointId = /* @__PURE__ */ new Map();
|
|
177
|
+
const collectDescendants = (node) => {
|
|
178
|
+
for (const descendant of node.children ?? []) {
|
|
179
|
+
descendantById.set(String(descendant.id), descendant);
|
|
180
|
+
descendantOwnerByEndpointId.set(String(descendant.id), descendant);
|
|
181
|
+
for (const port of descendant.ports ?? []) descendantOwnerByEndpointId.set(String(port.id), descendant);
|
|
182
|
+
collectDescendants(descendant);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
collectDescendants(child);
|
|
186
|
+
const internalEdges = (graph.edges ?? []).filter((edge) => {
|
|
187
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
188
|
+
return descendantOwnerByEndpointId.has(sourceId) && descendantOwnerByEndpointId.has(targetId);
|
|
189
|
+
});
|
|
190
|
+
const crossingEdges = (graph.edges ?? []).filter((edge) => {
|
|
191
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
192
|
+
return descendantOwnerByEndpointId.has(sourceId) !== descendantOwnerByEndpointId.has(targetId);
|
|
193
|
+
});
|
|
194
|
+
for (const edge of crossingEdges) hierarchyBoundaryCountByEdge.set(edge, (hierarchyBoundaryCountByEdge.get(edge) ?? 0) + 1);
|
|
195
|
+
hasHierarchyCrossingEdges ||= crossingEdges.length > 0;
|
|
196
|
+
getBooleanOption(layoutOptions, "layered.mergeHierarchyEdges");
|
|
197
|
+
const proxyByKind = /* @__PURE__ */ new Map();
|
|
198
|
+
const proxyFor = (kind) => {
|
|
199
|
+
const key = kind;
|
|
200
|
+
let proxy = proxyByKind.get(key);
|
|
201
|
+
if (!proxy) {
|
|
202
|
+
proxy = {
|
|
203
|
+
id: `__native_hierarchy_${String(child.id)}_${key.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
204
|
+
width: 0,
|
|
205
|
+
height: 0
|
|
206
|
+
};
|
|
207
|
+
proxyByKind.set(key, proxy);
|
|
208
|
+
}
|
|
209
|
+
return proxy;
|
|
210
|
+
};
|
|
211
|
+
const temporaryEdges = [
|
|
212
|
+
...child.edges ?? [],
|
|
213
|
+
...internalEdges.filter((edge) => !(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id))),
|
|
214
|
+
...crossingEdges.map((edge) => {
|
|
215
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
216
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
217
|
+
const proxy = proxyFor(sourceInside ? "output" : "input");
|
|
218
|
+
return {
|
|
219
|
+
...edge,
|
|
220
|
+
id: `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
221
|
+
sources: [sourceInside ? sourceId : proxy.id],
|
|
222
|
+
targets: [sourceInside ? proxy.id : targetId],
|
|
223
|
+
source: void 0,
|
|
224
|
+
target: void 0,
|
|
225
|
+
sections: void 0
|
|
226
|
+
};
|
|
227
|
+
})
|
|
228
|
+
];
|
|
229
|
+
const temporaryChild = {
|
|
230
|
+
...child,
|
|
231
|
+
children: [...child.children ?? [], ...proxyByKind.values()],
|
|
232
|
+
edges: temporaryEdges
|
|
233
|
+
};
|
|
234
|
+
await this.layout(temporaryChild, {
|
|
235
|
+
...arguments_,
|
|
236
|
+
layoutOptions: {
|
|
237
|
+
...arguments_.layoutOptions,
|
|
238
|
+
hierarchyHandling: "INCLUDE_CHILDREN"
|
|
239
|
+
},
|
|
240
|
+
logging: false,
|
|
241
|
+
measureExecutionTime: false
|
|
242
|
+
});
|
|
243
|
+
const childPadding = parsePadding(getOption({
|
|
244
|
+
...layoutOptions,
|
|
245
|
+
...child.layoutOptions
|
|
246
|
+
}, "padding"), 12);
|
|
247
|
+
if (proxyByKind.has("input")) {
|
|
248
|
+
const direction = getDirection(layoutOptions);
|
|
249
|
+
const horizontal = direction === "right" || direction === "left";
|
|
250
|
+
if (direction === "right" || direction === "down") {
|
|
251
|
+
const minimumFlow = Math.min(...(child.children ?? []).map((node) => horizontal ? node.x ?? 0 : node.y ?? 0));
|
|
252
|
+
const delta = (horizontal ? childPadding.left : childPadding.top) - minimumFlow;
|
|
253
|
+
for (const node of child.children ?? []) if (horizontal) node.x = (node.x ?? 0) + delta;
|
|
254
|
+
else node.y = (node.y ?? 0) + delta;
|
|
255
|
+
for (const edge of temporaryEdges) for (const section of edge.sections ?? []) for (const point of [
|
|
256
|
+
section.startPoint,
|
|
257
|
+
...section.bendPoints ?? [],
|
|
258
|
+
section.endPoint
|
|
259
|
+
]) if (horizontal) point.x += delta;
|
|
260
|
+
else point.y += delta;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const internalEdge of internalEdges) {
|
|
264
|
+
const temporary = temporaryEdges.find((candidate) => String(candidate.id) === String(internalEdge.id));
|
|
265
|
+
if (temporary?.sections) internalEdge.sections = temporary.sections;
|
|
266
|
+
}
|
|
267
|
+
child.width = Math.max(0, ...(child.children ?? []).map((node) => (node.x ?? 0) + (node.width ?? 0))) + childPadding.right;
|
|
268
|
+
child.height = Math.max(0, ...(child.children ?? []).map((node) => (node.y ?? 0) + (node.height ?? 0))) + childPadding.bottom;
|
|
269
|
+
const relativeRect = (id) => {
|
|
270
|
+
const owner = descendantOwnerByEndpointId.get(id);
|
|
271
|
+
if (!owner) return void 0;
|
|
272
|
+
const path = [];
|
|
273
|
+
const visit = (parent) => {
|
|
274
|
+
for (const candidate of parent.children ?? []) {
|
|
275
|
+
path.push(candidate);
|
|
276
|
+
if (candidate === owner || visit(candidate)) return true;
|
|
277
|
+
path.pop();
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
};
|
|
281
|
+
if (!visit(child)) return void 0;
|
|
282
|
+
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
283
|
+
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
284
|
+
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
285
|
+
if (port) return {
|
|
286
|
+
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
287
|
+
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
288
|
+
width: 0,
|
|
289
|
+
height: 0
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
x: ownerX,
|
|
293
|
+
y: ownerY,
|
|
294
|
+
width: path.at(-1)?.width ?? 0,
|
|
295
|
+
height: path.at(-1)?.height ?? 0
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
authoredPortsByCompound.set(child, child.ports);
|
|
299
|
+
authoredOptionsByCompound.set(child, child.layoutOptions);
|
|
300
|
+
const ports = [...child.ports ?? []];
|
|
301
|
+
for (const edge of crossingEdges) {
|
|
302
|
+
const original = originalHierarchyEndpoints.get(edge);
|
|
303
|
+
const { sourceId, targetId } = original;
|
|
304
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
305
|
+
const rect = relativeRect(sourceInside ? sourceId : targetId);
|
|
306
|
+
if (!rect) continue;
|
|
307
|
+
const portId = `__native_hierarchy_port_${String(child.id)}_${String(edge.id)}`;
|
|
308
|
+
syntheticPortIds.add(portId);
|
|
309
|
+
const direction = getDirection(layoutOptions);
|
|
310
|
+
const useFarSide = sourceInside === (direction === "right" || direction === "down");
|
|
311
|
+
ports.push({
|
|
312
|
+
id: portId,
|
|
313
|
+
width: 0,
|
|
314
|
+
height: 0,
|
|
315
|
+
x: direction === "right" || direction === "left" ? (rect.x ?? 0) + (useFarSide ? rect.width ?? 0 : 0) : (rect.x ?? 0) + (rect.width ?? 0) / 2,
|
|
316
|
+
y: direction === "down" || direction === "up" ? (rect.y ?? 0) + (useFarSide ? rect.height ?? 0 : 0) : (rect.y ?? 0) + (rect.height ?? 0) / 2
|
|
317
|
+
});
|
|
318
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) hierarchyRestorations.push({
|
|
319
|
+
edge,
|
|
320
|
+
...original
|
|
321
|
+
});
|
|
322
|
+
if (sourceInside) edge.sources = [portId];
|
|
323
|
+
else edge.targets = [portId];
|
|
324
|
+
edge.source = void 0;
|
|
325
|
+
edge.target = void 0;
|
|
326
|
+
}
|
|
327
|
+
child.ports = ports;
|
|
328
|
+
child.layoutOptions = {
|
|
329
|
+
...child.layoutOptions,
|
|
330
|
+
"elk.portConstraints": "FIXED_POS"
|
|
331
|
+
};
|
|
332
|
+
const insideLoopCount = (graph.edges ?? []).filter((edge) => isInsideSelfLoop(graph, edge, String(child.id))).length;
|
|
333
|
+
if (insideLoopCount > 0) {
|
|
334
|
+
const baseHeight = child.height ?? 0;
|
|
335
|
+
insideSelfLoopBaseHeightByNodeId.set(String(child.id), baseHeight);
|
|
336
|
+
child.height = baseHeight + insideLoopCount * 11;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
applyNodeMicroLayout(graph, layoutOptions);
|
|
341
|
+
const graph_ = toGraph(graph, layoutOptions);
|
|
342
|
+
const layerConstraintByNodeId = new Map((graph.children ?? []).map((node) => [String(node.id), String(getOption(node.layoutOptions ?? {}, "layered.layering.layerConstraint") ?? getOption(node.layoutOptions ?? {}, "layerConstraint") ?? "NONE")]));
|
|
139
343
|
if (algorithm === "layered" && graph_.edges.some((edge) => {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
return
|
|
344
|
+
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
345
|
+
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
346
|
+
return (sourceConstraint === "FIRST" || sourceConstraint === "FIRST_SEPARATE") && (targetConstraint === "FIRST" || targetConstraint === "FIRST_SEPARATE");
|
|
143
347
|
})) throw new Error("org.eclipse.elk.core.UnsupportedConfigurationException: Layer constraints conflict");
|
|
144
|
-
|
|
348
|
+
const padding = parsePadding(getOption(layoutOptions, "padding"), algorithm === "layered" ? 12 : 0);
|
|
349
|
+
const laidOut = algorithm === "sporeCompaction" ? getSporeCompactionLayout(graph_, {
|
|
145
350
|
padding,
|
|
146
351
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
147
352
|
}) : algorithm === "sporeOverlap" ? getSporeOverlapRemovalLayout(graph_, {
|
|
@@ -168,12 +373,153 @@ var ELK = class {
|
|
|
168
373
|
}) : algorithm === "fixed" ? getFixedLayout(graph_, { direction: getDirection(layoutOptions) }) : getLayeredLayout(graph_, {
|
|
169
374
|
direction: getDirection(layoutOptions),
|
|
170
375
|
spacing: {
|
|
171
|
-
node: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
172
|
-
layer: getNumberOption(layoutOptions, "layered.spacing.nodeNodeBetweenLayers")
|
|
376
|
+
node: getNumberOption(layoutOptions, "spacing.nodeNode") ?? getNumberOption(layoutOptions, "layered.spacing.baseValue"),
|
|
377
|
+
layer: (getNumberOption(layoutOptions, "layered.spacing.nodeNodeBetweenLayers") ?? getNumberOption(layoutOptions, "layered.spacing.baseValue") ?? 20) + (hasHierarchyCrossingEdges ? 5 * Math.max(...hierarchyBoundaryCountByEdge.values()) : 0)
|
|
173
378
|
},
|
|
174
379
|
padding,
|
|
175
|
-
constraints: { layer: (
|
|
176
|
-
|
|
380
|
+
constraints: { layer: () => void 0 },
|
|
381
|
+
settings: {
|
|
382
|
+
...getLayeredSettings(layoutOptions),
|
|
383
|
+
...hierarchyHandling === "INCLUDE_CHILDREN" && getOption(layoutOptions, "layered.crossingMinimization.greedySwitchHierarchical.type") !== void 0 ? { "crossingMinimization.greedySwitch.type": String(getOption(layoutOptions, "layered.crossingMinimization.greedySwitchHierarchical.type")) } : {}
|
|
384
|
+
},
|
|
385
|
+
nodeSettings: (node) => {
|
|
386
|
+
const child = graph.children?.find((candidate) => String(candidate.id) === node.id);
|
|
387
|
+
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
388
|
+
},
|
|
389
|
+
edgeSettings: (edge) => {
|
|
390
|
+
const elkEdge = graph.edges?.find((candidate) => String(candidate.id) === edge.id);
|
|
391
|
+
return {
|
|
392
|
+
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
393
|
+
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {})
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
portSettings: (port, node) => {
|
|
397
|
+
const elkPort = (graph.children?.find((candidate) => String(candidate.id) === node.id))?.ports?.find((candidate) => String(candidate.id) === port.name);
|
|
398
|
+
return {
|
|
399
|
+
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
400
|
+
"port.labelWidth": Math.max(0, ...(elkPort?.labels ?? []).map((label) => label.width ?? 0)),
|
|
401
|
+
"port.labelHeight": Math.max(0, ...(elkPort?.labels ?? []).map((label) => label.height ?? 0))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (hierarchyRestorations.length > 0) {
|
|
406
|
+
const direction = getDirection(layoutOptions);
|
|
407
|
+
const horizontal = direction === "right" || direction === "left";
|
|
408
|
+
const cross = (point) => horizontal ? point.y : point.x;
|
|
409
|
+
const nodeSpacing = getNumberOption(layoutOptions, "spacing.nodeNode") ?? 20;
|
|
410
|
+
const shiftsByOutsideId = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const restoration of hierarchyRestorations) {
|
|
412
|
+
const route = laidOut.edges.find((edge) => edge.id === String(restoration.edge.id))?.points;
|
|
413
|
+
if (!route || route.length < 2) continue;
|
|
414
|
+
const originalSourceId = String(restoration.sources?.[0] ?? restoration.source);
|
|
415
|
+
const originalTargetId = String(restoration.targets?.[0] ?? restoration.target);
|
|
416
|
+
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
417
|
+
const outsideId = sourceInside ? originalTargetId : originalSourceId;
|
|
418
|
+
const delta = sourceInside ? cross(route[0]) - cross(route.at(-1)) : cross(route.at(-1)) - cross(route[0]);
|
|
419
|
+
const candidates = shiftsByOutsideId.get(outsideId) ?? [];
|
|
420
|
+
candidates.push(delta);
|
|
421
|
+
shiftsByOutsideId.set(outsideId, candidates);
|
|
422
|
+
}
|
|
423
|
+
for (const [outsideId, candidates] of shiftsByOutsideId) {
|
|
424
|
+
const delta = [...candidates].sort((left, right) => Math.abs(left) - Math.abs(right))[0];
|
|
425
|
+
const node = laidOut.nodes.find((candidate) => candidate.id === outsideId);
|
|
426
|
+
if (!node || Math.abs(delta) < 1e-9 || Math.abs(delta) > nodeSpacing) continue;
|
|
427
|
+
if (horizontal) node.y = (node.y ?? 0) + delta;
|
|
428
|
+
else node.x = (node.x ?? 0) + delta;
|
|
429
|
+
for (const edge of laidOut.edges) {
|
|
430
|
+
const points = edge.points;
|
|
431
|
+
if (!points || points.length === 0) continue;
|
|
432
|
+
if (edge.sourceId === outsideId) if (horizontal) points[0].y += delta;
|
|
433
|
+
else points[0].x += delta;
|
|
434
|
+
if (edge.targetId === outsideId) if (horizontal) points.at(-1).y += delta;
|
|
435
|
+
else points.at(-1).x += delta;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const modelOrder = new Map((graph.children ?? []).map((node, index) => [String(node.id), index]));
|
|
439
|
+
const flowLayers = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const node of laidOut.nodes) {
|
|
441
|
+
const flow = horizontal ? node.x ?? 0 : node.y ?? 0;
|
|
442
|
+
const layer = flowLayers.get(flow) ?? [];
|
|
443
|
+
layer.push(node);
|
|
444
|
+
flowLayers.set(flow, layer);
|
|
445
|
+
}
|
|
446
|
+
for (const layer of flowLayers.values()) {
|
|
447
|
+
layer.sort((left, right) => (modelOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (modelOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER));
|
|
448
|
+
let crossEnd = Number.NEGATIVE_INFINITY;
|
|
449
|
+
for (const node of layer) {
|
|
450
|
+
const authoredCross = horizontal ? node.y ?? 0 : node.x ?? 0;
|
|
451
|
+
const compactedCross = Math.max(authoredCross, crossEnd === Number.NEGATIVE_INFINITY ? authoredCross : crossEnd + nodeSpacing);
|
|
452
|
+
const delta = compactedCross - authoredCross;
|
|
453
|
+
if (horizontal) node.y = compactedCross;
|
|
454
|
+
else node.x = compactedCross;
|
|
455
|
+
if (Math.abs(delta) > 1e-9) for (const edge of laidOut.edges) {
|
|
456
|
+
const points = edge.points;
|
|
457
|
+
if (!points || points.length === 0) continue;
|
|
458
|
+
if (edge.sourceId === node.id) if (horizontal) points[0].y += delta;
|
|
459
|
+
else points[0].x += delta;
|
|
460
|
+
if (edge.targetId === node.id) if (horizontal) points.at(-1).y += delta;
|
|
461
|
+
else points.at(-1).x += delta;
|
|
462
|
+
}
|
|
463
|
+
crossEnd = compactedCross + (horizontal ? node.height : node.width);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const edgeNodeSpacing = getNumberOption(layoutOptions, "spacing.edgeNodeBetweenLayers") ?? 10;
|
|
467
|
+
for (const restoration of hierarchyRestorations) {
|
|
468
|
+
const route = laidOut.edges.find((edge) => edge.id === String(restoration.edge.id))?.points;
|
|
469
|
+
if (!route || route.length < 2) continue;
|
|
470
|
+
const start = route[0];
|
|
471
|
+
const end = route.at(-1);
|
|
472
|
+
if (Math.abs(cross(start) - cross(end)) < 1e-9) {
|
|
473
|
+
route.splice(1, route.length - 2);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const originalSourceId = String(restoration.sources?.[0] ?? restoration.source);
|
|
477
|
+
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
478
|
+
const flowForward = direction === "right" || direction === "down";
|
|
479
|
+
const track = sourceInside ? (horizontal ? end.x : end.y) - (flowForward ? edgeNodeSpacing : -edgeNodeSpacing) : (horizontal ? start.x : start.y) + (flowForward ? edgeNodeSpacing : -edgeNodeSpacing);
|
|
480
|
+
route.splice(1, route.length - 2, horizontal ? {
|
|
481
|
+
x: track,
|
|
482
|
+
y: start.y
|
|
483
|
+
} : {
|
|
484
|
+
x: start.x,
|
|
485
|
+
y: track
|
|
486
|
+
}, horizontal ? {
|
|
487
|
+
x: track,
|
|
488
|
+
y: end.y
|
|
489
|
+
} : {
|
|
490
|
+
x: end.x,
|
|
491
|
+
y: track
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
applyLayout(graph, laidOut, padding, layoutOptions);
|
|
496
|
+
for (const restoration of hierarchyRestorations) {
|
|
497
|
+
restoration.edge.sources = restoration.sources;
|
|
498
|
+
restoration.edge.targets = restoration.targets;
|
|
499
|
+
restoration.edge.source = restoration.source;
|
|
500
|
+
restoration.edge.target = restoration.target;
|
|
501
|
+
for (const section of restoration.edge.sections ?? []) {
|
|
502
|
+
if (section.incomingShape != null && syntheticPortIds.has(String(section.incomingShape))) section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
503
|
+
if (section.outgoingShape != null && syntheticPortIds.has(String(section.outgoingShape))) section.outgoingShape = restoration.targets?.[0] ?? restoration.target;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const [compound, ports] of authoredPortsByCompound) compound.ports = ports;
|
|
507
|
+
for (const [compound, options_] of authoredOptionsByCompound) compound.layoutOptions = options_;
|
|
508
|
+
if (hasHierarchy && topdownLayout) for (const child of graph.children ?? []) {
|
|
509
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
510
|
+
const position = {
|
|
511
|
+
x: child.x,
|
|
512
|
+
y: child.y
|
|
513
|
+
};
|
|
514
|
+
await this.layout(child, {
|
|
515
|
+
...arguments_,
|
|
516
|
+
logging: false,
|
|
517
|
+
measureExecutionTime: false
|
|
518
|
+
});
|
|
519
|
+
child.x = position.x;
|
|
520
|
+
child.y = position.y;
|
|
521
|
+
}
|
|
522
|
+
applyInsideSelfLoops(graph, insideSelfLoopBaseHeightByNodeId);
|
|
177
523
|
if (arguments_.logging || arguments_.measureExecutionTime) graph.logging = {
|
|
178
524
|
name: "Native TypeScript layout",
|
|
179
525
|
children: [{ name: String(algorithm) }],
|
|
@@ -182,7 +528,7 @@ var ELK = class {
|
|
|
182
528
|
return graph;
|
|
183
529
|
}
|
|
184
530
|
};
|
|
185
|
-
function parsePadding(value) {
|
|
531
|
+
function parsePadding(value, fallback = 0) {
|
|
186
532
|
if (typeof value === "number") return {
|
|
187
533
|
top: value,
|
|
188
534
|
right: value,
|
|
@@ -190,10 +536,10 @@ function parsePadding(value) {
|
|
|
190
536
|
left: value
|
|
191
537
|
};
|
|
192
538
|
if (typeof value !== "string") return {
|
|
193
|
-
top:
|
|
194
|
-
right:
|
|
195
|
-
bottom:
|
|
196
|
-
left:
|
|
539
|
+
top: fallback,
|
|
540
|
+
right: fallback,
|
|
541
|
+
bottom: fallback,
|
|
542
|
+
left: fallback
|
|
197
543
|
};
|
|
198
544
|
const padding = {
|
|
199
545
|
top: 0,
|
|
@@ -207,6 +553,129 @@ function parsePadding(value) {
|
|
|
207
553
|
}
|
|
208
554
|
return padding;
|
|
209
555
|
}
|
|
556
|
+
function parseVector(value) {
|
|
557
|
+
if (typeof value === "object" && value !== null && "x" in value && "y" in value) return {
|
|
558
|
+
x: Number(value.x),
|
|
559
|
+
y: Number(value.y)
|
|
560
|
+
};
|
|
561
|
+
if (typeof value !== "string") return void 0;
|
|
562
|
+
const match = value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/);
|
|
563
|
+
return match ? {
|
|
564
|
+
x: Number(match[1]),
|
|
565
|
+
y: Number(match[2])
|
|
566
|
+
} : void 0;
|
|
567
|
+
}
|
|
568
|
+
function applyNodeMicroLayout(graph, globalOptions) {
|
|
569
|
+
const labelPadding = parsePadding(getOption(globalOptions, "nodeLabels.padding"), 5);
|
|
570
|
+
for (const node of graph.children ?? []) {
|
|
571
|
+
const constraints = String(getOption(node.layoutOptions ?? {}, "nodeSize.constraints") ?? "");
|
|
572
|
+
if (!constraints) continue;
|
|
573
|
+
const configuredSizeOptions = getOption(node.layoutOptions ?? {}, "nodeSize.options");
|
|
574
|
+
const sizeOptions = new Set(configuredSizeOptions === void 0 ? ["DEFAULT_MINIMUM_SIZE"] : String(configuredSizeOptions).split(/[\s,;]+/).filter(Boolean));
|
|
575
|
+
const effectivelyFixedPortLabelSize = constraints.includes("PORT_LABELS") && !constraints.includes("NODE_LABELS") && !constraints.includes("MINIMUM_SIZE");
|
|
576
|
+
let width = effectivelyFixedPortLabelSize ? node.width ?? 0 : 0;
|
|
577
|
+
let height = effectivelyFixedPortLabelSize ? node.height ?? 0 : 0;
|
|
578
|
+
let insideHorizontalInset = 0;
|
|
579
|
+
let insideVerticalInset = 0;
|
|
580
|
+
const insideLabelCells = /* @__PURE__ */ new Map();
|
|
581
|
+
if (constraints.includes("PORTS")) {
|
|
582
|
+
const spacing = getNumberOption(globalOptions, "spacing.portPort") ?? 10;
|
|
583
|
+
const sideCounts = {
|
|
584
|
+
NORTH: 0,
|
|
585
|
+
EAST: 0,
|
|
586
|
+
SOUTH: 0,
|
|
587
|
+
WEST: 0
|
|
588
|
+
};
|
|
589
|
+
for (const port of node.ports ?? []) {
|
|
590
|
+
const side = String(getOption(port.layoutOptions ?? {}, "port.side") ?? "EAST");
|
|
591
|
+
if (side in sideCounts) sideCounts[side]++;
|
|
592
|
+
}
|
|
593
|
+
const nodeId = String(node.id);
|
|
594
|
+
for (const edge of graph.edges ?? []) {
|
|
595
|
+
const source = String(edge.sources?.[0] ?? edge.source);
|
|
596
|
+
const target = String(edge.targets?.[0] ?? edge.target);
|
|
597
|
+
if (source === nodeId) sideCounts.EAST++;
|
|
598
|
+
if (target === nodeId) sideCounts.WEST++;
|
|
599
|
+
}
|
|
600
|
+
width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
|
|
601
|
+
height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
|
|
602
|
+
if (sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
|
|
603
|
+
if (sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
|
|
604
|
+
}
|
|
605
|
+
if (constraints.includes("NODE_LABELS")) {
|
|
606
|
+
for (const label of node.labels ?? []) {
|
|
607
|
+
if (!label.text) continue;
|
|
608
|
+
const placement = String(getOption({
|
|
609
|
+
...globalOptions,
|
|
610
|
+
...node.layoutOptions,
|
|
611
|
+
...label.layoutOptions
|
|
612
|
+
}, "nodeLabels.placement") ?? "");
|
|
613
|
+
if (!placement) continue;
|
|
614
|
+
const labelWidth = label.width ?? 0;
|
|
615
|
+
const labelHeight = label.height ?? 0;
|
|
616
|
+
if (placement.includes("OUTSIDE")) {
|
|
617
|
+
if (!sizeOptions.has("OUTSIDE_NODE_LABELS_OVERHANG")) width = Math.max(width, labelWidth);
|
|
618
|
+
} else {
|
|
619
|
+
width = Math.max(width, labelWidth + labelPadding.left + labelPadding.right);
|
|
620
|
+
const verticalInset = labelHeight * (placement.includes("V_CENTER") ? 1 : 2) + labelPadding.top + labelPadding.bottom;
|
|
621
|
+
height = Math.max(height, verticalInset);
|
|
622
|
+
insideHorizontalInset = Math.max(insideHorizontalInset, labelPadding.left + labelPadding.right);
|
|
623
|
+
insideVerticalInset = Math.max(insideVerticalInset, verticalInset);
|
|
624
|
+
const key = `${placement.includes("V_CENTER") ? "center" : placement.includes("V_BOTTOM") ? "bottom" : "top"}:${placement.includes("H_CENTER") ? "center" : placement.includes("H_RIGHT") ? "right" : "left"}`;
|
|
625
|
+
const cell = insideLabelCells.get(key) ?? {
|
|
626
|
+
width: 0,
|
|
627
|
+
height: 0
|
|
628
|
+
};
|
|
629
|
+
cell.width = Math.max(cell.width, labelWidth);
|
|
630
|
+
cell.height += labelHeight;
|
|
631
|
+
insideLabelCells.set(key, cell);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (insideLabelCells.size > 0) {
|
|
635
|
+
const rows = [
|
|
636
|
+
"top",
|
|
637
|
+
"center",
|
|
638
|
+
"bottom"
|
|
639
|
+
];
|
|
640
|
+
const columns = [
|
|
641
|
+
"left",
|
|
642
|
+
"center",
|
|
643
|
+
"right"
|
|
644
|
+
];
|
|
645
|
+
const cellWidth = (row, column) => insideLabelCells.get(`${row}:${column}`)?.width ?? 0;
|
|
646
|
+
const forceTabular = sizeOptions.has("FORCE_TABULAR_NODE_LABELS");
|
|
647
|
+
const asymmetrical = sizeOptions.has("ASYMMETRICAL");
|
|
648
|
+
const globalColumns = columns.map((column) => Math.max(...rows.map((row) => cellWidth(row, column))));
|
|
649
|
+
const labelGridWidth = forceTabular ? globalColumns.reduce((sum, value) => sum + value, 0) : Math.max(...rows.map((row) => {
|
|
650
|
+
const left = cellWidth(row, "left");
|
|
651
|
+
const center = cellWidth(row, "center");
|
|
652
|
+
const right = cellWidth(row, "right");
|
|
653
|
+
return asymmetrical ? left + center + right : 2 * Math.max(left, right) + center;
|
|
654
|
+
}));
|
|
655
|
+
const labelGridHeight = rows.reduce((sum, row) => sum + Math.max(...columns.map((column) => insideLabelCells.get(`${row}:${column}`)?.height ?? 0)), 0);
|
|
656
|
+
width = Math.max(width, labelGridWidth + labelPadding.left + labelPadding.right);
|
|
657
|
+
height = Math.max(height, labelGridHeight + labelPadding.top + labelPadding.bottom);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (constraints.includes("MINIMUM_SIZE")) {
|
|
661
|
+
const configuredMinimum = parseVector(getOption(node.layoutOptions ?? {}, "nodeSize.minimum"));
|
|
662
|
+
const minimum = {
|
|
663
|
+
x: configuredMinimum?.x && configuredMinimum.x > 0 ? configuredMinimum.x : sizeOptions.has("DEFAULT_MINIMUM_SIZE") ? 20 : 0,
|
|
664
|
+
y: configuredMinimum?.y && configuredMinimum.y > 0 ? configuredMinimum.y : sizeOptions.has("DEFAULT_MINIMUM_SIZE") ? 20 : 0
|
|
665
|
+
};
|
|
666
|
+
if (sizeOptions.has("MINIMUM_SIZE_ACCOUNTS_FOR_PADDING")) {
|
|
667
|
+
width = Math.max(width, minimum.x + insideHorizontalInset);
|
|
668
|
+
height = Math.max(height, minimum.y + insideVerticalInset);
|
|
669
|
+
} else {
|
|
670
|
+
width = Math.max(width, minimum.x);
|
|
671
|
+
height = Math.max(height, minimum.y);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
sizeOptions.has("COMPUTE_PADDING");
|
|
675
|
+
node.width = width;
|
|
676
|
+
node.height = height;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
210
679
|
function getOption(options, suffix) {
|
|
211
680
|
const exactKeys = [
|
|
212
681
|
suffix,
|
|
@@ -214,7 +683,83 @@ function getOption(options, suffix) {
|
|
|
214
683
|
`org.eclipse.elk.${suffix}`
|
|
215
684
|
];
|
|
216
685
|
for (const key of exactKeys) if (options[key] !== void 0) return options[key];
|
|
217
|
-
|
|
686
|
+
}
|
|
687
|
+
const ergonomicallyMappedLayeredSettings = new Set([
|
|
688
|
+
"direction",
|
|
689
|
+
"padding",
|
|
690
|
+
"spacing.node",
|
|
691
|
+
"spacing.layer"
|
|
692
|
+
]);
|
|
693
|
+
function coerceLayeredOptionValue(value, type) {
|
|
694
|
+
if (type === "BOOLEAN") {
|
|
695
|
+
if (typeof value === "boolean") return value;
|
|
696
|
+
if (typeof value === "string") return value.toLowerCase() === "true";
|
|
697
|
+
}
|
|
698
|
+
if (type === "DOUBLE" || type === "INT") {
|
|
699
|
+
if (typeof value === "number") return value;
|
|
700
|
+
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
|
701
|
+
}
|
|
702
|
+
return value;
|
|
703
|
+
}
|
|
704
|
+
function parseIndividualSpacing(value) {
|
|
705
|
+
if (typeof value === "object" && value !== null) return value;
|
|
706
|
+
if (typeof value !== "string") return value;
|
|
707
|
+
const result = {};
|
|
708
|
+
for (const entry of value.split(/;,;|;/)) {
|
|
709
|
+
const match = entry.match(/^\s*(?:org\.eclipse\.elk\.)?(?:layered\.)?([^:]+)\s*:\s*(-?\d+(?:\.\d+)?)\s*$/);
|
|
710
|
+
if (!match) continue;
|
|
711
|
+
const sourceName = match[1];
|
|
712
|
+
const definition = elkLayeredOptionDefinitions.find((candidate) => {
|
|
713
|
+
const suffix = candidate.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
714
|
+
return candidate.name === sourceName || suffix === sourceName;
|
|
715
|
+
});
|
|
716
|
+
if (definition) result[definition.name] = Number(match[2]);
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
function parseMargin(value) {
|
|
721
|
+
if (typeof value === "object" && value !== null) return value;
|
|
722
|
+
if (typeof value !== "string") return value;
|
|
723
|
+
return parsePadding(value, 0);
|
|
724
|
+
}
|
|
725
|
+
function getElementLayeredSettings(options) {
|
|
726
|
+
const settings = {};
|
|
727
|
+
for (const definition of elkLayeredOptionDefinitions) {
|
|
728
|
+
const suffix = definition.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
729
|
+
const value = [
|
|
730
|
+
definition.name,
|
|
731
|
+
suffix,
|
|
732
|
+
`elk.${suffix}`,
|
|
733
|
+
definition.elkId
|
|
734
|
+
].map((key) => options[key]).find((candidate) => candidate !== void 0);
|
|
735
|
+
if (value === void 0) continue;
|
|
736
|
+
const vectorMatch = definition.name === "port.anchor" && typeof value === "string" ? value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/) : void 0;
|
|
737
|
+
settings[definition.name] = vectorMatch ? {
|
|
738
|
+
x: Number(vectorMatch[1]),
|
|
739
|
+
y: Number(vectorMatch[2])
|
|
740
|
+
} : definition.name === "spacing.individual" ? parseIndividualSpacing(value) : definition.name === "spacing.portsSurrounding" ? parseMargin(value) : coerceLayeredOptionValue(value, definition.type);
|
|
741
|
+
}
|
|
742
|
+
return settings;
|
|
743
|
+
}
|
|
744
|
+
function getLayeredSettings(options) {
|
|
745
|
+
const settings = getElementLayeredSettings(options);
|
|
746
|
+
const baseValue = settings["spacing.baseValue"];
|
|
747
|
+
if (baseValue !== void 0) for (const [name, defaultValue] of [
|
|
748
|
+
["spacing.componentComponent", 20],
|
|
749
|
+
["spacing.edgeEdge", 10],
|
|
750
|
+
["spacing.edgeLabel", 2],
|
|
751
|
+
["spacing.edgeNode", 10],
|
|
752
|
+
["spacing.labelLabel", 0],
|
|
753
|
+
["spacing.labelNode", 5],
|
|
754
|
+
["spacing.labelPortHorizontal", 1],
|
|
755
|
+
["spacing.labelPortVertical", 1],
|
|
756
|
+
["spacing.nodeSelfLoop", 10],
|
|
757
|
+
["spacing.portPort", 10],
|
|
758
|
+
["spacing.edgeEdgeBetweenLayers", 10],
|
|
759
|
+
["spacing.edgeNodeBetweenLayers", 10]
|
|
760
|
+
]) settings[name] ??= baseValue * defaultValue / 20;
|
|
761
|
+
for (const name of ergonomicallyMappedLayeredSettings) delete settings[name];
|
|
762
|
+
return settings;
|
|
218
763
|
}
|
|
219
764
|
function getNumberOption(options, suffix) {
|
|
220
765
|
const value = getOption(options, suffix);
|
|
@@ -233,8 +778,8 @@ function getBooleanOption(options, suffix) {
|
|
|
233
778
|
}
|
|
234
779
|
}
|
|
235
780
|
function getDirection(options) {
|
|
236
|
-
const direction = String(getOption(options, "direction") ?? "
|
|
237
|
-
return direction === "up" || direction === "left" || direction === "
|
|
781
|
+
const direction = String(getOption(options, "direction") ?? "RIGHT").toLowerCase();
|
|
782
|
+
return direction === "up" || direction === "left" || direction === "down" ? direction : "right";
|
|
238
783
|
}
|
|
239
784
|
function endpoint(value, portOwnerById) {
|
|
240
785
|
const id = String(value);
|
|
@@ -244,11 +789,89 @@ function endpoint(value, portOwnerById) {
|
|
|
244
789
|
port: id
|
|
245
790
|
};
|
|
246
791
|
}
|
|
247
|
-
function
|
|
248
|
-
const
|
|
792
|
+
function isInsideSelfLoop(root, edge, expectedNodeId) {
|
|
793
|
+
const source = String(edge.sources?.[0] ?? edge.source);
|
|
794
|
+
if (source !== String(edge.targets?.[0] ?? edge.target) || expectedNodeId !== void 0 && source !== expectedNodeId) return false;
|
|
795
|
+
const node = root.children?.find((child) => String(child.id) === source);
|
|
796
|
+
return getBooleanOption(node?.layoutOptions ?? {}, "insideSelfLoops.activate") === true && getBooleanOption(edge.layoutOptions ?? {}, "insideSelfLoops.yo") === true;
|
|
797
|
+
}
|
|
798
|
+
function applyInsideSelfLoops(root, baseHeightByNodeId) {
|
|
799
|
+
const indexByNodeId = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const edge of root.edges ?? []) {
|
|
801
|
+
if (!isInsideSelfLoop(root, edge)) continue;
|
|
802
|
+
const nodeId = String(edge.sources?.[0] ?? edge.source);
|
|
803
|
+
const node = root.children?.find((child) => String(child.id) === nodeId);
|
|
804
|
+
const baseHeight = baseHeightByNodeId.get(nodeId);
|
|
805
|
+
if (!node || baseHeight === void 0) continue;
|
|
806
|
+
const index = indexByNodeId.get(nodeId) ?? 0;
|
|
807
|
+
indexByNodeId.set(nodeId, index + 1);
|
|
808
|
+
const y = (node.y ?? 0) + baseHeight - 2 + index * 11;
|
|
809
|
+
edge.sections = [{
|
|
810
|
+
id: `${String(edge.id)}_s0`,
|
|
811
|
+
startPoint: {
|
|
812
|
+
x: node.x ?? 0,
|
|
813
|
+
y
|
|
814
|
+
},
|
|
815
|
+
endPoint: {
|
|
816
|
+
x: (node.x ?? 0) + (node.width ?? 0),
|
|
817
|
+
y
|
|
818
|
+
},
|
|
819
|
+
incomingShape: edge.sources?.[0] ?? edge.source,
|
|
820
|
+
outgoingShape: edge.targets?.[0] ?? edge.target
|
|
821
|
+
}];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function toGraph(root, globalOptions = {}) {
|
|
825
|
+
const children = (root.children ?? []).filter((child) => getBooleanOption(child.layoutOptions ?? {}, "noLayout") !== true);
|
|
249
826
|
const nodeIds = new Set(children.map((child) => String(child.id)));
|
|
250
827
|
const portOwnerById = /* @__PURE__ */ new Map();
|
|
251
828
|
for (const child of children) for (const port of child.ports ?? []) if (port.id !== void 0) portOwnerById.set(String(port.id), String(child.id));
|
|
829
|
+
const sourcePortIds = new Set((root.edges ?? []).flatMap((edge) => (edge.sources ?? (edge.source === void 0 ? [] : [edge.source])).map(String)));
|
|
830
|
+
const targetPortIds = new Set((root.edges ?? []).flatMap((edge) => (edge.targets ?? (edge.target === void 0 ? [] : [edge.target])).map(String)));
|
|
831
|
+
const sourcePortDegree = /* @__PURE__ */ new Map();
|
|
832
|
+
const targetPortDegree = /* @__PURE__ */ new Map();
|
|
833
|
+
const edgeModelOrderByPortId = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const [edgeIndex, edge] of (root.edges ?? []).entries()) {
|
|
835
|
+
for (const source of edge.sources ?? (edge.source === void 0 ? [] : [edge.source])) {
|
|
836
|
+
const id = String(source);
|
|
837
|
+
sourcePortDegree.set(id, (sourcePortDegree.get(id) ?? 0) + 1);
|
|
838
|
+
if (portOwnerById.has(id) && !edgeModelOrderByPortId.has(id)) edgeModelOrderByPortId.set(id, edgeIndex);
|
|
839
|
+
}
|
|
840
|
+
for (const target of edge.targets ?? (edge.target === void 0 ? [] : [edge.target])) {
|
|
841
|
+
const id = String(target);
|
|
842
|
+
targetPortDegree.set(id, (targetPortDegree.get(id) ?? 0) + 1);
|
|
843
|
+
if (portOwnerById.has(id) && !edgeModelOrderByPortId.has(id)) edgeModelOrderByPortId.set(id, edgeIndex);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const orderedPorts = (child) => {
|
|
847
|
+
const ports = [...child.ports ?? []];
|
|
848
|
+
if (String(getOption(child.layoutOptions ?? {}, "portConstraints")) !== "FIXED_SIDE") return ports;
|
|
849
|
+
const side = (port) => String(getOption(port.layoutOptions ?? {}, "port.side"));
|
|
850
|
+
const sideOrder = [
|
|
851
|
+
"NORTH",
|
|
852
|
+
"EAST",
|
|
853
|
+
"SOUTH",
|
|
854
|
+
"WEST"
|
|
855
|
+
];
|
|
856
|
+
ports.sort((left, right) => {
|
|
857
|
+
const leftSide = side(left);
|
|
858
|
+
const rightSide = side(right);
|
|
859
|
+
const sideDifference = sideOrder.indexOf(leftSide) - sideOrder.indexOf(rightSide);
|
|
860
|
+
if (sideDifference !== 0) return sideDifference;
|
|
861
|
+
if (String(getOption(globalOptions, "layered.portSortingStrategy")) === "PORT_DEGREE") {
|
|
862
|
+
if (leftSide === "EAST") return (sourcePortDegree.get(String(right.id)) ?? 0) - (sourcePortDegree.get(String(left.id)) ?? 0);
|
|
863
|
+
if (leftSide === "WEST") return (targetPortDegree.get(String(left.id)) ?? 0) - (targetPortDegree.get(String(right.id)) ?? 0);
|
|
864
|
+
}
|
|
865
|
+
const modelOrderStrategy = String(getOption(globalOptions, "layered.considerModelOrder.strategy") ?? "NONE");
|
|
866
|
+
const usePortModelOrder = getBooleanOption(globalOptions, "layered.considerModelOrder.portModelOrder") === true;
|
|
867
|
+
if (modelOrderStrategy !== "NONE" && !usePortModelOrder) {
|
|
868
|
+
const edgeOrderDifference = (edgeModelOrderByPortId.get(String(left.id)) ?? Infinity) - (edgeModelOrderByPortId.get(String(right.id)) ?? Infinity);
|
|
869
|
+
if (edgeOrderDifference !== 0) return edgeOrderDifference;
|
|
870
|
+
}
|
|
871
|
+
return (leftSide === "WEST" ? -1 : 1) * ((child.ports?.indexOf(left) ?? 0) - (child.ports?.indexOf(right) ?? 0));
|
|
872
|
+
});
|
|
873
|
+
return ports;
|
|
874
|
+
};
|
|
252
875
|
return createGraph({
|
|
253
876
|
id: String(root.id),
|
|
254
877
|
nodes: children.map((child) => ({
|
|
@@ -259,9 +882,9 @@ function toGraph(root) {
|
|
|
259
882
|
width: child.width,
|
|
260
883
|
height: child.height,
|
|
261
884
|
label: child.labels?.[0]?.text,
|
|
262
|
-
ports: child.
|
|
885
|
+
ports: orderedPorts(child).filter((port) => getBooleanOption(port.layoutOptions ?? {}, "noLayout") !== true).map((port) => ({
|
|
263
886
|
name: String(port.id),
|
|
264
|
-
direction: "inout",
|
|
887
|
+
direction: sourcePortIds.has(String(port.id)) ? targetPortIds.has(String(port.id)) ? "inout" : "out" : targetPortIds.has(String(port.id)) ? "in" : "inout",
|
|
265
888
|
x: port.x,
|
|
266
889
|
y: port.y,
|
|
267
890
|
width: port.width,
|
|
@@ -269,8 +892,14 @@ function toGraph(root) {
|
|
|
269
892
|
}))
|
|
270
893
|
})),
|
|
271
894
|
edges: (root.edges ?? []).flatMap((edge) => {
|
|
895
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true) return [];
|
|
896
|
+
if (isInsideSelfLoop(root, edge)) return [];
|
|
272
897
|
const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
|
|
273
898
|
const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
|
|
899
|
+
const labels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
|
|
900
|
+
const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
901
|
+
const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
|
|
902
|
+
const labelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelLabelSpacing;
|
|
274
903
|
return nodeIds.has(source.nodeId) && nodeIds.has(target.nodeId) ? [{
|
|
275
904
|
id: String(edge.id),
|
|
276
905
|
sourceId: source.nodeId,
|
|
@@ -278,8 +907,8 @@ function toGraph(root) {
|
|
|
278
907
|
sourcePort: source.port,
|
|
279
908
|
targetPort: target.port,
|
|
280
909
|
label: edge.labels?.[0]?.text,
|
|
281
|
-
width:
|
|
282
|
-
height:
|
|
910
|
+
width: labelWidth,
|
|
911
|
+
height: labelHeight,
|
|
283
912
|
points: parsePoints(getOption(edge.layoutOptions ?? {}, "bendPoints"))
|
|
284
913
|
}] : [];
|
|
285
914
|
})
|
|
@@ -309,6 +938,8 @@ function toSection(edge, points) {
|
|
|
309
938
|
id: `${String(edge.id)}_s0`,
|
|
310
939
|
startPoint: { ...startPoint },
|
|
311
940
|
endPoint: { ...endPoint },
|
|
941
|
+
incomingShape: edge.sources?.[0] ?? edge.source,
|
|
942
|
+
outgoingShape: edge.targets?.[0] ?? edge.target,
|
|
312
943
|
...points.length > 2 ? { bendPoints: points.slice(1, -1).map((point) => ({ ...point })) } : {}
|
|
313
944
|
};
|
|
314
945
|
}
|
|
@@ -324,6 +955,7 @@ function applyLayout(root, graph, padding, layoutOptions) {
|
|
|
324
955
|
child.height = node.height;
|
|
325
956
|
placeNodeLabels(child, layoutOptions);
|
|
326
957
|
for (const port of child.ports ?? []) {
|
|
958
|
+
if (getBooleanOption(port.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
327
959
|
const laidOutPort = node.ports?.find((candidate) => candidate.name === String(port.id));
|
|
328
960
|
if (!laidOutPort) continue;
|
|
329
961
|
port.x = laidOutPort.x;
|
|
@@ -331,26 +963,147 @@ function applyLayout(root, graph, padding, layoutOptions) {
|
|
|
331
963
|
port.width = laidOutPort.width;
|
|
332
964
|
port.height = laidOutPort.height;
|
|
333
965
|
}
|
|
966
|
+
placePortLabels(child, layoutOptions);
|
|
334
967
|
}
|
|
335
968
|
for (const edge of root.edges ?? []) {
|
|
336
969
|
const laidOutEdge = edgeById.get(String(edge.id));
|
|
337
970
|
if (!laidOutEdge) {
|
|
971
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true) {
|
|
972
|
+
for (const section$2 of edge.sections ?? []) {
|
|
973
|
+
section$2.incomingShape ??= edge.sources?.[0] ?? edge.source;
|
|
974
|
+
section$2.outgoingShape ??= edge.targets?.[0] ?? edge.target;
|
|
975
|
+
}
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
338
978
|
const section$1 = getParentEdgeSection(root, edge);
|
|
339
979
|
if (section$1) edge.sections = [section$1];
|
|
340
980
|
continue;
|
|
341
981
|
}
|
|
342
982
|
const section = toSection(edge, laidOutEdge.points ?? []);
|
|
343
983
|
edge.sections = section ? [section] : [];
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
984
|
+
const target = root.children?.find((child) => String(child.id) === String(edge.targets?.[0] ?? edge.target));
|
|
985
|
+
if (getBooleanOption(target?.layoutOptions ?? {}, "hypernode") === true && (section?.bendPoints?.length ?? 0) > 0) edge.junctionPoints = [section.bendPoints.at(-1)];
|
|
986
|
+
let labelY = laidOutEdge.y ?? 0;
|
|
987
|
+
const points = laidOutEdge.points ?? [];
|
|
988
|
+
const firstPoint = points[0] ?? {
|
|
989
|
+
x: laidOutEdge.x ?? 0,
|
|
990
|
+
y: laidOutEdge.y ?? 0
|
|
991
|
+
};
|
|
992
|
+
const lastPoint = points.at(-1) ?? firstPoint;
|
|
993
|
+
const midpointX = (laidOutEdge.x ?? 0) + (laidOutEdge.width ?? 0) / 2;
|
|
994
|
+
const edgeLabelSpacing = getNumberOption(layoutOptions, "spacing.edgeLabel") ?? 2;
|
|
995
|
+
const labelLabelSpacing = getNumberOption(layoutOptions, "spacing.labelLabel") ?? 0;
|
|
996
|
+
for (const label of edge.labels ?? []) {
|
|
997
|
+
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) {
|
|
998
|
+
label.x ??= 0;
|
|
999
|
+
label.y ??= 0;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (!label.text) {
|
|
1003
|
+
label.x ??= 0;
|
|
1004
|
+
label.y ??= 0;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const placement = String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER");
|
|
1008
|
+
const width = label.width ?? 0;
|
|
1009
|
+
label.x = placement === "TAIL" ? firstPoint.x + edgeLabelSpacing : placement === "HEAD" ? lastPoint.x - width - edgeLabelSpacing : midpointX - width / 2;
|
|
1010
|
+
label.y = labelY;
|
|
1011
|
+
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
normalizeElkGraphBounds(root, padding, layoutOptions);
|
|
1015
|
+
}
|
|
1016
|
+
function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
|
|
1017
|
+
const authoredWidth = root.width;
|
|
1018
|
+
const authoredHeight = root.height;
|
|
1019
|
+
const fixedGraphSize = getBooleanOption(layoutOptions, "nodeSize.fixedGraphSize") === true;
|
|
1020
|
+
const layoutChildren = (root.children ?? []).filter((node) => getBooleanOption(node.layoutOptions ?? {}, "noLayout") !== true);
|
|
1021
|
+
let minimumX = Number.POSITIVE_INFINITY;
|
|
1022
|
+
let minimumY = Number.POSITIVE_INFINITY;
|
|
1023
|
+
for (const node of layoutChildren) {
|
|
1024
|
+
minimumX = Math.min(minimumX, node.x ?? 0, ...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0)), ...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0))));
|
|
1025
|
+
minimumY = Math.min(minimumY, node.y ?? 0, ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0)), ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0))));
|
|
1026
|
+
}
|
|
1027
|
+
for (const edge of root.edges ?? []) {
|
|
1028
|
+
const laidOutLabels = (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
|
|
1029
|
+
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1030
|
+
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
1031
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
|
|
1032
|
+
const points = (edge.sections ?? []).flatMap((section) => [
|
|
1033
|
+
section.startPoint,
|
|
1034
|
+
...section.bendPoints ?? [],
|
|
1035
|
+
section.endPoint
|
|
1036
|
+
]);
|
|
1037
|
+
minimumX = Math.min(minimumX, ...points.map((point) => point.x));
|
|
1038
|
+
minimumY = Math.min(minimumY, ...points.map((point) => point.y));
|
|
350
1039
|
}
|
|
351
1040
|
}
|
|
352
|
-
|
|
353
|
-
|
|
1041
|
+
const shiftX = Number.isFinite(minimumX) ? Math.max(0, padding.left - minimumX) : 0;
|
|
1042
|
+
const shiftY = Number.isFinite(minimumY) ? Math.max(0, padding.top - minimumY) : 0;
|
|
1043
|
+
if (shiftX !== 0 || shiftY !== 0) {
|
|
1044
|
+
for (const node of layoutChildren) {
|
|
1045
|
+
node.x = (node.x ?? 0) + shiftX;
|
|
1046
|
+
node.y = (node.y ?? 0) + shiftY;
|
|
1047
|
+
}
|
|
1048
|
+
for (const edge of root.edges ?? []) {
|
|
1049
|
+
for (const section of edge.sections ?? []) for (const point of [
|
|
1050
|
+
section.startPoint,
|
|
1051
|
+
...section.bendPoints ?? [],
|
|
1052
|
+
section.endPoint
|
|
1053
|
+
]) {
|
|
1054
|
+
point.x += shiftX;
|
|
1055
|
+
point.y += shiftY;
|
|
1056
|
+
}
|
|
1057
|
+
for (const label of (edge.labels ?? []).filter((candidate) => Boolean(candidate.text))) {
|
|
1058
|
+
label.x = (label.x ?? 0) + shiftX;
|
|
1059
|
+
label.y = (label.y ?? 0) + shiftY;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const maximumNodeX = Math.max(0, ...layoutChildren.map((node) => (node.x ?? 0) + (node.width ?? 0)));
|
|
1064
|
+
const maximumNodeY = Math.max(0, ...layoutChildren.map((node) => (node.y ?? 0) + (node.height ?? 0)));
|
|
1065
|
+
const layoutEdgePoints = (root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
1066
|
+
section.startPoint,
|
|
1067
|
+
...section.bendPoints ?? [],
|
|
1068
|
+
section.endPoint
|
|
1069
|
+
]));
|
|
1070
|
+
const wrappingStrategy = String(getOption(layoutOptions, "layered.wrapping.strategy") ?? "OFF");
|
|
1071
|
+
const direction = getDirection(layoutOptions);
|
|
1072
|
+
const laidOutChildById = new Map((root.children ?? []).map((child) => [String(child.id), child]));
|
|
1073
|
+
const wrappedEdgeCount = (root.edges ?? []).filter((edge) => {
|
|
1074
|
+
const source = laidOutChildById.get(String(edge.sources?.[0] ?? edge.source));
|
|
1075
|
+
const target = laidOutChildById.get(String(edge.targets?.[0] ?? edge.target));
|
|
1076
|
+
if (!source || !target) return false;
|
|
1077
|
+
return direction === "right" ? (source.x ?? 0) > (target.x ?? 0) : direction === "left" ? (source.x ?? 0) < (target.x ?? 0) : direction === "down" ? (source.y ?? 0) > (target.y ?? 0) : (source.y ?? 0) < (target.y ?? 0);
|
|
1078
|
+
}).length;
|
|
1079
|
+
const addBoundaryPixel = wrappingStrategy !== "MULTI_EDGE" && !(wrappingStrategy === "SINGLE_EDGE" && wrappedEdgeCount > 0) && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") === "NONE" && getBooleanOption(layoutOptions, "layered.feedbackEdges") !== true && String(getOption(layoutOptions, "layered.layering.nodePromotion.strategy") ?? "NONE") !== "MODEL_ORDER_LEFT_TO_RIGHT" && !(root.edges ?? []).some((edge) => edge.sources?.[0] === edge.targets?.[0]) && !(root.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
1080
|
+
const edgeBoundsExtraX = addBoundaryPixel && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9 ? 1 : 0;
|
|
1081
|
+
const edgeBoundsExtraY = addBoundaryPixel && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
|
|
1082
|
+
const singleMultiEdgeCutBoundsExtraY = wrappingStrategy === "MULTI_EDGE" && wrappedEdgeCount === 1 && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
|
|
1083
|
+
const postCompactionBoundsExtraX = !addBoundaryPixel && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) > maximumNodeX + 1e-9 ? .04 : 0;
|
|
1084
|
+
const postCompactionBoundsExtraY = !addBoundaryPixel && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) > maximumNodeY + 1e-9 ? .04 : 0;
|
|
1085
|
+
const calculatedWidth = Math.max(0, ...layoutChildren.flatMap((node) => [
|
|
1086
|
+
(node.x ?? 0) + (node.width ?? 0),
|
|
1087
|
+
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
1088
|
+
...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
|
|
1089
|
+
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
|
|
1090
|
+
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true).map((label) => (label.x ?? 0) + (label.width ?? 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
1091
|
+
section.startPoint.x,
|
|
1092
|
+
...(section.bendPoints ?? []).map((point) => point.x),
|
|
1093
|
+
section.endPoint.x
|
|
1094
|
+
]))) + padding.right + edgeBoundsExtraX + postCompactionBoundsExtraX + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up") ? 1 : 0);
|
|
1095
|
+
const calculatedHeight = Math.max(0, ...layoutChildren.flatMap((node) => [
|
|
1096
|
+
(node.y ?? 0) + (node.height ?? 0),
|
|
1097
|
+
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
1098
|
+
...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
|
|
1099
|
+
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
|
|
1100
|
+
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true).map((label) => (label.y ?? 0) + (label.height ?? 0) + (String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") === "CENTER" ? 1 : 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
1101
|
+
section.startPoint.y,
|
|
1102
|
+
...(section.bendPoints ?? []).map((point) => point.y),
|
|
1103
|
+
section.endPoint.y
|
|
1104
|
+
]))) + padding.bottom + edgeBoundsExtraY + singleMultiEdgeCutBoundsExtraY + postCompactionBoundsExtraY + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "right" || getDirection(layoutOptions) === "left") ? 1 : 0);
|
|
1105
|
+
root.width = fixedGraphSize ? authoredWidth ?? 0 : calculatedWidth;
|
|
1106
|
+
root.height = fixedGraphSize ? authoredHeight ?? 0 : calculatedHeight;
|
|
354
1107
|
}
|
|
355
1108
|
function getParentEdgeSection(root, edge) {
|
|
356
1109
|
const sourceId = String(edge.sources?.[0] ?? edge.source);
|
|
@@ -388,7 +1141,10 @@ function getParentEdgeSection(root, edge) {
|
|
|
388
1141
|
}
|
|
389
1142
|
}
|
|
390
1143
|
function placeNodeLabels(node, globalOptions) {
|
|
1144
|
+
const labelPadding = parsePadding(getOption(globalOptions, "nodeLabels.padding"), 5);
|
|
391
1145
|
for (const label of node.labels ?? []) {
|
|
1146
|
+
if (!label.text) continue;
|
|
1147
|
+
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
392
1148
|
const placement = String(getOption({
|
|
393
1149
|
...globalOptions,
|
|
394
1150
|
...node.layoutOptions,
|
|
@@ -399,12 +1155,60 @@ function placeNodeLabels(node, globalOptions) {
|
|
|
399
1155
|
const height = label.height ?? 0;
|
|
400
1156
|
const nodeWidth = node.width ?? 0;
|
|
401
1157
|
const nodeHeight = node.height ?? 0;
|
|
402
|
-
label.x = placement.includes("H_CENTER") ? (nodeWidth - width) / 2 : placement.includes("H_RIGHT") ? nodeWidth - width :
|
|
403
|
-
if (placement.includes("OUTSIDE") && placement.includes("V_TOP")) label.y = -height - 5;
|
|
404
|
-
else if (placement.includes("OUTSIDE") && placement.includes("V_BOTTOM")) label.y = nodeHeight + 5;
|
|
405
|
-
else if (placement.includes("V_CENTER")) label.y = (nodeHeight - height) / 2;
|
|
406
|
-
else if (placement.includes("V_BOTTOM")) label.y = nodeHeight - height;
|
|
407
|
-
else label.y =
|
|
1158
|
+
label.x = placement.includes("H_CENTER") ? (nodeWidth - width + labelPadding.left - labelPadding.right) / 2 : placement.includes("H_RIGHT") ? nodeWidth - width - labelPadding.right : labelPadding.left;
|
|
1159
|
+
if (placement.includes("OUTSIDE") && placement.includes("V_TOP")) label.y = -height - Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
|
|
1160
|
+
else if (placement.includes("OUTSIDE") && placement.includes("V_BOTTOM")) label.y = nodeHeight + Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
|
|
1161
|
+
else if (placement.includes("V_CENTER")) label.y = (nodeHeight - height + labelPadding.top - labelPadding.bottom) / 2;
|
|
1162
|
+
else if (placement.includes("V_BOTTOM")) label.y = nodeHeight - height - labelPadding.bottom;
|
|
1163
|
+
else label.y = labelPadding.top;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function placePortLabels(node, globalOptions) {
|
|
1167
|
+
const placement = String(getOption({
|
|
1168
|
+
...globalOptions,
|
|
1169
|
+
...node.layoutOptions
|
|
1170
|
+
}, "portLabels.placement") ?? "OUTSIDE");
|
|
1171
|
+
const inside = placement.includes("INSIDE");
|
|
1172
|
+
const alwaysOtherSide = placement.includes("ALWAYS_OTHER_SAME_SIDE");
|
|
1173
|
+
const spaceEfficient = placement.includes("SPACE_EFFICIENT") || String(getOption(node.layoutOptions ?? {}, "nodeSize.options") ?? "").includes("SPACE_EFFICIENT_PORT_LABELS");
|
|
1174
|
+
const horizontalSpacing = getNumberOption(globalOptions, "spacing.labelPortHorizontal") ?? 1;
|
|
1175
|
+
const verticalSpacing = getNumberOption(globalOptions, "spacing.labelPortVertical") ?? 1;
|
|
1176
|
+
const labelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
1177
|
+
const treatAsGroup = getBooleanOption(node.layoutOptions ?? {}, "portLabels.treatAsGroup") ?? false;
|
|
1178
|
+
const placeNextToPort = placement.includes("NEXT_TO_PORT_IF_POSSIBLE") || getBooleanOption(node.layoutOptions ?? {}, "portLabels.nextToPortIfPossible") === true;
|
|
1179
|
+
const nodeWidth = node.width ?? 0;
|
|
1180
|
+
for (const port of node.ports ?? []) {
|
|
1181
|
+
if (getBooleanOption(port.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
1182
|
+
const portWidth = port.width ?? 0;
|
|
1183
|
+
const portHeight = port.height ?? 0;
|
|
1184
|
+
const side = (port.x ?? 0) < 0 ? "WEST" : (port.x ?? 0) >= nodeWidth ? "EAST" : (port.y ?? 0) < 0 ? "NORTH" : "SOUTH";
|
|
1185
|
+
const portsOnSide = (node.ports ?? []).filter((candidate) => {
|
|
1186
|
+
return ((candidate.x ?? 0) < 0 ? "WEST" : (candidate.x ?? 0) >= nodeWidth ? "EAST" : (candidate.y ?? 0) < 0 ? "NORTH" : "SOUTH") === side;
|
|
1187
|
+
});
|
|
1188
|
+
const labels = (port.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
|
|
1189
|
+
const totalLabelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelSpacing;
|
|
1190
|
+
let stackedY = labels.length > 1 ? inside || placeNextToPort ? treatAsGroup ? (portHeight - totalLabelHeight) / 2 : (portHeight - (labels[0]?.height ?? 0)) / 2 : portHeight + verticalSpacing : void 0;
|
|
1191
|
+
for (const label of labels) {
|
|
1192
|
+
if (!label.text) continue;
|
|
1193
|
+
const width = label.width ?? 0;
|
|
1194
|
+
const height = label.height ?? 0;
|
|
1195
|
+
if (side === "EAST") {
|
|
1196
|
+
label.x = inside ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
1197
|
+
label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
|
|
1198
|
+
} else if (side === "WEST") {
|
|
1199
|
+
label.x = inside ? portWidth + horizontalSpacing : -width - horizontalSpacing;
|
|
1200
|
+
label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
|
|
1201
|
+
} else if (side === "NORTH") {
|
|
1202
|
+
label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
1203
|
+
label.y = inside ? portHeight + verticalSpacing : -height - verticalSpacing;
|
|
1204
|
+
} else {
|
|
1205
|
+
label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
1206
|
+
label.y = inside ? -height - verticalSpacing : portHeight + verticalSpacing;
|
|
1207
|
+
}
|
|
1208
|
+
if (spaceEfficient && portsOnSide[0] === port && portsOnSide.length >= 2 && !placeNextToPort) if (side === "EAST" || side === "WEST") label.y = -height - verticalSpacing;
|
|
1209
|
+
else label.x = -width - horizontalSpacing;
|
|
1210
|
+
if (stackedY !== void 0) stackedY += height + labelSpacing;
|
|
1211
|
+
}
|
|
408
1212
|
}
|
|
409
1213
|
}
|
|
410
1214
|
|