@dxos/diagram 0.11.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/chunk-parse.mjs +9406 -0
- package/dist/lib/chunk-parse.mjs.map +1 -0
- package/dist/lib/extension.mjs +204 -0
- package/dist/lib/extension.mjs.map +1 -0
- package/dist/lib/index.mjs +497 -2846
- package/dist/lib/index.mjs.map +1 -1
- package/dist/types/src/aesthetics.d.ts +12 -0
- package/dist/types/src/aesthetics.d.ts.map +1 -0
- package/dist/types/src/aesthetics.test.d.ts +2 -0
- package/dist/types/src/aesthetics.test.d.ts.map +1 -0
- package/dist/types/src/appeal.d.ts +55 -0
- package/dist/types/src/appeal.d.ts.map +1 -0
- package/dist/types/src/appeal.test.d.ts +2 -0
- package/dist/types/src/appeal.test.d.ts.map +1 -0
- package/dist/types/src/architecture.d.ts +86 -0
- package/dist/types/src/architecture.d.ts.map +1 -0
- package/dist/types/src/architecture.test.d.ts +2 -0
- package/dist/types/src/architecture.test.d.ts.map +1 -0
- package/dist/types/src/compact.d.ts +39 -0
- package/dist/types/src/compact.d.ts.map +1 -0
- package/dist/types/src/compact.test.d.ts +2 -0
- package/dist/types/src/compact.test.d.ts.map +1 -0
- package/dist/types/src/diagnostics.d.ts +70 -3
- package/dist/types/src/diagnostics.d.ts.map +1 -1
- package/dist/types/src/dsl/cli.d.ts +2 -0
- package/dist/types/src/dsl/cli.d.ts.map +1 -0
- package/dist/types/src/dsl/compile.d.ts +12 -0
- package/dist/types/src/dsl/compile.d.ts.map +1 -0
- package/dist/types/src/dsl/convert.d.ts +45 -0
- package/dist/types/src/dsl/convert.d.ts.map +1 -0
- package/dist/types/src/dsl/convert.test.d.ts +2 -0
- package/dist/types/src/dsl/convert.test.d.ts.map +1 -0
- package/dist/types/src/dsl/dsl.test.d.ts +2 -0
- package/dist/types/src/dsl/dsl.test.d.ts.map +1 -0
- package/dist/types/src/dsl/gen/diagram.d.ts +3 -0
- package/dist/types/src/dsl/gen/diagram.d.ts.map +1 -0
- package/dist/types/src/dsl/index.d.ts +7 -0
- package/dist/types/src/dsl/index.d.ts.map +1 -0
- package/dist/types/src/dsl/parse.d.ts +49 -0
- package/dist/types/src/dsl/parse.d.ts.map +1 -0
- package/dist/types/src/dsl/print.d.ts +19 -0
- package/dist/types/src/dsl/print.d.ts.map +1 -0
- package/dist/types/src/dsl/semantic.test.d.ts +2 -0
- package/dist/types/src/dsl/semantic.test.d.ts.map +1 -0
- package/dist/types/src/dsl/vocabulary.d.ts +61 -0
- package/dist/types/src/dsl/vocabulary.d.ts.map +1 -0
- package/dist/types/src/dx-svg.d.ts +12 -0
- package/dist/types/src/dx-svg.d.ts.map +1 -0
- package/dist/types/src/dx-svg.test.d.ts +2 -0
- package/dist/types/src/dx-svg.test.d.ts.map +1 -0
- package/dist/types/src/extension/complete.d.ts +3 -0
- package/dist/types/src/extension/complete.d.ts.map +1 -0
- package/dist/types/src/extension/extension.test.d.ts +2 -0
- package/dist/types/src/extension/extension.test.d.ts.map +1 -0
- package/dist/types/src/extension/highlight.d.ts +14 -0
- package/dist/types/src/extension/highlight.d.ts.map +1 -0
- package/dist/types/src/extension/index.d.ts +6 -0
- package/dist/types/src/extension/index.d.ts.map +1 -0
- package/dist/types/src/extension/language.d.ts +13 -0
- package/dist/types/src/extension/language.d.ts.map +1 -0
- package/dist/types/src/extension/lint.d.ts +21 -0
- package/dist/types/src/extension/lint.d.ts.map +1 -0
- package/dist/types/src/extension/syntax.d.ts +3 -0
- package/dist/types/src/extension/syntax.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +14 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/mermaid-engine.d.ts +40 -1
- package/dist/types/src/mermaid-engine.d.ts.map +1 -1
- package/dist/types/src/mermaid.d.ts +3 -3
- package/dist/types/src/mermaid.d.ts.map +1 -1
- package/dist/types/src/nudge.d.ts +30 -0
- package/dist/types/src/nudge.d.ts.map +1 -0
- package/dist/types/src/nudge.test.d.ts +2 -0
- package/dist/types/src/nudge.test.d.ts.map +1 -0
- package/dist/types/src/objective.d.ts +4 -0
- package/dist/types/src/objective.d.ts.map +1 -1
- package/dist/types/src/ortho-router.d.ts +13 -1
- package/dist/types/src/ortho-router.d.ts.map +1 -1
- package/dist/types/src/ports.d.ts +40 -0
- package/dist/types/src/ports.d.ts.map +1 -0
- package/dist/types/src/ports.test.d.ts +2 -0
- package/dist/types/src/ports.test.d.ts.map +1 -0
- package/dist/types/src/relations.test.d.ts +2 -0
- package/dist/types/src/relations.test.d.ts.map +1 -0
- package/dist/types/src/rules.d.ts +30 -0
- package/dist/types/src/rules.d.ts.map +1 -0
- package/dist/types/src/scene.d.ts +108 -74
- package/dist/types/src/scene.d.ts.map +1 -1
- package/dist/types/src/score.d.ts +54 -0
- package/dist/types/src/score.d.ts.map +1 -0
- package/dist/types/src/score.test.d.ts +2 -0
- package/dist/types/src/score.test.d.ts.map +1 -0
- package/dist/types/src/semantic-engine.d.ts +41 -0
- package/dist/types/src/semantic-engine.d.ts.map +1 -0
- package/dist/types/src/semantic-engine.test.d.ts +2 -0
- package/dist/types/src/semantic-engine.test.d.ts.map +1 -0
- package/dist/types/src/semantic-place.d.ts +81 -0
- package/dist/types/src/semantic-place.d.ts.map +1 -0
- package/dist/types/src/semantic-route.d.ts +133 -0
- package/dist/types/src/semantic-route.d.ts.map +1 -0
- package/dist/types/src/semantic.d.ts +164 -0
- package/dist/types/src/semantic.d.ts.map +1 -0
- package/dist/types/src/uml-grid.d.ts.map +1 -1
- package/dist/types/src/uml.d.ts +3 -5
- package/dist/types/src/uml.d.ts.map +1 -1
- package/dist/types/src/view.d.ts +53 -0
- package/dist/types/src/view.d.ts.map +1 -0
- package/dist/types/src/view.test.d.ts +2 -0
- package/dist/types/src/view.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -5
- package/rules/DIAGRAM.mdl +388 -0
- package/src/aesthetics.test.ts +89 -0
- package/src/aesthetics.ts +148 -0
- package/src/appeal.test.ts +144 -0
- package/src/appeal.ts +586 -0
- package/src/architecture.test.ts +94 -0
- package/src/architecture.ts +215 -0
- package/src/compact.test.ts +67 -0
- package/src/compact.ts +219 -0
- package/src/diagnostics.test.ts +207 -1
- package/src/diagnostics.ts +481 -37
- package/src/dsl/cli.ts +46 -0
- package/src/dsl/compile.ts +28 -0
- package/src/dsl/convert.test.ts +51 -0
- package/src/dsl/convert.ts +98 -0
- package/src/dsl/diagram.grammar +195 -0
- package/src/dsl/dsl.test.ts +286 -0
- package/src/dsl/gen/diagram.ts +22 -0
- package/src/dsl/index.ts +44 -0
- package/src/dsl/parse.ts +1054 -0
- package/src/dsl/print.ts +198 -0
- package/src/dsl/semantic.test.ts +216 -0
- package/src/dsl/vocabulary.ts +150 -0
- package/src/dx-svg.test.ts +44 -0
- package/src/dx-svg.ts +77 -0
- package/src/extension/complete.ts +105 -0
- package/src/extension/extension.test.ts +135 -0
- package/src/extension/highlight.ts +23 -0
- package/src/extension/index.ts +9 -0
- package/src/extension/language.ts +31 -0
- package/src/extension/lint.ts +60 -0
- package/src/extension/syntax.ts +62 -0
- package/src/index.ts +14 -0
- package/src/mermaid-engine.test.ts +6 -6
- package/src/mermaid-engine.ts +569 -119
- package/src/mermaid.test.ts +17 -1
- package/src/mermaid.ts +30 -18
- package/src/nudge.test.ts +186 -0
- package/src/nudge.ts +387 -0
- package/src/objective.test.ts +2 -0
- package/src/objective.ts +17 -1
- package/src/ortho-router.ts +240 -80
- package/src/ports.test.ts +157 -0
- package/src/ports.ts +186 -0
- package/src/relations.test.ts +98 -0
- package/src/rules.ts +117 -0
- package/src/scene.ts +66 -5
- package/src/score.test.ts +64 -0
- package/src/score.ts +130 -0
- package/src/semantic-engine.test.ts +635 -0
- package/src/semantic-engine.ts +891 -0
- package/src/semantic-place.ts +1508 -0
- package/src/semantic-route.ts +1136 -0
- package/src/semantic.ts +278 -0
- package/src/typings.d.ts +8 -0
- package/src/uml-grid.ts +6 -1
- package/src/uml.test.ts +5 -1
- package/src/uml.ts +24 -11
- package/src/view.test.ts +139 -0
- package/src/view.ts +376 -0
package/dist/lib/index.mjs
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
import { $ as scene_exports, A as compile$7, B as compile$5, C as SEMANTIC_KEYWORDS, D as mermaid_engine_exports, E as compile$6, F as objective_exports, H as parse$1, I as emit, J as layout_exports, K as diagnostics_exports, L as measureCell, M as appeal_exports, N as view_exports, O as ports_exports, P as rules_exports, Q as Command, R as uml_grid_exports, S as RESERVED, T as semantic_exports, U as relationRanks, V as isClassDiagram, W as uml_exports, X as architecture_exports, Y as rank, Z as judge$1, _ as ELEMENT_KINDS, a as withLayout, b as OBJECT_ATTRS, c as semantic_engine_exports, d as BARE_ID, et as __exportAll, f as BARE_REF, g as ELEMENT_ATTRS, h as EDGE_ATTRS, i as toScene, j as mermaid_exports, k as nudge_exports, l as semantic_route_exports, m as DIAGRAM_ATTRS, n as parseScene, o as parser, p as CLAUSE_KEYWORDS, q as backEdges, r as read, s as compile$8, t as parse, u as semantic_place_exports, v as GROUP_ATTRS, w as STATEMENT_KEYWORDS, x as RELATION_WORDS, y as NODE_ATTRS, z as zRouter } from "./chunk-parse.mjs";
|
|
1
2
|
import { invariant } from "@dxos/invariant";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
4
6
|
import ELK from "elkjs/lib/elk.bundled.js";
|
|
5
7
|
import * as SchemaAST from "effect/SchemaAST";
|
|
6
8
|
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
9
|
//#region src/content.ts
|
|
20
|
-
var __dxlog_file$
|
|
10
|
+
var __dxlog_file$1 = "/__w/dxos/dxos/packages/common/diagram/src/content.ts";
|
|
21
11
|
/** Apply a batch of edit commands to the canvas content (mutates `content`). */
|
|
22
12
|
var applyCommands = (content, commands, handler) => {
|
|
23
13
|
handler.scaffold?.(content);
|
|
@@ -60,7 +50,7 @@ var applyCommands = (content, commands, handler) => {
|
|
|
60
50
|
const previous = find(command.objectId);
|
|
61
51
|
invariant(previous, `unknown object: ${command.objectId} (use upsert-object to create it)`, {
|
|
62
52
|
"~LogMeta": "~LogMeta",
|
|
63
|
-
F: __dxlog_file$
|
|
53
|
+
F: __dxlog_file$1,
|
|
64
54
|
L: 128,
|
|
65
55
|
S: void 0,
|
|
66
56
|
A: ["previous", "`unknown object: ${command.objectId} (use upsert-object to create it)`"]
|
|
@@ -76,2865 +66,525 @@ var applyCommands = (content, commands, handler) => {
|
|
|
76
66
|
}
|
|
77
67
|
case "remove-elements": {
|
|
78
68
|
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);
|
|
69
|
+
removed += remove((identity) => identity.object === command.objectId && ids.has(identity.element));
|
|
70
|
+
break;
|
|
2078
71
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
|
72
|
+
case "remove-object":
|
|
73
|
+
removed += remove((identity) => identity.object === command.objectId);
|
|
74
|
+
break;
|
|
75
|
+
case "move-object": {
|
|
76
|
+
const previous = find(command.objectId);
|
|
77
|
+
invariant(previous, `unknown object: ${command.objectId}`, {
|
|
78
|
+
"~LogMeta": "~LogMeta",
|
|
79
|
+
F: __dxlog_file$1,
|
|
80
|
+
L: 149,
|
|
81
|
+
S: void 0,
|
|
82
|
+
A: ["previous", "`unknown object: ${command.objectId}`"]
|
|
2108
83
|
});
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
});
|
|
84
|
+
const delta = {
|
|
85
|
+
x: command.origin.x - previous.origin.x,
|
|
86
|
+
y: command.origin.y - previous.origin.y
|
|
87
|
+
};
|
|
88
|
+
for (const record of Object.values(content)) if (handler.identify(record)?.object === command.objectId) handler.translate(record, delta);
|
|
89
|
+
upserted.add(command.objectId);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
2132
92
|
}
|
|
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
93
|
return {
|
|
2187
|
-
|
|
2188
|
-
|
|
94
|
+
upserted: [...upserted],
|
|
95
|
+
removed
|
|
2189
96
|
};
|
|
2190
97
|
};
|
|
2191
|
-
/**
|
|
2192
|
-
var
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
|
98
|
+
/** Seed a fractional-index counter past any existing `a<n>` indexes. */
|
|
99
|
+
var nextIndex = (content, isCounted = () => true) => {
|
|
100
|
+
let max = 0;
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const record of Object.values(content)) {
|
|
103
|
+
if (!record || !isCounted(record)) continue;
|
|
104
|
+
count++;
|
|
105
|
+
const match = /^a(\d+)$/.exec(record.index ?? "");
|
|
106
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
107
|
+
}
|
|
108
|
+
return Math.max(max, count);
|
|
2236
109
|
};
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/dialect.ts
|
|
112
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/common/diagram/src/dialect.ts";
|
|
113
|
+
/** The core dialect: input is already scene edit commands (identity compile). */
|
|
114
|
+
var sceneDialect = {
|
|
115
|
+
id: "scene",
|
|
116
|
+
description: "Positioned world objects and elements; the agent does the layout.",
|
|
117
|
+
input: Schema.Array(Command),
|
|
118
|
+
compile: Effect.succeed
|
|
2243
119
|
};
|
|
2244
|
-
var
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
120
|
+
var DialectRegistry = class {
|
|
121
|
+
#dialects = /* @__PURE__ */ new Map();
|
|
122
|
+
constructor(dialects = [sceneDialect]) {
|
|
123
|
+
dialects.forEach((dialect) => this.register(dialect));
|
|
124
|
+
}
|
|
125
|
+
register(dialect) {
|
|
126
|
+
invariant(!this.#dialects.has(dialect.id), `duplicate dialect: ${dialect.id}`, {
|
|
127
|
+
"~LogMeta": "~LogMeta",
|
|
128
|
+
F: __dxlog_file,
|
|
129
|
+
L: 42,
|
|
130
|
+
S: this,
|
|
131
|
+
A: ["!this.#dialects.has(dialect.id)", "`duplicate dialect: ${dialect.id}`"]
|
|
132
|
+
});
|
|
133
|
+
this.#dialects.set(dialect.id, dialect);
|
|
134
|
+
}
|
|
135
|
+
get(id) {
|
|
136
|
+
return this.#dialects.get(id);
|
|
137
|
+
}
|
|
138
|
+
list() {
|
|
139
|
+
return [...this.#dialects.values()];
|
|
140
|
+
}
|
|
2253
141
|
};
|
|
2254
142
|
//#endregion
|
|
2255
|
-
//#region src/
|
|
2256
|
-
var
|
|
2257
|
-
|
|
2258
|
-
|
|
143
|
+
//#region src/aesthetics.ts
|
|
144
|
+
var aesthetics_exports = /* @__PURE__ */ __exportAll({
|
|
145
|
+
IMAGE_RULES: () => IMAGE_RULES,
|
|
146
|
+
RULES: () => RULES,
|
|
147
|
+
judge: () => judge
|
|
2259
148
|
});
|
|
2260
|
-
var
|
|
2261
|
-
var
|
|
2262
|
-
var
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
"
|
|
2287
|
-
|
|
2288
|
-
|
|
149
|
+
var HOW_TO_READ = "The input is a diagram as drawn: `layout` renders the page as text, a character drawing followed by a row-by-row reading of the boxes and of which way each arrow runs. Judge how the drawing reads to a person looking at it, not whether the architecture it shows is good.";
|
|
150
|
+
var HOW_TO_SEE = "The input is a diagram as drawn: the attached image is the page. Judge how the drawing reads to a person looking at it, not whether the architecture it shows is good.";
|
|
151
|
+
var rule = (id, key, description, question, criteria) => ({
|
|
152
|
+
id,
|
|
153
|
+
key,
|
|
154
|
+
description,
|
|
155
|
+
instructions: `${HOW_TO_READ} ${question}`,
|
|
156
|
+
criteria
|
|
157
|
+
});
|
|
158
|
+
var RULES = [
|
|
159
|
+
rule("arrows-follow-flow", "arrowsFollowFlow", "Arrows run with the main direction of the diagram, inside groups as well as between them.", "Do the arrows run with the main direction of the diagram (down in a top-to-bottom drawing, right in a left-to-right one), inside groups as well as between them, with at most a few pointing back against it? Arrows with a hollow triangle head (inheritance, implementation) point to the abstraction above by convention and do not count against the flow.", {
|
|
160
|
+
true: "Nearly every arrow runs with the flow; a reader follows dependencies in one direction.",
|
|
161
|
+
false: "Many arrows, or whole chains inside a group, run up or back against the flow."
|
|
162
|
+
}),
|
|
163
|
+
rule("readable-labels", "readableLabels", "Every box and arrow label can be read on its own.", "Can every box label and arrow label be read on its own, without running into another label, a line or a box?", {
|
|
164
|
+
true: "Every label sits clear of other text and lines.",
|
|
165
|
+
false: "Some label overlaps another label, a box or a line, or is cut off."
|
|
166
|
+
}),
|
|
167
|
+
rule("traceable-arrows", "traceableArrows", "Each arrow can be followed from its start to its end.", "Can a reader follow each arrow from its start to its end without ambiguity — no two arrows sharing a line segment or merged into one trunk, no arrow running through a group it does not connect to, no long detours around the page, and few crossings?", {
|
|
168
|
+
true: "Each arrow has its own short, direct route.",
|
|
169
|
+
false: "Arrows share trunks, detour far, or cross so that their ends are hard to pair up."
|
|
170
|
+
}),
|
|
171
|
+
rule("entries-face-callers", "entriesFaceCallers", "Boxes that outside arrows reach sit on the side of their group facing the callers.", "Within each group, do the boxes that arrows from outside the group reach sit on the side of the group facing those callers, so that cross-group arrows are short and do not run through or around the group?", {
|
|
172
|
+
true: "Cross-group arrows land on the near side of each group.",
|
|
173
|
+
false: "An arrow from outside travels through or around a group to reach a box on its far side."
|
|
174
|
+
}),
|
|
175
|
+
rule("compact", "compact", "The drawing is packed tightly: no large empty regions, groups sized to their content.", "Is the drawing compact — no large empty regions between or inside groups, groups no bigger than what they hold, and boxes that are connected placed next to each other?", {
|
|
176
|
+
true: "The page is tightly packed; connected boxes are neighbours.",
|
|
177
|
+
false: "There are large empty regions, stretched groups, or connected boxes far apart."
|
|
178
|
+
}),
|
|
179
|
+
rule("groups-aligned", "groupsAligned", "Groups line up with each other rather than floating at arbitrary offsets.", "Do the groups line up with one another — sharing a top edge or a left edge where they sit side by side or stacked — rather than one group floating at an arbitrary height or offset beside another?", {
|
|
180
|
+
true: "Groups share edges and read as one arrangement.",
|
|
181
|
+
false: "A group sits at an arbitrary offset from its neighbours."
|
|
182
|
+
}),
|
|
183
|
+
rule("names-only", "namesOnly", "Each box is labelled with a component name alone, without comments or qualifiers.", "Is every box labelled with just the name of what it is, with no comments, limits or qualifiers mixed into the name (such as \"no cap\", \"≤10\" or a colon followed by a description)?", {
|
|
184
|
+
true: "Box labels are plain names; any commentary sits on arrows or not at all.",
|
|
185
|
+
false: "Some box label mixes a name with a comment, a limit or a description."
|
|
186
|
+
}),
|
|
187
|
+
rule("aligned-grid", "alignedGrid", "Boxes line up in rows and columns with consistent spacing.", "Are the boxes aligned into clear rows and columns with consistent spacing, so the structure reads as a grid?", {
|
|
188
|
+
true: "Boxes share rows and columns with even gaps.",
|
|
189
|
+
false: "Boxes are staggered or unevenly spaced, with no clear rows or columns."
|
|
190
|
+
})
|
|
2289
191
|
];
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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
|
-
};
|
|
192
|
+
/** {@link RULES} for a judge shown the page as an image (`Architecture.Subject.images`) instead of `layout`. */
|
|
193
|
+
var IMAGE_RULES = RULES.map((rule) => ({
|
|
194
|
+
...rule,
|
|
195
|
+
instructions: `${HOW_TO_SEE}${rule.instructions.slice(269)}`
|
|
196
|
+
}));
|
|
2309
197
|
/**
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2312
|
-
* after quantization — so that clearance is a floor on the pitch.
|
|
198
|
+
* Every aesthetic rule as a score from one `DecisionModel` call. Without `layout` or an image of the
|
|
199
|
+
* page there is nothing to judge, so each rule reports an error rather than a guess.
|
|
2313
200
|
*/
|
|
2314
|
-
var
|
|
2315
|
-
const
|
|
201
|
+
var judge = (rules = RULES) => {
|
|
202
|
+
const inner = judge$1(rules, "aesthetics");
|
|
2316
203
|
return {
|
|
2317
|
-
|
|
2318
|
-
|
|
204
|
+
entries: inner.entries,
|
|
205
|
+
evaluate: (subject) => subject.content.layout || subject.images?.length ? inner.evaluate(subject) : Effect.succeed(rules.map(() => ({
|
|
206
|
+
score: 0,
|
|
207
|
+
error: "No layout to judge."
|
|
208
|
+
})))
|
|
2319
209
|
};
|
|
2320
210
|
};
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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;
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/dsl/print.ts
|
|
213
|
+
var ESCAPES = {
|
|
214
|
+
"\\": "\\\\",
|
|
215
|
+
"\"": "\\\"",
|
|
216
|
+
"\n": "\\n",
|
|
217
|
+
" ": "\\t",
|
|
218
|
+
"\r": "\\r"
|
|
2354
219
|
};
|
|
2355
220
|
/**
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
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.
|
|
221
|
+
* Shortest form that reads back: integers keep no trailing zeros, negatives keep their sign, and
|
|
222
|
+
* the exponent notation `String` switches to outside ~1e-7..1e21 is spelled as-is, which the
|
|
223
|
+
* grammar accepts. `NaN` and `Infinity` have no readable form at all, so they are refused here
|
|
224
|
+
* rather than silently producing a document that cannot be parsed.
|
|
2362
225
|
*/
|
|
2363
|
-
var
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
-
};
|
|
226
|
+
var formatNumber = (value) => {
|
|
227
|
+
if (!Number.isFinite(value)) throw new TypeError(`A scene coordinate must be finite; got ${value}.`);
|
|
228
|
+
return String(value);
|
|
229
|
+
};
|
|
230
|
+
var formatString = (value) => `"${value.replace(/[\\"\n\t\r]/g, (character) => ESCAPES[character] ?? character)}"`;
|
|
231
|
+
/** Bare where the grammar allows it, quoted where a word would be read as a keyword or fail to lex. */
|
|
232
|
+
var formatId = (id) => BARE_ID.test(id) && !RESERVED.has(id) ? id : formatString(id);
|
|
233
|
+
var formatPoint = ({ x, y }) => `${formatNumber(x)},${formatNumber(y)}`;
|
|
234
|
+
var formatSize = (w, h) => `${formatNumber(w)}x${formatNumber(h)}`;
|
|
2398
235
|
/**
|
|
2399
|
-
*
|
|
2400
|
-
*
|
|
2401
|
-
*
|
|
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`.
|
|
236
|
+
* Quoted unless every id in it is a bare word: a dialect may build a ref from a node id the
|
|
237
|
+
* grammar cannot lex, such as mermaid's `1st` becoming `1st/box`, and an unquoted one would make
|
|
238
|
+
* the printed scene fail to parse.
|
|
2406
239
|
*/
|
|
2407
|
-
var
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
240
|
+
var formatRef = (ref) => BARE_REF.test(ref) && !RESERVED.has(ref) ? ref : formatString(ref);
|
|
241
|
+
/** `_` is the end the schema leaves unset; a bound end prints as its ref, a free one as a point. */
|
|
242
|
+
var formatEndpoint = (ref, point) => ref !== void 0 ? formatRef(ref) : point !== void 0 ? formatPoint(point) : "_";
|
|
243
|
+
/** Prints `name=value` for the attributes of `kind` that are set, in the table's order. */
|
|
244
|
+
var formatAttrs = (kind, values) => ELEMENT_ATTRS[kind].flatMap(({ name, type }) => {
|
|
245
|
+
const value = values[name];
|
|
246
|
+
if (value === void 0) return [];
|
|
247
|
+
return [`${name}=${type === "string" && typeof value === "string" ? formatString(value) : String(value)}`];
|
|
248
|
+
});
|
|
249
|
+
var style = ({ color, fill, stroke, weight }) => ({
|
|
250
|
+
color,
|
|
251
|
+
fill,
|
|
252
|
+
stroke,
|
|
253
|
+
weight
|
|
254
|
+
});
|
|
255
|
+
var formatElement = (element) => {
|
|
256
|
+
switch (element.kind) {
|
|
257
|
+
case "rect":
|
|
258
|
+
case "ellipse":
|
|
259
|
+
case "diamond":
|
|
260
|
+
case "triangle": return [
|
|
261
|
+
element.kind,
|
|
262
|
+
formatId(element.id),
|
|
263
|
+
formatPoint(element),
|
|
264
|
+
formatSize(element.w, element.h),
|
|
265
|
+
...element.text === void 0 ? [] : [formatString(element.text)],
|
|
266
|
+
...formatAttrs(element.kind, {
|
|
267
|
+
rotation: element.rotation,
|
|
268
|
+
corners: element.corners,
|
|
269
|
+
...style(element)
|
|
270
|
+
})
|
|
271
|
+
].join(" ");
|
|
272
|
+
case "circle": return [
|
|
273
|
+
"circle",
|
|
274
|
+
formatId(element.id),
|
|
275
|
+
formatPoint({
|
|
276
|
+
x: element.cx,
|
|
277
|
+
y: element.cy
|
|
278
|
+
}),
|
|
279
|
+
formatNumber(element.r),
|
|
280
|
+
...element.text === void 0 ? [] : [formatString(element.text)],
|
|
281
|
+
...formatAttrs("circle", style(element))
|
|
282
|
+
].join(" ");
|
|
283
|
+
case "line": return [
|
|
284
|
+
"line",
|
|
285
|
+
formatId(element.id),
|
|
286
|
+
...element.points.map(formatPoint),
|
|
287
|
+
...formatAttrs("line", {
|
|
288
|
+
closed: element.closed,
|
|
289
|
+
...style(element)
|
|
290
|
+
})
|
|
291
|
+
].join(" ");
|
|
292
|
+
case "curve": return [
|
|
293
|
+
"curve",
|
|
294
|
+
formatId(element.id),
|
|
295
|
+
...element.points.map(formatPoint),
|
|
296
|
+
...formatAttrs("curve", style(element))
|
|
297
|
+
].join(" ");
|
|
298
|
+
case "arc": return [
|
|
299
|
+
"arc",
|
|
300
|
+
formatId(element.id),
|
|
301
|
+
formatPoint({
|
|
302
|
+
x: element.cx,
|
|
303
|
+
y: element.cy
|
|
304
|
+
}),
|
|
305
|
+
formatNumber(element.r),
|
|
306
|
+
`${formatNumber(element.startAngle)}..${formatNumber(element.endAngle)}`,
|
|
307
|
+
...formatAttrs("arc", style(element))
|
|
308
|
+
].join(" ");
|
|
309
|
+
case "text": return [
|
|
310
|
+
"text",
|
|
311
|
+
formatId(element.id),
|
|
312
|
+
formatPoint(element),
|
|
313
|
+
formatString(element.text),
|
|
314
|
+
...formatAttrs("text", {
|
|
315
|
+
w: element.w,
|
|
316
|
+
...style(element)
|
|
317
|
+
})
|
|
318
|
+
].join(" ");
|
|
319
|
+
case "arrow": return [
|
|
320
|
+
"arrow",
|
|
321
|
+
formatId(element.id),
|
|
322
|
+
formatEndpoint(element.from, element.start),
|
|
323
|
+
"->",
|
|
324
|
+
formatEndpoint(element.to, element.end),
|
|
325
|
+
...element.text === void 0 ? [] : [formatString(element.text)],
|
|
326
|
+
...formatAttrs("arrow", {
|
|
327
|
+
head: element.head,
|
|
328
|
+
tail: element.tail,
|
|
329
|
+
relation: element.relation,
|
|
330
|
+
...style(element)
|
|
331
|
+
})
|
|
332
|
+
].join(" ");
|
|
333
|
+
case "portal": return [
|
|
334
|
+
"portal",
|
|
335
|
+
formatId(element.id),
|
|
336
|
+
formatPoint(element),
|
|
337
|
+
formatSize(element.w, element.h),
|
|
338
|
+
...element.text === void 0 ? [] : [formatString(element.text)],
|
|
339
|
+
...formatAttrs("portal", {
|
|
340
|
+
ref: element.ref,
|
|
341
|
+
...style(element)
|
|
342
|
+
})
|
|
343
|
+
].join(" ");
|
|
344
|
+
}
|
|
2457
345
|
};
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
"
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
"
|
|
2480
|
-
"
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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);
|
|
346
|
+
var block = (header, elements) => [
|
|
347
|
+
`${header} {`,
|
|
348
|
+
...elements.map((element) => ` ${formatElement(element)}`),
|
|
349
|
+
"}"
|
|
350
|
+
].join("\n");
|
|
351
|
+
var formatObject = (object) => block([
|
|
352
|
+
"object",
|
|
353
|
+
formatId(object.id),
|
|
354
|
+
...object.origin === void 0 ? [] : ["@", formatPoint(object.origin)],
|
|
355
|
+
...OBJECT_ATTRS.flatMap(({ name, type }) => {
|
|
356
|
+
const value = {
|
|
357
|
+
scale: object.scale,
|
|
358
|
+
index: object.index,
|
|
359
|
+
ref: object.ref
|
|
360
|
+
}[name];
|
|
361
|
+
if (value === void 0) return [];
|
|
362
|
+
return [`${name}=${type === "string" && typeof value === "string" ? formatString(value) : String(value)}`];
|
|
363
|
+
})
|
|
364
|
+
].join(" "), object.elements);
|
|
365
|
+
var formatCommand = (command) => {
|
|
366
|
+
switch (command.op) {
|
|
367
|
+
case "upsert-object": return formatObject(command.object);
|
|
368
|
+
case "upsert-elements": return block(`elements ${formatId(command.objectId)}`, command.elements);
|
|
369
|
+
case "move-object": return `move ${formatId(command.objectId)} @ ${formatPoint(command.origin)}`;
|
|
370
|
+
case "remove-object": return `remove object ${formatId(command.objectId)}`;
|
|
371
|
+
case "remove-elements": return `remove elements ${formatId(command.objectId)} ${command.elementIds.map(formatId).join(" ")}`;
|
|
372
|
+
}
|
|
2526
373
|
};
|
|
2527
|
-
/**
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
...point,
|
|
2562
|
-
[axis]: point[axis] + shift
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
374
|
+
/** A whole document: statements separated by a blank line, newline-terminated. */
|
|
375
|
+
var printCommands = (commands) => commands.length === 0 ? "" : `${commands.map(formatCommand).join("\n\n")}\n`;
|
|
376
|
+
/** A scene document — every statement an `object`, which is what a drawing serializes to. */
|
|
377
|
+
var print = (scene) => printCommands(scene.objects.map((object) => ({
|
|
378
|
+
op: "upsert-object",
|
|
379
|
+
object
|
|
380
|
+
})));
|
|
381
|
+
var SOURCES = [
|
|
382
|
+
{
|
|
383
|
+
id: "uml",
|
|
384
|
+
description: "Mermaid classDiagram.",
|
|
385
|
+
detect: isClassDiagram,
|
|
386
|
+
compile: (source) => Effect.sync(() => compile$5(source))
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "mermaid",
|
|
390
|
+
description: "Mermaid flowchart, including the `%% ref` and UML edge-token extensions.",
|
|
391
|
+
detect: (source) => /^\s*(flowchart|graph)\s+/m.test(source),
|
|
392
|
+
compile: (source) => Effect.promise(() => compile$6(source))
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: "mermaid-layered",
|
|
396
|
+
description: "Mermaid flowchart with the dialect's own layered placement rather than ELK.",
|
|
397
|
+
detect: () => false,
|
|
398
|
+
compile: (source) => Effect.sync(() => compile$7(source))
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
/** The language a source string is written in, or undefined when nothing claims it. */
|
|
402
|
+
var detect = (source) => SOURCES.find(({ detect }) => detect(source));
|
|
403
|
+
var known = () => SOURCES.map(({ id }) => id).join(", ");
|
|
404
|
+
/** Tagged so the failure channel stays discriminated rather than collapsing into bare `Error`. */
|
|
405
|
+
var UnknownSourceError = class extends Data.TaggedError("DiagramUnknownSource") {
|
|
406
|
+
get message() {
|
|
407
|
+
return `Unknown source language "${this.id}"; known: ${known()}.`;
|
|
2565
408
|
}
|
|
2566
|
-
return result;
|
|
2567
409
|
};
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
-
});
|
|
410
|
+
var UndetectedSourceError = class extends Data.TaggedError("DiagramUndetectedSource") {
|
|
411
|
+
get message() {
|
|
412
|
+
return `Could not tell what language this is; pass one of: ${known()}.`;
|
|
2592
413
|
}
|
|
2593
|
-
return {
|
|
2594
|
-
nodes,
|
|
2595
|
-
frames
|
|
2596
|
-
};
|
|
2597
414
|
};
|
|
415
|
+
/** Compiles `text` through its dialect and prints the result as DSL text. */
|
|
416
|
+
var convert = (text, { source } = {}) => Effect.gen(function* () {
|
|
417
|
+
const language = source === void 0 ? detect(text) : SOURCES.find(({ id }) => id === source);
|
|
418
|
+
if (!language) return yield* Effect.fail(source === void 0 ? new UndetectedSourceError() : new UnknownSourceError({ id: source }));
|
|
419
|
+
return printCommands(yield* language.compile(text));
|
|
420
|
+
});
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/dsl/compile.ts
|
|
2598
423
|
/**
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
* coordinates. Mirrors the straightening pass in the UML emitter.
|
|
424
|
+
* Reads a document and lays out its semantic statements with the full search, which also tries the
|
|
425
|
+
* mermaid engine's placements when nothing was hinted. Asynchronous only because that engine's ELK
|
|
426
|
+
* is; a document of scene statements alone reads exactly as {@link parse} reads it.
|
|
2603
427
|
*/
|
|
2604
|
-
var
|
|
2605
|
-
const
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
428
|
+
var compile$4 = Effect.fn("Dsl.compile")(function* (text, options = {}) {
|
|
429
|
+
const reading = read(text);
|
|
430
|
+
const { diagram } = reading;
|
|
431
|
+
if (!diagram) return withLayout(reading);
|
|
432
|
+
const solution = yield* Effect.promise(() => compile$8(diagram, options));
|
|
433
|
+
return withLayout(reading, solution);
|
|
434
|
+
});
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/dsl/index.ts
|
|
437
|
+
var dsl_exports = /* @__PURE__ */ __exportAll({
|
|
438
|
+
CLAUSE_KEYWORDS: () => CLAUSE_KEYWORDS,
|
|
439
|
+
DIAGRAM_ATTRS: () => DIAGRAM_ATTRS,
|
|
440
|
+
EDGE_ATTRS: () => EDGE_ATTRS,
|
|
441
|
+
ELEMENT_ATTRS: () => ELEMENT_ATTRS,
|
|
442
|
+
ELEMENT_KINDS: () => ELEMENT_KINDS,
|
|
443
|
+
GROUP_ATTRS: () => GROUP_ATTRS,
|
|
444
|
+
NODE_ATTRS: () => NODE_ATTRS,
|
|
445
|
+
OBJECT_ATTRS: () => OBJECT_ATTRS,
|
|
446
|
+
RELATION_WORDS: () => RELATION_WORDS,
|
|
447
|
+
RESERVED: () => RESERVED,
|
|
448
|
+
SEMANTIC_KEYWORDS: () => SEMANTIC_KEYWORDS,
|
|
449
|
+
SOURCES: () => SOURCES,
|
|
450
|
+
STATEMENT_KEYWORDS: () => STATEMENT_KEYWORDS,
|
|
451
|
+
UndetectedSourceError: () => UndetectedSourceError,
|
|
452
|
+
UnknownSourceError: () => UnknownSourceError,
|
|
453
|
+
compile: () => compile$4,
|
|
454
|
+
convert: () => convert,
|
|
455
|
+
detect: () => detect,
|
|
456
|
+
formatCommand: () => formatCommand,
|
|
457
|
+
formatElement: () => formatElement,
|
|
458
|
+
formatId: () => formatId,
|
|
459
|
+
formatObject: () => formatObject,
|
|
460
|
+
formatString: () => formatString,
|
|
461
|
+
parse: () => parse,
|
|
462
|
+
parseScene: () => parseScene,
|
|
463
|
+
parser: () => parser,
|
|
464
|
+
print: () => print,
|
|
465
|
+
printCommands: () => printCommands,
|
|
466
|
+
read: () => read,
|
|
467
|
+
toScene: () => toScene,
|
|
468
|
+
withLayout: () => withLayout
|
|
469
|
+
});
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/dx-svg.ts
|
|
472
|
+
var dx_svg_exports = /* @__PURE__ */ __exportAll({
|
|
473
|
+
FORMAT: () => FORMAT,
|
|
474
|
+
embed: () => embed,
|
|
475
|
+
extract: () => extract,
|
|
476
|
+
isDxSvg: () => isDxSvg
|
|
477
|
+
});
|
|
478
|
+
/** The container format; versions the slot and encoding, not the payload, which versions itself. */
|
|
479
|
+
var FORMAT = "org.dxos.dx-svg@1";
|
|
480
|
+
var NAMESPACE = "https://dxos.org/ns/dx-svg";
|
|
481
|
+
var MEDIA_TYPE = "application/vnd.dxos.dx+json";
|
|
482
|
+
var NOTICE = "<!-- Drawn in DXOS: the embedded data is the source; edits to the picture alone are not reflected in it. -->";
|
|
2635
483
|
/**
|
|
2636
|
-
*
|
|
2637
|
-
*
|
|
2638
|
-
*
|
|
2639
|
-
* to the per-edge router.
|
|
484
|
+
* JSON with `<`, `>` and `&` as unicode escapes, so the text needs neither CDATA nor entity decoding
|
|
485
|
+
* and `JSON.parse` restores it exactly. A top-level `objects` array goes one object per line, so a
|
|
486
|
+
* changed drawing diffs by object.
|
|
2640
487
|
*/
|
|
2641
|
-
var
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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
|
-
});
|
|
488
|
+
var encode = (payload) => {
|
|
489
|
+
const escape = (json) => json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
490
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload) && "objects" in payload && Array.isArray(payload.objects)) {
|
|
491
|
+
const { objects, ...rest } = payload;
|
|
492
|
+
const head = JSON.stringify(rest);
|
|
493
|
+
const body = objects.map((object) => escape(JSON.stringify(object))).join(",\n");
|
|
494
|
+
return `${escape(head.slice(0, -1))}${head.length > 2 ? "," : ""}"objects":[\n${body}\n]}`;
|
|
2695
495
|
}
|
|
2696
|
-
return
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
496
|
+
return escape(JSON.stringify(payload));
|
|
497
|
+
};
|
|
498
|
+
/** Embeds a payload in a standalone SVG: a marker on the root and the payload as its first child. */
|
|
499
|
+
var embed = (svg, payload) => {
|
|
500
|
+
const root = /<svg\b[^>]*>/.exec(svg);
|
|
501
|
+
if (!root) throw new Error("Not an SVG document: no <svg> element.");
|
|
502
|
+
if (/\bdx:payload=/.test(root[0])) throw new Error("The SVG already carries a dx-svg payload.");
|
|
503
|
+
const open = root[0].replace(/<svg\b/, `<svg xmlns:dx="${NAMESPACE}" dx:payload="${FORMAT}"`);
|
|
504
|
+
const metadata = `<metadata id="dx-payload" data-type="${MEDIA_TYPE}" data-version="1" data-encoding="json">\n${encode(payload)}\n</metadata>`;
|
|
505
|
+
return `${svg.slice(0, root.index)}${NOTICE}\n${open}${metadata}${svg.slice(root.index + root[0].length)}`;
|
|
506
|
+
};
|
|
507
|
+
/** Whether an SVG carries a payload, without parsing it. */
|
|
508
|
+
var isDxSvg = (svg) => /<metadata\b[^>]*\bid="dx-payload"/.test(svg);
|
|
2701
509
|
/**
|
|
2702
|
-
*
|
|
2703
|
-
*
|
|
510
|
+
* The payload of a `.dx.svg`, or undefined for a plain SVG. Works on the text alone, so it runs in node,
|
|
511
|
+
* workers and the browser. An encoding this version does not know is an error, not a missing payload.
|
|
2704
512
|
*/
|
|
2705
|
-
var
|
|
2706
|
-
const
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
});
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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()
|
|
513
|
+
var extract = (svg) => {
|
|
514
|
+
const match = /<metadata\b([^>]*)\bid="dx-payload"([^>]*)>([\s\S]*?)<\/metadata>/.exec(svg);
|
|
515
|
+
if (!match) return;
|
|
516
|
+
const attributes = `${match[1]} ${match[2]}`;
|
|
517
|
+
const encoding = /\bdata-encoding="([^"]*)"/.exec(attributes)?.[1] ?? "json";
|
|
518
|
+
if (encoding !== "json") throw new Error(`Unsupported dx-svg payload encoding: ${encoding}.`);
|
|
519
|
+
return JSON.parse(match[3]);
|
|
520
|
+
};
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/score.ts
|
|
523
|
+
var score_exports = /* @__PURE__ */ __exportAll({
|
|
524
|
+
HALF_COST: () => 3,
|
|
525
|
+
constraintScorer: () => constraintScorer,
|
|
526
|
+
costScorer: () => costScorer,
|
|
527
|
+
evaluate: () => evaluate,
|
|
528
|
+
fromCost: () => fromCost,
|
|
529
|
+
fromObjective: () => fromObjective,
|
|
530
|
+
overall: () => overall
|
|
531
|
+
});
|
|
532
|
+
/** Maps a weighted cost in [0, ∞) onto (0, 1]: 1 at no cost, 0.5 at `half`, falling off hyperbolically. */
|
|
533
|
+
var fromCost = (weighted, half = 3) => 1 / (1 + Math.max(0, weighted) / half);
|
|
534
|
+
/** A hard requirement: 1 when it holds, 0 when anything violates it. */
|
|
535
|
+
var constraintScorer = (constraint) => ({
|
|
536
|
+
id: constraint.id,
|
|
537
|
+
kind: "constraint",
|
|
538
|
+
description: constraint.description,
|
|
539
|
+
evaluate: (layout) => Effect.sync(() => {
|
|
540
|
+
const violations = constraint.violations(layout);
|
|
541
|
+
return {
|
|
542
|
+
score: violations.length ? 0 : 1,
|
|
543
|
+
...violations.length ? { detail: violations.join("\n") } : {}
|
|
2769
544
|
};
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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;
|
|
545
|
+
})
|
|
546
|
+
});
|
|
547
|
+
/** A soft preference: its weighted cost through {@link fromCost}. */
|
|
548
|
+
var costScorer = (term, half = 3) => ({
|
|
549
|
+
id: term.id,
|
|
550
|
+
kind: "cost",
|
|
551
|
+
description: term.description,
|
|
552
|
+
evaluate: (layout) => Effect.sync(() => {
|
|
553
|
+
const value = term.measure(layout);
|
|
554
|
+
const weighted = value * term.weight;
|
|
555
|
+
return {
|
|
556
|
+
score: fromCost(weighted, half),
|
|
557
|
+
detail: `${round(value)} × ${term.weight} = ${round(weighted)}`
|
|
2816
558
|
};
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
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] : []);
|
|
559
|
+
})
|
|
560
|
+
});
|
|
561
|
+
/** Every constraint and cost term of an objective, constraints first. */
|
|
562
|
+
var fromObjective = (objective, half = 3) => [...objective.constraints.map(constraintScorer), ...objective.costs.map((term) => costScorer(term, half))];
|
|
563
|
+
var isBatch = (source) => "entries" in source;
|
|
564
|
+
/** Runs every source against one subject concurrently; results keep the sources' order, batches flattened. */
|
|
565
|
+
var evaluate = (sources, subject) => Effect.all(sources.map((source) => isBatch(source) ? source.evaluate(subject).pipe(Effect.map((results) => source.entries.map((entry, index) => ({
|
|
566
|
+
...entry,
|
|
567
|
+
...results[index]
|
|
568
|
+
})))) : source.evaluate(subject).pipe(Effect.map((result) => [{
|
|
569
|
+
id: source.id,
|
|
570
|
+
kind: source.kind,
|
|
571
|
+
description: source.description,
|
|
572
|
+
...result
|
|
573
|
+
}]))), { concurrency: "unbounded" }).pipe(Effect.map((groups) => groups.flat()));
|
|
2869
574
|
/**
|
|
2870
|
-
*
|
|
2871
|
-
*
|
|
2872
|
-
*
|
|
575
|
+
* One number for a set of scores: the worst constraint gates the mean of everything else, so a
|
|
576
|
+
* layout that breaks a constraint scores 0 however good it otherwise is — the same precedence
|
|
577
|
+
* `Objective.select` gives violations over cost — while every other kind contributes equally.
|
|
578
|
+
* `undefined` when nothing could be judged, so a set of failed scorers never reads as a perfect score.
|
|
2873
579
|
*/
|
|
2874
|
-
var
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
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;
|
|
580
|
+
var overall = (all) => {
|
|
581
|
+
const scores = all.filter(({ error }) => error === void 0);
|
|
582
|
+
if (scores.length === 0) return;
|
|
583
|
+
const gate = Math.min(1, ...scores.filter(({ kind }) => kind === "constraint").map(({ score }) => score));
|
|
584
|
+
const rest = scores.filter(({ kind }) => kind !== "constraint");
|
|
585
|
+
return gate * (rest.length ? rest.reduce((total, { score }) => total + score, 0) / rest.length : 1);
|
|
586
|
+
};
|
|
587
|
+
var round = (value) => Number.isInteger(value) ? `${value}` : value.toFixed(2);
|
|
2938
588
|
//#endregion
|
|
2939
589
|
//#region src/svg-handler.ts
|
|
2940
590
|
/** Discriminates the base `Drawing.Canvas` as SVG-rendered scene content. */
|
|
@@ -3451,8 +1101,8 @@ var elkPlace = async (model, { cell, gapMain, gapCross, direction }) => {
|
|
|
3451
1101
|
*/
|
|
3452
1102
|
var compile$2 = async (source, options = {}) => {
|
|
3453
1103
|
const { origin, scale, maxWidth = 192, engine = "elk" } = options;
|
|
3454
|
-
const model = parse(source);
|
|
3455
|
-
const cell = measureCell
|
|
1104
|
+
const model = parse$1(source);
|
|
1105
|
+
const cell = measureCell(model, {
|
|
3456
1106
|
maxWidth,
|
|
3457
1107
|
cell: options.cell,
|
|
3458
1108
|
titleHeight: options.titleHeight
|
|
@@ -3463,10 +1113,11 @@ var compile$2 = async (source, options = {}) => {
|
|
|
3463
1113
|
gapCross: snap$2(options.gapCross ?? GAP_CROSS),
|
|
3464
1114
|
direction: model.direction
|
|
3465
1115
|
};
|
|
3466
|
-
|
|
1116
|
+
const rects = engine === "dagre" ? dagrePlace(model, place) : await elkPlace(model, place);
|
|
1117
|
+
return emit({
|
|
3467
1118
|
model,
|
|
3468
1119
|
cell,
|
|
3469
|
-
rects
|
|
1120
|
+
rects,
|
|
3470
1121
|
ranks: relationRanks(model)
|
|
3471
1122
|
}, {
|
|
3472
1123
|
origin,
|
|
@@ -3644,7 +1295,7 @@ var packGroups = (model, groups) => {
|
|
|
3644
1295
|
from: groupOf.get(edge.from).id,
|
|
3645
1296
|
to: groupOf.get(edge.to).id
|
|
3646
1297
|
})).filter((edge) => edge.from !== edge.to);
|
|
3647
|
-
const groupRanks = rank
|
|
1298
|
+
const groupRanks = rank(groups.map((group) => group.id), crossEdges.map((edge) => ({
|
|
3648
1299
|
from: edge.to,
|
|
3649
1300
|
to: edge.from
|
|
3650
1301
|
})));
|
|
@@ -3722,8 +1373,8 @@ var compile$1 = (source, options = {}) => {
|
|
|
3722
1373
|
y: 0
|
|
3723
1374
|
}, scale = 1, maxWidth = 192 } = options;
|
|
3724
1375
|
const rules = options.rules ?? [inheritanceTreeRule, linearChainRule];
|
|
3725
|
-
const model = parse(source);
|
|
3726
|
-
const cell = measureCell
|
|
1376
|
+
const model = parse$1(source);
|
|
1377
|
+
const cell = measureCell(model, {
|
|
3727
1378
|
maxWidth,
|
|
3728
1379
|
cell: options.cell,
|
|
3729
1380
|
titleHeight: options.titleHeight
|
|
@@ -3734,7 +1385,7 @@ var compile$1 = (source, options = {}) => {
|
|
|
3734
1385
|
return [...frameCommands(groups, origins, {
|
|
3735
1386
|
origin,
|
|
3736
1387
|
scale
|
|
3737
|
-
}), ...emit
|
|
1388
|
+
}), ...emit({
|
|
3738
1389
|
model,
|
|
3739
1390
|
cell,
|
|
3740
1391
|
rects,
|
|
@@ -3945,8 +1596,8 @@ var compile = (source, options = {}) => {
|
|
|
3945
1596
|
y: 0
|
|
3946
1597
|
}, scale = 1, maxWidth = 192 } = options;
|
|
3947
1598
|
const rules = options.rules ?? [inheritanceTreeRule, linearChainRule];
|
|
3948
|
-
const model = parse(source);
|
|
3949
|
-
const cell = measureCell
|
|
1599
|
+
const model = parse$1(source);
|
|
1600
|
+
const cell = measureCell(model, {
|
|
3950
1601
|
maxWidth,
|
|
3951
1602
|
cell: options.cell,
|
|
3952
1603
|
titleHeight: options.titleHeight
|
|
@@ -3959,7 +1610,7 @@ var compile = (source, options = {}) => {
|
|
|
3959
1610
|
return [...frameCommands(groups, origins, {
|
|
3960
1611
|
origin,
|
|
3961
1612
|
scale
|
|
3962
|
-
}), ...emit
|
|
1613
|
+
}), ...emit({
|
|
3963
1614
|
model,
|
|
3964
1615
|
cell,
|
|
3965
1616
|
rects,
|
|
@@ -3970,6 +1621,6 @@ var compile = (source, options = {}) => {
|
|
|
3970
1621
|
})];
|
|
3971
1622
|
};
|
|
3972
1623
|
//#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 };
|
|
1624
|
+
export { aesthetics_exports as Aesthetics, appeal_exports as Appeal, architecture_exports as Architecture, diagnostics_exports as Diagnostics, DialectRegistry, dsl_exports as Dsl, dx_svg_exports as DxSvg, layout_exports as Layout, mermaid_exports as Mermaid, mermaid_engine_exports as MermaidEngine, nudge_exports as Nudge, objective_exports as Objective, ports_exports as Ports, rules_exports as Rules, SVG_SCHEMA, scene_exports as Scene, score_exports as Score, semantic_exports as Semantic, semantic_engine_exports as SemanticEngine, semantic_place_exports as SemanticPlace, semantic_route_exports as SemanticRoute, 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, view_exports as View, applyCommands, nextIndex, sceneDialect };
|
|
3974
1625
|
|
|
3975
1626
|
//# sourceMappingURL=index.mjs.map
|