@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
package/dist/morph.js ADDED
@@ -0,0 +1,496 @@
1
+ import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { GroupElementModel, TextElementModel, UmlNodeElementModel, } from '@formicoidea/labre-core/model';
3
+ import { umlCommandIcons, umlCommands } from './commands.js';
4
+ import { GUILLEMET_CLOSE, GUILLEMET_OPEN, UML_NAME_SEED } from './keywords.js';
5
+ import { umlFitComponent } from './node/fit.js';
6
+ import { umlMorphClears, umlMorphProps } from './presets.js';
7
+ import { UML_ROLE, UML_ROLE_OF_KIND } from './roles.js';
8
+ /**
9
+ * What a UML artefact may BECOME — the declaration behind the "Change type"
10
+ * dropdown on a selected component's contextual toolbar.
11
+ *
12
+ * ## Six families, and the kinds that are alone
13
+ *
14
+ * A class, an interface and an enumeration are THE SAME PICTURE: §9.5.4 draws
15
+ * one divided rectangle for all three and tells them apart by the keyword
16
+ * written above the name — `«interface»`, `«enumeration»`, nothing at all. That
17
+ * is the strongest statement of "the same artefact, said more precisely" any
18
+ * notation in this library makes.
19
+ *
20
+ * Phase 2 adds the second, and it is the same statement in a different clause:
21
+ * a **node**, a **device** and an **execution environment** are ONE 3D box
22
+ * (§19.4.4), told apart by `«device»` / `«executionEnvironment»` above the name
23
+ * and by nothing else. A deployment diagram is drawn coarse and then made
24
+ * precise — "this runs on a server", then "on that server's application
25
+ * container" — so the swap is the modelling act itself rather than a repair.
26
+ *
27
+ * Everything else is declared alone, and each singleton is a REFUSAL written
28
+ * down rather than an omission to be discovered later:
29
+ *
30
+ * - an **object** is an InstanceSpecification (§11.6), a thing that exists at
31
+ * run time. A class is a type. Offering the swap would invite a diagram that
32
+ * confuses the two levels, and its name compartment is `object : Class`
33
+ * underlined — different words as well as a different meaning;
34
+ * - a **package** is a namespace (§12.2), not a classifier in it;
35
+ * - a **note** is a Comment (Annex A) and carries no semantics at all;
36
+ * - an **actor** and a **use case** belong to a behaviour diagram (§18.1) and
37
+ * are not structural classifiers;
38
+ * - a **component** is a Class (§11.6) and would in principle join the first
39
+ * family, but it is the one classifier the notation draws with a MARK of its
40
+ * own — the two-tab icon in the corner — rather than with a keyword, and a
41
+ * component diagram is a different conversation from a class diagram. Left
42
+ * alone until a PO asks for the swap;
43
+ * - an **artifact** is a physical file (§19.3) — a `.war`, a script, a
44
+ * document. Offering to turn one into a class would invite a diagram that
45
+ * confuses what a thing IS with what it is deployed AS, which is the very
46
+ * distinction the `«manifest»` arrow exists to draw;
47
+ * - a **port** is a property of its owner (§11.3), not a classifier standing
48
+ * beside it, and it is the only artefact in the pack whose position is part
49
+ * of its meaning: it sits ON a border;
50
+ * - a **provided interface** and a **required interface** are the ball and the
51
+ * socket of §10.4.4 — two DIFFERENT drawings, unlike every family above, and
52
+ * a phase-2 pack should not decide by itself that a modeller who drew one
53
+ * meant the other. The pair is the obvious candidate for a third family and
54
+ * is deliberately left for a product decision.
55
+ *
56
+ * The declaration is therefore TOTAL over `UmlNodeKind` — every kind is named
57
+ * exactly once — which is what lets the unit suite prove that a kind added to
58
+ * the pack cannot be silently left out of this decision. The MENU is a different
59
+ * question, and {@link UML_MORPH_SPEC} answers it: a family of one has nothing
60
+ * to offer, so it is not offered.
61
+ *
62
+ * Families are DATA. Nothing derives them, and the role tree would have been the
63
+ * wrong source in the same way it is for BPMN and C4: `roleIsA` makes
64
+ * `uml:interface` a `uml:classifier` and thence reaches `uml:class`, but it says
65
+ * nothing about whether a reader would ACCEPT the swap — which is the only
66
+ * question this table answers.
67
+ */
68
+ export const UML_MORPH_FAMILIES = [
69
+ // Declaration order is menu order, and the family opens on the plain class:
70
+ // an undecorated rectangle is the honest first draft, the keyword is the
71
+ // refinement — the same call `commands.ts` makes about the senior sub-menu.
72
+ ['class', 'interface', 'enumeration'],
73
+ ['object'],
74
+ ['package'],
75
+ ['note'],
76
+ ['actor'],
77
+ ['use-case'],
78
+ /* ── Phase 2: components and deployment ──────────────────────────────── */
79
+ // The second family, and it opens on the plain NODE for the same reason the
80
+ // first opens on the plain class: §19.4.4's keyword is the refinement.
81
+ ['node', 'device', 'execution-environment'],
82
+ ['component'],
83
+ ['artifact'],
84
+ ['port'],
85
+ ['provided-interface'],
86
+ ['required-interface'],
87
+ /* ── Phase 2: activities and state machines ──────────────────────────── */
88
+ // Four families, and every one of them is a PAIR the notation draws as one
89
+ // picture with one mark changed — which is the only test a family has to
90
+ // pass here, because a swap inside one must move nothing (the suite proves
91
+ // the footprint and the silhouette are shared).
92
+ //
93
+ // - §16.3.4 / §16.10.4: a send signal and an accept event are the SAME
94
+ // pentagon with its angled edge on the other side, and "did this step send
95
+ // the message or wait for it" is the single most common thing to get
96
+ // backwards on an activity diagram;
97
+ // - §15.3.4: an activity final ends the whole activity, a flow final ends
98
+ // ONE token — two circles, a bullseye and a cross, and the distinction is
99
+ // a refinement an author makes after the fact;
100
+ // - §14.2.4: a shallow history restores the region's last sub-state, a deep
101
+ // history restores the whole nested configuration — `H` and `H*`, one
102
+ // asterisk apart;
103
+ // - §14.2.4: an entry point and an exit point are the circle on a composite
104
+ // state's border, hollow or crossed.
105
+ ['send-signal', 'accept-event'],
106
+ ['activity-final', 'flow-final'],
107
+ ['shallow-history', 'deep-history'],
108
+ ['entry-point', 'exit-point'],
109
+ // The rest are declared alone, and each refusal is an argument:
110
+ //
111
+ // - an **action** is a rounded rectangle the shape layer draws (§15.2.4),
112
+ // where the two signal shapes are pentagons this pack paints itself: a
113
+ // family must be silhouette-preserving, and this pair is not. It is the
114
+ // obvious candidate for a fifth family the day the three share a drawing;
115
+ // - an **initial** node is the one control node with no counterpart — there
116
+ // is exactly one per region (§15.3.4), and the swap a modeller wants from
117
+ // it is a deletion;
118
+ // - a **decision** and a **fork** are a diamond and a bar. §15.3.4 draws
119
+ // them differently because they MEAN differently — one token down one
120
+ // branch, versus one token down every branch — and a dropdown between two
121
+ // different pictures would be this pack inventing an equivalence;
122
+ // - a **choice** is the decision's diamond on a state machine and a
123
+ // **junction** its filled dot (§14.2.4); neither belongs with the activity
124
+ // node it is drawn like, because they are never on the same sheet;
125
+ // - an **object node** (§15.4.4) is data, not a step; a **time event**
126
+ // (§16.10.4) is the hourglass and nothing else is drawn as one;
127
+ // - a **state** is the rounded box of §14.2.4 and a **final state** the
128
+ // bullseye that ends it — a named thing and an unnamed one;
129
+ // - a **terminate** pseudostate is the cross that kills the machine
130
+ // (§14.2.4): there is nothing it is a more precise version of.
131
+ ['action'],
132
+ ['initial'],
133
+ ['decision'],
134
+ ['fork'],
135
+ ['object-node'],
136
+ ['time-event'],
137
+ ['state'],
138
+ ['final-state'],
139
+ ['choice'],
140
+ ['junction'],
141
+ ['terminate'],
142
+ /* ── Phase 3: the sequence vocabulary (§17.2.4) ──────────────────────── */
143
+ // Three more singletons, and the three refusals are the plainest in the
144
+ // table: these are not variations on one picture, they are the three
145
+ // different things an interaction is drawn with.
146
+ //
147
+ // - a **lifeline** is the participant itself — a head with a spine, and the
148
+ // only element on the sheet a message may attach to. There is nothing it
149
+ // is a more precise version of, and turning one into anything else would
150
+ // orphan every message drawn on it;
151
+ // - an **execution** is a bar ON a spine and a **destruction** is a cross on
152
+ // one. Both are marks, both are positioned by geometry, and what they mean
153
+ // is where they sit (§17.2.4) — "this participant is busy here" and "this
154
+ // participant ends here". A dropdown between them would be offering to
155
+ // turn a stretch of activity into an ending, which is not a refinement of
156
+ // anything: it is a different fact about a different moment.
157
+ ['lifeline'],
158
+ ['execution'],
159
+ ['destruction'],
160
+ ];
161
+ /**
162
+ * The families a MENU can be made of.
163
+ *
164
+ * A one-member family is a statement about the notation, not an affordance: a
165
+ * dropdown offering a package the choice of becoming a package is a control that
166
+ * cannot do anything, drawn on every package a user selects. The generic module
167
+ * already refuses a kind in NO family, and this gives it the same answer for a
168
+ * kind alone in one — so the table above stays total and readable while the
169
+ * toolbar stays honest.
170
+ */
171
+ const OFFERED_FAMILIES = UML_MORPH_FAMILIES.filter(family => family.length > 1);
172
+ /**
173
+ * The creation command that draws each kind, keyed BY that kind.
174
+ *
175
+ * Derived from `telemetry.element`, which is where the kind is already written
176
+ * down (`node:interface`) and is documented as a historical value that must not
177
+ * be renamed. Deriving rather than restating is what stops a second table of
178
+ * labels and icons drifting from the one the sub-menu and the catalogue read.
179
+ */
180
+ const NODE_COMMANDS = new Map(umlCommands.flatMap(command => {
181
+ const element = command.telemetry?.element;
182
+ return element?.startsWith('node:')
183
+ ? [[element.slice('node:'.length), command]]
184
+ : [];
185
+ }));
186
+ /** A kind's wording: the creation command's own key and English. */
187
+ function labelOf(kind) {
188
+ const command = NODE_COMMANDS.get(kind);
189
+ return {
190
+ key: command?.labelKey,
191
+ fallback: command?.labelFallback ?? kind,
192
+ };
193
+ }
194
+ /** A kind's icon: the creation command's own, reused rather than redrawn. */
195
+ function iconOf(kind) {
196
+ const iconKey = NODE_COMMANDS.get(kind)?.iconKey;
197
+ return (iconKey && umlCommandIcons[iconKey]) || umlCommandIcons['uml.class'];
198
+ }
199
+ /* ── Resolving the composite ───────────────────────────────────────────── */
200
+ /**
201
+ * The `umlNode` shape a selected GROUP is the component of — `undefined` when
202
+ * the group is not one.
203
+ *
204
+ * A UML artefact is a native `group` holding the shape and its tiers of words
205
+ * (`actions.ts`), so what a click selects is the group and what carries `kind`
206
+ * is the shape. This is the whole of the indirection, and it is also the gate: a
207
+ * plain group somebody lassoed round three rectangles, a C4 component (a group
208
+ * of a shape and three lines) and a group of two UML classifiers all answer
209
+ * `undefined`, so none of them is ever offered the menu.
210
+ *
211
+ * TWO shapes is a refusal rather than a first-wins pick. A group holding two
212
+ * classifiers is a group somebody made of two classifiers — morphing "it" would
213
+ * mean picking one of them by document order, and the honest answer to an
214
+ * ambiguous selection is the same one the generic module gives to a mixed
215
+ * family: nothing.
216
+ *
217
+ * ## Why this is not `component.ts`'s `umlNodeOfComponent`
218
+ *
219
+ * Because that one answers a different question with a different vocabulary: it
220
+ * takes a RESOLVED `UmlComponent` — the pure, Yjs-free shape the exporter
221
+ * and the renderer build out of ids and roles — and reads a field off it. This
222
+ * one takes a live model and walks `childElements`, because that is what the
223
+ * toolbar hands over and `instanceof` is what makes the refusals above real. The
224
+ * two are deliberately not merged: one is the pure half of the pack and must
225
+ * stay free of the model classes, and this one exists to touch them.
226
+ */
227
+ export function umlNodeOfGroup(model) {
228
+ if (!(model instanceof GroupElementModel))
229
+ return undefined;
230
+ let found;
231
+ for (const child of model.childElements) {
232
+ if (!(child instanceof UmlNodeElementModel))
233
+ continue;
234
+ if (found)
235
+ return undefined;
236
+ found = child;
237
+ }
238
+ return found;
239
+ }
240
+ /** One tier of a component, by the role it carries. */
241
+ function tierOf(model, role) {
242
+ if (!(model instanceof GroupElementModel))
243
+ return undefined;
244
+ return model.childElements.find((child) => child instanceof TextElementModel && child.role === role);
245
+ }
246
+ /**
247
+ * Write one tier, in place, if the decision says to.
248
+ *
249
+ * `decide` reads the tier as it is STORED and answers with the whole text to
250
+ * write, or `null` for "this is the author's, leave it". The comparison that
251
+ * decides whether anything happens is against the TRIMMED text — the same string
252
+ * `decide` was given — because a decision made on `'Payments '` returns
253
+ * `'«interface»\nPayments'`, and comparing that against the untrimmed original
254
+ * would call a no-op a change and spend a transaction rewriting the padding
255
+ * away.
256
+ *
257
+ * In place, in one transaction, and inside the caller's `captureSync`: the
258
+ * `Y.Text` instance is what any bound editor holds, so it is mutated rather than
259
+ * replaced, and the rewrite is part of the same single ctrl+z as the kind that
260
+ * made it necessary.
261
+ */
262
+ function rewriteTier(model, role, decide) {
263
+ const tier = tierOf(model, role);
264
+ if (!tier || tier.isLocked())
265
+ return;
266
+ const text = tier.text.toString().trim();
267
+ const next = decide(text);
268
+ if (next === null || next === text)
269
+ return;
270
+ tier.surface.store.transact(() => {
271
+ tier.text.delete(0, tier.text.length);
272
+ tier.text.insert(0, next);
273
+ });
274
+ }
275
+ /* ── The keyword line ──────────────────────────────────────────────────── */
276
+ /**
277
+ * The Annex C keyword a kind is ANNOUNCED by, or `undefined` for the kinds the
278
+ * notation announces by their silhouette.
279
+ *
280
+ * Derived from {@link UML_NAME_SEED} rather than restated, for the reason every
281
+ * derived table in this pack exists: the seed is what a fresh artefact actually
282
+ * says, and a second list of keywords would agree with it on the day it was
283
+ * written and drift on the first one that changed. A seed whose first line is a
284
+ * guillemet pair IS the keyword line — that is what `«interface»\nInterface`
285
+ * means — and a seed that has none declares a kind with none.
286
+ */
287
+ const KEYWORD_OF_KIND = Object.fromEntries(Object.keys(UML_NAME_SEED).flatMap(kind => {
288
+ const first = UML_NAME_SEED[kind].split('\n')[0];
289
+ return first.startsWith(GUILLEMET_OPEN) && first.endsWith(GUILLEMET_CLOSE)
290
+ ? [[kind, first]]
291
+ : [];
292
+ }));
293
+ /**
294
+ * The NAME line a fresh artefact of this kind carries — the seed minus its
295
+ * keyword line.
296
+ *
297
+ * `Class`, `Interface`, `Enumeration`, `object : Class`. This is the string a
298
+ * component NOBODY HAS NAMED still says, and the only name a morph is allowed to
299
+ * rewrite.
300
+ *
301
+ * ENGLISH, and knowingly so: since the creation sites resolve their seeds
302
+ * through the translation seam (`umlSeedKey`), an artefact placed in a
303
+ * translated host arrives carrying that host's word, which this table does not
304
+ * recognise. The morph then treats the name as the author's and leaves it —
305
+ * the timid half of the rule below, never the destructive one. Resolving it
306
+ * would mean handing `afterMorph` the `std` it is not given, which is the
307
+ * generic `MorphSpec`'s shape and not this pack's; C4's `c4MorphedName` carries
308
+ * exactly the same limitation, against the same seam.
309
+ */
310
+ const SEEDED_NAME = Object.fromEntries(Object.keys(UML_NAME_SEED).map(kind => {
311
+ const lines = UML_NAME_SEED[kind].split('\n');
312
+ return [kind, KEYWORD_OF_KIND[kind] ? lines.slice(1).join('\n') : lines[0]];
313
+ }));
314
+ /**
315
+ * What a name compartment should say once its shape has morphed — or `null` when
316
+ * nothing about it is the notation's to change.
317
+ *
318
+ * ## Two rewrites, and the one rule they share
319
+ *
320
+ * The rule is the same for both and it is deliberately timid: rewrite ONLY what
321
+ * the notation itself wrote, and never what an author typed. A morph is not
322
+ * allowed to take away words somebody put on the picture.
323
+ *
324
+ * - **The keyword line.** `«interface»` is not a name, it is what makes the same
325
+ * rectangle mean a different metaclass (§9.5.4) — so a class morphed to an
326
+ * interface that kept an unmarked name compartment would be an interface drawn
327
+ * exactly as a class, which is to say not drawn at all. The SOURCE kind's own
328
+ * keyword is dropped and the target's is put in its place; a first line an
329
+ * author wrote themselves (`«service»`, `«entity»` — Annex C is explicit that
330
+ * not every word in guillemets is a keyword) is not the source kind's, so it
331
+ * survives and the new keyword is written above it.
332
+ * - **The name.** Placeholder→placeholder and nothing else: the name is
333
+ * rewritten only when it is EXACTLY the source kind's own seed, which is what
334
+ * a classifier nobody has named still carries. A class called `Payments` is
335
+ * called that whatever keyword it ends up wearing, which is the PO's "the
336
+ * label is intact" for every name that is real content.
337
+ *
338
+ * Pure and total over every string, because the input is a canvas text element
339
+ * somebody may have typed anything into.
340
+ */
341
+ export function umlMorphedName(from, to, rawText) {
342
+ const text = (rawText ?? '').trim();
343
+ const lines = text.length ? text.split('\n') : [];
344
+ // The keyword line(s), if the SOURCE kind wrote one and it is still there.
345
+ // Plural for the same reason the target's loop below is: a document may
346
+ // arrive with the word already stacked.
347
+ const fromKeyword = KEYWORD_OF_KIND[from];
348
+ let body = lines;
349
+ while (fromKeyword && body[0]?.trim() === fromKeyword)
350
+ body = body.slice(1);
351
+ const toKeyword = KEYWORD_OF_KIND[to];
352
+ // …and the TARGET's, if it is ALREADY there. A keyword is written once: the
353
+ // line below puts `toKeyword` back on top, and without this a compartment that
354
+ // already opened with it would come out wearing it twice —
355
+ // `«interface»\n«interface»\nLigne`, then three, then four. The PO's recette of
356
+ // 14/09/2026 saw the stack; it is reachable whenever the shape's `kind` and
357
+ // its name compartment disagree, which an import, a paste from a tool that
358
+ // writes the keyword itself, or an author typing it by hand all produce.
359
+ //
360
+ // Only the target's own keyword is dropped. A first line the author wrote —
361
+ // `«service»`, `«entity»`, which Annex C is explicit are not keywords — is not
362
+ // one of ours to take away, so it survives and the new keyword goes above it.
363
+ // A LOOP, not one slice: a document that already stacked the keyword twice —
364
+ // an import, a paste from a tool that writes it itself, a morph run before
365
+ // this line existed — has two to take off, and taking one off left the other
366
+ // in place. The rewrite then came out identical to the text it started from
367
+ // and `umlMorphedName` answered `null`, so the stack was permanent.
368
+ while (toKeyword && body[0]?.trim() === toKeyword)
369
+ body = body.slice(1);
370
+ // The name, if it is still the source kind's own prompt.
371
+ const named = body.join('\n').trim() === SEEDED_NAME[from] ? [SEEDED_NAME[to]] : body;
372
+ const next = (toKeyword ? [toKeyword, ...named] : named).join('\n');
373
+ return next === text ? null : next;
374
+ }
375
+ /**
376
+ * Keep the component's own words saying what the shape now is — the `afterMorph`
377
+ * half of the UML spec.
378
+ *
379
+ * One tier, and it is the one the reader looks at: `uml:name`. The attributes
380
+ * and operations compartments are the author's whole content and say nothing
381
+ * about the metaclass, so nothing here touches them — an enumeration that used
382
+ * to be a class keeps every feature somebody wrote on it, which is exactly what
383
+ * makes the morph worth having.
384
+ *
385
+ * ## …and then re-fit the box, because the keyword is a LINE
386
+ *
387
+ * §9.5.4 writes `«interface»` ABOVE the name, so morphing a class called `Ligne`
388
+ * turns a one-line name compartment into a two-line one. Nothing used to re-lay
389
+ * the component for it — `UmlCompartmentWatcher` fires on an editing selection
390
+ * closing, and a morph opens no editor — so the keyword was written and the name
391
+ * under it was painted straight through the rule into the attributes
392
+ * compartment. That is the PO's "le titre va accumuler «interface» \n Class
393
+ * ligne" of 14/09/2026, measured: two lines needing 38 units in a 22.4-unit
394
+ * compartment, on a node that stayed 120 tall.
395
+ *
396
+ * So the same fit the watcher runs is run here, INSIDE the caller's checkpoint
397
+ * (`capture: false`): `applyMorph` has already opened one, and the re-layout
398
+ * belongs in the same single ctrl+z as the kind that made it necessary — which
399
+ * is exactly what {@link rewriteTier} promises about the words themselves.
400
+ */
401
+ function rewriteName(model, from, to) {
402
+ rewriteTier(model, UML_ROLE.name, text => umlMorphedName(from, to, text));
403
+ const node = umlNodeOfGroup(model);
404
+ if (!node)
405
+ return;
406
+ umlFitComponent(node.surface, node.surface.store, node.id,
407
+ // The morph's own checkpoint, not a second one.
408
+ { capture: false });
409
+ }
410
+ /**
411
+ * UML's morph declaration, handed to the generic `morphToolbarConfig`.
412
+ *
413
+ * `modelType` is the native `GroupElementModel` and not `UmlNodeElementModel`,
414
+ * because the group is what a click on a component selects. Everything that
415
+ * makes that safe is in {@link umlNodeOfGroup}: the toolbar's own homogeneity
416
+ * test only proves that every selected element is A group, and the resolution
417
+ * proves that every one of them is one of OURS.
418
+ *
419
+ * `propsOf` is the shipped creation builder minus `type` / `xywh` / `text`
420
+ * ({@link umlMorphProps}), so the toolbox and the morph cannot disagree about
421
+ * what an interface looks like; `clearOf` is empty on today's table and derived
422
+ * rather than assumed so (`presets.ts`).
423
+ */
424
+ export const UML_MORPH_SPEC = {
425
+ framework: 'uml',
426
+ families: OFFERED_FAMILIES,
427
+ modelType: GroupElementModel,
428
+ resolveTarget: umlNodeOfGroup,
429
+ kindOf: (model) => model instanceof UmlNodeElementModel ? model.kind : undefined,
430
+ // ↓ everything from here down is SHARED with {@link UML_BARE_MORPH_SPEC}.
431
+ // The SHAPE's role, which is the only role a UML artefact has: the group
432
+ // carries none by design, and the tiers carry roles of their own (`uml:name`,
433
+ // `uml:attributes`, `uml:operations`) that say which COMPARTMENT they are and
434
+ // not what kind of box they belong to — so a morph never rewrites them.
435
+ roleOf: kind => UML_ROLE_OF_KIND[kind],
436
+ propsOf: umlMorphProps,
437
+ clearOf: umlMorphClears,
438
+ afterMorph: rewriteName,
439
+ labelOf,
440
+ iconOf,
441
+ label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
442
+ };
443
+ /**
444
+ * The SAME dropdown, on a UML shape that is not inside a group.
445
+ *
446
+ * ## Why a second spec exists at all
447
+ *
448
+ * Because phase 2 made the pack's own rule about composites untrue for a third
449
+ * of its artefacts. Until then every UML artefact was a `group` — a shape and
450
+ * the canvas texts grouped with it — so `modelType: GroupElementModel` was the
451
+ * whole selection filter. The behaviour vocabulary is not: §15.3.4 and §14.2.4
452
+ * name none of their routing marks, so a bullseye, a bar, a diamond and a
453
+ * crossed circle are created as the SHAPE alone (`actions.ts`), and what a
454
+ * click selects for them is a `umlNode`, never a group.
455
+ *
456
+ * `umlNodeOfGroup` refuses a bare shape by design — it is the gate that keeps a
457
+ * plain lasso and another framework's component out of the menu — so the group
458
+ * spec resolves nothing for them and three whole families (`activity-final` /
459
+ * `flow-final`, `shallow-history` / `deep-history`, `entry-point` /
460
+ * `exit-point`) had a dropdown nobody could ever open.
461
+ *
462
+ * ## Why not one spec with a widened `modelType`
463
+ *
464
+ * Because the selection filter is not the only thing that differs: the two
465
+ * specs are registered under two DIFFERENT toolbar flavours. A bare `umlNode`'s
466
+ * contextual row is `affine:surface:umlNode`'s (the widget derives the flavour
467
+ * from `model.type`), and a grouped artefact's is `affine:surface:group`'s.
468
+ * Widening `modelType` to the abstract base would leave the group registration
469
+ * answering for shapes it will never be handed, and would make its homogeneity
470
+ * gate — the thing that refuses a group holding two classifiers — apply to a
471
+ * selection it was not written about. Two registrations, two keys, one table.
472
+ *
473
+ * Everything that decides what the menu SAYS and what a pick WRITES is shared
474
+ * with {@link UML_MORPH_SPEC} by reference, so the two can never offer
475
+ * different families or write different props.
476
+ *
477
+ * `resolveTarget` is absent, which is identity: a bare mark is one element —
478
+ * what the user selects, what carries the kind and what the patch lands on are
479
+ * the same object, exactly as they are for a connector. `afterMorph` is absent
480
+ * too, and that is the notation's doing rather than an omission: `rewriteName`
481
+ * exists to keep a `uml:name` tier saying what the shape now is, and these
482
+ * kinds have no tier at all — there is nothing to rewrite, and reaching for one
483
+ * would be looking for words the specification says are not there.
484
+ */
485
+ export const UML_BARE_MORPH_SPEC = {
486
+ framework: UML_MORPH_SPEC.framework,
487
+ families: UML_MORPH_SPEC.families,
488
+ modelType: UmlNodeElementModel,
489
+ kindOf: UML_MORPH_SPEC.kindOf,
490
+ roleOf: UML_MORPH_SPEC.roleOf,
491
+ propsOf: UML_MORPH_SPEC.propsOf,
492
+ clearOf: UML_MORPH_SPEC.clearOf,
493
+ labelOf: UML_MORPH_SPEC.labelOf,
494
+ iconOf: UML_MORPH_SPEC.iconOf,
495
+ label: UML_MORPH_SPEC.label,
496
+ };
@@ -0,0 +1,30 @@
1
+ import type { UmlNodeKind } from '@formicoidea/labre-core/model';
2
+ import type { GlyphBox } from './paint.js';
3
+ /**
4
+ * The ACTIVITY glyphs (§15.3.4 control nodes, §16.3.4 signals, §16.10.4 time
5
+ * events) — the marks an activity diagram is routed by.
6
+ *
7
+ * Split out of `node-renderer.ts` because the behaviour families roughly
8
+ * doubled the pack's glyph count, and a 900-line renderer is a file nobody
9
+ * reads the middle of. The renderer still owns the branch — which kind gets
10
+ * which picture, and the `never` that proves every kind gets one — and this
11
+ * file owns the drawing.
12
+ *
13
+ * Three kinds of the activity family are NOT here, and that is the same
14
+ * distinction `presets.ts` draws: an `action` is the native rounded rect of
15
+ * §15.3.4, a `decision` the native diamond, an `object-node` the plain native
16
+ * rect. The shape layer draws all three, and a glyph painted over them would be
17
+ * inventing a notation.
18
+ *
19
+ * Every proportion below is a fraction of the glyph's own box, so a mark dragged
20
+ * to any size stays the mark it is — the rule the whole pack follows.
21
+ */
22
+ /** The kinds this module draws. */
23
+ export type UmlActGlyphKind = 'initial' | 'activity-final' | 'flow-final' | 'fork' | 'send-signal' | 'accept-event' | 'time-event';
24
+ /**
25
+ * Paint one activity mark, in the element-local frame, with `fillStyle` /
26
+ * `strokeStyle` / `lineWidth` already set from the model.
27
+ */
28
+ export declare function paintActGlyph(kind: UmlActGlyphKind, ctx: CanvasRenderingContext2D, box: GlyphBox): void;
29
+ /** Whether the node renderer should hand this kind to {@link paintActGlyph}. */
30
+ export declare function isActGlyphKind(kind: UmlNodeKind): kind is UmlActGlyphKind;
@@ -0,0 +1,142 @@
1
+ import { UML_SIGNAL_POINT } from '../component.js';
2
+ import { cross, hollowDisc, inkDisc, solidPolygon, withInkFill, } from './paint.js';
3
+ /**
4
+ * The bull inside an activity final's ring (§15.3.4), as a fraction of the outer
5
+ * radius.
6
+ *
7
+ * A touch over half: §15.3.4's figure draws a solid disc with a clear moat round
8
+ * it, and a bull much larger than this closes the moat up at the sizes a control
9
+ * node is actually drawn at (28 units).
10
+ */
11
+ const BULLSEYE_BULL = 0.52;
12
+ /**
13
+ * The reach of a flow final's cross (§15.3.4), as a fraction of the rim radius.
14
+ *
15
+ * Short of the rim rather than touching it: the X is drawn INSIDE the circle,
16
+ * and arms that met the rim would read as a circle quartered rather than as a
17
+ * circle with a cross in it.
18
+ */
19
+ const FLOW_FINAL_CROSS = 0.62;
20
+ /**
21
+ * Paint one activity mark, in the element-local frame, with `fillStyle` /
22
+ * `strokeStyle` / `lineWidth` already set from the model.
23
+ */
24
+ export function paintActGlyph(kind, ctx, box) {
25
+ const { x0, y0, x1, y1, bw, bh, w, h } = box;
26
+ const cx = w / 2;
27
+ const cy = h / 2;
28
+ // One radius from whichever dimension is the tighter, so every round mark
29
+ // stays ROUND at any aspect ratio the element is dragged to — the rule the
30
+ // actor's head and the interface ball already follow.
31
+ const radius = Math.min(bw, bh) / 2;
32
+ switch (kind) {
33
+ // ── The initial node: a filled disc (§15.3.4) ──────────────────────────
34
+ // Shared with the state machine family, which draws the identical disc and
35
+ // means the identical thing by it (§14.2.4) — one kind, one role, one
36
+ // picture, and the role def records the sharing.
37
+ case 'initial':
38
+ inkDisc(ctx, cx, cy, radius);
39
+ return;
40
+ // ── The activity final: a bullseye (§15.3.4) ──────────────────────────
41
+ // A ring with a solid bull in it. Drawn rim first so the bull lands ON the
42
+ // paper the rim encloses rather than under it.
43
+ case 'activity-final':
44
+ hollowDisc(ctx, cx, cy, radius);
45
+ inkDisc(ctx, cx, cy, radius * BULLSEYE_BULL);
46
+ return;
47
+ // ── The flow final: a circle with an X in it (§15.3.4) ────────────────
48
+ // The one final that stops a single flow rather than the whole activity,
49
+ // and the cross is the entire difference on the page.
50
+ case 'flow-final':
51
+ hollowDisc(ctx, cx, cy, radius);
52
+ cross(ctx, cx, cy, radius * FLOW_FINAL_CROSS);
53
+ return;
54
+ // ── The fork / join: a filled bar (§15.3.4) ───────────────────────────
55
+ // The WHOLE inset box, because the element IS the bar: its default
56
+ // footprint is 120 × 10 and dragging it longer is how an author fits more
57
+ // outgoing flows onto it. Ink-filled, since §15.3.4 draws a solid segment
58
+ // and a hollow one would read as a very flat box.
59
+ case 'fork':
60
+ if (bw > 0 && bh > 0) {
61
+ withInkFill(ctx, () => {
62
+ ctx.beginPath();
63
+ ctx.moveTo(x0, y0);
64
+ ctx.lineTo(x1, y0);
65
+ ctx.lineTo(x1, y1);
66
+ ctx.lineTo(x0, y1);
67
+ ctx.closePath();
68
+ ctx.fill();
69
+ ctx.stroke();
70
+ });
71
+ }
72
+ return;
73
+ // ── The send signal: a convex pentagon (§16.3.4) ──────────────────────
74
+ // A rectangle with its right edge pulled out to a POINT — an arrow head
75
+ // saying the signal leaves here. The point's depth is `UML_SIGNAL_POINT`,
76
+ // the same number `component.ts` pulls the label box back by, so the name
77
+ // can never run out through the tip.
78
+ case 'send-signal': {
79
+ const point = w * UML_SIGNAL_POINT;
80
+ solidPolygon(ctx, [
81
+ [x0, y0],
82
+ [Math.max(x0, x1 - point), y0],
83
+ [x1, cy],
84
+ [Math.max(x0, x1 - point), y1],
85
+ [x0, y1],
86
+ ]);
87
+ return;
88
+ }
89
+ // ── The accept event: a concave pentagon (§16.3.4) ────────────────────
90
+ // The same rectangle with its left edge pushed IN to a notch — a socket
91
+ // saying the event arrives here. The mirror of the shape above, which is
92
+ // exactly how a reader tells a send from a receive at a glance.
93
+ case 'accept-event': {
94
+ const point = w * UML_SIGNAL_POINT;
95
+ solidPolygon(ctx, [
96
+ [x0, y0],
97
+ [x1, y0],
98
+ [x1, y1],
99
+ [x0, y1],
100
+ [Math.min(x1, x0 + point), cy],
101
+ ]);
102
+ return;
103
+ }
104
+ // ── The time event: an hourglass (§16.10.4) ───────────────────────────
105
+ // Two triangles meeting at a point in the middle of the box. Drawn as two
106
+ // closed paths rather than one crossed one, because a single bow-tie path
107
+ // fills by the even-odd rule on some canvases and by the non-zero rule on
108
+ // others — and an hourglass that is sometimes hollow is not a notation.
109
+ case 'time-event':
110
+ if (bw > 0 && bh > 0) {
111
+ solidPolygon(ctx, [
112
+ [x0, y0],
113
+ [x1, y0],
114
+ [cx, cy],
115
+ ]);
116
+ solidPolygon(ctx, [
117
+ [cx, cy],
118
+ [x1, y1],
119
+ [x0, y1],
120
+ ]);
121
+ }
122
+ return;
123
+ }
124
+ /**
125
+ * Every kind this module claims is drawn above. Narrowed to `never` only if
126
+ * the switch is exhaustive over {@link UmlActGlyphKind}, so a kind added to
127
+ * the union without a picture of its own stops the build rather than silently
128
+ * painting nothing.
129
+ */
130
+ const unhandled = kind;
131
+ void unhandled;
132
+ }
133
+ /** Whether the node renderer should hand this kind to {@link paintActGlyph}. */
134
+ export function isActGlyphKind(kind) {
135
+ return (kind === 'initial' ||
136
+ kind === 'activity-final' ||
137
+ kind === 'flow-final' ||
138
+ kind === 'fork' ||
139
+ kind === 'send-signal' ||
140
+ kind === 'accept-event' ||
141
+ kind === 'time-event');
142
+ }