@statelyai/layout 0.0.0 → 0.0.2

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.
Files changed (46) hide show
  1. package/NOTICE.md +9 -1
  2. package/README.md +50 -25
  3. package/dist/elkjs/index.d.mts +3 -0
  4. package/dist/elkjs/index.mjs +904 -97
  5. package/dist/index-8xYkohbz.d.mts +1212 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +3 -3
  8. package/dist/layered/index.d.mts +2 -2
  9. package/dist/layered/index.mjs +2 -2
  10. package/dist/layered-QwJn2gb2.mjs +8978 -0
  11. package/dist/{spore-cihb_Aht.mjs → spore-D15xIQKj.mjs} +1 -31
  12. package/package.json +59 -26
  13. package/src/box.ts +135 -0
  14. package/src/elkjs/index.ts +1825 -0
  15. package/src/elkjs/types.ts +103 -0
  16. package/src/errors.ts +16 -0
  17. package/src/fixed.ts +80 -0
  18. package/src/index.ts +92 -0
  19. package/src/java-random.ts +46 -0
  20. package/src/layered/bk-node-placement.ts +715 -0
  21. package/src/layered/elk-enum-values.ts +171 -0
  22. package/src/layered/elk-options.generated.ts +315 -0
  23. package/src/layered/elk-options.ts +98 -0
  24. package/src/layered/flexible-ports.ts +11 -0
  25. package/src/layered/high-degree.ts +127 -0
  26. package/src/layered/index.ts +2138 -0
  27. package/src/layered/layer-unzipping.ts +217 -0
  28. package/src/layered/linear-segments-node-placement.ts +447 -0
  29. package/src/layered/long-edges.ts +405 -0
  30. package/src/layered/min-width.ts +159 -0
  31. package/src/layered/multi-edge-wrapping.ts +460 -0
  32. package/src/layered/network-simplex-node-placement.ts +500 -0
  33. package/src/layered/network-simplex.ts +346 -0
  34. package/src/layered/node-promotion.ts +197 -0
  35. package/src/layered/spacing.ts +37 -0
  36. package/src/layered/spline-bezier.ts +102 -0
  37. package/src/layered/strategies.ts +5343 -0
  38. package/src/layered/stretch-width.ts +136 -0
  39. package/src/layered/types.ts +111 -0
  40. package/src/layout.ts +202 -0
  41. package/src/packing.ts +74 -0
  42. package/src/random.ts +142 -0
  43. package/src/spore.ts +103 -0
  44. package/src/types.ts +84 -0
  45. package/dist/index-v0P1Ake8.d.mts +0 -149
  46. package/dist/layered-ByNCZQgJ.mjs +0 -439
@@ -1,5 +1,5 @@
1
- import { t as getLayeredLayout } from "../layered-ByNCZQgJ.mjs";
2
- import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-cihb_Aht.mjs";
1
+ import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-QwJn2gb2.mjs";
2
+ import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-D15xIQKj.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
- id: "elk.algorithm",
56
- name: "Layout Algorithm",
57
- type: "STRING"
58
- },
59
- {
60
- id: "elk.direction",
61
- name: "Direction",
62
- type: "ENUM"
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
- if (hasHierarchy && hierarchyHandling !== void 0 && hierarchyHandling !== "INCLUDE_CHILDREN") throw new Error("org.eclipse.elk.core.UnsupportedGraphException: Hierarchical edges require INCLUDE_CHILDREN");
124
- if (hasHierarchy) for (const child of graph.children ?? []) {
125
- if ((child.children?.length ?? 0) === 0) continue;
126
- await this.layout(child, {
127
- ...arguments_,
128
- layoutOptions: {
129
- ...arguments_.layoutOptions,
130
- hierarchyHandling: "INCLUDE_CHILDREN"
131
- },
132
- logging: false,
133
- measureExecutionTime: false
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
- const graph_ = toGraph(graph);
137
- const padding = parsePadding(getOption(layoutOptions, "padding"));
138
- const constrainedLayerByNodeId = new Map((graph.children ?? []).flatMap((node) => getOption(node.layoutOptions ?? {}, "layerConstraint") === "FIRST" ? [[String(node.id), 0]] : []));
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 sourceLayer = constrainedLayerByNodeId.get(edge.sourceId);
141
- const targetLayer = constrainedLayerByNodeId.get(edge.targetId);
142
- return sourceLayer !== void 0 && targetLayer !== void 0 && targetLayer <= sourceLayer;
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
- applyLayout(graph, algorithm === "sporeCompaction" ? getSporeCompactionLayout(graph_, {
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: (node) => constrainedLayerByNodeId.get(node.id) }
176
- }), padding, layoutOptions);
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: 0,
194
- right: 0,
195
- bottom: 0,
196
- left: 0
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
- return Object.entries(options).find(([key]) => key.endsWith(`.${suffix}`))?.[1];
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") ?? "DOWN").toLowerCase();
237
- return direction === "up" || direction === "left" || direction === "right" ? direction : "down";
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 toGraph(root) {
248
- const children = root.children ?? [];
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.ports?.map((port) => ({
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: edge.labels?.[0]?.width,
282
- height: edge.labels?.[0]?.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,150 @@ 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 label = edge.labels?.[0];
345
- if (label) {
346
- label.x = laidOutEdge.x;
347
- label.y = laidOutEdge.y;
348
- label.width = laidOutEdge.width;
349
- label.height = laidOutEdge.height;
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
- root.width = Math.max(0, ...graph.nodes.map((node) => node.x + node.width)) + padding.right;
353
- root.height = Math.max(0, ...graph.nodes.map((node) => node.y + node.height)) + padding.bottom;
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 childByEndpointId = new Map(laidOutChildById);
1074
+ for (const child of root.children ?? []) for (const port of child.ports ?? []) childByEndpointId.set(String(port.id), child);
1075
+ const wrappedEdgeCount = (root.edges ?? []).filter((edge) => {
1076
+ const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
1077
+ const target = childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
1078
+ if (!source || !target) return false;
1079
+ 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);
1080
+ }).length;
1081
+ const hasWrappedEdge = wrappedEdgeCount > 0;
1082
+ const addBoundaryPixel = wrappingStrategy !== "MULTI_EDGE" && !(wrappingStrategy === "SINGLE_EDGE" && hasWrappedEdge) && 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);
1083
+ const edgeBoundsExtraX = addBoundaryPixel && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9 ? 1 : 0;
1084
+ const edgeBoundsExtraY = addBoundaryPixel && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
1085
+ const singleMultiEdgeCutBoundsExtraY = wrappingStrategy === "MULTI_EDGE" && wrappedEdgeCount === 1 && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
1086
+ const postCompactionBoundsExtraX = !addBoundaryPixel && !hasWrappedEdge && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) > maximumNodeX + 1e-9 ? .04 : 0;
1087
+ const postCompactionBoundsExtraY = !addBoundaryPixel && !hasWrappedEdge && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) > maximumNodeY + 1e-9 ? .04 : 0;
1088
+ const calculatedWidth = Math.max(0, ...layoutChildren.flatMap((node) => [
1089
+ (node.x ?? 0) + (node.width ?? 0),
1090
+ ...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
1091
+ ...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
1092
+ ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
1093
+ ]), ...(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) => [
1094
+ section.startPoint.x,
1095
+ ...(section.bendPoints ?? []).map((point) => point.x),
1096
+ section.endPoint.x
1097
+ ]))) + padding.right + edgeBoundsExtraX + postCompactionBoundsExtraX + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up") ? 1 : 0);
1098
+ const calculatedHeight = Math.max(0, ...layoutChildren.flatMap((node) => [
1099
+ (node.y ?? 0) + (node.height ?? 0),
1100
+ ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
1101
+ ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
1102
+ ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
1103
+ ]), ...(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) => [
1104
+ section.startPoint.y,
1105
+ ...(section.bendPoints ?? []).map((point) => point.y),
1106
+ section.endPoint.y
1107
+ ]))) + padding.bottom + edgeBoundsExtraY + singleMultiEdgeCutBoundsExtraY + postCompactionBoundsExtraY + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "right" || getDirection(layoutOptions) === "left") ? 1 : 0);
1108
+ root.width = fixedGraphSize ? authoredWidth ?? 0 : calculatedWidth;
1109
+ root.height = fixedGraphSize ? authoredHeight ?? 0 : calculatedHeight;
354
1110
  }
355
1111
  function getParentEdgeSection(root, edge) {
356
1112
  const sourceId = String(edge.sources?.[0] ?? edge.source);
@@ -388,7 +1144,10 @@ function getParentEdgeSection(root, edge) {
388
1144
  }
389
1145
  }
390
1146
  function placeNodeLabels(node, globalOptions) {
1147
+ const labelPadding = parsePadding(getOption(globalOptions, "nodeLabels.padding"), 5);
391
1148
  for (const label of node.labels ?? []) {
1149
+ if (!label.text) continue;
1150
+ if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) continue;
392
1151
  const placement = String(getOption({
393
1152
  ...globalOptions,
394
1153
  ...node.layoutOptions,
@@ -399,12 +1158,60 @@ function placeNodeLabels(node, globalOptions) {
399
1158
  const height = label.height ?? 0;
400
1159
  const nodeWidth = node.width ?? 0;
401
1160
  const nodeHeight = node.height ?? 0;
402
- label.x = placement.includes("H_CENTER") ? (nodeWidth - width) / 2 : placement.includes("H_RIGHT") ? nodeWidth - width : 0;
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 = 0;
1161
+ label.x = placement.includes("H_CENTER") ? (nodeWidth - width + labelPadding.left - labelPadding.right) / 2 : placement.includes("H_RIGHT") ? nodeWidth - width - labelPadding.right : labelPadding.left;
1162
+ if (placement.includes("OUTSIDE") && placement.includes("V_TOP")) label.y = -height - Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
1163
+ else if (placement.includes("OUTSIDE") && placement.includes("V_BOTTOM")) label.y = nodeHeight + Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
1164
+ else if (placement.includes("V_CENTER")) label.y = (nodeHeight - height + labelPadding.top - labelPadding.bottom) / 2;
1165
+ else if (placement.includes("V_BOTTOM")) label.y = nodeHeight - height - labelPadding.bottom;
1166
+ else label.y = labelPadding.top;
1167
+ }
1168
+ }
1169
+ function placePortLabels(node, globalOptions) {
1170
+ const placement = String(getOption({
1171
+ ...globalOptions,
1172
+ ...node.layoutOptions
1173
+ }, "portLabels.placement") ?? "OUTSIDE");
1174
+ const inside = placement.includes("INSIDE");
1175
+ const alwaysOtherSide = placement.includes("ALWAYS_OTHER_SAME_SIDE");
1176
+ const spaceEfficient = placement.includes("SPACE_EFFICIENT") || String(getOption(node.layoutOptions ?? {}, "nodeSize.options") ?? "").includes("SPACE_EFFICIENT_PORT_LABELS");
1177
+ const horizontalSpacing = getNumberOption(globalOptions, "spacing.labelPortHorizontal") ?? 1;
1178
+ const verticalSpacing = getNumberOption(globalOptions, "spacing.labelPortVertical") ?? 1;
1179
+ const labelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
1180
+ const treatAsGroup = getBooleanOption(node.layoutOptions ?? {}, "portLabels.treatAsGroup") ?? false;
1181
+ const placeNextToPort = placement.includes("NEXT_TO_PORT_IF_POSSIBLE") || getBooleanOption(node.layoutOptions ?? {}, "portLabels.nextToPortIfPossible") === true;
1182
+ const nodeWidth = node.width ?? 0;
1183
+ for (const port of node.ports ?? []) {
1184
+ if (getBooleanOption(port.layoutOptions ?? {}, "noLayout") === true) continue;
1185
+ const portWidth = port.width ?? 0;
1186
+ const portHeight = port.height ?? 0;
1187
+ const side = (port.x ?? 0) < 0 ? "WEST" : (port.x ?? 0) >= nodeWidth ? "EAST" : (port.y ?? 0) < 0 ? "NORTH" : "SOUTH";
1188
+ const portsOnSide = (node.ports ?? []).filter((candidate) => {
1189
+ return ((candidate.x ?? 0) < 0 ? "WEST" : (candidate.x ?? 0) >= nodeWidth ? "EAST" : (candidate.y ?? 0) < 0 ? "NORTH" : "SOUTH") === side;
1190
+ });
1191
+ const labels = (port.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
1192
+ const totalLabelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelSpacing;
1193
+ let stackedY = labels.length > 1 ? inside || placeNextToPort ? treatAsGroup ? (portHeight - totalLabelHeight) / 2 : (portHeight - (labels[0]?.height ?? 0)) / 2 : portHeight + verticalSpacing : void 0;
1194
+ for (const label of labels) {
1195
+ if (!label.text) continue;
1196
+ const width = label.width ?? 0;
1197
+ const height = label.height ?? 0;
1198
+ if (side === "EAST") {
1199
+ label.x = inside ? -width - horizontalSpacing : portWidth + horizontalSpacing;
1200
+ label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
1201
+ } else if (side === "WEST") {
1202
+ label.x = inside ? portWidth + horizontalSpacing : -width - horizontalSpacing;
1203
+ label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
1204
+ } else if (side === "NORTH") {
1205
+ label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
1206
+ label.y = inside ? portHeight + verticalSpacing : -height - verticalSpacing;
1207
+ } else {
1208
+ label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
1209
+ label.y = inside ? -height - verticalSpacing : portHeight + verticalSpacing;
1210
+ }
1211
+ if (spaceEfficient && portsOnSide[0] === port && portsOnSide.length >= 2 && !placeNextToPort) if (side === "EAST" || side === "WEST") label.y = -height - verticalSpacing;
1212
+ else label.x = -width - horizontalSpacing;
1213
+ if (stackedY !== void 0) stackedY += height + labelSpacing;
1214
+ }
408
1215
  }
409
1216
  }
410
1217