@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,611 @@
1
+ import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
3
+ import { DefaultTheme, } from '@formicoidea/labre-core/model';
4
+ import { UML_ACTOR_FIGURE, UML_CUBE_DEPTH, UML_PACKAGE_TAB, umlCompartmentBoxes, umlComponentSiblings, umlStackHeight, } from '../component.js';
5
+ import { isActGlyphKind, paintActGlyph } from './act-glyphs.js';
6
+ import { line, solidRect, TAU } from './paint.js';
7
+ import { isSdGlyphKind, paintSdGlyph } from './sd-glyphs.js';
8
+ import { isStmGlyphKind, paintStmGlyph } from './stm-glyphs.js';
9
+ /**
10
+ * Renderer for a UML node — the glyph layer, and nothing else.
11
+ *
12
+ * ## What the native shape does, and what this file does
13
+ *
14
+ * The native shape renderer runs FIRST and owns the fill, the stroke and the
15
+ * theme. This file then paints, in the element-local frame, only what a
16
+ * rectangle cannot be: the compartment separators of §11.4.4, the underline of
17
+ * an instance's name (§9.8.4), the package's tab (§12.2.4), the note's folded
18
+ * corner (Annex A), the actor's stick figure (§18.1.4), the component's
19
+ * two-tabbed icon (§11.6.4), the artifact's document icon (§19.3.4), the port's
20
+ * square (§11.3.4), the ball and socket of §10.4.4 and the 3-D cube of §19.4.4.
21
+ *
22
+ * The behaviour families of phase 2 add fourteen more marks, and they live in
23
+ * two modules of their own — `act-glyphs.ts` for the control nodes of §15.3.4
24
+ * and the signal shapes of §16.3.4 / §16.10.4, `stm-glyphs.ts` for the final
25
+ * state and the pseudostates of §14.2.4. The BRANCH stays here, because which
26
+ * kind gets which picture is this file's subject and the `never` at the end of
27
+ * `paintGlyph` is what keeps it total; the drawing moved out because a
28
+ * thousand-line renderer is a file nobody reads the middle of.
29
+ *
30
+ * Five kinds need nothing at all. A `use-case` IS the native ellipse (§18.1.4),
31
+ * an `action` and a `state`'s body the native ROUNDED rect (§15.3.4, §14.2.4),
32
+ * a `decision` and a `choice` the native diamond, an `object-node` the plain
33
+ * native rect (§15.4.4) — and drawing anything over any of them would be
34
+ * inventing a notation. An `object` is a class box with a split and a rule under
35
+ * its name, which is the first branch below rather than a picture; so are a
36
+ * `component` and an `artifact`, each with a small icon dropped in the corner of
37
+ * the box the shape layer has already drawn.
38
+ *
39
+ * ## Every offset is READ, never restated
40
+ *
41
+ * The separator positions come from `umlCompartmentBoxes` — the same pure
42
+ * function the creation site places the text tiers with — so a rule is drawn
43
+ * exactly between two compartments rather than near where they were last time
44
+ * somebody looked. The actor's figure is bounded by the top of its own label
45
+ * box, from the same call, so the words can never land on the legs.
46
+ *
47
+ * …and once a component's tiers are ON the canvas, they are the better source
48
+ * still: `umlNodeCompartments` reads the separators off the body tier's own box
49
+ * when the group can be reached, so a classifier `UmlCompartmentWatcher` has
50
+ * grown for a five-line attributes compartment is ruled where its compartments
51
+ * now are. The default stack is the answer for a shape with no group behind it,
52
+ * which is every one of them at creation time.
53
+ *
54
+ * ## Colours come off the MODEL
55
+ *
56
+ * Both fill and stroke are read from the element, never from a table: a node's
57
+ * colours are editable from the shape toolbar like any other shape's, and a
58
+ * glyph that painted the pack's own ink would silently ignore the user's choice.
59
+ * `presets.ts` is what SEEDS them.
60
+ *
61
+ * The glyph-bodied kinds — `package`, `note`, `actor`, `port`, the two interface
62
+ * glyphs and the three cubes — are created unfilled and unstroked
63
+ * (`presets.ts`), so the native rect paints nothing and the glyph IS the body:
64
+ * it fills with the element's `fillColor` and outlines with its `strokeColor`,
65
+ * which is what keeps them recolourable from the same toolbar as every other
66
+ * shape.
67
+ */
68
+ /* ── The glyph geometry, as fractions of the node box ──────────────────── */
69
+ /**
70
+ * How wide the package's tab is (§12.2.4), as a fraction of the box.
71
+ *
72
+ * Only the WIDTH: the tab's height is `UML_PACKAGE_TAB`, owned by
73
+ * `component.ts` because that is where the name compartment is placed under it,
74
+ * and two files each picking their own number is how a label comes to sit on a
75
+ * fold. Wide enough to read as a folder tab and no wider — a tab that ran most
76
+ * of the width would read as a compartment.
77
+ */
78
+ const PACKAGE_TAB_WIDTH = 0.42;
79
+ /** The note's folded corner (Annex A), as a fraction of the SHORTER side. */
80
+ const NOTE_FOLD = 0.22;
81
+ /**
82
+ * The actor's stick figure (§18.1.4), in fractions of the figure box — which is
83
+ * the element box down to the top of its label, never the element box itself.
84
+ */
85
+ const ACTOR = {
86
+ /** Head radius, against the figure's width and against its height. */
87
+ headOfWidth: 0.24,
88
+ headOfHeight: 0.18,
89
+ /** Where the spine ends, as a fraction of the figure's height. */
90
+ hipY: 0.66,
91
+ /** The arms: their height, and how far out they reach. */
92
+ armY: 0.46,
93
+ armX: 0.06,
94
+ /** Where the feet land. */
95
+ footY: 0.98,
96
+ footX: 0.12,
97
+ };
98
+ /**
99
+ * The component's own icon (§11.6.4) — the little rectangle with two tabs
100
+ * sticking out of its left edge, dropped in the top-right corner of the box.
101
+ *
102
+ * Fractions of the node box, like every other glyph metric here, so a component
103
+ * dragged to any size carries an icon in proportion to it. The icon is what
104
+ * MAKES the box a component: §11.6.4 offers the icon or the `«component»`
105
+ * keyword, and this pack draws the icon, which is why the name seed writes no
106
+ * keyword (`keywords.ts`).
107
+ */
108
+ const COMPONENT_ICON = {
109
+ /** The icon body, against the box's width and height. */
110
+ width: 0.16,
111
+ height: 0.2,
112
+ /** Its inset from the top-right corner, against the shorter side. */
113
+ margin: 0.06,
114
+ /** The two tabs, against the icon body: how far they protrude, how tall. */
115
+ tabWidth: 0.4,
116
+ tabHeight: 0.26,
117
+ /** Where each tab's top edge sits, against the icon body's height. */
118
+ tabTop: [0.16, 0.58],
119
+ };
120
+ /**
121
+ * The artifact's document icon (§19.3.4) — a small sheet of paper with its
122
+ * top-right corner turned down, in the same corner as the component's.
123
+ *
124
+ * Narrower and taller than the component's icon because it is a PAGE, and a page
125
+ * read as a square would read as a box.
126
+ */
127
+ const ARTIFACT_ICON = {
128
+ width: 0.1,
129
+ height: 0.19,
130
+ margin: 0.06,
131
+ /** The turned-down corner, against the icon's shorter side. */
132
+ fold: 0.36,
133
+ };
134
+ /**
135
+ * The ball and the socket (§10.4.4), as fractions of the glyph's box.
136
+ *
137
+ * One radius from whichever dimension is the tighter, so the ball stays ROUND at
138
+ * any aspect ratio the element is dragged to — the same rule the actor's head
139
+ * follows, and for the same reason. What is left under the circle is the STUB:
140
+ * the short line that, on a real diagram, runs to the component's border.
141
+ */
142
+ const INTERFACE_GLYPH = {
143
+ radiusOfWidth: 0.3,
144
+ radiusOfHeight: 0.34,
145
+ };
146
+ /* ── The glyphs ────────────────────────────────────────────────────────── */
147
+ /**
148
+ * Paint one kind, in the element-local frame, with `fillStyle` / `strokeStyle` /
149
+ * `lineWidth` already set from the model.
150
+ *
151
+ * @param inset half the stroke width — a body is drawn inside it, so the outline
152
+ * lands within the element's bounds rather than straddling them.
153
+ */
154
+ function paintGlyph(kind, ctx, w, h, inset, boxes) {
155
+ const x0 = inset;
156
+ const y0 = inset;
157
+ const x1 = w - inset;
158
+ const y1 = h - inset;
159
+ const bw = x1 - x0;
160
+ const bh = y1 - y0;
161
+ if (!(bw > 0) || !(bh > 0))
162
+ return;
163
+ // ── The compartmented kinds: one rule per split ──────────────────────
164
+ if (kind === 'class' ||
165
+ kind === 'interface' ||
166
+ kind === 'enumeration' ||
167
+ kind === 'object' ||
168
+ kind === 'component' ||
169
+ kind === 'artifact' ||
170
+ // §14.2.4 rules a state off between its name and its internal activities —
171
+ // the object's layout exactly, drawn on the native ROUNDED rect the preset
172
+ // gives it rather than on a square one.
173
+ kind === 'state') {
174
+ // Full width, edge to edge: §11.4.4 draws a compartment line right across
175
+ // the classifier, unlike the inset the text tiers sit within.
176
+ for (const split of boxes.splits) {
177
+ if (split <= y0 || split >= y1)
178
+ continue;
179
+ line(ctx, x0, split, x1, split);
180
+ }
181
+ // An instance's name is UNDERLINED (§9.8.4) — the one mark that tells a
182
+ // reader an object from the class it is an instance of. Drawn under the NAME
183
+ // box and only as wide as it, so it reads as a rule under the words rather
184
+ // than as a fifth compartment line.
185
+ if (kind === 'object') {
186
+ const { name } = boxes;
187
+ const under = name.y + name.h;
188
+ if (under > y0 && under < y1 && name.w > 0) {
189
+ line(ctx, name.x, under, name.x + name.w, under);
190
+ }
191
+ }
192
+ // The two kinds whose box is told from a class's by a picture in its
193
+ // corner, drawn over the body the shape layer has already filled.
194
+ if (kind === 'component')
195
+ paintComponentIcon(ctx, x1, y0, bw, bh);
196
+ if (kind === 'artifact')
197
+ paintArtifactIcon(ctx, x1, y0, bw, bh);
198
+ return;
199
+ }
200
+ // ── The package: a tab, and the body under it (§12.2.4) ──────────────
201
+ if (kind === 'package') {
202
+ const tabW = bw * PACKAGE_TAB_WIDTH;
203
+ // Against the ELEMENT box, not the stroke-inset one: `component.ts` places
204
+ // the name compartment under `h * UML_PACKAGE_TAB`, and the fold has to be
205
+ // the line that number names.
206
+ const tabH = Math.max(0, Math.min(h * UML_PACKAGE_TAB - y0, bh));
207
+ // The tab first, so the body's top edge — drawn over it — closes the join
208
+ // on the right of the tab and nowhere else, which is the folder silhouette.
209
+ ctx.beginPath();
210
+ ctx.moveTo(x0, y0 + tabH);
211
+ ctx.lineTo(x0, y0);
212
+ ctx.lineTo(x0 + tabW, y0);
213
+ ctx.lineTo(x0 + tabW, y0 + tabH);
214
+ ctx.closePath();
215
+ ctx.fill();
216
+ ctx.stroke();
217
+ ctx.beginPath();
218
+ ctx.moveTo(x0, y0 + tabH);
219
+ ctx.lineTo(x1, y0 + tabH);
220
+ ctx.lineTo(x1, y1);
221
+ ctx.lineTo(x0, y1);
222
+ ctx.closePath();
223
+ ctx.fill();
224
+ ctx.stroke();
225
+ return;
226
+ }
227
+ // ── The note: a rectangle with its top-right corner turned down ──────
228
+ if (kind === 'note') {
229
+ const fold = Math.min(bw, bh) * NOTE_FOLD;
230
+ ctx.beginPath();
231
+ ctx.moveTo(x0, y0);
232
+ ctx.lineTo(x1 - fold, y0);
233
+ ctx.lineTo(x1, y0 + fold);
234
+ ctx.lineTo(x1, y1);
235
+ ctx.lineTo(x0, y1);
236
+ ctx.closePath();
237
+ ctx.fill();
238
+ ctx.stroke();
239
+ // The fold itself: the two edges of the turned-down triangle. Drawn as
240
+ // lines and not as a filled flap, which is how Annex A draws it — a shaded
241
+ // flap would claim a light source the notation has none of.
242
+ ctx.beginPath();
243
+ ctx.moveTo(x1 - fold, y0);
244
+ ctx.lineTo(x1 - fold, y0 + fold);
245
+ ctx.lineTo(x1, y0 + fold);
246
+ ctx.stroke();
247
+ return;
248
+ }
249
+ // ── The actor: a stick figure over its own name (§18.1.4) ────────────
250
+ if (kind === 'actor') {
251
+ // The figure stops where the label starts: `component.ts` owns that line
252
+ // (`UML_ACTOR_FIGURE`) and places the name element below it, so the words
253
+ // can never land on the legs.
254
+ const figureH = Math.max(0, Math.min(h * UML_ACTOR_FIGURE - y0, bh));
255
+ if (!(figureH > 0))
256
+ return;
257
+ const cx = w / 2;
258
+ // One radius from whichever dimension is the tighter, so the head stays
259
+ // CIRCULAR at any aspect ratio the author drags the element to.
260
+ const headR = Math.max(0, Math.min(bw * ACTOR.headOfWidth, figureH * ACTOR.headOfHeight));
261
+ if (headR > 0) {
262
+ ctx.beginPath();
263
+ ctx.arc(cx, y0 + headR, headR, 0, TAU);
264
+ ctx.fill();
265
+ ctx.stroke();
266
+ }
267
+ const hipY = y0 + figureH * ACTOR.hipY;
268
+ // The spine, from under the head down to the hips.
269
+ line(ctx, cx, y0 + headR * 2, cx, hipY);
270
+ // The arms, straight across.
271
+ const armY = y0 + figureH * ACTOR.armY;
272
+ line(ctx, x0 + bw * ACTOR.armX, armY, x1 - bw * ACTOR.armX, armY);
273
+ // …and the two legs.
274
+ const footY = y0 + figureH * ACTOR.footY;
275
+ line(ctx, cx, hipY, x0 + bw * ACTOR.footX, footY);
276
+ line(ctx, cx, hipY, x1 - bw * ACTOR.footX, footY);
277
+ return;
278
+ }
279
+ // ── The port: a small filled square (§11.3.4) ────────────────────────
280
+ if (kind === 'port') {
281
+ // A SQUARE, from the shorter side and centred: a port dragged into a
282
+ // rectangle is still a port, and §11.3.4 draws one square. The whole body,
283
+ // because the port is created unstroked and unfilled — the glyph is it.
284
+ const side = Math.min(bw, bh);
285
+ solidRect(ctx, x0 + (bw - side) / 2, y0 + (bh - side) / 2, side, side);
286
+ return;
287
+ }
288
+ // ── The two interface glyphs: a ball, or a socket, on a stub ─────────
289
+ if (kind === 'provided-interface' || kind === 'required-interface') {
290
+ const radius = Math.max(0, Math.min(bw * INTERFACE_GLYPH.radiusOfWidth, bh * INTERFACE_GLYPH.radiusOfHeight));
291
+ if (!(radius > 0))
292
+ return;
293
+ const cx = w / 2;
294
+ const cy = y0 + radius;
295
+ if (kind === 'provided-interface') {
296
+ // The lollipop: a CLOSED circle, filled, saying the component offers this
297
+ // interface (§10.4.4).
298
+ ctx.beginPath();
299
+ ctx.arc(cx, cy, radius, 0, TAU);
300
+ ctx.fill();
301
+ ctx.stroke();
302
+ }
303
+ else {
304
+ // The socket: the BOTTOM half of the circle, a cup opening UPWARD — away
305
+ // from the component the stub below runs to, so a ball can nest in it
306
+ // (§10.4.4). Its deepest point is (cx, cy + radius), exactly where the
307
+ // stub starts, so the line meets the back of the cup and not a horn.
308
+ // Stroked and never filled: it is an arc, and a filled half-disc would
309
+ // read as a ball cut in two.
310
+ ctx.beginPath();
311
+ ctx.arc(cx, cy, radius, 0, Math.PI);
312
+ ctx.stroke();
313
+ }
314
+ // The stub: the short line that, on a real diagram, runs from the glyph to
315
+ // the border of the component it belongs to. Drawn straight DOWN, which is
316
+ // the one direction that needs no knowledge of where that component is.
317
+ if (cy + radius < y1)
318
+ line(ctx, cx, cy + radius, cx, y1);
319
+ return;
320
+ }
321
+ // ── The three deployment targets: one 3-D cube (§19.4.4) ─────────────
322
+ if (kind === 'node' ||
323
+ kind === 'device' ||
324
+ kind === 'execution-environment') {
325
+ // Against the ELEMENT box, not the stroke-inset one, and clamped: the front
326
+ // face has to be the rectangle `component.ts` writes the name inside, and
327
+ // two files each deriving their own depth is how a name ends up on a roof.
328
+ const depth = Math.max(0, Math.min(Math.min(w, h) * UML_CUBE_DEPTH, Math.min(bw, bh)));
329
+ const backX = x1 - depth;
330
+ const backY = y0 + depth;
331
+ // The top and the right first, the FRONT last: the front face is opaque, so
332
+ // painting it over the other two closes every join that should not be seen
333
+ // and leaves the three edges that make the solid read as a solid.
334
+ if (depth > 0) {
335
+ ctx.beginPath();
336
+ ctx.moveTo(x0, backY);
337
+ ctx.lineTo(x0 + depth, y0);
338
+ ctx.lineTo(x1, y0);
339
+ ctx.lineTo(backX, backY);
340
+ ctx.closePath();
341
+ ctx.fill();
342
+ ctx.stroke();
343
+ ctx.beginPath();
344
+ ctx.moveTo(backX, backY);
345
+ ctx.lineTo(x1, y0);
346
+ ctx.lineTo(x1, y1 - depth);
347
+ ctx.lineTo(backX, y1);
348
+ ctx.closePath();
349
+ ctx.fill();
350
+ ctx.stroke();
351
+ }
352
+ solidRect(ctx, x0, backY, backX - x0, y1 - backY);
353
+ return;
354
+ }
355
+ // ── The kinds the SHAPE LAYER draws whole, with nothing painted over ──
356
+ //
357
+ // A use case is the native ellipse of §18.1.4; an `action` and the body of a
358
+ // `state` are the native ROUNDED rect of §15.3.4 and §14.2.4 (`radius` on the
359
+ // preset, not a glyph); a `decision` and a `choice` are the native diamond;
360
+ // an `object-node` is the plain native rect of §15.4.4. Drawing anything over
361
+ // any of them would be inventing a notation.
362
+ //
363
+ // `state` is absent from this list because it is handled ABOVE, with the
364
+ // compartmented kinds: the rounded body is the shape layer's, the rule under
365
+ // the name is this file's.
366
+ if (kind === 'use-case' ||
367
+ kind === 'action' ||
368
+ kind === 'object-node' ||
369
+ kind === 'decision' ||
370
+ kind === 'choice' ||
371
+ // …and, since phase 3, §17.2.4's ExecutionSpecification: a thin FILLED
372
+ // rectangle on a lifeline's spine, which is a native rect and nothing more.
373
+ kind === 'execution') {
374
+ return;
375
+ }
376
+ // ── The behaviour marks, by family ───────────────────────────────────
377
+ //
378
+ // Delegated rather than inlined: the two families between them are fourteen
379
+ // pictures, and the branch a reader needs to follow is WHICH KIND GETS WHICH,
380
+ // not how a bullseye is struck. Each module closes its own switch with a
381
+ // `never`, so the delegation costs none of the exhaustiveness the last branch
382
+ // of this function exists for.
383
+ const box = { x0, y0, x1, y1, bw, bh, w, h };
384
+ if (isActGlyphKind(kind)) {
385
+ paintActGlyph(kind, ctx, box);
386
+ return;
387
+ }
388
+ if (isStmGlyphKind(kind)) {
389
+ paintStmGlyph(kind, ctx, box);
390
+ return;
391
+ }
392
+ // Phase 3's two: §17.3.4's lifeline — the one glyph in the pack that is
393
+ // BIGGER than the element it belongs to — and §17.2.4's destruction X.
394
+ if (isSdGlyphKind(kind)) {
395
+ paintSdGlyph(kind, ctx, box);
396
+ return;
397
+ }
398
+ /**
399
+ * Every kind is drawn above, and this is what keeps that true: `kind` is
400
+ * narrowed to `never` here only if the branches are exhaustive over
401
+ * {@link UmlNodeKind}, so a kind added to the model's union without a picture
402
+ * of its own stops the build.
403
+ *
404
+ * Which is the whole point of closing the last branch rather than letting it
405
+ * fall through. A renderer that silently paints a package tab on somebody's
406
+ * new artefact is worse than one that paints nothing: the first is a wrong
407
+ * picture nobody is told about, the second is a missing one everybody sees.
408
+ */
409
+ const unhandled = kind;
410
+ void unhandled;
411
+ }
412
+ /* ── The two corner icons ──────────────────────────────────────────────── */
413
+ /**
414
+ * The component icon (§11.6.4): a small rectangle with two tabs protruding from
415
+ * its LEFT edge, in the top-right corner of the box.
416
+ *
417
+ * Painted as three solid bodies rather than as one outline, because that is what
418
+ * the notation draws — the tabs are little plugs sticking out of the module, and
419
+ * an author who recolours the component recolours them with it.
420
+ *
421
+ * @param x1 the right edge of the body, already inset by half the stroke.
422
+ * @param y0 its top edge, likewise.
423
+ */
424
+ function paintComponentIcon(ctx, x1, y0, bw, bh) {
425
+ const iw = bw * COMPONENT_ICON.width;
426
+ const ih = bh * COMPONENT_ICON.height;
427
+ const margin = Math.min(bw, bh) * COMPONENT_ICON.margin;
428
+ const tabW = iw * COMPONENT_ICON.tabWidth;
429
+ const tabH = ih * COMPONENT_ICON.tabHeight;
430
+ // The tabs hang off the left of the body, so the body is indented by their
431
+ // width: the whole icon still fits inside the margin it was given.
432
+ const left = x1 - margin - iw;
433
+ const top = y0 + margin;
434
+ if (!(iw > 0) || !(ih > 0) || left - tabW < 0)
435
+ return;
436
+ solidRect(ctx, left, top, iw, ih);
437
+ for (const at of COMPONENT_ICON.tabTop) {
438
+ solidRect(ctx, left - tabW / 2, top + ih * at, tabW, tabH);
439
+ }
440
+ }
441
+ /**
442
+ * The artifact icon (§19.3.4): a sheet of paper with its top-right corner turned
443
+ * down, in the same corner of the box as the component's.
444
+ *
445
+ * The same silhouette as the note glyph, two orders of magnitude smaller and
446
+ * standing for something else entirely — which is the notation's doing, not a
447
+ * shortcut: a note is a piece of paper pinned to a diagram, an artifact is a
448
+ * file the system ships.
449
+ */
450
+ function paintArtifactIcon(ctx, x1, y0, bw, bh) {
451
+ const iw = bw * ARTIFACT_ICON.width;
452
+ const ih = bh * ARTIFACT_ICON.height;
453
+ const margin = Math.min(bw, bh) * ARTIFACT_ICON.margin;
454
+ const fold = Math.min(iw, ih) * ARTIFACT_ICON.fold;
455
+ const left = x1 - margin - iw;
456
+ const top = y0 + margin;
457
+ if (!(iw > 0) || !(ih > 0) || left < 0)
458
+ return;
459
+ ctx.beginPath();
460
+ ctx.moveTo(left, top);
461
+ ctx.lineTo(left + iw - fold, top);
462
+ ctx.lineTo(left + iw, top + fold);
463
+ ctx.lineTo(left + iw, top + ih);
464
+ ctx.lineTo(left, top + ih);
465
+ ctx.closePath();
466
+ ctx.fill();
467
+ ctx.stroke();
468
+ // The fold, as two lines — the same way the note draws its own, and for the
469
+ // same reason: a shaded flap would claim a light source UML has none of.
470
+ ctx.beginPath();
471
+ ctx.moveTo(left + iw - fold, top);
472
+ ctx.lineTo(left + iw - fold, top + fold);
473
+ ctx.lineTo(left + iw, top + fold);
474
+ ctx.stroke();
475
+ }
476
+ /**
477
+ * The stencil stack, in PROPORTION when the box is too short to hold it.
478
+ *
479
+ * `umlCompartmentBoxes` is written in absolute units — an 8-unit margin, a 16px
480
+ * name line, a 6-unit gap, three 13px attribute lines — because a classifier at
481
+ * its own size must be ruled where the specification rules it, whatever it has
482
+ * been dragged to. A box SHORTER than the stack those absolutes need is the one
483
+ * case they cannot answer: every separator lands past the bottom edge,
484
+ * {@link paintGlyph} clips them all, and a class drawn small is a bare
485
+ * rectangle with no compartments — the picture the notation is told apart BY.
486
+ *
487
+ * So under its natural height the stack is scaled rather than clipped. Nothing
488
+ * moves at or above that height: `squeeze` is 1 and every offset is the number
489
+ * it always was.
490
+ *
491
+ * ## A RENDER-time answer, and only that
492
+ *
493
+ * The squeeze lives here rather than in `component.ts` on purpose. What
494
+ * `umlCompartmentBoxes` answers is where a node's TEXT TIERS go, and the
495
+ * caller that asks with line counts — `UmlCompartmentWatcher` — reads an
496
+ * overflowing stack as its cue to GROW the node, which is the right answer for
497
+ * words an author has typed. This asks a different question: where does the
498
+ * renderer stroke a rule INSIDE the body it has, on a box nothing is going to
499
+ * grow. A legend swatch is that box, and so is any node an author has dragged
500
+ * under its own stack.
501
+ */
502
+ function umlSqueezedBoxes(kind, w, h) {
503
+ const natural = umlStackHeight(kind, {});
504
+ if (!natural || !(natural > 0) || h >= natural) {
505
+ return umlCompartmentBoxes(kind, 0, 0, w, h);
506
+ }
507
+ // Laid out at the height the stencil wants, then scaled into the box it has
508
+ // — so the three tiers keep the proportions §11.4.4 draws them in rather
509
+ // than each being clamped on its own.
510
+ const squeeze = h / natural;
511
+ const full = umlCompartmentBoxes(kind, 0, 0, w, natural);
512
+ return {
513
+ ...full,
514
+ name: { ...full.name, y: full.name.y * squeeze, h: full.name.h * squeeze },
515
+ splits: full.splits.map(split => split * squeeze),
516
+ };
517
+ }
518
+ /**
519
+ * The compartment layout to stroke this node's separators from: the one its
520
+ * TIERS describe if they can be reached, and the default stack otherwise.
521
+ *
522
+ * ## Why the tiers and not the line counts
523
+ *
524
+ * `UmlCompartmentWatcher` grows a classifier whose attributes tier has
525
+ * overflowed and moves the three texts to the boxes the taller stack yields —
526
+ * so after an edit the compartments are no longer the default three-line stack a
527
+ * rectangle implies, and a renderer still drawing that stack would rule a line
528
+ * straight through the fourth attribute.
529
+ *
530
+ * It could be told the LINE COUNTS and recompute the same walk. It reads the
531
+ * tiers' own boxes instead, for two reasons. It is cheaper — `deserializedXYWH`
532
+ * is cached on the element, while a line count means materializing every tier's
533
+ * `Y.Text` into a string on every frame of every pan — and it cannot disagree
534
+ * with what the reader sees: a separator IS the boundary between two
535
+ * compartments, and reading the boundary off the compartments is the shortest
536
+ * possible route to that. An author who nudges a tier by hand takes the rule
537
+ * with them, which is the same answer.
538
+ *
539
+ * ## The ceiling
540
+ *
541
+ * ponytail: the tiers are read in the node's own unrotated frame, so a ROTATED
542
+ * `umlNode` falls back to the default stack. A UML group cannot be rotated
543
+ * (`GroupElementModel.rotate` is a constant 0), so this only reaches a shape
544
+ * somebody rotated out of its own group — at which point the words are not
545
+ * turning with it either and the picture is already the author's problem.
546
+ * Raising it means projecting each tier's box through `-rotate` about the
547
+ * node's centre, which is four lines nobody has asked for yet.
548
+ */
549
+ export function umlNodeCompartments(model, w, h) {
550
+ const defaults = umlSqueezedBoxes(model.kind, w, h);
551
+ const group = model.group;
552
+ // No splits at all is a kind that is a PICTURE (a package, an actor, a cube):
553
+ // there are no compartments to read off, and nothing below would mean
554
+ // anything. A rotated shape is the documented ceiling above.
555
+ if (defaults.splits.length === 0 || model.rotate || !group)
556
+ return defaults;
557
+ const children = group.childElements;
558
+ const component = umlComponentSiblings({ id: group.id, childIds: group.childIds }, children);
559
+ // The BODY tier is what both separators hang off: the line above it is where
560
+ // the name compartment stops, the line below it where the operations start.
561
+ // Its role is `uml:attributes` on all seven compartmented kinds — a slot list,
562
+ // a component's parts, a state's activities included (`component.ts`).
563
+ const body = children.find(child => child.id === component.attributes?.id);
564
+ if (!body)
565
+ return defaults;
566
+ const [nodeX, nodeY] = model.deserializedXYWH;
567
+ const top = body.y - nodeY;
568
+ const bottom = top + body.h;
569
+ // A tier dragged clean off its node says nothing useful about where a line
570
+ // INSIDE the node goes.
571
+ if (!(top > 0) || !(bottom > top))
572
+ return defaults;
573
+ const name = children.find(child => child.id === component.name?.id);
574
+ return {
575
+ // The name box is read for the one thing it is used for: the rule under an
576
+ // INSTANCE's name (§9.8.4), which has to stay under the words once a
577
+ // two-line name has pushed them down.
578
+ name: name
579
+ ? { x: name.x - nodeX, y: name.y - nodeY, w: name.w, h: name.h }
580
+ : defaults.name,
581
+ // How MANY separators is the notation's answer and the default walk already
582
+ // made it — one for the kinds §9.8.4 / §11.6.4 / §19.3.4 / §14.2.4 draw as a
583
+ // name over a single body, two for a classifier. Only the positions are read
584
+ // off the tier.
585
+ splits: defaults.splits.length === 1 ? [top] : [top, bottom],
586
+ };
587
+ }
588
+ export const umlNode = (model, ctx, matrix, renderer, rc, bound) => {
589
+ const [, , w, h] = model.deserializedXYWH;
590
+ const cx = w / 2;
591
+ const cy = h / 2;
592
+ // Capture the element-local transform BEFORE the shape renderer mutates the
593
+ // matrix, so the glyph can be drawn in the same space afterwards.
594
+ const glyphMatrix = DOMMatrix.fromMatrix(matrix)
595
+ .translateSelf(cx, cy)
596
+ .rotateSelf(model.rotate)
597
+ .translateSelf(-cx, -cy);
598
+ // Native shape: fill, stroke and theme, all handled natively.
599
+ shapeRenderer(model, ctx, matrix, renderer, rc, bound);
600
+ const stroke = renderer.getColorValue(model.strokeColor, DefaultTheme.shapeStrokeColor, true);
601
+ const fill = renderer.getColorValue(model.fillColor, DefaultTheme.shapeFillColor, true);
602
+ const strokeWidth = model.strokeWidth || 1;
603
+ ctx.setTransform(glyphMatrix);
604
+ ctx.strokeStyle = stroke;
605
+ ctx.fillStyle = fill;
606
+ ctx.lineWidth = strokeWidth;
607
+ ctx.lineCap = 'round';
608
+ ctx.lineJoin = 'round';
609
+ paintGlyph(model.kind, ctx, w, h, strokeWidth / 2, umlNodeCompartments(model, w, h));
610
+ };
611
+ export const UmlNodeRendererExtension = ElementRendererExtension('umlNode', umlNode);
@@ -0,0 +1,51 @@
1
+ import { type UmlNodeElementModel } from '@formicoidea/labre-core/model';
2
+ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
+ /**
4
+ * View for a UML node. Registering it ensures `gfx.view.get(model)` returns a
5
+ * view (required so move / select / connector interactions work).
6
+ *
7
+ * ## Double-clicking the body edits the NAME — which is not on this element
8
+ *
9
+ * A UML artefact is a GROUP: the shape, plus the canvas TEXT elements that carry
10
+ * its words, every one of them edited in place exactly like any other words on
11
+ * the canvas (R16, and the same arrangement C4's recette of 28/08/2026 settled).
12
+ * The shape itself carries no text at all. Which leaves a gesture with nowhere
13
+ * obvious to go: the body is the biggest target in the component, double-clicking
14
+ * a shape is how everybody in this editor starts typing, and the shape's own
15
+ * inline editor would write into the one field that must stay empty — a "shadow"
16
+ * name, invisible under the real one, disagreeing with it, exported by neither.
17
+ *
18
+ * So the gesture is ROUTED: a double-click on the body opens the NAME child's
19
+ * own editor, `mountTextElementEditor` — the very same call `TextElementView`
20
+ * makes when you double-click those words directly. The author gets one editor
21
+ * for one name whichever half of the component they aimed at.
22
+ *
23
+ * ## `uml:name`, `uml:label` or `uml:lifeline-ident`, and why all three
24
+ *
25
+ * A compartmented artefact's first tier is `uml:name` — it is the NAME
26
+ * COMPARTMENT of §11.4.4, a rectangle with a rule under it. An actor's and a use
27
+ * case's single word is `uml:label`: the same tier, named differently because
28
+ * there is no compartment for it to be a compartment OF. A lifeline's head is
29
+ * `uml:lifeline-ident`, a third time: §17.3.4 prints a grammar for what goes in
30
+ * it and the validation pack reads that grammar and no other (`roles.ts`). The
31
+ * gesture is one gesture, so it accepts any of the three and takes whichever the
32
+ * group holds — the kind is not consulted, which means an artefact regrouped by
33
+ * hand still opens on the words it actually has.
34
+ *
35
+ * ## …except on an element that has no text child
36
+ *
37
+ * A node whose group was released and whose texts were deleted lands on
38
+ * `mountShapeTextEditor` and gets the native behaviour rather than nothing at
39
+ * all. Same fallback C4's node view keeps, and for the same reason: a gesture
40
+ * that silently does nothing reads as a broken editor.
41
+ *
42
+ * Hit testing is `UmlNodeElementModel.includesPoint`'s doing: the glyph-bodied
43
+ * kinds are created unfilled, and an empty unfilled shape would normally be a
44
+ * target only near its border. The override forces the interior test, so the
45
+ * whole body answers this gesture on all eight kinds.
46
+ */
47
+ export declare class UmlNodeView extends GfxElementModelView<UmlNodeElementModel> {
48
+ #private;
49
+ static type: string;
50
+ onCreated(): void;
51
+ }