@crazyhappyone/auto-graph 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/README.zh-CN.md +154 -0
- package/dist/cli/index.cjs +2736 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2734 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2804 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +514 -0
- package/dist/index.d.ts +514 -0
- package/dist/index.js +2766 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2804 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pretext = require('@chenglou/pretext');
|
|
4
|
+
var buffer = require('buffer');
|
|
5
|
+
var yaml = require('yaml');
|
|
6
|
+
var zod = require('zod');
|
|
7
|
+
var dagre = require('@dagrejs/dagre');
|
|
8
|
+
|
|
9
|
+
// src/geometry/boxes.ts
|
|
10
|
+
function normalizeInsets(input = 0) {
|
|
11
|
+
if (typeof input === "number") {
|
|
12
|
+
validateMargin(input, "margin");
|
|
13
|
+
return {
|
|
14
|
+
top: input,
|
|
15
|
+
right: input,
|
|
16
|
+
bottom: input,
|
|
17
|
+
left: input
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
validateMargin(input.top, "insets.top");
|
|
21
|
+
validateMargin(input.right, "insets.right");
|
|
22
|
+
validateMargin(input.bottom, "insets.bottom");
|
|
23
|
+
validateMargin(input.left, "insets.left");
|
|
24
|
+
return {
|
|
25
|
+
top: input.top,
|
|
26
|
+
right: input.right,
|
|
27
|
+
bottom: input.bottom,
|
|
28
|
+
left: input.left
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function validateBox(box, label = "box") {
|
|
32
|
+
validateFinite(box.x, `${label}.x`);
|
|
33
|
+
validateFinite(box.y, `${label}.y`);
|
|
34
|
+
validateFinite(box.width, `${label}.width`);
|
|
35
|
+
validateFinite(box.height, `${label}.height`);
|
|
36
|
+
if (box.width < 0 || box.height < 0) {
|
|
37
|
+
throw new TypeError(`${label} dimensions must be non-negative`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function boxCenter(box) {
|
|
41
|
+
validateBox(box);
|
|
42
|
+
return {
|
|
43
|
+
x: box.x + box.width / 2,
|
|
44
|
+
y: box.y + box.height / 2
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function expandBox(box, margin) {
|
|
48
|
+
validateBox(box);
|
|
49
|
+
const insets = normalizeInsets(margin);
|
|
50
|
+
return {
|
|
51
|
+
x: box.x - insets.left,
|
|
52
|
+
y: box.y - insets.top,
|
|
53
|
+
width: box.width + insets.left + insets.right,
|
|
54
|
+
height: box.height + insets.top + insets.bottom
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function unionBoxes(boxes) {
|
|
58
|
+
if (boxes.length === 0) {
|
|
59
|
+
throw new TypeError("Cannot union empty box collection");
|
|
60
|
+
}
|
|
61
|
+
for (const [index, box] of boxes.entries()) {
|
|
62
|
+
validateBox(box, `boxes[${index}]`);
|
|
63
|
+
}
|
|
64
|
+
const minX = Math.min(...boxes.map((box) => box.x));
|
|
65
|
+
const minY = Math.min(...boxes.map((box) => box.y));
|
|
66
|
+
const maxX = Math.max(...boxes.map((box) => box.x + box.width));
|
|
67
|
+
const maxY = Math.max(...boxes.map((box) => box.y + box.height));
|
|
68
|
+
return {
|
|
69
|
+
x: minX,
|
|
70
|
+
y: minY,
|
|
71
|
+
width: maxX - minX,
|
|
72
|
+
height: maxY - minY
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function intersectsAabb(a, b) {
|
|
76
|
+
validateBox(a, "a");
|
|
77
|
+
validateBox(b, "b");
|
|
78
|
+
return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
79
|
+
}
|
|
80
|
+
function validateMargin(value, label) {
|
|
81
|
+
validateFinite(value, label);
|
|
82
|
+
if (value < 0) {
|
|
83
|
+
throw new TypeError(`${label} must be non-negative`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function validateFinite(value, label) {
|
|
87
|
+
if (!Number.isFinite(value)) {
|
|
88
|
+
throw new TypeError(`${label} must be finite`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/constraints/solver.ts
|
|
93
|
+
function applyLayoutConstraints(input) {
|
|
94
|
+
const diagnostics = [];
|
|
95
|
+
const boxes = cloneValidBoxes(input.boxes, diagnostics);
|
|
96
|
+
const locks = /* @__PURE__ */ new Map();
|
|
97
|
+
const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
|
|
98
|
+
applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
|
|
99
|
+
applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
|
|
100
|
+
applyContainment(input.constraints, boxes, locks, diagnostics);
|
|
101
|
+
applyRelative(input.constraints, boxes, locks, diagnostics);
|
|
102
|
+
applyAlign(input.constraints, boxes, locks, diagnostics);
|
|
103
|
+
applyDistribute(input.constraints, boxes, locks, diagnostics);
|
|
104
|
+
repairOverlaps(input, boxes, locks, diagnostics);
|
|
105
|
+
return { boxes, locks, diagnostics };
|
|
106
|
+
}
|
|
107
|
+
function cloneValidBoxes(input, diagnostics) {
|
|
108
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const [id, box] of input) {
|
|
110
|
+
if (isFiniteBox(box)) {
|
|
111
|
+
boxes.set(id, { ...box });
|
|
112
|
+
} else {
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
severity: "error",
|
|
115
|
+
code: "constraints.position.invalid",
|
|
116
|
+
message: `Box ${id} contains invalid coordinates.`,
|
|
117
|
+
path: ["boxes", id],
|
|
118
|
+
detail: { nodeId: id }
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return boxes;
|
|
123
|
+
}
|
|
124
|
+
function applyFixedPositionLocks(nodes, boxes, locks, diagnostics) {
|
|
125
|
+
for (const node of nodes) {
|
|
126
|
+
if (node.position === void 0) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const box = boxes.get(node.id);
|
|
130
|
+
if (box === void 0) {
|
|
131
|
+
missingReference(diagnostics, "node", node.id);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!isFinitePoint(node.position)) {
|
|
135
|
+
diagnostics.push({
|
|
136
|
+
severity: "error",
|
|
137
|
+
code: "constraints.position.invalid",
|
|
138
|
+
message: `Fixed position for ${node.id} is invalid.`,
|
|
139
|
+
path: ["nodes", node.id, "position"],
|
|
140
|
+
detail: { nodeId: node.id }
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
boxes.set(node.id, { ...box, x: node.position.x, y: node.position.y });
|
|
145
|
+
locks.set(node.id, { nodeId: node.id, source: "fixed-position" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
|
|
149
|
+
for (const constraint of constraints) {
|
|
150
|
+
if (constraint.kind !== "exact-position") {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const targetId = constraintTargetId(constraint);
|
|
154
|
+
if (targetId === void 0 || !nodeById.has(targetId)) {
|
|
155
|
+
missingReference(diagnostics, "target", targetId);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const box = boxes.get(targetId);
|
|
159
|
+
if (box === void 0) {
|
|
160
|
+
missingReference(diagnostics, "box", targetId);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!isFinitePoint(constraint.position)) {
|
|
164
|
+
diagnostics.push({
|
|
165
|
+
severity: "error",
|
|
166
|
+
code: "constraints.position.invalid",
|
|
167
|
+
message: `Exact position for ${targetId} is invalid.`,
|
|
168
|
+
path: ["constraints", constraint.id ?? targetId, "position"],
|
|
169
|
+
detail: { nodeId: targetId }
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const existingLock = locks.get(targetId);
|
|
174
|
+
if (existingLock !== void 0 && (box.x !== constraint.position.x || box.y !== constraint.position.y)) {
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
severity: "error",
|
|
177
|
+
code: "constraints.conflict.exact-position",
|
|
178
|
+
message: `Exact position conflicts with existing lock for ${targetId}.`,
|
|
179
|
+
path: ["constraints", constraint.id ?? targetId],
|
|
180
|
+
detail: { nodeId: targetId, source: existingLock.source }
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
boxes.set(targetId, {
|
|
185
|
+
...box,
|
|
186
|
+
x: constraint.position.x,
|
|
187
|
+
y: constraint.position.y
|
|
188
|
+
});
|
|
189
|
+
locks.set(targetId, { nodeId: targetId, source: "exact-position" });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function applyContainment(constraints, boxes, locks, diagnostics) {
|
|
193
|
+
for (const constraint of constraints) {
|
|
194
|
+
if (constraint.kind !== "containment") {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const container = boxes.get(constraint.containerId);
|
|
198
|
+
if (container === void 0) {
|
|
199
|
+
missingReference(diagnostics, "container", constraint.containerId);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const content = contentBox(container, constraint.padding);
|
|
203
|
+
for (const childId of constraint.childIds) {
|
|
204
|
+
const child = boxes.get(childId);
|
|
205
|
+
if (child === void 0) {
|
|
206
|
+
missingReference(diagnostics, "child", childId);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const next = moveInside(child, content);
|
|
210
|
+
if (samePosition(child, next)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (locks.has(childId)) {
|
|
214
|
+
diagnostics.push({
|
|
215
|
+
severity: "warning",
|
|
216
|
+
code: "constraints.locked-target-not-moved",
|
|
217
|
+
message: `Locked child ${childId} was not moved into containment.`,
|
|
218
|
+
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
219
|
+
detail: { nodeId: childId }
|
|
220
|
+
});
|
|
221
|
+
if (!isInside(child, content)) {
|
|
222
|
+
diagnostics.push({
|
|
223
|
+
severity: "error",
|
|
224
|
+
code: "constraints.containment.impossible",
|
|
225
|
+
message: `Locked child ${childId} cannot fit inside ${constraint.containerId}.`,
|
|
226
|
+
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
227
|
+
detail: { nodeId: childId, containerId: constraint.containerId }
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (next.width > content.width || next.height > content.height) {
|
|
233
|
+
diagnostics.push({
|
|
234
|
+
severity: "error",
|
|
235
|
+
code: "constraints.containment.impossible",
|
|
236
|
+
message: `Child ${childId} cannot fit inside ${constraint.containerId}.`,
|
|
237
|
+
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
238
|
+
detail: { nodeId: childId, containerId: constraint.containerId }
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
boxes.set(childId, next);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function applyRelative(constraints, boxes, locks, diagnostics) {
|
|
247
|
+
for (const constraint of constraints) {
|
|
248
|
+
if (constraint.kind !== "relative-position") {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const source = boxes.get(constraint.sourceId);
|
|
252
|
+
const reference = boxes.get(constraint.referenceId);
|
|
253
|
+
if (source === void 0) {
|
|
254
|
+
missingReference(diagnostics, "source", constraint.sourceId);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (reference === void 0) {
|
|
258
|
+
missingReference(diagnostics, "reference", constraint.referenceId);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const next = relativeBox(source, reference, constraint);
|
|
262
|
+
setUnlockedBox(
|
|
263
|
+
constraint.sourceId,
|
|
264
|
+
next,
|
|
265
|
+
boxes,
|
|
266
|
+
locks,
|
|
267
|
+
diagnostics,
|
|
268
|
+
constraint
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function applyAlign(constraints, boxes, locks, diagnostics) {
|
|
273
|
+
for (const constraint of constraints) {
|
|
274
|
+
if (constraint.kind !== "align") {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const targets = collectTargets(constraint.targetIds, boxes, diagnostics);
|
|
278
|
+
const anchor = targets[0];
|
|
279
|
+
if (anchor === void 0) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const value = alignmentValue(anchor.box, constraint.axis);
|
|
283
|
+
for (const { id, box } of targets.slice(1)) {
|
|
284
|
+
const next = alignBox(box, constraint.axis, value);
|
|
285
|
+
setUnlockedBox(id, next, boxes, locks, diagnostics, constraint);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function applyDistribute(constraints, boxes, locks, diagnostics) {
|
|
290
|
+
for (const constraint of constraints) {
|
|
291
|
+
if (constraint.kind !== "distribute") {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const targets = collectTargets(
|
|
295
|
+
constraint.targetIds,
|
|
296
|
+
boxes,
|
|
297
|
+
diagnostics
|
|
298
|
+
).sort((a, b) => {
|
|
299
|
+
const delta = constraint.axis === "horizontal" ? a.box.x - b.box.x : a.box.y - b.box.y;
|
|
300
|
+
return delta === 0 ? a.id.localeCompare(b.id) : delta;
|
|
301
|
+
});
|
|
302
|
+
if (targets.length < 3) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const first = targets[0];
|
|
306
|
+
const last = targets[targets.length - 1];
|
|
307
|
+
if (first === void 0 || last === void 0) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const spacing = constraint.spacing ?? (distributionStart(last.box, constraint.axis) - distributionStart(first.box, constraint.axis)) / (targets.length - 1);
|
|
311
|
+
for (const [index, target] of targets.slice(1, -1).entries()) {
|
|
312
|
+
const nextStart = distributionStart(first.box, constraint.axis) + spacing * (index + 1);
|
|
313
|
+
const next = constraint.axis === "horizontal" ? { ...target.box, x: nextStart } : { ...target.box, y: nextStart };
|
|
314
|
+
setUnlockedBox(target.id, next, boxes, locks, diagnostics, constraint);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function repairOverlaps(input, boxes, locks, diagnostics) {
|
|
319
|
+
const spacing = input.overlapSpacing ?? 40;
|
|
320
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
321
|
+
const ids = [...boxes.keys()].sort();
|
|
322
|
+
for (const firstId of ids) {
|
|
323
|
+
for (const secondId of ids) {
|
|
324
|
+
if (firstId >= secondId) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const first = boxes.get(firstId);
|
|
328
|
+
const second = boxes.get(secondId);
|
|
329
|
+
if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const firstLocked = locks.has(firstId);
|
|
333
|
+
const secondLocked = locks.has(secondId);
|
|
334
|
+
if (firstLocked === secondLocked) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const movingId = firstLocked ? secondId : firstId;
|
|
338
|
+
const moving = firstLocked ? second : first;
|
|
339
|
+
const fixed = firstLocked ? first : second;
|
|
340
|
+
const moved = movePastOverlap(moving, fixed, axis, spacing);
|
|
341
|
+
boxes.set(movingId, moved);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const firstId of ids) {
|
|
345
|
+
for (const secondId of ids) {
|
|
346
|
+
if (firstId >= secondId) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const first = boxes.get(firstId);
|
|
350
|
+
const second = boxes.get(secondId);
|
|
351
|
+
if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
|
|
352
|
+
diagnostics.push({
|
|
353
|
+
severity: "warning",
|
|
354
|
+
code: "constraints.overlap.unresolved",
|
|
355
|
+
message: `Boxes ${firstId} and ${secondId} still overlap after stable sorted primary axis repair with configured spacing.`,
|
|
356
|
+
path: ["boxes"],
|
|
357
|
+
detail: { firstId, secondId }
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
|
|
364
|
+
const current = boxes.get(id);
|
|
365
|
+
if (current === void 0) {
|
|
366
|
+
missingReference(diagnostics, "target", id);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!isFiniteBox(next)) {
|
|
370
|
+
diagnostics.push({
|
|
371
|
+
severity: "error",
|
|
372
|
+
code: "constraints.position.invalid",
|
|
373
|
+
message: `Constraint produced an invalid position for ${id}.`,
|
|
374
|
+
path: ["constraints", constraint.id ?? id],
|
|
375
|
+
detail: { nodeId: id }
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (locks.has(id) && !samePosition(current, next)) {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
severity: "warning",
|
|
382
|
+
code: "constraints.locked-target-not-moved",
|
|
383
|
+
message: `Locked target ${id} was not moved by ${constraint.kind}.`,
|
|
384
|
+
path: ["constraints", constraint.id ?? id],
|
|
385
|
+
detail: { nodeId: id, constraintKind: constraint.kind }
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
boxes.set(id, next);
|
|
390
|
+
}
|
|
391
|
+
function constraintTargetId(constraint) {
|
|
392
|
+
return constraint.targetId ?? constraint.target?.id;
|
|
393
|
+
}
|
|
394
|
+
function collectTargets(ids, boxes, diagnostics) {
|
|
395
|
+
const targets = [];
|
|
396
|
+
for (const id of ids) {
|
|
397
|
+
const box = boxes.get(id);
|
|
398
|
+
if (box === void 0) {
|
|
399
|
+
missingReference(diagnostics, "target", id);
|
|
400
|
+
} else {
|
|
401
|
+
targets.push({ id, box });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return targets;
|
|
405
|
+
}
|
|
406
|
+
function relativeBox(source, reference, constraint) {
|
|
407
|
+
const offset = constraint.offset ?? { x: 0, y: 0 };
|
|
408
|
+
switch (constraint.relation) {
|
|
409
|
+
case "above":
|
|
410
|
+
return {
|
|
411
|
+
...source,
|
|
412
|
+
x: reference.x + offset.x,
|
|
413
|
+
y: reference.y - source.height + offset.y
|
|
414
|
+
};
|
|
415
|
+
case "right-of":
|
|
416
|
+
return {
|
|
417
|
+
...source,
|
|
418
|
+
x: reference.x + reference.width + offset.x,
|
|
419
|
+
y: reference.y + offset.y
|
|
420
|
+
};
|
|
421
|
+
case "below":
|
|
422
|
+
return {
|
|
423
|
+
...source,
|
|
424
|
+
x: reference.x + offset.x,
|
|
425
|
+
y: reference.y + reference.height + offset.y
|
|
426
|
+
};
|
|
427
|
+
case "left-of":
|
|
428
|
+
return {
|
|
429
|
+
...source,
|
|
430
|
+
x: reference.x - source.width + offset.x,
|
|
431
|
+
y: reference.y + offset.y
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function alignmentValue(box, axis) {
|
|
436
|
+
switch (axis) {
|
|
437
|
+
case "x":
|
|
438
|
+
case "left":
|
|
439
|
+
return box.x;
|
|
440
|
+
case "y":
|
|
441
|
+
case "top":
|
|
442
|
+
return box.y;
|
|
443
|
+
case "center-x":
|
|
444
|
+
return box.x + box.width / 2;
|
|
445
|
+
case "center-y":
|
|
446
|
+
return box.y + box.height / 2;
|
|
447
|
+
case "right":
|
|
448
|
+
return box.x + box.width;
|
|
449
|
+
case "bottom":
|
|
450
|
+
return box.y + box.height;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function alignBox(box, axis, value) {
|
|
454
|
+
switch (axis) {
|
|
455
|
+
case "x":
|
|
456
|
+
case "left":
|
|
457
|
+
return { ...box, x: value };
|
|
458
|
+
case "y":
|
|
459
|
+
case "top":
|
|
460
|
+
return { ...box, y: value };
|
|
461
|
+
case "center-x":
|
|
462
|
+
return { ...box, x: value - box.width / 2 };
|
|
463
|
+
case "center-y":
|
|
464
|
+
return { ...box, y: value - box.height / 2 };
|
|
465
|
+
case "right":
|
|
466
|
+
return { ...box, x: value - box.width };
|
|
467
|
+
case "bottom":
|
|
468
|
+
return { ...box, y: value - box.height };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function distributionStart(box, axis) {
|
|
472
|
+
return axis === "horizontal" ? box.x : box.y;
|
|
473
|
+
}
|
|
474
|
+
function contentBox(container, padding) {
|
|
475
|
+
const margin = padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
|
|
476
|
+
return {
|
|
477
|
+
x: container.x + margin.left,
|
|
478
|
+
y: container.y + margin.top,
|
|
479
|
+
width: container.width - margin.left - margin.right,
|
|
480
|
+
height: container.height - margin.top - margin.bottom
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function moveInside(child, container) {
|
|
484
|
+
return {
|
|
485
|
+
...child,
|
|
486
|
+
x: Math.min(
|
|
487
|
+
Math.max(child.x, container.x),
|
|
488
|
+
container.x + container.width - child.width
|
|
489
|
+
),
|
|
490
|
+
y: Math.min(
|
|
491
|
+
Math.max(child.y, container.y),
|
|
492
|
+
container.y + container.height - child.height
|
|
493
|
+
)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function isInside(child, container) {
|
|
497
|
+
return child.x >= container.x && child.y >= container.y && child.x + child.width <= container.x + container.width && child.y + child.height <= container.y + container.height;
|
|
498
|
+
}
|
|
499
|
+
function movePastOverlap(moving, fixed, primaryAxis, spacing) {
|
|
500
|
+
if (primaryAxis === "x") {
|
|
501
|
+
const movingCenter2 = moving.x + moving.width / 2;
|
|
502
|
+
const fixedCenter2 = fixed.x + fixed.width / 2;
|
|
503
|
+
const x = movingCenter2 >= fixedCenter2 ? fixed.x + fixed.width + spacing : fixed.x - moving.width - spacing;
|
|
504
|
+
return { ...moving, x };
|
|
505
|
+
}
|
|
506
|
+
const movingCenter = moving.y + moving.height / 2;
|
|
507
|
+
const fixedCenter = fixed.y + fixed.height / 2;
|
|
508
|
+
const y = movingCenter >= fixedCenter ? fixed.y + fixed.height + spacing : fixed.y - moving.height - spacing;
|
|
509
|
+
return { ...moving, y };
|
|
510
|
+
}
|
|
511
|
+
function samePosition(a, b) {
|
|
512
|
+
return a.x === b.x && a.y === b.y;
|
|
513
|
+
}
|
|
514
|
+
function isFiniteBox(box) {
|
|
515
|
+
try {
|
|
516
|
+
validateBox(box);
|
|
517
|
+
return true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function isFinitePoint(point2) {
|
|
523
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
524
|
+
}
|
|
525
|
+
function missingReference(diagnostics, referenceKind, id) {
|
|
526
|
+
diagnostics.push({
|
|
527
|
+
severity: "error",
|
|
528
|
+
code: "constraints.reference.missing",
|
|
529
|
+
message: `Missing ${referenceKind} reference${id === void 0 ? "" : `: ${id}`}.`,
|
|
530
|
+
path: ["constraints", referenceKind],
|
|
531
|
+
detail: id === void 0 ? {} : { id }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/dsl/diagnostics.ts
|
|
536
|
+
var SEVERITY_RANK = /* @__PURE__ */ new Map([
|
|
537
|
+
["error", 0],
|
|
538
|
+
["warning", 1],
|
|
539
|
+
["info", 2]
|
|
540
|
+
]);
|
|
541
|
+
var LAYER_RANK = /* @__PURE__ */ new Map([
|
|
542
|
+
["parse", 0],
|
|
543
|
+
["validate", 1],
|
|
544
|
+
["solve", 2],
|
|
545
|
+
["export", 3],
|
|
546
|
+
["io", 4]
|
|
547
|
+
]);
|
|
548
|
+
function sortDslDiagnostics(diagnostics) {
|
|
549
|
+
return [...diagnostics].sort((a, b) => {
|
|
550
|
+
const severityDelta = (SEVERITY_RANK.get(a.severity) ?? 99) - (SEVERITY_RANK.get(b.severity) ?? 99);
|
|
551
|
+
if (severityDelta !== 0) {
|
|
552
|
+
return severityDelta;
|
|
553
|
+
}
|
|
554
|
+
const layerDelta = (LAYER_RANK.get(a.layer) ?? 99) - (LAYER_RANK.get(b.layer) ?? 99);
|
|
555
|
+
if (layerDelta !== 0) {
|
|
556
|
+
return layerDelta;
|
|
557
|
+
}
|
|
558
|
+
const pathDelta = pathKey(a.path).localeCompare(pathKey(b.path));
|
|
559
|
+
if (pathDelta !== 0) {
|
|
560
|
+
return pathDelta;
|
|
561
|
+
}
|
|
562
|
+
return a.code.localeCompare(b.code);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function createSchemaDiagnostic(path, message) {
|
|
566
|
+
return {
|
|
567
|
+
severity: "error",
|
|
568
|
+
layer: "validate",
|
|
569
|
+
code: "validate.schema.invalid",
|
|
570
|
+
message,
|
|
571
|
+
path,
|
|
572
|
+
hint: hintForPath(path)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function createParseDiagnostic(code, message, hint) {
|
|
576
|
+
return {
|
|
577
|
+
severity: "error",
|
|
578
|
+
layer: "parse",
|
|
579
|
+
code,
|
|
580
|
+
message,
|
|
581
|
+
hint
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function hintForPath(path) {
|
|
585
|
+
const pathText = pathKey(path);
|
|
586
|
+
if (pathText.endsWith(".shape")) {
|
|
587
|
+
return "Use one of: rectangle, rounded-rectangle, ellipse, diamond, parallelogram, hexagon, cylinder.";
|
|
588
|
+
}
|
|
589
|
+
if (pathText.endsWith(".format")) {
|
|
590
|
+
return "Use output format svg or excalidraw.";
|
|
591
|
+
}
|
|
592
|
+
if (pathText.includes(".position.")) {
|
|
593
|
+
return "Use finite numeric x and y coordinates.";
|
|
594
|
+
}
|
|
595
|
+
return "Check the DSL value at this path against the supported schema.";
|
|
596
|
+
}
|
|
597
|
+
function pathKey(path) {
|
|
598
|
+
return (path ?? []).map(String).join(".");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/dsl/edges.ts
|
|
602
|
+
var EDGE_ID_PATTERN = /^[A-Za-z0-9_.:-]+$/;
|
|
603
|
+
var SHORTHAND_PATTERN = /^(.+?)\s*->\s*([^:]+)(?::(.*))?$/;
|
|
604
|
+
function parseEdgeShorthand(value, path) {
|
|
605
|
+
const match = SHORTHAND_PATTERN.exec(value.trim());
|
|
606
|
+
if (match === null) {
|
|
607
|
+
return invalidEdgeShorthand(path);
|
|
608
|
+
}
|
|
609
|
+
const sourceId = match[1]?.trim() ?? "";
|
|
610
|
+
const targetId = match[2]?.trim() ?? "";
|
|
611
|
+
const labelText = match[3]?.trim();
|
|
612
|
+
if (!isValidEdgeId(sourceId) || !isValidEdgeId(targetId)) {
|
|
613
|
+
return invalidEdgeShorthand(path);
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
edge: {
|
|
617
|
+
sourceId,
|
|
618
|
+
targetId,
|
|
619
|
+
...labelText === void 0 || labelText === "" ? {} : { label: { text: labelText } }
|
|
620
|
+
},
|
|
621
|
+
diagnostics: []
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function invalidEdgeShorthand(path) {
|
|
625
|
+
return {
|
|
626
|
+
diagnostics: [
|
|
627
|
+
{
|
|
628
|
+
severity: "error",
|
|
629
|
+
layer: "validate",
|
|
630
|
+
code: "validate.edge-shorthand.invalid",
|
|
631
|
+
message: "Invalid edge shorthand.",
|
|
632
|
+
path,
|
|
633
|
+
hint: 'Use "source -> target" or "source -> target: label".'
|
|
634
|
+
}
|
|
635
|
+
]
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function isValidEdgeId(value) {
|
|
639
|
+
return value.length > 0 && EDGE_ID_PATTERN.test(value);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/geometry/shapes.ts
|
|
643
|
+
var SUPPORTED_SHAPES = /* @__PURE__ */ new Set([
|
|
644
|
+
"rectangle",
|
|
645
|
+
"rounded-rectangle",
|
|
646
|
+
"ellipse",
|
|
647
|
+
"diamond",
|
|
648
|
+
"parallelogram",
|
|
649
|
+
"hexagon",
|
|
650
|
+
"cylinder"
|
|
651
|
+
]);
|
|
652
|
+
function computeShapeGeometry(input) {
|
|
653
|
+
validateShape(input.shape);
|
|
654
|
+
validateBox(input.box);
|
|
655
|
+
const box = { ...input.box };
|
|
656
|
+
return {
|
|
657
|
+
shape: input.shape,
|
|
658
|
+
box,
|
|
659
|
+
center: boxCenter(box),
|
|
660
|
+
anchors: createAnchors(box),
|
|
661
|
+
obstacleBox: expandBox(box, input.obstacleMargin ?? 0)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function getEdgePort(geometry, toward, preferredAnchor) {
|
|
665
|
+
validateShape(geometry.shape);
|
|
666
|
+
validateBox(geometry.box);
|
|
667
|
+
validatePoint(toward, "toward");
|
|
668
|
+
if (preferredAnchor !== void 0) {
|
|
669
|
+
const anchor = geometry.anchors.find((candidate) => {
|
|
670
|
+
return candidate.name === preferredAnchor;
|
|
671
|
+
});
|
|
672
|
+
if (anchor === void 0) {
|
|
673
|
+
throw new TypeError(`Unsupported anchor: ${preferredAnchor}`);
|
|
674
|
+
}
|
|
675
|
+
return { ...anchor.point };
|
|
676
|
+
}
|
|
677
|
+
if (geometry.shape === "rectangle" || geometry.shape === "rounded-rectangle") {
|
|
678
|
+
return rayToBox(geometry.box, toward);
|
|
679
|
+
}
|
|
680
|
+
return snapToNearestAnchor(geometry, toward);
|
|
681
|
+
}
|
|
682
|
+
function createAnchors(box) {
|
|
683
|
+
const left = box.x;
|
|
684
|
+
const right = box.x + box.width;
|
|
685
|
+
const top = box.y;
|
|
686
|
+
const bottom = box.y + box.height;
|
|
687
|
+
const center = boxCenter(box);
|
|
688
|
+
return [
|
|
689
|
+
{ name: "center", point: center },
|
|
690
|
+
{ name: "top", point: { x: center.x, y: top } },
|
|
691
|
+
{ name: "right", point: { x: right, y: center.y } },
|
|
692
|
+
{ name: "bottom", point: { x: center.x, y: bottom } },
|
|
693
|
+
{ name: "left", point: { x: left, y: center.y } },
|
|
694
|
+
{ name: "top-left", point: { x: left, y: top } },
|
|
695
|
+
{ name: "top-right", point: { x: right, y: top } },
|
|
696
|
+
{ name: "bottom-right", point: { x: right, y: bottom } },
|
|
697
|
+
{ name: "bottom-left", point: { x: left, y: bottom } }
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
function rayToBox(box, toward) {
|
|
701
|
+
const center = boxCenter(box);
|
|
702
|
+
const dx = toward.x - center.x;
|
|
703
|
+
const dy = toward.y - center.y;
|
|
704
|
+
if (dx === 0 && dy === 0) {
|
|
705
|
+
return center;
|
|
706
|
+
}
|
|
707
|
+
const halfWidth = box.width / 2;
|
|
708
|
+
const halfHeight = box.height / 2;
|
|
709
|
+
const scaleX = dx === 0 ? Number.POSITIVE_INFINITY : halfWidth / Math.abs(dx);
|
|
710
|
+
const scaleY = dy === 0 ? Number.POSITIVE_INFINITY : halfHeight / Math.abs(dy);
|
|
711
|
+
const scale = Math.min(scaleX, scaleY);
|
|
712
|
+
return clampPointToBox(
|
|
713
|
+
{
|
|
714
|
+
x: center.x + dx * scale,
|
|
715
|
+
y: center.y + dy * scale
|
|
716
|
+
},
|
|
717
|
+
box
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
function snapToNearestAnchor(geometry, toward) {
|
|
721
|
+
let best = geometry.anchors[0];
|
|
722
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
723
|
+
for (const anchor of geometry.anchors) {
|
|
724
|
+
if (anchor.name === "center") {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const distance = squaredDistance(anchor.point, toward);
|
|
728
|
+
if (distance < bestDistance) {
|
|
729
|
+
best = anchor;
|
|
730
|
+
bestDistance = distance;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (best === void 0) {
|
|
734
|
+
return { ...geometry.center };
|
|
735
|
+
}
|
|
736
|
+
return clampPointToBox(best.point, geometry.box);
|
|
737
|
+
}
|
|
738
|
+
function clampPointToBox(point2, box) {
|
|
739
|
+
return {
|
|
740
|
+
x: Math.min(Math.max(point2.x, box.x), box.x + box.width),
|
|
741
|
+
y: Math.min(Math.max(point2.y, box.y), box.y + box.height)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function squaredDistance(a, b) {
|
|
745
|
+
const dx = a.x - b.x;
|
|
746
|
+
const dy = a.y - b.y;
|
|
747
|
+
return dx * dx + dy * dy;
|
|
748
|
+
}
|
|
749
|
+
function validateShape(shape) {
|
|
750
|
+
if (!SUPPORTED_SHAPES.has(shape)) {
|
|
751
|
+
throw new TypeError(`Unsupported shape: ${shape}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function validatePoint(point2, label) {
|
|
755
|
+
if (!Number.isFinite(point2.x) || !Number.isFinite(point2.y)) {
|
|
756
|
+
throw new TypeError(`${label} point must be finite`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/geometry/containers.ts
|
|
761
|
+
function computeContainerGeometry(input) {
|
|
762
|
+
const childBounds = unionBoxes(input.childBoxes);
|
|
763
|
+
const padding = normalizeInsets(input.padding);
|
|
764
|
+
const minSize = normalizeMinSize(input.minSize);
|
|
765
|
+
const headerHeight = input.labelLayout?.fittedSize.height ?? input.labelLayout?.box.height ?? 0;
|
|
766
|
+
const intrinsicBox = {
|
|
767
|
+
x: childBounds.x - padding.left,
|
|
768
|
+
y: childBounds.y - padding.top - headerHeight,
|
|
769
|
+
width: childBounds.width + padding.left + padding.right,
|
|
770
|
+
height: childBounds.height + padding.top + padding.bottom + headerHeight
|
|
771
|
+
};
|
|
772
|
+
const box = {
|
|
773
|
+
...intrinsicBox,
|
|
774
|
+
width: Math.max(intrinsicBox.width, minSize.width ?? 0),
|
|
775
|
+
height: Math.max(intrinsicBox.height, minSize.height ?? 0)
|
|
776
|
+
};
|
|
777
|
+
const contentBox2 = {
|
|
778
|
+
x: childBounds.x,
|
|
779
|
+
y: childBounds.y,
|
|
780
|
+
width: childBounds.width,
|
|
781
|
+
height: childBounds.height
|
|
782
|
+
};
|
|
783
|
+
const shape = computeShapeGeometry({
|
|
784
|
+
shape: "rectangle",
|
|
785
|
+
box
|
|
786
|
+
});
|
|
787
|
+
const obstacleBox = expandBox(box, input.obstacleMargin ?? 0);
|
|
788
|
+
return {
|
|
789
|
+
id: input.id,
|
|
790
|
+
box,
|
|
791
|
+
contentBox: contentBox2,
|
|
792
|
+
childBounds,
|
|
793
|
+
...input.labelLayout === void 0 ? {} : { labelLayout: input.labelLayout },
|
|
794
|
+
anchors: shape.anchors,
|
|
795
|
+
obstacleBox,
|
|
796
|
+
diagnostics: []
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function normalizeMinSize(minSize = {}) {
|
|
800
|
+
if (minSize.width !== void 0) {
|
|
801
|
+
validateSize(minSize.width, "minSize.width");
|
|
802
|
+
}
|
|
803
|
+
if (minSize.height !== void 0) {
|
|
804
|
+
validateSize(minSize.height, "minSize.height");
|
|
805
|
+
}
|
|
806
|
+
return { ...minSize };
|
|
807
|
+
}
|
|
808
|
+
function validateSize(value, label) {
|
|
809
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
810
|
+
throw new TypeError(`${label} must be finite and non-negative`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/text/types.ts
|
|
815
|
+
function assertFinitePositive(value, label) {
|
|
816
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
817
|
+
throw new TypeError(`${label} must be finite and positive`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function assertFiniteNonNegative(value, label) {
|
|
821
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
822
|
+
throw new TypeError(`${label} must be a finite non-negative width`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function validateTextStyle(style) {
|
|
826
|
+
assertFinitePositive(style.fontSize, "fontSize");
|
|
827
|
+
if (style.lineHeight !== void 0) {
|
|
828
|
+
assertFinitePositive(style.lineHeight, "lineHeight");
|
|
829
|
+
}
|
|
830
|
+
if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
|
|
831
|
+
throw new TypeError("letterSpacing must be finite");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function resolveLineHeight(style) {
|
|
835
|
+
validateTextStyle(style);
|
|
836
|
+
return style.lineHeight ?? style.fontSize * 1.2;
|
|
837
|
+
}
|
|
838
|
+
function toCanvasFont(style) {
|
|
839
|
+
validateTextStyle(style);
|
|
840
|
+
const fontStyle = style.fontStyle === "italic" ? "italic " : "";
|
|
841
|
+
const fontWeight = style.fontWeight ?? 400;
|
|
842
|
+
return `${fontStyle}${fontWeight} ${style.fontSize}px ${style.fontFamily}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/text/fallback.ts
|
|
846
|
+
var DeterministicTextMeasurer = class {
|
|
847
|
+
prepare(text, style) {
|
|
848
|
+
validateTextStyle(style);
|
|
849
|
+
return {
|
|
850
|
+
text,
|
|
851
|
+
font: toCanvasFont(style),
|
|
852
|
+
style: { ...style },
|
|
853
|
+
backend: "deterministic"
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
|
|
857
|
+
assertFiniteNonNegative(maxWidth, "maxWidth");
|
|
858
|
+
assertFinitePositiveLineHeight(lineHeight);
|
|
859
|
+
const lines = this.wrap(prepared, maxWidth);
|
|
860
|
+
const width = lines.reduce(
|
|
861
|
+
(current, line) => Math.max(current, line.width),
|
|
862
|
+
0
|
|
863
|
+
);
|
|
864
|
+
return {
|
|
865
|
+
width,
|
|
866
|
+
height: lines.length * lineHeight,
|
|
867
|
+
lineHeight,
|
|
868
|
+
lineCount: lines.length,
|
|
869
|
+
lines,
|
|
870
|
+
diagnostics: []
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
naturalWidth(prepared) {
|
|
874
|
+
const charWidth = getCharacterWidth(prepared.style);
|
|
875
|
+
return prepared.text.split("\n").reduce((width, line) => {
|
|
876
|
+
return Math.max(width, line.length * charWidth);
|
|
877
|
+
}, 0);
|
|
878
|
+
}
|
|
879
|
+
wrap(prepared, maxWidth) {
|
|
880
|
+
const charWidth = getCharacterWidth(prepared.style);
|
|
881
|
+
const sourceLines = prepared.text.split("\n");
|
|
882
|
+
const output = [];
|
|
883
|
+
let segmentIndex = 0;
|
|
884
|
+
for (const sourceLine of sourceLines) {
|
|
885
|
+
if (sourceLine.length === 0) {
|
|
886
|
+
output.push(createLine("", 0, segmentIndex, 0, 0));
|
|
887
|
+
segmentIndex += 1;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const maxChars = maxWidth <= 0 ? 1 : Math.max(1, Math.floor(maxWidth / charWidth));
|
|
891
|
+
for (let start = 0; start < sourceLine.length; start += maxChars) {
|
|
892
|
+
const text = sourceLine.slice(start, start + maxChars);
|
|
893
|
+
output.push(
|
|
894
|
+
createLine(
|
|
895
|
+
text,
|
|
896
|
+
text.length * charWidth,
|
|
897
|
+
segmentIndex,
|
|
898
|
+
start,
|
|
899
|
+
start + text.length
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
segmentIndex += 1;
|
|
904
|
+
}
|
|
905
|
+
if (output.length === 0) {
|
|
906
|
+
output.push(createLine("", 0, 0, 0, 0));
|
|
907
|
+
}
|
|
908
|
+
return output;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
function getCharacterWidth(style) {
|
|
912
|
+
const letterSpacing = style.letterSpacing ?? 0;
|
|
913
|
+
return Math.max(0, style.fontSize * 0.6 + letterSpacing);
|
|
914
|
+
}
|
|
915
|
+
function createLine(text, width, segmentIndex, start, end) {
|
|
916
|
+
return {
|
|
917
|
+
text,
|
|
918
|
+
width,
|
|
919
|
+
start: {
|
|
920
|
+
segmentIndex,
|
|
921
|
+
graphemeIndex: start
|
|
922
|
+
},
|
|
923
|
+
end: {
|
|
924
|
+
segmentIndex,
|
|
925
|
+
graphemeIndex: end
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function assertFinitePositiveLineHeight(lineHeight) {
|
|
930
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
931
|
+
throw new TypeError("lineHeight must be finite and positive");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
|
|
935
|
+
function isPretextRuntimeAvailable() {
|
|
936
|
+
return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
|
|
937
|
+
}
|
|
938
|
+
var PretextTextMeasurer = class {
|
|
939
|
+
prepare(text, style) {
|
|
940
|
+
if (!isPretextRuntimeAvailable()) {
|
|
941
|
+
throw new TypeError(RUNTIME_UNAVAILABLE);
|
|
942
|
+
}
|
|
943
|
+
validateTextStyle(style);
|
|
944
|
+
const font = toCanvasFont(style);
|
|
945
|
+
const options = {
|
|
946
|
+
...style.whiteSpace === void 0 ? {} : { whiteSpace: style.whiteSpace },
|
|
947
|
+
...style.wordBreak === void 0 ? {} : { wordBreak: style.wordBreak },
|
|
948
|
+
...style.letterSpacing === void 0 ? {} : { letterSpacing: style.letterSpacing }
|
|
949
|
+
};
|
|
950
|
+
const prepared = pretext.prepareWithSegments(text, font, options);
|
|
951
|
+
return {
|
|
952
|
+
text,
|
|
953
|
+
font,
|
|
954
|
+
style: { ...style },
|
|
955
|
+
backend: "pretext",
|
|
956
|
+
pretextPrepared: prepared
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
|
|
960
|
+
assertFiniteNonNegative(maxWidth, "maxWidth");
|
|
961
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
962
|
+
throw new TypeError("lineHeight must be finite and positive");
|
|
963
|
+
}
|
|
964
|
+
const result = pretext.layoutWithLines(
|
|
965
|
+
toInternalPrepared(prepared),
|
|
966
|
+
maxWidth,
|
|
967
|
+
lineHeight
|
|
968
|
+
);
|
|
969
|
+
const width = result.lines.reduce(
|
|
970
|
+
(current, line) => Math.max(current, line.width),
|
|
971
|
+
0
|
|
972
|
+
);
|
|
973
|
+
return {
|
|
974
|
+
width,
|
|
975
|
+
height: result.height,
|
|
976
|
+
lineHeight,
|
|
977
|
+
lineCount: result.lineCount,
|
|
978
|
+
lines: result.lines.map((line) => ({
|
|
979
|
+
text: line.text,
|
|
980
|
+
width: line.width,
|
|
981
|
+
start: {
|
|
982
|
+
segmentIndex: line.start.segmentIndex,
|
|
983
|
+
graphemeIndex: line.start.graphemeIndex
|
|
984
|
+
},
|
|
985
|
+
end: {
|
|
986
|
+
segmentIndex: line.end.segmentIndex,
|
|
987
|
+
graphemeIndex: line.end.graphemeIndex
|
|
988
|
+
}
|
|
989
|
+
})),
|
|
990
|
+
diagnostics: []
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
naturalWidth(prepared) {
|
|
994
|
+
return pretext.measureNaturalWidth(toInternalPrepared(prepared));
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
function toInternalPrepared(prepared) {
|
|
998
|
+
if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
|
|
999
|
+
throw new TypeError("prepared text was not created by PretextTextMeasurer");
|
|
1000
|
+
}
|
|
1001
|
+
return prepared.pretextPrepared;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/labels/fit.ts
|
|
1005
|
+
function fitLabel(text, options, measurer) {
|
|
1006
|
+
return computeLabelLayout(text, options, measurer);
|
|
1007
|
+
}
|
|
1008
|
+
var LabelFitter = class {
|
|
1009
|
+
constructor(measurer) {
|
|
1010
|
+
this.measurer = measurer;
|
|
1011
|
+
}
|
|
1012
|
+
measurer;
|
|
1013
|
+
fit(text, options) {
|
|
1014
|
+
return computeLabelLayout(text, options, this.measurer);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function computeLabelLayout(text, options, measurer) {
|
|
1018
|
+
const padding = normalizeInsets(options.padding);
|
|
1019
|
+
const minSize = normalizeMinSize2(options.minSize);
|
|
1020
|
+
const lineHeight = resolveLineHeight(options.font);
|
|
1021
|
+
const maxWidth = normalizeMaxWidth(options.maxWidth);
|
|
1022
|
+
const prepared = measurer.prepare(text, options.font);
|
|
1023
|
+
const naturalTextWidth = measurer.naturalWidth(prepared);
|
|
1024
|
+
const contentMaxWidth = maxWidth === void 0 ? naturalTextWidth : Math.max(0, maxWidth - padding.left - padding.right);
|
|
1025
|
+
const textLayout = measurer.layout(prepared, contentMaxWidth, lineHeight);
|
|
1026
|
+
const naturalSize = {
|
|
1027
|
+
width: naturalTextWidth,
|
|
1028
|
+
height: textLayout.height
|
|
1029
|
+
};
|
|
1030
|
+
const contentWidth = Math.max(
|
|
1031
|
+
textLayout.width,
|
|
1032
|
+
minContentWidth(minSize, padding)
|
|
1033
|
+
);
|
|
1034
|
+
const contentHeight = Math.max(
|
|
1035
|
+
textLayout.height,
|
|
1036
|
+
minContentHeight(minSize, padding)
|
|
1037
|
+
);
|
|
1038
|
+
const idealWidth = contentWidth + padding.left + padding.right;
|
|
1039
|
+
const idealHeight = contentHeight + padding.top + padding.bottom;
|
|
1040
|
+
const fittedSize = {
|
|
1041
|
+
width: maxWidth === void 0 ? idealWidth : Math.min(maxWidth, idealWidth),
|
|
1042
|
+
height: idealHeight
|
|
1043
|
+
};
|
|
1044
|
+
const box = {
|
|
1045
|
+
x: 0,
|
|
1046
|
+
y: 0,
|
|
1047
|
+
width: fittedSize.width,
|
|
1048
|
+
height: fittedSize.height
|
|
1049
|
+
};
|
|
1050
|
+
const contentBox2 = {
|
|
1051
|
+
x: padding.left,
|
|
1052
|
+
y: padding.top,
|
|
1053
|
+
width: Math.max(0, box.width - padding.left - padding.right),
|
|
1054
|
+
height: Math.max(0, box.height - padding.top - padding.bottom)
|
|
1055
|
+
};
|
|
1056
|
+
const overflow = {
|
|
1057
|
+
horizontal: textLayout.width > contentBox2.width,
|
|
1058
|
+
vertical: textLayout.height > contentBox2.height || diagnosedHeightConstraintOverflow(textLayout.height, padding, minSize),
|
|
1059
|
+
truncated: options.overflow === "truncate" && textLayout.width > contentBox2.width
|
|
1060
|
+
};
|
|
1061
|
+
const diagnostics = buildDiagnostics(overflow, options.overflow);
|
|
1062
|
+
return {
|
|
1063
|
+
text,
|
|
1064
|
+
box,
|
|
1065
|
+
contentBox: contentBox2,
|
|
1066
|
+
naturalSize,
|
|
1067
|
+
fittedSize,
|
|
1068
|
+
padding,
|
|
1069
|
+
font: { ...options.font },
|
|
1070
|
+
lineHeight,
|
|
1071
|
+
lines: buildLines(textLayout, contentBox2, lineHeight),
|
|
1072
|
+
overflow,
|
|
1073
|
+
diagnostics
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function buildLines(textLayout, contentBox2, lineHeight) {
|
|
1077
|
+
return textLayout.lines.map((line, lineIndex) => ({
|
|
1078
|
+
text: line.text,
|
|
1079
|
+
box: {
|
|
1080
|
+
x: contentBox2.x,
|
|
1081
|
+
y: contentBox2.y + lineIndex * lineHeight,
|
|
1082
|
+
width: line.width,
|
|
1083
|
+
height: lineHeight
|
|
1084
|
+
},
|
|
1085
|
+
baselineY: contentBox2.y + lineIndex * lineHeight + lineHeight * 0.8,
|
|
1086
|
+
width: line.width,
|
|
1087
|
+
lineIndex,
|
|
1088
|
+
sourceStart: { ...line.start },
|
|
1089
|
+
sourceEnd: { ...line.end }
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
function normalizeMinSize2(minSize = {}) {
|
|
1093
|
+
if (minSize.width !== void 0) {
|
|
1094
|
+
assertFiniteNonNegative(minSize.width, "minSize.width");
|
|
1095
|
+
}
|
|
1096
|
+
if (minSize.height !== void 0) {
|
|
1097
|
+
assertFiniteNonNegative(minSize.height, "minSize.height");
|
|
1098
|
+
}
|
|
1099
|
+
return { ...minSize };
|
|
1100
|
+
}
|
|
1101
|
+
function normalizeMaxWidth(maxWidth) {
|
|
1102
|
+
if (maxWidth === void 0) {
|
|
1103
|
+
return void 0;
|
|
1104
|
+
}
|
|
1105
|
+
assertFiniteNonNegative(maxWidth, "maxWidth");
|
|
1106
|
+
return maxWidth;
|
|
1107
|
+
}
|
|
1108
|
+
function minContentWidth(minSize, padding) {
|
|
1109
|
+
return Math.max(0, (minSize.width ?? 0) - padding.left - padding.right);
|
|
1110
|
+
}
|
|
1111
|
+
function minContentHeight(minSize, padding) {
|
|
1112
|
+
return Math.max(0, (minSize.height ?? 0) - padding.top - padding.bottom);
|
|
1113
|
+
}
|
|
1114
|
+
function diagnosedHeightConstraintOverflow(textHeight, padding, minSize) {
|
|
1115
|
+
return minSize.height !== void 0 && textHeight + padding.top + padding.bottom > minSize.height;
|
|
1116
|
+
}
|
|
1117
|
+
function buildDiagnostics(overflow, mode = "allow") {
|
|
1118
|
+
if (mode !== "diagnose") {
|
|
1119
|
+
return [];
|
|
1120
|
+
}
|
|
1121
|
+
const diagnostics = [];
|
|
1122
|
+
if (overflow.horizontal) {
|
|
1123
|
+
diagnostics.push({
|
|
1124
|
+
severity: "warning",
|
|
1125
|
+
code: "label.overflow.horizontal",
|
|
1126
|
+
message: "Label text exceeds the fitted content width."
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
if (overflow.vertical) {
|
|
1130
|
+
diagnostics.push({
|
|
1131
|
+
severity: "warning",
|
|
1132
|
+
code: "label.overflow.vertical",
|
|
1133
|
+
message: "Label text exceeds the fitted content height."
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return diagnostics;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/dsl/normalize.ts
|
|
1140
|
+
var DEFAULT_NODE_PADDING = {
|
|
1141
|
+
top: 12,
|
|
1142
|
+
right: 16,
|
|
1143
|
+
bottom: 12,
|
|
1144
|
+
left: 16
|
|
1145
|
+
};
|
|
1146
|
+
var DEFAULT_NODE_MIN_SIZE = { width: 80, height: 40 };
|
|
1147
|
+
var DEFAULT_GROUP_PADDING = {
|
|
1148
|
+
top: 16,
|
|
1149
|
+
right: 16,
|
|
1150
|
+
bottom: 16,
|
|
1151
|
+
left: 16
|
|
1152
|
+
};
|
|
1153
|
+
var DEFAULT_LABEL_MAX_WIDTH = 160;
|
|
1154
|
+
var DEFAULT_FONT = { fontFamily: "Arial", fontSize: 14, lineHeight: 18 };
|
|
1155
|
+
function normalizeDiagramDsl(dslValue, options = {}) {
|
|
1156
|
+
const dsl = dslValue;
|
|
1157
|
+
const diagnostics = validateReferences(dsl);
|
|
1158
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
1159
|
+
return {
|
|
1160
|
+
diagnostics: sortDslDiagnostics(diagnostics),
|
|
1161
|
+
...outputResult(dsl)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
|
|
1165
|
+
const routeKind = dsl.routing?.kind ?? "orthogonal";
|
|
1166
|
+
const diagram = {
|
|
1167
|
+
id: options.id ?? dsl.id ?? "diagram",
|
|
1168
|
+
...dsl.title === void 0 ? {} : { title: dsl.title },
|
|
1169
|
+
direction: dsl.layout?.direction ?? dsl.direction ?? "TB",
|
|
1170
|
+
nodes: normalizeNodes(dsl, measurer),
|
|
1171
|
+
edges: normalizeEdges(dsl),
|
|
1172
|
+
groups: normalizeGroups(dsl, measurer),
|
|
1173
|
+
constraints: normalizeConstraints(dsl),
|
|
1174
|
+
diagnostics: [],
|
|
1175
|
+
metadata: { routeKind }
|
|
1176
|
+
};
|
|
1177
|
+
return {
|
|
1178
|
+
diagram,
|
|
1179
|
+
diagnostics: [],
|
|
1180
|
+
...outputResult(dsl)
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function outputResult(dsl) {
|
|
1184
|
+
return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
|
|
1185
|
+
}
|
|
1186
|
+
function normalizeNodes(dsl, measurer) {
|
|
1187
|
+
return Object.keys(dsl.nodes).sort().map((id) => {
|
|
1188
|
+
const node = dsl.nodes[id];
|
|
1189
|
+
const label = toLabel(node?.label);
|
|
1190
|
+
const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
|
|
1191
|
+
const fittedSize = labelLayout?.fittedSize;
|
|
1192
|
+
return {
|
|
1193
|
+
id,
|
|
1194
|
+
...label === void 0 ? {} : { label },
|
|
1195
|
+
shape: node?.shape ?? "rectangle",
|
|
1196
|
+
...node?.position === void 0 ? {} : { position: point(node.position) },
|
|
1197
|
+
size: {
|
|
1198
|
+
width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
|
|
1199
|
+
height: Math.max(
|
|
1200
|
+
DEFAULT_NODE_MIN_SIZE.height,
|
|
1201
|
+
fittedSize?.height ?? 0
|
|
1202
|
+
)
|
|
1203
|
+
},
|
|
1204
|
+
padding: { ...DEFAULT_NODE_PADDING },
|
|
1205
|
+
...labelLayout === void 0 ? {} : { labelLayout }
|
|
1206
|
+
};
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
function normalizeEdges(dsl) {
|
|
1210
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1211
|
+
return (dsl.edges ?? []).map((edge) => {
|
|
1212
|
+
const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? edge.source ?? "";
|
|
1213
|
+
const targetId = typeof edge === "string" ? "" : edge.targetId ?? edge.target ?? "";
|
|
1214
|
+
const baseId = `${sourceId}-${targetId}`;
|
|
1215
|
+
const count = counts.get(baseId) ?? 0;
|
|
1216
|
+
counts.set(baseId, count + 1);
|
|
1217
|
+
const id = typeof edge === "string" ? baseId : edge.id ?? (count === 0 ? baseId : `${baseId}-${count + 1}`);
|
|
1218
|
+
const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
|
|
1219
|
+
return {
|
|
1220
|
+
id,
|
|
1221
|
+
source: { nodeId: sourceId },
|
|
1222
|
+
target: { nodeId: targetId },
|
|
1223
|
+
...label === void 0 ? {} : { label }
|
|
1224
|
+
};
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeGroups(dsl, measurer) {
|
|
1228
|
+
return Object.keys(dsl.groups ?? {}).sort().map((id) => {
|
|
1229
|
+
const group = dsl.groups?.[id];
|
|
1230
|
+
const label = toLabel(group?.label);
|
|
1231
|
+
const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
|
|
1232
|
+
return {
|
|
1233
|
+
id,
|
|
1234
|
+
...label === void 0 ? {} : { label },
|
|
1235
|
+
nodeIds: [...group?.nodes ?? []],
|
|
1236
|
+
groupIds: [...group?.groups ?? []],
|
|
1237
|
+
padding: group?.padding ?? { ...DEFAULT_GROUP_PADDING },
|
|
1238
|
+
...labelLayout === void 0 ? {} : { labelLayout }
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
function normalizeConstraints(dsl) {
|
|
1243
|
+
const constraints = [];
|
|
1244
|
+
for (const constraint of dsl.constraints ?? []) {
|
|
1245
|
+
switch (constraint.kind) {
|
|
1246
|
+
case "exact-position":
|
|
1247
|
+
constraints.push({
|
|
1248
|
+
kind: "exact-position",
|
|
1249
|
+
targetId: constraint.targetId ?? constraint.target ?? "",
|
|
1250
|
+
position: point(constraint.position)
|
|
1251
|
+
});
|
|
1252
|
+
break;
|
|
1253
|
+
case "relative-position":
|
|
1254
|
+
constraints.push({
|
|
1255
|
+
kind: "relative-position",
|
|
1256
|
+
sourceId: constraint.sourceId ?? constraint.source ?? "",
|
|
1257
|
+
referenceId: constraint.referenceId ?? constraint.reference ?? "",
|
|
1258
|
+
relation: constraint.relation,
|
|
1259
|
+
...constraint.offset === void 0 ? {} : { offset: point(constraint.offset) }
|
|
1260
|
+
});
|
|
1261
|
+
break;
|
|
1262
|
+
case "align":
|
|
1263
|
+
constraints.push({
|
|
1264
|
+
kind: "align",
|
|
1265
|
+
axis: constraint.axis,
|
|
1266
|
+
targetIds: [...constraint.targetIds ?? constraint.targets ?? []]
|
|
1267
|
+
});
|
|
1268
|
+
break;
|
|
1269
|
+
case "distribute":
|
|
1270
|
+
constraints.push({
|
|
1271
|
+
kind: "distribute",
|
|
1272
|
+
axis: constraint.axis,
|
|
1273
|
+
targetIds: [...constraint.targetIds ?? constraint.targets ?? []],
|
|
1274
|
+
...constraint.spacing === void 0 ? {} : { spacing: constraint.spacing }
|
|
1275
|
+
});
|
|
1276
|
+
break;
|
|
1277
|
+
case "containment":
|
|
1278
|
+
constraints.push({
|
|
1279
|
+
kind: "containment",
|
|
1280
|
+
containerId: constraint.containerId ?? constraint.container ?? "",
|
|
1281
|
+
childIds: [...constraint.childIds ?? constraint.children ?? []],
|
|
1282
|
+
...constraint.padding === void 0 ? {} : { padding: constraint.padding }
|
|
1283
|
+
});
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return constraints;
|
|
1288
|
+
}
|
|
1289
|
+
function validateReferences(dsl) {
|
|
1290
|
+
const diagnostics = [];
|
|
1291
|
+
const nodeIds = new Set(Object.keys(dsl.nodes));
|
|
1292
|
+
const groupIds = new Set(Object.keys(dsl.groups ?? {}));
|
|
1293
|
+
(dsl.edges ?? []).forEach((edge, index) => {
|
|
1294
|
+
if (typeof edge === "string") {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const sourceId = edge.sourceId ?? edge.source;
|
|
1298
|
+
const targetId = edge.targetId ?? edge.target;
|
|
1299
|
+
if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
|
|
1300
|
+
diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
|
|
1301
|
+
}
|
|
1302
|
+
if (targetId !== void 0 && !nodeIds.has(targetId)) {
|
|
1303
|
+
diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
|
|
1307
|
+
(group.nodes ?? []).forEach((nodeId, index) => {
|
|
1308
|
+
if (!nodeIds.has(nodeId)) {
|
|
1309
|
+
diagnostics.push(
|
|
1310
|
+
referenceMissing(["groups", groupId, "nodes", index], nodeId)
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
(group.groups ?? []).forEach((childGroupId, index) => {
|
|
1315
|
+
if (!groupIds.has(childGroupId)) {
|
|
1316
|
+
diagnostics.push(
|
|
1317
|
+
referenceMissing(["groups", groupId, "groups", index], childGroupId)
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
(dsl.constraints ?? []).forEach((constraint, index) => {
|
|
1323
|
+
switch (constraint.kind) {
|
|
1324
|
+
case "exact-position": {
|
|
1325
|
+
const target = constraint.targetId ?? constraint.target;
|
|
1326
|
+
if (target !== void 0 && !hasNodeOrGroup(target, nodeIds, groupIds)) {
|
|
1327
|
+
diagnostics.push(
|
|
1328
|
+
referenceMissing(["constraints", index, "target"], target)
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
case "relative-position": {
|
|
1334
|
+
const source = constraint.sourceId ?? constraint.source;
|
|
1335
|
+
const reference = constraint.referenceId ?? constraint.reference;
|
|
1336
|
+
if (source !== void 0 && !hasNodeOrGroup(source, nodeIds, groupIds)) {
|
|
1337
|
+
diagnostics.push(
|
|
1338
|
+
referenceMissing(["constraints", index, "source"], source)
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (reference !== void 0 && !hasNodeOrGroup(reference, nodeIds, groupIds)) {
|
|
1342
|
+
diagnostics.push(
|
|
1343
|
+
referenceMissing(["constraints", index, "reference"], reference)
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
case "align":
|
|
1349
|
+
case "distribute":
|
|
1350
|
+
(constraint.targetIds ?? constraint.targets ?? []).forEach(
|
|
1351
|
+
(target, targetIndex) => {
|
|
1352
|
+
if (!hasNodeOrGroup(target, nodeIds, groupIds)) {
|
|
1353
|
+
diagnostics.push(
|
|
1354
|
+
referenceMissing(
|
|
1355
|
+
["constraints", index, "targets", targetIndex],
|
|
1356
|
+
target
|
|
1357
|
+
)
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
break;
|
|
1363
|
+
case "containment": {
|
|
1364
|
+
const container = constraint.containerId ?? constraint.container;
|
|
1365
|
+
if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
|
|
1366
|
+
diagnostics.push(
|
|
1367
|
+
referenceMissing(["constraints", index, "container"], container)
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
(constraint.childIds ?? constraint.children ?? []).forEach(
|
|
1371
|
+
(child, childIndex) => {
|
|
1372
|
+
if (!hasNodeOrGroup(child, nodeIds, groupIds)) {
|
|
1373
|
+
diagnostics.push(
|
|
1374
|
+
referenceMissing(
|
|
1375
|
+
["constraints", index, "children", childIndex],
|
|
1376
|
+
child
|
|
1377
|
+
)
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
);
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
return sortDslDiagnostics(diagnostics);
|
|
1387
|
+
}
|
|
1388
|
+
function referenceMissing(path, id) {
|
|
1389
|
+
return {
|
|
1390
|
+
severity: "error",
|
|
1391
|
+
layer: "validate",
|
|
1392
|
+
code: "validate.reference.missing",
|
|
1393
|
+
message: `Reference "${id}" does not exist.`,
|
|
1394
|
+
path,
|
|
1395
|
+
hint: "Define the referenced node or group id, or update this reference."
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function hasNodeOrGroup(id, nodeIds, groupIds) {
|
|
1399
|
+
return nodeIds.has(id) || groupIds.has(id);
|
|
1400
|
+
}
|
|
1401
|
+
function toLabel(value) {
|
|
1402
|
+
if (value === void 0) {
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof value === "string") {
|
|
1406
|
+
return { text: value };
|
|
1407
|
+
}
|
|
1408
|
+
return value.maxWidth === void 0 ? { text: value.text } : { text: value.text, maxWidth: value.maxWidth };
|
|
1409
|
+
}
|
|
1410
|
+
function fitDslLabel(label, measurer) {
|
|
1411
|
+
return fitLabel(
|
|
1412
|
+
label.text,
|
|
1413
|
+
{
|
|
1414
|
+
font: DEFAULT_FONT,
|
|
1415
|
+
padding: DEFAULT_NODE_PADDING,
|
|
1416
|
+
minSize: DEFAULT_NODE_MIN_SIZE,
|
|
1417
|
+
maxWidth: label.maxWidth ?? DEFAULT_LABEL_MAX_WIDTH
|
|
1418
|
+
},
|
|
1419
|
+
measurer
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
function point(value) {
|
|
1423
|
+
return { x: value.x, y: value.y };
|
|
1424
|
+
}
|
|
1425
|
+
var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
|
|
1426
|
+
var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
|
|
1427
|
+
var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
|
|
1428
|
+
var nodeShapeSchema = zod.z.enum([
|
|
1429
|
+
"rectangle",
|
|
1430
|
+
"rounded-rectangle",
|
|
1431
|
+
"ellipse",
|
|
1432
|
+
"diamond",
|
|
1433
|
+
"parallelogram",
|
|
1434
|
+
"hexagon",
|
|
1435
|
+
"cylinder"
|
|
1436
|
+
]);
|
|
1437
|
+
var finiteNumberSchema = zod.z.number().finite();
|
|
1438
|
+
var pointSchema = zod.z.object({
|
|
1439
|
+
x: finiteNumberSchema,
|
|
1440
|
+
y: finiteNumberSchema
|
|
1441
|
+
});
|
|
1442
|
+
var insetsSchema = zod.z.object({
|
|
1443
|
+
top: finiteNumberSchema,
|
|
1444
|
+
right: finiteNumberSchema,
|
|
1445
|
+
bottom: finiteNumberSchema,
|
|
1446
|
+
left: finiteNumberSchema
|
|
1447
|
+
});
|
|
1448
|
+
var labelSchema = zod.z.union([
|
|
1449
|
+
zod.z.string(),
|
|
1450
|
+
zod.z.object({
|
|
1451
|
+
text: zod.z.string(),
|
|
1452
|
+
maxWidth: finiteNumberSchema.optional()
|
|
1453
|
+
})
|
|
1454
|
+
]);
|
|
1455
|
+
var nodeSchema = zod.z.object({
|
|
1456
|
+
label: labelSchema.optional(),
|
|
1457
|
+
shape: nodeShapeSchema.optional(),
|
|
1458
|
+
position: pointSchema.optional()
|
|
1459
|
+
});
|
|
1460
|
+
var structuredEdgeSchema = zod.z.object({
|
|
1461
|
+
id: zod.z.string().optional(),
|
|
1462
|
+
source: zod.z.string().optional(),
|
|
1463
|
+
target: zod.z.string().optional(),
|
|
1464
|
+
sourceId: zod.z.string().optional(),
|
|
1465
|
+
targetId: zod.z.string().optional(),
|
|
1466
|
+
label: labelSchema.optional()
|
|
1467
|
+
}).superRefine((edge, context) => {
|
|
1468
|
+
if (edge.source === void 0 && edge.sourceId === void 0) {
|
|
1469
|
+
context.addIssue({
|
|
1470
|
+
code: "custom",
|
|
1471
|
+
message: "Edge requires source or sourceId.",
|
|
1472
|
+
path: ["source"]
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (edge.target === void 0 && edge.targetId === void 0) {
|
|
1476
|
+
context.addIssue({
|
|
1477
|
+
code: "custom",
|
|
1478
|
+
message: "Edge requires target or targetId.",
|
|
1479
|
+
path: ["target"]
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
var edgeSchema = zod.z.union([zod.z.string(), structuredEdgeSchema]);
|
|
1484
|
+
var groupSchema = zod.z.object({
|
|
1485
|
+
label: labelSchema.optional(),
|
|
1486
|
+
nodes: zod.z.array(zod.z.string()).optional(),
|
|
1487
|
+
groups: zod.z.array(zod.z.string()).optional(),
|
|
1488
|
+
padding: insetsSchema.optional()
|
|
1489
|
+
});
|
|
1490
|
+
var exactPositionConstraintSchema = zod.z.object({
|
|
1491
|
+
kind: zod.z.literal("exact-position"),
|
|
1492
|
+
target: zod.z.string().optional(),
|
|
1493
|
+
targetId: zod.z.string().optional(),
|
|
1494
|
+
position: pointSchema
|
|
1495
|
+
});
|
|
1496
|
+
var relativePositionConstraintSchema = zod.z.object({
|
|
1497
|
+
kind: zod.z.literal("relative-position"),
|
|
1498
|
+
source: zod.z.string().optional(),
|
|
1499
|
+
sourceId: zod.z.string().optional(),
|
|
1500
|
+
reference: zod.z.string().optional(),
|
|
1501
|
+
referenceId: zod.z.string().optional(),
|
|
1502
|
+
relation: zod.z.enum(["above", "right-of", "below", "left-of"]),
|
|
1503
|
+
offset: pointSchema.optional()
|
|
1504
|
+
});
|
|
1505
|
+
var alignConstraintSchema = zod.z.object({
|
|
1506
|
+
kind: zod.z.literal("align"),
|
|
1507
|
+
axis: zod.z.enum([
|
|
1508
|
+
"x",
|
|
1509
|
+
"y",
|
|
1510
|
+
"center-x",
|
|
1511
|
+
"center-y",
|
|
1512
|
+
"top",
|
|
1513
|
+
"right",
|
|
1514
|
+
"bottom",
|
|
1515
|
+
"left"
|
|
1516
|
+
]),
|
|
1517
|
+
targets: zod.z.array(zod.z.string()).optional(),
|
|
1518
|
+
targetIds: zod.z.array(zod.z.string()).optional()
|
|
1519
|
+
});
|
|
1520
|
+
var distributeConstraintSchema = zod.z.object({
|
|
1521
|
+
kind: zod.z.literal("distribute"),
|
|
1522
|
+
axis: zod.z.enum(["horizontal", "vertical"]),
|
|
1523
|
+
targets: zod.z.array(zod.z.string()).optional(),
|
|
1524
|
+
targetIds: zod.z.array(zod.z.string()).optional(),
|
|
1525
|
+
spacing: finiteNumberSchema.optional()
|
|
1526
|
+
});
|
|
1527
|
+
var containmentConstraintSchema = zod.z.object({
|
|
1528
|
+
kind: zod.z.literal("containment"),
|
|
1529
|
+
container: zod.z.string().optional(),
|
|
1530
|
+
containerId: zod.z.string().optional(),
|
|
1531
|
+
children: zod.z.array(zod.z.string()).optional(),
|
|
1532
|
+
childIds: zod.z.array(zod.z.string()).optional(),
|
|
1533
|
+
padding: insetsSchema.optional()
|
|
1534
|
+
});
|
|
1535
|
+
var constraintSchema = zod.z.union([
|
|
1536
|
+
exactPositionConstraintSchema,
|
|
1537
|
+
relativePositionConstraintSchema,
|
|
1538
|
+
alignConstraintSchema,
|
|
1539
|
+
distributeConstraintSchema,
|
|
1540
|
+
containmentConstraintSchema
|
|
1541
|
+
]);
|
|
1542
|
+
var diagramDslSchema = zod.z.object({
|
|
1543
|
+
id: zod.z.string().optional(),
|
|
1544
|
+
title: zod.z.string().optional(),
|
|
1545
|
+
direction: directionSchema.optional(),
|
|
1546
|
+
layout: zod.z.object({
|
|
1547
|
+
direction: directionSchema.optional()
|
|
1548
|
+
}).optional(),
|
|
1549
|
+
routing: zod.z.object({
|
|
1550
|
+
kind: routeKindSchema.optional()
|
|
1551
|
+
}).optional(),
|
|
1552
|
+
nodes: zod.z.record(zod.z.string(), nodeSchema),
|
|
1553
|
+
edges: zod.z.array(edgeSchema).optional(),
|
|
1554
|
+
groups: zod.z.record(zod.z.string(), groupSchema).optional(),
|
|
1555
|
+
constraints: zod.z.array(constraintSchema).optional(),
|
|
1556
|
+
output: zod.z.object({
|
|
1557
|
+
format: outputFormatSchema.optional()
|
|
1558
|
+
}).optional()
|
|
1559
|
+
});
|
|
1560
|
+
function validateDiagramDsl(value) {
|
|
1561
|
+
const result = diagramDslSchema.safeParse(value);
|
|
1562
|
+
if (result.success) {
|
|
1563
|
+
return { value: result.data, diagnostics: [] };
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
diagnostics: sortDslDiagnostics(
|
|
1567
|
+
result.error.issues.map(
|
|
1568
|
+
(issue) => createSchemaDiagnostic(toDiagnosticPath(issue.path), issue.message)
|
|
1569
|
+
)
|
|
1570
|
+
)
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function toDiagnosticPath(path) {
|
|
1574
|
+
return path.flatMap(
|
|
1575
|
+
(segment) => typeof segment === "string" || typeof segment === "number" ? [segment] : []
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/dsl/parse.ts
|
|
1580
|
+
var DEFAULT_DSL_MAX_BYTES = 1e6;
|
|
1581
|
+
function parseDiagramDsl(source, options = {}) {
|
|
1582
|
+
const maxBytes = options.maxBytes ?? DEFAULT_DSL_MAX_BYTES;
|
|
1583
|
+
if (buffer.Buffer.byteLength(source, "utf8") > maxBytes) {
|
|
1584
|
+
return {
|
|
1585
|
+
diagnostics: [
|
|
1586
|
+
createParseDiagnostic(
|
|
1587
|
+
"parse.input.too-large",
|
|
1588
|
+
`Input exceeds the ${maxBytes} byte limit.`,
|
|
1589
|
+
"Split the diagram into smaller inputs or raise maxBytes for trusted sources."
|
|
1590
|
+
)
|
|
1591
|
+
]
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
const parsed = parseSource(source, options);
|
|
1595
|
+
if (parsed.value === void 0 || hasErrorDiagnostics(parsed.diagnostics)) {
|
|
1596
|
+
return { diagnostics: sortDslDiagnostics(parsed.diagnostics) };
|
|
1597
|
+
}
|
|
1598
|
+
const expanded = expandEdgeShorthand(parsed.value);
|
|
1599
|
+
if (hasErrorDiagnostics(expanded.diagnostics)) {
|
|
1600
|
+
return { diagnostics: sortDslDiagnostics(expanded.diagnostics) };
|
|
1601
|
+
}
|
|
1602
|
+
const validated = validateDiagramDsl(expanded.value);
|
|
1603
|
+
return {
|
|
1604
|
+
value: validated.value,
|
|
1605
|
+
diagnostics: sortDslDiagnostics([
|
|
1606
|
+
...parsed.diagnostics,
|
|
1607
|
+
...validated.diagnostics
|
|
1608
|
+
])
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function parseSource(source, options) {
|
|
1612
|
+
if (isJsonSource(options)) {
|
|
1613
|
+
return parseJsonSource(source);
|
|
1614
|
+
}
|
|
1615
|
+
return parseYamlSource(source);
|
|
1616
|
+
}
|
|
1617
|
+
function parseJsonSource(source) {
|
|
1618
|
+
try {
|
|
1619
|
+
return { value: JSON.parse(source), diagnostics: [] };
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return {
|
|
1622
|
+
diagnostics: [
|
|
1623
|
+
createParseDiagnostic(
|
|
1624
|
+
"parse.json.invalid",
|
|
1625
|
+
`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
1626
|
+
"Fix the JSON syntax or use a .yaml source file."
|
|
1627
|
+
)
|
|
1628
|
+
]
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function parseYamlSource(source) {
|
|
1633
|
+
const document = yaml.parseDocument(source);
|
|
1634
|
+
const diagnostics = [
|
|
1635
|
+
...document.errors.map(
|
|
1636
|
+
(error) => createParseDiagnostic(
|
|
1637
|
+
"parse.yaml.invalid",
|
|
1638
|
+
`Invalid YAML: ${error.message}`,
|
|
1639
|
+
"Fix the YAML syntax near the reported parser error."
|
|
1640
|
+
)
|
|
1641
|
+
),
|
|
1642
|
+
...document.warnings.map((warning) => ({
|
|
1643
|
+
severity: "warning",
|
|
1644
|
+
layer: "parse",
|
|
1645
|
+
code: "parse.yaml.warning",
|
|
1646
|
+
message: `YAML warning: ${warning.message}`,
|
|
1647
|
+
hint: "Review the YAML warning before relying on the parsed value."
|
|
1648
|
+
}))
|
|
1649
|
+
];
|
|
1650
|
+
if (document.errors.length > 0) {
|
|
1651
|
+
return { diagnostics };
|
|
1652
|
+
}
|
|
1653
|
+
return { value: document.toJS(), diagnostics };
|
|
1654
|
+
}
|
|
1655
|
+
function isJsonSource(options) {
|
|
1656
|
+
return options.sourceFormat === "json" || (options.sourcePath?.toLowerCase().endsWith(".json") ?? false);
|
|
1657
|
+
}
|
|
1658
|
+
function hasErrorDiagnostics(diagnostics) {
|
|
1659
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1660
|
+
}
|
|
1661
|
+
function expandEdgeShorthand(value) {
|
|
1662
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || !("edges" in value)) {
|
|
1663
|
+
return { value, diagnostics: [] };
|
|
1664
|
+
}
|
|
1665
|
+
const record = value;
|
|
1666
|
+
if (!Array.isArray(record.edges)) {
|
|
1667
|
+
return { value, diagnostics: [] };
|
|
1668
|
+
}
|
|
1669
|
+
const diagnostics = [];
|
|
1670
|
+
const edges = record.edges.map((edge, index) => {
|
|
1671
|
+
const shorthand = edgeShorthandText(edge);
|
|
1672
|
+
if (shorthand === void 0) {
|
|
1673
|
+
return edge;
|
|
1674
|
+
}
|
|
1675
|
+
const result = parseEdgeShorthand(shorthand, ["edges", index]);
|
|
1676
|
+
diagnostics.push(...result.diagnostics);
|
|
1677
|
+
if (result.edge === void 0) {
|
|
1678
|
+
return edge;
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
sourceId: result.edge.sourceId,
|
|
1682
|
+
targetId: result.edge.targetId,
|
|
1683
|
+
...result.edge.label === void 0 ? {} : { label: result.edge.label }
|
|
1684
|
+
};
|
|
1685
|
+
});
|
|
1686
|
+
return { value: { ...record, edges }, diagnostics };
|
|
1687
|
+
}
|
|
1688
|
+
function edgeShorthandText(edge) {
|
|
1689
|
+
if (typeof edge === "string") {
|
|
1690
|
+
return edge;
|
|
1691
|
+
}
|
|
1692
|
+
if (edge === null || typeof edge !== "object" || Array.isArray(edge)) {
|
|
1693
|
+
return void 0;
|
|
1694
|
+
}
|
|
1695
|
+
const entries = Object.entries(edge);
|
|
1696
|
+
if (entries.length !== 1) {
|
|
1697
|
+
return void 0;
|
|
1698
|
+
}
|
|
1699
|
+
const entry = entries[0];
|
|
1700
|
+
if (entry === void 0) {
|
|
1701
|
+
return void 0;
|
|
1702
|
+
}
|
|
1703
|
+
const [key, value] = entry;
|
|
1704
|
+
if (!key.includes("->") || typeof value !== "string") {
|
|
1705
|
+
return void 0;
|
|
1706
|
+
}
|
|
1707
|
+
return `${key}: ${value}`;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/exporters/arrow.ts
|
|
1711
|
+
function computeArrowhead(points, options = {}) {
|
|
1712
|
+
const { length = 10, width = 8 } = options;
|
|
1713
|
+
for (let index = points.length - 1; index > 0; index -= 1) {
|
|
1714
|
+
const tip = points[index];
|
|
1715
|
+
const previous = points[index - 1];
|
|
1716
|
+
if (tip === void 0 || previous === void 0) {
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const dx = tip.x - previous.x;
|
|
1720
|
+
const dy = tip.y - previous.y;
|
|
1721
|
+
const magnitude = Math.hypot(dx, dy);
|
|
1722
|
+
if (magnitude === 0) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const direction = { x: dx / magnitude, y: dy / magnitude };
|
|
1726
|
+
const perpendicular = { x: -direction.y, y: direction.x };
|
|
1727
|
+
const base = {
|
|
1728
|
+
x: tip.x - direction.x * length,
|
|
1729
|
+
y: tip.y - direction.y * length
|
|
1730
|
+
};
|
|
1731
|
+
const halfWidth = width / 2;
|
|
1732
|
+
return {
|
|
1733
|
+
tip: { ...tip },
|
|
1734
|
+
left: {
|
|
1735
|
+
x: base.x + perpendicular.x * halfWidth,
|
|
1736
|
+
y: base.y + perpendicular.y * halfWidth
|
|
1737
|
+
},
|
|
1738
|
+
right: {
|
|
1739
|
+
x: base.x - perpendicular.x * halfWidth,
|
|
1740
|
+
y: base.y - perpendicular.y * halfWidth
|
|
1741
|
+
},
|
|
1742
|
+
direction
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
throw new TypeError("Arrowhead requires at least one non-zero segment");
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/exporters/excalidraw.ts
|
|
1749
|
+
function exportExcalidraw(diagram, options = {}) {
|
|
1750
|
+
const elements = [];
|
|
1751
|
+
const groupIdByChildId = createGroupMembership(diagram.groups);
|
|
1752
|
+
for (const group of diagram.groups) {
|
|
1753
|
+
const groupElementId = groupElementIdFor(group.id);
|
|
1754
|
+
elements.push(renderGroup(group));
|
|
1755
|
+
const text = renderText(
|
|
1756
|
+
`group-text:${group.id}`,
|
|
1757
|
+
group.label,
|
|
1758
|
+
group.box,
|
|
1759
|
+
groupElementId,
|
|
1760
|
+
groupIdByChildId.get(group.id) ?? []
|
|
1761
|
+
);
|
|
1762
|
+
if (text !== void 0) {
|
|
1763
|
+
elements.push(text);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (const node of diagram.nodes) {
|
|
1767
|
+
elements.push(renderNode(node, groupIdByChildId.get(node.id) ?? []));
|
|
1768
|
+
const text = renderText(
|
|
1769
|
+
`node-text:${node.id}`,
|
|
1770
|
+
node.label,
|
|
1771
|
+
node.box,
|
|
1772
|
+
`node:${node.id}`,
|
|
1773
|
+
groupIdByChildId.get(node.id) ?? []
|
|
1774
|
+
);
|
|
1775
|
+
if (text !== void 0) {
|
|
1776
|
+
elements.push(text);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
for (const edge of diagram.edges) {
|
|
1780
|
+
elements.push(renderArrow(edge));
|
|
1781
|
+
}
|
|
1782
|
+
const scene = {
|
|
1783
|
+
type: "excalidraw",
|
|
1784
|
+
version: 2,
|
|
1785
|
+
source: "auto-graph",
|
|
1786
|
+
elements,
|
|
1787
|
+
appState: {
|
|
1788
|
+
name: options.title ?? diagram.title ?? diagram.id,
|
|
1789
|
+
viewBackgroundColor: "#ffffff",
|
|
1790
|
+
gridSize: null
|
|
1791
|
+
},
|
|
1792
|
+
files: {}
|
|
1793
|
+
};
|
|
1794
|
+
return `${JSON.stringify(scene, null, 2)}
|
|
1795
|
+
`;
|
|
1796
|
+
}
|
|
1797
|
+
function renderGroup(group) {
|
|
1798
|
+
return {
|
|
1799
|
+
...baseElement(`group:${group.id}`, "rectangle", group.box),
|
|
1800
|
+
backgroundColor: "transparent",
|
|
1801
|
+
strokeStyle: "dashed",
|
|
1802
|
+
groupIds: groupGroupIds(group.id)
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function renderNode(node, groupIds) {
|
|
1806
|
+
return {
|
|
1807
|
+
...baseElement(`node:${node.id}`, mapShape(node.shape), node.box),
|
|
1808
|
+
groupIds
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
function renderArrow(edge) {
|
|
1812
|
+
const first = edge.points[0];
|
|
1813
|
+
if (first === void 0) {
|
|
1814
|
+
throw new TypeError(
|
|
1815
|
+
`Excalidraw edge ${edge.id} requires at least one point`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
const relativePoints = edge.points.map((point2) => ({
|
|
1819
|
+
x: point2.x - first.x,
|
|
1820
|
+
y: point2.y - first.y
|
|
1821
|
+
}));
|
|
1822
|
+
const box = pointsBox(relativePoints);
|
|
1823
|
+
return {
|
|
1824
|
+
...baseElement(`edge:${edge.id}`, "arrow", {
|
|
1825
|
+
x: first.x,
|
|
1826
|
+
y: first.y,
|
|
1827
|
+
width: box.width,
|
|
1828
|
+
height: box.height
|
|
1829
|
+
}),
|
|
1830
|
+
backgroundColor: "transparent",
|
|
1831
|
+
points: relativePoints,
|
|
1832
|
+
startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
|
|
1833
|
+
endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
|
|
1834
|
+
startArrowhead: null,
|
|
1835
|
+
endArrowhead: "arrow"
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function renderText(id, label, box, containerId, groupIds) {
|
|
1839
|
+
if (label?.text === void 0) {
|
|
1840
|
+
return void 0;
|
|
1841
|
+
}
|
|
1842
|
+
const fontSize = 14;
|
|
1843
|
+
return {
|
|
1844
|
+
...baseElement(id, "text", {
|
|
1845
|
+
x: box.x,
|
|
1846
|
+
y: box.y + box.height / 2 - fontSize / 2,
|
|
1847
|
+
width: box.width,
|
|
1848
|
+
height: fontSize
|
|
1849
|
+
}),
|
|
1850
|
+
backgroundColor: "transparent",
|
|
1851
|
+
strokeColor: "#111827",
|
|
1852
|
+
groupIds,
|
|
1853
|
+
text: label.text,
|
|
1854
|
+
fontSize,
|
|
1855
|
+
fontFamily: 1,
|
|
1856
|
+
textAlign: "center",
|
|
1857
|
+
verticalAlign: "middle",
|
|
1858
|
+
baseline: fontSize,
|
|
1859
|
+
containerId,
|
|
1860
|
+
originalText: label.text,
|
|
1861
|
+
lineHeight: 1.25,
|
|
1862
|
+
boundElements: null,
|
|
1863
|
+
link: null,
|
|
1864
|
+
locked: false,
|
|
1865
|
+
seed: seedFor(id),
|
|
1866
|
+
versionNonce: seedFor(`${id}:nonce`)
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function baseElement(id, type, box) {
|
|
1870
|
+
return {
|
|
1871
|
+
id,
|
|
1872
|
+
type,
|
|
1873
|
+
x: finite(box.x),
|
|
1874
|
+
y: finite(box.y),
|
|
1875
|
+
width: finite(box.width),
|
|
1876
|
+
height: finite(box.height),
|
|
1877
|
+
angle: 0,
|
|
1878
|
+
strokeColor: "#374151",
|
|
1879
|
+
backgroundColor: "#f8fafc",
|
|
1880
|
+
fillStyle: "solid",
|
|
1881
|
+
strokeWidth: 1,
|
|
1882
|
+
strokeStyle: "solid",
|
|
1883
|
+
roughness: 0,
|
|
1884
|
+
opacity: 100,
|
|
1885
|
+
groupIds: [],
|
|
1886
|
+
seed: seedFor(id),
|
|
1887
|
+
version: 1,
|
|
1888
|
+
versionNonce: seedFor(`${id}:nonce`),
|
|
1889
|
+
isDeleted: false,
|
|
1890
|
+
boundElements: null,
|
|
1891
|
+
updated: 0,
|
|
1892
|
+
link: null,
|
|
1893
|
+
locked: false
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
function mapShape(shape) {
|
|
1897
|
+
switch (shape) {
|
|
1898
|
+
case "rounded-rectangle":
|
|
1899
|
+
case "rectangle":
|
|
1900
|
+
return "rectangle";
|
|
1901
|
+
case "ellipse":
|
|
1902
|
+
return "ellipse";
|
|
1903
|
+
case "diamond":
|
|
1904
|
+
return "diamond";
|
|
1905
|
+
case "parallelogram":
|
|
1906
|
+
return "parallelogram";
|
|
1907
|
+
case "hexagon":
|
|
1908
|
+
return "hexagon";
|
|
1909
|
+
case "cylinder":
|
|
1910
|
+
return "cylinder";
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function createGroupMembership(groups) {
|
|
1914
|
+
const membership = /* @__PURE__ */ new Map();
|
|
1915
|
+
for (const group of groups) {
|
|
1916
|
+
const groupElementId = groupElementIdFor(group.id);
|
|
1917
|
+
for (const nodeId of group.nodeIds) {
|
|
1918
|
+
addMembership(membership, nodeId, groupElementId);
|
|
1919
|
+
}
|
|
1920
|
+
for (const childGroupId of group.groupIds) {
|
|
1921
|
+
addMembership(membership, childGroupId, groupElementId);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return membership;
|
|
1925
|
+
}
|
|
1926
|
+
function addMembership(membership, childId, groupElementId) {
|
|
1927
|
+
const existing = membership.get(childId) ?? [];
|
|
1928
|
+
membership.set(childId, [...existing, groupElementId].sort());
|
|
1929
|
+
}
|
|
1930
|
+
function groupGroupIds(groupId) {
|
|
1931
|
+
return [groupElementIdFor(groupId)];
|
|
1932
|
+
}
|
|
1933
|
+
function groupElementIdFor(groupId) {
|
|
1934
|
+
return `group:${groupId}`;
|
|
1935
|
+
}
|
|
1936
|
+
function pointsBox(points) {
|
|
1937
|
+
const xs = points.map((point2) => point2.x);
|
|
1938
|
+
const ys = points.map((point2) => point2.y);
|
|
1939
|
+
const minX = Math.min(...xs);
|
|
1940
|
+
const maxX = Math.max(...xs);
|
|
1941
|
+
const minY = Math.min(...ys);
|
|
1942
|
+
const maxY = Math.max(...ys);
|
|
1943
|
+
return {
|
|
1944
|
+
x: minX,
|
|
1945
|
+
y: minY,
|
|
1946
|
+
width: maxX - minX,
|
|
1947
|
+
height: maxY - minY
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
function finite(value) {
|
|
1951
|
+
if (!Number.isFinite(value)) {
|
|
1952
|
+
throw new TypeError(
|
|
1953
|
+
"Excalidraw export requires finite coordinated numbers"
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
return Number.parseFloat(value.toFixed(3));
|
|
1957
|
+
}
|
|
1958
|
+
function seedFor(id) {
|
|
1959
|
+
let hash = 2166136261;
|
|
1960
|
+
for (let index = 0; index < id.length; index += 1) {
|
|
1961
|
+
hash ^= id.charCodeAt(index);
|
|
1962
|
+
hash = Math.imul(hash, 16777619);
|
|
1963
|
+
}
|
|
1964
|
+
return Math.abs(hash);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/exporters/svg.ts
|
|
1968
|
+
var NODE_FILL = "#f8fafc";
|
|
1969
|
+
var GROUP_FILL = "#f9fafb";
|
|
1970
|
+
var STROKE = "#374151";
|
|
1971
|
+
var EDGE_STROKE = "#111827";
|
|
1972
|
+
var FONT_FAMILY = "Arial, sans-serif";
|
|
1973
|
+
function exportSvg(diagram, options = {}) {
|
|
1974
|
+
const title = options.title ?? diagram.title;
|
|
1975
|
+
const lines = [
|
|
1976
|
+
`<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
|
|
1977
|
+
...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
|
|
1978
|
+
` <rect class="background" x="${formatNumber(diagram.bounds.x)}" y="${formatNumber(diagram.bounds.y)}" width="${formatNumber(diagram.bounds.width)}" height="${formatNumber(diagram.bounds.height)}" fill="#ffffff"/>`,
|
|
1979
|
+
...diagram.groups.map((group) => indent(renderGroup2(group))),
|
|
1980
|
+
...diagram.edges.flatMap((edge) => {
|
|
1981
|
+
const path = renderEdgePath(edge.points, edge.id);
|
|
1982
|
+
if (path === void 0) {
|
|
1983
|
+
return [];
|
|
1984
|
+
}
|
|
1985
|
+
return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
|
|
1986
|
+
}),
|
|
1987
|
+
...diagram.nodes.map((node) => indent(renderNode2(node))),
|
|
1988
|
+
...diagram.groups.flatMap(
|
|
1989
|
+
(group) => renderLabel(group.label, group.box, group)
|
|
1990
|
+
),
|
|
1991
|
+
...diagram.nodes.flatMap((node) => renderLabel(node.label, node.box, node)),
|
|
1992
|
+
"</svg>"
|
|
1993
|
+
];
|
|
1994
|
+
return `${lines.join("\n")}
|
|
1995
|
+
`;
|
|
1996
|
+
}
|
|
1997
|
+
function renderGroup2(group) {
|
|
1998
|
+
return `<rect class="group" data-id="${escapeAttribute(group.id)}" x="${formatNumber(group.box.x)}" y="${formatNumber(group.box.y)}" width="${formatNumber(group.box.width)}" height="${formatNumber(group.box.height)}" fill="${GROUP_FILL}" stroke="${STROKE}" stroke-dasharray="6 4"/>`;
|
|
1999
|
+
}
|
|
2000
|
+
function renderNode2(node) {
|
|
2001
|
+
const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${NODE_FILL}" stroke="${STROKE}"`;
|
|
2002
|
+
switch (node.shape) {
|
|
2003
|
+
case "rectangle":
|
|
2004
|
+
return renderRect(node.box, common);
|
|
2005
|
+
case "rounded-rectangle":
|
|
2006
|
+
return renderRect(node.box, `${common} rx="8" ry="8"`);
|
|
2007
|
+
case "ellipse":
|
|
2008
|
+
return `<ellipse ${common} cx="${formatNumber(node.box.x + node.box.width / 2)}" cy="${formatNumber(node.box.y + node.box.height / 2)}" rx="${formatNumber(node.box.width / 2)}" ry="${formatNumber(node.box.height / 2)}"/>`;
|
|
2009
|
+
case "diamond":
|
|
2010
|
+
case "parallelogram":
|
|
2011
|
+
case "hexagon":
|
|
2012
|
+
return `<polygon ${common} points="${formatPoints(shapePoints(node.shape, node.box))}"/>`;
|
|
2013
|
+
case "cylinder":
|
|
2014
|
+
return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
function renderRect(box, attributes) {
|
|
2018
|
+
return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
|
|
2019
|
+
}
|
|
2020
|
+
function renderLabel(label, box, item) {
|
|
2021
|
+
const labelLayout = item.labelLayout;
|
|
2022
|
+
if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
|
|
2023
|
+
return [
|
|
2024
|
+
` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
|
|
2025
|
+
...labelLayout.lines.map(
|
|
2026
|
+
(line) => ` <tspan x="${formatNumber(line.box.x)}" y="${formatNumber(line.baselineY)}">${escapeXml(line.text)}</tspan>`
|
|
2027
|
+
),
|
|
2028
|
+
" </text>"
|
|
2029
|
+
];
|
|
2030
|
+
}
|
|
2031
|
+
if (label?.text === void 0) {
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
return [
|
|
2035
|
+
` <text class="label" data-for="${escapeAttribute(item.id)}" x="${formatNumber(box.x + box.width / 2)}" y="${formatNumber(box.y + box.height / 2)}" text-anchor="middle" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="14" fill="#111827">${escapeXml(label.text)}</text>`
|
|
2036
|
+
];
|
|
2037
|
+
}
|
|
2038
|
+
function renderEdgePath(points, id) {
|
|
2039
|
+
if (points.length < 2) {
|
|
2040
|
+
return void 0;
|
|
2041
|
+
}
|
|
2042
|
+
return `<path class="edge" data-id="${escapeAttribute(id)}" d="${formatPath(points)}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"/>`;
|
|
2043
|
+
}
|
|
2044
|
+
function renderArrowhead(points, id) {
|
|
2045
|
+
const arrowhead = computeArrowhead(points);
|
|
2046
|
+
return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${EDGE_STROKE}" stroke="${EDGE_STROKE}"/>`;
|
|
2047
|
+
}
|
|
2048
|
+
function shapePoints(shape, box) {
|
|
2049
|
+
const left = box.x;
|
|
2050
|
+
const right = box.x + box.width;
|
|
2051
|
+
const top = box.y;
|
|
2052
|
+
const bottom = box.y + box.height;
|
|
2053
|
+
const midX = box.x + box.width / 2;
|
|
2054
|
+
const midY = box.y + box.height / 2;
|
|
2055
|
+
const skew = Math.min(box.width * 0.2, 24);
|
|
2056
|
+
switch (shape) {
|
|
2057
|
+
case "diamond":
|
|
2058
|
+
return [
|
|
2059
|
+
{ x: midX, y: top },
|
|
2060
|
+
{ x: right, y: midY },
|
|
2061
|
+
{ x: midX, y: bottom },
|
|
2062
|
+
{ x: left, y: midY }
|
|
2063
|
+
];
|
|
2064
|
+
case "parallelogram":
|
|
2065
|
+
return [
|
|
2066
|
+
{ x: left + skew, y: top },
|
|
2067
|
+
{ x: right, y: top },
|
|
2068
|
+
{ x: right - skew, y: bottom },
|
|
2069
|
+
{ x: left, y: bottom }
|
|
2070
|
+
];
|
|
2071
|
+
case "hexagon": {
|
|
2072
|
+
const inset = Math.min(box.width * 0.2, 24);
|
|
2073
|
+
return [
|
|
2074
|
+
{ x: left + inset, y: top },
|
|
2075
|
+
{ x: right - inset, y: top },
|
|
2076
|
+
{ x: right, y: midY },
|
|
2077
|
+
{ x: right - inset, y: bottom },
|
|
2078
|
+
{ x: left + inset, y: bottom },
|
|
2079
|
+
{ x: left, y: midY }
|
|
2080
|
+
];
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
function formatCylinderPath(box) {
|
|
2085
|
+
const rx = box.width / 2;
|
|
2086
|
+
const ry = Math.min(12, box.height / 4);
|
|
2087
|
+
const left = box.x;
|
|
2088
|
+
const right = box.x + box.width;
|
|
2089
|
+
const top = box.y;
|
|
2090
|
+
const bottom = box.y + box.height;
|
|
2091
|
+
const midX = box.x + rx;
|
|
2092
|
+
return [
|
|
2093
|
+
`M ${formatNumber(left)} ${formatNumber(top + ry)}`,
|
|
2094
|
+
`A ${formatNumber(rx)} ${formatNumber(ry)} 0 0 1 ${formatNumber(right)} ${formatNumber(top + ry)}`,
|
|
2095
|
+
`L ${formatNumber(right)} ${formatNumber(bottom - ry)}`,
|
|
2096
|
+
`A ${formatNumber(rx)} ${formatNumber(ry)} 0 0 1 ${formatNumber(left)} ${formatNumber(bottom - ry)}`,
|
|
2097
|
+
"Z",
|
|
2098
|
+
`M ${formatNumber(left)} ${formatNumber(top + ry)}`,
|
|
2099
|
+
`A ${formatNumber(rx)} ${formatNumber(ry)} 0 0 0 ${formatNumber(right)} ${formatNumber(top + ry)}`,
|
|
2100
|
+
`M ${formatNumber(left)} ${formatNumber(bottom - ry)}`,
|
|
2101
|
+
`A ${formatNumber(rx)} ${formatNumber(ry)} 0 0 0 ${formatNumber(right)} ${formatNumber(bottom - ry)}`,
|
|
2102
|
+
`M ${formatNumber(midX)} ${formatNumber(top)}`
|
|
2103
|
+
].join(" ");
|
|
2104
|
+
}
|
|
2105
|
+
function formatPath(points) {
|
|
2106
|
+
return points.map((point2, index) => {
|
|
2107
|
+
const command = index === 0 ? "M" : "L";
|
|
2108
|
+
return `${command} ${formatNumber(point2.x)} ${formatNumber(point2.y)}`;
|
|
2109
|
+
}).join(" ");
|
|
2110
|
+
}
|
|
2111
|
+
function formatPoints(points) {
|
|
2112
|
+
return points.map((point2) => `${formatNumber(point2.x)},${formatNumber(point2.y)}`).join(" ");
|
|
2113
|
+
}
|
|
2114
|
+
function formatBoxViewBox(box) {
|
|
2115
|
+
return `${formatNumber(box.x)} ${formatNumber(box.y)} ${formatNumber(box.width)} ${formatNumber(box.height)}`;
|
|
2116
|
+
}
|
|
2117
|
+
function formatNumber(value) {
|
|
2118
|
+
if (!Number.isFinite(value)) {
|
|
2119
|
+
throw new TypeError("SVG export requires finite coordinated numbers");
|
|
2120
|
+
}
|
|
2121
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
|
|
2122
|
+
}
|
|
2123
|
+
function escapeXml(value) {
|
|
2124
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
2125
|
+
}
|
|
2126
|
+
function escapeAttribute(value) {
|
|
2127
|
+
return escapeXml(value).replaceAll('"', """);
|
|
2128
|
+
}
|
|
2129
|
+
function indent(value) {
|
|
2130
|
+
return ` ${value}`;
|
|
2131
|
+
}
|
|
2132
|
+
var DEFAULT_OPTIONS = {
|
|
2133
|
+
nodesep: 80,
|
|
2134
|
+
ranksep: 100,
|
|
2135
|
+
edgesep: 40,
|
|
2136
|
+
marginx: 0,
|
|
2137
|
+
marginy: 0,
|
|
2138
|
+
ranker: "network-simplex"
|
|
2139
|
+
};
|
|
2140
|
+
function runDagreInitialLayout(input) {
|
|
2141
|
+
const diagnostics = [];
|
|
2142
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
2143
|
+
const validNodeIds = /* @__PURE__ */ new Set();
|
|
2144
|
+
const graph = new dagre.Graph({
|
|
2145
|
+
directed: true,
|
|
2146
|
+
multigraph: true,
|
|
2147
|
+
compound: false
|
|
2148
|
+
});
|
|
2149
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
2150
|
+
graph.setGraph({
|
|
2151
|
+
rankdir: input.direction,
|
|
2152
|
+
nodesep: options.nodesep,
|
|
2153
|
+
ranksep: options.ranksep,
|
|
2154
|
+
edgesep: options.edgesep,
|
|
2155
|
+
marginx: options.marginx,
|
|
2156
|
+
marginy: options.marginy,
|
|
2157
|
+
ranker: options.ranker
|
|
2158
|
+
});
|
|
2159
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
2160
|
+
for (const node of input.nodes) {
|
|
2161
|
+
if (!isValidDimension(node.size.width) || !isValidDimension(node.size.height)) {
|
|
2162
|
+
diagnostics.push({
|
|
2163
|
+
severity: "error",
|
|
2164
|
+
code: "layout.node-size.invalid",
|
|
2165
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
2166
|
+
path: ["nodes", node.id, "size"],
|
|
2167
|
+
detail: { nodeId: node.id }
|
|
2168
|
+
});
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
validNodeIds.add(node.id);
|
|
2172
|
+
graph.setNode(node.id, {
|
|
2173
|
+
width: node.size.width,
|
|
2174
|
+
height: node.size.height
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
for (const edge of input.edges) {
|
|
2178
|
+
if (!validNodeIds.has(edge.sourceId) || !validNodeIds.has(edge.targetId)) {
|
|
2179
|
+
diagnostics.push({
|
|
2180
|
+
severity: "error",
|
|
2181
|
+
code: "layout.edge-reference.missing",
|
|
2182
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
2183
|
+
path: ["edges", edge.id],
|
|
2184
|
+
detail: {
|
|
2185
|
+
edgeId: edge.id,
|
|
2186
|
+
sourceId: edge.sourceId,
|
|
2187
|
+
targetId: edge.targetId
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
graph.setEdge(
|
|
2193
|
+
edge.sourceId,
|
|
2194
|
+
edge.targetId,
|
|
2195
|
+
{ minlen: 1, weight: 1 },
|
|
2196
|
+
edge.id
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
dagre.layout(graph);
|
|
2200
|
+
for (const node of input.nodes) {
|
|
2201
|
+
if (!validNodeIds.has(node.id)) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const label = graph.node(node.id);
|
|
2205
|
+
const centerX = label?.x;
|
|
2206
|
+
const centerY = label?.y;
|
|
2207
|
+
if (typeof centerX !== "number" || typeof centerY !== "number" || !Number.isFinite(centerX) || !Number.isFinite(centerY)) {
|
|
2208
|
+
diagnostics.push({
|
|
2209
|
+
severity: "error",
|
|
2210
|
+
code: "layout.node-position.invalid",
|
|
2211
|
+
message: `Dagre returned an invalid position for node ${node.id}.`,
|
|
2212
|
+
path: ["nodes", node.id],
|
|
2213
|
+
detail: { nodeId: node.id }
|
|
2214
|
+
});
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
boxes.set(node.id, {
|
|
2218
|
+
x: centerX - node.size.width / 2,
|
|
2219
|
+
y: centerY - node.size.height / 2,
|
|
2220
|
+
width: node.size.width,
|
|
2221
|
+
height: node.size.height
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
return { boxes, diagnostics };
|
|
2225
|
+
}
|
|
2226
|
+
function isValidDimension(value) {
|
|
2227
|
+
return Number.isFinite(value) && value >= 0;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/routing/routes.ts
|
|
2231
|
+
function routeEdge(input) {
|
|
2232
|
+
const diagnostics = [];
|
|
2233
|
+
const source = getEdgePort(
|
|
2234
|
+
input.source,
|
|
2235
|
+
input.target.center,
|
|
2236
|
+
input.sourceAnchor
|
|
2237
|
+
);
|
|
2238
|
+
const target = getEdgePort(
|
|
2239
|
+
input.target,
|
|
2240
|
+
input.source.center,
|
|
2241
|
+
input.targetAnchor
|
|
2242
|
+
);
|
|
2243
|
+
if ((input.kind ?? "orthogonal") === "straight") {
|
|
2244
|
+
return { points: simplifyRoute([source, target]), diagnostics };
|
|
2245
|
+
}
|
|
2246
|
+
const candidates = orthogonalCandidates(source, target, input.direction);
|
|
2247
|
+
for (const candidate of candidates) {
|
|
2248
|
+
if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
|
|
2249
|
+
return { points: simplifyRoute(candidate), diagnostics };
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
diagnostics.push({
|
|
2253
|
+
severity: "warning",
|
|
2254
|
+
code: "routing.obstacle.unavoidable",
|
|
2255
|
+
message: "No bounded orthogonal route candidate avoided all obstacles."
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
points: simplifyRoute(candidates[0] ?? [source, target]),
|
|
2259
|
+
diagnostics
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
function simplifyRoute(points) {
|
|
2263
|
+
const withoutDuplicates = [];
|
|
2264
|
+
for (const point2 of points) {
|
|
2265
|
+
const previous = withoutDuplicates.at(-1);
|
|
2266
|
+
if (previous === void 0 || previous.x !== point2.x || previous.y !== point2.y) {
|
|
2267
|
+
withoutDuplicates.push({ ...point2 });
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
const simplified = [];
|
|
2271
|
+
for (const point2 of withoutDuplicates) {
|
|
2272
|
+
const previous = simplified.at(-1);
|
|
2273
|
+
const beforePrevious = simplified.at(-2);
|
|
2274
|
+
if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
|
|
2275
|
+
simplified[simplified.length - 1] = { ...point2 };
|
|
2276
|
+
} else {
|
|
2277
|
+
simplified.push({ ...point2 });
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return simplified;
|
|
2281
|
+
}
|
|
2282
|
+
function orthogonalCandidates(source, target, direction) {
|
|
2283
|
+
const midpointX = (source.x + target.x) / 2;
|
|
2284
|
+
const midpointY = (source.y + target.y) / 2;
|
|
2285
|
+
const candidates = [
|
|
2286
|
+
[source, { x: target.x, y: source.y }, target],
|
|
2287
|
+
[source, { x: source.x, y: target.y }, target]
|
|
2288
|
+
];
|
|
2289
|
+
if (direction === "TB" || direction === "BT") {
|
|
2290
|
+
candidates.push([
|
|
2291
|
+
source,
|
|
2292
|
+
{ x: midpointX, y: source.y },
|
|
2293
|
+
{ x: midpointX, y: target.y },
|
|
2294
|
+
target
|
|
2295
|
+
]);
|
|
2296
|
+
} else {
|
|
2297
|
+
candidates.push([
|
|
2298
|
+
source,
|
|
2299
|
+
{ x: source.x, y: midpointY },
|
|
2300
|
+
{ x: target.x, y: midpointY },
|
|
2301
|
+
target
|
|
2302
|
+
]);
|
|
2303
|
+
}
|
|
2304
|
+
return candidates;
|
|
2305
|
+
}
|
|
2306
|
+
function routeIntersectsObstacles(points, obstacles) {
|
|
2307
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
2308
|
+
const a = points[index];
|
|
2309
|
+
const b = points[index + 1];
|
|
2310
|
+
if (a === void 0 || b === void 0) {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
const segment = segmentBox(a, b);
|
|
2314
|
+
for (const obstacle of obstacles) {
|
|
2315
|
+
validateBox(obstacle);
|
|
2316
|
+
if (intersectsAabb(segment, obstacle)) {
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
function segmentBox(a, b) {
|
|
2324
|
+
const minX = Math.min(a.x, b.x);
|
|
2325
|
+
const minY = Math.min(a.y, b.y);
|
|
2326
|
+
return {
|
|
2327
|
+
x: minX,
|
|
2328
|
+
y: minY,
|
|
2329
|
+
width: Math.max(1, Math.abs(a.x - b.x)),
|
|
2330
|
+
height: Math.max(1, Math.abs(a.y - b.y))
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function areCollinear(a, b, c) {
|
|
2334
|
+
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/solver/solve.ts
|
|
2338
|
+
function solveDiagram(diagram, options = {}) {
|
|
2339
|
+
const diagnostics = [...diagram.diagnostics];
|
|
2340
|
+
const nodes = stableById(diagram.nodes);
|
|
2341
|
+
const edges = stableById(diagram.edges);
|
|
2342
|
+
const groups = stableById(diagram.groups);
|
|
2343
|
+
const constraints = stableByConstraintId(diagram.constraints);
|
|
2344
|
+
const layout2 = runDagreInitialLayout({
|
|
2345
|
+
direction: diagram.direction,
|
|
2346
|
+
nodes: nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
2347
|
+
edges: edges.map((edge) => ({
|
|
2348
|
+
id: edge.id,
|
|
2349
|
+
sourceId: edge.source.nodeId,
|
|
2350
|
+
targetId: edge.target.nodeId
|
|
2351
|
+
}))
|
|
2352
|
+
});
|
|
2353
|
+
diagnostics.push(...layout2.diagnostics);
|
|
2354
|
+
const constrained = applyLayoutConstraints({
|
|
2355
|
+
direction: diagram.direction,
|
|
2356
|
+
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
2357
|
+
boxes: layout2.boxes,
|
|
2358
|
+
nodes,
|
|
2359
|
+
constraints
|
|
2360
|
+
});
|
|
2361
|
+
diagnostics.push(...constrained.diagnostics);
|
|
2362
|
+
const coordinatedNodes = coordinateNodes(
|
|
2363
|
+
nodes,
|
|
2364
|
+
constrained.boxes,
|
|
2365
|
+
options,
|
|
2366
|
+
diagnostics
|
|
2367
|
+
);
|
|
2368
|
+
const nodeGeometryById = new Map(
|
|
2369
|
+
coordinatedNodes.map((node) => [
|
|
2370
|
+
node.id,
|
|
2371
|
+
computeShapeGeometry({
|
|
2372
|
+
shape: node.shape,
|
|
2373
|
+
box: node.box,
|
|
2374
|
+
obstacleMargin: options.obstacleMargin ?? 0
|
|
2375
|
+
})
|
|
2376
|
+
])
|
|
2377
|
+
);
|
|
2378
|
+
const coordinatedGroups = coordinateGroups(
|
|
2379
|
+
groups,
|
|
2380
|
+
constrained.boxes,
|
|
2381
|
+
options,
|
|
2382
|
+
diagnostics
|
|
2383
|
+
);
|
|
2384
|
+
const groupBoxes = new Map(
|
|
2385
|
+
coordinatedGroups.map((group) => [group.id, group.box])
|
|
2386
|
+
);
|
|
2387
|
+
const coordinatedEdges = coordinateEdges(
|
|
2388
|
+
edges,
|
|
2389
|
+
nodeGeometryById,
|
|
2390
|
+
[...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
|
|
2391
|
+
diagram.direction,
|
|
2392
|
+
options,
|
|
2393
|
+
diagnostics
|
|
2394
|
+
);
|
|
2395
|
+
const allBoxes = [
|
|
2396
|
+
...coordinatedNodes.map((node) => node.box),
|
|
2397
|
+
...groupBoxes.values()
|
|
2398
|
+
];
|
|
2399
|
+
return {
|
|
2400
|
+
id: diagram.id,
|
|
2401
|
+
...diagram.title === void 0 ? {} : { title: diagram.title },
|
|
2402
|
+
direction: diagram.direction,
|
|
2403
|
+
nodes: coordinatedNodes,
|
|
2404
|
+
edges: coordinatedEdges,
|
|
2405
|
+
groups: coordinatedGroups,
|
|
2406
|
+
diagnostics,
|
|
2407
|
+
bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
|
|
2408
|
+
...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
2412
|
+
const coordinated = [];
|
|
2413
|
+
for (const node of nodes) {
|
|
2414
|
+
const box = boxes.get(node.id);
|
|
2415
|
+
if (box === void 0) {
|
|
2416
|
+
diagnostics.push({
|
|
2417
|
+
severity: "error",
|
|
2418
|
+
code: "solver.node-box.missing",
|
|
2419
|
+
message: `Node ${node.id} has no solved box.`,
|
|
2420
|
+
path: ["nodes", node.id],
|
|
2421
|
+
detail: { nodeId: node.id }
|
|
2422
|
+
});
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
const geometry = computeShapeGeometry({
|
|
2426
|
+
shape: node.shape,
|
|
2427
|
+
box,
|
|
2428
|
+
obstacleMargin: options.obstacleMargin ?? 0
|
|
2429
|
+
});
|
|
2430
|
+
coordinated.push({
|
|
2431
|
+
id: node.id,
|
|
2432
|
+
...node.label === void 0 ? {} : { label: node.label },
|
|
2433
|
+
...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
|
|
2434
|
+
shape: node.shape,
|
|
2435
|
+
...node.metadata === void 0 ? {} : { metadata: node.metadata },
|
|
2436
|
+
box: geometry.box,
|
|
2437
|
+
anchors: geometry.anchors,
|
|
2438
|
+
...node.parentId === void 0 ? {} : { parentId: node.parentId }
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
return coordinated;
|
|
2442
|
+
}
|
|
2443
|
+
function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
|
|
2444
|
+
const coordinated = [];
|
|
2445
|
+
const groupBoxes = /* @__PURE__ */ new Map();
|
|
2446
|
+
for (const group of groups) {
|
|
2447
|
+
const childBoxes = [];
|
|
2448
|
+
let missing = false;
|
|
2449
|
+
for (const nodeId of group.nodeIds) {
|
|
2450
|
+
const box = nodeBoxes.get(nodeId);
|
|
2451
|
+
if (box === void 0) {
|
|
2452
|
+
missing = true;
|
|
2453
|
+
diagnostics.push(groupReferenceMissing(group.id, "node", nodeId));
|
|
2454
|
+
} else {
|
|
2455
|
+
childBoxes.push(box);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
for (const childGroupId of group.groupIds) {
|
|
2459
|
+
const box = groupBoxes.get(childGroupId);
|
|
2460
|
+
if (box === void 0) {
|
|
2461
|
+
missing = true;
|
|
2462
|
+
diagnostics.push(
|
|
2463
|
+
groupReferenceMissing(group.id, "group", childGroupId)
|
|
2464
|
+
);
|
|
2465
|
+
} else {
|
|
2466
|
+
childBoxes.push(box);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (missing || childBoxes.length === 0) {
|
|
2470
|
+
if (childBoxes.length === 0) {
|
|
2471
|
+
diagnostics.push(groupReferenceMissing(group.id, "child", void 0));
|
|
2472
|
+
}
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const geometry = computeContainerGeometry({
|
|
2476
|
+
id: group.id,
|
|
2477
|
+
childBoxes,
|
|
2478
|
+
padding: group.padding,
|
|
2479
|
+
...group.labelLayout === void 0 ? {} : { labelLayout: group.labelLayout },
|
|
2480
|
+
obstacleMargin: options.obstacleMargin ?? 0
|
|
2481
|
+
});
|
|
2482
|
+
groupBoxes.set(group.id, geometry.box);
|
|
2483
|
+
diagnostics.push(...geometry.diagnostics);
|
|
2484
|
+
coordinated.push({
|
|
2485
|
+
...group,
|
|
2486
|
+
box: geometry.box
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
return coordinated;
|
|
2490
|
+
}
|
|
2491
|
+
function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
|
|
2492
|
+
const coordinated = [];
|
|
2493
|
+
for (const edge of edges) {
|
|
2494
|
+
const source = nodes.get(edge.source.nodeId);
|
|
2495
|
+
const target = nodes.get(edge.target.nodeId);
|
|
2496
|
+
if (source === void 0 || target === void 0) {
|
|
2497
|
+
diagnostics.push({
|
|
2498
|
+
severity: "error",
|
|
2499
|
+
code: "solver.edge-reference.missing",
|
|
2500
|
+
message: `Edge ${edge.id} references a missing coordinated node.`,
|
|
2501
|
+
path: ["edges", edge.id],
|
|
2502
|
+
detail: {
|
|
2503
|
+
edgeId: edge.id,
|
|
2504
|
+
sourceId: edge.source.nodeId,
|
|
2505
|
+
targetId: edge.target.nodeId
|
|
2506
|
+
}
|
|
2507
|
+
});
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
const route = routeEdge({
|
|
2511
|
+
kind: options.routeKind ?? "orthogonal",
|
|
2512
|
+
direction,
|
|
2513
|
+
source,
|
|
2514
|
+
target,
|
|
2515
|
+
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
2516
|
+
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
2517
|
+
obstacles: obstacles.filter(
|
|
2518
|
+
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
2519
|
+
)
|
|
2520
|
+
});
|
|
2521
|
+
diagnostics.push(
|
|
2522
|
+
...route.diagnostics.map((diagnostic) => ({
|
|
2523
|
+
...diagnostic,
|
|
2524
|
+
detail: { ...diagnostic.detail, edgeId: edge.id }
|
|
2525
|
+
}))
|
|
2526
|
+
);
|
|
2527
|
+
coordinated.push({
|
|
2528
|
+
...edge,
|
|
2529
|
+
points: route.points
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
return coordinated;
|
|
2533
|
+
}
|
|
2534
|
+
function stableById(items) {
|
|
2535
|
+
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
2536
|
+
}
|
|
2537
|
+
function stableByConstraintId(items) {
|
|
2538
|
+
return [...items].sort(
|
|
2539
|
+
(a, b) => `${a.id ?? a.kind}`.localeCompare(`${b.id ?? b.kind}`)
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
function groupReferenceMissing(groupId, referenceKind, id) {
|
|
2543
|
+
return {
|
|
2544
|
+
severity: "error",
|
|
2545
|
+
code: "solver.group-reference.missing",
|
|
2546
|
+
message: `Group ${groupId} references a missing ${referenceKind}.`,
|
|
2547
|
+
path: ["groups", groupId],
|
|
2548
|
+
detail: id === void 0 ? { groupId } : { groupId, id }
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// src/dsl/render.ts
|
|
2553
|
+
function resolveOutputFormat(cliFormat, dslFormat) {
|
|
2554
|
+
const selected = cliFormat ?? dslFormat ?? "svg";
|
|
2555
|
+
if (selected === "svg" || selected === "excalidraw") {
|
|
2556
|
+
return { format: selected, diagnostics: [] };
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
diagnostics: [
|
|
2560
|
+
{
|
|
2561
|
+
severity: "error",
|
|
2562
|
+
layer: "validate",
|
|
2563
|
+
code: "validate.output-format.unsupported",
|
|
2564
|
+
message: `Unsupported output format "${selected}".`,
|
|
2565
|
+
path: ["output", "format"],
|
|
2566
|
+
hint: "Use svg or excalidraw."
|
|
2567
|
+
}
|
|
2568
|
+
]
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
function exportDiagram(format, diagram) {
|
|
2572
|
+
const content = format === "svg" ? exportSvg(diagram) : exportExcalidraw(diagram);
|
|
2573
|
+
return { format, content, diagnostics: [] };
|
|
2574
|
+
}
|
|
2575
|
+
function renderDiagramDsl(source, options = {}) {
|
|
2576
|
+
const parsed = parseDiagramDsl(source, options);
|
|
2577
|
+
if (hasErrorDiagnostics2(parsed.diagnostics) || parsed.value === void 0) {
|
|
2578
|
+
return { diagnostics: parsed.diagnostics };
|
|
2579
|
+
}
|
|
2580
|
+
const normalized = normalizeDiagramDsl(
|
|
2581
|
+
parsed.value,
|
|
2582
|
+
options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
2583
|
+
);
|
|
2584
|
+
const format = resolveOutputFormat(options.format, normalized.output?.format);
|
|
2585
|
+
const diagnostics = sortDslDiagnostics([
|
|
2586
|
+
...parsed.diagnostics,
|
|
2587
|
+
...normalized.diagnostics,
|
|
2588
|
+
...format.diagnostics
|
|
2589
|
+
]);
|
|
2590
|
+
if (normalized.diagram === void 0 || format.format === void 0 || hasErrorDiagnostics2(diagnostics)) {
|
|
2591
|
+
return { diagnostics };
|
|
2592
|
+
}
|
|
2593
|
+
const solved = solveDiagram(normalized.diagram, {
|
|
2594
|
+
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
|
|
2595
|
+
});
|
|
2596
|
+
const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
|
|
2597
|
+
if (hasErrorDiagnostics2(solveDiagnostics)) {
|
|
2598
|
+
return {
|
|
2599
|
+
diagram: solved,
|
|
2600
|
+
diagnostics: sortDslDiagnostics(solveDiagnostics)
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
try {
|
|
2604
|
+
const exported = exportDiagram(format.format, solved);
|
|
2605
|
+
return {
|
|
2606
|
+
format: exported.format,
|
|
2607
|
+
content: exported.content,
|
|
2608
|
+
diagram: solved,
|
|
2609
|
+
diagnostics: sortDslDiagnostics([
|
|
2610
|
+
...diagnostics,
|
|
2611
|
+
...solveDiagnostics,
|
|
2612
|
+
...exported.diagnostics.map(toExportDiagnostic)
|
|
2613
|
+
])
|
|
2614
|
+
};
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
return {
|
|
2617
|
+
diagram: solved,
|
|
2618
|
+
diagnostics: [
|
|
2619
|
+
{
|
|
2620
|
+
severity: "error",
|
|
2621
|
+
layer: "export",
|
|
2622
|
+
code: "export.failed",
|
|
2623
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2624
|
+
hint: "Check the coordinated diagram and selected output format."
|
|
2625
|
+
}
|
|
2626
|
+
]
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function toSolveDiagnostic(diagnostic) {
|
|
2631
|
+
return { ...diagnostic, layer: "solve" };
|
|
2632
|
+
}
|
|
2633
|
+
function toExportDiagnostic(diagnostic) {
|
|
2634
|
+
return { ...diagnostic, layer: "export" };
|
|
2635
|
+
}
|
|
2636
|
+
function hasErrorDiagnostics2(diagnostics) {
|
|
2637
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// src/serialization/canonical.ts
|
|
2641
|
+
var DEFAULT_CANONICAL_PRECISION = 3;
|
|
2642
|
+
var UNORDERED_COLLECTION_KEYS = /* @__PURE__ */ new Set([
|
|
2643
|
+
"nodes",
|
|
2644
|
+
"edges",
|
|
2645
|
+
"groups",
|
|
2646
|
+
"constraints",
|
|
2647
|
+
"diagnostics",
|
|
2648
|
+
"anchors"
|
|
2649
|
+
]);
|
|
2650
|
+
var IDENTITY_KEYS = [
|
|
2651
|
+
"id",
|
|
2652
|
+
"name",
|
|
2653
|
+
"sourceId",
|
|
2654
|
+
"targetId",
|
|
2655
|
+
"nodeId",
|
|
2656
|
+
"groupId",
|
|
2657
|
+
"kind"
|
|
2658
|
+
];
|
|
2659
|
+
function canonicalize(value, options = {}) {
|
|
2660
|
+
const precision = resolvePrecision(
|
|
2661
|
+
options.precision ?? DEFAULT_CANONICAL_PRECISION
|
|
2662
|
+
);
|
|
2663
|
+
return canonicalizeValue(value, precision);
|
|
2664
|
+
}
|
|
2665
|
+
function stringifyCanonical(value, precision = DEFAULT_CANONICAL_PRECISION) {
|
|
2666
|
+
return `${JSON.stringify(
|
|
2667
|
+
canonicalize(value, { precision: resolvePrecision(precision) }),
|
|
2668
|
+
null,
|
|
2669
|
+
2
|
|
2670
|
+
)}
|
|
2671
|
+
`;
|
|
2672
|
+
}
|
|
2673
|
+
function resolvePrecision(precision) {
|
|
2674
|
+
if (!Number.isInteger(precision) || precision < 0) {
|
|
2675
|
+
throw new TypeError("Canonical precision must be a non-negative integer");
|
|
2676
|
+
}
|
|
2677
|
+
return precision;
|
|
2678
|
+
}
|
|
2679
|
+
function canonicalizeValue(value, precision, parentKey) {
|
|
2680
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
2681
|
+
return value;
|
|
2682
|
+
}
|
|
2683
|
+
if (typeof value === "number") {
|
|
2684
|
+
if (!Number.isFinite(value)) {
|
|
2685
|
+
throw new TypeError("Non-finite number cannot be canonicalized");
|
|
2686
|
+
}
|
|
2687
|
+
if (Object.is(value, -0)) {
|
|
2688
|
+
return 0;
|
|
2689
|
+
}
|
|
2690
|
+
const factor = 10 ** precision;
|
|
2691
|
+
const rounded = Math.round(value * factor) / factor;
|
|
2692
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
2693
|
+
}
|
|
2694
|
+
if (Array.isArray(value)) {
|
|
2695
|
+
return canonicalizeArray(value, precision, parentKey);
|
|
2696
|
+
}
|
|
2697
|
+
if (typeof value === "object") {
|
|
2698
|
+
return canonicalizeObject(value, precision);
|
|
2699
|
+
}
|
|
2700
|
+
throw new TypeError("Unsupported value cannot be canonicalized");
|
|
2701
|
+
}
|
|
2702
|
+
function canonicalizeArray(value, precision, parentKey) {
|
|
2703
|
+
const canonicalItems = value.map(
|
|
2704
|
+
(item) => canonicalizeValue(item, precision, parentKey)
|
|
2705
|
+
);
|
|
2706
|
+
if (!shouldSortArray(value, parentKey)) {
|
|
2707
|
+
return canonicalItems;
|
|
2708
|
+
}
|
|
2709
|
+
return [...canonicalItems].sort(compareCanonicalItems);
|
|
2710
|
+
}
|
|
2711
|
+
function canonicalizeObject(value, precision) {
|
|
2712
|
+
const result = {};
|
|
2713
|
+
for (const key of Object.keys(value).sort()) {
|
|
2714
|
+
const rawValue = value[key];
|
|
2715
|
+
if (rawValue === void 0) {
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
result[key] = canonicalizeValue(rawValue, precision, key);
|
|
2719
|
+
}
|
|
2720
|
+
return result;
|
|
2721
|
+
}
|
|
2722
|
+
function shouldSortArray(value, parentKey) {
|
|
2723
|
+
if (parentKey === "points" || value.every(isPointLikeRecord)) {
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
if (parentKey !== void 0 && UNORDERED_COLLECTION_KEYS.has(parentKey)) {
|
|
2727
|
+
return value.every(isPlainObject);
|
|
2728
|
+
}
|
|
2729
|
+
return value.length > 0 && value.every((item) => isPlainObject(item) && hasIdentityKey(item));
|
|
2730
|
+
}
|
|
2731
|
+
function compareCanonicalItems(a, b) {
|
|
2732
|
+
const aKey = itemSortKey(a);
|
|
2733
|
+
const bKey = itemSortKey(b);
|
|
2734
|
+
return aKey.localeCompare(bKey);
|
|
2735
|
+
}
|
|
2736
|
+
function itemSortKey(value) {
|
|
2737
|
+
if (!isCanonicalObject(value)) {
|
|
2738
|
+
return "";
|
|
2739
|
+
}
|
|
2740
|
+
return IDENTITY_KEYS.map((key) => identityPart(key, value)).join("\0");
|
|
2741
|
+
}
|
|
2742
|
+
function identityPart(key, value) {
|
|
2743
|
+
const part = value[key];
|
|
2744
|
+
if (typeof part === "string" || typeof part === "number") {
|
|
2745
|
+
return String(part);
|
|
2746
|
+
}
|
|
2747
|
+
return "";
|
|
2748
|
+
}
|
|
2749
|
+
function hasIdentityKey(value) {
|
|
2750
|
+
return IDENTITY_KEYS.some((key) => key in value);
|
|
2751
|
+
}
|
|
2752
|
+
function isPlainObject(value) {
|
|
2753
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2754
|
+
return false;
|
|
2755
|
+
}
|
|
2756
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2757
|
+
return prototype === Object.prototype || prototype === null;
|
|
2758
|
+
}
|
|
2759
|
+
function isCanonicalObject(value) {
|
|
2760
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2761
|
+
}
|
|
2762
|
+
function isPointLikeRecord(value) {
|
|
2763
|
+
return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
exports.DEFAULT_CANONICAL_PRECISION = DEFAULT_CANONICAL_PRECISION;
|
|
2767
|
+
exports.DEFAULT_DSL_MAX_BYTES = DEFAULT_DSL_MAX_BYTES;
|
|
2768
|
+
exports.DeterministicTextMeasurer = DeterministicTextMeasurer;
|
|
2769
|
+
exports.LabelFitter = LabelFitter;
|
|
2770
|
+
exports.PretextTextMeasurer = PretextTextMeasurer;
|
|
2771
|
+
exports.applyLayoutConstraints = applyLayoutConstraints;
|
|
2772
|
+
exports.assertFiniteNonNegative = assertFiniteNonNegative;
|
|
2773
|
+
exports.assertFinitePositive = assertFinitePositive;
|
|
2774
|
+
exports.boxCenter = boxCenter;
|
|
2775
|
+
exports.canonicalize = canonicalize;
|
|
2776
|
+
exports.computeArrowhead = computeArrowhead;
|
|
2777
|
+
exports.computeContainerGeometry = computeContainerGeometry;
|
|
2778
|
+
exports.computeShapeGeometry = computeShapeGeometry;
|
|
2779
|
+
exports.expandBox = expandBox;
|
|
2780
|
+
exports.exportExcalidraw = exportExcalidraw;
|
|
2781
|
+
exports.exportSvg = exportSvg;
|
|
2782
|
+
exports.fitLabel = fitLabel;
|
|
2783
|
+
exports.getEdgePort = getEdgePort;
|
|
2784
|
+
exports.intersectsAabb = intersectsAabb;
|
|
2785
|
+
exports.isPretextRuntimeAvailable = isPretextRuntimeAvailable;
|
|
2786
|
+
exports.normalizeDiagramDsl = normalizeDiagramDsl;
|
|
2787
|
+
exports.normalizeInsets = normalizeInsets;
|
|
2788
|
+
exports.parseDiagramDsl = parseDiagramDsl;
|
|
2789
|
+
exports.parseEdgeShorthand = parseEdgeShorthand;
|
|
2790
|
+
exports.renderDiagramDsl = renderDiagramDsl;
|
|
2791
|
+
exports.resolveLineHeight = resolveLineHeight;
|
|
2792
|
+
exports.resolveOutputFormat = resolveOutputFormat;
|
|
2793
|
+
exports.routeEdge = routeEdge;
|
|
2794
|
+
exports.runDagreInitialLayout = runDagreInitialLayout;
|
|
2795
|
+
exports.simplifyRoute = simplifyRoute;
|
|
2796
|
+
exports.solveDiagram = solveDiagram;
|
|
2797
|
+
exports.sortDslDiagnostics = sortDslDiagnostics;
|
|
2798
|
+
exports.stringifyCanonical = stringifyCanonical;
|
|
2799
|
+
exports.toCanvasFont = toCanvasFont;
|
|
2800
|
+
exports.unionBoxes = unionBoxes;
|
|
2801
|
+
exports.validateBox = validateBox;
|
|
2802
|
+
exports.validateTextStyle = validateTextStyle;
|
|
2803
|
+
//# sourceMappingURL=index.cjs.map
|
|
2804
|
+
//# sourceMappingURL=index.cjs.map
|