@graph-ir/mcp-server 0.2.0 → 0.2.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.
- package/README.md +12 -3
- package/dist/index.js +28 -8
- package/dist/index.js.map +1 -1
- package/dist/tools/generate.d.ts +2 -16
- package/dist/tools/generate.d.ts.map +1 -1
- package/dist/tools/generate.js +29 -14
- package/dist/tools/generate.js.map +1 -1
- package/dist/tools/layout.d.ts +37 -81
- package/dist/tools/layout.d.ts.map +1 -1
- package/dist/tools/layout.js +598 -194
- package/dist/tools/layout.js.map +1 -1
- package/dist/tools/visualize.d.ts.map +1 -1
- package/dist/tools/visualize.js +21 -4
- package/dist/tools/visualize.js.map +1 -1
- package/package.json +4 -3
package/dist/tools/layout.js
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Graph-IR Layout Tool
|
|
3
|
-
*
|
|
4
|
-
* Computes graph layout using ELK algorithms.
|
|
5
|
-
*/
|
|
6
1
|
import ELKConstructor from 'elkjs/lib/elk.bundled.js';
|
|
7
|
-
|
|
8
|
-
const ELKClass = ELKConstructor.default ??
|
|
9
|
-
|
|
2
|
+
import { validateIR, } from '@graph-ir/core';
|
|
3
|
+
const ELKClass = ELKConstructor.default ??
|
|
4
|
+
ELKConstructor;
|
|
5
|
+
// @ts-expect-error elkjs exposes a CJS-shaped constructor to ESM.
|
|
10
6
|
const elk = new ELKClass();
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Algorithm Mapping
|
|
13
|
-
// ============================================================================
|
|
14
7
|
const ALGORITHM_MAPPING = {
|
|
15
8
|
layered: 'org.eclipse.elk.layered',
|
|
16
9
|
force: 'org.eclipse.elk.force',
|
|
10
|
+
tree: 'org.eclipse.elk.mrtree',
|
|
17
11
|
mrtree: 'org.eclipse.elk.mrtree',
|
|
18
12
|
box: 'org.eclipse.elk.box',
|
|
19
13
|
stress: 'org.eclipse.elk.stress',
|
|
20
14
|
};
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
UP: 'UP',
|
|
15
|
+
const EDGE_ROUTING_MAPPING = {
|
|
16
|
+
orthogonal: 'ORTHOGONAL',
|
|
17
|
+
polyline: 'POLYLINE',
|
|
18
|
+
spline: 'SPLINES',
|
|
26
19
|
};
|
|
27
20
|
const DEFAULT_OPTIONS = {
|
|
28
21
|
nodeWidth: 150,
|
|
@@ -31,232 +24,643 @@ const DEFAULT_OPTIONS = {
|
|
|
31
24
|
verticalSpacing: 50,
|
|
32
25
|
direction: 'RIGHT',
|
|
33
26
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
const REQUEST_OPTION_KEYS = new Set([
|
|
28
|
+
'nodeWidth',
|
|
29
|
+
'nodeHeight',
|
|
30
|
+
'horizontalSpacing',
|
|
31
|
+
'verticalSpacing',
|
|
32
|
+
'direction',
|
|
33
|
+
]);
|
|
34
|
+
const REQUEST_DIRECTIONS = new Set(['RIGHT', 'DOWN', 'LEFT', 'UP']);
|
|
35
|
+
const ELK_ROOT_ID = '__graph_ir_internal_root__';
|
|
36
|
+
export async function layoutIR(irJson, requestedAlgorithm, options = {}) {
|
|
42
37
|
let ir;
|
|
43
38
|
try {
|
|
44
39
|
ir = JSON.parse(irJson);
|
|
45
40
|
}
|
|
46
|
-
catch (
|
|
47
|
-
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
catch (error) {
|
|
42
|
+
return failed(requestedAlgorithm ?? 'layered', [{
|
|
43
|
+
code: 'INVALID_JSON',
|
|
44
|
+
path: '',
|
|
45
|
+
message: `Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`,
|
|
46
|
+
}]);
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
if (!
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
errors: ['Invalid Graph-IR structure: missing nodes array'],
|
|
58
|
-
algorithm,
|
|
59
|
-
};
|
|
48
|
+
const validation = validateIR(ir);
|
|
49
|
+
if (!validation.valid) {
|
|
50
|
+
return failed(requestedAlgorithm ?? 'layered', validation.errors.map(({ code, path, message }) => ({ code, path, message })));
|
|
60
51
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
algorithm,
|
|
66
|
-
};
|
|
52
|
+
const graph = ir;
|
|
53
|
+
const requestErrors = validateRequestOptions(options);
|
|
54
|
+
if (requestErrors.length > 0) {
|
|
55
|
+
return failed(requestedAlgorithm ?? graph.layoutOptions?.algorithm ?? 'layered', requestErrors);
|
|
67
56
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
57
|
+
const conflict = optionConflicts(graph, requestedAlgorithm, options);
|
|
58
|
+
if (conflict.length > 0) {
|
|
59
|
+
return failed(requestedAlgorithm ?? graph.layoutOptions?.algorithm ?? 'layered', conflict);
|
|
60
|
+
}
|
|
61
|
+
const algorithm = requestedAlgorithm ?? graph.layoutOptions?.algorithm ?? 'layered';
|
|
62
|
+
const capabilityErrors = unsupportedCapabilities(graph, algorithm, options);
|
|
63
|
+
if (capabilityErrors.length > 0)
|
|
64
|
+
return failed(algorithm, capabilityErrors);
|
|
65
|
+
const requestOptions = normalizeOptions(graph, options);
|
|
72
66
|
try {
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const layoutedIR = elkToGraphIR(layoutedElk, ir, algorithm);
|
|
67
|
+
const elkGraph = graphIRToELK(graph, requestOptions, algorithm);
|
|
68
|
+
const resolvedElk = await elk.layout(elkGraph);
|
|
69
|
+
const verification = verifyResolvedLayout(graph, resolvedElk);
|
|
70
|
+
if (verification.length > 0)
|
|
71
|
+
return failed(algorithm, verification);
|
|
79
72
|
return {
|
|
80
73
|
success: true,
|
|
81
|
-
|
|
74
|
+
resolved: elkToResolvedLayout(resolvedElk, graph, algorithm),
|
|
82
75
|
errors: [],
|
|
83
76
|
algorithm,
|
|
84
77
|
};
|
|
85
78
|
}
|
|
86
|
-
catch (
|
|
87
|
-
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
catch (error) {
|
|
80
|
+
return failed(algorithm, [{
|
|
81
|
+
code: 'LAYOUT_FAILED',
|
|
82
|
+
path: '',
|
|
83
|
+
message: `Layout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
84
|
+
}]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function layoutIRFromToolArguments(args) {
|
|
88
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
89
|
+
return failed('layered', [{
|
|
90
|
+
code: 'INVALID_LAYOUT_REQUEST',
|
|
91
|
+
path: '',
|
|
92
|
+
message: 'Tool arguments must be an object',
|
|
93
|
+
}]);
|
|
94
|
+
}
|
|
95
|
+
const record = args;
|
|
96
|
+
const errors = [];
|
|
97
|
+
for (const key of Object.keys(record)) {
|
|
98
|
+
if (!['ir', 'algorithm', 'options'].includes(key)) {
|
|
99
|
+
errors.push({
|
|
100
|
+
code: 'INVALID_LAYOUT_REQUEST',
|
|
101
|
+
path: `/${key}`,
|
|
102
|
+
message: `Unknown layout tool argument "${key}"`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof record.ir !== 'string') {
|
|
107
|
+
errors.push({
|
|
108
|
+
code: 'INVALID_LAYOUT_REQUEST',
|
|
109
|
+
path: '/ir',
|
|
110
|
+
message: 'ir must be a JSON string',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (record.algorithm !== undefined && typeof record.algorithm !== 'string') {
|
|
114
|
+
errors.push({
|
|
115
|
+
code: 'INVALID_LAYOUT_REQUEST',
|
|
116
|
+
path: '/algorithm',
|
|
117
|
+
message: 'algorithm must be a string',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
errors.push(...validateRequestOptions(record.options ?? {}));
|
|
121
|
+
if (errors.length > 0) {
|
|
122
|
+
return failed(typeof record.algorithm === 'string' ? record.algorithm : 'layered', errors);
|
|
123
|
+
}
|
|
124
|
+
return layoutIR(record.ir, record.algorithm, record.options);
|
|
125
|
+
}
|
|
126
|
+
function validateRequestOptions(value) {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
128
|
+
return [{
|
|
129
|
+
code: 'INVALID_LAYOUT_REQUEST',
|
|
130
|
+
path: '/options',
|
|
131
|
+
message: 'options must be an object',
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
const options = value;
|
|
135
|
+
const errors = [];
|
|
136
|
+
for (const [key, option] of Object.entries(options)) {
|
|
137
|
+
const path = `/options/${key}`;
|
|
138
|
+
if (!REQUEST_OPTION_KEYS.has(key)) {
|
|
139
|
+
errors.push({ code: 'INVALID_LAYOUT_REQUEST', path, message: `Unknown layout option "${key}"` });
|
|
140
|
+
}
|
|
141
|
+
else if (key === 'direction') {
|
|
142
|
+
if (typeof option !== 'string' || !REQUEST_DIRECTIONS.has(option)) {
|
|
143
|
+
errors.push({ code: 'INVALID_LAYOUT_REQUEST', path, message: 'Invalid layout direction' });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (typeof option !== 'number' || !Number.isFinite(option) || option < 0) {
|
|
147
|
+
errors.push({ code: 'INVALID_LAYOUT_REQUEST', path, message: 'Layout size and spacing must be finite and nonnegative' });
|
|
148
|
+
}
|
|
92
149
|
}
|
|
150
|
+
return errors;
|
|
151
|
+
}
|
|
152
|
+
function optionConflicts(graph, requestedAlgorithm, rawOptions) {
|
|
153
|
+
const errors = [];
|
|
154
|
+
if (requestedAlgorithm &&
|
|
155
|
+
graph.layoutOptions?.algorithm &&
|
|
156
|
+
requestedAlgorithm !== graph.layoutOptions.algorithm) {
|
|
157
|
+
errors.push({
|
|
158
|
+
code: 'LAYOUT_OPTION_CONFLICT',
|
|
159
|
+
path: '/layoutOptions/algorithm',
|
|
160
|
+
message: 'Transient algorithm conflicts with authored layout algorithm',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (typeof rawOptions.direction === 'string' &&
|
|
164
|
+
graph.layoutOptions?.direction &&
|
|
165
|
+
rawOptions.direction !== graph.layoutOptions.direction) {
|
|
166
|
+
errors.push({
|
|
167
|
+
code: 'LAYOUT_OPTION_CONFLICT',
|
|
168
|
+
path: '/layoutOptions/direction',
|
|
169
|
+
message: 'Transient direction conflicts with authored layout direction',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const authoredSpacing = graph.layoutOptions?.spacing;
|
|
173
|
+
const spacingConflicts = [
|
|
174
|
+
['horizontalSpacing', 'nodeNode'],
|
|
175
|
+
['verticalSpacing', 'layerSpacing'],
|
|
176
|
+
];
|
|
177
|
+
for (const [requestKey, authoredKey] of spacingConflicts) {
|
|
178
|
+
const requested = rawOptions[requestKey];
|
|
179
|
+
const authored = authoredSpacing?.[authoredKey];
|
|
180
|
+
if (typeof requested === 'number' && authored !== undefined && requested !== authored) {
|
|
181
|
+
errors.push({
|
|
182
|
+
code: 'LAYOUT_OPTION_CONFLICT',
|
|
183
|
+
path: `/layoutOptions/spacing/${authoredKey}`,
|
|
184
|
+
message: `Transient ${requestKey} conflicts with authored ${authoredKey}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return errors;
|
|
189
|
+
}
|
|
190
|
+
function unsupportedCapabilities(graph, algorithm, rawOptions) {
|
|
191
|
+
const errors = [];
|
|
192
|
+
const unsupported = (path, message) => errors.push({
|
|
193
|
+
code: 'UNSUPPORTED_LAYOUT_CAPABILITY',
|
|
194
|
+
path,
|
|
195
|
+
message,
|
|
196
|
+
});
|
|
197
|
+
if (!(algorithm in ALGORITHM_MAPPING)) {
|
|
198
|
+
unsupported('/layoutOptions/algorithm', `Algorithm "${algorithm}" is not supported`);
|
|
199
|
+
}
|
|
200
|
+
const layered = algorithm === 'layered';
|
|
201
|
+
const directional = layered || algorithm === 'tree' || algorithm === 'mrtree';
|
|
202
|
+
if (!directional && (graph.layoutOptions?.direction || rawOptions.direction !== undefined)) {
|
|
203
|
+
unsupported(graph.layoutOptions?.direction ? '/layoutOptions/direction' : '/options/direction', `Algorithm "${algorithm}" does not honor direction`);
|
|
204
|
+
}
|
|
205
|
+
if (!layered && graph.layoutOptions?.spacing) {
|
|
206
|
+
unsupported('/layoutOptions/spacing', `Algorithm "${algorithm}" does not honor portable spacing`);
|
|
207
|
+
}
|
|
208
|
+
if (!layered && (rawOptions.horizontalSpacing !== undefined || rawOptions.verticalSpacing !== undefined)) {
|
|
209
|
+
unsupported('/options', `Algorithm "${algorithm}" does not honor transient spacing`);
|
|
210
|
+
}
|
|
211
|
+
if (!layered && graph.layoutOptions?.edgeRouting) {
|
|
212
|
+
unsupported('/layoutOptions/edgeRouting', `Algorithm "${algorithm}" does not honor graph edge routing`);
|
|
213
|
+
}
|
|
214
|
+
if (graph.layoutOptions?.edgeRouting === 'spline') {
|
|
215
|
+
unsupported('/layoutOptions/edgeRouting', 'Spline control-point semantics are not represented by ResolvedLayout');
|
|
216
|
+
}
|
|
217
|
+
if (algorithm === 'box' && graph.edges.length > 0) {
|
|
218
|
+
unsupported('/edges', 'Algorithm "box" does not route authored edges');
|
|
219
|
+
}
|
|
220
|
+
if (!layered) {
|
|
221
|
+
graph.edges.forEach((edge, index) => {
|
|
222
|
+
if (edge.source === edge.target) {
|
|
223
|
+
unsupported(`/edges/${index}`, `Algorithm "${algorithm}" does not produce a verified self-loop route`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (!layered && hasNestedNodes(graph.nodes)) {
|
|
228
|
+
unsupported('/nodes', `Algorithm "${algorithm}" does not support verified compound layout`);
|
|
229
|
+
}
|
|
230
|
+
if (graph.constraints?.length)
|
|
231
|
+
unsupported('/constraints', 'Spatial constraints require a solver');
|
|
232
|
+
if (graph.profile)
|
|
233
|
+
unsupported('/profile', 'Notation profiles require a profile-aware layout adapter');
|
|
234
|
+
if (graph.interaction)
|
|
235
|
+
unsupported('/interaction', 'Sequence interactions require a sequence-aware layout adapter');
|
|
236
|
+
if (graph.layoutOptions?.hierarchyHandling === 'SEPARATE_CHILDREN') {
|
|
237
|
+
unsupported('/layoutOptions/hierarchyHandling', 'SEPARATE_CHILDREN is not supported');
|
|
238
|
+
}
|
|
239
|
+
visitNodes(graph.nodes, '/nodes', (node, path) => {
|
|
240
|
+
if (node.labelPlacement)
|
|
241
|
+
unsupported(`${path}/labelPlacement`, 'Node label placement is not supported');
|
|
242
|
+
if (node.classifier)
|
|
243
|
+
unsupported(`${path}/classifier`, 'Classifier semantics require a class-aware layout adapter');
|
|
244
|
+
if (node.position)
|
|
245
|
+
unsupported(`${path}/position`, 'Exact authored positions are not supported');
|
|
246
|
+
if (node.pinned)
|
|
247
|
+
unsupported(`${path}/pinned`, 'Pinned positions are not supported');
|
|
248
|
+
if (node.children?.length && node.dimensions) {
|
|
249
|
+
unsupported(`${path}/dimensions`, 'Fixed outer dimensions for containers are not supported');
|
|
250
|
+
}
|
|
251
|
+
const layout = node.layoutOptions;
|
|
252
|
+
if (!layered && node.ports?.length)
|
|
253
|
+
unsupported(`${path}/ports`, `Algorithm "${algorithm}" does not preserve authored port sides`);
|
|
254
|
+
if (layout?.portAlignment)
|
|
255
|
+
unsupported(`${path}/layoutOptions/portAlignment`, 'Port alignment is not supported');
|
|
256
|
+
if (layout?.minimumSize)
|
|
257
|
+
unsupported(`${path}/layoutOptions/minimumSize`, 'Minimum size is not supported');
|
|
258
|
+
if (layout?.sizeConstraints?.length)
|
|
259
|
+
unsupported(`${path}/layoutOptions/sizeConstraints`, 'Size constraints are not supported');
|
|
260
|
+
if (node.ports?.length && layout?.portConstraints === 'FREE') {
|
|
261
|
+
unsupported(`${path}/layoutOptions/portConstraints`, 'FREE would ignore authored port sides');
|
|
262
|
+
}
|
|
263
|
+
if (layout?.portConstraints === 'FIXED_ORDER' || layout?.portConstraints === 'FIXED_POS') {
|
|
264
|
+
unsupported(`${path}/layoutOptions/portConstraints`, `${layout.portConstraints} is not supported`);
|
|
265
|
+
}
|
|
266
|
+
node.ports?.forEach((port, index) => {
|
|
267
|
+
if (port.position !== undefined) {
|
|
268
|
+
unsupported(`${path}/ports/${index}/position`, 'Exact port position is not supported');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
graph.edges.forEach((edge, index) => {
|
|
273
|
+
if (edge.routeIntent)
|
|
274
|
+
unsupported(`/edges/${index}/routeIntent`, 'Authored route intent is not supported');
|
|
275
|
+
if (edge.labelPlacement)
|
|
276
|
+
unsupported(`/edges/${index}/labelPlacement`, 'Edge label placement is not supported');
|
|
277
|
+
if (edge.style?.routing)
|
|
278
|
+
unsupported(`/edges/${index}/style/routing`, 'Per-edge routing is not supported');
|
|
279
|
+
if (edge.sourceEnd)
|
|
280
|
+
unsupported(`/edges/${index}/sourceEnd`, 'Relationship-end semantics are not supported');
|
|
281
|
+
if (edge.targetEnd)
|
|
282
|
+
unsupported(`/edges/${index}/targetEnd`, 'Relationship-end semantics are not supported');
|
|
283
|
+
});
|
|
284
|
+
return errors;
|
|
285
|
+
}
|
|
286
|
+
function normalizeOptions(graph, raw) {
|
|
287
|
+
const authoredSpacing = graph.layoutOptions?.spacing;
|
|
288
|
+
return {
|
|
289
|
+
nodeWidth: numberOption(raw.nodeWidth, DEFAULT_OPTIONS.nodeWidth),
|
|
290
|
+
nodeHeight: numberOption(raw.nodeHeight, DEFAULT_OPTIONS.nodeHeight),
|
|
291
|
+
horizontalSpacing: numberOption(raw.horizontalSpacing, authoredSpacing?.nodeNode ?? DEFAULT_OPTIONS.horizontalSpacing),
|
|
292
|
+
verticalSpacing: numberOption(raw.verticalSpacing, authoredSpacing?.layerSpacing ?? DEFAULT_OPTIONS.verticalSpacing),
|
|
293
|
+
direction: (typeof raw.direction === 'string'
|
|
294
|
+
? raw.direction
|
|
295
|
+
: graph.layoutOptions?.direction ?? DEFAULT_OPTIONS.direction),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function numberOption(value, fallback) {
|
|
299
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
93
300
|
}
|
|
94
|
-
// ============================================================================
|
|
95
|
-
// Conversion Functions
|
|
96
|
-
// ============================================================================
|
|
97
|
-
/**
|
|
98
|
-
* Convert Graph-IR to ELK graph format
|
|
99
|
-
*/
|
|
100
301
|
export function graphIRToELK(ir, options, algorithm = 'layered') {
|
|
101
|
-
const
|
|
102
|
-
|
|
302
|
+
const identifiers = buildElkIdentifiers(ir);
|
|
303
|
+
const authoredSpacing = ir.layoutOptions?.spacing;
|
|
304
|
+
const normalized = {
|
|
305
|
+
...DEFAULT_OPTIONS,
|
|
306
|
+
...options,
|
|
307
|
+
horizontalSpacing: options.horizontalSpacing ??
|
|
308
|
+
authoredSpacing?.nodeNode ?? DEFAULT_OPTIONS.horizontalSpacing,
|
|
309
|
+
verticalSpacing: options.verticalSpacing ??
|
|
310
|
+
authoredSpacing?.layerSpacing ?? DEFAULT_OPTIONS.verticalSpacing,
|
|
311
|
+
direction: options.direction ?? ir.layoutOptions?.direction ?? DEFAULT_OPTIONS.direction,
|
|
312
|
+
};
|
|
103
313
|
const elkLayoutOptions = {
|
|
104
|
-
'elk.algorithm': ALGORITHM_MAPPING[algorithm]
|
|
105
|
-
'elk.direction':
|
|
106
|
-
'elk.spacing.nodeNode': String(
|
|
107
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': String(
|
|
314
|
+
'elk.algorithm': ALGORITHM_MAPPING[algorithm] ?? ALGORITHM_MAPPING.layered,
|
|
315
|
+
'elk.direction': normalized.direction,
|
|
316
|
+
'elk.spacing.nodeNode': String(normalized.horizontalSpacing),
|
|
317
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': String(normalized.verticalSpacing),
|
|
318
|
+
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
108
319
|
};
|
|
109
|
-
|
|
110
|
-
|
|
320
|
+
if (authoredSpacing?.nodeEdge !== undefined) {
|
|
321
|
+
elkLayoutOptions['elk.spacing.edgeNode'] = String(authoredSpacing.nodeEdge);
|
|
322
|
+
}
|
|
323
|
+
if (authoredSpacing?.edgeEdge !== undefined) {
|
|
324
|
+
elkLayoutOptions['elk.spacing.edgeEdge'] = String(authoredSpacing.edgeEdge);
|
|
325
|
+
}
|
|
326
|
+
if (ir.layoutOptions?.edgeRouting) {
|
|
327
|
+
elkLayoutOptions['elk.edgeRouting'] = EDGE_ROUTING_MAPPING[ir.layoutOptions.edgeRouting];
|
|
328
|
+
}
|
|
329
|
+
const root = {
|
|
330
|
+
id: ELK_ROOT_ID,
|
|
111
331
|
layoutOptions: elkLayoutOptions,
|
|
112
|
-
children: ir.nodes.map((node) =>
|
|
113
|
-
edges:
|
|
332
|
+
children: ir.nodes.map((node) => nodeToELK(node, normalized, identifiers)),
|
|
333
|
+
edges: [],
|
|
114
334
|
};
|
|
115
|
-
|
|
335
|
+
distributeEdges(ir, root, identifiers);
|
|
336
|
+
return root;
|
|
116
337
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
id: node.id,
|
|
123
|
-
width: opts.nodeWidth,
|
|
124
|
-
height: opts.nodeHeight,
|
|
125
|
-
labels: node.label ? [{ text: node.label }] : undefined,
|
|
338
|
+
function buildElkIdentifiers(ir) {
|
|
339
|
+
const identifiers = {
|
|
340
|
+
nodes: new Map(),
|
|
341
|
+
ports: new Map(),
|
|
342
|
+
edges: new Map(),
|
|
126
343
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
344
|
+
let nodeIndex = 0;
|
|
345
|
+
let portIndex = 0;
|
|
346
|
+
visitNodes(ir.nodes, '/nodes', (node) => {
|
|
347
|
+
identifiers.nodes.set(node.id, `n${nodeIndex++}`);
|
|
348
|
+
for (const port of node.ports ?? []) {
|
|
349
|
+
identifiers.ports.set(portKey(node.id, port.id), `p${portIndex++}`);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
ir.edges.forEach((edge, index) => identifiers.edges.set(edge.id, `e${index}`));
|
|
353
|
+
return identifiers;
|
|
354
|
+
}
|
|
355
|
+
function distributeEdges(ir, root, identifiers) {
|
|
356
|
+
const ancestorPaths = new Map();
|
|
357
|
+
collectAncestorPaths(ir.nodes, ancestorPaths);
|
|
358
|
+
const elkNodes = new Map();
|
|
359
|
+
collectElkNodes(root, elkNodes);
|
|
360
|
+
for (const edge of ir.edges) {
|
|
361
|
+
const sourceAncestors = ancestorPaths.get(edge.source) ?? [];
|
|
362
|
+
const targetAncestors = ancestorPaths.get(edge.target) ?? [];
|
|
363
|
+
let owner = root;
|
|
364
|
+
for (let index = 0; index < Math.min(sourceAncestors.length, targetAncestors.length); index += 1) {
|
|
365
|
+
if (sourceAncestors[index] !== targetAncestors[index])
|
|
366
|
+
break;
|
|
367
|
+
owner = elkNodes.get(identifiers.nodes.get(sourceAncestors[index])) ?? root;
|
|
368
|
+
}
|
|
369
|
+
owner.edges = [...(owner.edges ?? []), edgeToELK(edge, identifiers)];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function collectAncestorPaths(nodes, target, ancestors = []) {
|
|
373
|
+
for (const node of nodes) {
|
|
374
|
+
target.set(node.id, ancestors);
|
|
375
|
+
if (node.children) {
|
|
376
|
+
collectAncestorPaths(node.children, target, [...ancestors, node.id]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function hasNestedNodes(nodes) {
|
|
381
|
+
return nodes.some((node) => Boolean(node.children?.length));
|
|
382
|
+
}
|
|
383
|
+
function nodeToELK(node, options, identifiers) {
|
|
384
|
+
const hasChildren = Boolean(node.children?.length);
|
|
385
|
+
const elkNode = { id: identifiers.nodes.get(node.id) };
|
|
386
|
+
if (!hasChildren) {
|
|
387
|
+
elkNode.width = node.dimensions?.width ?? options.nodeWidth;
|
|
388
|
+
elkNode.height = node.dimensions?.height ?? options.nodeHeight;
|
|
389
|
+
}
|
|
390
|
+
if (node.label)
|
|
391
|
+
elkNode.labels = [{ text: node.label }];
|
|
392
|
+
if (node.ports?.length) {
|
|
393
|
+
elkNode.ports = node.ports.map((port) => portToELK(node.id, port, identifiers));
|
|
130
394
|
elkNode.layoutOptions = {
|
|
131
395
|
...elkNode.layoutOptions,
|
|
132
396
|
'elk.portConstraints': 'FIXED_SIDE',
|
|
133
397
|
};
|
|
134
398
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
399
|
+
if (hasChildren) {
|
|
400
|
+
elkNode.children = node.children.map((child) => nodeToELK(child, options, identifiers));
|
|
401
|
+
const padding = node.layoutOptions?.padding ?? {};
|
|
138
402
|
elkNode.layoutOptions = {
|
|
139
403
|
...elkNode.layoutOptions,
|
|
140
|
-
'elk.
|
|
404
|
+
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
405
|
+
'elk.padding': `[top=${padding.top ?? 0},left=${padding.left ?? 0},bottom=${padding.bottom ?? 0},right=${padding.right ?? 0}]`,
|
|
141
406
|
};
|
|
142
407
|
}
|
|
143
408
|
return elkNode;
|
|
144
409
|
}
|
|
145
|
-
|
|
146
|
-
* Convert a Graph-IR port to ELK format
|
|
147
|
-
*/
|
|
148
|
-
function convertPortToELK(port) {
|
|
149
|
-
const sideMapping = {
|
|
150
|
-
NORTH: 'NORTH',
|
|
151
|
-
SOUTH: 'SOUTH',
|
|
152
|
-
EAST: 'EAST',
|
|
153
|
-
WEST: 'WEST',
|
|
154
|
-
};
|
|
410
|
+
function portToELK(ownerId, port, identifiers) {
|
|
155
411
|
return {
|
|
156
|
-
id: port.id,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Convert a Graph-IR edge to ELK format
|
|
164
|
-
*/
|
|
165
|
-
function convertEdgeToELK(edge) {
|
|
166
|
-
const elkEdge = {
|
|
167
|
-
id: edge.id,
|
|
168
|
-
sources: [edge.sourcePort ? `${edge.source}.${edge.sourcePort}` : edge.source],
|
|
169
|
-
targets: [edge.targetPort ? `${edge.target}.${edge.targetPort}` : edge.target],
|
|
412
|
+
id: identifiers.ports.get(portKey(ownerId, port.id)),
|
|
413
|
+
width: 1,
|
|
414
|
+
height: 1,
|
|
415
|
+
layoutOptions: { 'elk.port.side': port.side },
|
|
170
416
|
};
|
|
171
|
-
return elkEdge;
|
|
172
417
|
}
|
|
173
|
-
|
|
174
|
-
* Convert ELK result back to Graph-IR format
|
|
175
|
-
*/
|
|
176
|
-
export function elkToGraphIR(elkResult, originalIR, algorithm) {
|
|
177
|
-
// Build lookup for ELK nodes
|
|
178
|
-
const elkNodeMap = new Map();
|
|
179
|
-
collectElkNodes(elkResult, elkNodeMap);
|
|
180
|
-
// Build lookup for original edges
|
|
181
|
-
const originalEdgeMap = new Map(originalIR.edges.map((e) => [e.id, e]));
|
|
182
|
-
// Convert nodes
|
|
183
|
-
const layoutedNodes = convertElkNodesToGraphIR(originalIR.nodes, elkNodeMap);
|
|
184
|
-
// Convert edges
|
|
185
|
-
const layoutedEdges = convertElkEdgesToGraphIR(elkResult.edges || [], originalEdgeMap);
|
|
418
|
+
function edgeToELK(edge, identifiers) {
|
|
186
419
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
},
|
|
420
|
+
id: identifiers.edges.get(edge.id),
|
|
421
|
+
sources: [edge.sourcePort
|
|
422
|
+
? identifiers.ports.get(portKey(edge.source, edge.sourcePort))
|
|
423
|
+
: identifiers.nodes.get(edge.source)],
|
|
424
|
+
targets: [edge.targetPort
|
|
425
|
+
? identifiers.ports.get(portKey(edge.target, edge.targetPort))
|
|
426
|
+
: identifiers.nodes.get(edge.target)],
|
|
195
427
|
};
|
|
196
428
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
*/
|
|
200
|
-
function collectElkNodes(node, map) {
|
|
201
|
-
if (node.id !== 'root') {
|
|
202
|
-
map.set(node.id, node);
|
|
203
|
-
}
|
|
204
|
-
if (node.children) {
|
|
205
|
-
for (const child of node.children) {
|
|
206
|
-
collectElkNodes(child, map);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
429
|
+
function portKey(nodeId, portId) {
|
|
430
|
+
return JSON.stringify([nodeId, portId]);
|
|
209
431
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
432
|
+
export function verifyResolvedLayout(graph, root) {
|
|
433
|
+
const errors = [];
|
|
434
|
+
const identifiers = buildElkIdentifiers(graph);
|
|
435
|
+
const elkNodes = new Map();
|
|
436
|
+
const elkParents = new Map();
|
|
437
|
+
collectElkNodes(root, elkNodes, elkParents);
|
|
438
|
+
const authoredParents = new Map();
|
|
439
|
+
collectAuthoredParents(graph.nodes, authoredParents);
|
|
440
|
+
const elkEdges = new Map();
|
|
441
|
+
collectElkEdges(root, elkEdges);
|
|
442
|
+
if (!finiteNonnegative(root.width) || !finiteNonnegative(root.height)) {
|
|
443
|
+
errors.push({
|
|
444
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
445
|
+
path: '',
|
|
446
|
+
message: 'Resolved graph bounds must be finite and nonnegative',
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
visitNodes(graph.nodes, '/nodes', (node, path) => {
|
|
450
|
+
const resolved = elkNodes.get(identifiers.nodes.get(node.id));
|
|
451
|
+
if (!resolved) {
|
|
452
|
+
errors.push({ code: 'LAYOUT_VERIFICATION_FAILED', path, message: 'Layout omitted node' });
|
|
453
|
+
return;
|
|
230
454
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
455
|
+
if (!finite(resolved.x) || !finite(resolved.y) ||
|
|
456
|
+
!finiteNonnegative(resolved.width) || !finiteNonnegative(resolved.height)) {
|
|
457
|
+
errors.push({
|
|
458
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
459
|
+
path,
|
|
460
|
+
message: 'Resolved node rectangle must be finite with nonnegative size',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const resolvedParent = elkParents.get(identifiers.nodes.get(node.id));
|
|
464
|
+
const expectedParent = authoredParents.get(node.id);
|
|
465
|
+
const expectedElkParent = expectedParent === undefined
|
|
466
|
+
? undefined
|
|
467
|
+
: identifiers.nodes.get(expectedParent);
|
|
468
|
+
if (resolvedParent !== expectedElkParent) {
|
|
469
|
+
errors.push({
|
|
470
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
471
|
+
path,
|
|
472
|
+
message: 'Resolved containment differs from authored children',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!node.children?.length && node.dimensions) {
|
|
476
|
+
if (resolved.width !== node.dimensions.width || resolved.height !== node.dimensions.height) {
|
|
477
|
+
errors.push({
|
|
478
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
479
|
+
path: `${path}/dimensions`,
|
|
480
|
+
message: 'Resolved leaf outer size differs from authored fixed dimensions',
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (node.children?.length && node.layoutOptions?.padding) {
|
|
485
|
+
const padding = node.layoutOptions.padding;
|
|
486
|
+
for (const child of resolved.children ?? []) {
|
|
487
|
+
const x = child.x ?? 0;
|
|
488
|
+
const y = child.y ?? 0;
|
|
489
|
+
const width = child.width ?? 0;
|
|
490
|
+
const height = child.height ?? 0;
|
|
491
|
+
if (x < (padding.left ?? 0) ||
|
|
492
|
+
y < (padding.top ?? 0) ||
|
|
493
|
+
x + width > (resolved.width ?? 0) - (padding.right ?? 0) ||
|
|
494
|
+
y + height > (resolved.height ?? 0) - (padding.bottom ?? 0)) {
|
|
495
|
+
errors.push({
|
|
496
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
497
|
+
path: `${path}/layoutOptions/padding`,
|
|
498
|
+
message: 'Resolved child bounds violate authored container padding',
|
|
499
|
+
});
|
|
500
|
+
break;
|
|
247
501
|
}
|
|
248
|
-
points.push(section.endPoint);
|
|
249
502
|
}
|
|
250
503
|
}
|
|
504
|
+
node.ports?.forEach((port, portIndex) => {
|
|
505
|
+
const resolvedPort = resolved.ports?.find((candidate) => candidate.id === identifiers.ports.get(portKey(node.id, port.id)));
|
|
506
|
+
const portPath = `${path}/ports/${portIndex}`;
|
|
507
|
+
if (!resolvedPort) {
|
|
508
|
+
errors.push({
|
|
509
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
510
|
+
path: portPath,
|
|
511
|
+
message: 'Layout omitted authored port',
|
|
512
|
+
});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!finite(resolvedPort.x) || !finite(resolvedPort.y) ||
|
|
516
|
+
!finiteNonnegative(resolvedPort.width) || !finiteNonnegative(resolvedPort.height)) {
|
|
517
|
+
errors.push({
|
|
518
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
519
|
+
path: portPath,
|
|
520
|
+
message: 'Resolved port geometry must be finite with nonnegative size',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (!portRemainsOnSide(resolved, resolvedPort, port.side)) {
|
|
524
|
+
errors.push({
|
|
525
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
526
|
+
path: `${portPath}/side`,
|
|
527
|
+
message: `Resolved port is not on authored ${port.side} side`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
graph.edges.forEach((edge, index) => {
|
|
533
|
+
const resolved = elkEdges.get(identifiers.edges.get(edge.id));
|
|
534
|
+
if (!resolved) {
|
|
535
|
+
errors.push({
|
|
536
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
537
|
+
path: `/edges/${index}`,
|
|
538
|
+
message: 'Layout omitted authored edge',
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const sections = resolved.edge.sections ?? [];
|
|
543
|
+
const points = sections.flatMap((section) => [
|
|
544
|
+
section.startPoint,
|
|
545
|
+
...(section.bendPoints ?? []),
|
|
546
|
+
section.endPoint,
|
|
547
|
+
]);
|
|
548
|
+
if (sections.length === 0 || points.some((point) => !finite(point.x) || !finite(point.y))) {
|
|
549
|
+
errors.push({
|
|
550
|
+
code: 'LAYOUT_VERIFICATION_FAILED',
|
|
551
|
+
path: `/edges/${index}`,
|
|
552
|
+
message: 'Resolved edge route must contain finite section points',
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
return errors;
|
|
557
|
+
}
|
|
558
|
+
function portRemainsOnSide(node, port, side) {
|
|
559
|
+
if (node.width === undefined || node.height === undefined ||
|
|
560
|
+
port.x === undefined || port.y === undefined)
|
|
561
|
+
return false;
|
|
562
|
+
const centerX = port.x + (port.width ?? 0) / 2;
|
|
563
|
+
const centerY = port.y + (port.height ?? 0) / 2;
|
|
564
|
+
const epsilonX = (port.width ?? 0) / 2 + 1e-6;
|
|
565
|
+
const epsilonY = (port.height ?? 0) / 2 + 1e-6;
|
|
566
|
+
const withinHorizontalSpan = centerX >= -1e-6 && centerX <= node.width + 1e-6;
|
|
567
|
+
const withinVerticalSpan = centerY >= -1e-6 && centerY <= node.height + 1e-6;
|
|
568
|
+
if (side === 'WEST')
|
|
569
|
+
return Math.abs(centerX) <= epsilonX && withinVerticalSpan;
|
|
570
|
+
if (side === 'EAST') {
|
|
571
|
+
return Math.abs(centerX - node.width) <= epsilonX && withinVerticalSpan;
|
|
572
|
+
}
|
|
573
|
+
if (side === 'NORTH')
|
|
574
|
+
return Math.abs(centerY) <= epsilonY && withinHorizontalSpan;
|
|
575
|
+
return Math.abs(centerY - node.height) <= epsilonY && withinHorizontalSpan;
|
|
576
|
+
}
|
|
577
|
+
export function elkToResolvedLayout(elkResult, original, algorithm) {
|
|
578
|
+
const identifiers = buildElkIdentifiers(original);
|
|
579
|
+
const nodeMap = new Map();
|
|
580
|
+
collectElkNodes(elkResult, nodeMap);
|
|
581
|
+
const edgeMap = new Map();
|
|
582
|
+
collectElkEdges(elkResult, edgeMap);
|
|
583
|
+
return {
|
|
584
|
+
graphId: original.id,
|
|
585
|
+
nodes: resolveNodes(original.nodes, nodeMap, identifiers),
|
|
586
|
+
edges: original.edges.flatMap((edge) => {
|
|
587
|
+
const resolved = edgeMap.get(identifiers.edges.get(edge.id)) ?? edgeMap.get(edge.id);
|
|
588
|
+
return resolved ? [resolveEdge(resolved, edge.id)] : [];
|
|
589
|
+
}),
|
|
590
|
+
bounds: { width: elkResult.width ?? 0, height: elkResult.height ?? 0 },
|
|
591
|
+
algorithm,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function resolveNodes(nodes, map, identifiers) {
|
|
595
|
+
return nodes.map((node) => {
|
|
596
|
+
const resolved = map.get(identifiers?.nodes.get(node.id) ?? node.id) ?? map.get(node.id);
|
|
251
597
|
return {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
598
|
+
id: node.id,
|
|
599
|
+
rectangle: {
|
|
600
|
+
x: resolved?.x ?? 0,
|
|
601
|
+
y: resolved?.y ?? 0,
|
|
602
|
+
width: resolved?.width ?? 0,
|
|
603
|
+
height: resolved?.height ?? 0,
|
|
258
604
|
},
|
|
605
|
+
children: node.children ? resolveNodes(node.children, map, identifiers) : undefined,
|
|
259
606
|
};
|
|
260
607
|
});
|
|
261
608
|
}
|
|
609
|
+
function resolveEdge(collected, authoredId) {
|
|
610
|
+
const { edge, offsetX, offsetY } = collected;
|
|
611
|
+
const points = [];
|
|
612
|
+
for (const section of edge.sections ?? []) {
|
|
613
|
+
points.push(absolutePoint(section.startPoint, offsetX, offsetY));
|
|
614
|
+
points.push(...(section.bendPoints ?? []).map((point) => absolutePoint(point, offsetX, offsetY)));
|
|
615
|
+
points.push(absolutePoint(section.endPoint, offsetX, offsetY));
|
|
616
|
+
}
|
|
617
|
+
return { id: authoredId, points };
|
|
618
|
+
}
|
|
619
|
+
function absolutePoint(point, offsetX, offsetY) {
|
|
620
|
+
return { x: point.x + offsetX, y: point.y + offsetY };
|
|
621
|
+
}
|
|
622
|
+
function collectElkNodes(node, target, parents, parentId) {
|
|
623
|
+
if (node.id !== ELK_ROOT_ID)
|
|
624
|
+
target.set(node.id, node);
|
|
625
|
+
if (node.id !== ELK_ROOT_ID)
|
|
626
|
+
parents?.set(node.id, parentId);
|
|
627
|
+
const childParent = node.id === ELK_ROOT_ID ? undefined : node.id;
|
|
628
|
+
for (const child of node.children ?? []) {
|
|
629
|
+
collectElkNodes(child, target, parents, childParent);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function collectElkEdges(node, target, parentX = 0, parentY = 0) {
|
|
633
|
+
const offsetX = parentX + (node.id === ELK_ROOT_ID ? 0 : node.x ?? 0);
|
|
634
|
+
const offsetY = parentY + (node.id === ELK_ROOT_ID ? 0 : node.y ?? 0);
|
|
635
|
+
for (const edge of node.edges ?? []) {
|
|
636
|
+
target.set(edge.id, { edge, offsetX, offsetY });
|
|
637
|
+
}
|
|
638
|
+
for (const child of node.children ?? []) {
|
|
639
|
+
collectElkEdges(child, target, offsetX, offsetY);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function finite(value) {
|
|
643
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
644
|
+
}
|
|
645
|
+
function finiteNonnegative(value) {
|
|
646
|
+
return finite(value) && value >= 0;
|
|
647
|
+
}
|
|
648
|
+
function collectAuthoredParents(nodes, target, parentId) {
|
|
649
|
+
for (const node of nodes) {
|
|
650
|
+
target.set(node.id, parentId);
|
|
651
|
+
if (node.children)
|
|
652
|
+
collectAuthoredParents(node.children, target, node.id);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function visitNodes(nodes, base, visit) {
|
|
656
|
+
nodes.forEach((node, index) => {
|
|
657
|
+
const path = `${base}/${index}`;
|
|
658
|
+
visit(node, path);
|
|
659
|
+
if (node.children)
|
|
660
|
+
visitNodes(node.children, `${path}/children`, visit);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function failed(algorithm, errors) {
|
|
664
|
+
return { success: false, errors, algorithm };
|
|
665
|
+
}
|
|
262
666
|
//# sourceMappingURL=layout.js.map
|