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