@formicoidea/labre-framework-uml 0.42.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.
Files changed (114) hide show
  1. package/LICENSE +384 -0
  2. package/dist/actions.d.ts +430 -0
  3. package/dist/actions.js +724 -0
  4. package/dist/background.d.ts +153 -0
  5. package/dist/background.js +557 -0
  6. package/dist/board-hit.d.ts +137 -0
  7. package/dist/board-hit.js +191 -0
  8. package/dist/commands-manifest.d.ts +19 -0
  9. package/dist/commands-manifest.js +571 -0
  10. package/dist/commands.d.ts +4 -0
  11. package/dist/commands.js +983 -0
  12. package/dist/component.d.ts +521 -0
  13. package/dist/component.js +708 -0
  14. package/dist/consts.d.ts +365 -0
  15. package/dist/consts.js +463 -0
  16. package/dist/descriptor.d.ts +12 -0
  17. package/dist/descriptor.js +10 -0
  18. package/dist/drawio-decode.d.ts +32 -0
  19. package/dist/drawio-decode.js +96 -0
  20. package/dist/drawio-import.d.ts +133 -0
  21. package/dist/drawio-import.js +885 -0
  22. package/dist/edge-morph.d.ts +154 -0
  23. package/dist/edge-morph.js +153 -0
  24. package/dist/edge-styles.d.ts +78 -0
  25. package/dist/edge-styles.js +210 -0
  26. package/dist/effects.d.ts +9 -0
  27. package/dist/effects.js +6 -0
  28. package/dist/element-renderer.d.ts +64 -0
  29. package/dist/element-renderer.js +114 -0
  30. package/dist/element-view.d.ts +234 -0
  31. package/dist/element-view.js +430 -0
  32. package/dist/export.d.ts +61 -0
  33. package/dist/export.js +41 -0
  34. package/dist/filename.d.ts +17 -0
  35. package/dist/filename.js +26 -0
  36. package/dist/frame-tag.d.ts +70 -0
  37. package/dist/frame-tag.js +78 -0
  38. package/dist/grammar.d.ts +695 -0
  39. package/dist/grammar.js +1436 -0
  40. package/dist/import.d.ts +306 -0
  41. package/dist/import.js +1270 -0
  42. package/dist/index.d.ts +47 -0
  43. package/dist/index.js +67 -0
  44. package/dist/interchange.d.ts +125 -0
  45. package/dist/interchange.js +372 -0
  46. package/dist/keywords.d.ts +204 -0
  47. package/dist/keywords.js +361 -0
  48. package/dist/kinds.d.ts +73 -0
  49. package/dist/kinds.js +91 -0
  50. package/dist/legend.d.ts +30 -0
  51. package/dist/legend.js +255 -0
  52. package/dist/model.d.ts +862 -0
  53. package/dist/model.js +1316 -0
  54. package/dist/morph.d.ts +178 -0
  55. package/dist/morph.js +496 -0
  56. package/dist/node/act-glyphs.d.ts +30 -0
  57. package/dist/node/act-glyphs.js +142 -0
  58. package/dist/node/compartment-watcher.d.ts +107 -0
  59. package/dist/node/compartment-watcher.js +163 -0
  60. package/dist/node/fit.d.ts +85 -0
  61. package/dist/node/fit.js +129 -0
  62. package/dist/node/node-renderer.d.ts +49 -0
  63. package/dist/node/node-renderer.js +611 -0
  64. package/dist/node/node-view.d.ts +51 -0
  65. package/dist/node/node-view.js +89 -0
  66. package/dist/node/paint.d.ts +84 -0
  67. package/dist/node/paint.js +143 -0
  68. package/dist/node/sd-glyphs.d.ts +36 -0
  69. package/dist/node/sd-glyphs.js +79 -0
  70. package/dist/node/stm-glyphs.d.ts +22 -0
  71. package/dist/node/stm-glyphs.js +111 -0
  72. package/dist/node/tier-metrics.d.ts +38 -0
  73. package/dist/node/tier-metrics.js +45 -0
  74. package/dist/operators.d.ts +45 -0
  75. package/dist/operators.js +75 -0
  76. package/dist/plantuml-import.d.ts +113 -0
  77. package/dist/plantuml-import.js +1723 -0
  78. package/dist/plantuml.d.ts +94 -0
  79. package/dist/plantuml.js +985 -0
  80. package/dist/presets.d.ts +149 -0
  81. package/dist/presets.js +302 -0
  82. package/dist/profiles.d.ts +2 -0
  83. package/dist/profiles.js +358 -0
  84. package/dist/reading.d.ts +160 -0
  85. package/dist/reading.js +648 -0
  86. package/dist/roles.d.ts +257 -0
  87. package/dist/roles.js +997 -0
  88. package/dist/rules.d.ts +267 -0
  89. package/dist/rules.js +3090 -0
  90. package/dist/templates.d.ts +2 -0
  91. package/dist/templates.js +197 -0
  92. package/dist/toolbar/config.d.ts +228 -0
  93. package/dist/toolbar/config.js +833 -0
  94. package/dist/toolbar/icons.d.ts +440 -0
  95. package/dist/toolbar/icons.js +711 -0
  96. package/dist/toolbar/senior-tool.d.ts +11 -0
  97. package/dist/toolbar/senior-tool.js +21 -0
  98. package/dist/toolbar/uml-menu.d.ts +17 -0
  99. package/dist/toolbar/uml-menu.js +20 -0
  100. package/dist/toolbar/uml-senior-button.d.ts +18 -0
  101. package/dist/toolbar/uml-senior-button.js +22 -0
  102. package/dist/translations.d.ts +27 -0
  103. package/dist/translations.js +117 -0
  104. package/dist/view.d.ts +33 -0
  105. package/dist/view.js +236 -0
  106. package/dist/xmi-import.d.ts +174 -0
  107. package/dist/xmi-import.js +2268 -0
  108. package/dist/xmi.d.ts +61 -0
  109. package/dist/xmi.js +1860 -0
  110. package/dist/xml-reader.d.ts +157 -0
  111. package/dist/xml-reader.js +430 -0
  112. package/dist/xml.d.ts +72 -0
  113. package/dist/xml.js +96 -0
  114. package/package.json +38 -0
@@ -0,0 +1,724 @@
1
+ import { createBoardLegend, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
+ import { ConnectorMode, TextAlign, UmlDiagramElementModel, } from '@formicoidea/labre-core/model';
4
+ import { translateKey } from '@formicoidea/labre-core/shared/services';
5
+ import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
6
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
7
+ import { GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
8
+ import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
9
+ import { UML_BODY_FONT_SIZE, UML_DIAGRAM_BOX, UML_EDGE_WIDTH, UML_FRAGMENT_BOX, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_PARTITION_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
10
+ import { decodeDrawio } from './drawio-decode.js';
11
+ import { UML_EDGE_STYLE } from './edge-styles.js';
12
+ import { umlSafeFilename } from './filename.js';
13
+ import { UML_DRAWIO_IMPORT, UML_PLANTUML_EXPORT, UML_PLANTUML_IMPORT, UML_XMI_EXPORT, UML_XMI_IMPORT, } from './interchange.js';
14
+ import { UML_ATTRIBUTES_SEED, UML_ATTRIBUTES_SEED_KEY, UML_NAME_SEED, UML_OPERATIONS_SEED, UML_OPERATIONS_SEED_KEY, UML_SLOTS_SEED, UML_SLOTS_SEED_KEY, UML_UNLABELLED_KINDS, umlSeedKey, } from './keywords.js';
15
+ import { umlNodeProps, umlTextProps } from './presets.js';
16
+ import { UML_ROLE, umlDiagramRoleKey, umlRoleKey, umlSubjectRoleKey, } from './roles.js';
17
+ /**
18
+ * Standalone creation/activation actions for the UML toolbox — the same shape
19
+ * BPMN's and C4's `actions.ts` have, and for the same reason: the menu is a pure
20
+ * renderer over the command registry, so what a button DOES lives here and
21
+ * telemetry is emitted once, by `runCommand` (R29). Nothing in this file emits.
22
+ */
23
+ const gfxOf = (std) => std.get(GfxControllerIdentifier);
24
+ function finish(gfx, id) {
25
+ gfx.doc.captureSync();
26
+ gfx.tool.setTool(DefaultTool);
27
+ gfx.selection.set({ elements: [id], editing: false });
28
+ // Keep the palette open (native sub-menu behaviour).
29
+ }
30
+ /** One compartment of a component, as a canvas TEXT element. */
31
+ function addTier(surface, index, role, text, box, options) {
32
+ return surface.addElement({
33
+ ...umlTextProps(role, box, options),
34
+ text,
35
+ index,
36
+ });
37
+ }
38
+ /* ── The sheet and the frame on it ─────────────────────────────────────── */
39
+ /**
40
+ * Create a UML diagram frame centred on the viewport — the sheet one diagram is
41
+ * drawn on (Annex A).
42
+ *
43
+ * The KIND is written here and it is not optional: Annex A's heading is
44
+ * `<kind> <name>`, so a frame with no kind has nothing to write in its tag. It
45
+ * defaults to `class`, which is the diagram an architect reaches for first and
46
+ * the only one Annex A gives no abbreviation for — and the kind picker on the
47
+ * frame's own toolbar changes it afterwards.
48
+ *
49
+ * The NAME is seeded through the translation seam rather than left to the
50
+ * model's default, so a diagram drawn in a French host does not start out called
51
+ * "Diagram" (#183). The key is the diagram ROLE's: the two are the same noun,
52
+ * and a second key would ask a host to word it twice. It is content from that
53
+ * moment on — the heading is editable on a double-click — so it is asked for
54
+ * once and never re-resolved.
55
+ */
56
+ export function createUmlDiagram(std, kind = 'class') {
57
+ const gfx = gfxOf(std);
58
+ const surface = gfx.surface;
59
+ if (!surface)
60
+ return;
61
+ const { w, h } = UML_DIAGRAM_BOX;
62
+ const { centerX: cx, centerY: cy } = gfx.viewport;
63
+ const id = surface.addElement({
64
+ type: 'umlDiagram',
65
+ // The FRAME the elements are drawn on, and a role of its own: a rule written
66
+ // on the artefacts must never fall on the sheet holding them.
67
+ role: UML_ROLE.diagram,
68
+ name: translateKey(std, umlDiagramRoleKey, 'Diagram'),
69
+ kind,
70
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
71
+ });
72
+ finish(gfx, id);
73
+ }
74
+ /**
75
+ * Create a use case SUBJECT centred on the viewport (§18.1.4) — the rectangle
76
+ * drawn around the use cases a system offers, with the actors left outside it.
77
+ *
78
+ * No variant and no kind, unlike a C4 boundary: §18.1.4 draws ONE rectangle with
79
+ * ONE name, so there is nothing for a discriminant to discriminate.
80
+ */
81
+ export function createUmlSubject(std) {
82
+ const gfx = gfxOf(std);
83
+ const surface = gfx.surface;
84
+ if (!surface)
85
+ return;
86
+ const { w, h } = UML_SUBJECT_BOX;
87
+ const { centerX: cx, centerY: cy } = gfx.viewport;
88
+ const id = surface.addElement({
89
+ type: 'umlSubject',
90
+ role: UML_ROLE.subject,
91
+ name: translateKey(std, umlSubjectRoleKey, 'Subject'),
92
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
93
+ });
94
+ finish(gfx, id);
95
+ }
96
+ /**
97
+ * The two behaviour BACKGROUNDS, named through the same seam the subject is.
98
+ *
99
+ * `umlRoleKey` rather than a `umlPartitionRoleKey` export, and it is the same
100
+ * string either way (`roles.ts` derives both from the role's local name): a
101
+ * default name is asked for ONCE, at creation, and it is the author's content
102
+ * from that moment on — so the call site needs the key and nothing else.
103
+ */
104
+ const umlPartitionRoleKey = umlRoleKey('partition');
105
+ const umlRegionRoleKey = umlRoleKey('region');
106
+ /**
107
+ * Create an activity PARTITION — a swimlane (§15.6.4), the band an activity
108
+ * diagram draws round the steps one participant is responsible for.
109
+ *
110
+ * ## Why a background and not a field on the frame
111
+ *
112
+ * Because membership is GEOMETRIC and partitions nest and overlap: §15.6.4 lets
113
+ * a diagram be cut vertically by role and horizontally by phase at the same
114
+ * time, and an action belongs to whichever bands its centre falls in. A list of
115
+ * lane names on `umlDiagram` could express neither, and adding a field to a
116
+ * shipped model is the red zone `docs/adr/0009` guards. So a partition is an
117
+ * element like the subject beside it, read at audit and export time by centre
118
+ * containment (R11).
119
+ *
120
+ * ## VERTICAL, and it is a default rather than a preference
121
+ *
122
+ * §15.6.4's own figures are drawn as vertical columns, which is also the way a
123
+ * left-to-right flow reads: the steps travel across the lanes rather than down
124
+ * them. The model's own default says so, and this site deliberately writes no
125
+ * `orientation` at all — a creation that restated the default would be a second
126
+ * place for it to be changed, and the band's own toolbar is where an author
127
+ * turns a column into a row.
128
+ */
129
+ export function createUmlPartition(std) {
130
+ const gfx = gfxOf(std);
131
+ const surface = gfx.surface;
132
+ if (!surface)
133
+ return;
134
+ const { w, h } = UML_PARTITION_BOX;
135
+ const { centerX: cx, centerY: cy } = gfx.viewport;
136
+ const id = surface.addElement({
137
+ type: 'umlPartition',
138
+ role: UML_ROLE.partition,
139
+ name: translateKey(std, umlPartitionRoleKey, 'Partition'),
140
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
141
+ });
142
+ finish(gfx, id);
143
+ }
144
+ /**
145
+ * Create a REGION — the rounded, named rectangle a composite state is drawn as
146
+ * (§14.2.4), and the container its sub-states sit inside.
147
+ *
148
+ * A composite state IS a region here rather than a `state` node holding
149
+ * regions, and the simplification is deliberate and has a ceiling worth
150
+ * stating: §14.2.4 draws a composite state as a rounded rectangle with its name
151
+ * in a band at the top and its sub-machine below, which is exactly this
152
+ * element. What it does NOT give is a state with two or more ORTHOGONAL regions
153
+ * separated by dashed lines — that is one state drawn as several boxes, and it
154
+ * is a phase-3 refinement rather than something this element can be stretched
155
+ * into.
156
+ */
157
+ export function createUmlRegion(std) {
158
+ const gfx = gfxOf(std);
159
+ const surface = gfx.surface;
160
+ if (!surface)
161
+ return;
162
+ const { w, h } = UML_REGION_BOX;
163
+ const { centerX: cx, centerY: cy } = gfx.viewport;
164
+ const id = surface.addElement({
165
+ type: 'umlRegion',
166
+ role: UML_ROLE.region,
167
+ name: translateKey(std, umlRegionRoleKey, 'Region'),
168
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
169
+ });
170
+ finish(gfx, id);
171
+ }
172
+ /* ── Phase 3: the combined fragment (§17.6.4) and the interaction use ───── */
173
+ /**
174
+ * Create a COMBINED FRAGMENT — the box §17.6.4 draws round the part of an
175
+ * interaction that only happens sometimes, or happens repeatedly, or happens in
176
+ * parallel with something else.
177
+ *
178
+ * ## Why the operator is a parameter and the name is not a title
179
+ *
180
+ * §17.6.4 draws the fragment as a rectangle with a pentagon tag in its
181
+ * top-left corner holding the INTERACTION OPERATOR — `alt`, `opt`, `loop`,
182
+ * `par` — and that word is what the frame IS. It is written here rather than
183
+ * left to the model's default for the same reason `createUmlDiagram` writes a
184
+ * kind: the tag has to have something in it from the first frame the author
185
+ * draws, and the picker on the fragment's own row is what changes it
186
+ * afterwards.
187
+ *
188
+ * ## Nothing is NAMED, which is the one place this frame differs from the other
189
+ * three
190
+ *
191
+ * A diagram, a subject, a partition and a region are all seeded with a default
192
+ * name through the translation seam. This one is not, and the model says why in
193
+ * as many words: its `name` is the GUARD of the first operand — §17.6.4's
194
+ * `[condition]` — or, under `ref`, the name of the interaction being referred
195
+ * to (§17.7.4), and the notation draws a guard only where there is one. An
196
+ * `alt` born carrying `[condition]` would be a condition the author never
197
+ * wrote, sitting on the picture until somebody noticed and deleted it. So the
198
+ * field is left at the model's empty default and the author writes the guard
199
+ * into the band on a double-click, exactly as a state's behaviour compartment
200
+ * is seeded empty for the same reason (§14.2.4).
201
+ *
202
+ * ## One frame, no operands
203
+ *
204
+ * A fresh fragment has ONE zone and therefore no `operands` at all — the same
205
+ * promise a BPMN pool makes about its lanes, and the same bytes: nothing is
206
+ * written until the author asks for a second operand from the row, and the
207
+ * dashed separator §17.6.4 draws appears with it. An `alt` with one branch is
208
+ * the honest first draft of an `alt`, and a stencil that seeded two would be
209
+ * putting a branch on the sheet nobody asked for.
210
+ */
211
+ export function createUmlFragment(std, operator = 'alt') {
212
+ const gfx = gfxOf(std);
213
+ const surface = gfx.surface;
214
+ if (!surface)
215
+ return;
216
+ const { w, h } = UML_FRAGMENT_BOX;
217
+ const { centerX: cx, centerY: cy } = gfx.viewport;
218
+ const id = surface.addElement({
219
+ type: 'umlFragment',
220
+ role: UML_ROLE.fragment,
221
+ operator,
222
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
223
+ });
224
+ finish(gfx, id);
225
+ }
226
+ /**
227
+ * Create an INTERACTION USE — §17.7.4's `ref` frame, the box that says "and
228
+ * here the sequence described on another sheet happens".
229
+ *
230
+ * The same element as {@link createUmlFragment} with a different operator, and
231
+ * that is §17.7.4's own doing rather than an economy: an InteractionUse is
232
+ * drawn as the very rectangle a combined fragment is, with `ref` in the
233
+ * pentagon instead of `alt`, and the name of the referenced interaction in the
234
+ * fragment's own `name` — which this pack anchors where every other guard is
235
+ * written, at the top-left of the plot, rather than centred as §17.7.4's figure
236
+ * draws it. Giving it an element of its own would mean two things to
237
+ * select, two rows, two sets of rules and two exporters for one picture.
238
+ *
239
+ * It has a COMMAND of its own all the same, because it is a different modelling
240
+ * act: an author reaching for `ref` is pointing at another diagram, not
241
+ * branching this one, and making them draw an `alt` and then change the
242
+ * operator would be hiding a gesture behind a dropdown.
243
+ */
244
+ export function createUmlInteractionUse(std) {
245
+ createUmlFragment(std, 'ref');
246
+ }
247
+ /**
248
+ * The glyph kinds whose one tier is a `uml:name` rather than a `uml:label`.
249
+ *
250
+ * The line between the two is drawn by `roles.ts` and it is not about having a
251
+ * rectangle: `uml:name` is the tier a KEYWORD may be written on and the grammar
252
+ * reads back, `uml:label` is a name and nothing else. So:
253
+ *
254
+ * - a **package** name is a namespace, parseable and qualified (§12.2.4), and a
255
+ * **note**'s text is what the exporter writes as the comment's body;
256
+ * - the three **deployment cubes** are named INSIDE their front face, over a
257
+ * keyword line — `«device»`, `«executionEnvironment»` (§19.4.4) — which is
258
+ * exactly what a name compartment is for. It is also load-bearing rather than
259
+ * tidy: the morph's `afterMorph` rewrites `uml:name` and only that, so a cube
260
+ * labelled instead of named would morph into a device that never said so.
261
+ *
262
+ * Everything else here carries a LABEL: a port's word sits beside a 16-unit
263
+ * square and the two interface marks' beside a ball and a socket, none of them
264
+ * inside anything, and none of them ever carrying a keyword.
265
+ */
266
+ const NAMED_GLYPHS = new Set([
267
+ 'package',
268
+ 'note',
269
+ 'node',
270
+ 'device',
271
+ 'execution-environment',
272
+ ]);
273
+ /**
274
+ * Where the viewport centre puts a node of this kind.
275
+ *
276
+ * Shared by the two creation functions so a class and a use case are centred by
277
+ * the same arithmetic — the geometry is the only thing the two gestures have in
278
+ * common, and it is worth not writing twice.
279
+ */
280
+ function centredBox(gfx, kind) {
281
+ const { w, h } = UML_NODE_BOX[kind];
282
+ const { centerX: cx, centerY: cy } = gfx.viewport;
283
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
284
+ }
285
+ /**
286
+ * Create one of the SINGLE-LABEL artefacts — a package, a note, an actor, a use
287
+ * case, and since phase 2 a port, the two interface marks and the three
288
+ * deployment cubes — as the shape, its one word-bearing child, and the group
289
+ * that makes the two one thing.
290
+ *
291
+ * ## Why three elements and not one shape carrying text
292
+ *
293
+ * R16: a label is a free text element grouped with the shape, never text ON the
294
+ * shape. The pack has to obey it here even though these have only one tier,
295
+ * because a classifier next door has three — and two mechanisms for "the words
296
+ * on a UML artefact" would mean two editors, two toolbars and two sets of rules
297
+ * for the same gesture. One click selects the artefact, a second descends into
298
+ * its label.
299
+ *
300
+ * ## `uml:name` for five of them, `uml:label` for the rest
301
+ *
302
+ * See {@link NAMED_GLYPHS}: a package, a note and the three cubes carry a NAME
303
+ * the grammar parses a keyword off; a port, a ball and a socket carry a LABEL.
304
+ * Keeping them apart is what lets the exporter read a tier without first asking
305
+ * what shape it is under.
306
+ *
307
+ * ## The label is where the notation puts it, and that is not this file's call
308
+ *
309
+ * The box comes out of {@link umlCompartmentBoxes}, which is where every
310
+ * "beside the glyph" and "inside the front face" decision is written down: a
311
+ * port's label sits next to the square rather than on it (the square is 16
312
+ * units, and words do not fit in it), an interface mark's sits beside the ball
313
+ * or the socket, and a cube's sits in its FRONT FACE, which is the only part of
314
+ * a 3D box §19.4.4 writes in. Creation reads the answer; it does not have one.
315
+ *
316
+ * ## The alignments
317
+ *
318
+ * Centred, except the NOTE. A note holds a sentence rather than a title (Annex
319
+ * A), and a centred paragraph of three lines is the one thing on a UML diagram
320
+ * that reads as a poem.
321
+ *
322
+ * ## The kinds that get no words at all, and no group either
323
+ *
324
+ * Phase 2's behaviour vocabulary is the first in this pack with artefacts that
325
+ * carry NOTHING: a filled disc, a bullseye, a bar, a cross. §15.3.4 and §14.2.4
326
+ * name none of them — an initial node has no name, a fork has no name, a
327
+ * history mark is an `H` and a final state is a target — and a stencil that
328
+ * seeded one would be putting a word on the picture that the notation says is
329
+ * not there, which the `label-presence` audits would then be right to complain
330
+ * about forever.
331
+ *
332
+ * {@link UML_UNLABELLED_KINDS} is that list, and creation READS it rather than
333
+ * restating it: those kinds arrive as the shape and nothing else — no text, and
334
+ * therefore no group, because a group of one element is a wrapper round nothing
335
+ * that a user would have to descend through to reach the mark it holds. The
336
+ * SHAPE is what the gesture produced, so the shape is what is selected.
337
+ *
338
+ * Every kind that IS labelled here takes exactly one tier, and the one that
339
+ * would have been the exception does not travel this path at all: a **state**
340
+ * is a divided box (§14.2.4) and is built by {@link createUmlClassifier}, whose
341
+ * walk gives it the name compartment and the behaviour compartment the renderer
342
+ * rules off and the exporter reads.
343
+ */
344
+ export function createUmlNode(std, kind) {
345
+ const gfx = gfxOf(std);
346
+ const surface = gfx.surface;
347
+ if (!surface)
348
+ return;
349
+ const { x, y, w, h } = centredBox(gfx, kind);
350
+ const shapeId = surface.addElement({
351
+ // Every prop the kind is worth, from the ONE table the morph also reads
352
+ // (`presets.ts`). NO `text`: the words are the child below, and the shape is
353
+ // a body and nothing else.
354
+ ...umlNodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
355
+ index: gfx.layer.generateIndex(),
356
+ });
357
+ // The marks the notation draws without a word on them: the shape IS the
358
+ // artefact, so there is nothing to group it with and nothing to seed.
359
+ if (UML_UNLABELLED_KINDS.has(kind)) {
360
+ finish(gfx, shapeId);
361
+ return;
362
+ }
363
+ const { name: box } = umlCompartmentBoxes(kind, x, y, w, h);
364
+ const isLabel = !NAMED_GLYPHS.has(kind);
365
+ const textId = addTier(surface, gfx.layer.generateIndex(),
366
+ // WHICH label role is the kind's own answer, not this file's: a lifeline's
367
+ // head carries `uml:lifeline-ident` and every other single word carries
368
+ // `uml:label` (`component.ts`, {@link umlLabelRoleOf}).
369
+ isLabel ? umlLabelRoleOf(kind) : UML_ROLE.name,
370
+ // Through the SEAM, like the frames above: the words a gesture writes are
371
+ // persisted, so a host that has a catalogue must be asked before they land
372
+ // — a package dropped in a French editor is never called "Package"
373
+ // afterwards (R30). Resolved once, here; content from that moment on.
374
+ translateKey(std, umlSeedKey(kind), UML_NAME_SEED[kind]), box, {
375
+ fontSize: UML_NAME_FONT_SIZE,
376
+ align: kind === 'note' ? TextAlign.Left : TextAlign.Center,
377
+ });
378
+ // The group LAST, so it is written after everything it holds: the elements are
379
+ // created in painting order — the shape first and the words above it — and the
380
+ // group's own `xywh` is derived from its children.
381
+ const groupId = surface.addElement({
382
+ type: 'group',
383
+ index: gfx.layer.generateIndex(),
384
+ // A plain record is a legal `children`: the group's own `propsToY` takes the
385
+ // KEYS and forces every value to `true`.
386
+ children: { [shapeId]: true, [textId]: true },
387
+ // No title, deliberately. The group renderer paints one only while the
388
+ // artefact is selected, and a package announcing itself as "Group 3" above
389
+ // its own name is a label nobody wrote.
390
+ });
391
+ finish(gfx, groupId);
392
+ }
393
+ /**
394
+ * What the SECOND compartment says on a fresh artefact, where it is not the
395
+ * §9.5.4 attribute line every classifier gets.
396
+ *
397
+ * Two exceptions and a default, rather than three entries, because the default
398
+ * is the rule: `+ attribute : Type` is what a divided rectangle's body tier
399
+ * holds unless the kind's own clause says otherwise.
400
+ *
401
+ * - an **object**'s tier holds SLOTS (§9.8.4) — `attribute = value`, an
402
+ * instance giving its classifier's features values rather than redeclaring
403
+ * them;
404
+ * - a **state**'s holds INTERNAL ACTIVITIES (§14.2.4.4) and is seeded EMPTY.
405
+ * That is the one place in this pack where a blank tier is the right
406
+ * stencil, and it is the exporter that decides it: `model.ts` reads
407
+ * `entry / …`, `do / …` and `exit / …` off these lines and treats a label
408
+ * with nothing after it as no behaviour at all, so a seeded `entry / ` would
409
+ * be a prompt that reads as content and exports as nothing. §14.2.4's own
410
+ * figures draw exactly this — a named state with a ruled-off compartment
411
+ * waiting to be filled — and the author types the first line the grammar
412
+ * then reads back.
413
+ *
414
+ * Each is paired with the key it is the English default of, and the state's is
415
+ * `null` for the reason an unlabelled kind contributes none: there is no word
416
+ * to translate.
417
+ */
418
+ const BODY_SEED = {
419
+ object: [UML_SLOTS_SEED_KEY, UML_SLOTS_SEED],
420
+ state: [null, ''],
421
+ };
422
+ /**
423
+ * Create one of the COMPARTMENTED artefacts — a class, an interface, an
424
+ * enumeration, an object, and since phase 2 a component, an artifact or a
425
+ * state — as the shape, its compartments, and the group.
426
+ *
427
+ * ## How many tiers is the LAYOUT's answer, not this file's
428
+ *
429
+ * The walk below writes a tier for each box `umlCompartmentBoxes` returns and
430
+ * for no other, which is what lets two kinds be added to the notation without a
431
+ * line changing here: a component and an artifact arrive with the name tier and
432
+ * the one body tier §11.6.4 and §19.3.4 draw them with, because that is what the
433
+ * layout hands over. The alternative — a table of tier counts in this file —
434
+ * would be a second statement of the geometry, and the first one to drift.
435
+ *
436
+ * ## Four or five elements, and the object is the four
437
+ *
438
+ * A class, an interface and an enumeration arrive as shape + name + attributes +
439
+ * operations + group. An OBJECT arrives with no operations compartment at all,
440
+ * and that is §9.8.4 rather than an economy: an instance specification has
441
+ * SLOTS — values its attributes hold — and no behaviour of its own. Its second
442
+ * tier is stamped `uml:attributes` all the same, because a slot is what an
443
+ * attribute is worth on an instance, and giving it a fifth role would make every
444
+ * reader of the vocabulary handle two spellings of one idea.
445
+ *
446
+ * ## Seeded, not empty
447
+ *
448
+ * Every tier carries the stencil's own prompt from the moment the artefact is
449
+ * drawn: the kind's name line (`«interface»\nInterface` for an interface —
450
+ * §9.5.4's keyword, which is what tells the reader it is not a class),
451
+ * `+ attribute : Type`, `+ operation() : Type`. The author meets a worked
452
+ * example of the notation rather than an empty box, and the grammar can be read
453
+ * back off it immediately.
454
+ *
455
+ * ## The name is the one bold tier, and the features are read LEFT
456
+ *
457
+ * A compartment of attributes is a LIST of signatures, and a list is read down
458
+ * its left edge: centring `+ attribute : Type` over `+ operation() : Type` would
459
+ * put the `+` of every line in a different column and cost the reader the one
460
+ * visual cue UML gives for visibility (§9.5.4).
461
+ */
462
+ export function createUmlClassifier(std, kind) {
463
+ const gfx = gfxOf(std);
464
+ const surface = gfx.surface;
465
+ if (!surface)
466
+ return;
467
+ const { x, y, w, h } = centredBox(gfx, kind);
468
+ const shapeId = surface.addElement({
469
+ ...umlNodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
470
+ index: gfx.layer.generateIndex(),
471
+ });
472
+ const boxes = umlCompartmentBoxes(kind, x, y, w, h);
473
+ const children = { [shapeId]: true };
474
+ children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.name,
475
+ // Through the seam, exactly as {@link createUmlNode} writes its one tier.
476
+ translateKey(std, umlSeedKey(kind), UML_NAME_SEED[kind]), boxes.name, {
477
+ fontSize: UML_NAME_FONT_SIZE,
478
+ align: TextAlign.Center,
479
+ bold: true,
480
+ })] = true;
481
+ if (boxes.attributes) {
482
+ const [bodyKey, bodyText] = BODY_SEED[kind] ?? [
483
+ UML_ATTRIBUTES_SEED_KEY,
484
+ UML_ATTRIBUTES_SEED,
485
+ ];
486
+ children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.attributes, bodyKey ? translateKey(std, bodyKey, bodyText) : bodyText, boxes.attributes, { fontSize: UML_BODY_FONT_SIZE, align: TextAlign.Left })] = true;
487
+ }
488
+ if (boxes.operations) {
489
+ children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.operations, translateKey(std, UML_OPERATIONS_SEED_KEY, UML_OPERATIONS_SEED), boxes.operations, { fontSize: UML_BODY_FONT_SIZE, align: TextAlign.Left })] = true;
490
+ }
491
+ const groupId = surface.addElement({
492
+ type: 'group',
493
+ index: gfx.layer.generateIndex(),
494
+ children,
495
+ });
496
+ finish(gfx, groupId);
497
+ }
498
+ /**
499
+ * Arm the native connector tool, pre-styled for one UML relationship.
500
+ *
501
+ * ## One function for every edge, where C4 and BPMN wrote one each
502
+ *
503
+ * Because UML has twelve of them and they differ on three props out of a table
504
+ * that is otherwise identical — the same mode, the same ink, the same weight. A
505
+ * dozen near-identical functions would be a dozen places for a stroke width to
506
+ * drift, and {@link UML_EDGE_STYLE} is the notation itself, which is the thing
507
+ * worth reading. It is also what made phase 2 free: `deploy`, `manifest` and
508
+ * `communication-path` arm through this function without a line changing in it.
509
+ *
510
+ * ## STRAIGHT, always
511
+ *
512
+ * A class diagram is a graph, not a process laid out in lanes: the elbows an
513
+ * orthogonal router adds would read as a route through the diagram that nobody
514
+ * drew. BPMN routes orthogonally because a sequence flow really does travel
515
+ * through a layout; UML's lines state a relationship between two classifiers and
516
+ * the shortest one says it best.
517
+ *
518
+ * The role is carried by the TOOL, so the connector is born with it rather than
519
+ * acquiring one afterwards (`docs/adr/0010`) — and the look rides on the
520
+ * activation, never through the last-props store: the plain connector tool must
521
+ * keep the user's own style (#144 M1).
522
+ */
523
+ export function activateUmlEdge(std, role) {
524
+ gfxOf(std).tool.setTool(ConnectorTool, {
525
+ mode: ConnectorMode.Straight,
526
+ role: UML_ROLE[role],
527
+ style: {
528
+ stroke: UML_INK,
529
+ strokeWidth: UML_EDGE_WIDTH,
530
+ ...UML_EDGE_STYLE[role],
531
+ },
532
+ });
533
+ // Keep the palette open (native sub-menu behaviour).
534
+ }
535
+ /* ── The legend ────────────────────────────────────────────────────────── */
536
+ /**
537
+ * The diagram frames of the current selection, on an EDITABLE document.
538
+ *
539
+ * The read-only filter is here rather than delegated to the caller because it
540
+ * belongs to what this list is FOR: generating a legend writes elements onto the
541
+ * canvas, so it is offered only where writing is possible. The export's own
542
+ * {@link umlDiagramsForExport} deliberately has no such filter — reading a
543
+ * diagram and handing the reader a file is exactly what a published, read-only
544
+ * document is for.
545
+ */
546
+ export function umlDiagramsSelected(std) {
547
+ if (std.store.readonly)
548
+ return [];
549
+ return gfxOf(std).selection.selectedElements.filter((model) => model instanceof UmlDiagramElementModel);
550
+ }
551
+ /**
552
+ * Draw the legend of what is actually on the selected frame, bottom-left of it.
553
+ *
554
+ * The FIRST selected frame and no other: a legend is placed relative to one
555
+ * background, and two of them would put two boxes on top of whatever sits in
556
+ * that corner. Everything about the gesture — the scan, the placement, the box —
557
+ * is `createBoardLegend`'s; UML contributes nothing but the rows its own
558
+ * commands subscribe (`commands.ts`, `docs/adr/0026`).
559
+ *
560
+ * The one action in this file with no command behind it, exactly as in C4: the
561
+ * legend is reached from the selected frame's contextual toolbar and from
562
+ * nowhere else (PO arbitration, 27/08/2026 — see `toolbar/config.ts`). It is a
563
+ * legend OF something you have in front of you, not an artefact to pick off a
564
+ * palette. Kept here beside its siblings all the same, because it is the same
565
+ * kind of thing — a gesture that writes elements — and because a unit test can
566
+ * drive it without a toolbar.
567
+ *
568
+ * A legend earns its place in UML more than in most packs: the notation's dozen
569
+ * relationships differ by a diamond's fill and a triangle's outline, and a
570
+ * reader who is not fluent in §11.5.4 cannot tell a shared aggregation from a
571
+ * composition without a key on the sheet.
572
+ */
573
+ export function createUmlLegend(std) {
574
+ const diagram = umlDiagramsSelected(std)[0];
575
+ if (!diagram)
576
+ return;
577
+ createBoardLegend(std, diagram, 'uml');
578
+ }
579
+ /* ── Export (PlantUML, XMI 2.5.1) ──────────────────────────────────────── */
580
+ /**
581
+ * The diagrams the export speaks about: the SELECTED ones, or every diagram on
582
+ * the surface when nothing is selected.
583
+ *
584
+ * ## Why the fallback, where C4 has none
585
+ *
586
+ * `c4BoardsForExport` returns the selection and stops there, because a C4 board
587
+ * is one LEVEL of one model and merging three of them produces the picture C4
588
+ * exists to stop people drawing. UML is the opposite object: ADR 0017 ships the
589
+ * four diagram kinds as ONE framework precisely because they are views of one
590
+ * model — the classes in a class diagram are the classifiers the objects in the
591
+ * object diagram instantiate — and both target formats are built to hold all of
592
+ * them. An XMI `uml:Model` is a single model element holding every packaged
593
+ * element; a `.puml` file is one document. So a document with four frames on it
594
+ * and nothing selected exports as the model it is.
595
+ *
596
+ * Selecting a frame narrows it, which is the gesture an author makes when they
597
+ * want one diagram out of a page of them.
598
+ *
599
+ * No read-only filter: an export WRITES nothing. A diagram published read-only
600
+ * is precisely the one somebody wants to take away.
601
+ */
602
+ export function umlDiagramsForExport(std) {
603
+ const gfx = gfxOf(std);
604
+ const selected = gfx.selection.selectedElements.filter((model) => model instanceof UmlDiagramElementModel);
605
+ if (selected.length > 0)
606
+ return selected;
607
+ return (gfx.surface?.elementModels ?? []).filter((model) => model instanceof UmlDiagramElementModel);
608
+ }
609
+ /**
610
+ * The elements the export speaks about, as ONE list the declared capability
611
+ * takes: the diagrams in scope, then everything else on the surface in document
612
+ * order.
613
+ *
614
+ * The scope is expressed by which DIAGRAMS are in the list — that is the
615
+ * capability's contract for this framework (`interchange.ts`) — so an unselected
616
+ * frame is the one thing left out, and the exporter's own geometric attribution
617
+ * reads back exactly what {@link umlDiagramsForExport} says.
618
+ *
619
+ * Document order matters and is preserved for the same reason it does in BPMN
620
+ * and C4: it is the tie-break attribution breaks on — a centre inside two
621
+ * overlapping subjects goes to the first — and sorting here would make the file
622
+ * disagree with the badge the user can see.
623
+ *
624
+ * The compartment texts and the groups that say whose words they are ride along
625
+ * untouched: a tier belongs to its node through the GROUP rather than through
626
+ * the geometry, so both lists cross the seam whole and unfiltered.
627
+ */
628
+ export function umlExportElementsOf(std) {
629
+ const elements = gfxOf(std).surface?.elementModels ?? [];
630
+ return [
631
+ ...umlDiagramsForExport(std),
632
+ ...elements.filter(element => !(element instanceof UmlDiagramElementModel)),
633
+ ];
634
+ }
635
+ /**
636
+ * What the downloaded file is called, minus the extension.
637
+ *
638
+ * The document's own title first — a diagram is what the file is OF — then the
639
+ * name of the frame whose toolbar launched the export, then a last resort.
640
+ * Making the answer safe to write to disk is {@link umlSafeFilename}'s job, so
641
+ * the command and the interchange capability cannot name the same diagram
642
+ * differently.
643
+ */
644
+ export function umlExportFilename(std) {
645
+ const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
646
+ const diagram = umlDiagramsForExport(std)[0]?.name;
647
+ return umlSafeFilename(title || diagram);
648
+ }
649
+ /**
650
+ * Serialize the diagrams in scope as PlantUML and hand the file to the browser.
651
+ *
652
+ * Three steps, and only the first and the last know what an editor is: read the
653
+ * surface, run the DECLARED capability (`docs/adr/0012`), download what it
654
+ * produced. The middle step is not re-implemented here — the document, the
655
+ * filename and the content type all come out of `UML_PLANTUML_EXPORT.run`, so
656
+ * the command and the registry cannot describe the same diagram differently.
657
+ * There is one door; the registry is the label on it.
658
+ */
659
+ export function exportUmlPlantumlFile(std) {
660
+ const { text, filename, mime } = UML_PLANTUML_EXPORT.run(umlExportElementsOf(std), { name: umlExportFilename(std) });
661
+ downloadBlob(new Blob([text], { type: mime }), filename);
662
+ }
663
+ /**
664
+ * Serialize the diagrams in scope as XMI 2.5.1 and hand the file to the browser.
665
+ *
666
+ * The same three steps as {@link exportUmlPlantumlFile}, against the other
667
+ * capability. Two formats because they answer different questions: PlantUML is
668
+ * what a reader diffs and commits next to the code, XMI is what another UML tool
669
+ * reads back — the OMG's own interchange format, and the reason this pack
670
+ * implements 2.5.1 rather than a dialect.
671
+ */
672
+ export function exportUmlXmiFile(std) {
673
+ const { text, filename, mime } = UML_XMI_EXPORT.run(umlExportElementsOf(std), {
674
+ name: umlExportFilename(std),
675
+ });
676
+ downloadBlob(new Blob([text], { type: mime }), filename);
677
+ }
678
+ /* ── Import (ADR 0019 — three formats in) ─────────────────────────────── */
679
+ /**
680
+ * Read a PlantUML source the user picks, draw it, and say what it cost.
681
+ *
682
+ * The whole gesture is {@link runInterchangeImportFile}, over the capability
683
+ * UML declares: pick the file, run the DECLARED reader, write what it returned,
684
+ * fit the drawing, report. `UML_PLANTUML_IMPORT.run` is the same function
685
+ * labre-mcp calls, so the command and the registry cannot read the same file
686
+ * differently — one door, and the registry is the label on it. The picker's
687
+ * filter comes off `UML_PLANTUML_FORMAT` rather than a table of file types, so
688
+ * `.plantuml` is offered beside `.puml` because the declaration says both.
689
+ *
690
+ * Three commands and three one-line bodies, which is the point of the seam: a
691
+ * second format costs a declaration and a command, never a pipeline. What
692
+ * differs between them is the PROMISE, and the promise is made by each
693
+ * command's own label and description before the picker ever opens (ADR 0012
694
+ * P2) — two semantic readers that carry the model, and one visual reader that
695
+ * recognises a drawing.
696
+ */
697
+ export async function importUmlPlantumlFile(std) {
698
+ await runInterchangeImportFile(std, UML_PLANTUML_IMPORT);
699
+ }
700
+ /** Read an XMI 2.5.1 document the user picks. The same four steps. */
701
+ export async function importUmlXmiFile(std) {
702
+ await runInterchangeImportFile(std, UML_XMI_IMPORT);
703
+ }
704
+ /**
705
+ * Read a draw.io drawing the user picks — the one import with a step before the
706
+ * reader (`docs/adr/0019`).
707
+ *
708
+ * A `.drawio` file holds its `<mxGraphModel>` as base64 of a raw deflate, and
709
+ * inflating it needs `DecompressionStream`: a platform API, asynchronous, and
710
+ * therefore forbidden inside a reader that ADR 0012 P3 makes pure. So the
711
+ * unwrapping rides on the pipeline's `decode` hook, which runs here — where
712
+ * there is already an editor and an `await` — and `UML_DRAWIO_IMPORT.run` still
713
+ * takes decoded XML and still answers a compressed payload with a remark rather
714
+ * than a guess.
715
+ *
716
+ * {@link decodeDrawio} is identity for a file that is already plain XML, so the
717
+ * `.drawio.xml` case and the "Compressed: off" case cost nothing and take the
718
+ * same path.
719
+ */
720
+ export async function importUmlDrawioFile(std) {
721
+ await runInterchangeImportFile(std, UML_DRAWIO_IMPORT, {
722
+ decode: decodeDrawio,
723
+ });
724
+ }