@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.
- package/LICENSE +105 -0
- package/README.md +10 -0
- package/dist/lib/index.mjs +3975 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +68 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/content.d.ts +60 -0
- package/dist/types/src/content.d.ts.map +1 -0
- package/dist/types/src/diagnostics.d.ts +56 -0
- package/dist/types/src/diagnostics.d.ts.map +1 -0
- package/dist/types/src/diagnostics.test.d.ts +2 -0
- package/dist/types/src/diagnostics.test.d.ts.map +1 -0
- package/dist/types/src/dialect.d.ts +19 -0
- package/dist/types/src/dialect.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +16 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/layout.d.ts +26 -0
- package/dist/types/src/layout.d.ts.map +1 -0
- package/dist/types/src/mermaid-engine.d.ts +83 -0
- package/dist/types/src/mermaid-engine.d.ts.map +1 -0
- package/dist/types/src/mermaid-engine.test.d.ts +2 -0
- package/dist/types/src/mermaid-engine.test.d.ts.map +1 -0
- package/dist/types/src/mermaid.d.ts +56 -0
- package/dist/types/src/mermaid.d.ts.map +1 -0
- package/dist/types/src/mermaid.test.d.ts +3 -0
- package/dist/types/src/mermaid.test.d.ts.map +1 -0
- package/dist/types/src/objective.d.ts +75 -0
- package/dist/types/src/objective.d.ts.map +1 -0
- package/dist/types/src/objective.test.d.ts +2 -0
- package/dist/types/src/objective.test.d.ts.map +1 -0
- package/dist/types/src/ortho-router.d.ts +13 -0
- package/dist/types/src/ortho-router.d.ts.map +1 -0
- package/dist/types/src/scene.d.ts +1006 -0
- package/dist/types/src/scene.d.ts.map +1 -0
- package/dist/types/src/scene.test.d.ts +2 -0
- package/dist/types/src/scene.test.d.ts.map +1 -0
- package/dist/types/src/svg-handler.d.ts +5 -0
- package/dist/types/src/svg-handler.d.ts.map +1 -0
- package/dist/types/src/svg-handler.test.d.ts +2 -0
- package/dist/types/src/svg-handler.test.d.ts.map +1 -0
- package/dist/types/src/testing.d.ts +11 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/src/ui.d.ts +67 -0
- package/dist/types/src/ui.d.ts.map +1 -0
- package/dist/types/src/ui.test.d.ts +2 -0
- package/dist/types/src/ui.test.d.ts.map +1 -0
- package/dist/types/src/uml-engine.d.ts +14 -0
- package/dist/types/src/uml-engine.d.ts.map +1 -0
- package/dist/types/src/uml-engine.test.d.ts +2 -0
- package/dist/types/src/uml-engine.test.d.ts.map +1 -0
- package/dist/types/src/uml-grid.d.ts +99 -0
- package/dist/types/src/uml-grid.d.ts.map +1 -0
- package/dist/types/src/uml-grid.test.d.ts +2 -0
- package/dist/types/src/uml-grid.test.d.ts.map +1 -0
- package/dist/types/src/uml-rules.d.ts +56 -0
- package/dist/types/src/uml-rules.d.ts.map +1 -0
- package/dist/types/src/uml-rules.test.d.ts +2 -0
- package/dist/types/src/uml-rules.test.d.ts.map +1 -0
- package/dist/types/src/uml-search.d.ts +26 -0
- package/dist/types/src/uml-search.d.ts.map +1 -0
- package/dist/types/src/uml-search.test.d.ts +2 -0
- package/dist/types/src/uml-search.test.d.ts.map +1 -0
- package/dist/types/src/uml.d.ts +60 -0
- package/dist/types/src/uml.d.ts.map +1 -0
- package/dist/types/src/uml.test.d.ts +2 -0
- package/dist/types/src/uml.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/src/__snapshots__/diagnostics.test.ts.snap +36 -0
- package/src/__snapshots__/mermaid-engine.test.ts.snap +22 -0
- package/src/content.ts +180 -0
- package/src/diagnostics.test.ts +173 -0
- package/src/diagnostics.ts +393 -0
- package/src/dialect.ts +53 -0
- package/src/index.ts +19 -0
- package/src/layout.ts +99 -0
- package/src/mermaid-engine.test.ts +244 -0
- package/src/mermaid-engine.ts +863 -0
- package/src/mermaid.test.ts +138 -0
- package/src/mermaid.ts +326 -0
- package/src/objective.test.ts +82 -0
- package/src/objective.ts +157 -0
- package/src/ortho-router.ts +372 -0
- package/src/scene.test.ts +36 -0
- package/src/scene.ts +281 -0
- package/src/svg-handler.test.ts +75 -0
- package/src/svg-handler.ts +119 -0
- package/src/testing.ts +68 -0
- package/src/ui.test.ts +183 -0
- package/src/ui.ts +398 -0
- package/src/uml-engine.test.ts +63 -0
- package/src/uml-engine.ts +146 -0
- package/src/uml-grid.test.ts +175 -0
- package/src/uml-grid.ts +559 -0
- package/src/uml-rules.test.ts +109 -0
- package/src/uml-rules.ts +319 -0
- package/src/uml-search.test.ts +108 -0
- package/src/uml-search.ts +266 -0
- package/src/uml.test.ts +172 -0
- 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
|