@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,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
+ }
@@ -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;
@@ -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>;