@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,107 @@
1
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
2
+ /**
3
+ * Keeps a classifier's BOX big enough for the words in it.
4
+ *
5
+ * ## The overflow, and why a pure layout cannot fix it
6
+ *
7
+ * `umlCompartmentBoxes` lays a divided rectangle out from the rectangle alone:
8
+ * a margin, the name, a gap, THE LINE, three lines of attributes, THE SECOND
9
+ * LINE, and the operations take the rest (§11.4.4). Three attribute lines is
10
+ * what a fresh 200 × 120 class is sized for, and it is right until somebody
11
+ * types a fourth — at which point the fourth line is drawn over the separator
12
+ * under it, the fifth over the operations, and the box says nothing about it.
13
+ * Nothing in a rectangle can say otherwise: how many lines a tier holds is a
14
+ * fact about the TEXT.
15
+ *
16
+ * So the text is measured once, when an edit commits, and the two facts that
17
+ * follow from it are written: the node grows to the height the stack now needs
18
+ * (`umlStackHeight`), and the tiers move to the boxes that height yields. The
19
+ * separators follow for free — the renderer reads them off the tiers
20
+ * (`node-renderer.ts`), so the rule is drawn between the compartments as they
21
+ * ARE rather than where a default-sized stack would have put them. The measuring
22
+ * and the writing are {@link umlFitComponent}'s, shared with the morph.
23
+ *
24
+ * ## Why the seam is the SELECTION and not `elementUpdated`
25
+ *
26
+ * The same reason `C4TypeLineWatcher` gives, and it bites harder here. A canvas
27
+ * text editor binds its inline editor straight onto the element's `Y.Text`, so
28
+ * `surface.elementUpdated` fires once per KEYSTROKE: a re-layout on it would
29
+ * move the box under the caret on the way to the fourth line and again on the
30
+ * way back from it. The text editor holds an EDITING selection for as long as
31
+ * it is mounted and drops it when it closes, so an id LEAVING that set is a
32
+ * commit and nothing else is. One re-layout per edit, after the editor is gone,
33
+ * and a single undo entry for it.
34
+ *
35
+ * It is also the seam that puts the tier BACK. The canvas text editor
36
+ * shrink-wraps the element it is mounted on while the author types — it measures
37
+ * the rich text on every render and writes the element's `xywh` from it — so a
38
+ * tier left to itself ends an edit as a box the size of its words rather than as
39
+ * a compartment. Writing the layout after the editor has closed is what makes
40
+ * the compartment the last word.
41
+ *
42
+ * ## It grows, and never shrinks
43
+ *
44
+ * A box an author has dragged taller has dragged it taller FOR the operations
45
+ * (`component.ts`), and §11.4.4 hands the bottom compartment whatever is left on
46
+ * purpose — an author leaves themselves room to type. A watcher that reclaimed
47
+ * it the moment a line was deleted would be undoing a gesture nobody asked it to
48
+ * judge, and it would do so while the author was still working. So the height
49
+ * only ever goes UP; deleting lines moves the separators up (the tiers are
50
+ * re-laid either way) and leaves the box where it was put.
51
+ *
52
+ * ## Always-on, and local-only
53
+ *
54
+ * Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): it
55
+ * authors nothing — no element created, no field added — it keeps words that are
56
+ * ALREADY in the document readable, and a diagram drawn while the UML button was
57
+ * on must stay editable when it goes off.
58
+ *
59
+ * It is also inherently local, which is what stands in for the `local` guard
60
+ * every `elementUpdated` cascade needs (`docs/contribute/07-dev-practices.md`):
61
+ * the only input is THIS peer's selection. A remote peer typing five lines into
62
+ * a tier moves no editing selection here, so no re-layout is attempted on their
63
+ * behalf and the fleet cannot re-apply one write each. An undo is the same story
64
+ * from the other side: it mounts no editor and closes none, so nothing leaves
65
+ * the editing set and the restored box stays restored.
66
+ */
67
+ export declare class UmlCompartmentWatcher extends InteractivityExtension {
68
+ static key: string;
69
+ private _subscription;
70
+ private _editing;
71
+ mounted(): void;
72
+ unmounted(): void;
73
+ /**
74
+ * Re-fit the classifier the committed tier belongs to.
75
+ *
76
+ * This is the TRIGGER and nothing else: what was committed has to be one of
77
+ * the tiers a classifier's stack is made of, or there is nothing about the
78
+ * layout for an edit to have changed. Everything past that — resolving the
79
+ * component, measuring the words, growing the box — is {@link umlFitComponent},
80
+ * which the morph reaches for too (`morph.ts`) so a keyword line written by a
81
+ * "Change type" lands in a compartment as surely as a typed one does.
82
+ */
83
+ private _fit;
84
+ }
85
+ /**
86
+ * Which ids just stopped being edited, and what is being edited now.
87
+ *
88
+ * Deliberately a copy of `c4EditingTransition` rather than a shared helper: the
89
+ * framework packs are independent by design (`docs/adr/0015`), this is nine
90
+ * lines of set arithmetic, and a framework module importing another framework
91
+ * module for it would be the first such edge in the library. If a third pack
92
+ * needs it, `std/gfx` is where it moves — not into whichever pack wrote it
93
+ * first.
94
+ *
95
+ * Pure, and exported, because it is the whole of the seam's logic and the only
96
+ * part of it worth being wrong about: an id that appears in the new editing set
97
+ * has merely STARTED, an id in both is still going, and only one that has LEFT
98
+ * is a commit. Total over an empty selection, which is what arrives when the
99
+ * editor closes onto bare canvas.
100
+ */
101
+ export declare function umlEditingTransition(previous: ReadonlySet<string>, selections: readonly {
102
+ elements: readonly string[];
103
+ editing?: boolean;
104
+ }[]): {
105
+ editing: Set<string>;
106
+ left: string[];
107
+ };
@@ -0,0 +1,163 @@
1
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
2
+ import { UML_ROLE } from '../roles.js';
3
+ import { umlFitComponent } from './fit.js';
4
+ /**
5
+ * The four tier roles an edit can commit into.
6
+ *
7
+ * `uml:lifeline-ident` is deliberately NOT among them, and it is the one tier
8
+ * this set leaves out: §17.3.4's head is painted at a fixed size across the top
9
+ * of a 16-wide column (`consts.ts`, `UML_LIFELINE_HEAD`), so no amount of typing
10
+ * in it can change the element's box — a lifeline resizes in HEIGHT and that is
11
+ * the spine's length, not the words'. Accepting it as a trigger would mean
12
+ * asking `umlStackHeight` a question it answers `null` to on every keystroke,
13
+ * which is the work this set exists to avoid.
14
+ */
15
+ const TIER_ROLES = new Set([
16
+ UML_ROLE.name,
17
+ UML_ROLE.attributes,
18
+ UML_ROLE.operations,
19
+ // A glyph's single word. Accepted as a TRIGGER and almost always rejected a
20
+ // step later: a label belongs to a picture — an actor, a use case, a port —
21
+ // and `umlStackHeight` answers `null` for every one of them. It is in the set
22
+ // so a hand-built group (a label regrouped with a class box) is asked the
23
+ // question rather than silently skipped.
24
+ UML_ROLE.label,
25
+ ]);
26
+ /**
27
+ * Keeps a classifier's BOX big enough for the words in it.
28
+ *
29
+ * ## The overflow, and why a pure layout cannot fix it
30
+ *
31
+ * `umlCompartmentBoxes` lays a divided rectangle out from the rectangle alone:
32
+ * a margin, the name, a gap, THE LINE, three lines of attributes, THE SECOND
33
+ * LINE, and the operations take the rest (§11.4.4). Three attribute lines is
34
+ * what a fresh 200 × 120 class is sized for, and it is right until somebody
35
+ * types a fourth — at which point the fourth line is drawn over the separator
36
+ * under it, the fifth over the operations, and the box says nothing about it.
37
+ * Nothing in a rectangle can say otherwise: how many lines a tier holds is a
38
+ * fact about the TEXT.
39
+ *
40
+ * So the text is measured once, when an edit commits, and the two facts that
41
+ * follow from it are written: the node grows to the height the stack now needs
42
+ * (`umlStackHeight`), and the tiers move to the boxes that height yields. The
43
+ * separators follow for free — the renderer reads them off the tiers
44
+ * (`node-renderer.ts`), so the rule is drawn between the compartments as they
45
+ * ARE rather than where a default-sized stack would have put them. The measuring
46
+ * and the writing are {@link umlFitComponent}'s, shared with the morph.
47
+ *
48
+ * ## Why the seam is the SELECTION and not `elementUpdated`
49
+ *
50
+ * The same reason `C4TypeLineWatcher` gives, and it bites harder here. A canvas
51
+ * text editor binds its inline editor straight onto the element's `Y.Text`, so
52
+ * `surface.elementUpdated` fires once per KEYSTROKE: a re-layout on it would
53
+ * move the box under the caret on the way to the fourth line and again on the
54
+ * way back from it. The text editor holds an EDITING selection for as long as
55
+ * it is mounted and drops it when it closes, so an id LEAVING that set is a
56
+ * commit and nothing else is. One re-layout per edit, after the editor is gone,
57
+ * and a single undo entry for it.
58
+ *
59
+ * It is also the seam that puts the tier BACK. The canvas text editor
60
+ * shrink-wraps the element it is mounted on while the author types — it measures
61
+ * the rich text on every render and writes the element's `xywh` from it — so a
62
+ * tier left to itself ends an edit as a box the size of its words rather than as
63
+ * a compartment. Writing the layout after the editor has closed is what makes
64
+ * the compartment the last word.
65
+ *
66
+ * ## It grows, and never shrinks
67
+ *
68
+ * A box an author has dragged taller has dragged it taller FOR the operations
69
+ * (`component.ts`), and §11.4.4 hands the bottom compartment whatever is left on
70
+ * purpose — an author leaves themselves room to type. A watcher that reclaimed
71
+ * it the moment a line was deleted would be undoing a gesture nobody asked it to
72
+ * judge, and it would do so while the author was still working. So the height
73
+ * only ever goes UP; deleting lines moves the separators up (the tiers are
74
+ * re-laid either way) and leaves the box where it was put.
75
+ *
76
+ * ## Always-on, and local-only
77
+ *
78
+ * Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): it
79
+ * authors nothing — no element created, no field added — it keeps words that are
80
+ * ALREADY in the document readable, and a diagram drawn while the UML button was
81
+ * on must stay editable when it goes off.
82
+ *
83
+ * It is also inherently local, which is what stands in for the `local` guard
84
+ * every `elementUpdated` cascade needs (`docs/contribute/07-dev-practices.md`):
85
+ * the only input is THIS peer's selection. A remote peer typing five lines into
86
+ * a tier moves no editing selection here, so no re-layout is attempted on their
87
+ * behalf and the fleet cannot re-apply one write each. An undo is the same story
88
+ * from the other side: it mounts no editor and closes none, so nothing leaves
89
+ * the editing set and the restored box stays restored.
90
+ */
91
+ export class UmlCompartmentWatcher extends InteractivityExtension {
92
+ constructor() {
93
+ super(...arguments);
94
+ this._subscription = null;
95
+ this._editing = new Set();
96
+ }
97
+ static { this.key = 'uml-compartment-watcher'; }
98
+ mounted() {
99
+ this._subscription = this.gfx.selection.slots.updated.subscribe(selections => {
100
+ const { editing, left } = umlEditingTransition(this._editing, selections);
101
+ this._editing = editing;
102
+ for (const id of left)
103
+ this._fit(id);
104
+ });
105
+ }
106
+ unmounted() {
107
+ this._subscription?.unsubscribe();
108
+ this._subscription = null;
109
+ this._editing = new Set();
110
+ super.unmounted();
111
+ }
112
+ /**
113
+ * Re-fit the classifier the committed tier belongs to.
114
+ *
115
+ * This is the TRIGGER and nothing else: what was committed has to be one of
116
+ * the tiers a classifier's stack is made of, or there is nothing about the
117
+ * layout for an edit to have changed. Everything past that — resolving the
118
+ * component, measuring the words, growing the box — is {@link umlFitComponent},
119
+ * which the morph reaches for too (`morph.ts`) so a keyword line written by a
120
+ * "Change type" lands in a compartment as surely as a typed one does.
121
+ */
122
+ _fit(elementId) {
123
+ const surface = this.gfx.surface;
124
+ if (!surface)
125
+ return;
126
+ const tier = surface.getElementById(elementId);
127
+ if (!tier || tier.type !== 'text')
128
+ return;
129
+ if (tier.role === undefined || !TIER_ROLES.has(tier.role))
130
+ return;
131
+ umlFitComponent(surface, this.std.store, elementId);
132
+ }
133
+ }
134
+ /**
135
+ * Which ids just stopped being edited, and what is being edited now.
136
+ *
137
+ * Deliberately a copy of `c4EditingTransition` rather than a shared helper: the
138
+ * framework packs are independent by design (`docs/adr/0015`), this is nine
139
+ * lines of set arithmetic, and a framework module importing another framework
140
+ * module for it would be the first such edge in the library. If a third pack
141
+ * needs it, `std/gfx` is where it moves — not into whichever pack wrote it
142
+ * first.
143
+ *
144
+ * Pure, and exported, because it is the whole of the seam's logic and the only
145
+ * part of it worth being wrong about: an id that appears in the new editing set
146
+ * has merely STARTED, an id in both is still going, and only one that has LEFT
147
+ * is a commit. Total over an empty selection, which is what arrives when the
148
+ * editor closes onto bare canvas.
149
+ */
150
+ export function umlEditingTransition(previous, selections) {
151
+ const editing = new Set();
152
+ for (const selection of selections) {
153
+ if (!selection.editing)
154
+ continue;
155
+ for (const id of selection.elements)
156
+ editing.add(id);
157
+ }
158
+ const left = [];
159
+ for (const id of previous)
160
+ if (!editing.has(id))
161
+ left.push(id);
162
+ return { editing, left };
163
+ }
@@ -0,0 +1,85 @@
1
+ import type { UmlNodeKind } from '@formicoidea/labre-core/model';
2
+ import { type SerializedXYWH, type XYWH } from '@formicoidea/labre-core/global/gfx';
3
+ import { type UmlTierFace } from './tier-metrics.js';
4
+ /**
5
+ * The little a fit needs of a canvas TEXT element.
6
+ *
7
+ * Structural, and not `TextElementModel`, for the reason `component.ts` states
8
+ * about `UmlComponentElement`: what is read here is an id, a role, a face,
9
+ * whatever the tier says and where it is — facts a fixture can state as plainly
10
+ * as a model can, which is what lets the decision below be tested without a Yjs
11
+ * document standing behind every box.
12
+ */
13
+ export interface UmlTierElement extends UmlTierFace {
14
+ id: string;
15
+ type: string;
16
+ role?: string;
17
+ text?: unknown;
18
+ xywh: SerializedXYWH;
19
+ isLocked(): boolean;
20
+ }
21
+ /** …and of the SHAPE those words are about. */
22
+ export interface UmlNodeShape {
23
+ id: string;
24
+ type: string;
25
+ kind: UmlNodeKind;
26
+ xywh: SerializedXYWH;
27
+ deserializedXYWH: XYWH;
28
+ isLocked(): boolean;
29
+ }
30
+ /** The surface a fit reads its elements off and writes its boxes back to. */
31
+ export interface UmlFitSurface {
32
+ elementModels: readonly unknown[];
33
+ getElementById(id: string): unknown;
34
+ }
35
+ /**
36
+ * …and the store it writes THROUGH.
37
+ *
38
+ * Passed beside the surface rather than read off it because the two callers hold
39
+ * it differently — the watcher has `std.store`, the morph has the element's own
40
+ * `surface.store` — and because it is the whole of what a fit does to a
41
+ * document: one checkpoint, one transaction.
42
+ */
43
+ export interface UmlFitStore {
44
+ readonly: boolean;
45
+ captureSync(): void;
46
+ transact(fn: () => void): void;
47
+ }
48
+ /**
49
+ * Re-fit the classifier a member belongs to: grow the node to the stack its
50
+ * tiers now need, and put the tiers into the compartments that height yields.
51
+ *
52
+ * ## One function, two callers, and why they are the same act
53
+ *
54
+ * `UmlCompartmentWatcher` calls it when an edit commits — the author typed a
55
+ * fourth attribute line and the box has to grow. `rewriteName` (`morph.ts`)
56
+ * calls it when a morph writes a KEYWORD LINE into the name compartment, which
57
+ * is the same overflow arriving by a different hand: morphing a class called
58
+ * `Ligne` to an interface turns one name line into two (`«interface»` over the
59
+ * name, §9.5.4) and, until the PO's recette of 14/09/2026, nothing re-laid the
60
+ * component — so the second line was painted straight through the rule under it.
61
+ *
62
+ * ## It grows, and never shrinks
63
+ *
64
+ * A box an author has dragged taller has dragged it taller FOR the operations
65
+ * (`component.ts`), and §11.4.4 hands the bottom compartment whatever is left on
66
+ * purpose — an author leaves themselves room to type. A fit that reclaimed it
67
+ * the moment a line was deleted would be undoing a gesture nobody asked it to
68
+ * judge. So the height only ever goes UP; deleting lines moves the separators up
69
+ * (the tiers are re-laid either way) and leaves the box where it was put.
70
+ *
71
+ * Every early return is a case where there is nothing honest to do, and writing
72
+ * nothing is the answer to all of them: a read-only document, a tier whose group
73
+ * was released, a group with no shape in it, a shape that is a picture rather
74
+ * than a divided box, a locked component, and — the common one — a stack that
75
+ * already fits, which is what every edit that did not change the painted line
76
+ * count produces.
77
+ *
78
+ * @param capture whether to open the fit's OWN undo entry. The watcher does
79
+ * (the layout is a consequence of an edit that has already been captured); the
80
+ * morph does not, because its re-layout belongs in the same single ctrl+z as the
81
+ * kind that made it necessary.
82
+ */
83
+ export declare function umlFitComponent(surface: UmlFitSurface, store: UmlFitStore, memberId: string, { capture }?: {
84
+ capture?: boolean;
85
+ }): void;
@@ -0,0 +1,129 @@
1
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
2
+ import { umlCompartmentBoxes, umlComponentSiblings, umlGroupOf, umlStackHeight, UML_TIER_SIDE_INSET, umlTierLineCount, } from '../component.js';
3
+ import { umlTierWrapper } from './tier-metrics.js';
4
+ /**
5
+ * Re-fit the classifier a member belongs to: grow the node to the stack its
6
+ * tiers now need, and put the tiers into the compartments that height yields.
7
+ *
8
+ * ## One function, two callers, and why they are the same act
9
+ *
10
+ * `UmlCompartmentWatcher` calls it when an edit commits — the author typed a
11
+ * fourth attribute line and the box has to grow. `rewriteName` (`morph.ts`)
12
+ * calls it when a morph writes a KEYWORD LINE into the name compartment, which
13
+ * is the same overflow arriving by a different hand: morphing a class called
14
+ * `Ligne` to an interface turns one name line into two (`«interface»` over the
15
+ * name, §9.5.4) and, until the PO's recette of 14/09/2026, nothing re-laid the
16
+ * component — so the second line was painted straight through the rule under it.
17
+ *
18
+ * ## It grows, and never shrinks
19
+ *
20
+ * A box an author has dragged taller has dragged it taller FOR the operations
21
+ * (`component.ts`), and §11.4.4 hands the bottom compartment whatever is left on
22
+ * purpose — an author leaves themselves room to type. A fit that reclaimed it
23
+ * the moment a line was deleted would be undoing a gesture nobody asked it to
24
+ * judge. So the height only ever goes UP; deleting lines moves the separators up
25
+ * (the tiers are re-laid either way) and leaves the box where it was put.
26
+ *
27
+ * Every early return is a case where there is nothing honest to do, and writing
28
+ * nothing is the answer to all of them: a read-only document, a tier whose group
29
+ * was released, a group with no shape in it, a shape that is a picture rather
30
+ * than a divided box, a locked component, and — the common one — a stack that
31
+ * already fits, which is what every edit that did not change the painted line
32
+ * count produces.
33
+ *
34
+ * @param capture whether to open the fit's OWN undo entry. The watcher does
35
+ * (the layout is a consequence of an edit that has already been captured); the
36
+ * morph does not, because its re-layout belongs in the same single ctrl+z as the
37
+ * kind that made it necessary.
38
+ */
39
+ export function umlFitComponent(surface, store, memberId, { capture = true } = {}) {
40
+ // A read-only document is READ: every write below would be refused, and
41
+ // nobody can have typed the line that would need one.
42
+ if (store.readonly)
43
+ return;
44
+ const element = (id) => (surface.getElementById(id) ?? null);
45
+ const groups = surface.elementModels.filter(model => model.type === 'group');
46
+ const group = umlGroupOf(memberId, groups);
47
+ if (!group)
48
+ return;
49
+ // The same pure resolution the exporter and the node view use — group
50
+ // membership, then roles — so the tiers moved here are the tiers the file
51
+ // comes out with.
52
+ const component = umlComponentSiblings(group, surface.elementModels);
53
+ const node = component.node && element(component.node.id);
54
+ if (!node || node.type !== 'umlNode')
55
+ return;
56
+ // `isLocked` walks the ancestors too, so this covers a locked GROUP as well
57
+ // as a locked shape — the two ways a reviewer freezes a component.
58
+ if (node.isLocked())
59
+ return;
60
+ const { kind } = node;
61
+ const [x, y, w, h] = node.deserializedXYWH;
62
+ // The width the tiers will be laid out at, which is the width their words are
63
+ // WRAPPED at — never the box a tier happens to wear right now, because the
64
+ // text editor shrink-wraps the element it is mounted on while the author
65
+ // types.
66
+ const tierWidth = w - w * UML_TIER_SIDE_INSET * 2;
67
+ const lines = (member) => {
68
+ if (!member)
69
+ return undefined;
70
+ const tier = element(member.id);
71
+ if (!tier || tier.type !== 'text')
72
+ return undefined;
73
+ return umlTierLineCount(tier.text, umlTierWrapper(tier, tierWidth));
74
+ };
75
+ const tiers = {
76
+ name: lines(component.name),
77
+ attributes: lines(component.attributes),
78
+ operations: lines(component.operations),
79
+ };
80
+ const required = umlStackHeight(kind, tiers);
81
+ // A picture, not a divided box: an actor's label has no stack to overflow,
82
+ // and there is no height at which a stick figure "fits" its name.
83
+ if (required === null)
84
+ return;
85
+ const height = Math.max(h, required);
86
+ const boxes = umlCompartmentBoxes(kind, x, y, w, height, tiers);
87
+ // Which tier goes in which box. `undefined` on either side is a component
88
+ // somebody took apart by hand, and it is simply skipped.
89
+ const moves = [
90
+ [component.name?.id, boxes.name],
91
+ [component.attributes?.id, boxes.attributes],
92
+ [component.operations?.id, boxes.operations],
93
+ ];
94
+ const writes = [];
95
+ if (height !== h) {
96
+ const xywh = new Bound(x, y, w, height).serialize();
97
+ writes.push(() => (node.xywh = xywh));
98
+ }
99
+ for (const [id, box] of moves) {
100
+ if (!id || !box)
101
+ continue;
102
+ const tier = element(id);
103
+ if (!tier || tier.type !== 'text' || tier.isLocked())
104
+ continue;
105
+ const xywh = new Bound(box.x, box.y, box.w, box.h).serialize();
106
+ if (tier.xywh === xywh)
107
+ continue;
108
+ writes.push(() => (tier.xywh = xywh));
109
+ }
110
+ // Nothing grew and nothing moved: the stack already fits, which is what most
111
+ // edits produce. An unchanged write would push an empty undo entry and cost
112
+ // the author a ctrl-Z for nothing.
113
+ if (writes.length === 0)
114
+ return;
115
+ // Its OWN undo entry when the caller asks for one, deliberately. The
116
+ // alternative — letting the writes merge into whatever the author last typed
117
+ // — makes the entry's contents depend on how long they paused before clicking
118
+ // away, and a layout that sometimes comes back with the words and sometimes
119
+ // does not is worse than one that always behaves the same.
120
+ if (capture)
121
+ store.captureSync();
122
+ // ONE transaction, so the growth and every move are ONE undo entry: an author
123
+ // who undoes it gets the whole layout back, rather than walking it back a tier
124
+ // at a time past a broken picture at every stop.
125
+ store.transact(() => {
126
+ for (const write of writes)
127
+ write();
128
+ });
129
+ }
@@ -0,0 +1,49 @@
1
+ import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
2
+ import { type UmlNodeElementModel } from '@formicoidea/labre-core/model';
3
+ import { type UmlCompartmentBoxes } from '../component.js';
4
+ /**
5
+ * The half of {@link UmlCompartmentBoxes} the glyph layer draws from: where the
6
+ * separators go, and where the name is.
7
+ *
8
+ * The other two boxes are the TEXT ELEMENTS' business — they are the tiers, and
9
+ * they are already on the canvas — so a renderer holding them would be holding a
10
+ * second opinion about where words it does not draw ought to be.
11
+ */
12
+ type UmlSeparators = Pick<UmlCompartmentBoxes, 'name' | 'splits'>;
13
+ /**
14
+ * The compartment layout to stroke this node's separators from: the one its
15
+ * TIERS describe if they can be reached, and the default stack otherwise.
16
+ *
17
+ * ## Why the tiers and not the line counts
18
+ *
19
+ * `UmlCompartmentWatcher` grows a classifier whose attributes tier has
20
+ * overflowed and moves the three texts to the boxes the taller stack yields —
21
+ * so after an edit the compartments are no longer the default three-line stack a
22
+ * rectangle implies, and a renderer still drawing that stack would rule a line
23
+ * straight through the fourth attribute.
24
+ *
25
+ * It could be told the LINE COUNTS and recompute the same walk. It reads the
26
+ * tiers' own boxes instead, for two reasons. It is cheaper — `deserializedXYWH`
27
+ * is cached on the element, while a line count means materializing every tier's
28
+ * `Y.Text` into a string on every frame of every pan — and it cannot disagree
29
+ * with what the reader sees: a separator IS the boundary between two
30
+ * compartments, and reading the boundary off the compartments is the shortest
31
+ * possible route to that. An author who nudges a tier by hand takes the rule
32
+ * with them, which is the same answer.
33
+ *
34
+ * ## The ceiling
35
+ *
36
+ * ponytail: the tiers are read in the node's own unrotated frame, so a ROTATED
37
+ * `umlNode` falls back to the default stack. A UML group cannot be rotated
38
+ * (`GroupElementModel.rotate` is a constant 0), so this only reaches a shape
39
+ * somebody rotated out of its own group — at which point the words are not
40
+ * turning with it either and the picture is already the author's problem.
41
+ * Raising it means projecting each tier's box through `-rotate` about the
42
+ * node's centre, which is four lines nobody has asked for yet.
43
+ */
44
+ export declare function umlNodeCompartments(model: UmlNodeElementModel, w: number, h: number): UmlSeparators;
45
+ export declare const umlNode: ElementRenderer<UmlNodeElementModel>;
46
+ export declare const UmlNodeRendererExtension: import("@formicoidea/labre-core/store/index.js").ExtensionType & {
47
+ identifier: import("@formicoidea/labre-core/_pkgs/global/di/types.js").ServiceIdentifier<ElementRenderer<UmlNodeElementModel>>;
48
+ };
49
+ export {};