@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,708 @@
1
+ import { UML_BODY_FONT_SIZE, UML_LIFELINE_HEAD, UML_NAME_FONT_SIZE, } from './consts.js';
2
+ import { UML_ROLE, UML_ROLE_OF_KIND } from './roles.js';
3
+ /**
4
+ * A UML component — the SHAPE and its own words, grouped.
5
+ *
6
+ * ## Why a UML artefact is several elements
7
+ *
8
+ * UML 2.5.1 draws a classifier as a rectangle divided into COMPARTMENTS
9
+ * (§9.2.4, §11.4.4): the name on top, the attributes under a line, the
10
+ * operations under a second one. Those compartments are not decoration — they
11
+ * are three separate statements about the same classifier, and an author edits
12
+ * them one at a time.
13
+ *
14
+ * The library already knows how to draw that: the same arrangement C4 arrived
15
+ * at through the PO's recette of 28/08/2026 (`gfx/c4/src/component.ts`), which
16
+ * R15/R16 then generalised into a rule. A component is a native `group` holding
17
+ * a `umlNode` shape that carries NO inner text and one canvas `text` element per
18
+ * compartment, each stamped with its own role. One click selects the classifier,
19
+ * a second descends into the compartment under the pointer, and every gesture
20
+ * the editor already has works on it.
21
+ *
22
+ * ## This module
23
+ *
24
+ * The two questions that arrangement raises, both PURE and both free of `std`
25
+ * and of Yjs, so the creation site, the node renderer, the exporter and a test
26
+ * all answer them the same way:
27
+ *
28
+ * - **where do the compartments go**, given a node's box
29
+ * ({@link umlCompartmentBoxes}) — and, as a by-product nobody else can
30
+ * compute, WHERE THE SEPARATORS ARE, which is what the renderer strokes;
31
+ * - **which words belong to which shape**, given a group and a flat element
32
+ * list ({@link umlComponentSiblings}) — resolved by group membership plus the
33
+ * role stamped on each text, never by position in `children`.
34
+ *
35
+ * The role stays on the SHAPE and on each text; the group carries none
36
+ * (`roles.ts`).
37
+ */
38
+ /* ── The metrics ───────────────────────────────────────────────────────── */
39
+ /**
40
+ * The gap between a compartment's words and the line that closes it, and
41
+ * between the words and the outline of the box.
42
+ *
43
+ * One number for the top, the bottom and — through {@link UML_TIER_SIDE_INSET}
44
+ * — the sides, because UML draws one: the specification's figures are typeset
45
+ * with a single uniform padding inside every compartment, and a second value
46
+ * would read as a distinction between compartments that the notation never
47
+ * makes.
48
+ */
49
+ export const UML_TIER_MARGIN = 8;
50
+ /**
51
+ * The side padding, as a FRACTION of the box width rather than the absolute
52
+ * {@link UML_TIER_MARGIN}.
53
+ *
54
+ * A UML box is resized far more often horizontally than vertically — an author
55
+ * widens a class to fit `+ findByCustomerId(id : CustomerId) : Order`, they do
56
+ * not make it taller to fit a longer word. A proportional inset keeps the text
57
+ * column looking deliberate at every width; an absolute one would leave a
58
+ * 200-unit box looking padded and a 600-unit box looking unpadded.
59
+ */
60
+ export const UML_TIER_SIDE_INSET = 0.08;
61
+ /**
62
+ * Line height, as a multiple of the font size — the one number that turns
63
+ * "three lines of attributes" into a height.
64
+ */
65
+ export const UML_TIER_LINE_HEIGHT = 1.4;
66
+ /**
67
+ * The gap between the NAME and the line under it.
68
+ *
69
+ * Smaller than {@link UML_TIER_MARGIN} on purpose: the name compartment of a
70
+ * fresh classifier holds one line and, for an interface or an enumeration, a
71
+ * keyword line above it (§9.5.4 — `«interface»`), and that pair has to read as
72
+ * one heading rather than as a block floating in an over-tall compartment.
73
+ */
74
+ export const UML_NAME_GAP = 6;
75
+ /**
76
+ * How many lines the ATTRIBUTES compartment is sized for.
77
+ *
78
+ * Three, because that is what a default 200 × 120 box leaves room for once the
79
+ * name compartment and the margins are taken out, and because a classifier with
80
+ * three attributes and one or two operations is the shape of nearly every class
81
+ * anybody draws first. The operations compartment takes whatever is left, which
82
+ * is the one tier that must never be sized in lines: it is the bottom of the
83
+ * box, and a box an author has dragged taller has dragged it taller FOR the
84
+ * operations.
85
+ *
86
+ * Since the compartment watcher this is the DEFAULT rather than the sizing: a
87
+ * caller that has read the tier passes its real line count
88
+ * ({@link UmlTierLines}), and three is what a caller who has not read it gets —
89
+ * the stencil's own shape, which is every classifier's until somebody types.
90
+ */
91
+ export const UML_ATTRIBUTE_LINES = 3;
92
+ /**
93
+ * The share of a `package` box its TAB occupies, top-left (§12.2.4).
94
+ *
95
+ * A fraction rather than an absolute, because the tab is part of the DRAWING:
96
+ * a package dragged to twice the height is the same folder, twice as big, and a
97
+ * tab pinned to 26 units would turn into a sliver. Exported so the node
98
+ * renderer strokes the tab exactly where the name compartment stops, rather
99
+ * than the two agreeing by coincidence.
100
+ */
101
+ export const UML_PACKAGE_TAB = 0.2;
102
+ /**
103
+ * The share of an `actor` box the STICK FIGURE occupies, measured from the top
104
+ * (§18.1.4 — the figure, and its name written underneath it).
105
+ *
106
+ * Exported for the same reason as {@link UML_PACKAGE_TAB}: the renderer draws
107
+ * the figure ABOVE this line and {@link umlCompartmentBoxes} puts the name
108
+ * BELOW it, and a head overlapping its own label is what happens when two files
109
+ * each pick their own number.
110
+ */
111
+ export const UML_ACTOR_FIGURE = 0.74;
112
+ /**
113
+ * The DEPTH of the 3-D cube a node, a device and an execution environment are
114
+ * drawn as (§19.4.4), as a fraction of the box's shorter side.
115
+ *
116
+ * A fraction of the SHORTER side rather than of the width, because the depth is
117
+ * drawn at 45° in both directions: taking it from the width alone would make a
118
+ * cube dragged wide and flat lose its top face off the top of the element.
119
+ *
120
+ * Exported for the reason {@link UML_PACKAGE_TAB} is: the renderer draws the
121
+ * three faces from this number and {@link umlCompartmentBoxes} writes the name
122
+ * INSIDE the front one, so a name floating over the cube's roof is what happens
123
+ * when two files each pick their own depth. 12% is the proportion §19.4.4's own
124
+ * figures show — enough to read as a solid, little enough to leave the front
125
+ * face the size of a box.
126
+ */
127
+ export const UML_CUBE_DEPTH = 0.12;
128
+ /**
129
+ * How many lines the name written in a cube's front face is sized for.
130
+ *
131
+ * Two, because a deployment target's seed is a keyword line over an instance
132
+ * name — `«device»` over `:Device` (§19.4.4) — which is the same pair a
133
+ * classifier's name compartment holds and the same reason it gets two lines.
134
+ */
135
+ const CUBE_LABEL_LINES = 2;
136
+ /**
137
+ * The gap between a glyph too small to write in and the name written BESIDE it.
138
+ *
139
+ * Three kinds are in that position, and all three by the notation's own doing: a
140
+ * port is a small square on a border (§11.3.4), and the ball and socket of
141
+ * §10.4.4 are a curve on a stub. None of them has an inside, so the label goes
142
+ * next to the picture — which is exactly how the specification's figures write
143
+ * them.
144
+ */
145
+ export const UML_BESIDE_LABEL_GAP = 6;
146
+ /**
147
+ * How wide that label is allowed to be before it wraps.
148
+ *
149
+ * An absolute rather than a fraction, unlike every other measurement here,
150
+ * because it is measuring the WORDS and not the glyph: a port dragged twice as
151
+ * big is a bigger square with the same name beside it, and a name column scaled
152
+ * to a 16-unit square would be four characters wide.
153
+ */
154
+ export const UML_BESIDE_LABEL_WIDTH = 120;
155
+ /**
156
+ * How deep the POINT of a signal pentagon bites into its box (§16.3.4), as a
157
+ * fraction of the width.
158
+ *
159
+ * Exported for the reason {@link UML_PACKAGE_TAB} is: the renderer draws the
160
+ * pentagon from this number and {@link umlCompartmentBoxes} pulls the label box
161
+ * back out of the point by it, so a signal's name cannot end up running out
162
+ * through the tip. One number, two readers.
163
+ *
164
+ * The same depth for both shapes, because §16.3.4 draws the same pentagon twice
165
+ * and merely turns it: a send signal's point sticks OUT of the right edge, an
166
+ * accept event's bites IN from the left, and a reader tells them apart by which
167
+ * way the arrow of the silhouette runs. Two depths would make the pair look like
168
+ * two unrelated shapes.
169
+ */
170
+ export const UML_SIGNAL_POINT = 0.16;
171
+ /**
172
+ * How many lines a use case's label is sized for.
173
+ *
174
+ * Two, where every other label gets one: a use case is named with a VERB PHRASE
175
+ * (§18.1.4 — "Passer une commande", "Consulter le solde"), which is the longest
176
+ * label in the pack, and it is centred in an ELLIPSE, whose widest chord is the
177
+ * only place two lines fit comfortably.
178
+ */
179
+ const USE_CASE_LABEL_LINES = 2;
180
+ /**
181
+ * How many lines a lifeline's head is sized for.
182
+ *
183
+ * Two, and the same argument the cube's makes: §17.3.4's head holds
184
+ * `<name> : <Type>`, which is one line until a participant's type is long
185
+ * enough to wrap — and a head that clipped the classifier would be a lifeline
186
+ * that does not say what it is an instance of.
187
+ */
188
+ const LIFELINE_LABEL_LINES = 2;
189
+ /** A tier's line count, floored at one line, or the notation's default. */
190
+ const tierLines = (lines, fallback) => lines === undefined ? fallback : Math.max(1, Math.floor(lines));
191
+ /**
192
+ * How many lines a tier's text is PAINTED as.
193
+ *
194
+ * Takes the VALUE rather than the element, for the reason {@link umlTierText}
195
+ * does — a `Y.Text` on a model, a string in a fixture. The author's newlines are
196
+ * counted always; `wrap` adds the ones the renderer makes, and without it the
197
+ * count is the newlines alone.
198
+ *
199
+ * ## Why `wrap` is not optional in practice
200
+ *
201
+ * It was, until the PO's recette of 14/09/2026. A tier is created with
202
+ * `hasMaxWidth`, so the text editor wraps a long signature at the compartment's
203
+ * width and grows the box downward while the author types — and the watcher then
204
+ * SHRANK that box back to one line's worth on commit, painting the wrapped
205
+ * remainder straight through the separator under it. Measured on a 200-unit
206
+ * class: three typed attribute lines, six painted, a 92.7-unit stack put back
207
+ * into a 54.6-unit compartment.
208
+ *
209
+ * So every caller that can measure must: `umlFitComponent` does, through
210
+ * `umlTierWrapper` (`node/tier-metrics.ts`), and the `wrap`-less form is what a
211
+ * fixture and a pure test get.
212
+ *
213
+ * An empty tier is one line, which is `\n`-count + 1 and also what §14.2.4
214
+ * wants: a state's behaviour compartment is seeded EMPTY and is still ruled off.
215
+ */
216
+ export function umlTierLineCount(text, wrap) {
217
+ if (text === null || text === undefined)
218
+ return 1;
219
+ const logical = String(text).split('\n');
220
+ if (!wrap)
221
+ return logical.length;
222
+ // `Math.max(1, …)` so a measurer that answers 0 for an empty line — which a
223
+ // wrapper built on a string split can — still counts the blank line the
224
+ // renderer paints.
225
+ return logical.reduce((total, line) => total + Math.max(1, wrap(line)), 0);
226
+ }
227
+ /** The kinds drawn as a divided rectangle — everything with a compartment. */
228
+ const COMPARTMENTED = new Set([
229
+ 'class',
230
+ 'interface',
231
+ 'enumeration',
232
+ 'object',
233
+ // §11.6.4 and §19.3.4 draw both as a rectangle with a name over a body, the
234
+ // icon in the corner being the only thing that tells them from a class. Two
235
+ // tiers, one separator — the object's layout exactly.
236
+ 'component',
237
+ 'artifact',
238
+ // §14.2.4 draws a state as a round-cornered rectangle with a NAME
239
+ // COMPARTMENT over its internal activities (`entry /`, `do /`, `exit /`),
240
+ // ruled off by a line — the object's layout with rounded corners. The one
241
+ // behaviour kind that is a divided box rather than a picture or a mark.
242
+ 'state',
243
+ ]);
244
+ /**
245
+ * The kinds whose second tier is a BODY rather than a feature list — a name
246
+ * compartment, one separator, and everything below it.
247
+ *
248
+ * Three different statements sharing one layout, and it is worth saying why they
249
+ * are not one: an object's second tier holds SLOTS (§9.8.4 — an instance has
250
+ * values, not behaviour), a component's holds its parts or its realized
251
+ * interfaces (§11.6.4), an artifact's holds what the file contains (§19.3.4).
252
+ * The notation draws them the same way, so this module does; the ROLE on the
253
+ * text element is what keeps them apart for everybody else.
254
+ */
255
+ const ONE_SPLIT = new Set([
256
+ 'object',
257
+ 'component',
258
+ 'artifact',
259
+ // A state's second tier holds its INTERNAL ACTIVITIES (§14.2.4) — the
260
+ // `entry / …`, `do / …`, `exit / …` lines a machine runs while it rests
261
+ // there. A fourth statement on the same layout, and the role on the text is
262
+ // again what keeps it apart from the other three.
263
+ //
264
+ // A fresh state's behaviour tier arrives EMPTY, which §14.2.4's own figures
265
+ // draw: a named state with a ruled-off compartment waiting to be filled. The
266
+ // alternative — hiding the rule until somebody types — would mean deciding
267
+ // the layout from the text, which no pure function of a box can do.
268
+ 'state',
269
+ ]);
270
+ /** The three deployment targets, drawn as the same cube (§19.4.4). */
271
+ const CUBES = new Set(['node', 'device', 'execution-environment']);
272
+ /**
273
+ * The three glyphs with no INSIDE, whose name is therefore written next to them
274
+ * — outside the element's own box, which nothing else in this module does.
275
+ *
276
+ * Exported because that is a fact a caller has to know: a port's label is the
277
+ * one tier that makes the GROUP bigger than the shape it belongs to, and a test
278
+ * or a layout asserting "every tier is inside its node" has to say which three
279
+ * kinds the notation exempts (§11.3.4, §10.4.4).
280
+ */
281
+ export const UML_BESIDE_LABEL_KINDS = new Set([
282
+ 'port',
283
+ 'provided-interface',
284
+ 'required-interface',
285
+ // §16.10.4's hourglass is the fourth, and the notation puts it there too:
286
+ // the time expression — `after (2 days)` — is written NEXT TO the glyph
287
+ // because a 40 × 56 hourglass has no inside, exactly like a port.
288
+ 'time-event',
289
+ ]);
290
+ /**
291
+ * The kinds whose one tier is written in a HEAD drawn wider than the element
292
+ * itself — today the lifeline, and it is the second and last way a tier is
293
+ * allowed outside its own box.
294
+ *
295
+ * ## Why a lifeline is shaped like this at all
296
+ *
297
+ * §17.2.4 draws a lifeline as a NAMED BOX with a dashed line falling out of the
298
+ * bottom of it, and the dashed line is the part that matters: a message is
299
+ * attached to it at the height where it happens, and an execution bar sits on
300
+ * it. So the ELEMENT is the spine — a narrow, tall column whose left and right
301
+ * perimeters are where the connector layer puts an anchor — and the head is a
302
+ * rectangle the renderer draws across the top of it, centred, overflowing it by
303
+ * the same amount on each side (`consts.ts`, `UML_LIFELINE_HEAD`).
304
+ *
305
+ * That makes the head the one place the participant's name can go. Writing it
306
+ * inside a 16-unit column would be writing it on the dashes.
307
+ *
308
+ * ## Why it is not {@link UML_BESIDE_LABEL_KINDS}
309
+ *
310
+ * Because it is not beside anything: a port's name sits to the RIGHT of a glyph
311
+ * that has no inside, and a lifeline's name sits IN a head the renderer draws.
312
+ * The two would share nothing but the fact that a bounding box has to allow for
313
+ * them, and folding them together would give a lifeline a label floating 6
314
+ * units off the spine. Two sets, two placements, one invariant each.
315
+ */
316
+ export const UML_HEAD_LABEL_KINDS = new Set(['lifeline']);
317
+ /**
318
+ * WHICH tier role a kind's single word carries, for every kind whose word is a
319
+ * label rather than a name compartment.
320
+ *
321
+ * The companion of {@link glyphLabel}: that answers where the one tier GOES,
322
+ * this answers what it IS, and the two questions have had the same shape since
323
+ * the lifeline arrived — a placement chosen per kind and a role chosen per
324
+ * kind, which is exactly one table each.
325
+ *
326
+ * Today it says one thing: a lifeline's head carries `uml:lifeline-ident` and
327
+ * everything else carries `uml:label`. §17.3.4 prints a BNF for what goes in
328
+ * that head (`parseLifelineIdent` reads it back) and §18.1.4 prints none for an
329
+ * actor's word, so the two tiers answer "does this parse" differently and a
330
+ * single role could only ever have asked them the same question (`roles.ts`).
331
+ *
332
+ * It does NOT answer for the compartmented kinds or for the five glyphs named
333
+ * rather than labelled (`actions.ts`, `NAMED_GLYPHS`): those carry `uml:name`,
334
+ * which is a different statement — a tier a keyword may be written over — and a
335
+ * helper that folded all three answers together would be a second place to
336
+ * decide what the grammar reads.
337
+ */
338
+ export function umlLabelRoleOf(kind) {
339
+ return UML_HEAD_LABEL_KINDS.has(kind)
340
+ ? UML_ROLE['lifeline-ident']
341
+ : UML_ROLE.label;
342
+ }
343
+ /**
344
+ * The compartments of a node, laid out against its own box.
345
+ *
346
+ * ## The rhythm of a classifier
347
+ *
348
+ * A margin, the name over one line, a small gap, THE LINE, three lines of
349
+ * attributes, THE SECOND LINE, and the operations take the rest down to the
350
+ * bottom margin. Which is exactly what a 200 × 120 box is tall enough for,
351
+ * because the box was derived from this stack (`consts.ts`) rather than the
352
+ * stack fitted into the box — so a fresh class is neither cramped nor half
353
+ * empty.
354
+ *
355
+ * The tiers are stacked by walking DOWN, each placed under the last, rather
356
+ * than by six independent offsets: six offsets is six chances for two
357
+ * compartments to overlap, and a walk cannot produce one.
358
+ *
359
+ * An `object` walks the same path and stops one tier early: §9.8.4 draws an
360
+ * instance specification as a name compartment over a SLOT compartment, and
361
+ * there is no third. So it gets one split, not two, and the slots take the rest
362
+ * of the box — the tier an author of an object diagram actually types in. A
363
+ * `component` (§11.6.4) and an `artifact` (§19.3.4) walk exactly the same path,
364
+ * for exactly the same reason: a name over one body tier.
365
+ *
366
+ * ## The ones that are not boxes
367
+ *
368
+ * A package, a note, an actor, a use case, the three cubes and the three
369
+ * interface glyphs are SHAPES the notation draws, not rectangles it divides, so
370
+ * each returns a single label box positioned against its own picture and no
371
+ * splits at all:
372
+ *
373
+ * - **package** — under the tab, centred in the body (§12.2.4);
374
+ * - **note** — the inner rectangle, top-aligned and read left, because a note
375
+ * holds a sentence rather than a title (Annex A);
376
+ * - **actor** — under the stick figure, which is where §18.1.4 writes it;
377
+ * - **use case** — centred in the ellipse, the only shape with no corner to
378
+ * hang a label in;
379
+ * - **node / device / execution-environment** — inside the cube's FRONT FACE,
380
+ * the only one of its three faces that is not drawn at an angle (§19.4.4);
381
+ * - **port / provided-interface / required-interface / time-event** — BESIDE
382
+ * the glyph, and outside the element's own box: a small square, a ball on a
383
+ * stick and a 40-unit hourglass have no inside to write in (§11.3.4,
384
+ * §10.4.4, §16.10.4). The one place this module returns a tier that is not
385
+ * contained by the node it belongs to, and
386
+ * {@link UML_BESIDE_LABEL_KINDS} is how it says so out loud;
387
+ * - **action / object-node** — centred in the box, which is where §15.3.4 and
388
+ * §15.4.4 write a verb phrase and a value's name;
389
+ * - **send-signal / accept-event** — centred too, but pulled back out of the
390
+ * pentagon's POINT by {@link UML_SIGNAL_POINT}, so the words never run out
391
+ * through the tip (§16.3.4).
392
+ *
393
+ * ## The ones with no words at all
394
+ *
395
+ * The control nodes of §15.3.4 and the pseudostates of §14.2.4 are MARKS — a
396
+ * disc, a bar, a bullseye, a cross — and carry no tier. They still get a box
397
+ * back from here, centred, because a box is always computable from a box and a
398
+ * second return shape would cost every caller a branch. What decides whether a
399
+ * text element is ever created is `UML_UNLABELLED_KINDS` in `keywords.ts`, and
400
+ * that is the one authority on the question.
401
+ *
402
+ * ## What this is not
403
+ *
404
+ * A creation-time answer, a render-time one and a commit-time one, and nothing
405
+ * more. The compartments are real elements from the moment they are drawn: an
406
+ * author who moves one has moved it, and nothing here runs on its own to put it
407
+ * back. The margins are absolutes, so a node dragged taller keeps them where
408
+ * they were and gives the extra height to the tier that takes "the rest".
409
+ *
410
+ * The commit-time caller is `UmlCompartmentWatcher` (`node/`), and it is the
411
+ * one that passes `lines`: when an edit into a tier commits, it re-asks this
412
+ * function where the compartments go now that the second one holds five lines
413
+ * instead of three, grows the node if the answer no longer fits
414
+ * ({@link umlStackHeight}) and writes the tiers to the boxes it gets back. It
415
+ * is still not a layout engine — nothing runs between keystrokes, and nothing
416
+ * reclaims height an author has dragged out.
417
+ */
418
+ export function umlCompartmentBoxes(kind, x, y, w, h, lines) {
419
+ const inset = w * UML_TIER_SIDE_INSET;
420
+ const width = w - inset * 2;
421
+ const nameLine = UML_NAME_FONT_SIZE * UML_TIER_LINE_HEIGHT;
422
+ const box = (top, height) => ({
423
+ x: x + inset,
424
+ y: y + top,
425
+ w: width,
426
+ h: height,
427
+ });
428
+ if (!COMPARTMENTED.has(kind)) {
429
+ // A glyph's label is placed against its PICTURE — beside a port, inside a
430
+ // cube's front face, under a stick figure — and a picture has no stack to
431
+ // grow. So `lines` is deliberately not read here: it would make an actor's
432
+ // head and its name overlap the moment somebody typed a second line, and
433
+ // the fix for that is a bigger element, not a taller label.
434
+ return {
435
+ name: glyphLabel(kind, { box, x, y, w, h, nameHeight: nameLine }),
436
+ splits: [],
437
+ };
438
+ }
439
+ // The walk. `top` is an offset from the node's top edge throughout, which is
440
+ // also the unit `splits` is in — so a separator is simply the value of `top`
441
+ // at the moment one compartment closes and the next opens.
442
+ const nameHeight = nameLine * tierLines(lines?.name, 1);
443
+ let top = UML_TIER_MARGIN;
444
+ const name = box(top, nameHeight);
445
+ top += nameHeight + UML_NAME_GAP;
446
+ const splits = [top];
447
+ if (ONE_SPLIT.has(kind)) {
448
+ // A name over a single body tier, one line between them, and the body takes
449
+ // everything down to the bottom margin: an instance's slots (§9.8.4), a
450
+ // component's parts (§11.6.4), an artifact's contents (§19.3.4).
451
+ return {
452
+ name,
453
+ attributes: box(top, Math.max(0, h - top - UML_TIER_MARGIN)),
454
+ splits,
455
+ };
456
+ }
457
+ const attributesHeight = UML_BODY_FONT_SIZE *
458
+ UML_TIER_LINE_HEIGHT *
459
+ tierLines(lines?.attributes, UML_ATTRIBUTE_LINES);
460
+ const attributes = box(top, attributesHeight);
461
+ top += attributesHeight;
462
+ splits.push(top);
463
+ return {
464
+ name,
465
+ attributes,
466
+ operations: box(top, Math.max(0, h - top - UML_TIER_MARGIN)),
467
+ splits,
468
+ };
469
+ }
470
+ /**
471
+ * How tall the node has to BE for the words its tiers hold to fit — `null` for
472
+ * a kind that has no stack.
473
+ *
474
+ * The same walk as {@link umlCompartmentBoxes}, read the other way round: that
475
+ * function is given a box and hands back where the compartments go, this one is
476
+ * given the compartments and hands back the box they need. Both are here so
477
+ * they cannot disagree, which is the whole reason the separators were computed
478
+ * in this module in the first place.
479
+ *
480
+ * The last tier is the one difference between the two. `umlCompartmentBoxes`
481
+ * gives the operations compartment (or a one-split kind's body) EVERYTHING left
482
+ * down to the bottom margin, because a box an author has dragged taller has
483
+ * dragged it taller for them; this returns the height at which "everything
484
+ * left" is exactly the lines it holds, which is the smallest box that fits.
485
+ *
486
+ * `null` rather than a number for a package, an actor, a cube or a mark: those
487
+ * are pictures the notation draws, not rectangles it divides, and there is no
488
+ * honest answer to "how tall must this be" for a stick figure. It doubles as
489
+ * the predicate — a caller that gets a number is looking at a divided box.
490
+ */
491
+ export function umlStackHeight(kind, lines) {
492
+ if (!COMPARTMENTED.has(kind))
493
+ return null;
494
+ const bodyLine = UML_BODY_FONT_SIZE * UML_TIER_LINE_HEIGHT;
495
+ const head = UML_TIER_MARGIN +
496
+ UML_NAME_FONT_SIZE * UML_TIER_LINE_HEIGHT * tierLines(lines.name, 1) +
497
+ UML_NAME_GAP;
498
+ if (ONE_SPLIT.has(kind)) {
499
+ return head + bodyLine * tierLines(lines.attributes, 1) + UML_TIER_MARGIN;
500
+ }
501
+ return (head +
502
+ bodyLine * tierLines(lines.attributes, UML_ATTRIBUTE_LINES) +
503
+ bodyLine * tierLines(lines.operations, 1) +
504
+ UML_TIER_MARGIN);
505
+ }
506
+ /** The single label box of every kind that is a picture, not a divided box. */
507
+ function glyphLabel(kind, site) {
508
+ const { box, x, y, w, h, nameHeight } = site;
509
+ if (CUBES.has(kind)) {
510
+ // Inside the FRONT FACE, which is the only face a name can be written on:
511
+ // the other two are drawn at 45° and words on them would be words on a
512
+ // roof. Centred in it, with the same proportional gutter every other tier
513
+ // gets — measured against the face's width, not the element's, or a cube
514
+ // would look padded on the left and flush on the right (§19.4.4).
515
+ const depth = Math.min(w, h) * UML_CUBE_DEPTH;
516
+ const faceW = Math.max(0, w - depth);
517
+ const faceH = Math.max(0, h - depth);
518
+ const inset = faceW * UML_TIER_SIDE_INSET;
519
+ const height = Math.min(nameHeight * CUBE_LABEL_LINES, faceH);
520
+ return {
521
+ x: x + inset,
522
+ y: y + depth + (faceH - height) / 2,
523
+ w: Math.max(0, faceW - inset * 2),
524
+ h: height,
525
+ };
526
+ }
527
+ if (UML_HEAD_LABEL_KINDS.has(kind)) {
528
+ // Inside the HEAD, which is the only part of a lifeline that is a box: the
529
+ // rest of the element is a dashed spine 16 units wide (§17.2.4). The head
530
+ // is centred on the column and flush with its top, so its left edge is a
531
+ // negative offset from the element's own — the one measurement in this
532
+ // module that reaches to the LEFT of the node it belongs to.
533
+ //
534
+ // The same proportional gutter every other tier gets, measured against the
535
+ // HEAD's width rather than the element's — the cube's argument, for the
536
+ // same reason: a 16-unit inset would be the whole column.
537
+ const headW = UML_LIFELINE_HEAD.w;
538
+ const headH = Math.min(UML_LIFELINE_HEAD.h, h);
539
+ const inset = headW * UML_TIER_SIDE_INSET;
540
+ const height = Math.min(nameHeight * LIFELINE_LABEL_LINES, headH);
541
+ return {
542
+ x: x + (w - headW) / 2 + inset,
543
+ y: y + (headH - height) / 2,
544
+ w: Math.max(0, headW - inset * 2),
545
+ h: height,
546
+ };
547
+ }
548
+ if (UML_BESIDE_LABEL_KINDS.has(kind)) {
549
+ // BESIDE the glyph, and deliberately outside the element's own box: a port
550
+ // is a 16-unit square and a lollipop is a ball on a stick, so there is
551
+ // nowhere in either of them for a word to go (§11.3.4, §10.4.4). To the
552
+ // RIGHT and vertically centred on the glyph, which is how the
553
+ // specification's own figures set them, and the one placement that reads
554
+ // the same whichever border of a component the port has been dragged onto.
555
+ return {
556
+ x: x + w + UML_BESIDE_LABEL_GAP,
557
+ y: y + (h - nameHeight) / 2,
558
+ w: UML_BESIDE_LABEL_WIDTH,
559
+ h: nameHeight,
560
+ };
561
+ }
562
+ switch (kind) {
563
+ case 'package': {
564
+ const tab = h * UML_PACKAGE_TAB;
565
+ return box(tab + (h - tab - nameHeight) / 2, nameHeight);
566
+ }
567
+ case 'note':
568
+ // The whole inner rectangle: a note is words first and a shape second,
569
+ // and its text starts at the top because it is a paragraph.
570
+ return box(UML_TIER_MARGIN, Math.max(0, h - UML_TIER_MARGIN * 2));
571
+ case 'actor':
572
+ return box(h * UML_ACTOR_FIGURE, nameHeight);
573
+ case 'send-signal':
574
+ case 'accept-event': {
575
+ // The pentagon's words are centred like an action's, then pulled back out
576
+ // of the POINT: a send signal's tip sticks out to the right and an accept
577
+ // event's notch bites in from the left, so each gives up one side
578
+ // (§16.3.4). Measured with the same number the renderer draws the tip
579
+ // from, so a name can never run out through it.
580
+ const point = w * UML_SIGNAL_POINT;
581
+ const height = Math.min(nameHeight * USE_CASE_LABEL_LINES, h);
582
+ const inset = w * UML_TIER_SIDE_INSET;
583
+ const left = kind === 'accept-event' ? x + point + inset : x + inset;
584
+ return {
585
+ x: left,
586
+ y: y + (h - height) / 2,
587
+ w: Math.max(0, w - point - inset * 2),
588
+ h: height,
589
+ };
590
+ }
591
+ default: {
592
+ // The use case, the action, the object node, every mark that carries no
593
+ // words at all, and anything a later phase adds without saying where its
594
+ // label goes: centred, which is the answer that is never wrong.
595
+ //
596
+ // Clamped to the element, which the phase-2 marks made necessary: two
597
+ // lines of 16px face is 45 units and a junction is 16 tall, so an
598
+ // unclamped box would hang half of itself off the top of the dot and
599
+ // break the one invariant every other tier keeps — a tier is inside the
600
+ // node it belongs to, unless {@link UML_BESIDE_LABEL_KINDS} says
601
+ // otherwise.
602
+ const height = Math.min(nameHeight * USE_CASE_LABEL_LINES, h);
603
+ return box((h - height) / 2, height);
604
+ }
605
+ }
606
+ }
607
+ /**
608
+ * The roles a SHAPE may carry — the values of `UML_ROLE_OF_KIND`, as a set.
609
+ *
610
+ * Derived rather than listed so a kind added to the pack is recognised here the
611
+ * day its role is mapped, and not the day somebody notices that its words stop
612
+ * resolving. Which of the eight it is does not matter to a resolution: it is
613
+ * looking for "the shape in this group", and every node role answers that.
614
+ */
615
+ const NODE_ROLES = new Set(Object.values(UML_ROLE_OF_KIND));
616
+ /**
617
+ * Everything one group holds, sorted into the shape and its tiers.
618
+ *
619
+ * ## Group membership, then roles
620
+ *
621
+ * The group answers "which words are THIS classifier's" — two classes side by
622
+ * side both carry a `+ attribute : Type`, and only the grouping says which is
623
+ * which. The role then answers "which of these words is the operations
624
+ * compartment", which position in `children` cannot: a group's child order is
625
+ * an implementation detail that a reorder, a copy or a regroup rewrites, while
626
+ * a role is written on the element and travels with it.
627
+ *
628
+ * ## `name` and `label` are two fields, not one
629
+ *
630
+ * Because they are two roles, and the vocabulary means them differently: a
631
+ * `uml:name` is the name of a CLASSIFIER, parsed by the grammar for keywords in
632
+ * guillemets and for an abstract name in italics (§9.5.4); a `uml:label` is the
633
+ * plain text of an actor or a use case, which is a name and nothing else. An
634
+ * exporter that had to ask "is this the name field or the label field" of one
635
+ * merged slot would be asking about the element's KIND, which the shape already
636
+ * states. Keeping them apart costs one field and removes the question.
637
+ *
638
+ * The FIRST element of each role wins. A group holding two names is a group
639
+ * somebody built by hand out of two components, and document order is the same
640
+ * tie-break the export's geometric attribution uses.
641
+ */
642
+ export function umlComponentSiblings(group, elements) {
643
+ const children = new Set(group.childIds);
644
+ const component = {};
645
+ for (const element of elements) {
646
+ if (!children.has(element.id))
647
+ continue;
648
+ const role = element.role;
649
+ if (role === undefined)
650
+ continue;
651
+ if (NODE_ROLES.has(role))
652
+ component.node ??= element;
653
+ else if (role === UML_ROLE.name)
654
+ component.name ??= element;
655
+ else if (role === UML_ROLE.attributes)
656
+ component.attributes ??= element;
657
+ else if (role === UML_ROLE.operations)
658
+ component.operations ??= element;
659
+ else if (role === UML_ROLE.label)
660
+ component.label ??= element;
661
+ else if (role === UML_ROLE['lifeline-ident'])
662
+ component.ident ??= element;
663
+ }
664
+ return component;
665
+ }
666
+ /**
667
+ * The group an element belongs to — `undefined` when it belongs to none.
668
+ *
669
+ * The other direction of the same question, and the one the node view needs:
670
+ * given the shape somebody has just double-clicked, which group holds the text
671
+ * to open an editor on? The C4 pack answers it with `c4ComponentSiblings(id,
672
+ * groups)`; this returns the group itself rather than its id list, because the
673
+ * next call is always {@link umlComponentSiblings}, which takes exactly that.
674
+ *
675
+ * The FIRST group holding the element wins. Groups nest, so a component grouped
676
+ * again inside a bigger group has two ancestors; document order picks the one
677
+ * written first, which is the innermost — the one the creation site made.
678
+ */
679
+ export function umlGroupOf(elementId, groups) {
680
+ return groups.find(group => group.childIds.includes(elementId));
681
+ }
682
+ /**
683
+ * The shape of a resolved component — its `umlNode`, or `undefined` for a group
684
+ * that holds none.
685
+ *
686
+ * A one-line accessor with a name, rather than `component.node` at eleven call
687
+ * sites, because the QUESTION is worth spelling: "which box are these words
688
+ * about". The exporter asks it to read a `kind`, the renderer to read a
689
+ * geometry, and a rule to read a role.
690
+ */
691
+ export function umlNodeOfComponent(component) {
692
+ return component.node;
693
+ }
694
+ /* ── What a tier actually says ─────────────────────────────────────────── */
695
+ /**
696
+ * Whatever a tier says, as a plain trimmed string.
697
+ *
698
+ * Takes the VALUE rather than the element, because that is what every caller
699
+ * has: a `Y.Text` off a model, a string off a fixture, `undefined` off a tier
700
+ * that was never created. Stringifying here is what lets the grammar, the
701
+ * exporter and the renderer stay free of a Yjs import — a `Y.Text` answers
702
+ * `toString()` with its content, which is the whole of the coupling.
703
+ */
704
+ export function umlTierText(text) {
705
+ if (text === null || text === undefined)
706
+ return '';
707
+ return String(text).trim();
708
+ }