@dxos/diagram 0.11.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.
Files changed (100) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +10 -0
  3. package/dist/lib/index.mjs +3975 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/testing.mjs +68 -0
  6. package/dist/lib/testing.mjs.map +1 -0
  7. package/dist/types/src/content.d.ts +60 -0
  8. package/dist/types/src/content.d.ts.map +1 -0
  9. package/dist/types/src/diagnostics.d.ts +56 -0
  10. package/dist/types/src/diagnostics.d.ts.map +1 -0
  11. package/dist/types/src/diagnostics.test.d.ts +2 -0
  12. package/dist/types/src/diagnostics.test.d.ts.map +1 -0
  13. package/dist/types/src/dialect.d.ts +19 -0
  14. package/dist/types/src/dialect.d.ts.map +1 -0
  15. package/dist/types/src/index.d.ts +16 -0
  16. package/dist/types/src/index.d.ts.map +1 -0
  17. package/dist/types/src/layout.d.ts +26 -0
  18. package/dist/types/src/layout.d.ts.map +1 -0
  19. package/dist/types/src/mermaid-engine.d.ts +83 -0
  20. package/dist/types/src/mermaid-engine.d.ts.map +1 -0
  21. package/dist/types/src/mermaid-engine.test.d.ts +2 -0
  22. package/dist/types/src/mermaid-engine.test.d.ts.map +1 -0
  23. package/dist/types/src/mermaid.d.ts +56 -0
  24. package/dist/types/src/mermaid.d.ts.map +1 -0
  25. package/dist/types/src/mermaid.test.d.ts +3 -0
  26. package/dist/types/src/mermaid.test.d.ts.map +1 -0
  27. package/dist/types/src/objective.d.ts +75 -0
  28. package/dist/types/src/objective.d.ts.map +1 -0
  29. package/dist/types/src/objective.test.d.ts +2 -0
  30. package/dist/types/src/objective.test.d.ts.map +1 -0
  31. package/dist/types/src/ortho-router.d.ts +13 -0
  32. package/dist/types/src/ortho-router.d.ts.map +1 -0
  33. package/dist/types/src/scene.d.ts +1006 -0
  34. package/dist/types/src/scene.d.ts.map +1 -0
  35. package/dist/types/src/scene.test.d.ts +2 -0
  36. package/dist/types/src/scene.test.d.ts.map +1 -0
  37. package/dist/types/src/svg-handler.d.ts +5 -0
  38. package/dist/types/src/svg-handler.d.ts.map +1 -0
  39. package/dist/types/src/svg-handler.test.d.ts +2 -0
  40. package/dist/types/src/svg-handler.test.d.ts.map +1 -0
  41. package/dist/types/src/testing.d.ts +11 -0
  42. package/dist/types/src/testing.d.ts.map +1 -0
  43. package/dist/types/src/ui.d.ts +67 -0
  44. package/dist/types/src/ui.d.ts.map +1 -0
  45. package/dist/types/src/ui.test.d.ts +2 -0
  46. package/dist/types/src/ui.test.d.ts.map +1 -0
  47. package/dist/types/src/uml-engine.d.ts +14 -0
  48. package/dist/types/src/uml-engine.d.ts.map +1 -0
  49. package/dist/types/src/uml-engine.test.d.ts +2 -0
  50. package/dist/types/src/uml-engine.test.d.ts.map +1 -0
  51. package/dist/types/src/uml-grid.d.ts +99 -0
  52. package/dist/types/src/uml-grid.d.ts.map +1 -0
  53. package/dist/types/src/uml-grid.test.d.ts +2 -0
  54. package/dist/types/src/uml-grid.test.d.ts.map +1 -0
  55. package/dist/types/src/uml-rules.d.ts +56 -0
  56. package/dist/types/src/uml-rules.d.ts.map +1 -0
  57. package/dist/types/src/uml-rules.test.d.ts +2 -0
  58. package/dist/types/src/uml-rules.test.d.ts.map +1 -0
  59. package/dist/types/src/uml-search.d.ts +26 -0
  60. package/dist/types/src/uml-search.d.ts.map +1 -0
  61. package/dist/types/src/uml-search.test.d.ts +2 -0
  62. package/dist/types/src/uml-search.test.d.ts.map +1 -0
  63. package/dist/types/src/uml.d.ts +60 -0
  64. package/dist/types/src/uml.d.ts.map +1 -0
  65. package/dist/types/src/uml.test.d.ts +2 -0
  66. package/dist/types/src/uml.test.d.ts.map +1 -0
  67. package/dist/types/tsconfig.tsbuildinfo +1 -0
  68. package/package.json +48 -0
  69. package/src/__snapshots__/diagnostics.test.ts.snap +36 -0
  70. package/src/__snapshots__/mermaid-engine.test.ts.snap +22 -0
  71. package/src/content.ts +180 -0
  72. package/src/diagnostics.test.ts +173 -0
  73. package/src/diagnostics.ts +393 -0
  74. package/src/dialect.ts +53 -0
  75. package/src/index.ts +19 -0
  76. package/src/layout.ts +99 -0
  77. package/src/mermaid-engine.test.ts +244 -0
  78. package/src/mermaid-engine.ts +863 -0
  79. package/src/mermaid.test.ts +138 -0
  80. package/src/mermaid.ts +326 -0
  81. package/src/objective.test.ts +82 -0
  82. package/src/objective.ts +157 -0
  83. package/src/ortho-router.ts +372 -0
  84. package/src/scene.test.ts +36 -0
  85. package/src/scene.ts +281 -0
  86. package/src/svg-handler.test.ts +75 -0
  87. package/src/svg-handler.ts +119 -0
  88. package/src/testing.ts +68 -0
  89. package/src/ui.test.ts +183 -0
  90. package/src/ui.ts +398 -0
  91. package/src/uml-engine.test.ts +63 -0
  92. package/src/uml-engine.ts +146 -0
  93. package/src/uml-grid.test.ts +175 -0
  94. package/src/uml-grid.ts +559 -0
  95. package/src/uml-rules.test.ts +109 -0
  96. package/src/uml-rules.ts +319 -0
  97. package/src/uml-search.test.ts +108 -0
  98. package/src/uml-search.ts +266 -0
  99. package/src/uml.test.ts +172 -0
  100. package/src/uml.ts +390 -0
@@ -0,0 +1,3975 @@
1
+ import { invariant } from "@dxos/invariant";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import ELK from "elkjs/lib/elk.bundled.js";
5
+ import * as SchemaAST from "effect/SchemaAST";
6
+ import { graphlib, layout } from "@dagrejs/dagre";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/content.ts
20
+ var __dxlog_file$3 = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/infinite-canvas-depth-7e0949/packages/common/diagram/src/content.ts";
21
+ /** Apply a batch of edit commands to the canvas content (mutates `content`). */
22
+ var applyCommands = (content, commands, handler) => {
23
+ handler.scaffold?.(content);
24
+ const upserted = /* @__PURE__ */ new Set();
25
+ let removed = 0;
26
+ const find = (objectId) => handler.read(content).scene.objects.find((object) => object.id === objectId);
27
+ const merge = (object, placement) => {
28
+ const records = handler.render(object, placement, content);
29
+ if (handler.merge) handler.merge(content, records);
30
+ else Object.assign(content, records);
31
+ };
32
+ const remove = (predicate) => {
33
+ let count = 0;
34
+ for (const [key, record] of Object.entries(content)) {
35
+ const identity = handler.identify(record);
36
+ if (identity && predicate(identity)) {
37
+ const isPart = identity.part !== void 0;
38
+ delete content[key];
39
+ if (!isPart) count++;
40
+ }
41
+ }
42
+ handler.prune?.(content);
43
+ return count;
44
+ };
45
+ for (const command of commands) switch (command.op) {
46
+ case "upsert-object": {
47
+ const previous = find(command.object.id);
48
+ removed += remove((identity) => identity.object === command.object.id);
49
+ merge(command.object, {
50
+ origin: command.object.origin ?? previous?.origin ?? {
51
+ x: 0,
52
+ y: 0
53
+ },
54
+ scale: command.object.scale ?? previous?.scale ?? 1
55
+ });
56
+ upserted.add(command.object.id);
57
+ break;
58
+ }
59
+ case "upsert-elements": {
60
+ const previous = find(command.objectId);
61
+ invariant(previous, `unknown object: ${command.objectId} (use upsert-object to create it)`, {
62
+ "~LogMeta": "~LogMeta",
63
+ F: __dxlog_file$3,
64
+ L: 128,
65
+ S: void 0,
66
+ A: ["previous", "`unknown object: ${command.objectId} (use upsert-object to create it)`"]
67
+ });
68
+ const ids = new Set(command.elements.map((element) => element.id));
69
+ removed += remove((identity) => identity.object === command.objectId && ids.has(identity.element));
70
+ merge({
71
+ id: command.objectId,
72
+ elements: command.elements
73
+ }, previous);
74
+ upserted.add(command.objectId);
75
+ break;
76
+ }
77
+ case "remove-elements": {
78
+ const ids = new Set(command.elementIds);
79
+ removed += remove((identity) => identity.object === command.objectId && ids.has(identity.element));
80
+ break;
81
+ }
82
+ case "remove-object":
83
+ removed += remove((identity) => identity.object === command.objectId);
84
+ break;
85
+ case "move-object": {
86
+ const previous = find(command.objectId);
87
+ invariant(previous, `unknown object: ${command.objectId}`, {
88
+ "~LogMeta": "~LogMeta",
89
+ F: __dxlog_file$3,
90
+ L: 149,
91
+ S: void 0,
92
+ A: ["previous", "`unknown object: ${command.objectId}`"]
93
+ });
94
+ const delta = {
95
+ x: command.origin.x - previous.origin.x,
96
+ y: command.origin.y - previous.origin.y
97
+ };
98
+ for (const record of Object.values(content)) if (handler.identify(record)?.object === command.objectId) handler.translate(record, delta);
99
+ upserted.add(command.objectId);
100
+ break;
101
+ }
102
+ }
103
+ return {
104
+ upserted: [...upserted],
105
+ removed
106
+ };
107
+ };
108
+ /** Seed a fractional-index counter past any existing `a<n>` indexes. */
109
+ var nextIndex = (content, isCounted = () => true) => {
110
+ let max = 0;
111
+ let count = 0;
112
+ for (const record of Object.values(content)) {
113
+ if (!record || !isCounted(record)) continue;
114
+ count++;
115
+ const match = /^a(\d+)$/.exec(record.index ?? "");
116
+ if (match) max = Math.max(max, Number(match[1]));
117
+ }
118
+ return Math.max(max, count);
119
+ };
120
+ //#endregion
121
+ //#region src/scene.ts
122
+ var scene_exports = /* @__PURE__ */ __exportAll({
123
+ Arc: () => Arc,
124
+ Arrow: () => Arrow,
125
+ ArrowHead: () => ArrowHead,
126
+ ArrowTail: () => ArrowTail,
127
+ Box: () => Box,
128
+ Circle: () => Circle,
129
+ Color: () => Color,
130
+ Command: () => Command,
131
+ Curve: () => Curve,
132
+ Element: () => Element,
133
+ Fill: () => Fill,
134
+ MoveObject: () => MoveObject,
135
+ Point: () => Point,
136
+ Polyline: () => Polyline,
137
+ Portal: () => Portal,
138
+ RemoveElements: () => RemoveElements,
139
+ RemoveObject: () => RemoveObject,
140
+ Scene: () => Scene,
141
+ Stroke: () => Stroke,
142
+ Text: () => Text,
143
+ UpsertElements: () => UpsertElements,
144
+ UpsertObject: () => UpsertObject,
145
+ Weight: () => Weight,
146
+ WorldObject: () => WorldObject,
147
+ formatRef: () => formatRef,
148
+ parseRef: () => parseRef,
149
+ resolveRef: () => resolveRef
150
+ });
151
+ var Color = Schema.Literals([
152
+ "black",
153
+ "grey",
154
+ "light-violet",
155
+ "violet",
156
+ "blue",
157
+ "light-blue",
158
+ "yellow",
159
+ "orange",
160
+ "green",
161
+ "light-green",
162
+ "light-red",
163
+ "red",
164
+ "white"
165
+ ]);
166
+ var Fill = Schema.Literals([
167
+ "none",
168
+ "solid",
169
+ "pattern"
170
+ ]);
171
+ var Stroke = Schema.Literals([
172
+ "sketchy",
173
+ "solid",
174
+ "dashed",
175
+ "dotted"
176
+ ]);
177
+ var Weight = Schema.Literals([
178
+ "s",
179
+ "m",
180
+ "l",
181
+ "xl"
182
+ ]);
183
+ var Point = Schema.Struct({
184
+ x: Schema.Number,
185
+ y: Schema.Number
186
+ });
187
+ /** Style props shared by all elements; every field is optional with sensible defaults. */
188
+ var styleFields = {
189
+ color: Schema.optional(Color).annotate({ description: "Stroke/text color." }),
190
+ fill: Schema.optional(Fill).annotate({ description: "Interior fill of closed shapes." }),
191
+ stroke: Schema.optional(Stroke).annotate({ description: "Stroke rendering; sketchy is hand-drawn." }),
192
+ weight: Schema.optional(Weight).annotate({ description: "Stroke weight / text size." })
193
+ };
194
+ var id = Schema.String.annotate({ description: "Stable element id, unique within the object (e.g. \"left-eye\"). Used to edit or delete the element." });
195
+ /** Closed shape drawn inside a local bounding box. */
196
+ var Box = Schema.Struct({
197
+ kind: Schema.Literals([
198
+ "rect",
199
+ "ellipse",
200
+ "diamond",
201
+ "triangle"
202
+ ]),
203
+ id,
204
+ x: Schema.Number.annotate({ description: "Left edge (object-local units)." }),
205
+ y: Schema.Number.annotate({ description: "Top edge (object-local units)." }),
206
+ w: Schema.Number,
207
+ h: Schema.Number,
208
+ rotation: Schema.optional(Schema.Number).annotate({ description: "Clockwise rotation in degrees." }),
209
+ text: Schema.optional(Schema.String).annotate({ description: "Centered label." }),
210
+ corners: Schema.optional(Schema.Literals([
211
+ "top",
212
+ "bottom",
213
+ "none"
214
+ ])).annotate({ description: "Rects only: round just these corners (default all); backends without corner control ignore it." }),
215
+ ...styleFields
216
+ });
217
+ /** Circle sugar; reads back as an ellipse. */
218
+ var Circle = Schema.Struct({
219
+ kind: Schema.Literal("circle"),
220
+ id,
221
+ cx: Schema.Number.annotate({ description: "Center x (object-local units)." }),
222
+ cy: Schema.Number.annotate({ description: "Center y (object-local units)." }),
223
+ r: Schema.Number,
224
+ text: Schema.optional(Schema.String),
225
+ ...styleFields
226
+ });
227
+ /** Straight segment or open polyline through the given points. */
228
+ var Polyline = Schema.Struct({
229
+ kind: Schema.Literal("line"),
230
+ id,
231
+ points: Schema.Array(Point).annotate({ description: "Two or more points (object-local units)." }),
232
+ closed: Schema.optional(Schema.Boolean).annotate({ description: "Close the path back to the first point." }),
233
+ ...styleFields
234
+ });
235
+ /** Smooth curve through the given points (spline). */
236
+ var Curve = Schema.Struct({
237
+ kind: Schema.Literal("curve"),
238
+ id,
239
+ points: Schema.Array(Point).annotate({ description: "Control points the curve passes through." }),
240
+ ...styleFields
241
+ });
242
+ /**
243
+ * Circular arc sugar; reads back as a curve. Angles are degrees, measured clockwise from
244
+ * the positive x-axis in screen coordinates (y grows downward): 0–180 is the lower half
245
+ * (a smile), 180–360 the upper half (a frown).
246
+ */
247
+ var Arc = Schema.Struct({
248
+ kind: Schema.Literal("arc"),
249
+ id,
250
+ cx: Schema.Number,
251
+ cy: Schema.Number,
252
+ r: Schema.Number,
253
+ startAngle: Schema.Number,
254
+ endAngle: Schema.Number,
255
+ ...styleFields
256
+ });
257
+ /** Free-standing text. */
258
+ var Text = Schema.Struct({
259
+ kind: Schema.Literal("text"),
260
+ id,
261
+ x: Schema.Number,
262
+ y: Schema.Number,
263
+ w: Schema.optional(Schema.Number).annotate({ description: "Wrap width; omit for auto-size." }),
264
+ text: Schema.String,
265
+ ...styleFields
266
+ });
267
+ /** UML-style end markers: the head sits at the target, the tail at the source. */
268
+ var ArrowHead = Schema.Literals([
269
+ "arrow",
270
+ "triangle",
271
+ "crowsfoot",
272
+ "none"
273
+ ]);
274
+ var ArrowTail = Schema.Literals(["none", "circle"]);
275
+ /**
276
+ * Connector. Endpoints are element refs — `"<elementId>"` within the same object or
277
+ * `"<objectId>/<elementId>"` across objects, either with an optional `#<port>` naming an
278
+ * attachment point on the element — or explicit local points. Bound endpoints track their
279
+ * target when it moves; without a port the renderer picks the attachment point.
280
+ */
281
+ var Arrow = Schema.Struct({
282
+ kind: Schema.Literal("arrow"),
283
+ id,
284
+ from: Schema.optional(Schema.String).annotate({ description: "Source element ref, optionally `#port`." }),
285
+ to: Schema.optional(Schema.String).annotate({ description: "Target element ref, optionally `#port`." }),
286
+ start: Schema.optional(Point).annotate({ description: "Explicit start (used when `from` is omitted)." }),
287
+ end: Schema.optional(Point).annotate({ description: "Explicit end (used when `to` is omitted)." }),
288
+ text: Schema.optional(Schema.String).annotate({ description: "Label at the arrow midpoint." }),
289
+ head: Schema.optional(ArrowHead).annotate({ description: "Marker at the target end (default arrow): triangle for inheritance, crowsfoot for has-many." }),
290
+ tail: Schema.optional(ArrowTail).annotate({ description: "Marker at the source end (default none): circle for containment." }),
291
+ ...styleFields
292
+ });
293
+ /** Parse `"[object/]element[#port]"`; element ids may not contain `/` or `#`. */
294
+ var parseRef = (ref) => {
295
+ const hash = ref.indexOf("#");
296
+ const target = hash === -1 ? ref : ref.slice(0, hash);
297
+ const port = hash === -1 ? void 0 : ref.slice(hash + 1);
298
+ const slash = target.indexOf("/");
299
+ return {
300
+ ...slash === -1 ? {} : { object: target.slice(0, slash) },
301
+ element: slash === -1 ? target : target.slice(slash + 1),
302
+ ...port ? { port } : {}
303
+ };
304
+ };
305
+ /** Inverse of {@link parseRef}. */
306
+ var formatRef = ({ object, element, port }) => `${object === void 0 ? "" : `${object}/`}${element}${port === void 0 ? "" : `#${port}`}`;
307
+ /**
308
+ * Canonical `object/element` handle of a ref, resolving a bare element id against the owning
309
+ * object and dropping the port, so renderers without ports bind to the element itself.
310
+ */
311
+ var resolveRef = (ref, objectId) => {
312
+ const { object = objectId, element } = parseRef(ref);
313
+ return `${object}/${element}`;
314
+ };
315
+ /**
316
+ * Window onto another drawing: the referenced scene is rendered inside the box as a nested level
317
+ * of detail. Renderers without nesting ignore it.
318
+ */
319
+ var Portal = Schema.Struct({
320
+ kind: Schema.Literal("portal"),
321
+ id,
322
+ x: Schema.Number.annotate({ description: "Left edge (object-local units)." }),
323
+ y: Schema.Number.annotate({ description: "Top edge (object-local units)." }),
324
+ w: Schema.Number,
325
+ h: Schema.Number,
326
+ ref: Schema.String.annotate({ description: "The drawing shown inside the box: an ECHO object reference (DXN)." }),
327
+ text: Schema.optional(Schema.String).annotate({ description: "Title shown on the frame." }),
328
+ ...styleFields
329
+ });
330
+ var Element = Schema.Union([
331
+ Box,
332
+ Circle,
333
+ Polyline,
334
+ Curve,
335
+ Arc,
336
+ Text,
337
+ Arrow,
338
+ Portal
339
+ ]);
340
+ /**
341
+ * A semantic group of elements ("face", "hat") authored in its own local unit space.
342
+ * `origin` (canvas px) and `scale` (px per unit) place it on the canvas:
343
+ * `canvas = origin + local * scale`. On read, `origin` is derived as the top-left of the
344
+ * object's bounding box, so the model survives users dragging shapes around.
345
+ */
346
+ var WorldObject = Schema.Struct({
347
+ id: Schema.String.annotate({ description: "Stable object id naming the thing depicted (e.g. \"face\", \"hat\")." }),
348
+ origin: Schema.optional(Point).annotate({ description: "Canvas position (px). Omit on upsert to keep the current position." }),
349
+ scale: Schema.optional(Schema.Number).annotate({ description: "Canvas px per local unit (default 1)." }),
350
+ index: Schema.optional(Schema.String).annotate({ description: "Fractional z-order index (e.g. \"a1\"); objects with one paint above those without, in index order." }),
351
+ ref: Schema.optional(Schema.String).annotate({ description: "What the object depicts: an ECHO object reference (activating the node opens it) or any other URI or path, carried for tooling." }),
352
+ elements: Schema.Array(Element)
353
+ });
354
+ var Scene = Schema.Struct({ objects: Schema.Array(WorldObject) });
355
+ var UpsertObject = Schema.Struct({
356
+ op: Schema.Literal("upsert-object"),
357
+ object: WorldObject
358
+ }).annotate({ description: "Create an object, or replace an existing one wholesale (same id)." });
359
+ var UpsertElements = Schema.Struct({
360
+ op: Schema.Literal("upsert-elements"),
361
+ objectId: Schema.String,
362
+ elements: Schema.Array(Element)
363
+ }).annotate({ description: "Add elements to an existing object, replacing any with matching ids." });
364
+ var RemoveElements = Schema.Struct({
365
+ op: Schema.Literal("remove-elements"),
366
+ objectId: Schema.String,
367
+ elementIds: Schema.Array(Schema.String)
368
+ });
369
+ var RemoveObject = Schema.Struct({
370
+ op: Schema.Literal("remove-object"),
371
+ objectId: Schema.String
372
+ });
373
+ var MoveObject = Schema.Struct({
374
+ op: Schema.Literal("move-object"),
375
+ objectId: Schema.String,
376
+ origin: Point.annotate({ description: "New canvas position (px) for the object top-left." })
377
+ });
378
+ var Command = Schema.Union([
379
+ UpsertObject,
380
+ UpsertElements,
381
+ RemoveElements,
382
+ RemoveObject,
383
+ MoveObject
384
+ ]);
385
+ //#endregion
386
+ //#region src/dialect.ts
387
+ var __dxlog_file$2 = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/infinite-canvas-depth-7e0949/packages/common/diagram/src/dialect.ts";
388
+ /** The core dialect: input is already scene edit commands (identity compile). */
389
+ var sceneDialect = {
390
+ id: "scene",
391
+ description: "Positioned world objects and elements; the agent does the layout.",
392
+ input: Schema.Array(Command),
393
+ compile: Effect.succeed
394
+ };
395
+ var DialectRegistry = class {
396
+ #dialects = /* @__PURE__ */ new Map();
397
+ constructor(dialects = [sceneDialect]) {
398
+ dialects.forEach((dialect) => this.register(dialect));
399
+ }
400
+ register(dialect) {
401
+ invariant(!this.#dialects.has(dialect.id), `duplicate dialect: ${dialect.id}`, {
402
+ "~LogMeta": "~LogMeta",
403
+ F: __dxlog_file$2,
404
+ L: 42,
405
+ S: this,
406
+ A: ["!this.#dialects.has(dialect.id)", "`duplicate dialect: ${dialect.id}`"]
407
+ });
408
+ this.#dialects.set(dialect.id, dialect);
409
+ }
410
+ get(id) {
411
+ return this.#dialects.get(id);
412
+ }
413
+ list() {
414
+ return [...this.#dialects.values()];
415
+ }
416
+ };
417
+ //#endregion
418
+ //#region src/layout.ts
419
+ var layout_exports = /* @__PURE__ */ __exportAll({
420
+ FONT_METRICS: () => FONT_METRICS,
421
+ backEdges: () => backEdges,
422
+ rank: () => rank$1
423
+ });
424
+ /**
425
+ * Empirical text metrics per scene weight, calibrated to tldraw's draw font (`size` s/m/l/xl runs
426
+ * ~18/25/38/48 px): charW ≈ 0.6–0.67 em, lineH ≈ 1.35 em. Dialects size boxes from these so text
427
+ * fits without the renderer growing shapes over their neighbours.
428
+ */
429
+ var FONT_METRICS = {
430
+ s: {
431
+ charW: 12,
432
+ lineH: 26
433
+ },
434
+ m: {
435
+ charW: 15,
436
+ lineH: 34
437
+ },
438
+ l: {
439
+ charW: 23,
440
+ lineH: 51
441
+ },
442
+ xl: {
443
+ charW: 29,
444
+ lineH: 65
445
+ }
446
+ };
447
+ /**
448
+ * Edges that close a cycle, found by DFS: an edge into a node still on the stack points backwards.
449
+ * Ranking ignores them, so `C --> Y --> C` ranks C by its forward predecessors alone instead of
450
+ * chasing the loop.
451
+ */
452
+ var backEdges = (nodeIds, edges) => {
453
+ const outgoing = /* @__PURE__ */ new Map();
454
+ for (const edge of edges) outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge]);
455
+ const back = /* @__PURE__ */ new Set();
456
+ const done = /* @__PURE__ */ new Set();
457
+ const onStack = /* @__PURE__ */ new Set();
458
+ const visit = (id) => {
459
+ onStack.add(id);
460
+ for (const edge of outgoing.get(id) ?? []) if (onStack.has(edge.to)) back.add(edge);
461
+ else if (!done.has(edge.to)) visit(edge.to);
462
+ onStack.delete(id);
463
+ done.add(id);
464
+ };
465
+ const targets = new Set(edges.map((edge) => edge.to));
466
+ for (const id of nodeIds) if (!targets.has(id) && !done.has(id)) visit(id);
467
+ for (const id of nodeIds) if (!done.has(id)) visit(id);
468
+ return back;
469
+ };
470
+ /** Assign each node a rank one past its deepest forward predecessor. */
471
+ var rank$1 = (nodeIds, edges) => {
472
+ const back = backEdges(nodeIds, edges);
473
+ const forward = edges.filter((edge) => !back.has(edge));
474
+ const ranks = new Map(nodeIds.map((id) => [id, 0]));
475
+ for (let pass = 0; pass < nodeIds.length; pass++) {
476
+ let changed = false;
477
+ for (const edge of forward) {
478
+ const next = (ranks.get(edge.from) ?? 0) + 1;
479
+ if (next > (ranks.get(edge.to) ?? 0)) {
480
+ ranks.set(edge.to, next);
481
+ changed = true;
482
+ }
483
+ }
484
+ if (!changed) break;
485
+ }
486
+ return ranks;
487
+ };
488
+ //#endregion
489
+ //#region src/diagnostics.ts
490
+ var diagnostics_exports = /* @__PURE__ */ __exportAll({
491
+ Code: () => Code,
492
+ Diagnostic: () => Diagnostic,
493
+ SEVERITY: () => SEVERITY,
494
+ Severity: () => Severity,
495
+ analyze: () => analyze,
496
+ errors: () => errors
497
+ });
498
+ var Severity = Schema.Literals(["error", "warning"]);
499
+ /**
500
+ * `error` codes are objective defects — the picture is wrong, not merely busy — and gate CI.
501
+ * `warning` codes are quality metrics that trend rather than pass/fail; they are golden-filed so
502
+ * a layout change reads as a reviewable diff instead of a threshold argument.
503
+ */
504
+ var Code = Schema.Literals([
505
+ "node-overlap",
506
+ "route-through-node",
507
+ "label-overflow",
508
+ "edge-crossing",
509
+ "excessive-bends"
510
+ ]);
511
+ var SEVERITY = {
512
+ "node-overlap": "error",
513
+ "route-through-node": "error",
514
+ "label-overflow": "error",
515
+ "edge-crossing": "warning",
516
+ "excessive-bends": "warning"
517
+ };
518
+ /** A schema so operations can return diagnostics to the agent — the repair loop. */
519
+ var Diagnostic = Schema.Struct({
520
+ code: Code,
521
+ severity: Severity,
522
+ message: Schema.String,
523
+ refs: Schema.Array(Schema.String).annotate({ description: "Scene refs implicated: `objectId` or `objectId/elementId`, as `Arrow.from`/`to` spell them." })
524
+ });
525
+ /** Tolerance in scene units; ports land on a half-grid, so this only absorbs float error. */
526
+ var EPSILON = .5;
527
+ var place$1 = (object, point) => {
528
+ const { x = 0, y = 0 } = object.origin ?? {};
529
+ const scale = object.scale ?? 1;
530
+ return {
531
+ x: x + point.x * scale,
532
+ y: y + point.y * scale
533
+ };
534
+ };
535
+ var isBox = (element) => element.kind === "rect" || element.kind === "ellipse" || element.kind === "diamond" || element.kind === "triangle";
536
+ /** Absolute rects for every closed shape, keyed by scene ref. */
537
+ var nodes = (objects) => objects.flatMap((object) => object.elements.filter(isBox).map((element) => {
538
+ const origin = place$1(object, {
539
+ x: element.x,
540
+ y: element.y
541
+ });
542
+ const scale = object.scale ?? 1;
543
+ return {
544
+ ref: `${object.id}/${element.id}`,
545
+ rect: {
546
+ x: origin.x,
547
+ y: origin.y,
548
+ w: element.w * scale,
549
+ h: element.h * scale
550
+ },
551
+ text: element.text,
552
+ weight: element.weight ?? "m"
553
+ };
554
+ }));
555
+ /**
556
+ * Connector paths, rejoining the `line` prefix and `arrow` head the emitters split a routed edge
557
+ * into (`<id>-path` + `<id>`); a plain arrow is a one-segment path.
558
+ */
559
+ var connectors = (objects) => {
560
+ const paths = /* @__PURE__ */ new Map();
561
+ for (const object of objects) {
562
+ for (const element of object.elements) if (element.kind === "line") {
563
+ const id = element.id.replace(/-path$/, "");
564
+ const ref = `${object.id}/${id}`;
565
+ paths.set(ref, {
566
+ ref,
567
+ points: element.points.map((point) => place$1(object, point))
568
+ });
569
+ }
570
+ for (const element of object.elements) if (element.kind === "arrow" && element.start && element.end) {
571
+ const ref = `${object.id}/${element.id}`;
572
+ const head = [place$1(object, element.start), place$1(object, element.end)];
573
+ const existing = paths.get(ref);
574
+ paths.set(ref, {
575
+ ref,
576
+ points: existing ? [...existing.points.slice(0, -1), ...head] : head
577
+ });
578
+ }
579
+ }
580
+ return [...paths.values()];
581
+ };
582
+ var segmentsOf$1 = ({ points }) => points.slice(0, -1).map((point, index) => [point, points[index + 1]]);
583
+ var orientation = (origin, a, b) => (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x);
584
+ var coincident = (a, b) => Math.abs(a.x - b.x) < EPSILON && Math.abs(a.y - b.y) < EPSILON;
585
+ /** Proper crossing: the segments intersect away from all four endpoints (shared ports do not count). */
586
+ var properlyCrosses = ([a1, a2], [b1, b2]) => {
587
+ if ([a1, a2].some((a) => [b1, b2].some((b) => coincident(a, b)))) return false;
588
+ const d1 = orientation(a1, a2, b1);
589
+ const d2 = orientation(a1, a2, b2);
590
+ const d3 = orientation(b1, b2, a1);
591
+ const d4 = orientation(b1, b2, a2);
592
+ return d1 > 0 !== d2 > 0 && d3 > 0 !== d4 > 0;
593
+ };
594
+ /** Overlap area of two rects; 0 when they merely touch. */
595
+ var overlapArea = (a, b) => {
596
+ const w = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
597
+ const h = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
598
+ return w > EPSILON && h > EPSILON ? w * h : 0;
599
+ };
600
+ /**
601
+ * Strict enclosure: the inner rect fits inside AND is smaller. Two identical rects are stacked
602
+ * nodes — the strongest overlap — not a container and its member.
603
+ */
604
+ var contains = (outer, inner) => outer.x - EPSILON <= inner.x && outer.y - EPSILON <= inner.y && outer.x + outer.w + EPSILON >= inner.x + inner.w && outer.y + outer.h + EPSILON >= inner.y + inner.h && (inner.w < outer.w - EPSILON || inner.h < outer.h - EPSILON);
605
+ /**
606
+ * Length of the segment's run through the rect's INTERIOR (Liang-Barsky). A connector legitimately
607
+ * terminating at a node only touches its border, so an interior run identifies a route drawn
608
+ * across a node it does not belong to — no knowledge of the edge's terminals required.
609
+ */
610
+ var interiorRun = ([start, end], rect) => {
611
+ const dx = end.x - start.x;
612
+ const dy = end.y - start.y;
613
+ const box = {
614
+ x: rect.x + EPSILON,
615
+ y: rect.y + EPSILON,
616
+ w: rect.w - EPSILON * 2,
617
+ h: rect.h - EPSILON * 2
618
+ };
619
+ if (box.w <= 0 || box.h <= 0) return 0;
620
+ let enter = 0;
621
+ let exit = 1;
622
+ for (const [p, q] of [
623
+ [-dx, start.x - box.x],
624
+ [dx, box.x + box.w - start.x],
625
+ [-dy, start.y - box.y],
626
+ [dy, box.y + box.h - start.y]
627
+ ]) {
628
+ if (p === 0) {
629
+ if (q < 0) return 0;
630
+ continue;
631
+ }
632
+ const r = q / p;
633
+ if (p < 0) enter = Math.max(enter, r);
634
+ else exit = Math.min(exit, r);
635
+ }
636
+ return enter >= exit ? 0 : Math.hypot(dx, dy) * (exit - enter);
637
+ };
638
+ /** Bends in an orthogonal path: waypoints where the run changes axis. */
639
+ var bendCount = ({ points }) => {
640
+ let bends = 0;
641
+ for (let index = 1; index < points.length - 1; index++) {
642
+ const [previous, point, next] = [
643
+ points[index - 1],
644
+ points[index],
645
+ points[index + 1]
646
+ ];
647
+ if (!(Math.abs(previous.x - point.x) < EPSILON && Math.abs(point.x - next.x) < EPSILON || Math.abs(previous.y - point.y) < EPSILON && Math.abs(point.y - next.y) < EPSILON)) bends++;
648
+ }
649
+ return bends;
650
+ };
651
+ /**
652
+ * Estimated bounds of a wrapped label, using the shared font metrics the dialects size boxes by.
653
+ * The last line carries no trailing leading: `lineH ≈ 1.35em` (see `FONT_METRICS`), so the glyph
654
+ * block is `(lines − 1) · lineH + em`.
655
+ */
656
+ var labelFits = ({ rect, text, weight }) => {
657
+ if (!text) return true;
658
+ const font = FONT_METRICS[weight];
659
+ const perLine = Math.max(1, Math.floor(rect.w / font.charW));
660
+ return (text.split("\n").reduce((total, line) => total + Math.max(1, Math.ceil(line.length / perLine)), 0) - 1) * font.lineH + font.lineH / 1.35 <= rect.h + EPSILON;
661
+ };
662
+ /**
663
+ * Analyzes an emitted scene for layout defects and quality metrics. Pure: the same scene always
664
+ * yields the same report, so the soft metrics can be golden-filed and the hard codes asserted.
665
+ */
666
+ var analyze = (objects, { maxBends = 3 } = {}) => {
667
+ const diagnostics = [];
668
+ const allNodes = nodes(objects);
669
+ const allConnectors = connectors(objects);
670
+ const ownerOf = (ref) => ref.slice(0, ref.indexOf("/"));
671
+ const containers = new Set(allNodes.filter((outer) => allNodes.some((inner) => ownerOf(inner.ref) !== ownerOf(outer.ref) && contains(outer.rect, inner.rect))).map(({ ref }) => ref));
672
+ for (let i = 0; i < allNodes.length; i++) for (let j = i + 1; j < allNodes.length; j++) {
673
+ const [left, right] = [allNodes[i], allNodes[j]];
674
+ if (ownerOf(left.ref) === ownerOf(right.ref)) continue;
675
+ if (contains(left.rect, right.rect) || contains(right.rect, left.rect)) continue;
676
+ if (overlapArea(left.rect, right.rect) > 0) diagnostics.push({
677
+ code: "node-overlap",
678
+ severity: SEVERITY["node-overlap"],
679
+ message: `Nodes "${left.ref}" and "${right.ref}" overlap.`,
680
+ refs: [left.ref, right.ref]
681
+ });
682
+ }
683
+ for (const connector of allConnectors) for (const node of allNodes) {
684
+ if (ownerOf(node.ref) === ownerOf(connector.ref) || containers.has(node.ref)) continue;
685
+ if (segmentsOf$1(connector).reduce((total, segment) => total + interiorRun(segment, node.rect), 0) > EPSILON) diagnostics.push({
686
+ code: "route-through-node",
687
+ severity: SEVERITY["route-through-node"],
688
+ message: `Connector "${connector.ref}" is drawn across node "${node.ref}".`,
689
+ refs: [connector.ref, node.ref]
690
+ });
691
+ }
692
+ for (const node of allNodes) if (!labelFits(node)) diagnostics.push({
693
+ code: "label-overflow",
694
+ severity: SEVERITY["label-overflow"],
695
+ message: `Label of "${node.ref}" does not fit its shape.`,
696
+ refs: [node.ref]
697
+ });
698
+ let crossings = 0;
699
+ for (let i = 0; i < allConnectors.length; i++) for (let j = i + 1; j < allConnectors.length; j++) {
700
+ const [left, right] = [allConnectors[i], allConnectors[j]];
701
+ if (segmentsOf$1(left).some((a) => segmentsOf$1(right).some((b) => properlyCrosses(a, b)))) {
702
+ crossings++;
703
+ diagnostics.push({
704
+ code: "edge-crossing",
705
+ severity: SEVERITY["edge-crossing"],
706
+ message: `Connectors "${left.ref}" and "${right.ref}" cross.`,
707
+ refs: [left.ref, right.ref]
708
+ });
709
+ }
710
+ }
711
+ let bends = 0;
712
+ let length = 0;
713
+ for (const connector of allConnectors) {
714
+ length += segmentsOf$1(connector).reduce((total, [a, b]) => total + Math.hypot(b.x - a.x, b.y - a.y), 0);
715
+ const count = bendCount(connector);
716
+ bends += count;
717
+ if (count > maxBends) diagnostics.push({
718
+ code: "excessive-bends",
719
+ severity: SEVERITY["excessive-bends"],
720
+ message: `Connector "${connector.ref}" takes ${count} bends.`,
721
+ refs: [connector.ref]
722
+ });
723
+ }
724
+ const xs = allNodes.flatMap(({ rect }) => [rect.x, rect.x + rect.w]);
725
+ const ys = allNodes.flatMap(({ rect }) => [rect.y, rect.y + rect.h]);
726
+ const frameRects = allNodes.filter(({ ref }) => containers.has(ref)).map(({ rect }) => rect);
727
+ const gapBetween = (a, b) => Math.max(a.x - (b.x + b.w), b.x - (a.x + a.w), a.y - (b.y + b.h), b.y - (a.y + a.h));
728
+ const nearestGaps = frameRects.flatMap((frame, index) => {
729
+ const gaps = frameRects.filter((_, other) => other !== index).map((other) => gapBetween(frame, other)).filter((gap) => gap >= 0);
730
+ return gaps.length ? [Math.min(...gaps)] : [];
731
+ });
732
+ return {
733
+ diagnostics,
734
+ metrics: {
735
+ nodes: allNodes.length,
736
+ connectors: allConnectors.length,
737
+ overlaps: diagnostics.filter(({ code }) => code === "node-overlap").length,
738
+ routesThroughNodes: diagnostics.filter(({ code }) => code === "route-through-node").length,
739
+ labelOverflows: diagnostics.filter(({ code }) => code === "label-overflow").length,
740
+ crossings,
741
+ bends,
742
+ length,
743
+ width: xs.length ? Math.max(...xs) - Math.min(...xs) : 0,
744
+ height: ys.length ? Math.max(...ys) - Math.min(...ys) : 0,
745
+ containers: frameRects.length,
746
+ frameGapMin: nearestGaps.length ? Math.min(...nearestGaps) : Infinity,
747
+ frameGapSpread: nearestGaps.length >= 2 ? Math.max(...nearestGaps) - Math.min(...nearestGaps) : 0
748
+ }
749
+ };
750
+ };
751
+ /** Errors only — the set that gates CI and that an agent must fix before the diagram is usable. */
752
+ var errors = (report) => report.diagnostics.filter(({ severity }) => severity === "error");
753
+ //#endregion
754
+ //#region src/mermaid.ts
755
+ var mermaid_exports = /* @__PURE__ */ __exportAll({
756
+ compile: () => compile$7,
757
+ markers: () => markers,
758
+ parse: () => parse$1,
759
+ toStandard: () => toStandard
760
+ });
761
+ /**
762
+ * Edge tokens: mermaid's own arrows read as references; the classDiagram-style `--|>` (hollow
763
+ * triangle) and ER-style `--{` (crow's foot) and `o-->` (circle at the source) extend the flowchart
764
+ * grammar with the UML kinds, at the cost of mermaid.js rejecting those lines.
765
+ */
766
+ var EDGE_KINDS = {
767
+ "-->": "reference",
768
+ "---": "reference",
769
+ "-.->": "reference",
770
+ "==>": "reference",
771
+ "--|>": "inheritance",
772
+ "--{": "hasMany",
773
+ "o-->": "contains"
774
+ };
775
+ /** Reference-render labels for the UML kinds, used when the edge carries none of its own. */
776
+ var UML_LABELS = {
777
+ "--|>": "extends",
778
+ "--{": "has many",
779
+ "o-->": "contains"
780
+ };
781
+ var UML_EDGE = /^(\s*\S+)\s*(o-->|--\|>|--\{)\s*(?:\|(.*?)\|\s*)?(\S+)\s*$/;
782
+ /**
783
+ * Rewrite the UML edge tokens into mermaid-legal labelled arrows, for rendering the source with
784
+ * mermaid.js itself (as a reference beside our layout). Lossy by design: mermaid flowcharts have no
785
+ * triangle or crow's foot, so the kind becomes the label unless the edge already has one; `%% ref`
786
+ * lines are already comments.
787
+ */
788
+ var toStandard = (source) => source.split("\n").map((line) => {
789
+ const match = UML_EDGE.exec(line);
790
+ return match ? `${match[1]} -->|${match[3] ?? UML_LABELS[match[2]]}| ${match[4]}` : line;
791
+ }).join("\n");
792
+ /** Scene arrow markers for a relationship kind. */
793
+ var markers = (kind) => {
794
+ switch (kind) {
795
+ case "inheritance": return { head: "triangle" };
796
+ case "hasMany": return { head: "crowsfoot" };
797
+ case "contains": return { tail: "circle" };
798
+ default: return {};
799
+ }
800
+ };
801
+ var DIRECTIONS$1 = [
802
+ "TB",
803
+ "BT",
804
+ "LR",
805
+ "RL"
806
+ ];
807
+ var NODE = /^([A-Za-z0-9_-]+)(?:\[(.*?)\]|\((.*?)\)|\{(.*?)\})?$/;
808
+ var EDGE = /^(.+?)\s*(o-->|--\|>|--\{|-->|---|-\.->|==>)\s*(?:\|(.*?)\|\s*)?(.+)$/;
809
+ var SUBGRAPH = /^subgraph\s+([A-Za-z0-9_-]+)(?:\s*\[(.*?)\])?\s*$/;
810
+ var REF = /^%%\s*ref\s+([A-Za-z0-9_-]+)\s+(\S+)\s*$/;
811
+ var unquote = (value) => value.trim().replace(/^"(.*)"$/, "$1");
812
+ /** Parse a mermaid flowchart into a neutral graph. Unrecognised lines are ignored. */
813
+ var parse$1 = (source) => {
814
+ const nodes = /* @__PURE__ */ new Map();
815
+ const groups = [];
816
+ const edges = [];
817
+ let direction = "TB";
818
+ const stack = [];
819
+ const declare = (token) => {
820
+ const match = NODE.exec(token.trim());
821
+ if (!match) return;
822
+ const [, id, square, round, curly] = match;
823
+ const label = unquote(square ?? round ?? curly ?? id);
824
+ const group = stack[stack.length - 1];
825
+ const existing = nodes.get(id);
826
+ if (existing) {
827
+ if (square ?? round ?? curly) existing.label = label;
828
+ return id;
829
+ }
830
+ nodes.set(id, {
831
+ id,
832
+ label,
833
+ ...group ? { group } : {}
834
+ });
835
+ if (group) groups.find((entry) => entry.id === group)?.children.push(id);
836
+ return id;
837
+ };
838
+ const refs = /* @__PURE__ */ new Map();
839
+ for (const raw of source.split("\n")) {
840
+ const line = raw.trim();
841
+ const ref = REF.exec(line);
842
+ if (ref) {
843
+ refs.set(ref[1], ref[2]);
844
+ continue;
845
+ }
846
+ if (!line || line.startsWith("%%")) continue;
847
+ const header = /^(?:flowchart|graph)\s+([A-Za-z]{2})$/.exec(line);
848
+ if (header) {
849
+ const value = header[1].toUpperCase();
850
+ direction = DIRECTIONS$1.includes(value) ? value : "TB";
851
+ continue;
852
+ }
853
+ const subgraph = SUBGRAPH.exec(line);
854
+ if (subgraph) {
855
+ const [, id, label] = subgraph;
856
+ groups.push({
857
+ id,
858
+ label: unquote(label ?? id),
859
+ children: []
860
+ });
861
+ stack.push(id);
862
+ continue;
863
+ }
864
+ if (line === "end") {
865
+ stack.pop();
866
+ continue;
867
+ }
868
+ const edge = EDGE.exec(line);
869
+ if (edge) {
870
+ const [, from, token, label, to] = edge;
871
+ const fromId = declare(from);
872
+ const toId = declare(to);
873
+ if (fromId && toId) edges.push({
874
+ from: fromId,
875
+ to: toId,
876
+ kind: EDGE_KINDS[token] ?? "reference",
877
+ ...label ? { label: unquote(label) } : {}
878
+ });
879
+ continue;
880
+ }
881
+ declare(line);
882
+ }
883
+ return {
884
+ direction,
885
+ nodes: [...nodes.values()].map((node) => refs.has(node.id) ? {
886
+ ...node,
887
+ ref: refs.get(node.id)
888
+ } : node),
889
+ groups,
890
+ edges
891
+ };
892
+ };
893
+ /** Layout constants in scene units. */
894
+ var NODE_W = 100;
895
+ var NODE_H = 50;
896
+ var GAP_MAIN$3 = 60;
897
+ var GAP_CROSS$3 = 40;
898
+ var GROUP_PAD = 24;
899
+ /** Assign each node a rank one past its deepest forward predecessor. */
900
+ var rank = (graph) => rank$1(graph.nodes.map((node) => node.id), graph.edges);
901
+ /**
902
+ * Compile a mermaid flowchart into scene commands: one world object per node (element `box`),
903
+ * one per subgraph frame, and an `edges` object holding the connectors as bound arrows.
904
+ */
905
+ var compile$7 = (source, options = {}) => {
906
+ const graph = parse$1(source);
907
+ const { origin = {
908
+ x: 0,
909
+ y: 0
910
+ }, scale = 1, gapMain = GAP_MAIN$3, gapCross = GAP_CROSS$3 } = options;
911
+ const ranks = rank(graph);
912
+ const horizontal = graph.direction === "LR" || graph.direction === "RL";
913
+ const lanes = /* @__PURE__ */ new Map();
914
+ for (const node of graph.nodes) {
915
+ const value = ranks.get(node.id) ?? 0;
916
+ lanes.set(value, [...lanes.get(value) ?? [], node]);
917
+ }
918
+ const widest = Math.max(...[...lanes.values()].map((lane) => lane.length), 1);
919
+ const mainSize = horizontal ? NODE_W : NODE_H;
920
+ const crossSize = horizontal ? NODE_H : NODE_W;
921
+ const positions = /* @__PURE__ */ new Map();
922
+ for (const [lane, members] of lanes) {
923
+ const span = members.length * crossSize + (members.length - 1) * gapCross;
924
+ const offset = (widest * crossSize + (widest - 1) * gapCross - span) / 2;
925
+ members.forEach((node, index) => {
926
+ const cross = offset + index * (crossSize + gapCross);
927
+ const main = lane * (mainSize + gapMain);
928
+ positions.set(node.id, horizontal ? {
929
+ x: main,
930
+ y: cross
931
+ } : {
932
+ x: cross,
933
+ y: main
934
+ });
935
+ });
936
+ }
937
+ const commands = [];
938
+ for (const group of graph.groups) {
939
+ const members = group.children.map((id) => positions.get(id)).filter((point) => !!point);
940
+ if (members.length === 0) continue;
941
+ const minX = Math.min(...members.map((point) => point.x)) - GROUP_PAD;
942
+ const minY = Math.min(...members.map((point) => point.y)) - GROUP_PAD;
943
+ const maxX = Math.max(...members.map((point) => point.x)) + NODE_W + GROUP_PAD;
944
+ const maxY = Math.max(...members.map((point) => point.y)) + NODE_H + GROUP_PAD;
945
+ commands.push({
946
+ op: "upsert-object",
947
+ object: {
948
+ id: group.id,
949
+ origin: {
950
+ x: origin.x + minX * scale,
951
+ y: origin.y + minY * scale
952
+ },
953
+ scale,
954
+ elements: [{
955
+ kind: "rect",
956
+ id: "frame",
957
+ x: 0,
958
+ y: 0,
959
+ w: maxX - minX,
960
+ h: maxY - minY,
961
+ stroke: "dashed",
962
+ color: "grey",
963
+ ...group.label.trim() ? { text: group.label } : {}
964
+ }]
965
+ }
966
+ });
967
+ }
968
+ for (const node of graph.nodes) {
969
+ const point = positions.get(node.id);
970
+ commands.push({
971
+ op: "upsert-object",
972
+ object: {
973
+ id: node.id,
974
+ origin: {
975
+ x: origin.x + point.x * scale,
976
+ y: origin.y + point.y * scale
977
+ },
978
+ scale,
979
+ ...node.ref ? { ref: node.ref } : {},
980
+ elements: [{
981
+ kind: "rect",
982
+ id: "box",
983
+ x: 0,
984
+ y: 0,
985
+ w: NODE_W,
986
+ h: NODE_H,
987
+ text: node.label
988
+ }]
989
+ }
990
+ });
991
+ }
992
+ if (graph.edges.length > 0) commands.push({
993
+ op: "upsert-object",
994
+ object: {
995
+ id: "edges",
996
+ origin,
997
+ scale,
998
+ elements: graph.edges.map((edge) => ({
999
+ kind: "arrow",
1000
+ id: `${edge.from}-${edge.to}`,
1001
+ from: `${edge.from}/box`,
1002
+ to: `${edge.to}/box`,
1003
+ ...markers(edge.kind),
1004
+ ...edge.label ? { text: edge.label } : {}
1005
+ }))
1006
+ }
1007
+ });
1008
+ return commands;
1009
+ };
1010
+ //#endregion
1011
+ //#region src/ortho-router.ts
1012
+ /** Clearance kept between a route and any node border, in steps. */
1013
+ var CLEARANCE = 1;
1014
+ /** One bend costs as much as this many steps — turns dominate, distance breaks ties. */
1015
+ var TURN_COST = 1e3;
1016
+ /** Traversing a cell another edge already used costs this many extra steps. */
1017
+ var USED_COST = 24;
1018
+ /** Margin around the diagram bounds the search may roam into, in steps. */
1019
+ var MARGIN = 8;
1020
+ /** Directions: right, down, left, up. */
1021
+ var DX = [
1022
+ 1,
1023
+ 0,
1024
+ -1,
1025
+ 0
1026
+ ];
1027
+ var DY = [
1028
+ 0,
1029
+ 1,
1030
+ 0,
1031
+ -1
1032
+ ];
1033
+ /**
1034
+ * Creates a router that avoids the given node rects. Stateful across edges: earlier routes
1035
+ * penalize (not block) the cells they occupy, spreading parallel runs apart. `fallback` handles
1036
+ * the (fenced-in) edges the search cannot reach. The step is passed in rather than derived from
1037
+ * `uml-grid`'s `GRID`, which keeps this module free of a value import cycle with the dialects.
1038
+ */
1039
+ var makeAvoidingRouter = (obstacles, fallback, { step: STEP }) => {
1040
+ const xs = obstacles.flatMap((rect) => [rect.x, rect.x + rect.w]);
1041
+ const ys = obstacles.flatMap((rect) => [rect.y, rect.y + rect.h]);
1042
+ const bounds = {
1043
+ x0: Math.floor(Math.min(...xs) / STEP) - MARGIN,
1044
+ y0: Math.floor(Math.min(...ys) / STEP) - MARGIN,
1045
+ x1: Math.ceil(Math.max(...xs) / STEP) + MARGIN,
1046
+ y1: Math.ceil(Math.max(...ys) / STEP) + MARGIN
1047
+ };
1048
+ const width = bounds.x1 - bounds.x0 + 1;
1049
+ const height = bounds.y1 - bounds.y0 + 1;
1050
+ const inBounds = (x, y) => x >= bounds.x0 && x <= bounds.x1 && y >= bounds.y0 && y <= bounds.y1;
1051
+ const cellIndex = (x, y) => (y - bounds.y0) * width + (x - bounds.x0);
1052
+ const blockedGrid = new Uint8Array(width * height);
1053
+ for (const rect of obstacles) {
1054
+ const x0 = Math.floor(rect.x / STEP) - CLEARANCE;
1055
+ const y0 = Math.floor(rect.y / STEP) - CLEARANCE;
1056
+ const x1 = Math.ceil((rect.x + rect.w) / STEP) + CLEARANCE;
1057
+ const y1 = Math.ceil((rect.y + rect.h) / STEP) + CLEARANCE;
1058
+ for (let y = Math.max(bounds.y0, y0 + 1); y < Math.min(bounds.y1 + 1, y1); y++) for (let x = Math.max(bounds.x0, x0 + 1); x < Math.min(bounds.x1 + 1, x1); x++) blockedGrid[cellIndex(x, y)] = 1;
1059
+ }
1060
+ const blocked = (x, y) => inBounds(x, y) && blockedGrid[cellIndex(x, y)] !== 0;
1061
+ const usedGrid = new Uint8Array(width * height);
1062
+ const isUsed = (x, y) => inBounds(x, y) && usedGrid[cellIndex(x, y)] !== 0;
1063
+ const markUsed = (x, y) => {
1064
+ if (inBounds(x, y)) usedGrid[cellIndex(x, y)] = 1;
1065
+ };
1066
+ const settledCost = new Float64Array(width * height * 4);
1067
+ const settledGen = new Int32Array(width * height * 4);
1068
+ let generation = 0;
1069
+ /** Binary min-heap on cost + estimate, so each pop is O(log n) instead of a frontier scan. */
1070
+ const heapPush = (heap, state) => {
1071
+ heap.push(state);
1072
+ let index = heap.length - 1;
1073
+ while (index > 0) {
1074
+ const parent = index - 1 >> 1;
1075
+ if (heap[parent].f <= heap[index].f) break;
1076
+ [heap[parent], heap[index]] = [heap[index], heap[parent]];
1077
+ index = parent;
1078
+ }
1079
+ };
1080
+ const heapPop = (heap) => {
1081
+ const top = heap[0];
1082
+ const last = heap.pop();
1083
+ if (heap.length > 0) {
1084
+ heap[0] = last;
1085
+ let index = 0;
1086
+ for (;;) {
1087
+ const left = index * 2 + 1;
1088
+ const right = left + 1;
1089
+ let smallest = index;
1090
+ if (left < heap.length && heap[left].f < heap[smallest].f) smallest = left;
1091
+ if (right < heap.length && heap[right].f < heap[smallest].f) smallest = right;
1092
+ if (smallest === index) break;
1093
+ [heap[smallest], heap[index]] = [heap[index], heap[smallest]];
1094
+ index = smallest;
1095
+ }
1096
+ }
1097
+ return top;
1098
+ };
1099
+ /**
1100
+ * Fewest turns any path from (x, y) heading `dir` needs to reach the target: none while aligned
1101
+ * and heading at it, two when heading away along its axis, otherwise one. Admissible, and since
1102
+ * a turn costs as much as a thousand steps it is what makes the search converge on a diagram
1103
+ * hundreds of cells across — distance alone leaves it exploring almost uniformly.
1104
+ */
1105
+ const turnsNeeded = (x, y, dir, target) => {
1106
+ const dx = target.x - x;
1107
+ const dy = target.y - y;
1108
+ if (dx !== 0 && dy !== 0) return 1;
1109
+ if (dx === 0 && dy === 0) return 0;
1110
+ const towards = dx !== 0 ? dx > 0 ? 0 : 2 : dy > 0 ? 1 : 3;
1111
+ return dir === towards ? 0 : (dir + 2) % 4 === towards ? 2 : 1;
1112
+ };
1113
+ const estimateFrom = (x, y, dir, target) => Math.abs(target.x - x) + Math.abs(target.y - y) + turnsNeeded(x, y, dir, target) * TURN_COST;
1114
+ const budget = Math.max(5e4, (bounds.x1 - bounds.x0) * (bounds.y1 - bounds.y0) * 4);
1115
+ /**
1116
+ * Bounded A* between two stub ends (grid coordinates). Successors already dominated by a
1117
+ * settled state are pruned before pushing; undefined when the target is unreachable.
1118
+ */
1119
+ const search = (source, startDir, target, endDir) => {
1120
+ const open = [{
1121
+ x: source.x,
1122
+ y: source.y,
1123
+ dir: startDir,
1124
+ cost: 0,
1125
+ f: estimateFrom(source.x, source.y, startDir, target)
1126
+ }];
1127
+ generation++;
1128
+ const settledAt = (index) => settledGen[index] === generation ? settledCost[index] : void 0;
1129
+ let found;
1130
+ for (let iterations = 0; open.length > 0 && iterations < budget; iterations++) {
1131
+ const current = heapPop(open);
1132
+ if (current.x === target.x && current.y === target.y) {
1133
+ found = current;
1134
+ break;
1135
+ }
1136
+ const stateIndex = cellIndex(current.x, current.y) * 4 + current.dir;
1137
+ const seen = settledAt(stateIndex);
1138
+ if (seen !== void 0 && seen <= current.cost) continue;
1139
+ settledGen[stateIndex] = generation;
1140
+ settledCost[stateIndex] = current.cost;
1141
+ for (let dir = 0; dir < 4; dir++) {
1142
+ if ((dir + 2) % 4 === current.dir) continue;
1143
+ const x = current.x + DX[dir];
1144
+ const y = current.y + DY[dir];
1145
+ if (!inBounds(x, y)) continue;
1146
+ const cell = cellIndex(x, y);
1147
+ if (blockedGrid[cell] !== 0) continue;
1148
+ const cost = current.cost + 1 + (dir === current.dir ? 0 : TURN_COST) + (usedGrid[cell] !== 0 ? USED_COST : 0) + (x === target.x && y === target.y && dir !== endDir ? TURN_COST : 0);
1149
+ const dominated = settledAt(cell * 4 + dir);
1150
+ if (dominated !== void 0 && dominated <= cost) continue;
1151
+ heapPush(open, {
1152
+ x,
1153
+ y,
1154
+ dir,
1155
+ cost,
1156
+ f: cost + estimateFrom(x, y, dir, target),
1157
+ prev: current
1158
+ });
1159
+ }
1160
+ }
1161
+ if (!found) return;
1162
+ const cells = [];
1163
+ for (let state = found; state; state = state.prev) cells.unshift({
1164
+ x: state.x,
1165
+ y: state.y
1166
+ });
1167
+ return {
1168
+ cost: found.cost,
1169
+ cells
1170
+ };
1171
+ };
1172
+ return (edge) => {
1173
+ const { from, to, horizontal, ports } = edge;
1174
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
1175
+ const alongY = horizontal ? sameLane : !sameLane;
1176
+ const down = to.y >= from.y;
1177
+ const right = to.x >= from.x;
1178
+ const flowStart = alongY ? {
1179
+ point: {
1180
+ x: ports?.start ?? from.x + from.w / 2,
1181
+ y: down ? from.y + from.h : from.y
1182
+ },
1183
+ dir: down ? 1 : 3
1184
+ } : {
1185
+ point: {
1186
+ x: right ? from.x + from.w : from.x,
1187
+ y: ports?.start ?? from.y + from.h / 2
1188
+ },
1189
+ dir: right ? 0 : 2
1190
+ };
1191
+ const flowEnd = alongY ? {
1192
+ point: {
1193
+ x: ports?.end ?? to.x + to.w / 2,
1194
+ y: down ? to.y : to.y + to.h
1195
+ },
1196
+ dir: down ? 1 : 3
1197
+ } : {
1198
+ point: {
1199
+ x: right ? to.x : to.x + to.w,
1200
+ y: ports?.end ?? to.y + to.h / 2
1201
+ },
1202
+ dir: right ? 0 : 2
1203
+ };
1204
+ const crossStart = alongY ? {
1205
+ point: {
1206
+ x: right ? from.x + from.w : from.x,
1207
+ y: from.y + from.h / 2
1208
+ },
1209
+ dir: right ? 0 : 2
1210
+ } : {
1211
+ point: {
1212
+ x: from.x + from.w / 2,
1213
+ y: down ? from.y + from.h : from.y
1214
+ },
1215
+ dir: down ? 1 : 3
1216
+ };
1217
+ const crossEnd = alongY ? {
1218
+ point: {
1219
+ x: right ? to.x : to.x + to.w,
1220
+ y: to.y + to.h / 2
1221
+ },
1222
+ dir: right ? 0 : 2
1223
+ } : {
1224
+ point: {
1225
+ x: to.x + to.w / 2,
1226
+ y: down ? to.y : to.y + to.h
1227
+ },
1228
+ dir: down ? 1 : 3
1229
+ };
1230
+ const configurations = (alongY ? flowStart.point.x !== flowEnd.point.x : flowStart.point.y !== flowEnd.point.y) ? [
1231
+ [flowStart, flowEnd],
1232
+ [crossStart, flowEnd],
1233
+ [flowStart, crossEnd]
1234
+ ] : [[flowStart, flowEnd]];
1235
+ const stub = (point, dir, out) => {
1236
+ const sign = out ? 1 : -1;
1237
+ return {
1238
+ x: Math.round(point.x / STEP) + DX[dir] * 2 * sign,
1239
+ y: Math.round(point.y / STEP) + DY[dir] * 2 * sign
1240
+ };
1241
+ };
1242
+ let start;
1243
+ let end;
1244
+ let best;
1245
+ for (const [startTerminal, endTerminal] of configurations) {
1246
+ const result = search(stub(startTerminal.point, startTerminal.dir, true), startTerminal.dir, stub(endTerminal.point, endTerminal.dir, false), endTerminal.dir);
1247
+ if (result && (!best || result.cost < best.cost)) {
1248
+ best = result;
1249
+ start = startTerminal.point;
1250
+ end = endTerminal.point;
1251
+ }
1252
+ }
1253
+ if (!best || !start || !end) return fallback(edge);
1254
+ const points = [
1255
+ start,
1256
+ ...best.cells.map((cell) => ({
1257
+ x: cell.x * STEP,
1258
+ y: cell.y * STEP
1259
+ })),
1260
+ end
1261
+ ];
1262
+ const simplified = [points[0]];
1263
+ for (let index = 1; index < points.length - 1; index++) {
1264
+ const previous = simplified[simplified.length - 1];
1265
+ const next = points[index + 1];
1266
+ const point = points[index];
1267
+ if (!(previous.x === point.x && point.x === next.x || previous.y === point.y && point.y === next.y)) simplified.push(point);
1268
+ }
1269
+ simplified.push(points[points.length - 1]);
1270
+ const clearRun = (a, b) => {
1271
+ const ax = Math.round(a.x / STEP);
1272
+ const ay = Math.round(a.y / STEP);
1273
+ const bx = Math.round(b.x / STEP);
1274
+ const by = Math.round(b.y / STEP);
1275
+ const steps = Math.max(Math.abs(bx - ax), Math.abs(by - ay));
1276
+ const dx = Math.sign(bx - ax);
1277
+ const dy = Math.sign(by - ay);
1278
+ for (let step = 0; step <= steps; step++) if (blocked(ax + dx * step, ay + dy * step) || isUsed(ax + dx * step, ay + dy * step)) return false;
1279
+ return true;
1280
+ };
1281
+ const past = (from, to) => ({
1282
+ x: from.x + Math.sign(to.x - from.x) * 2 * STEP,
1283
+ y: from.y + Math.sign(to.y - from.y) * 2 * STEP
1284
+ });
1285
+ if (simplified.length === 4) {
1286
+ const [p0, p1, p2, p3] = simplified;
1287
+ if (p1.y === p2.y && p0.y !== p3.y) {
1288
+ const mid = Math.round((p0.y + p3.y) / 2 / STEP) * STEP;
1289
+ const shifted = [{
1290
+ x: p1.x,
1291
+ y: mid
1292
+ }, {
1293
+ x: p2.x,
1294
+ y: mid
1295
+ }];
1296
+ if (clearRun(shifted[0], shifted[1]) && clearRun(past(p0, shifted[0]), shifted[0]) && clearRun(shifted[1], past(p3, shifted[1]))) {
1297
+ simplified[1] = shifted[0];
1298
+ simplified[2] = shifted[1];
1299
+ }
1300
+ } else if (p1.x === p2.x && p0.x !== p3.x) {
1301
+ const mid = Math.round((p0.x + p3.x) / 2 / STEP) * STEP;
1302
+ const shifted = [{
1303
+ x: mid,
1304
+ y: p1.y
1305
+ }, {
1306
+ x: mid,
1307
+ y: p2.y
1308
+ }];
1309
+ if (clearRun(shifted[0], shifted[1]) && clearRun(past(p0, shifted[0]), shifted[0]) && clearRun(shifted[1], past(p3, shifted[1]))) {
1310
+ simplified[1] = shifted[0];
1311
+ simplified[2] = shifted[1];
1312
+ }
1313
+ }
1314
+ }
1315
+ for (let index = 0; index < simplified.length - 1; index++) {
1316
+ const a = simplified[index];
1317
+ const b = simplified[index + 1];
1318
+ const ax = Math.round(a.x / STEP);
1319
+ const ay = Math.round(a.y / STEP);
1320
+ const steps = Math.max(Math.abs(Math.round(b.x / STEP) - ax), Math.abs(Math.round(b.y / STEP) - ay));
1321
+ const dx = Math.sign(b.x - a.x);
1322
+ const dy = Math.sign(b.y - a.y);
1323
+ for (let step = 0; step <= steps; step++) markUsed(ax + dx * step, ay + dy * step);
1324
+ }
1325
+ return simplified;
1326
+ };
1327
+ };
1328
+ //#endregion
1329
+ //#region src/uml.ts
1330
+ var uml_exports = /* @__PURE__ */ __exportAll({
1331
+ compile: () => compile$6,
1332
+ isClassDiagram: () => isClassDiagram,
1333
+ parse: () => parse,
1334
+ relationRanks: () => relationRanks,
1335
+ relationStyle: () => relationStyle,
1336
+ relationText: () => relationText
1337
+ });
1338
+ var DIRECTIONS = [
1339
+ "TB",
1340
+ "BT",
1341
+ "LR",
1342
+ "RL"
1343
+ ];
1344
+ var CLASS = /^class\s+([A-Za-z0-9_~<>-]+)(?:\s*\[\s*"(.*?)"\s*\])?\s*(\{)?\s*$/;
1345
+ var INLINE_MEMBER = /^([A-Za-z0-9_~<>-]+)\s*:\s*(.+)$/;
1346
+ var STEREOTYPE = /^<<\s*(.*?)\s*>>$/;
1347
+ /**
1348
+ * Relation arrows, normalized to (kind, whether the LEFT side is the arrow target).
1349
+ * `A <|-- B` reads "B inherits A": the parent is on the flat side of the triangle.
1350
+ */
1351
+ var RELATIONS = [
1352
+ [
1353
+ "<|--",
1354
+ "inheritance",
1355
+ true
1356
+ ],
1357
+ [
1358
+ "--|>",
1359
+ "inheritance",
1360
+ false
1361
+ ],
1362
+ [
1363
+ "<|..",
1364
+ "realization",
1365
+ true
1366
+ ],
1367
+ [
1368
+ "..|>",
1369
+ "realization",
1370
+ false
1371
+ ],
1372
+ [
1373
+ "*--",
1374
+ "composition",
1375
+ false
1376
+ ],
1377
+ [
1378
+ "--*",
1379
+ "composition",
1380
+ true
1381
+ ],
1382
+ [
1383
+ "o--",
1384
+ "aggregation",
1385
+ false
1386
+ ],
1387
+ [
1388
+ "--o",
1389
+ "aggregation",
1390
+ true
1391
+ ],
1392
+ [
1393
+ "-->",
1394
+ "association",
1395
+ false
1396
+ ],
1397
+ [
1398
+ "<--",
1399
+ "association",
1400
+ true
1401
+ ],
1402
+ [
1403
+ "..>",
1404
+ "dependency",
1405
+ false
1406
+ ],
1407
+ [
1408
+ "<..",
1409
+ "dependency",
1410
+ true
1411
+ ],
1412
+ [
1413
+ "..",
1414
+ "dependency",
1415
+ false
1416
+ ],
1417
+ [
1418
+ "--",
1419
+ "association",
1420
+ false
1421
+ ]
1422
+ ];
1423
+ var RELATION = new RegExp(`^([A-Za-z0-9_~<>-]+)\\s*(?:"([^"]*)"\\s*)?(${RELATIONS.map(([token]) => token.replace(/[|*.>-]/g, "\\$&")).join("|")})\\s*(?:"([^"]*)"\\s*)?([A-Za-z0-9_~<>-]+)\\s*(?::\\s*(.+))?\$`);
1424
+ /** Mermaid generics use `~T~`; render them as `<T>`. */
1425
+ var genericLabel = (id) => id.replace(/~([^~]*)~/g, "<$1>");
1426
+ /** Parse a mermaid class diagram into a neutral model. Unrecognised lines are ignored. */
1427
+ var parse = (source) => {
1428
+ const classes = /* @__PURE__ */ new Map();
1429
+ const relations = [];
1430
+ let direction = "TB";
1431
+ let open;
1432
+ const declare = (id) => {
1433
+ let entry = classes.get(id);
1434
+ if (!entry) {
1435
+ entry = {
1436
+ id,
1437
+ label: genericLabel(id),
1438
+ attributes: [],
1439
+ methods: []
1440
+ };
1441
+ classes.set(id, entry);
1442
+ }
1443
+ return entry;
1444
+ };
1445
+ const addMember = (entry, raw) => {
1446
+ const member = raw.trim();
1447
+ const stereotype = STEREOTYPE.exec(member);
1448
+ if (stereotype) {
1449
+ entry.stereotype = stereotype[1];
1450
+ return;
1451
+ }
1452
+ if (member) (member.includes("(") ? entry.methods : entry.attributes).push(member);
1453
+ };
1454
+ for (const raw of source.split("\n")) {
1455
+ const line = raw.trim();
1456
+ if (!line || line.startsWith("%%") || /^classDiagram(-v2)?$/.test(line)) continue;
1457
+ if (open) {
1458
+ if (line === "}") open = void 0;
1459
+ else addMember(open, line);
1460
+ continue;
1461
+ }
1462
+ const header = /^direction\s+([A-Za-z]{2})$/.exec(line);
1463
+ if (header) {
1464
+ const value = header[1].toUpperCase();
1465
+ direction = DIRECTIONS.includes(value) ? value : "TB";
1466
+ continue;
1467
+ }
1468
+ const cls = CLASS.exec(line);
1469
+ if (cls) {
1470
+ const [, id, label, brace] = cls;
1471
+ const entry = declare(id);
1472
+ if (label) entry.label = label;
1473
+ if (brace) open = entry;
1474
+ continue;
1475
+ }
1476
+ const relation = RELATION.exec(line);
1477
+ if (relation) {
1478
+ const [, left, leftCardinality, token, rightCardinality, right, label] = relation;
1479
+ declare(left);
1480
+ declare(right);
1481
+ const [, kind, leftIsTarget] = RELATIONS.find(([candidate]) => candidate === token);
1482
+ const [from, to] = leftIsTarget ? [right, left] : [left, right];
1483
+ const [fromCardinality, toCardinality] = leftIsTarget ? [rightCardinality, leftCardinality] : [leftCardinality, rightCardinality];
1484
+ relations.push({
1485
+ from,
1486
+ to,
1487
+ kind,
1488
+ ...label ? { label: label.trim() } : {},
1489
+ ...fromCardinality ? { fromCardinality } : {},
1490
+ ...toCardinality ? { toCardinality } : {}
1491
+ });
1492
+ continue;
1493
+ }
1494
+ const inline = INLINE_MEMBER.exec(line);
1495
+ if (inline) {
1496
+ const [, id, member] = inline;
1497
+ addMember(declare(id), member);
1498
+ }
1499
+ }
1500
+ return {
1501
+ direction,
1502
+ classes: [...classes.values()],
1503
+ relations
1504
+ };
1505
+ };
1506
+ /**
1507
+ * Box geometry in scene units; text sizes come from the shared per-weight metrics table
1508
+ * (`Layout.FONT_METRICS`): the title renders at the default weight (m), members at s.
1509
+ */
1510
+ var TITLE_FONT$1 = FONT_METRICS.m;
1511
+ var MEMBER_FONT$1 = FONT_METRICS.s;
1512
+ var MIN_W$2 = 160;
1513
+ var MAX_W$2 = 400;
1514
+ var PAD_X$2 = 24;
1515
+ var TITLE_PAD$1 = 10;
1516
+ var SECTION_PAD$1 = 14;
1517
+ var GAP_MAIN$2 = 90;
1518
+ var GAP_CROSS$2 = 60;
1519
+ /** Compartment sizes derived from member counts and line lengths, per compartment font. */
1520
+ var measure = (entry, maxWidth) => {
1521
+ const titleW = Math.max(entry.label.length, (entry.stereotype?.length ?? 0) + 2) * TITLE_FONT$1.charW;
1522
+ const memberW = Math.max(0, ...[...entry.attributes, ...entry.methods].map((line) => line.length)) * MEMBER_FONT$1.charW;
1523
+ const w = Math.min(maxWidth, Math.max(MIN_W$2, Math.ceil(Math.max(titleW, memberW)) + PAD_X$2));
1524
+ const lines = (text, font) => Math.max(1, Math.ceil(text.length * font.charW / (w - PAD_X$2)));
1525
+ const memberH = (members) => members.length ? members.reduce((sum, line) => sum + lines(line, MEMBER_FONT$1), 0) * MEMBER_FONT$1.lineH + SECTION_PAD$1 : 0;
1526
+ const titleH = (lines(entry.label, TITLE_FONT$1) + (entry.stereotype ? 1 : 0)) * TITLE_FONT$1.lineH + TITLE_PAD$1;
1527
+ const attributesH = memberH(entry.attributes);
1528
+ const methodsH = memberH(entry.methods);
1529
+ return {
1530
+ entry,
1531
+ w,
1532
+ h: titleH + attributesH + methodsH,
1533
+ titleH,
1534
+ attributesH,
1535
+ methodsH
1536
+ };
1537
+ };
1538
+ /** Arrow styling per relation kind; dependency and realization render dashed, as in UML. */
1539
+ var relationStyle = (kind) => kind === "dependency" || kind === "realization" ? { stroke: "dashed" } : {};
1540
+ /** Mid-arrow label: cardinalities and the diamond glyph UML puts at the whole end. */
1541
+ var relationText = (relation) => {
1542
+ const glyph = relation.kind === "composition" ? "◆" : relation.kind === "aggregation" ? "◇" : void 0;
1543
+ return [
1544
+ relation.fromCardinality,
1545
+ relation.label ?? glyph,
1546
+ relation.toCardinality
1547
+ ].filter(Boolean).join(" ") || void 0;
1548
+ };
1549
+ /**
1550
+ * Rank so arrows read upward where UML convention points at abstractions — inheritance,
1551
+ * realization, and dependency targets sit above their sources; containment and association
1552
+ * flow downward (whole/source above part/target).
1553
+ */
1554
+ var relationRanks = (model) => rank$1(model.classes.map((entry) => entry.id), model.relations.map((relation) => relation.kind === "inheritance" || relation.kind === "realization" || relation.kind === "dependency" ? {
1555
+ from: relation.to,
1556
+ to: relation.from
1557
+ } : {
1558
+ from: relation.from,
1559
+ to: relation.to
1560
+ }));
1561
+ /**
1562
+ * Compile a mermaid class diagram into scene commands: one world object per class (compartment
1563
+ * rects `title`/`attributes`/`methods`) and an `edges` object holding the relations as bound
1564
+ * arrows. Supertypes rank above subtypes, so inheritance reads downward.
1565
+ */
1566
+ var compile$6 = (source, options = {}) => {
1567
+ const model = parse(source);
1568
+ const { origin = {
1569
+ x: 0,
1570
+ y: 0
1571
+ }, scale = 1, gapMain = GAP_MAIN$2, gapCross = GAP_CROSS$2, maxWidth = MAX_W$2 } = options;
1572
+ const horizontal = model.direction === "LR" || model.direction === "RL";
1573
+ const ranks = relationRanks(model);
1574
+ const boxes = new Map(model.classes.map((entry) => [entry.id, measure(entry, maxWidth)]));
1575
+ const lanes = /* @__PURE__ */ new Map();
1576
+ for (const entry of model.classes) {
1577
+ const value = ranks.get(entry.id) ?? 0;
1578
+ lanes.set(value, [...lanes.get(value) ?? [], boxes.get(entry.id)]);
1579
+ }
1580
+ const cross = (box) => horizontal ? box.h : box.w;
1581
+ const main = (box) => horizontal ? box.w : box.h;
1582
+ const laneSpan = (members) => members.reduce((sum, box) => sum + cross(box), 0) + (members.length - 1) * gapCross;
1583
+ const widest = Math.max(...[...lanes.values()].map(laneSpan), 0);
1584
+ const positions = /* @__PURE__ */ new Map();
1585
+ let mainOffset = 0;
1586
+ for (const lane of [...lanes.keys()].sort((left, right) => left - right)) {
1587
+ const members = lanes.get(lane);
1588
+ let crossOffset = (widest - laneSpan(members)) / 2;
1589
+ for (const box of members) {
1590
+ positions.set(box.entry.id, horizontal ? {
1591
+ x: mainOffset,
1592
+ y: crossOffset
1593
+ } : {
1594
+ x: crossOffset,
1595
+ y: mainOffset
1596
+ });
1597
+ crossOffset += cross(box) + gapCross;
1598
+ }
1599
+ mainOffset += Math.max(...members.map(main)) + gapMain;
1600
+ }
1601
+ const facing = (id, peerId) => {
1602
+ const box = boxes.get(id);
1603
+ const peer = boxes.get(peerId);
1604
+ const targetY = positions.get(peerId).y + peer.h / 2;
1605
+ const baseY = positions.get(id).y;
1606
+ const compartments = [["title", box.titleH / 2]];
1607
+ if (box.attributesH) compartments.push(["attributes", box.titleH + box.attributesH / 2]);
1608
+ if (box.methodsH) compartments.push(["methods", box.titleH + box.attributesH + box.methodsH / 2]);
1609
+ return compartments.reduce((best, candidate) => Math.abs(baseY + candidate[1] - targetY) < Math.abs(baseY + best[1] - targetY) ? candidate : best)[0];
1610
+ };
1611
+ const commands = [];
1612
+ for (const entry of model.classes) {
1613
+ const box = boxes.get(entry.id);
1614
+ const point = positions.get(entry.id);
1615
+ const title = entry.stereotype ? `«${entry.stereotype}»\n${entry.label}` : entry.label;
1616
+ const elements = [{
1617
+ kind: "rect",
1618
+ id: "title",
1619
+ x: 0,
1620
+ y: 0,
1621
+ w: box.w,
1622
+ h: box.titleH,
1623
+ text: title,
1624
+ fill: "solid"
1625
+ }];
1626
+ if (box.attributesH) elements.push({
1627
+ kind: "rect",
1628
+ id: "attributes",
1629
+ x: 0,
1630
+ y: box.titleH,
1631
+ w: box.w,
1632
+ h: box.attributesH,
1633
+ text: entry.attributes.join("\n"),
1634
+ weight: "s"
1635
+ });
1636
+ if (box.methodsH) elements.push({
1637
+ kind: "rect",
1638
+ id: "methods",
1639
+ x: 0,
1640
+ y: box.titleH + box.attributesH,
1641
+ w: box.w,
1642
+ h: box.methodsH,
1643
+ text: entry.methods.join("\n"),
1644
+ weight: "s"
1645
+ });
1646
+ commands.push({
1647
+ op: "upsert-object",
1648
+ object: {
1649
+ id: entry.id,
1650
+ origin: {
1651
+ x: origin.x + point.x * scale,
1652
+ y: origin.y + point.y * scale
1653
+ },
1654
+ scale,
1655
+ elements
1656
+ }
1657
+ });
1658
+ }
1659
+ if (model.relations.length > 0) commands.push({
1660
+ op: "upsert-object",
1661
+ object: {
1662
+ id: "edges",
1663
+ origin,
1664
+ scale,
1665
+ elements: model.relations.map((relation, index) => {
1666
+ const text = relationText(relation);
1667
+ return {
1668
+ kind: "arrow",
1669
+ id: `${relation.from}-${relation.to}-${index}`,
1670
+ from: `${relation.from}/${facing(relation.from, relation.to)}`,
1671
+ to: `${relation.to}/${facing(relation.to, relation.from)}`,
1672
+ ...relationStyle(relation.kind),
1673
+ ...text ? { text } : {}
1674
+ };
1675
+ })
1676
+ }
1677
+ });
1678
+ return commands;
1679
+ };
1680
+ /** True when the mermaid source declares a class diagram rather than a flowchart. */
1681
+ var isClassDiagram = (source) => /^\s*classDiagram(-v2)?\b/m.test(source);
1682
+ //#endregion
1683
+ //#region src/uml-grid.ts
1684
+ var uml_grid_exports = /* @__PURE__ */ __exportAll({
1685
+ GRID: () => 32,
1686
+ compile: () => compile$5,
1687
+ emit: () => emit$1,
1688
+ measureCell: () => measureCell$1,
1689
+ zRouter: () => zRouter
1690
+ });
1691
+ /** Half-cell minor grid, used for sub-cell nudges (ports, channel separation, label offsets). */
1692
+ var GRID_FINE$1 = 16;
1693
+ var MIN_W$1 = 64;
1694
+ var MAX_W$1 = 192;
1695
+ var MIN_H$1 = 64;
1696
+ var MAX_H = 160;
1697
+ var MIN_TITLE_H = 32;
1698
+ var MAX_TITLE_H = 64;
1699
+ var TITLE_PAD = 16;
1700
+ var TEXT_PAD = 16;
1701
+ var PAD_X$1 = 64;
1702
+ var SECTION_PAD = 16;
1703
+ var GAP_MAIN$1 = 128;
1704
+ var GAP_CROSS$1 = 128;
1705
+ var TITLE_FONT = FONT_METRICS.m;
1706
+ var MEMBER_FONT = FONT_METRICS.s;
1707
+ var snap$3 = (value) => Math.ceil(value / 32) * 32;
1708
+ /** Z route along y: leave the facing horizontal edge, run a channel at mid, enter the peer's. */
1709
+ var routeAlongY = (from, to, shift, ports) => {
1710
+ const down = to.y >= from.y;
1711
+ const start = {
1712
+ x: ports?.start ?? from.x + from.w / 2,
1713
+ y: down ? from.y + from.h : from.y
1714
+ };
1715
+ const end = {
1716
+ x: ports?.end ?? to.x + to.w / 2,
1717
+ y: down ? to.y : to.y + to.h
1718
+ };
1719
+ if (start.x === end.x) return [{
1720
+ x: start.x + shift,
1721
+ y: start.y
1722
+ }, {
1723
+ x: end.x + shift,
1724
+ y: end.y
1725
+ }];
1726
+ const mid = snap$3((start.y + end.y) / 2) + shift;
1727
+ return [
1728
+ start,
1729
+ {
1730
+ x: start.x,
1731
+ y: mid
1732
+ },
1733
+ {
1734
+ x: end.x,
1735
+ y: mid
1736
+ },
1737
+ end
1738
+ ];
1739
+ };
1740
+ /** Z route along x; mirror of `routeAlongY`. */
1741
+ var routeAlongX = (from, to, shift, ports) => {
1742
+ const right = to.x >= from.x;
1743
+ const start = {
1744
+ x: right ? from.x + from.w : from.x,
1745
+ y: ports?.start ?? from.y + from.h / 2
1746
+ };
1747
+ const end = {
1748
+ x: right ? to.x : to.x + to.w,
1749
+ y: ports?.end ?? to.y + to.h / 2
1750
+ };
1751
+ if (start.y === end.y) return [{
1752
+ x: start.x,
1753
+ y: start.y + shift
1754
+ }, {
1755
+ x: end.x,
1756
+ y: end.y + shift
1757
+ }];
1758
+ const mid = snap$3((start.x + end.x) / 2) + shift;
1759
+ return [
1760
+ start,
1761
+ {
1762
+ x: mid,
1763
+ y: start.y
1764
+ },
1765
+ {
1766
+ x: mid,
1767
+ y: end.y
1768
+ },
1769
+ end
1770
+ ];
1771
+ };
1772
+ /**
1773
+ * Default router: an orthogonal Z along the flow axis — rank-crossing edges route through the
1774
+ * inter-rank gutter (channel shifted per parallel edge); same-rank edges route out the side.
1775
+ */
1776
+ var zRouter = ({ from, to, offset, horizontal, ports }) => {
1777
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
1778
+ return ((horizontal ? sameLane : !sameLane) ? routeAlongY : routeAlongX)(from, to, offset * GRID_FINE$1, ports);
1779
+ };
1780
+ /**
1781
+ * Pull strength per relation kind when aligning columns: parts stick to their whole's column
1782
+ * before subtypes, which in turn beat loose dependencies/associations.
1783
+ */
1784
+ var COLUMN_WEIGHT = {
1785
+ composition: 4,
1786
+ aggregation: 3,
1787
+ inheritance: 2,
1788
+ realization: 2,
1789
+ dependency: 1,
1790
+ association: 1
1791
+ };
1792
+ /**
1793
+ * Assign each node an integer column: the first lane keeps declaration order, later lanes pull
1794
+ * each node toward the weighted mean column of its already-placed neighbours. The strongest-tied
1795
+ * node picks its column first, so a part claims the column under its whole even when weaker
1796
+ * relations compete for it; collisions take the free column nearest the desired mean. Aligned
1797
+ * columns turn rank-crossing connectors into straight lines.
1798
+ */
1799
+ var assignColumns = (model, ranks, lanes) => {
1800
+ const neighbors = /* @__PURE__ */ new Map();
1801
+ for (const relation of model.relations) {
1802
+ const weight = COLUMN_WEIGHT[relation.kind];
1803
+ neighbors.set(relation.from, [...neighbors.get(relation.from) ?? [], {
1804
+ peer: relation.to,
1805
+ weight
1806
+ }]);
1807
+ neighbors.set(relation.to, [...neighbors.get(relation.to) ?? [], {
1808
+ peer: relation.from,
1809
+ weight
1810
+ }]);
1811
+ }
1812
+ const columns = /* @__PURE__ */ new Map();
1813
+ [...lanes.keys()].sort((left, right) => left - right).forEach((lane, position) => {
1814
+ const desired = lanes.get(lane).map((id, index) => {
1815
+ const placed = (neighbors.get(id) ?? []).filter((link) => columns.has(link.peer));
1816
+ const pull = placed.reduce((sum, link) => sum + link.weight, 0);
1817
+ return {
1818
+ id,
1819
+ mean: position > 0 && pull > 0 ? placed.reduce((sum, link) => sum + link.weight * columns.get(link.peer), 0) / pull : index,
1820
+ pull,
1821
+ index
1822
+ };
1823
+ });
1824
+ desired.sort((left, right) => right.pull - left.pull || left.index - right.index);
1825
+ const used = /* @__PURE__ */ new Set();
1826
+ for (const entry of desired) {
1827
+ const base = Math.ceil(entry.mean - .5);
1828
+ let column = base;
1829
+ for (let step = 1; used.has(column); step++) {
1830
+ const left = base - step;
1831
+ const right = base + step;
1832
+ if (!used.has(left) && (used.has(right) || Math.abs(left - entry.mean) <= Math.abs(right - entry.mean))) column = left;
1833
+ else if (!used.has(right)) column = right;
1834
+ }
1835
+ used.add(column);
1836
+ columns.set(entry.id, column);
1837
+ }
1838
+ });
1839
+ const min = Math.min(...columns.values(), 0);
1840
+ return new Map([...columns].map(([id, column]) => [id, column - min]));
1841
+ };
1842
+ /** One cell fits the largest class: widest line (wrapped at maxWidth) and tallest member list. */
1843
+ var measureCell$1 = (model, { maxWidth, cell: override = {}, titleHeight }) => {
1844
+ const memberLines = (entry) => [...entry.attributes, ...entry.methods];
1845
+ const titleW = Math.max(0, ...model.classes.map((entry) => Math.max(entry.label.length, (entry.stereotype?.length ?? 0) + 2))) * TITLE_FONT.charW;
1846
+ const memberW = Math.max(0, ...model.classes.flatMap((entry) => memberLines(entry).map((line) => line.length))) * MEMBER_FONT.charW;
1847
+ const w = snap$3(override.w ?? Math.min(maxWidth, Math.max(MIN_W$1, Math.ceil(Math.max(titleW, memberW)) + PAD_X$1)));
1848
+ const wrapped = (length, font) => Math.max(1, Math.ceil(length * font.charW / (w - 32)));
1849
+ const titleLines = Math.max(1, ...model.classes.map((entry) => wrapped(entry.label.length, TITLE_FONT) + (entry.stereotype ? 1 : 0)));
1850
+ const bodyLines = Math.max(1, ...model.classes.map((entry) => memberLines(entry).reduce((sum, line) => sum + wrapped(line.length, MEMBER_FONT), 0) + (entry.attributes.length && entry.methods.length ? 1 : 0)));
1851
+ const titleH = titleHeight !== void 0 ? Math.ceil(titleHeight / GRID_FINE$1) * GRID_FINE$1 : Math.min(MAX_TITLE_H, Math.max(MIN_TITLE_H, snap$3(titleLines * TITLE_FONT.lineH + TITLE_PAD)));
1852
+ const bodyH = snap$3(bodyLines * MEMBER_FONT.lineH + SECTION_PAD);
1853
+ const measured = Math.min(MAX_H, Math.max(MIN_H$1, titleH + bodyH));
1854
+ return {
1855
+ w,
1856
+ h: override.h !== void 0 ? Math.max(snap$3(override.h), titleH + 32) : measured,
1857
+ titleH
1858
+ };
1859
+ };
1860
+ /**
1861
+ * Compile a mermaid class diagram into scene commands with equal-size grid-aligned nodes: per
1862
+ * class a `title` bar plus a `body` compartment filling the cell, and an `edges` object holding
1863
+ * orthogonal connector paths (polyline waypoints, an arrowhead segment, and a floating label).
1864
+ */
1865
+ var compile$5 = (source, options = {}) => {
1866
+ const model = parse(source);
1867
+ const { origin = {
1868
+ x: 0,
1869
+ y: 0
1870
+ }, scale = 1, maxWidth = MAX_W$1, route } = options;
1871
+ const gapMain = snap$3(options.gapMain ?? GAP_MAIN$1);
1872
+ const gapCross = snap$3(options.gapCross ?? GAP_CROSS$1);
1873
+ const horizontal = model.direction === "LR" || model.direction === "RL";
1874
+ const cell = measureCell$1(model, {
1875
+ maxWidth,
1876
+ cell: options.cell,
1877
+ titleHeight: options.titleHeight
1878
+ });
1879
+ const ranks = relationRanks(model);
1880
+ const lanes = /* @__PURE__ */ new Map();
1881
+ for (const entry of model.classes) {
1882
+ const value = ranks.get(entry.id) ?? 0;
1883
+ lanes.set(value, [...lanes.get(value) ?? [], entry.id]);
1884
+ }
1885
+ const cross = horizontal ? cell.h : cell.w;
1886
+ const main = horizontal ? cell.w : cell.h;
1887
+ const columns = assignColumns(model, ranks, lanes);
1888
+ const rects = /* @__PURE__ */ new Map();
1889
+ for (const [lane, members] of lanes) for (const id of members) {
1890
+ const crossOffset = columns.get(id) * (cross + gapCross);
1891
+ const mainOffset = lane * (main + gapMain);
1892
+ rects.set(id, {
1893
+ x: horizontal ? mainOffset : crossOffset,
1894
+ y: horizontal ? crossOffset : mainOffset,
1895
+ w: cell.w,
1896
+ h: cell.h
1897
+ });
1898
+ }
1899
+ return emit$1({
1900
+ model,
1901
+ cell,
1902
+ rects,
1903
+ ranks
1904
+ }, {
1905
+ origin,
1906
+ scale,
1907
+ route
1908
+ });
1909
+ };
1910
+ /**
1911
+ * Emit scene commands from a placed model — the shared back half of `compile`, also used by the
1912
+ * engine-backed dialects (`uml-engine.ts`) which supply their own placement.
1913
+ */
1914
+ var emit$1 = ({ model, cell, rects, ranks }, { origin = {
1915
+ x: 0,
1916
+ y: 0
1917
+ }, scale = 1, route } = {}) => {
1918
+ const horizontal = model.direction === "LR" || model.direction === "RL";
1919
+ const router = route ?? makeAvoidingRouter([...rects.values()], zRouter, { step: 8 });
1920
+ const commands = [];
1921
+ for (const entry of model.classes) {
1922
+ const rect = rects.get(entry.id);
1923
+ const title = entry.stereotype ? `«${entry.stereotype}»\n${entry.label}` : entry.label;
1924
+ const members = [
1925
+ ...entry.attributes,
1926
+ ...entry.attributes.length && entry.methods.length ? [""] : [],
1927
+ ...entry.methods
1928
+ ].join("\n");
1929
+ commands.push({
1930
+ op: "upsert-object",
1931
+ object: {
1932
+ id: entry.id,
1933
+ origin: {
1934
+ x: origin.x + rect.x * scale,
1935
+ y: origin.y + rect.y * scale
1936
+ },
1937
+ scale,
1938
+ elements: [
1939
+ {
1940
+ kind: "rect",
1941
+ id: "frame",
1942
+ x: 0,
1943
+ y: 0,
1944
+ w: cell.w,
1945
+ h: cell.h
1946
+ },
1947
+ {
1948
+ kind: "rect",
1949
+ id: "title",
1950
+ x: 0,
1951
+ y: 0,
1952
+ w: cell.w,
1953
+ h: cell.titleH,
1954
+ text: title,
1955
+ fill: "solid",
1956
+ corners: "top"
1957
+ },
1958
+ ...members ? [{
1959
+ kind: "text",
1960
+ id: "body",
1961
+ x: TEXT_PAD,
1962
+ y: cell.titleH + SECTION_PAD / 2,
1963
+ w: cell.w - 32,
1964
+ text: members,
1965
+ weight: "s"
1966
+ }] : []
1967
+ ]
1968
+ }
1969
+ });
1970
+ }
1971
+ if (model.relations.length > 0) {
1972
+ const gutterOf = (relation) => {
1973
+ const from = rects.get(relation.from);
1974
+ const to = rects.get(relation.to);
1975
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
1976
+ if (horizontal ? sameLane : !sameLane) {
1977
+ if (from.x + from.w / 2 === to.x + to.w / 2) return `v:${from.x + from.w / 2}`;
1978
+ return `y:${snap$3(((to.y >= from.y ? from.y + from.h : from.y) + (to.y >= from.y ? to.y : to.y + to.h)) / 2)}`;
1979
+ }
1980
+ if (from.y + from.h / 2 === to.y + to.h / 2) return `h:${from.y + from.h / 2}`;
1981
+ return `x:${snap$3(((to.x >= from.x ? from.x + from.w : from.x) + (to.x >= from.x ? to.x : to.x + to.w)) / 2)}`;
1982
+ };
1983
+ const gutters = /* @__PURE__ */ new Map();
1984
+ for (const relation of model.relations) {
1985
+ const gutter = gutterOf(relation);
1986
+ gutters.set(gutter, (gutters.get(gutter) ?? 0) + 1);
1987
+ }
1988
+ const portGroups = /* @__PURE__ */ new Map();
1989
+ const addSlot = (key, slot) => portGroups.set(key, [...portGroups.get(key) ?? [], slot]);
1990
+ for (const relation of model.relations) {
1991
+ const from = rects.get(relation.from);
1992
+ const to = rects.get(relation.to);
1993
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
1994
+ if (horizontal ? sameLane : !sameLane) {
1995
+ const down = to.y >= from.y;
1996
+ addSlot(`${relation.from}:${down ? "bottom" : "top"}`, {
1997
+ relation,
1998
+ end: false,
1999
+ order: to.x + to.w / 2
2000
+ });
2001
+ addSlot(`${relation.to}:${down ? "top" : "bottom"}`, {
2002
+ relation,
2003
+ end: true,
2004
+ order: from.x + from.w / 2
2005
+ });
2006
+ } else {
2007
+ const right = to.x >= from.x;
2008
+ addSlot(`${relation.from}:${right ? "right" : "left"}`, {
2009
+ relation,
2010
+ end: false,
2011
+ order: to.y + to.h / 2
2012
+ });
2013
+ addSlot(`${relation.to}:${right ? "left" : "right"}`, {
2014
+ relation,
2015
+ end: true,
2016
+ order: from.y + from.h / 2
2017
+ });
2018
+ }
2019
+ }
2020
+ const ports = /* @__PURE__ */ new Map();
2021
+ for (const [key, slots] of portGroups) {
2022
+ if (slots.length < 2) continue;
2023
+ const nodeId = key.slice(0, key.lastIndexOf(":"));
2024
+ const rect = rects.get(nodeId);
2025
+ const across = key.endsWith(":top") || key.endsWith(":bottom");
2026
+ slots.sort((left, right) => left.order - right.order);
2027
+ const taken = /* @__PURE__ */ new Set();
2028
+ slots.forEach((slot, index) => {
2029
+ const fraction = (index + 1) / (slots.length + 1);
2030
+ const exact = across ? rect.x + rect.w * fraction : rect.y + rect.h * fraction;
2031
+ const rounded = Math.round(exact / GRID_FINE$1) * GRID_FINE$1;
2032
+ const coord = taken.has(rounded) ? exact : rounded;
2033
+ taken.add(coord);
2034
+ const entry = ports.get(slot.relation) ?? {};
2035
+ entry[slot.end ? "end" : "start"] = coord;
2036
+ ports.set(slot.relation, entry);
2037
+ });
2038
+ }
2039
+ const takenBySide = /* @__PURE__ */ new Map();
2040
+ for (const [key, slots] of portGroups) {
2041
+ const nodeId = key.slice(0, key.lastIndexOf(":"));
2042
+ const rect = rects.get(nodeId);
2043
+ const across = key.endsWith(":top") || key.endsWith(":bottom");
2044
+ const coords = /* @__PURE__ */ new Map();
2045
+ for (const slot of slots) {
2046
+ const port = ports.get(slot.relation)?.[slot.end ? "end" : "start"];
2047
+ coords.set(slot.relation, port ?? (across ? rect.x + rect.w / 2 : rect.y + rect.h / 2));
2048
+ }
2049
+ takenBySide.set(key, coords);
2050
+ }
2051
+ const fineSnap = (value) => Math.round(value / GRID_FINE$1) * GRID_FINE$1;
2052
+ for (const relation of model.relations) {
2053
+ const from = rects.get(relation.from);
2054
+ const to = rects.get(relation.to);
2055
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
2056
+ const alongY = horizontal ? sameLane : !sameLane;
2057
+ const lo = alongY ? Math.max(from.x, to.x) + GRID_FINE$1 : Math.max(from.y, to.y) + GRID_FINE$1;
2058
+ const hi = alongY ? Math.min(from.x + from.w, to.x + to.w) - GRID_FINE$1 : Math.min(from.y + from.h, to.y + to.h) - GRID_FINE$1;
2059
+ if (lo > hi) continue;
2060
+ const startKey = alongY ? `${relation.from}:${to.y >= from.y ? "bottom" : "top"}` : `${relation.from}:${to.x >= from.x ? "right" : "left"}`;
2061
+ const endKey = alongY ? `${relation.to}:${to.y >= from.y ? "top" : "bottom"}` : `${relation.to}:${to.x >= from.x ? "left" : "right"}`;
2062
+ const entry = ports.get(relation) ?? {};
2063
+ const startCoord = entry.start ?? (alongY ? from.x + from.w / 2 : from.y + from.h / 2);
2064
+ const endCoord = entry.end ?? (alongY ? to.x + to.w / 2 : to.y + to.h / 2);
2065
+ const isFree = (coord) => ![startKey, endKey].some((side) => [...takenBySide.get(side) ?? /* @__PURE__ */ new Map()].some(([peer, taken]) => peer !== relation && Math.abs(taken - coord) < GRID_FINE$1));
2066
+ const base = Math.min(hi, Math.max(lo, fineSnap((startCoord + endCoord) / 2)));
2067
+ let chosen;
2068
+ for (let step = 0; step <= Math.ceil((hi - lo) / GRID_FINE$1) && chosen === void 0; step++) for (const candidate of step === 0 ? [base] : [base - step * GRID_FINE$1, base + step * GRID_FINE$1]) if (candidate >= lo && candidate <= hi && isFree(candidate)) {
2069
+ chosen = candidate;
2070
+ break;
2071
+ }
2072
+ if (chosen === void 0) continue;
2073
+ entry.start = chosen;
2074
+ entry.end = chosen;
2075
+ ports.set(relation, entry);
2076
+ takenBySide.get(startKey)?.set(relation, chosen);
2077
+ takenBySide.get(endKey)?.set(relation, chosen);
2078
+ }
2079
+ const assigned = /* @__PURE__ */ new Map();
2080
+ const elements = [];
2081
+ model.relations.forEach((relation, index) => {
2082
+ const gutter = gutterOf(relation);
2083
+ const position = assigned.get(gutter) ?? 0;
2084
+ assigned.set(gutter, position + 1);
2085
+ const offset = position - Math.floor((gutters.get(gutter) - 1) / 2);
2086
+ const points = router({
2087
+ relation,
2088
+ from: rects.get(relation.from),
2089
+ to: rects.get(relation.to),
2090
+ horizontal,
2091
+ offset,
2092
+ ports: ports.get(relation)
2093
+ });
2094
+ const id = `${relation.from}-${relation.to}-${index}`;
2095
+ const style = relationStyle(relation.kind);
2096
+ if (points.length > 2) elements.push({
2097
+ kind: "line",
2098
+ id: `${id}-path`,
2099
+ points: points.slice(0, -1),
2100
+ ...style
2101
+ });
2102
+ elements.push({
2103
+ kind: "arrow",
2104
+ id,
2105
+ start: points[points.length - 2],
2106
+ end: points[points.length - 1],
2107
+ ...style
2108
+ });
2109
+ const text = relationText(relation);
2110
+ if (text) {
2111
+ const head = points[Math.floor(points.length / 2) - 1];
2112
+ const tail = points[Math.floor(points.length / 2)];
2113
+ elements.push({
2114
+ kind: "text",
2115
+ id: `${id}-label`,
2116
+ x: (head.x + tail.x) / 2 + GRID_FINE$1 / 2,
2117
+ y: (head.y + tail.y) / 2 - MEMBER_FONT.lineH,
2118
+ text,
2119
+ weight: "s"
2120
+ });
2121
+ }
2122
+ });
2123
+ commands.push({
2124
+ op: "upsert-object",
2125
+ object: {
2126
+ id: "edges",
2127
+ origin,
2128
+ scale,
2129
+ elements
2130
+ }
2131
+ });
2132
+ }
2133
+ return commands;
2134
+ };
2135
+ //#endregion
2136
+ //#region src/objective.ts
2137
+ var objective_exports = /* @__PURE__ */ __exportAll({
2138
+ DEFAULT: () => DEFAULT,
2139
+ bends: () => bends$1,
2140
+ compactness: () => compactness,
2141
+ connectorLength: () => connectorLength,
2142
+ crossings: () => crossings,
2143
+ evaluate: () => evaluate,
2144
+ framesApart: () => framesApart,
2145
+ noHardDefects: () => noHardDefects,
2146
+ select: () => select,
2147
+ unevenFrameGaps: () => unevenFrameGaps
2148
+ });
2149
+ var __dxlog_file$1 = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/infinite-canvas-depth-7e0949/packages/common/diagram/src/objective.ts";
2150
+ var evaluate = (objective, layout) => {
2151
+ const violations = objective.constraints.flatMap((constraint) => constraint.violations(layout).map((message) => ({
2152
+ constraint: constraint.id,
2153
+ message
2154
+ })));
2155
+ const terms = objective.costs.map((term) => {
2156
+ const value = term.measure(layout);
2157
+ return {
2158
+ id: term.id,
2159
+ value,
2160
+ weighted: value * term.weight
2161
+ };
2162
+ });
2163
+ return {
2164
+ violations,
2165
+ cost: terms.reduce((total, term) => total + term.weighted, 0),
2166
+ terms
2167
+ };
2168
+ };
2169
+ /**
2170
+ * Picks the best candidate: fewest constraint violations first (so an infeasible set still yields
2171
+ * the least-bad layout rather than nothing), then lowest cost. Stable on ties, so the generator's
2172
+ * own order breaks them.
2173
+ */
2174
+ var select = (objective, candidates) => {
2175
+ invariant(candidates.length > 0, "select needs at least one candidate", {
2176
+ "~LogMeta": "~LogMeta",
2177
+ F: __dxlog_file$1,
2178
+ L: 74,
2179
+ S: void 0,
2180
+ A: ["candidates.length > 0", "'select needs at least one candidate'"]
2181
+ });
2182
+ const ranked = candidates.map((candidate) => ({
2183
+ candidate,
2184
+ evaluation: evaluate(objective, candidate.layout)
2185
+ })).sort((left, right) => left.evaluation.violations.length - right.evaluation.violations.length || left.evaluation.cost - right.evaluation.cost);
2186
+ return {
2187
+ chosen: ranked[0],
2188
+ ranked
2189
+ };
2190
+ };
2191
+ /** Every `error` diagnostic is a violation: overlaps, routes through nodes, labels that do not fit. */
2192
+ var noHardDefects = {
2193
+ id: "no-hard-defects",
2194
+ description: "No node overlaps, no connector through a node, every label fits its shape.",
2195
+ violations: ({ report }) => report.diagnostics.filter(({ severity }) => severity === "error").map(({ message }) => message)
2196
+ };
2197
+ /** Containers keep at least `gap` between them, so packages read as separate. */
2198
+ var framesApart = (gap = 32) => ({
2199
+ id: "frames-apart",
2200
+ description: `Frames are at least ${gap} apart.`,
2201
+ violations: ({ report }) => report.metrics.containers >= 2 && report.metrics.frameGapMin < gap ? [`Frames are ${report.metrics.frameGapMin} apart; at least ${gap} required.`] : []
2202
+ });
2203
+ var crossings = {
2204
+ id: "crossings",
2205
+ description: "Connector pairs that cross.",
2206
+ weight: 3,
2207
+ measure: ({ report }) => report.metrics.crossings
2208
+ };
2209
+ var bends$1 = {
2210
+ id: "bends",
2211
+ description: "Turns taken by connectors.",
2212
+ weight: 1,
2213
+ measure: ({ report }) => report.metrics.bends
2214
+ };
2215
+ /**
2216
+ * Uneven gutters between frames, in grid units. Weighted so a whole cell of unevenness costs about
2217
+ * half a crossing: on the pipeline corpus diagram a weight of 2 made the objective accept three
2218
+ * crossings to even out a 7-unit spread, which no reader would.
2219
+ */
2220
+ var unevenFrameGaps = {
2221
+ id: "uneven-frame-gaps",
2222
+ description: "Spread of the gaps between neighbouring frames.",
2223
+ weight: .5,
2224
+ measure: ({ report }) => report.metrics.frameGapSpread / 32
2225
+ };
2226
+ /**
2227
+ * Total connector length in grid units: a long edge is hard to follow even when it is straight.
2228
+ * Weighted so ~100 grid units of extra connector cost one crossing — at 0.1 the objective traded
2229
+ * crossings for shorter edges on the compute and edge corpus diagrams.
2230
+ */
2231
+ var connectorLength = {
2232
+ id: "connector-length",
2233
+ description: "Total connector length.",
2234
+ weight: .03,
2235
+ measure: ({ report }) => report.metrics.length / 32
2236
+ };
2237
+ /** Bounding perimeter in grid units; a light preference for the tighter of otherwise-equal layouts. */
2238
+ var compactness = {
2239
+ id: "compactness",
2240
+ description: "Bounding width + height.",
2241
+ weight: .05,
2242
+ measure: ({ report }) => (report.metrics.width + report.metrics.height) / 32
2243
+ };
2244
+ var DEFAULT = {
2245
+ constraints: [noHardDefects, framesApart()],
2246
+ costs: [
2247
+ crossings,
2248
+ bends$1,
2249
+ connectorLength,
2250
+ unevenFrameGaps,
2251
+ compactness
2252
+ ]
2253
+ };
2254
+ //#endregion
2255
+ //#region src/mermaid-engine.ts
2256
+ var mermaid_engine_exports = /* @__PURE__ */ __exportAll({
2257
+ compile: () => compile$4,
2258
+ layout: () => layout$1
2259
+ });
2260
+ var __dxlog_file = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/infinite-canvas-depth-7e0949/packages/common/diagram/src/mermaid-engine.ts";
2261
+ var MIN_W = 96;
2262
+ var MAX_W = 192;
2263
+ var MIN_H = 64;
2264
+ var PAD_X = 32;
2265
+ var PAD_Y = 16;
2266
+ /** Frame inset around a subgraph's members; the top also holds the frame label. */
2267
+ var FRAME_PAD$1 = 32;
2268
+ var FRAME_LABEL_H = 32;
2269
+ /** Clear space between two frames, so packages read as separate even when their nodes are adjacent. */
2270
+ var FRAME_GAP = 32;
2271
+ var GRID_FINE = 16;
2272
+ var FONT = FONT_METRICS.m;
2273
+ var LABEL_FONT = FONT_METRICS.s;
2274
+ var ELK_DIRECTION$1 = {
2275
+ TB: "DOWN",
2276
+ BT: "UP",
2277
+ LR: "RIGHT",
2278
+ RL: "LEFT"
2279
+ };
2280
+ var snapUp = (value) => Math.ceil(value / 32) * 32;
2281
+ var fineSnap = (value) => Math.round(value / GRID_FINE) * GRID_FINE;
2282
+ var LATTICES = [1.5, 2];
2283
+ var ORDERS = ["model", "free"];
2284
+ var ARRANGEMENTS = ["layered", "columns"];
2285
+ var LAYERINGS = [
2286
+ "down",
2287
+ "up",
2288
+ "free"
2289
+ ];
2290
+ var ALIGNMENTS = ["none", "edges"];
2291
+ /** The direction perpendicular to the flow, for arranging groups across it. */
2292
+ var ACROSS = {
2293
+ TB: "RIGHT",
2294
+ BT: "RIGHT",
2295
+ LR: "DOWN",
2296
+ RL: "DOWN"
2297
+ };
2298
+ /** One cell size for every node, sized to the longest label wrapped at `maxWidth`. */
2299
+ var measureCell = (graph, maxWidth) => {
2300
+ const longest = Math.max(0, ...graph.nodes.map((node) => node.label.length));
2301
+ const w = snapUp(Math.min(maxWidth, Math.max(MIN_W, longest * FONT.charW + PAD_X * 2)));
2302
+ const perLine = Math.max(1, Math.floor((w - PAD_X) / FONT.charW));
2303
+ const lines = Math.max(1, ...graph.nodes.map((node) => Math.ceil(node.label.length / perLine)));
2304
+ return {
2305
+ w,
2306
+ h: snapUp(Math.max(MIN_H, (lines - 1) * FONT.lineH + FONT.lineH / 1.35 + PAD_Y * 2))
2307
+ };
2308
+ };
2309
+ /**
2310
+ * Lattice pitch for a cell. With groups, the gutter between lattice rows must also hold two frame
2311
+ * borders, a label band, and clear space between the frames, or adjacent frames touch or overlap
2312
+ * after quantization — so that clearance is a floor on the pitch.
2313
+ */
2314
+ var pitchFor = (graph, cell, lattice) => {
2315
+ const framed = graph.groups.some((group) => group.children.length > 0);
2316
+ return {
2317
+ x: snapUp(Math.max(cell.w * lattice, framed ? cell.w + FRAME_PAD$1 * 2 + FRAME_GAP : 0)),
2318
+ y: snapUp(Math.max(cell.h * lattice, framed ? cell.h + FRAME_PAD$1 * 2 + FRAME_LABEL_H + FRAME_GAP : 0))
2319
+ };
2320
+ };
2321
+ /**
2322
+ * Quantize ELK's positions to the lattice. Each node takes the nearest lattice point; when two
2323
+ * nodes claim the same point, the later one slides along the cross axis to the nearest free slot,
2324
+ * so quantization can never stack nodes.
2325
+ */
2326
+ var quantize = (positions, pitch, horizontal) => {
2327
+ const taken = /* @__PURE__ */ new Set();
2328
+ const result = /* @__PURE__ */ new Map();
2329
+ const ordered = [...positions.entries()].sort(([, a], [, b]) => horizontal ? a.x - b.x || a.y - b.y : a.y - b.y || a.x - b.x);
2330
+ for (const [id, point] of ordered) {
2331
+ const col = Math.round(point.x / pitch.x);
2332
+ const row = Math.round(point.y / pitch.y);
2333
+ for (let step = 0;; step++) {
2334
+ const offset = step === 0 ? 0 : (step % 2 ? 1 : -1) * Math.ceil(step / 2);
2335
+ const cell = horizontal ? {
2336
+ col,
2337
+ row: row + offset
2338
+ } : {
2339
+ col: col + offset,
2340
+ row
2341
+ };
2342
+ const key = `${cell.col}:${cell.row}`;
2343
+ if (!taken.has(key)) {
2344
+ taken.add(key);
2345
+ result.set(id, {
2346
+ x: cell.col * pitch.x,
2347
+ y: cell.row * pitch.y
2348
+ });
2349
+ break;
2350
+ }
2351
+ }
2352
+ }
2353
+ return result;
2354
+ };
2355
+ /**
2356
+ * Close surplus gutters between groups along the cross axis. ELK spaces compound nodes by their
2357
+ * own padded extents, so after quantization two groups can sit a whole lattice pitch further apart
2358
+ * than the frame clearance needs while another pair sits at the minimum — the uneven gaps a reader
2359
+ * notices first. Groups are walked in cross-axis order and each, with everything beyond it, is
2360
+ * pulled back by whole pitches while its gap to the previous group stays above the clearance, so
2361
+ * every node remains on the lattice and cross-group connectors stay straight.
2362
+ */
2363
+ var compactGroups = (graph, positions, cell, pitch, horizontal) => {
2364
+ const axis = horizontal ? "y" : "x";
2365
+ const step = horizontal ? pitch.y : pitch.x;
2366
+ const size = horizontal ? cell.h : cell.w;
2367
+ const groups = graph.groups.filter((group) => group.children.some((id) => positions.has(id))).map((group) => {
2368
+ const coords = group.children.flatMap((id) => positions.has(id) ? [positions.get(id)[axis]] : []);
2369
+ return {
2370
+ ids: group.children,
2371
+ start: Math.min(...coords),
2372
+ end: Math.max(...coords)
2373
+ };
2374
+ }).sort((left, right) => left.start - right.start);
2375
+ const result = new Map(positions);
2376
+ for (let index = 1; index < groups.length; index++) {
2377
+ const previous = groups[index - 1];
2378
+ const current = groups[index];
2379
+ const gap = current.start - FRAME_PAD$1 - (previous.end + size + FRAME_PAD$1);
2380
+ let surplus = Math.floor((gap - FRAME_GAP) / step) * step;
2381
+ if (surplus <= 0) continue;
2382
+ const moving = [...result].filter(([, point]) => point[axis] >= current.start);
2383
+ const staying = new Set([...result].filter(([, point]) => point[axis] < current.start).map(([, point]) => `${point.x}:${point.y}`));
2384
+ const collides = (shift) => moving.some(([, point]) => staying.has(`${axis === "x" ? point.x - shift : point.x}:${axis === "y" ? point.y - shift : point.y}`));
2385
+ while (surplus > 0 && collides(surplus)) surplus -= step;
2386
+ if (surplus <= 0) continue;
2387
+ for (const [id, point] of moving) result.set(id, {
2388
+ ...point,
2389
+ [axis]: point[axis] - surplus
2390
+ });
2391
+ for (const later of groups.slice(index)) {
2392
+ later.start -= surplus;
2393
+ later.end -= surplus;
2394
+ }
2395
+ }
2396
+ return result;
2397
+ };
2398
+ /**
2399
+ * Edges for ELK's layering. Inheritance points at the abstraction, which ranks ABOVE its
2400
+ * subtypes — so those edges are reversed, as `relationRanks` does for class diagrams; has-many
2401
+ * and containment already flow owner-above-owned. In `columns`, the root lays out groups without
2402
+ * seeing inside them (`SEPARATE_CHILDREN`), so every edge is lifted to its endpoints' root-level
2403
+ * representatives — the group, or the node itself when ungrouped — and the groups fall into
2404
+ * dependency order; edges within one group stay where they are for that group's own layout.
2405
+ * A reference between packages always orders the packages; within one it ranks per `layering`.
2406
+ */
2407
+ var layeringEdges = (graph, arrangement, layering) => {
2408
+ const groupOf = new Map(graph.groups.flatMap((group) => group.children.map((id) => [id, group.id])));
2409
+ const betweenGroups = (edge) => groupOf.has(edge.from) && groupOf.has(edge.to) && groupOf.get(edge.from) !== groupOf.get(edge.to);
2410
+ const oriented = graph.edges.flatMap((edge) => {
2411
+ if (edge.kind === "inheritance") return [{
2412
+ from: edge.to,
2413
+ to: edge.from
2414
+ }];
2415
+ if (edge.kind !== "reference" || layering === "down" || betweenGroups(edge)) return [{
2416
+ from: edge.from,
2417
+ to: edge.to
2418
+ }];
2419
+ return layering === "up" ? [{
2420
+ from: edge.to,
2421
+ to: edge.from
2422
+ }] : [];
2423
+ });
2424
+ if (arrangement === "layered") return oriented.map((edge, index) => ({
2425
+ id: `edge-${index}`,
2426
+ sources: [edge.from],
2427
+ targets: [edge.to]
2428
+ }));
2429
+ const edges = [];
2430
+ const seen = /* @__PURE__ */ new Set();
2431
+ oriented.forEach((edge, index) => {
2432
+ const from = groupOf.get(edge.from);
2433
+ const to = groupOf.get(edge.to);
2434
+ if (from !== void 0 && from === to) {
2435
+ edges.push({
2436
+ id: `edge-${index}`,
2437
+ sources: [edge.from],
2438
+ targets: [edge.to]
2439
+ });
2440
+ return;
2441
+ }
2442
+ const lifted = {
2443
+ from: from ?? edge.from,
2444
+ to: to ?? edge.to
2445
+ };
2446
+ const key = `${lifted.from}->${lifted.to}`;
2447
+ if (lifted.from !== lifted.to && !seen.has(key)) {
2448
+ seen.add(key);
2449
+ edges.push({
2450
+ id: `group-edge-${index}`,
2451
+ sources: [lifted.from],
2452
+ targets: [lifted.to]
2453
+ });
2454
+ }
2455
+ });
2456
+ return edges;
2457
+ };
2458
+ /**
2459
+ * ELK compound layout. Groups become hierarchical nodes whose children ELK lays out inside
2460
+ * them; positions come back parent-relative and are flattened here. Node origins are then
2461
+ * quantized to the lattice and frames recomputed from their quantized members, so quantization
2462
+ * can never break containment.
2463
+ */
2464
+ var place = async (graph, cell, pitch, order, arrangement, layering) => {
2465
+ const horizontal = graph.direction === "LR" || graph.direction === "RL";
2466
+ const grouped = new Set(graph.groups.flatMap((group) => group.children));
2467
+ const leaf = (id) => ({
2468
+ id,
2469
+ width: cell.w,
2470
+ height: cell.h
2471
+ });
2472
+ const spacing = {
2473
+ "elk.layered.spacing.nodeNodeBetweenLayers": String(horizontal ? pitch.x - cell.w : pitch.y - cell.h),
2474
+ "elk.spacing.nodeNode": String(horizontal ? pitch.y - cell.h : pitch.x - cell.w)
2475
+ };
2476
+ const flow = {
2477
+ "elk.algorithm": "layered",
2478
+ "elk.direction": ELK_DIRECTION$1[graph.direction],
2479
+ "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
2480
+ "elk.layered.considerModelOrder.strategy": order === "model" ? "PREFER_NODES" : "NONE"
2481
+ };
2482
+ const children = [...graph.nodes.filter((node) => !grouped.has(node.id)).map((node) => leaf(node.id)), ...graph.groups.filter((group) => group.children.length > 0).map((group) => ({
2483
+ id: group.id,
2484
+ layoutOptions: {
2485
+ ...spacing,
2486
+ ...arrangement === "columns" ? flow : {},
2487
+ "elk.padding": `[top=${FRAME_PAD$1 + FRAME_LABEL_H},left=${FRAME_PAD$1},bottom=${FRAME_PAD$1},right=${FRAME_PAD$1}]`
2488
+ },
2489
+ children: group.children.map(leaf)
2490
+ }))];
2491
+ const result = await new ELK().layout({
2492
+ id: "root",
2493
+ layoutOptions: arrangement === "columns" ? {
2494
+ ...flow,
2495
+ "elk.direction": ACROSS[graph.direction],
2496
+ "elk.hierarchyHandling": "SEPARATE_CHILDREN",
2497
+ ...spacing
2498
+ } : {
2499
+ ...flow,
2500
+ "elk.hierarchyHandling": "INCLUDE_CHILDREN",
2501
+ ...spacing
2502
+ },
2503
+ children,
2504
+ edges: layeringEdges(graph, arrangement, layering)
2505
+ });
2506
+ const raw = /* @__PURE__ */ new Map();
2507
+ const visit = (node, offset) => {
2508
+ for (const child of node.children ?? []) {
2509
+ const x = offset.x + (child.x ?? 0);
2510
+ const y = offset.y + (child.y ?? 0);
2511
+ if (child.children?.length) visit(child, {
2512
+ x,
2513
+ y
2514
+ });
2515
+ else raw.set(child.id, {
2516
+ x,
2517
+ y
2518
+ });
2519
+ }
2520
+ };
2521
+ visit(result, {
2522
+ x: 0,
2523
+ y: 0
2524
+ });
2525
+ return compactGroups(graph, quantize(raw, pitch, horizontal), cell, pitch, horizontal);
2526
+ };
2527
+ /**
2528
+ * Align each package to its cross-package edges: among the whole-row shifts that put one of its
2529
+ * members on the row of that member's partner, take the one leaving its edges the least total row
2530
+ * offset (nearest to no shift on a tie), onto free lattice points only. Two sweeps, since a partner
2531
+ * may itself move in the first.
2532
+ */
2533
+ var alignGroups = (graph, positions, direction, alignment) => {
2534
+ if (alignment === "none") return positions;
2535
+ const axis = direction === "LR" || direction === "RL" ? "x" : "y";
2536
+ const groupOf = new Map(graph.groups.flatMap((group) => group.children.map((id) => [id, group.id])));
2537
+ const result = new Map(positions);
2538
+ for (let sweep = 0; sweep < 2; sweep++) for (const group of graph.groups) {
2539
+ const members = new Set(group.children.filter((id) => result.has(id)));
2540
+ const pairs = graph.edges.flatMap((edge) => {
2541
+ const [member, partner] = members.has(edge.from) ? [edge.from, edge.to] : [edge.to, edge.from];
2542
+ const here = result.get(member);
2543
+ const there = result.get(partner);
2544
+ return members.has(member) && !members.has(partner) && groupOf.has(partner) && here && there ? [{
2545
+ here: here[axis],
2546
+ there: there[axis]
2547
+ }] : [];
2548
+ });
2549
+ if (pairs.length === 0) continue;
2550
+ const offset = (shift) => pairs.reduce((sum, pair) => sum + Math.abs(pair.here + shift - pair.there), 0);
2551
+ const shifts = [.../* @__PURE__ */ new Set([0, ...pairs.map((pair) => pair.there - pair.here)])].sort((left, right) => offset(left) - offset(right) || Math.abs(left) - Math.abs(right));
2552
+ const others = new Set([...result].filter(([id]) => !members.has(id)).map(([, point]) => `${point.x}:${point.y}`));
2553
+ const shift = shifts.find((candidate) => ![...members].some((id) => {
2554
+ const point = result.get(id);
2555
+ return point && others.has(`${axis === "x" ? point.x + candidate : point.x}:${axis === "y" ? point.y + candidate : point.y}`);
2556
+ }));
2557
+ if (!shift) continue;
2558
+ for (const id of members) {
2559
+ const point = result.get(id);
2560
+ if (point) result.set(id, {
2561
+ ...point,
2562
+ [axis]: point[axis] + shift
2563
+ });
2564
+ }
2565
+ }
2566
+ return result;
2567
+ };
2568
+ /** Node rects at the origin plus the frame around each group's members. */
2569
+ var frame$1 = (graph, positions, cell) => {
2570
+ const minX = Math.min(0, ...[...positions.values()].map((point) => point.x));
2571
+ const minY = Math.min(0, ...[...positions.values()].map((point) => point.y));
2572
+ const nodes = new Map([...positions].map(([id, point]) => [id, {
2573
+ x: point.x - minX,
2574
+ y: point.y - minY,
2575
+ w: cell.w,
2576
+ h: cell.h
2577
+ }]));
2578
+ const frames = /* @__PURE__ */ new Map();
2579
+ for (const group of graph.groups) {
2580
+ const members = group.children.flatMap((id) => nodes.get(id) ?? []);
2581
+ if (members.length === 0) continue;
2582
+ const x = Math.min(...members.map((rect) => rect.x)) - FRAME_PAD$1;
2583
+ const y = Math.min(...members.map((rect) => rect.y)) - FRAME_PAD$1 - FRAME_LABEL_H;
2584
+ const right = Math.max(...members.map((rect) => rect.x + rect.w)) + FRAME_PAD$1;
2585
+ const bottom = Math.max(...members.map((rect) => rect.y + rect.h)) + FRAME_PAD$1;
2586
+ frames.set(group.id, {
2587
+ x,
2588
+ y,
2589
+ w: right - x,
2590
+ h: bottom - y
2591
+ });
2592
+ }
2593
+ return {
2594
+ nodes,
2595
+ frames
2596
+ };
2597
+ };
2598
+ /**
2599
+ * Straightened ports: when an edge's nodes overlap on the cross axis, both terminals take one
2600
+ * shared coordinate inside the overlap, so the connector is a single straight run instead of a
2601
+ * jog between nearly aligned nodes. Distinct edges leaving the same node side take distinct
2602
+ * coordinates. Mirrors the straightening pass in the UML emitter.
2603
+ */
2604
+ var straighten = (edges, nodes, isHorizontal) => {
2605
+ const ports = /* @__PURE__ */ new Map();
2606
+ const taken = /* @__PURE__ */ new Map();
2607
+ for (const edge of edges) {
2608
+ const from = nodes.get(edge.from);
2609
+ const to = nodes.get(edge.to);
2610
+ if (!from || !to) continue;
2611
+ const horizontal = isHorizontal(edge);
2612
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
2613
+ const alongY = horizontal ? sameLane : !sameLane;
2614
+ const lo = (alongY ? Math.max(from.x, to.x) : Math.max(from.y, to.y)) + GRID_FINE;
2615
+ const hi = (alongY ? Math.min(from.x + from.w, to.x + to.w) : Math.min(from.y + from.h, to.y + to.h)) - GRID_FINE;
2616
+ if (lo > hi) continue;
2617
+ const sides = alongY ? [`${edge.from}:${to.y >= from.y ? "bottom" : "top"}`, `${edge.to}:${to.y >= from.y ? "top" : "bottom"}`] : [`${edge.from}:${to.x >= from.x ? "right" : "left"}`, `${edge.to}:${to.x >= from.x ? "left" : "right"}`];
2618
+ const center = alongY ? (from.x + from.w / 2 + to.x + to.w / 2) / 2 : (from.y + from.h / 2 + to.y + to.h / 2) / 2;
2619
+ const base = Math.min(hi, Math.max(lo, fineSnap(center)));
2620
+ const isFree = (coord) => sides.every((side) => (taken.get(side) ?? []).every((used) => Math.abs(used - coord) >= GRID_FINE));
2621
+ let chosen;
2622
+ for (let step = 0; step <= Math.ceil((hi - lo) / GRID_FINE) && chosen === void 0; step++) for (const candidate of step === 0 ? [base] : [base - step * GRID_FINE, base + step * GRID_FINE]) if (candidate >= lo && candidate <= hi && isFree(candidate)) {
2623
+ chosen = candidate;
2624
+ break;
2625
+ }
2626
+ if (chosen === void 0) continue;
2627
+ ports.set(edge, {
2628
+ start: chosen,
2629
+ end: chosen
2630
+ });
2631
+ for (const side of sides) taken.set(side, [...taken.get(side) ?? [], chosen]);
2632
+ }
2633
+ return ports;
2634
+ };
2635
+ /**
2636
+ * Inheritance bus (TB only): subtypes of one base that sit on one row above which the base sits
2637
+ * connect through a shared horizontal bus and a single triangle-headed trunk, the UML tree
2638
+ * connector. Returns the elements and the edges it consumed; edges that do not qualify are left
2639
+ * to the per-edge router.
2640
+ */
2641
+ var inheritanceBuses = (edges, nodes) => {
2642
+ const elements = [];
2643
+ const consumed = /* @__PURE__ */ new Set();
2644
+ const byBase = /* @__PURE__ */ new Map();
2645
+ for (const edge of edges) if (edge.kind === "inheritance" && !edge.label) byBase.set(edge.to, [...byBase.get(edge.to) ?? [], edge]);
2646
+ for (const [baseId, group] of byBase) {
2647
+ const base = nodes.get(baseId);
2648
+ const subs = group.map((edge) => nodes.get(edge.from));
2649
+ if (!base || group.length < 2 || subs.some((sub) => !sub)) continue;
2650
+ const rects = subs;
2651
+ const row = rects[0].y;
2652
+ if (rects.some((sub) => sub.y !== row) || base.y + base.h >= row) continue;
2653
+ const busY = fineSnap((base.y + base.h + row) / 2);
2654
+ const trunkX = fineSnap(base.x + base.w / 2);
2655
+ const centers = rects.map((sub) => fineSnap(sub.x + sub.w / 2));
2656
+ const span = [Math.min(trunkX, ...centers), Math.max(trunkX, ...centers)];
2657
+ elements.push({
2658
+ kind: "line",
2659
+ id: `${baseId}-bus`,
2660
+ points: [{
2661
+ x: span[0],
2662
+ y: busY
2663
+ }, {
2664
+ x: span[1],
2665
+ y: busY
2666
+ }]
2667
+ });
2668
+ group.forEach((edge, index) => {
2669
+ elements.push({
2670
+ kind: "line",
2671
+ id: `${edge.from}-${baseId}-stub-${index}`,
2672
+ points: [{
2673
+ x: centers[index],
2674
+ y: row
2675
+ }, {
2676
+ x: centers[index],
2677
+ y: busY
2678
+ }]
2679
+ });
2680
+ consumed.add(edge);
2681
+ });
2682
+ elements.push({
2683
+ kind: "arrow",
2684
+ id: `${baseId}-inherit`,
2685
+ start: {
2686
+ x: trunkX,
2687
+ y: busY
2688
+ },
2689
+ end: {
2690
+ x: trunkX,
2691
+ y: base.y + base.h
2692
+ },
2693
+ head: "triangle"
2694
+ });
2695
+ }
2696
+ return {
2697
+ elements,
2698
+ consumed
2699
+ };
2700
+ };
2701
+ /**
2702
+ * Scene commands for a placement: one world object per subgraph frame (painted first) and per
2703
+ * node, plus an `edges` object of connectors.
2704
+ */
2705
+ var emit = (graph, cell, { nodes, frames }, { origin, scale, bus, arrangement, route }) => {
2706
+ const horizontal = graph.direction === "LR" || graph.direction === "RL";
2707
+ const at = (rect) => ({
2708
+ x: origin.x + rect.x * scale,
2709
+ y: origin.y + rect.y * scale
2710
+ });
2711
+ const commands = [];
2712
+ for (const group of graph.groups) {
2713
+ const frame = frames.get(group.id);
2714
+ if (!frame) continue;
2715
+ commands.push({
2716
+ op: "upsert-object",
2717
+ object: {
2718
+ id: group.id,
2719
+ origin: at(frame),
2720
+ scale,
2721
+ elements: [{
2722
+ kind: "rect",
2723
+ id: "frame",
2724
+ x: 0,
2725
+ y: 0,
2726
+ w: frame.w,
2727
+ h: frame.h,
2728
+ stroke: "dashed",
2729
+ color: "grey"
2730
+ }, ...group.label.trim() ? [{
2731
+ kind: "text",
2732
+ id: "label",
2733
+ x: FRAME_PAD$1 / 2,
2734
+ y: (FRAME_LABEL_H - LABEL_FONT.lineH) / 2,
2735
+ text: group.label,
2736
+ weight: "s",
2737
+ color: "grey"
2738
+ }] : []]
2739
+ }
2740
+ });
2741
+ }
2742
+ for (const node of graph.nodes) {
2743
+ const rect = nodes.get(node.id);
2744
+ if (!rect) continue;
2745
+ commands.push({
2746
+ op: "upsert-object",
2747
+ object: {
2748
+ id: node.id,
2749
+ origin: at(rect),
2750
+ scale,
2751
+ ...node.ref ? { ref: node.ref } : {},
2752
+ elements: [{
2753
+ kind: "rect",
2754
+ id: "box",
2755
+ x: 0,
2756
+ y: 0,
2757
+ w: cell.w,
2758
+ h: cell.h,
2759
+ text: node.label
2760
+ }]
2761
+ }
2762
+ });
2763
+ }
2764
+ if (graph.edges.length > 0) {
2765
+ const router = route ?? makeAvoidingRouter([...nodes.values()], zRouter, { step: 8 });
2766
+ const buses = bus && !horizontal ? inheritanceBuses(graph.edges, nodes) : {
2767
+ elements: [],
2768
+ consumed: /* @__PURE__ */ new Set()
2769
+ };
2770
+ const routed = graph.edges.filter((edge) => !buses.consumed.has(edge));
2771
+ const groupOf = new Map(graph.groups.flatMap((group) => group.children.map((id) => [id, group.id])));
2772
+ const rootOf = (id) => groupOf.get(id) ?? id;
2773
+ const isHorizontal = (edge) => arrangement === "columns" && rootOf(edge.from) !== rootOf(edge.to) ? !horizontal : horizontal;
2774
+ const ports = straighten(routed, nodes, isHorizontal);
2775
+ const elements = [...buses.elements];
2776
+ const terminals = /* @__PURE__ */ new Map();
2777
+ const taken = (nodeId, point, role) => (terminals.get(nodeId) ?? []).some((other) => other.role !== role && Math.abs(other.point.x - point.x) < GRID_FINE && Math.abs(other.point.y - point.y) < GRID_FINE);
2778
+ const NUDGES = [
2779
+ [0, 0],
2780
+ [1, 1],
2781
+ [-1, -1],
2782
+ [2, 2],
2783
+ [-2, -2],
2784
+ [1, 0],
2785
+ [-1, 0],
2786
+ [0, 1],
2787
+ [0, -1]
2788
+ ];
2789
+ const routeEdge = (edge, from, to) => {
2790
+ const horizontal = isHorizontal(edge);
2791
+ const sameLane = horizontal ? from.x === to.x : from.y === to.y;
2792
+ const alongY = horizontal ? sameLane : !sameLane;
2793
+ const base = ports.get(edge) ?? {};
2794
+ const center = (rect) => alongY ? rect.x + rect.w / 2 : rect.y + rect.h / 2;
2795
+ const within = (rect, value) => alongY ? Math.min(rect.x + rect.w - GRID_FINE, Math.max(rect.x + GRID_FINE, value)) : Math.min(rect.y + rect.h - GRID_FINE, Math.max(rect.y + GRID_FINE, value));
2796
+ const route = ([startNudge, endNudge]) => router({
2797
+ relation: edge,
2798
+ from,
2799
+ to,
2800
+ horizontal,
2801
+ offset: 0,
2802
+ ports: {
2803
+ start: within(from, (base.start ?? center(from)) + startNudge * GRID_FINE),
2804
+ end: within(to, (base.end ?? center(to)) + endNudge * GRID_FINE)
2805
+ }
2806
+ });
2807
+ const free = (points) => !taken(edge.from, points[0], "exit") && !taken(edge.to, points[points.length - 1], "entry");
2808
+ const direct = route(NUDGES[0]);
2809
+ if (free(direct)) return direct;
2810
+ let best;
2811
+ for (const nudge of NUDGES.slice(1)) {
2812
+ const attempt = route(nudge);
2813
+ if (free(attempt) && (!best || attempt.length < best.length)) best = attempt;
2814
+ }
2815
+ return best ?? direct;
2816
+ };
2817
+ routed.forEach((edge, index) => {
2818
+ const from = nodes.get(edge.from);
2819
+ const to = nodes.get(edge.to);
2820
+ if (!from || !to) return;
2821
+ const points = routeEdge(edge, from, to);
2822
+ terminals.set(edge.from, [...terminals.get(edge.from) ?? [], {
2823
+ point: points[0],
2824
+ role: "exit"
2825
+ }]);
2826
+ terminals.set(edge.to, [...terminals.get(edge.to) ?? [], {
2827
+ point: points[points.length - 1],
2828
+ role: "entry"
2829
+ }]);
2830
+ const id = `${edge.from}-${edge.to}-${index}`;
2831
+ if (points.length > 2) elements.push({
2832
+ kind: "line",
2833
+ id: `${id}-path`,
2834
+ points: points.slice(0, -1)
2835
+ });
2836
+ elements.push({
2837
+ kind: "arrow",
2838
+ id,
2839
+ start: points[points.length - 2],
2840
+ end: points[points.length - 1],
2841
+ ...markers(edge.kind)
2842
+ });
2843
+ if (edge.label) {
2844
+ const head = points[Math.floor(points.length / 2) - 1];
2845
+ const tail = points[Math.floor(points.length / 2)];
2846
+ elements.push({
2847
+ kind: "text",
2848
+ id: `${id}-label`,
2849
+ x: (head.x + tail.x) / 2 + 8,
2850
+ y: (head.y + tail.y) / 2 - LABEL_FONT.lineH,
2851
+ text: edge.label,
2852
+ weight: "s"
2853
+ });
2854
+ }
2855
+ });
2856
+ commands.push({
2857
+ op: "upsert-object",
2858
+ object: {
2859
+ id: "edges",
2860
+ origin,
2861
+ scale,
2862
+ elements
2863
+ }
2864
+ });
2865
+ }
2866
+ return commands;
2867
+ };
2868
+ var objectsOf = (commands) => commands.flatMap((command) => command.op === "upsert-object" ? [command.object] : []);
2869
+ /**
2870
+ * Lay out a mermaid flowchart: place once per lattice × ordering, emit with and without the
2871
+ * inheritance bus, analyze every candidate, and let the objective choose. All candidates come back
2872
+ * ranked so a bench can show what lost and why. Async because ELK is.
2873
+ */
2874
+ var layout$1 = async (source, options = {}) => {
2875
+ const graph = parse$1(source);
2876
+ const { origin = {
2877
+ x: 0,
2878
+ y: 0
2879
+ }, scale = 1, maxWidth = MAX_W, objective = DEFAULT, route } = options;
2880
+ const cell = measureCell(graph, maxWidth);
2881
+ const lattices = typeof options.lattice === "number" ? [options.lattice] : options.lattice ?? LATTICES;
2882
+ const orders = options.order ?? ORDERS;
2883
+ const buses = options.bus ?? [true, false];
2884
+ const grouped = graph.groups.filter((group) => group.children.length > 0).length;
2885
+ const arrangements = options.arrangement ?? (grouped >= 2 ? ARRANGEMENTS : ["layered"]);
2886
+ const layerings = options.layering ?? LAYERINGS;
2887
+ const alignments = options.alignment ?? ALIGNMENTS;
2888
+ invariant(lattices.length > 0 && orders.length > 0 && buses.length > 0 && arrangements.length > 0 && layerings.length > 0 && alignments.length > 0, "every candidate axis needs a value", {
2889
+ "~LogMeta": "~LogMeta",
2890
+ F: __dxlog_file,
2891
+ L: 806,
2892
+ S: void 0,
2893
+ A: ["lattices.length > 0 &&\n orders.length > 0 &&\n buses.length > 0 &&\n arrangements.length > 0 &&\n layerings.length > 0 &&\n alignments.length > 0", "'every candidate axis needs a value'"]
2894
+ });
2895
+ const candidates = [];
2896
+ const seen = /* @__PURE__ */ new Set();
2897
+ for (const lattice of lattices) for (const order of orders) for (const arrangement of arrangements) for (const layering of layerings) {
2898
+ const positions = await place(graph, cell, pitchFor(graph, cell, lattice), order, arrangement, layering);
2899
+ for (const alignment of arrangement === "columns" ? alignments : ["none"]) {
2900
+ const placement = frame$1(graph, alignGroups(graph, positions, graph.direction, alignment), cell);
2901
+ const key = [...placement.nodes].map(([id, rect]) => `${id}:${rect.x}:${rect.y}`).sort().join(" ");
2902
+ for (const bus of buses) {
2903
+ if (seen.has(`${arrangement}|${bus}|${key}`)) continue;
2904
+ seen.add(`${arrangement}|${bus}|${key}`);
2905
+ const commands = emit(graph, cell, placement, {
2906
+ origin,
2907
+ scale,
2908
+ bus,
2909
+ arrangement,
2910
+ route
2911
+ });
2912
+ const objects = objectsOf(commands);
2913
+ candidates.push({
2914
+ lattice,
2915
+ order,
2916
+ arrangement,
2917
+ layering,
2918
+ alignment,
2919
+ bus,
2920
+ commands,
2921
+ layout: {
2922
+ objects,
2923
+ report: analyze(objects)
2924
+ }
2925
+ });
2926
+ }
2927
+ }
2928
+ }
2929
+ const { chosen, ranked } = select(objective, candidates);
2930
+ return {
2931
+ commands: chosen.candidate.commands,
2932
+ chosen,
2933
+ ranked
2934
+ };
2935
+ };
2936
+ /** The chosen layout's scene commands; see {@link layout} for the candidates and verdicts. */
2937
+ var compile$4 = async (source, options = {}) => (await layout$1(source, options)).commands;
2938
+ //#endregion
2939
+ //#region src/svg-handler.ts
2940
+ /** Discriminates the base `Drawing.Canvas` as SVG-rendered scene content. */
2941
+ var SVG_SCHEMA = "dxos.org/svg/1";
2942
+ var isSvgRecord = (record) => typeof record?.object === "string" && typeof record?.element === "string" && record?.data !== void 0;
2943
+ /** Objects without an index paint first, in insertion order; indexed objects follow in index order. */
2944
+ var byIndex = (left, right) => left.index === void 0 ? right.index === void 0 ? 0 : -1 : right.index === void 0 ? 1 : left.index < right.index ? -1 : left.index > right.index ? 1 : 0;
2945
+ var SvgHandler = {
2946
+ identify: (record) => isSvgRecord(record) ? {
2947
+ object: record.object,
2948
+ element: record.element
2949
+ } : void 0,
2950
+ render: (object, placement, content) => {
2951
+ let order = Math.max(-1, ...Object.values(content).filter(isSvgRecord).map((record) => record.order)) + 1;
2952
+ const existing = Object.values(content).find((record) => isSvgRecord(record) && record.object === object.id);
2953
+ const ref = "ref" in object && object.ref || existing?.ref;
2954
+ const index = "index" in object && object.index || existing?.index;
2955
+ const records = {};
2956
+ for (const element of object.elements) records[`${object.id}/${element.id}`] = {
2957
+ object: object.id,
2958
+ element: element.id,
2959
+ order: order++,
2960
+ placement: {
2961
+ origin: { ...placement.origin },
2962
+ scale: placement.scale
2963
+ },
2964
+ ...ref ? { ref } : {},
2965
+ ...index ? { index } : {},
2966
+ data: JSON.parse(JSON.stringify(element))
2967
+ };
2968
+ return records;
2969
+ },
2970
+ read: (content) => {
2971
+ const records = Object.values(content).filter(isSvgRecord).sort((left, right) => left.order - right.order);
2972
+ const objects = /* @__PURE__ */ new Map();
2973
+ let unmanaged = 0;
2974
+ for (const record of Object.values(content)) if (!isSvgRecord(record)) unmanaged++;
2975
+ for (const record of records) {
2976
+ let object = objects.get(record.object);
2977
+ if (!object) {
2978
+ object = {
2979
+ id: record.object,
2980
+ origin: { ...record.placement.origin },
2981
+ scale: record.placement.scale,
2982
+ ...record.ref ? { ref: record.ref } : {},
2983
+ ...record.index ? { index: record.index } : {},
2984
+ elements: []
2985
+ };
2986
+ objects.set(record.object, object);
2987
+ }
2988
+ object.elements.push(record.data);
2989
+ }
2990
+ return {
2991
+ scene: { objects: [...objects.values()].sort(byIndex) },
2992
+ unmanaged
2993
+ };
2994
+ },
2995
+ translate: (record, delta) => {
2996
+ if (isSvgRecord(record)) record.placement.origin = {
2997
+ x: record.placement.origin.x + delta.x,
2998
+ y: record.placement.origin.y + delta.y
2999
+ };
3000
+ }
3001
+ };
3002
+ //#endregion
3003
+ //#region src/ui.ts
3004
+ var ui_exports = /* @__PURE__ */ __exportAll({
3005
+ compile: () => compile$3,
3006
+ deckOf: () => deckOf,
3007
+ fromSchema: () => fromSchema,
3008
+ renderAscii: () => renderAscii,
3009
+ schemaTitle: () => schemaTitle
3010
+ });
3011
+ var unwrapSuspend = (ast) => SchemaAST.isSuspend(ast) ? unwrapSuspend(ast.thunk()) : ast;
3012
+ var unwrapOptional = (ast) => {
3013
+ let node = ast;
3014
+ while (SchemaAST.isUnion(node) && node.types.length === 2 && SchemaAST.isUndefined(node.types[1])) node = node.types[0];
3015
+ return node;
3016
+ };
3017
+ var propertiesOf = (ast) => {
3018
+ const node = unwrapSuspend(ast);
3019
+ return node._tag === "Objects" ? node.propertySignatures : [];
3020
+ };
3021
+ var isLiteralUnion = (node) => SchemaAST.isUnion(node) && node.types.length > 0 && node.types.every(SchemaAST.isLiteral);
3022
+ var arrayElementOf = (node) => SchemaAST.isArrays(node) && node.elements.length === 0 && node.rest.length === 1 ? node.rest[0] : void 0;
3023
+ var titleOf = (ast) => {
3024
+ const title = ast.annotations?.title ?? SchemaAST.resolve(ast)?.title;
3025
+ return typeof title === "string" ? title : void 0;
3026
+ };
3027
+ var label = (name, type) => titleOf(type) ?? String(name);
3028
+ var controlOf = (type, name, path) => {
3029
+ return {
3030
+ kind: "control",
3031
+ control: isLiteralUnion(type) ? "select" : type._tag === "Boolean" ? "checkbox" : "input",
3032
+ label: label(name, type),
3033
+ path
3034
+ };
3035
+ };
3036
+ var childOf = (type, name, path, depth) => {
3037
+ const base = unwrapOptional(unwrapSuspend(type));
3038
+ const element = arrayElementOf(base);
3039
+ if (element && depth > 0) {
3040
+ const item = {
3041
+ ...childOf(element, "", `${path}[]`, depth - 1),
3042
+ label: ""
3043
+ };
3044
+ return {
3045
+ kind: "array",
3046
+ label: label(name, base),
3047
+ path,
3048
+ item
3049
+ };
3050
+ }
3051
+ if (depth > 0 && !isLiteralUnion(base)) {
3052
+ const properties = propertiesOf(base);
3053
+ if (properties.length > 0) return {
3054
+ kind: "group",
3055
+ label: label(name, base),
3056
+ path,
3057
+ children: properties.map((property) => childOf(property.type, property.name, `${path}.${String(property.name)}`, depth - 1))
3058
+ };
3059
+ }
3060
+ return controlOf(base, name, path);
3061
+ };
3062
+ /** Convert a schema into a form drawing: the phase-1 slice of the app model. */
3063
+ var fromSchema = (schema, options = {}) => {
3064
+ const { maxDepth = 4 } = options;
3065
+ return {
3066
+ kind: "form",
3067
+ children: propertiesOf(schema.ast).filter((property) => property.name !== "id").map((property) => childOf(property.type, property.name, String(property.name), maxDepth))
3068
+ };
3069
+ };
3070
+ /** The schema's title annotation — container chrome, not form content. */
3071
+ var schemaTitle = (schema) => titleOf(schema.ast);
3072
+ /** Wrap a form in the app-ontology containers for a full schematic. */
3073
+ var deckOf = (form, title) => ({
3074
+ kind: "deck",
3075
+ planks: [{
3076
+ kind: "plank",
3077
+ label: title,
3078
+ child: {
3079
+ kind: "panel",
3080
+ child: form
3081
+ }
3082
+ }]
3083
+ });
3084
+ var CONTROL_W = 200;
3085
+ var CONTROL_H = 28;
3086
+ var TOGGLE_W = 28;
3087
+ var LABEL_H = 18;
3088
+ var GAP$2 = 12;
3089
+ var PAD = 16;
3090
+ var TITLE_H = 22;
3091
+ var PLUS_H = 20;
3092
+ var controlHeight = (node) => (node.label ? LABEL_H : 0) + CONTROL_H;
3093
+ var heightOf = (node) => {
3094
+ switch (node.kind) {
3095
+ case "control": return controlHeight(node);
3096
+ case "group":
3097
+ if (!node.label) return node.children.reduce((sum, child, index) => sum + heightOf(child) + (index > 0 ? GAP$2 : 0), 0);
3098
+ return TITLE_H + node.children.reduce((sum, child) => sum + heightOf(child) + GAP$2, 0) + PAD;
3099
+ case "array": return TITLE_H + heightOf(node.item) + PAD;
3100
+ case "form": return node.children.reduce((sum, child, index) => sum + heightOf(child) + (index > 0 ? GAP$2 : 0), 0);
3101
+ case "panel": return heightOf(node.child) + 32 + TITLE_H;
3102
+ case "plank": return heightOf(node.child) + 32 + (node.label ? TITLE_H : 0);
3103
+ case "deck": return Math.max(...node.planks.map(heightOf), 0) + 32;
3104
+ }
3105
+ };
3106
+ var widthOf = (node) => {
3107
+ switch (node.kind) {
3108
+ case "control": return CONTROL_W;
3109
+ case "group": return Math.max(...node.children.map(widthOf), CONTROL_W) + (node.label ? PAD : 0);
3110
+ case "array": return Math.max(widthOf(node.item), CONTROL_W) + PAD;
3111
+ case "form": return Math.max(...node.children.map(widthOf), CONTROL_W);
3112
+ case "panel": return widthOf(node.child) + 32;
3113
+ case "plank": return widthOf(node.child) + 32;
3114
+ case "deck": return node.planks.reduce((sum, plank, index) => sum + widthOf(plank) + (index > 0 ? GAP$2 : 0), 0) + 32;
3115
+ }
3116
+ };
3117
+ /** Qualify a node id with its container, so two planks over the same schema stay addressable. */
3118
+ var qualify = (prefix, path) => prefix ? `${prefix}.${path}` : path;
3119
+ /** One world object per control (id = container path), so drawn controls stay individually addressable. */
3120
+ var emitControl = (node, x, y, emit, prefix) => {
3121
+ const top = node.label ? LABEL_H : 0;
3122
+ const elements = node.label ? [{
3123
+ kind: "text",
3124
+ id: "label",
3125
+ x: 0,
3126
+ y: 0,
3127
+ text: node.label,
3128
+ weight: "s",
3129
+ color: "grey"
3130
+ }] : [];
3131
+ switch (node.control) {
3132
+ case "checkbox":
3133
+ case "switch":
3134
+ elements.push({
3135
+ kind: "rect",
3136
+ id: "box",
3137
+ x: 0,
3138
+ y: top,
3139
+ w: TOGGLE_W,
3140
+ h: CONTROL_H,
3141
+ stroke: "solid"
3142
+ });
3143
+ break;
3144
+ case "select":
3145
+ elements.push({
3146
+ kind: "rect",
3147
+ id: "box",
3148
+ x: 0,
3149
+ y: top,
3150
+ w: CONTROL_W,
3151
+ h: CONTROL_H,
3152
+ stroke: "solid"
3153
+ }, {
3154
+ kind: "text",
3155
+ id: "caret",
3156
+ x: 180,
3157
+ y: top + 4,
3158
+ text: "*",
3159
+ weight: "s",
3160
+ color: "grey"
3161
+ });
3162
+ break;
3163
+ case "input": elements.push({
3164
+ kind: "rect",
3165
+ id: "box",
3166
+ x: 0,
3167
+ y: top,
3168
+ w: CONTROL_W,
3169
+ h: CONTROL_H,
3170
+ stroke: "solid"
3171
+ });
3172
+ }
3173
+ emit.commands.push({
3174
+ op: "upsert-object",
3175
+ object: {
3176
+ id: qualify(prefix, node.path),
3177
+ origin: {
3178
+ x,
3179
+ y
3180
+ },
3181
+ elements
3182
+ }
3183
+ });
3184
+ };
3185
+ var frame = (id, x, y, w, h, title, emit) => {
3186
+ const elements = [{
3187
+ kind: "rect",
3188
+ id: "frame",
3189
+ x: 0,
3190
+ y: 0,
3191
+ w,
3192
+ h,
3193
+ stroke: "dashed",
3194
+ color: "grey"
3195
+ }];
3196
+ if (title) elements.push({
3197
+ kind: "text",
3198
+ id: "title",
3199
+ x: PAD / 2,
3200
+ y: 2,
3201
+ text: title,
3202
+ weight: "s"
3203
+ });
3204
+ emit.commands.push({
3205
+ op: "upsert-object",
3206
+ object: {
3207
+ id,
3208
+ origin: {
3209
+ x,
3210
+ y
3211
+ },
3212
+ elements
3213
+ }
3214
+ });
3215
+ };
3216
+ var emitNode = (node, x, y, emit, prefix = "") => {
3217
+ switch (node.kind) {
3218
+ case "control":
3219
+ emitControl(node, x, y, emit, prefix);
3220
+ break;
3221
+ case "group": {
3222
+ const anonymous = !node.label;
3223
+ if (!anonymous) frame(qualify(prefix, node.path), x, y, widthOf(node), heightOf(node), node.label, emit);
3224
+ let cursor = y + (anonymous ? 0 : TITLE_H);
3225
+ for (const child of node.children) {
3226
+ emitNode(child, x + (anonymous ? 0 : PAD), cursor, emit, prefix);
3227
+ cursor += heightOf(child) + GAP$2;
3228
+ }
3229
+ break;
3230
+ }
3231
+ case "array": {
3232
+ const width = widthOf(node);
3233
+ const id = qualify(prefix, node.path);
3234
+ frame(id, x, y, width, heightOf(node), node.label, emit);
3235
+ emit.commands.push({
3236
+ op: "upsert-object",
3237
+ object: {
3238
+ id: `${id}.add`,
3239
+ origin: {
3240
+ x: x + width - TOGGLE_W - 4,
3241
+ y: y + 2
3242
+ },
3243
+ elements: [{
3244
+ kind: "rect",
3245
+ id: "box",
3246
+ x: 0,
3247
+ y: 0,
3248
+ w: TOGGLE_W,
3249
+ h: PLUS_H,
3250
+ text: "+",
3251
+ stroke: "solid"
3252
+ }]
3253
+ }
3254
+ });
3255
+ emitNode(node.item, x + PAD, y + TITLE_H, emit, prefix);
3256
+ break;
3257
+ }
3258
+ case "form": {
3259
+ let cursor = y;
3260
+ for (const child of node.children) {
3261
+ emitNode(child, x, cursor, emit, prefix);
3262
+ cursor += heightOf(child) + GAP$2;
3263
+ }
3264
+ break;
3265
+ }
3266
+ case "panel": {
3267
+ const id = qualify(prefix, "panel");
3268
+ frame(id, x, y, widthOf(node), heightOf(node), node.label ?? "panel", emit);
3269
+ emitNode(node.child, x + PAD, y + TITLE_H + PAD, emit, id);
3270
+ break;
3271
+ }
3272
+ case "plank": {
3273
+ const id = prefix || "plank";
3274
+ frame(id, x, y, widthOf(node), heightOf(node), node.label, emit);
3275
+ emitNode(node.child, x + PAD, y + PAD + (node.label ? TITLE_H : 0), emit, id);
3276
+ break;
3277
+ }
3278
+ case "deck": {
3279
+ const id = qualify(prefix, "deck");
3280
+ frame(id, x, y, widthOf(node), heightOf(node), void 0, emit);
3281
+ let cursor = x + PAD;
3282
+ node.planks.forEach((plank, index) => {
3283
+ emitNode(plank, cursor, y + PAD, emit, `${id}.plank${index}`);
3284
+ cursor += widthOf(plank) + GAP$2;
3285
+ });
3286
+ break;
3287
+ }
3288
+ }
3289
+ };
3290
+ /** Compile a UI drawing into scene commands (frames first, controls above them). */
3291
+ var compile$3 = (node, options = {}) => {
3292
+ const { origin = {
3293
+ x: 0,
3294
+ y: 0
3295
+ }, scale = 1 } = options;
3296
+ const emit = {
3297
+ commands: [],
3298
+ elements: []
3299
+ };
3300
+ emitNode(node, 0, 0, emit);
3301
+ return emit.commands.map((command) => command.op === "upsert-object" ? {
3302
+ ...command,
3303
+ object: {
3304
+ ...command.object,
3305
+ scale,
3306
+ origin: {
3307
+ x: origin.x + (command.object.origin?.x ?? 0) * scale,
3308
+ y: origin.y + (command.object.origin?.y ?? 0) * scale
3309
+ }
3310
+ }
3311
+ } : command);
3312
+ };
3313
+ var GLYPHS = {
3314
+ input: "[__________]",
3315
+ select: "[_________*]",
3316
+ checkbox: "[ ]",
3317
+ switch: "(o )"
3318
+ };
3319
+ var indent = (lines, prefix) => lines.map((line) => (prefix + line).trimEnd());
3320
+ /** Wrap lines in a +--- box, optionally titled. */
3321
+ var boxed = (lines, title) => {
3322
+ const width = Math.max(...lines.map((line) => line.length), (title?.length ?? 0) + 4, 12);
3323
+ return [
3324
+ title ? `+- ${title} ${"-".repeat(Math.max(width - title.length - 1, 1))}+` : `+${"-".repeat(width + 2)}+`,
3325
+ ...lines.map((line) => `| ${line.padEnd(width)} |`),
3326
+ `+${"-".repeat(width + 2)}+`
3327
+ ];
3328
+ };
3329
+ var asciiLines = (node) => {
3330
+ switch (node.kind) {
3331
+ case "control": return node.label ? [node.label, GLYPHS[node.control]] : [GLYPHS[node.control]];
3332
+ case "group": {
3333
+ const children = node.children.flatMap((child, index) => index > 0 ? ["", ...asciiLines(child)] : asciiLines(child));
3334
+ return node.label ? [node.label, ...indent(children, " ")] : children;
3335
+ }
3336
+ case "array": return [`${node.label} [+]`, ...indent(asciiLines(node.item), " ")];
3337
+ case "form": return node.children.flatMap((child, index) => index > 0 ? ["", ...asciiLines(child)] : asciiLines(child));
3338
+ case "panel": return boxed(asciiLines(node.child), node.label);
3339
+ case "plank": return boxed(asciiLines(node.child), node.label);
3340
+ case "deck": return node.planks.flatMap((plank, index) => index > 0 ? ["", ...asciiLines(plank)] : asciiLines(plank));
3341
+ }
3342
+ };
3343
+ /** Render a UI drawing as ASCII art. */
3344
+ var renderAscii = (node) => asciiLines(node).join("\n");
3345
+ //#endregion
3346
+ //#region src/uml-engine.ts
3347
+ var uml_engine_exports = /* @__PURE__ */ __exportAll({ compile: () => compile$2 });
3348
+ var GAP_MAIN = 128;
3349
+ var GAP_CROSS = 128;
3350
+ var snap$2 = (value) => Math.round(value / 32) * 32;
3351
+ var DAGRE_DIRECTION = {
3352
+ TB: "TB",
3353
+ BT: "BT",
3354
+ LR: "LR",
3355
+ RL: "RL"
3356
+ };
3357
+ var ELK_DIRECTION = {
3358
+ TB: "DOWN",
3359
+ BT: "UP",
3360
+ LR: "RIGHT",
3361
+ RL: "LEFT"
3362
+ };
3363
+ /**
3364
+ * Edges oriented for layering: inheritance, realization, and dependency point at abstractions,
3365
+ * which rank ABOVE their sources — so those edges are reversed for the engine (which lays out
3366
+ * source-above-target), matching `relationRanks`.
3367
+ */
3368
+ var orientedEdges = (model) => model.relations.map((relation) => relation.kind === "inheritance" || relation.kind === "realization" || relation.kind === "dependency" ? {
3369
+ from: relation.to,
3370
+ to: relation.from
3371
+ } : {
3372
+ from: relation.from,
3373
+ to: relation.to
3374
+ });
3375
+ /** Snap engine output to the document grid and normalize the top-left to (0, 0). */
3376
+ var snapRects = (rects) => {
3377
+ const snapped = new Map([...rects].map(([id, rect]) => [id, {
3378
+ ...rect,
3379
+ x: snap$2(rect.x),
3380
+ y: snap$2(rect.y)
3381
+ }]));
3382
+ const minX = Math.min(...[...snapped.values()].map((rect) => rect.x), 0);
3383
+ const minY = Math.min(...[...snapped.values()].map((rect) => rect.y), 0);
3384
+ return new Map([...snapped].map(([id, rect]) => [id, {
3385
+ ...rect,
3386
+ x: rect.x - minX,
3387
+ y: rect.y - minY
3388
+ }]));
3389
+ };
3390
+ /** dagre: network-simplex layering + median-sweep crossing minimization. Sync. */
3391
+ var dagrePlace = (model, { cell, gapMain, gapCross, direction }) => {
3392
+ const graph = new graphlib.Graph();
3393
+ graph.setGraph({
3394
+ rankdir: DAGRE_DIRECTION[direction],
3395
+ ranker: "network-simplex",
3396
+ ranksep: gapMain,
3397
+ nodesep: gapCross,
3398
+ edgesep: gapCross / 2
3399
+ });
3400
+ graph.setDefaultEdgeLabel(() => ({}));
3401
+ for (const entry of model.classes) graph.setNode(entry.id, {
3402
+ width: cell.w,
3403
+ height: cell.h
3404
+ });
3405
+ for (const edge of orientedEdges(model)) graph.setEdge(edge.from, edge.to);
3406
+ layout(graph);
3407
+ return snapRects(new Map(model.classes.map((entry) => {
3408
+ const node = graph.node(entry.id);
3409
+ return [entry.id, {
3410
+ x: node.x - cell.w / 2,
3411
+ y: node.y - cell.h / 2,
3412
+ w: cell.w,
3413
+ h: cell.h
3414
+ }];
3415
+ })));
3416
+ };
3417
+ /** ELK layered: layer-sweep crossing minimization + Brandes-Köpf placement. Async. */
3418
+ var elkPlace = async (model, { cell, gapMain, gapCross, direction }) => {
3419
+ const result = await new ELK().layout({
3420
+ id: "root",
3421
+ layoutOptions: {
3422
+ "elk.algorithm": "layered",
3423
+ "elk.direction": ELK_DIRECTION[direction],
3424
+ "elk.layered.spacing.nodeNodeBetweenLayers": String(gapMain),
3425
+ "elk.spacing.nodeNode": String(gapCross),
3426
+ "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
3427
+ "elk.layered.considerModelOrder.strategy": "PREFER_NODES"
3428
+ },
3429
+ children: model.classes.map((entry) => ({
3430
+ id: entry.id,
3431
+ width: cell.w,
3432
+ height: cell.h
3433
+ })),
3434
+ edges: orientedEdges(model).map((edge, index) => ({
3435
+ id: `edge-${index}`,
3436
+ sources: [edge.from],
3437
+ targets: [edge.to]
3438
+ }))
3439
+ });
3440
+ return snapRects(new Map((result.children ?? []).map((child) => [child.id, {
3441
+ x: child.x ?? 0,
3442
+ y: child.y ?? 0,
3443
+ w: cell.w,
3444
+ h: cell.h
3445
+ }])));
3446
+ };
3447
+ /**
3448
+ * Compile a mermaid class diagram with engine-backed placement; identical output structure to
3449
+ * `UmlGrid.compile` (equal-size grid cells, orthogonal Z-routed connectors), but node positions
3450
+ * come from dagre/ELK. Async because ELK is.
3451
+ */
3452
+ var compile$2 = async (source, options = {}) => {
3453
+ const { origin, scale, maxWidth = 192, engine = "elk" } = options;
3454
+ const model = parse(source);
3455
+ const cell = measureCell$1(model, {
3456
+ maxWidth,
3457
+ cell: options.cell,
3458
+ titleHeight: options.titleHeight
3459
+ });
3460
+ const place = {
3461
+ cell,
3462
+ gapMain: snap$2(options.gapMain ?? GAP_MAIN),
3463
+ gapCross: snap$2(options.gapCross ?? GAP_CROSS),
3464
+ direction: model.direction
3465
+ };
3466
+ return emit$1({
3467
+ model,
3468
+ cell,
3469
+ rects: engine === "dagre" ? dagrePlace(model, place) : await elkPlace(model, place),
3470
+ ranks: relationRanks(model)
3471
+ }, {
3472
+ origin,
3473
+ scale
3474
+ });
3475
+ };
3476
+ //#endregion
3477
+ //#region src/uml-rules.ts
3478
+ var uml_rules_exports = /* @__PURE__ */ __exportAll({
3479
+ buildGroups: () => buildGroups,
3480
+ compile: () => compile$1,
3481
+ frameCommands: () => frameCommands,
3482
+ inheritanceTreeRule: () => inheritanceTreeRule,
3483
+ linearChainRule: () => linearChainRule,
3484
+ packGroups: () => packGroups,
3485
+ resolveRects: () => resolveRects
3486
+ });
3487
+ var GAP$1 = 64;
3488
+ var GROUP_GAP = 96;
3489
+ var FRAME_PAD = 32;
3490
+ var snap$1 = (value) => Math.round(value / 32) * 32;
3491
+ /** Relations oriented so the target should sit ABOVE the source (arrows read upward). */
3492
+ var upEdges = (model) => model.relations.map((relation) => relation.kind === "inheritance" || relation.kind === "realization" || relation.kind === "dependency" ? {
3493
+ from: relation.from,
3494
+ to: relation.to
3495
+ } : {
3496
+ from: relation.to,
3497
+ to: relation.from
3498
+ });
3499
+ /**
3500
+ * Inheritance trees: each supertype with claimable subtypes forms a group — the hierarchy stacks
3501
+ * vertically (root on top, arrows up) and peers share a horizontal axis, packed as a tidy tree
3502
+ * (each parent centered over its subtree).
3503
+ */
3504
+ var inheritanceTreeRule = {
3505
+ id: "inheritance",
3506
+ apply: (model, unclaimed, cell) => {
3507
+ const children = /* @__PURE__ */ new Map();
3508
+ const hasParent = /* @__PURE__ */ new Set();
3509
+ for (const relation of model.relations) if ((relation.kind === "inheritance" || relation.kind === "realization") && unclaimed.has(relation.from) && unclaimed.has(relation.to)) {
3510
+ children.set(relation.to, [...children.get(relation.to) ?? [], relation.from]);
3511
+ hasParent.add(relation.from);
3512
+ }
3513
+ const groups = [];
3514
+ for (const [root] of children) {
3515
+ if (hasParent.has(root)) continue;
3516
+ const rects = /* @__PURE__ */ new Map();
3517
+ const claimable = (id) => unclaimed.has(id) && !rects.has(id);
3518
+ const widths = /* @__PURE__ */ new Map();
3519
+ const layoutSubtree = (id, x, depth) => {
3520
+ rects.set(id, {
3521
+ x: 0,
3522
+ y: 0,
3523
+ w: cell.w,
3524
+ h: cell.h
3525
+ });
3526
+ const kids = (children.get(id) ?? []).filter(claimable);
3527
+ const kidWidths = kids.map((kid) => subtreeWidth(kid));
3528
+ const rowWidth = kidWidths.reduce((sum, w) => sum + w, 0) + Math.max(0, kids.length - 1) * GAP$1;
3529
+ const width = Math.max(cell.w, rowWidth);
3530
+ let childX = x + (width - rowWidth) / 2;
3531
+ for (const [index, kid] of kids.entries()) {
3532
+ layoutSubtree(kid, childX, depth + 1);
3533
+ childX += kidWidths[index] + GAP$1;
3534
+ }
3535
+ rects.set(id, {
3536
+ x: x + (width - cell.w) / 2,
3537
+ y: depth * (cell.h + GAP$1),
3538
+ w: cell.w,
3539
+ h: cell.h
3540
+ });
3541
+ return width;
3542
+ };
3543
+ const subtreeWidth = (id, seen = /* @__PURE__ */ new Set()) => {
3544
+ const cached = widths.get(id);
3545
+ if (cached !== void 0) return cached;
3546
+ if (seen.has(id)) return cell.w;
3547
+ seen.add(id);
3548
+ const kids = (children.get(id) ?? []).filter((kid) => unclaimed.has(kid) && !rects.has(kid) && !seen.has(kid));
3549
+ const width = kids.length === 0 ? cell.w : Math.max(cell.w, kids.reduce((sum, kid) => sum + subtreeWidth(kid, seen), 0) + (kids.length - 1) * GAP$1);
3550
+ widths.set(id, width);
3551
+ return width;
3552
+ };
3553
+ const width = layoutSubtree(root, 0, 0);
3554
+ const depth = Math.max(...[...rects.values()].map((rect) => rect.y)) + cell.h;
3555
+ if (rects.size < 2) continue;
3556
+ for (const id of rects.keys()) unclaimed.delete(id);
3557
+ groups.push({
3558
+ id: `tree:${root}`,
3559
+ rule: "inheritance",
3560
+ rects,
3561
+ w: width,
3562
+ h: depth
3563
+ });
3564
+ }
3565
+ return groups;
3566
+ }
3567
+ };
3568
+ /**
3569
+ * Longest linear chain: the longest path through the still-unclaimed up-oriented relation graph
3570
+ * renders left to right (each target directly right of its source).
3571
+ */
3572
+ var linearChainRule = {
3573
+ id: "chain",
3574
+ apply: (model, unclaimed, cell) => {
3575
+ const edges = upEdges(model).filter((edge) => unclaimed.has(edge.from) && unclaimed.has(edge.to));
3576
+ const back = backEdges([...unclaimed], edges);
3577
+ const forward = edges.filter((edge) => !back.has(edge));
3578
+ const next = /* @__PURE__ */ new Map();
3579
+ for (const edge of forward) next.set(edge.from, [...next.get(edge.from) ?? [], edge.to]);
3580
+ const memo = /* @__PURE__ */ new Map();
3581
+ const longestFrom = (id) => {
3582
+ const cached = memo.get(id);
3583
+ if (cached) return cached;
3584
+ let best = [];
3585
+ for (const target of next.get(id) ?? []) {
3586
+ const path = longestFrom(target);
3587
+ if (path.length > best.length) best = path;
3588
+ }
3589
+ const result = [id, ...best];
3590
+ memo.set(id, result);
3591
+ return result;
3592
+ };
3593
+ let chain = [];
3594
+ for (const id of unclaimed) {
3595
+ const path = longestFrom(id);
3596
+ if (path.length > chain.length) chain = path;
3597
+ }
3598
+ if (chain.length < 3) return [];
3599
+ const rects = /* @__PURE__ */ new Map();
3600
+ chain.forEach((id, index) => {
3601
+ rects.set(id, {
3602
+ x: index * (cell.w + GAP$1),
3603
+ y: 0,
3604
+ w: cell.w,
3605
+ h: cell.h
3606
+ });
3607
+ unclaimed.delete(id);
3608
+ });
3609
+ return [{
3610
+ id: `chain:${chain[0]}`,
3611
+ rule: "chain",
3612
+ rects,
3613
+ w: chain.length * cell.w + (chain.length - 1) * GAP$1,
3614
+ h: cell.h
3615
+ }];
3616
+ }
3617
+ };
3618
+ /** Run the rules over the model; leftovers become singleton groups. */
3619
+ var buildGroups = (model, cell, rules) => {
3620
+ const unclaimed = new Set(model.classes.map((entry) => entry.id));
3621
+ const groups = [];
3622
+ for (const rule of rules) groups.push(...rule.apply(model, unclaimed, cell));
3623
+ for (const id of unclaimed) groups.push({
3624
+ id: `node:${id}`,
3625
+ rects: /* @__PURE__ */ new Map([[id, {
3626
+ x: 0,
3627
+ y: 0,
3628
+ w: cell.w,
3629
+ h: cell.h
3630
+ }]]),
3631
+ w: cell.w,
3632
+ h: cell.h
3633
+ });
3634
+ return groups;
3635
+ };
3636
+ /**
3637
+ * Pack groups as super-nodes: rank by up-oriented cross-group edges (target group above), then
3638
+ * lay ranks out as rows, bottom rank last — mirroring the node-level convention.
3639
+ */
3640
+ var packGroups = (model, groups) => {
3641
+ const groupOf = /* @__PURE__ */ new Map();
3642
+ for (const group of groups) for (const id of group.rects.keys()) groupOf.set(id, group);
3643
+ const crossEdges = upEdges(model).map((edge) => ({
3644
+ from: groupOf.get(edge.from).id,
3645
+ to: groupOf.get(edge.to).id
3646
+ })).filter((edge) => edge.from !== edge.to);
3647
+ const groupRanks = rank$1(groups.map((group) => group.id), crossEdges.map((edge) => ({
3648
+ from: edge.to,
3649
+ to: edge.from
3650
+ })));
3651
+ const lanes = /* @__PURE__ */ new Map();
3652
+ for (const group of groups) {
3653
+ const rank = groupRanks.get(group.id) ?? 0;
3654
+ lanes.set(rank, [...lanes.get(rank) ?? [], group]);
3655
+ }
3656
+ const origins = /* @__PURE__ */ new Map();
3657
+ let y = 0;
3658
+ for (const rank of [...lanes.keys()].sort((left, right) => left - right)) {
3659
+ const members = lanes.get(rank);
3660
+ let x = 0;
3661
+ for (const group of members) {
3662
+ origins.set(group.id, {
3663
+ x: snap$1(x),
3664
+ y: snap$1(y)
3665
+ });
3666
+ x += group.w + GROUP_GAP;
3667
+ }
3668
+ y += Math.max(...members.map((group) => group.h)) + GROUP_GAP;
3669
+ }
3670
+ return origins;
3671
+ };
3672
+ /** Absolute node rects for a (possibly partial) group placement. */
3673
+ var resolveRects = (groups, origins) => {
3674
+ const rects = /* @__PURE__ */ new Map();
3675
+ for (const group of groups) {
3676
+ const at = origins.get(group.id);
3677
+ if (!at) continue;
3678
+ for (const [id, rect] of group.rects) rects.set(id, {
3679
+ ...rect,
3680
+ x: at.x + rect.x,
3681
+ y: at.y + rect.y
3682
+ });
3683
+ }
3684
+ return rects;
3685
+ };
3686
+ /** Dashed frames behind multi-node groups. */
3687
+ var frameCommands = (groups, origins, { origin = {
3688
+ x: 0,
3689
+ y: 0
3690
+ }, scale = 1 } = {}) => groups.filter((group) => group.rects.size > 1 && origins.has(group.id)).map((group) => {
3691
+ const at = origins.get(group.id);
3692
+ return {
3693
+ op: "upsert-object",
3694
+ object: {
3695
+ id: `group:${group.id}`,
3696
+ origin: {
3697
+ x: origin.x + (at.x - FRAME_PAD) * scale,
3698
+ y: origin.y + (at.y - FRAME_PAD) * scale
3699
+ },
3700
+ scale,
3701
+ elements: [{
3702
+ kind: "rect",
3703
+ id: "frame",
3704
+ x: 0,
3705
+ y: 0,
3706
+ w: group.w + FRAME_PAD * 2,
3707
+ h: group.h + FRAME_PAD * 2,
3708
+ stroke: "dashed",
3709
+ color: "grey"
3710
+ }]
3711
+ }
3712
+ };
3713
+ });
3714
+ /**
3715
+ * Compile with rule-based grouping: rules claim groups, leftovers become singletons, groups pack
3716
+ * as super-nodes ranked by their cross-group edges (lanes advance vertically; no borders overlap
3717
+ * by construction), and multi-node groups render a dashed frame behind their members.
3718
+ */
3719
+ var compile$1 = (source, options = {}) => {
3720
+ const { origin = {
3721
+ x: 0,
3722
+ y: 0
3723
+ }, scale = 1, maxWidth = 192 } = options;
3724
+ const rules = options.rules ?? [inheritanceTreeRule, linearChainRule];
3725
+ const model = parse(source);
3726
+ const cell = measureCell$1(model, {
3727
+ maxWidth,
3728
+ cell: options.cell,
3729
+ titleHeight: options.titleHeight
3730
+ });
3731
+ const groups = buildGroups(model, cell, rules);
3732
+ const origins = packGroups(model, groups);
3733
+ const rects = resolveRects(groups, origins);
3734
+ return [...frameCommands(groups, origins, {
3735
+ origin,
3736
+ scale
3737
+ }), ...emit$1({
3738
+ model,
3739
+ cell,
3740
+ rects,
3741
+ ranks: relationRanks(model)
3742
+ }, {
3743
+ origin,
3744
+ scale
3745
+ })];
3746
+ };
3747
+ //#endregion
3748
+ //#region src/uml-search.ts
3749
+ var uml_search_exports = /* @__PURE__ */ __exportAll({
3750
+ compile: () => compile,
3751
+ scoreLayout: () => scoreLayout,
3752
+ searchPlacement: () => searchPlacement
3753
+ });
3754
+ var GAP = 96;
3755
+ var snap = (value) => Math.round(value / 32) * 32;
3756
+ var segmentsOf = (points) => points.slice(0, -1).map((point, index) => [point, points[index + 1]]);
3757
+ /** Orthogonal segment crossing: proper intersections plus collinear overlaps. */
3758
+ var crosses = ([a1, a2], [b1, b2]) => {
3759
+ const aVertical = a1.x === a2.x;
3760
+ if (aVertical !== (b1.x === b2.x)) {
3761
+ const [v1, v2] = aVertical ? [a1, a2] : [b1, b2];
3762
+ const [h1, h2] = aVertical ? [b1, b2] : [a1, a2];
3763
+ return v1.x > Math.min(h1.x, h2.x) && v1.x < Math.max(h1.x, h2.x) && h1.y > Math.min(v1.y, v2.y) && h1.y < Math.max(v1.y, v2.y);
3764
+ }
3765
+ if (aVertical) return a1.x === b1.x && Math.min(a1.y, a2.y) < Math.max(b1.y, b2.y) && Math.min(b1.y, b2.y) < Math.max(a1.y, a2.y);
3766
+ return a1.y === b1.y && Math.min(a1.x, a2.x) < Math.max(b1.x, b2.x) && Math.min(b1.x, b2.x) < Math.max(a1.x, a2.x);
3767
+ };
3768
+ /** Direction changes along a polyline, ignoring zero-length segments. */
3769
+ var bends = (route) => {
3770
+ const points = route.filter((point, index) => index === 0 || point.x !== route[index - 1].x || point.y !== route[index - 1].y);
3771
+ let count = 0;
3772
+ for (let index = 2; index < points.length; index++) {
3773
+ const [a, b, c] = [
3774
+ points[index - 2],
3775
+ points[index - 1],
3776
+ points[index]
3777
+ ];
3778
+ if (!(a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y)) count++;
3779
+ }
3780
+ return count;
3781
+ };
3782
+ /**
3783
+ * Layout score over the placed subset: +1 per straight or single-bend (L) route, −1 per crossing
3784
+ * pair. Routes come from the cheap Z-router — the score ranks candidates, the A* router draws
3785
+ * the final picture.
3786
+ */
3787
+ var scoreLayout = (model, rects) => {
3788
+ const horizontal = model.direction === "LR" || model.direction === "RL";
3789
+ const routes = [];
3790
+ for (const relation of model.relations) {
3791
+ const from = rects.get(relation.from);
3792
+ const to = rects.get(relation.to);
3793
+ if (!from || !to) continue;
3794
+ routes.push(zRouter({
3795
+ relation,
3796
+ from,
3797
+ to,
3798
+ horizontal,
3799
+ offset: 0
3800
+ }));
3801
+ }
3802
+ let score = 0;
3803
+ for (const route of routes) if (bends(route) <= 1) score += 1;
3804
+ for (let a = 0; a < routes.length; a++) for (let b = a + 1; b < routes.length; b++) if (segmentsOf(routes[a]).some((left) => segmentsOf(routes[b]).some((right) => crosses(left, right)))) score -= 1;
3805
+ return score;
3806
+ };
3807
+ var overlaps = (a, b) => a.x < b.x + b.w + GAP && b.x < a.x + a.w + GAP && a.y < b.y + b.h + GAP && b.y < a.y + a.h + GAP;
3808
+ /**
3809
+ * Greedy tree search: place the chain (or largest) group first, then repeatedly place the
3810
+ * unplaced group connected to the earliest-placed node, trying axis-aligned candidates around
3811
+ * the anchor (below and above preferred, then right/left, sliding off collisions) and keeping
3812
+ * the best-scoring one.
3813
+ */
3814
+ var searchPlacement = (model, groups) => {
3815
+ const groupOf = /* @__PURE__ */ new Map();
3816
+ for (const group of groups) for (const id of group.rects.keys()) groupOf.set(id, group);
3817
+ const seed = groups.find((group) => group.rule === "chain") ?? [...groups].sort((left, right) => right.rects.size - left.rects.size)[0];
3818
+ if (!seed) return /* @__PURE__ */ new Map();
3819
+ const origins = /* @__PURE__ */ new Map();
3820
+ origins.set(seed.id, {
3821
+ x: 0,
3822
+ y: 0
3823
+ });
3824
+ const placedNodes = [...seed.rects.keys()];
3825
+ const absolute = (id) => {
3826
+ const group = groupOf.get(id);
3827
+ const at = origins.get(group.id);
3828
+ if (!at) return;
3829
+ const local = group.rects.get(id);
3830
+ return {
3831
+ ...local,
3832
+ x: at.x + local.x,
3833
+ y: at.y + local.y
3834
+ };
3835
+ };
3836
+ const placedBounds = () => groups.flatMap((group) => {
3837
+ const at = origins.get(group.id);
3838
+ return at ? [{
3839
+ x: at.x,
3840
+ y: at.y,
3841
+ w: group.w,
3842
+ h: group.h
3843
+ }] : [];
3844
+ });
3845
+ while (origins.size < groups.length) {
3846
+ let anchorId;
3847
+ let connection;
3848
+ for (const placed of placedNodes) {
3849
+ for (const relation of model.relations) {
3850
+ const other = relation.from === placed ? relation.to : relation.to === placed ? relation.from : void 0;
3851
+ if (other && !origins.has(groupOf.get(other).id)) {
3852
+ anchorId = placed;
3853
+ connection = {
3854
+ group: groupOf.get(other),
3855
+ node: other
3856
+ };
3857
+ break;
3858
+ }
3859
+ }
3860
+ if (connection) break;
3861
+ }
3862
+ if (!connection || !anchorId) {
3863
+ const group = groups.find((candidate) => !origins.has(candidate.id));
3864
+ const rightmost = Math.max(...placedBounds().map((rect) => rect.x + rect.w), 0);
3865
+ origins.set(group.id, {
3866
+ x: snap(rightmost + GAP),
3867
+ y: 0
3868
+ });
3869
+ placedNodes.push(...group.rects.keys());
3870
+ continue;
3871
+ }
3872
+ const anchor = absolute(anchorId);
3873
+ const local = connection.group.rects.get(connection.node);
3874
+ const anchorCenterX = anchor.x + anchor.w / 2;
3875
+ const anchorCenterY = anchor.y + anchor.h / 2;
3876
+ const candidates = [
3877
+ {
3878
+ x: anchorCenterX - local.x - local.w / 2,
3879
+ y: anchor.y + anchor.h + GAP
3880
+ },
3881
+ {
3882
+ x: anchorCenterX - local.x - local.w / 2,
3883
+ y: anchor.y - GAP - connection.group.h
3884
+ },
3885
+ {
3886
+ x: anchor.x + anchor.w + GAP,
3887
+ y: anchorCenterY - local.y - local.h / 2
3888
+ },
3889
+ {
3890
+ x: anchor.x - GAP - connection.group.w,
3891
+ y: anchorCenterY - local.y - local.h / 2
3892
+ }
3893
+ ];
3894
+ let best;
3895
+ for (const [candidateIndex, candidate] of candidates.entries()) {
3896
+ const at = {
3897
+ x: snap(candidate.x),
3898
+ y: snap(candidate.y)
3899
+ };
3900
+ const bounds = placedBounds();
3901
+ let tries = 0;
3902
+ const vertical = candidateIndex < 2;
3903
+ while (bounds.some((rect) => overlaps(rect, {
3904
+ x: at.x,
3905
+ y: at.y,
3906
+ w: connection.group.w,
3907
+ h: connection.group.h
3908
+ })) && tries < 24) {
3909
+ if (vertical) at.x += 64;
3910
+ else at.y += 64;
3911
+ tries++;
3912
+ }
3913
+ if (tries >= 24) continue;
3914
+ origins.set(connection.group.id, at);
3915
+ const score = scoreLayout(model, resolveRects(groups, originsSubset(groups, origins)));
3916
+ origins.delete(connection.group.id);
3917
+ if (!best || score > best.score) best = {
3918
+ at,
3919
+ score
3920
+ };
3921
+ }
3922
+ const at = best?.at ?? {
3923
+ x: snap(Math.max(...placedBounds().map((rect) => rect.x + rect.w)) + GAP),
3924
+ y: 0
3925
+ };
3926
+ origins.set(connection.group.id, at);
3927
+ placedNodes.push(...connection.group.rects.keys());
3928
+ }
3929
+ const minX = Math.min(...[...origins.values()].map((point) => point.x));
3930
+ const minY = Math.min(...[...origins.values()].map((point) => point.y));
3931
+ return new Map([...origins].map(([id, point]) => [id, {
3932
+ x: point.x - minX,
3933
+ y: point.y - minY
3934
+ }]));
3935
+ };
3936
+ /** Placement restricted to placed groups, so partial layouts score without phantom rects. */
3937
+ var originsSubset = (groups, origins) => new Map(groups.filter((group) => origins.has(group.id)).map((group) => [group.id, origins.get(group.id)]));
3938
+ /**
3939
+ * Compile via the scored search, falling back to whichever of {search, rank packing} scores
3940
+ * higher — the search is greedy, so the packing occasionally beats it.
3941
+ */
3942
+ var compile = (source, options = {}) => {
3943
+ const { origin = {
3944
+ x: 0,
3945
+ y: 0
3946
+ }, scale = 1, maxWidth = 192 } = options;
3947
+ const rules = options.rules ?? [inheritanceTreeRule, linearChainRule];
3948
+ const model = parse(source);
3949
+ const cell = measureCell$1(model, {
3950
+ maxWidth,
3951
+ cell: options.cell,
3952
+ titleHeight: options.titleHeight
3953
+ });
3954
+ const groups = buildGroups(model, cell, rules);
3955
+ const searched = searchPlacement(model, groups);
3956
+ const packed = packGroups(model, groups);
3957
+ const origins = scoreLayout(model, resolveRects(groups, searched)) >= scoreLayout(model, resolveRects(groups, packed)) ? searched : packed;
3958
+ const rects = resolveRects(groups, origins);
3959
+ return [...frameCommands(groups, origins, {
3960
+ origin,
3961
+ scale
3962
+ }), ...emit$1({
3963
+ model,
3964
+ cell,
3965
+ rects,
3966
+ ranks: relationRanks(model)
3967
+ }, {
3968
+ origin,
3969
+ scale
3970
+ })];
3971
+ };
3972
+ //#endregion
3973
+ export { diagnostics_exports as Diagnostics, DialectRegistry, layout_exports as Layout, mermaid_exports as Mermaid, mermaid_engine_exports as MermaidEngine, objective_exports as Objective, SVG_SCHEMA, scene_exports as Scene, SvgHandler, ui_exports as Ui, uml_exports as Uml, uml_engine_exports as UmlEngine, uml_grid_exports as UmlGrid, uml_rules_exports as UmlRules, uml_search_exports as UmlSearch, applyCommands, nextIndex, sceneDialect };
3974
+
3975
+ //# sourceMappingURL=index.mjs.map