@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.
- package/LICENSE +384 -0
- package/dist/actions.d.ts +430 -0
- package/dist/actions.js +724 -0
- package/dist/background.d.ts +153 -0
- package/dist/background.js +557 -0
- package/dist/board-hit.d.ts +137 -0
- package/dist/board-hit.js +191 -0
- package/dist/commands-manifest.d.ts +19 -0
- package/dist/commands-manifest.js +571 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +983 -0
- package/dist/component.d.ts +521 -0
- package/dist/component.js +708 -0
- package/dist/consts.d.ts +365 -0
- package/dist/consts.js +463 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/drawio-decode.d.ts +32 -0
- package/dist/drawio-decode.js +96 -0
- package/dist/drawio-import.d.ts +133 -0
- package/dist/drawio-import.js +885 -0
- package/dist/edge-morph.d.ts +154 -0
- package/dist/edge-morph.js +153 -0
- package/dist/edge-styles.d.ts +78 -0
- package/dist/edge-styles.js +210 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +64 -0
- package/dist/element-renderer.js +114 -0
- package/dist/element-view.d.ts +234 -0
- package/dist/element-view.js +430 -0
- package/dist/export.d.ts +61 -0
- package/dist/export.js +41 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +26 -0
- package/dist/frame-tag.d.ts +70 -0
- package/dist/frame-tag.js +78 -0
- package/dist/grammar.d.ts +695 -0
- package/dist/grammar.js +1436 -0
- package/dist/import.d.ts +306 -0
- package/dist/import.js +1270 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +67 -0
- package/dist/interchange.d.ts +125 -0
- package/dist/interchange.js +372 -0
- package/dist/keywords.d.ts +204 -0
- package/dist/keywords.js +361 -0
- package/dist/kinds.d.ts +73 -0
- package/dist/kinds.js +91 -0
- package/dist/legend.d.ts +30 -0
- package/dist/legend.js +255 -0
- package/dist/model.d.ts +862 -0
- package/dist/model.js +1316 -0
- package/dist/morph.d.ts +178 -0
- package/dist/morph.js +496 -0
- package/dist/node/act-glyphs.d.ts +30 -0
- package/dist/node/act-glyphs.js +142 -0
- package/dist/node/compartment-watcher.d.ts +107 -0
- package/dist/node/compartment-watcher.js +163 -0
- package/dist/node/fit.d.ts +85 -0
- package/dist/node/fit.js +129 -0
- package/dist/node/node-renderer.d.ts +49 -0
- package/dist/node/node-renderer.js +611 -0
- package/dist/node/node-view.d.ts +51 -0
- package/dist/node/node-view.js +89 -0
- package/dist/node/paint.d.ts +84 -0
- package/dist/node/paint.js +143 -0
- package/dist/node/sd-glyphs.d.ts +36 -0
- package/dist/node/sd-glyphs.js +79 -0
- package/dist/node/stm-glyphs.d.ts +22 -0
- package/dist/node/stm-glyphs.js +111 -0
- package/dist/node/tier-metrics.d.ts +38 -0
- package/dist/node/tier-metrics.js +45 -0
- package/dist/operators.d.ts +45 -0
- package/dist/operators.js +75 -0
- package/dist/plantuml-import.d.ts +113 -0
- package/dist/plantuml-import.js +1723 -0
- package/dist/plantuml.d.ts +94 -0
- package/dist/plantuml.js +985 -0
- package/dist/presets.d.ts +149 -0
- package/dist/presets.js +302 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +358 -0
- package/dist/reading.d.ts +160 -0
- package/dist/reading.js +648 -0
- package/dist/roles.d.ts +257 -0
- package/dist/roles.js +997 -0
- package/dist/rules.d.ts +267 -0
- package/dist/rules.js +3090 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +197 -0
- package/dist/toolbar/config.d.ts +228 -0
- package/dist/toolbar/config.js +833 -0
- package/dist/toolbar/icons.d.ts +440 -0
- package/dist/toolbar/icons.js +711 -0
- package/dist/toolbar/senior-tool.d.ts +11 -0
- package/dist/toolbar/senior-tool.js +21 -0
- package/dist/toolbar/uml-menu.d.ts +17 -0
- package/dist/toolbar/uml-menu.js +20 -0
- package/dist/toolbar/uml-senior-button.d.ts +18 -0
- package/dist/toolbar/uml-senior-button.js +22 -0
- package/dist/translations.d.ts +27 -0
- package/dist/translations.js +117 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +236 -0
- package/dist/xmi-import.d.ts +174 -0
- package/dist/xmi-import.js +2268 -0
- package/dist/xmi.d.ts +61 -0
- package/dist/xmi.js +1860 -0
- package/dist/xml-reader.d.ts +157 -0
- package/dist/xml-reader.js +430 -0
- package/dist/xml.d.ts +72 -0
- package/dist/xml.js +96 -0
- 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;
|
package/dist/node/fit.js
ADDED
|
@@ -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 {};
|