@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,430 @@
|
|
|
1
|
+
import { backgroundLabelHits, DeclaredBackgroundView, EdgelessCRUDIdentifier, hitTestBackgroundLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { TranslationProvider } from '@formicoidea/labre-core/shared/services';
|
|
3
|
+
import { UML_DIAGRAM_FRAME, UML_FRAGMENT_FRAME, UML_REGION_FRAME, UML_SUBJECT_FRAME, umlPartitionFrame, } from './background.js';
|
|
4
|
+
import { umlFragmentOperands, umlInDiagramBand, umlInPartitionBand, umlInRegionBand, umlOperandBoundaryAt, umlOperandGuardAt, } from './board-hit.js';
|
|
5
|
+
import { UML_OPERAND_MIN_HEIGHT } from './consts.js';
|
|
6
|
+
import { umlFragmentAsPainted } from './element-renderer.js';
|
|
7
|
+
/**
|
|
8
|
+
* The one gesture every UML frame carries: a double-click on the name edits it
|
|
9
|
+
* in place.
|
|
10
|
+
*
|
|
11
|
+
* The gesture itself — the rotation arithmetic, the widened `includesPoint`,
|
|
12
|
+
* the double-click handler and the `<input>` — is the library's shared one,
|
|
13
|
+
* `DeclaredBackgroundView` (issue #355, R14: extend, never copy). What this
|
|
14
|
+
* class adds is only what UML answers differently: WHICH words a double-click
|
|
15
|
+
* aims at, and the model those words are read from.
|
|
16
|
+
*
|
|
17
|
+
* Which labels exist, where they sit and what they SAY all come from the
|
|
18
|
+
* declaration the renderer paints (`backgroundLabelHits`), so a label can never
|
|
19
|
+
* be drawn in one place and clicked in another — which is why this class has no
|
|
20
|
+
* coordinates of its own.
|
|
21
|
+
*
|
|
22
|
+
* ponytail: like every other framework view in the library, only `name` may be
|
|
23
|
+
* written — see {@link UmlDiagramView} for what that costs the diagram frame,
|
|
24
|
+
* whose drawn label is not `name`.
|
|
25
|
+
*/
|
|
26
|
+
class UmlFrameView extends DeclaredBackgroundView {
|
|
27
|
+
/**
|
|
28
|
+
* The model this view hit-tests against — the one the RENDERER was handed.
|
|
29
|
+
*
|
|
30
|
+
* The stored model for every frame but one, and the hook exists for that one:
|
|
31
|
+
* a combined fragment paints through `umlFragmentAsPainted`, which hides a
|
|
32
|
+
* declared `name` that operand zero's guard already covers. The boxes
|
|
33
|
+
* `backgroundLabelHits` derives are only the drawn words if they are derived
|
|
34
|
+
* from the same declaration — otherwise the suppressed corner stays clickable
|
|
35
|
+
* and opens an editor on a string nothing paints.
|
|
36
|
+
*
|
|
37
|
+
* A getter and not a cached value: the props it reads are the document's, and
|
|
38
|
+
* a view outlives every edit to them.
|
|
39
|
+
*/
|
|
40
|
+
get _painted() {
|
|
41
|
+
return this.model;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The declaration's editable labels, element-local, derived from the PAINTED
|
|
45
|
+
* model — see {@link _painted}. This is `DeclaredBackgroundView.labelAt`'s
|
|
46
|
+
* own walk with that one substitution, which is why the frames below do not
|
|
47
|
+
* call `super.labelAt`.
|
|
48
|
+
*/
|
|
49
|
+
_hits(w, h) {
|
|
50
|
+
return backgroundLabelHits(this.def, this._painted, w, h, this.gfx.std.getOptional(TranslationProvider));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Which region of the frame aims at the name, given an ELEMENT-LOCAL point.
|
|
54
|
+
*
|
|
55
|
+
* The DRAWN WORDS by default — the box `backgroundLabelHits` derives from the
|
|
56
|
+
* very anchor the renderer paints at. That is the right answer for a name
|
|
57
|
+
* written INSIDE the plot, over the drawing, where anything wider would
|
|
58
|
+
* swallow clicks meant for the elements around it: the subject's name sits in
|
|
59
|
+
* its top-left corner, and this is its whole story.
|
|
60
|
+
*
|
|
61
|
+
* A frame whose name has a band of its own overrides this — see
|
|
62
|
+
* {@link UmlDiagramView}.
|
|
63
|
+
*/
|
|
64
|
+
labelAt(lx, ly, w, h) {
|
|
65
|
+
const hit = hitTestBackgroundLabel(this._hits(w, h), lx, ly);
|
|
66
|
+
return hit && hit.prop === 'name' ? { prop: 'name', text: hit.text } : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The name label, answered over a whole BAND rather than over its words —
|
|
70
|
+
* for the frames whose name has a strip of its own.
|
|
71
|
+
*
|
|
72
|
+
* @param inBand whether the point is in the frame's band, as the frame's own
|
|
73
|
+
* band helper (`board-hit.ts`) decides.
|
|
74
|
+
*/
|
|
75
|
+
_nameInBand(inBand, w, h) {
|
|
76
|
+
if (!inBand)
|
|
77
|
+
return null;
|
|
78
|
+
const name = this._hits(w, h).find(hit => hit.prop === 'name');
|
|
79
|
+
return name ? { prop: 'name', text: name.text } : null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The sheet a UML diagram is drawn on. Double-click ANYWHERE in its heading band
|
|
84
|
+
* to rename it.
|
|
85
|
+
*
|
|
86
|
+
* The band and not the words, which is the first thing this subclass changes —
|
|
87
|
+
* the same call the C4 board and the BPMN pool's participant band both make: a
|
|
88
|
+
* strip you may only double-click the eleven characters of is a target that lies
|
|
89
|
+
* about where it is. Here it also buys something they do not need: the tag is
|
|
90
|
+
* drawn round the words with padding on either side, so a user aiming at the
|
|
91
|
+
* pentagon they can see is inside the zone whether or not they hit a glyph.
|
|
92
|
+
*
|
|
93
|
+
* The second is what the editor opens on, and that is the one place UML parts
|
|
94
|
+
* company with the C4 board it is modelled on. Annex A's heading is
|
|
95
|
+
* `<kind> <name>` — a derived string, joined from a closed discriminant the
|
|
96
|
+
* picker owns and a name the author owns — so opening on the drawn words would
|
|
97
|
+
* hand the user `class Orders` to edit, and committing it would write the kind
|
|
98
|
+
* INTO the name, to be prefixed again on the next paint. The editor therefore
|
|
99
|
+
* opens on `name` — the author's half — and writes `name` back, never the drawn
|
|
100
|
+
* label's own prop: `heading` is a getter, and a getter is not somewhere a
|
|
101
|
+
* rename can land.
|
|
102
|
+
*/
|
|
103
|
+
export class UmlDiagramView extends UmlFrameView {
|
|
104
|
+
static { this.type = 'umlDiagram'; }
|
|
105
|
+
get def() {
|
|
106
|
+
return UML_DIAGRAM_FRAME;
|
|
107
|
+
}
|
|
108
|
+
labelAt(lx, ly, w, h) {
|
|
109
|
+
if (!umlInDiagramBand(this.model, [lx, ly]))
|
|
110
|
+
return null;
|
|
111
|
+
const heading = this._hits(w, h).some(hit => hit.prop === 'heading');
|
|
112
|
+
return heading ? { prop: 'name', text: this.model.name } : null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** The rectangle drawn round a system's use cases. Double-click its name. */
|
|
116
|
+
export class UmlSubjectView extends UmlFrameView {
|
|
117
|
+
static { this.type = 'umlSubject'; }
|
|
118
|
+
get def() {
|
|
119
|
+
return UML_SUBJECT_FRAME;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* An activity swimlane (§15.6.4). Double-click ANYWHERE in its name band to
|
|
124
|
+
* rename it — across the top of a vertical lane, down the left edge of a
|
|
125
|
+
* horizontal one.
|
|
126
|
+
*
|
|
127
|
+
* The band and not the words, the same call the diagram frame makes: a strip you
|
|
128
|
+
* may only double-click the eight characters of is a target that lies about
|
|
129
|
+
* where it is. It matters more here than on the sheet, because a partition is
|
|
130
|
+
* TRANSPARENT: every point that is not its own furniture falls through to the
|
|
131
|
+
* actions drawn inside it, so the header is the only place a user can take hold
|
|
132
|
+
* of the lane at all.
|
|
133
|
+
*
|
|
134
|
+
* The declaration this hit-tests against is chosen by the element's own
|
|
135
|
+
* `orientation`, through the one function that makes that choice — so the view,
|
|
136
|
+
* the renderer and the band helper cannot disagree about which edge the band is
|
|
137
|
+
* on.
|
|
138
|
+
*/
|
|
139
|
+
export class UmlPartitionView extends UmlFrameView {
|
|
140
|
+
static { this.type = 'umlPartition'; }
|
|
141
|
+
get def() {
|
|
142
|
+
return umlPartitionFrame(this.model);
|
|
143
|
+
}
|
|
144
|
+
labelAt(lx, ly, w, h) {
|
|
145
|
+
return this._nameInBand(umlInPartitionBand(this.model, [lx, ly]), w, h);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A composite state (§14.2.4). Double-click anywhere in its name band to rename
|
|
150
|
+
* it.
|
|
151
|
+
*
|
|
152
|
+
* The partition's story with no orientation to turn: the band is across the top,
|
|
153
|
+
* always, because that is where §14.2.4 writes a state's name.
|
|
154
|
+
*/
|
|
155
|
+
export class UmlRegionView extends UmlFrameView {
|
|
156
|
+
static { this.type = 'umlRegion'; }
|
|
157
|
+
get def() {
|
|
158
|
+
return UML_REGION_FRAME;
|
|
159
|
+
}
|
|
160
|
+
labelAt(lx, ly, w, h) {
|
|
161
|
+
return this._nameInBand(umlInRegionBand(this.model, [lx, ly]), w, h);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A COMBINED FRAGMENT (§17.6.4). Two gestures of its own, on top of the rename
|
|
166
|
+
* every UML frame carries:
|
|
167
|
+
*
|
|
168
|
+
* - **dblclick on a GUARD** — the `[condition]` written in an operand's
|
|
169
|
+
* top-left corner — edits that guard in place. On an unsplit fragment there
|
|
170
|
+
* is one, and it is the element's own `name`; once the box has been cut into
|
|
171
|
+
* operands each of them carries its own, in `operands[i].name`;
|
|
172
|
+
* - **drag on an internal operand SEPARATOR** moves the dashed line, taking
|
|
173
|
+
* from one operand and giving to the other.
|
|
174
|
+
*
|
|
175
|
+
* ## The operator is NOT renamed here
|
|
176
|
+
*
|
|
177
|
+
* Double-clicking the pentagon does nothing, deliberately. `operator` is a
|
|
178
|
+
* closed discriminant of thirteen values (§17.6.4 plus §17.7.4's `ref`) and it
|
|
179
|
+
* is picked from the toolbar; a free-text editor over it would let an author
|
|
180
|
+
* type `altt` and lose every rule, every reading and every export that keys on
|
|
181
|
+
* the word. The same call the diagram frame's `kind` already makes.
|
|
182
|
+
*
|
|
183
|
+
* ## How the separator drag takes the gesture
|
|
184
|
+
*
|
|
185
|
+
* Armed, exactly as `BpmnPoolView`'s is and for the reason stated there:
|
|
186
|
+
* `GfxElementModelView.dispatch` reports a drag as handled whenever a handler
|
|
187
|
+
* is REGISTERED, so a permanent `dragstart` would make the fragment
|
|
188
|
+
* undraggable. The handlers are attached while the pointer is over a separator
|
|
189
|
+
* of a SELECTED fragment and detached the moment it is not.
|
|
190
|
+
*
|
|
191
|
+
* ponytail: a ROTATED fragment is not accounted for by the separator drag —
|
|
192
|
+
* its pointer is converted to element-local coordinates by subtraction, so the
|
|
193
|
+
* separator boxes assume an upright frame (the guards do not: the shared base
|
|
194
|
+
* de-rotates before {@link labelAt}). The same reserve `board-hit.ts`
|
|
195
|
+
* documents; nothing rotates a framework background today.
|
|
196
|
+
*/
|
|
197
|
+
export class UmlFragmentView extends UmlFrameView {
|
|
198
|
+
constructor() {
|
|
199
|
+
super(...arguments);
|
|
200
|
+
/** The separator the pointer is over, and the handlers armed for it. */
|
|
201
|
+
this._armed = null;
|
|
202
|
+
/** Everything a separator drag needs, frozen at `dragstart`. */
|
|
203
|
+
this._drag = null;
|
|
204
|
+
}
|
|
205
|
+
static { this.type = 'umlFragment'; }
|
|
206
|
+
get def() {
|
|
207
|
+
return UML_FRAGMENT_FRAME;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The suppression the renderer applies, read back here so the rename boxes
|
|
211
|
+
* are the drawn words and nothing else.
|
|
212
|
+
*
|
|
213
|
+
* On a SPLIT fragment written by an older build the stored model carries both
|
|
214
|
+
* `name` and `operands`, and the canvas paints operand zero's guard over the
|
|
215
|
+
* declared one's corner — i.e. paints `name` nowhere. `umlFragmentAsPainted`
|
|
216
|
+
* is the single statement of which of the two wins; calling it here is what
|
|
217
|
+
* keeps {@link labelAt}'s fallback from opening an editor on the invisible
|
|
218
|
+
* corner. The operand branch of that method is unaffected: it reads
|
|
219
|
+
* `operands`, which the suppression never touches.
|
|
220
|
+
*/
|
|
221
|
+
get _painted() {
|
|
222
|
+
return umlFragmentAsPainted(this.model);
|
|
223
|
+
}
|
|
224
|
+
onCreated() {
|
|
225
|
+
super.onCreated();
|
|
226
|
+
this.on('pointermove', e => this._updateHover(e));
|
|
227
|
+
this.on('pointerdown', e => this._updateHover(e));
|
|
228
|
+
this.on('pointerleave', () => this._leave());
|
|
229
|
+
}
|
|
230
|
+
onDestroyed() {
|
|
231
|
+
this._leave();
|
|
232
|
+
super.onDestroyed();
|
|
233
|
+
}
|
|
234
|
+
/* ── The rename targets ────────────────────────────────────────────── */
|
|
235
|
+
/**
|
|
236
|
+
* The operand guards first, the declaration's own label second.
|
|
237
|
+
*
|
|
238
|
+
* The order is the order the gestures win in, and it only matters on a SPLIT
|
|
239
|
+
* fragment: the first operand's corner is the very corner the declared
|
|
240
|
+
* `guard` label is anchored in. The operand takes it, because on a split
|
|
241
|
+
* fragment the guards live in `operands` and `name` has been moved into the
|
|
242
|
+
* first of them (`background.ts` states the contract) — handing the
|
|
243
|
+
* double-click to `name` would open an editor on a string nothing paints.
|
|
244
|
+
*/
|
|
245
|
+
labelAt(lx, ly, w, h) {
|
|
246
|
+
const index = umlOperandGuardAt(this.model, [lx, ly]);
|
|
247
|
+
if (index !== null) {
|
|
248
|
+
const operand = this._operands()[index];
|
|
249
|
+
if (operand) {
|
|
250
|
+
// A guard lives INSIDE `operands`, which no `{ [prop]: value }` patch
|
|
251
|
+
// can reach — hence the label's own `commit`, the base's one write
|
|
252
|
+
// hook. `prop` names the prop the hook rewrites; the base never writes
|
|
253
|
+
// it itself.
|
|
254
|
+
return {
|
|
255
|
+
prop: 'operands',
|
|
256
|
+
text: operand.name ?? '',
|
|
257
|
+
commit: value => this._writeGuard(index, value),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return super.labelAt(lx, ly, w, h);
|
|
262
|
+
}
|
|
263
|
+
_operands() {
|
|
264
|
+
const stored = this.model.operands;
|
|
265
|
+
return Array.isArray(stored) ? stored : [];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Write one operand's guard, dropping the key entirely when it is cleared.
|
|
269
|
+
*
|
|
270
|
+
* The same shape `renameBpmnLane` writes: an absent `name` is an unguarded
|
|
271
|
+
* operand — which is what an `else` branch with no condition is — and an
|
|
272
|
+
* empty string left in the array would be a key that means nothing and
|
|
273
|
+
* paints nothing.
|
|
274
|
+
*
|
|
275
|
+
* No undo boundary here: the shared editor captures one before it calls a
|
|
276
|
+
* label's `commit`.
|
|
277
|
+
*/
|
|
278
|
+
_writeGuard(index, value) {
|
|
279
|
+
const operands = this._operands();
|
|
280
|
+
const operand = operands[index];
|
|
281
|
+
if (!operand)
|
|
282
|
+
return;
|
|
283
|
+
const trimmed = value.trim();
|
|
284
|
+
if ((operand.name ?? '') === trimmed)
|
|
285
|
+
return;
|
|
286
|
+
const next = operands.map((entry, i) => i === index
|
|
287
|
+
? {
|
|
288
|
+
id: entry.id,
|
|
289
|
+
...(trimmed ? { name: trimmed } : {}),
|
|
290
|
+
size: entry.size,
|
|
291
|
+
}
|
|
292
|
+
: entry);
|
|
293
|
+
this.gfx.std
|
|
294
|
+
.get(EdgelessCRUDIdentifier)
|
|
295
|
+
.updateElement(this.model.id, { operands: next });
|
|
296
|
+
}
|
|
297
|
+
/* ── The separator drag ────────────────────────────────────────────── */
|
|
298
|
+
/**
|
|
299
|
+
* A fragment is SELECTED by its border and its operator band — but the
|
|
300
|
+
* internal operand separators are twelve-unit strips in the middle of the
|
|
301
|
+
* plot, and they must still receive their pointer events.
|
|
302
|
+
*
|
|
303
|
+
* Not a selection zone: a click between two messages still goes to whatever
|
|
304
|
+
* is under it, because picking asks the MODEL. This only decides where the
|
|
305
|
+
* fragment's own gestures are heard.
|
|
306
|
+
*/
|
|
307
|
+
includesPoint(x, y, options, host) {
|
|
308
|
+
if (super.includesPoint(x, y, options, host))
|
|
309
|
+
return true;
|
|
310
|
+
const [ex, ey] = this.model.deserializedXYWH;
|
|
311
|
+
return umlOperandBoundaryAt(this.model, [x - ex, y - ey]) !== null;
|
|
312
|
+
}
|
|
313
|
+
/** Whether this fragment may be written to at all. */
|
|
314
|
+
get _writable() {
|
|
315
|
+
return !this.gfx.std.store.readonly && !this.model.isLocked();
|
|
316
|
+
}
|
|
317
|
+
_leave() {
|
|
318
|
+
this._disarm();
|
|
319
|
+
if (!this._drag)
|
|
320
|
+
this.gfx.cursor$.value = 'default';
|
|
321
|
+
}
|
|
322
|
+
_localPoint(e) {
|
|
323
|
+
const [mx, my] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
|
324
|
+
const [ex, ey] = this.model.deserializedXYWH;
|
|
325
|
+
return [mx - ex, my - ey];
|
|
326
|
+
}
|
|
327
|
+
/** One pass over the pointer: the cursor, and whether a drag is armed. */
|
|
328
|
+
_updateHover(e) {
|
|
329
|
+
const local = this._localPoint(e);
|
|
330
|
+
const selected = this.gfx.selection.selectedIds.includes(this.model.id);
|
|
331
|
+
if (this._writable && selected) {
|
|
332
|
+
const index = umlOperandBoundaryAt(this.model, local);
|
|
333
|
+
if (index !== null) {
|
|
334
|
+
// Reasserted on every move rather than only on arrival: the cursor is
|
|
335
|
+
// shared with the resize handles and the tools, and whoever set it last
|
|
336
|
+
// wins — so the one that is still true says so again.
|
|
337
|
+
this.gfx.cursor$.value = 'ns-resize';
|
|
338
|
+
if (this._armed?.index !== index) {
|
|
339
|
+
this._disarm();
|
|
340
|
+
this._armed = {
|
|
341
|
+
index,
|
|
342
|
+
disposers: [
|
|
343
|
+
this.on('dragstart', () => this._onDragStart()),
|
|
344
|
+
this.on('dragmove', evt => this._onDragMove(evt)),
|
|
345
|
+
this.on('dragend', () => this._onDragEnd()),
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this._disarm();
|
|
353
|
+
// A guard announces itself, selected or not: finding out that a condition
|
|
354
|
+
// can be changed should not cost a click first.
|
|
355
|
+
const overGuard = this._writable && umlOperandGuardAt(this.model, local) !== null;
|
|
356
|
+
this.gfx.cursor$.value = overGuard ? 'text' : 'default';
|
|
357
|
+
}
|
|
358
|
+
_disarm() {
|
|
359
|
+
// Never mid-gesture: the pointer leaves the separator as soon as the drag
|
|
360
|
+
// starts moving, and disarming there would drop it on its first step.
|
|
361
|
+
if (this._drag)
|
|
362
|
+
return;
|
|
363
|
+
if (!this._armed)
|
|
364
|
+
return;
|
|
365
|
+
this._armed.disposers.forEach(dispose => dispose());
|
|
366
|
+
this._armed = null;
|
|
367
|
+
}
|
|
368
|
+
_onDragStart() {
|
|
369
|
+
const index = this._armed?.index;
|
|
370
|
+
const geometry = umlFragmentOperands(this.model);
|
|
371
|
+
if (index === undefined || !geometry || !this._writable)
|
|
372
|
+
return;
|
|
373
|
+
const operands = this._operands();
|
|
374
|
+
if (!operands[index - 1] || !operands[index])
|
|
375
|
+
return;
|
|
376
|
+
const total = operands.reduce((sum, operand) => sum + operand.size, 0);
|
|
377
|
+
if (!(total > 0))
|
|
378
|
+
return;
|
|
379
|
+
this._drag = {
|
|
380
|
+
index,
|
|
381
|
+
operands: operands.map(operand => ({ ...operand })),
|
|
382
|
+
total,
|
|
383
|
+
plotHeight: geometry.plot.height,
|
|
384
|
+
// The floor is a HEIGHT the user can see, stated in the model units of a
|
|
385
|
+
// fragment at its reference height and converted to a weight against this
|
|
386
|
+
// fragment's own total — so a fragment stretched to twice the height
|
|
387
|
+
// keeps the same visible floor, which is the point of weights.
|
|
388
|
+
minWeight: (UML_OPERAND_MIN_HEIGHT / geometry.plot.height) * total,
|
|
389
|
+
};
|
|
390
|
+
// Local writes until the release: the intermediate weights repaint the
|
|
391
|
+
// canvas but never reach the document, so the whole drag is ONE undo step.
|
|
392
|
+
this.model.stash('operands');
|
|
393
|
+
}
|
|
394
|
+
_onDragMove(e) {
|
|
395
|
+
const drag = this._drag;
|
|
396
|
+
if (!drag)
|
|
397
|
+
return;
|
|
398
|
+
// The travel SINCE the drag started, in view pixels (`e.delta` is the step
|
|
399
|
+
// since the last move, which is not the same thing), converted to model
|
|
400
|
+
// units. Always recomputed from the FROZEN pair rather than nudged: a nudge
|
|
401
|
+
// would accumulate the clamp and drift away from the pointer.
|
|
402
|
+
const dy = (e.y - e.start.y) / (this.gfx.viewport.zoom || 1);
|
|
403
|
+
const perUnit = drag.total / drag.plotHeight;
|
|
404
|
+
const above = drag.operands[drag.index - 1];
|
|
405
|
+
const below = drag.operands[drag.index];
|
|
406
|
+
const pair = above.size + below.size;
|
|
407
|
+
// The pair's total is invariant: the separator takes from one and gives to
|
|
408
|
+
// the other, so no operand the user is not touching changes size.
|
|
409
|
+
const wanted = above.size + dy * perUnit;
|
|
410
|
+
const floor = Math.min(drag.minWeight, pair / 2);
|
|
411
|
+
const nextAbove = Math.max(floor, Math.min(pair - floor, wanted));
|
|
412
|
+
this.model.operands = drag.operands.map((operand, i) => i === drag.index - 1
|
|
413
|
+
? { ...operand, size: nextAbove }
|
|
414
|
+
: i === drag.index
|
|
415
|
+
? { ...operand, size: pair - nextAbove }
|
|
416
|
+
: operand);
|
|
417
|
+
}
|
|
418
|
+
_onDragEnd() {
|
|
419
|
+
const drag = this._drag;
|
|
420
|
+
this._drag = null;
|
|
421
|
+
if (!drag)
|
|
422
|
+
return;
|
|
423
|
+
// Before the commit, not after: `pop` writes straight into the Y.Map, and
|
|
424
|
+
// without a boundary here a separator moved within half a second of the
|
|
425
|
+
// previous edit would be undone together with it.
|
|
426
|
+
this.gfx.std.store.captureSync();
|
|
427
|
+
this.model.pop('operands');
|
|
428
|
+
this._disarm();
|
|
429
|
+
}
|
|
430
|
+
}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { UmlModel } from './model.js';
|
|
2
|
+
/**
|
|
3
|
+
* The glue between the models and the two writers — what the interchange
|
|
4
|
+
* capabilities and the commands both call.
|
|
5
|
+
*
|
|
6
|
+
* Twenty lines and no cleverness, which is the point: there is ONE place that
|
|
7
|
+
* decides what several selected diagrams mean for each format, so the command
|
|
8
|
+
* and the registry cannot produce different bytes. The formats answer that
|
|
9
|
+
* question differently, and the difference is theirs rather than ours:
|
|
10
|
+
*
|
|
11
|
+
* - **PlantUML renders one diagram per document.** `@startuml … @enduml` is a
|
|
12
|
+
* picture; two of them in one file is two pictures, which every renderer in
|
|
13
|
+
* the ecosystem handles by drawing them in sequence. So several diagrams are
|
|
14
|
+
* several documents, concatenated.
|
|
15
|
+
* - **XMI carries one model.** Four sheets of one architecture are four
|
|
16
|
+
* packages of one `uml:Model`, not four files — and writing them as four
|
|
17
|
+
* would lose the fact that a class on one sheet is the class on another.
|
|
18
|
+
*
|
|
19
|
+
* Pure: models in, a string and its warnings out. No `std`, no DOM, no clock —
|
|
20
|
+
* `docs/adr/0012` P3.
|
|
21
|
+
*/
|
|
22
|
+
/** A document, plus what the writer could not say about the drawing. */
|
|
23
|
+
export interface UmlExportResult {
|
|
24
|
+
text: string;
|
|
25
|
+
/**
|
|
26
|
+
* One line each, in the user's words, deduplicated.
|
|
27
|
+
*
|
|
28
|
+
* Deduplicated because the warnings are a SENTENCE about a kind of loss, not
|
|
29
|
+
* a row per element: three untyped connectors on one board produce one line
|
|
30
|
+
* telling the author what to do about all three. A per-element list would need
|
|
31
|
+
* names the exporter does not have (a connector has no name) and would read as
|
|
32
|
+
* a stack trace.
|
|
33
|
+
*/
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface UmlExportOptions {
|
|
37
|
+
/** The name the document takes — the board's title, already sanitized. */
|
|
38
|
+
name?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The diagrams as PlantUML — one `@startuml … @enduml` document each.
|
|
42
|
+
*
|
|
43
|
+
* Takes no name, where the XMI writer does: a PlantUML `title` is the DIAGRAM's
|
|
44
|
+
* own heading — `class Orders`, the frame tag of Annex A — and the file's name
|
|
45
|
+
* is a different fact about a different thing. Overriding one with the other
|
|
46
|
+
* would drop the kind tag, which is the half of the heading a `.puml` has
|
|
47
|
+
* nowhere else to put.
|
|
48
|
+
*
|
|
49
|
+
* No diagram is not an error: it is a selection nobody made a statement with,
|
|
50
|
+
* and the honest answer is the smallest document that still parses. The same
|
|
51
|
+
* reading `exportC4Mermaid` gives an empty board.
|
|
52
|
+
*/
|
|
53
|
+
export declare function exportUmlPlantuml(models: readonly UmlModel[]): UmlExportResult;
|
|
54
|
+
/**
|
|
55
|
+
* The diagrams as one XMI document — one `uml:Model`, one package per diagram.
|
|
56
|
+
*
|
|
57
|
+
* The caller's `name` names the MODEL rather than any package, because that is
|
|
58
|
+
* what it is: the document's own name, the board title the user exported under.
|
|
59
|
+
* Each package keeps the name of the diagram it was drawn as.
|
|
60
|
+
*/
|
|
61
|
+
export declare function exportUmlXmi(models: readonly UmlModel[], options?: UmlExportOptions): UmlExportResult;
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { exportPlantuml } from './plantuml.js';
|
|
2
|
+
import { exportXmi } from './xmi.js';
|
|
3
|
+
/** Every warning the models collected, in order, without repeats. */
|
|
4
|
+
function warningsOf(models) {
|
|
5
|
+
return [...new Set(models.flatMap(model => model.warnings))];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The diagrams as PlantUML — one `@startuml … @enduml` document each.
|
|
9
|
+
*
|
|
10
|
+
* Takes no name, where the XMI writer does: a PlantUML `title` is the DIAGRAM's
|
|
11
|
+
* own heading — `class Orders`, the frame tag of Annex A — and the file's name
|
|
12
|
+
* is a different fact about a different thing. Overriding one with the other
|
|
13
|
+
* would drop the kind tag, which is the half of the heading a `.puml` has
|
|
14
|
+
* nowhere else to put.
|
|
15
|
+
*
|
|
16
|
+
* No diagram is not an error: it is a selection nobody made a statement with,
|
|
17
|
+
* and the honest answer is the smallest document that still parses. The same
|
|
18
|
+
* reading `exportC4Mermaid` gives an empty board.
|
|
19
|
+
*/
|
|
20
|
+
export function exportUmlPlantuml(models) {
|
|
21
|
+
if (models.length === 0) {
|
|
22
|
+
return { text: '@startuml\n@enduml\n', warnings: [] };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
text: models.map(model => exportPlantuml(model)).join('\n'),
|
|
26
|
+
warnings: warningsOf(models),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The diagrams as one XMI document — one `uml:Model`, one package per diagram.
|
|
31
|
+
*
|
|
32
|
+
* The caller's `name` names the MODEL rather than any package, because that is
|
|
33
|
+
* what it is: the document's own name, the board title the user exported under.
|
|
34
|
+
* Each package keeps the name of the diagram it was drawn as.
|
|
35
|
+
*/
|
|
36
|
+
export function exportUmlXmi(models, options = {}) {
|
|
37
|
+
return {
|
|
38
|
+
text: exportXmi(models, options),
|
|
39
|
+
warnings: warningsOf(models),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name a file system will accept, minus the extension.
|
|
3
|
+
*
|
|
4
|
+
* The same transformations `c4SafeFilename` and `bpmnSafeFilename` apply, and
|
|
5
|
+
* for the same reasons — reserved characters become `-`, whitespace runs
|
|
6
|
+
* collapse, the result is capped, and the Windows tail of dots and spaces is
|
|
7
|
+
* trimmed AFTER the cap so the extension is not the thing that gets eaten.
|
|
8
|
+
*
|
|
9
|
+
* Duplicated rather than imported, which is the trade the two packs before this
|
|
10
|
+
* one made explicitly: a filename rule is not API, and coupling three frameworks
|
|
11
|
+
* so they can share eight lines buys nothing and costs a shared thing to break.
|
|
12
|
+
*
|
|
13
|
+
* `fallback` is a parameter here where the other two hard-code theirs, because
|
|
14
|
+
* UML writes two formats out of one pack and the word a nameless document should
|
|
15
|
+
* take is the FORMAT's rather than the framework's.
|
|
16
|
+
*/
|
|
17
|
+
export declare function umlSafeFilename(raw: string | undefined, fallback?: string): string;
|
package/dist/filename.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name a file system will accept, minus the extension.
|
|
3
|
+
*
|
|
4
|
+
* The same transformations `c4SafeFilename` and `bpmnSafeFilename` apply, and
|
|
5
|
+
* for the same reasons — reserved characters become `-`, whitespace runs
|
|
6
|
+
* collapse, the result is capped, and the Windows tail of dots and spaces is
|
|
7
|
+
* trimmed AFTER the cap so the extension is not the thing that gets eaten.
|
|
8
|
+
*
|
|
9
|
+
* Duplicated rather than imported, which is the trade the two packs before this
|
|
10
|
+
* one made explicitly: a filename rule is not API, and coupling three frameworks
|
|
11
|
+
* so they can share eight lines buys nothing and costs a shared thing to break.
|
|
12
|
+
*
|
|
13
|
+
* `fallback` is a parameter here where the other two hard-code theirs, because
|
|
14
|
+
* UML writes two formats out of one pack and the word a nameless document should
|
|
15
|
+
* take is the FORMAT's rather than the framework's.
|
|
16
|
+
*/
|
|
17
|
+
export function umlSafeFilename(raw, fallback = 'diagram') {
|
|
18
|
+
const safe = (raw ?? '')
|
|
19
|
+
.trim()
|
|
20
|
+
.replaceAll(/[\\/:*?"<>|]/g, '-')
|
|
21
|
+
.replaceAll(/\s+/g, ' ')
|
|
22
|
+
.trim()
|
|
23
|
+
.slice(0, 120)
|
|
24
|
+
.replace(/[. ]+$/, '');
|
|
25
|
+
return safe || fallback;
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BackgroundRect, ElementRenderer, FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
/**
|
|
4
|
+
* The cut-corner heading tag of UML 2.5.1 Annex A, as a DECORATOR over a
|
|
5
|
+
* framework-background renderer.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a decorator and not a new field of the declaration language
|
|
8
|
+
*
|
|
9
|
+
* The background primitive draws cards, bands, washes, zones, axes and labels.
|
|
10
|
+
* The pentagon Annex A pins to the top-left corner of every diagram frame is
|
|
11
|
+
* none of those: it is one shape, in one notation, and teaching
|
|
12
|
+
* `FrameworkBackgroundDef` to describe a polygon with a bitten corner would put
|
|
13
|
+
* a UML-shaped hole in a language six other frameworks share. So the frame keeps
|
|
14
|
+
* declaring its band and its label exactly like every other background, and this
|
|
15
|
+
* wrapper adds the outline after the fact — the same seam the BPMN pool's lane
|
|
16
|
+
* separators and the C4 node's glyphs use, one level up.
|
|
17
|
+
*
|
|
18
|
+
* The tag carries NO words of its own. The heading is drawn by the declaration,
|
|
19
|
+
* from the declaration's anchor, in the declaration's font; this file only
|
|
20
|
+
* measures it and draws a box round it. A tag that wrote its own text would be
|
|
21
|
+
* the same string painted twice, by two files, which is how a heading comes to
|
|
22
|
+
* be drawn in one place and clicked in another.
|
|
23
|
+
*
|
|
24
|
+
* ## The matrix trap
|
|
25
|
+
*
|
|
26
|
+
* `createFrameworkBackgroundRenderer` composes the element-local frame by
|
|
27
|
+
* MUTATING the matrix it is handed (`matrix.translateSelf(...).rotateSelf(...)`)
|
|
28
|
+
* and then calls `setTransform` with it. Reading the matrix after the base has
|
|
29
|
+
* run therefore yields a frame that has already been translated and rotated
|
|
30
|
+
* once, and composing on top of it rotates the tag twice — which is invisible on
|
|
31
|
+
* an upright frame and wildly wrong on a rotated one. The copy below is taken
|
|
32
|
+
* BEFORE the base renderer is called, which is the same precaution
|
|
33
|
+
* `gfx/c4/src/node/node-renderer.ts` documents for the shape renderer.
|
|
34
|
+
*/
|
|
35
|
+
export interface UmlFrameTagOptions {
|
|
36
|
+
/**
|
|
37
|
+
* The model prop the heading is read from — the same one the declaration's
|
|
38
|
+
* band label binds, so the tag measures the string that is actually painted.
|
|
39
|
+
*/
|
|
40
|
+
prop: string;
|
|
41
|
+
/** The band the tag is drawn in: the frame's top margin. */
|
|
42
|
+
bandHeight: number;
|
|
43
|
+
/** How much shorter than the band the tag is — the air under it. */
|
|
44
|
+
foot: number;
|
|
45
|
+
/** Horizontal padding either side of the measured heading. */
|
|
46
|
+
padding: number;
|
|
47
|
+
/** The bite out of the bottom-right corner. */
|
|
48
|
+
cut: number;
|
|
49
|
+
/** Ink and weight of the outline — the frame's own. */
|
|
50
|
+
stroke: string;
|
|
51
|
+
lineWidth: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The tag's box, in ELEMENT-LOCAL model units — everything but the corner bite.
|
|
55
|
+
*
|
|
56
|
+
* Split out and exported so the geometry can be asserted without a canvas, and
|
|
57
|
+
* so a future gesture (aim at the tag, not at the band) has one place to ask.
|
|
58
|
+
* `null` when there is nothing to draw: a frame dragged to nothing, or a heading
|
|
59
|
+
* of no words at all.
|
|
60
|
+
*/
|
|
61
|
+
export declare function umlFrameTagRect(width: number, height: number, textWidth: number, opts: UmlFrameTagOptions): BackgroundRect | null;
|
|
62
|
+
/**
|
|
63
|
+
* Wrap a frame declaration's renderer so the heading is drawn inside Annex A's
|
|
64
|
+
* pentagon.
|
|
65
|
+
*
|
|
66
|
+
* Stroke only, in the frame's own ink: the tag is an outline pinned to the
|
|
67
|
+
* sheet's corner, and filling it would hide the card behind the words — which
|
|
68
|
+
* matters the moment a user drags an element under the heading.
|
|
69
|
+
*/
|
|
70
|
+
export declare function withUmlFrameTag<T extends GfxPrimitiveElementModel>(def: FrameworkBackgroundDef, opts: UmlFrameTagOptions): ElementRenderer<T>;
|