@formicoidea/labre-framework-uml 0.59.0 → 0.60.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/dist/actions.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type UmlDiagramKind, UmlDiagramElementModel, type UmlFragmentOperator } from '@formicoidea/labre-core/model';
2
2
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
3
  import { type GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
- import { type UmlEdgeRole } from './edge-styles.js';
4
+ import type { UmlEdgeRole } from './edge-styles.js';
5
5
  /**
6
6
  * Create a UML diagram frame centred on the viewport — the sheet one diagram is
7
7
  * drawn on (Annex A).
@@ -164,14 +164,14 @@ export type UmlGlyphKind = 'package' | 'note' | 'actor' | 'use-case' | 'port' |
164
164
  *
165
165
  * ## `uml:name` for five of them, `uml:label` for the rest
166
166
  *
167
- * See {@link NAMED_GLYPHS}: a package, a note and the three cubes carry a NAME
168
- * the grammar parses a keyword off; a port, a ball and a socket carry a LABEL.
169
- * Keeping them apart is what lets the exporter read a tier without first asking
170
- * what shape it is under.
167
+ * See `NAMED_GLYPH_KINDS` (`presets.ts`): a package, a note and the three
168
+ * cubes carry a NAME the grammar parses a keyword off; a port, a ball and a
169
+ * socket carry a LABEL. Keeping them apart is what lets the exporter read a
170
+ * tier without first asking what shape it is under.
171
171
  *
172
172
  * ## The label is where the notation puts it, and that is not this file's call
173
173
  *
174
- * The box comes out of {@link umlCompartmentBoxes}, which is where every
174
+ * The box comes out of `umlCompartmentBoxes`, which is where every
175
175
  * "beside the glyph" and "inside the front face" decision is written down: a
176
176
  * port's label sits next to the square rather than on it (the square is 16
177
177
  * units, and words do not fit in it), an interface mark's sits beside the ball
@@ -214,12 +214,13 @@ export declare function createUmlNode(std: BlockStdScope, kind: UmlGlyphKind): v
214
214
  *
215
215
  * ## How many tiers is the LAYOUT's answer, not this file's
216
216
  *
217
- * The walk below writes a tier for each box `umlCompartmentBoxes` returns and
218
- * for no other, which is what lets two kinds be added to the notation without a
219
- * line changing here: a component and an artifact arrive with the name tier and
220
- * the one body tier §11.6.4 and §19.3.4 draw them with, because that is what the
221
- * layout hands over. The alternative — a table of tier counts in this file —
222
- * would be a second statement of the geometry, and the first one to drift.
217
+ * The assembly (`umlArtefactElements`) writes a tier for each box
218
+ * `umlCompartmentBoxes` returns and for no other, which is what lets two kinds
219
+ * be added to the notation without a line changing here: a component and an
220
+ * artifact arrive with the name tier and the one body tier §11.6.4 and §19.3.4
221
+ * draw them with, because that is what the layout hands over. The alternative
222
+ * — a table of tier counts in this file — would be a second statement of the
223
+ * geometry, and the first one to drift.
223
224
  *
224
225
  * ## Four or five elements, and the object is the four
225
226
  *
@@ -264,7 +265,7 @@ export type { UmlEdgeRole };
264
265
  * Because UML has twelve of them and they differ on three props out of a table
265
266
  * that is otherwise identical — the same mode, the same ink, the same weight. A
266
267
  * dozen near-identical functions would be a dozen places for a stroke width to
267
- * drift, and {@link UML_EDGE_STYLE} is the notation itself, which is the thing
268
+ * drift, and `UML_EDGE_STYLE` is the notation itself, which is the thing
268
269
  * worth reading. It is also what made phase 2 free: `deploy`, `manifest` and
269
270
  * `communication-path` arm through this function without a line changing in it.
270
271
  *
package/dist/actions.js CHANGED
@@ -1,19 +1,16 @@
1
1
  import { DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
- import { ConnectorMode, TextAlign, UmlDiagramElementModel, } from '@formicoidea/labre-core/model';
3
+ import { UmlDiagramElementModel, } from '@formicoidea/labre-core/model';
4
4
  import { translateKey } from '@formicoidea/labre-core/shared/services';
5
5
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
6
- import { Bound } from '@formicoidea/labre-core/global/gfx';
7
6
  import { GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
8
- import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
9
- import { UML_BODY_FONT_SIZE, UML_DIAGRAM_BOX, UML_EDGE_WIDTH, UML_FRAGMENT_BOX, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_PARTITION_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
7
+ import { UML_NODE_BOX } from './consts.js';
10
8
  import { decodeDrawio } from './drawio-decode.js';
11
- import { UML_EDGE_STYLE } from './edge-styles.js';
12
9
  import { umlSafeFilename } from './filename.js';
13
10
  import { UML_DRAWIO_IMPORT, UML_PLANTUML_EXPORT, UML_PLANTUML_IMPORT, UML_XMI_EXPORT, UML_XMI_IMPORT, } from './interchange.js';
14
- import { UML_ATTRIBUTES_SEED, UML_ATTRIBUTES_SEED_KEY, UML_NAME_SEED, UML_OPERATIONS_SEED, UML_OPERATIONS_SEED_KEY, UML_SLOTS_SEED, UML_SLOTS_SEED_KEY, UML_UNLABELLED_KINDS, umlSeedKey, } from './keywords.js';
15
- import { umlNameDecoration, umlNodeProps, umlTextProps } from './presets.js';
16
- import { UML_ROLE, umlDiagramRoleKey, umlRoleKey, umlSubjectRoleKey, } from './roles.js';
11
+ import { UML_NAME_SEED, UML_OPERATIONS_SEED, UML_OPERATIONS_SEED_KEY, UML_UNLABELLED_KINDS, umlBodySeed, umlSeedKey, } from './keywords.js';
12
+ import { umlConnectorProps, UML_FRAME_BOX, } from './presets.js';
13
+ import { UML_FRAME_SEED, umlFrameElement, umlNodeElements } from './toolbox.js';
17
14
  /**
18
15
  * Standalone creation/activation actions for the UML toolbox — the same shape
19
16
  * BPMN's and C4's `actions.ts` have, and for the same reason: the menu is a pure
@@ -27,13 +24,64 @@ function finish(gfx, id) {
27
24
  gfx.selection.set({ elements: [id], editing: false });
28
25
  // Keep the palette open (native sub-menu behaviour).
29
26
  }
30
- /** One compartment of a component, as a canvas TEXT element. */
31
- function addTier(surface, index, role, text, box, options) {
32
- return surface.addElement({
33
- ...umlTextProps(role, box, options),
34
- text,
35
- index,
36
- });
27
+ /**
28
+ * Write a toolbox builder's elements (`toolbox.ts`, `docs/adr/0045`) the way
29
+ * the toolbar does, and say what the gesture produced.
30
+ *
31
+ * What only an editor has stays here: the layer index, minted per element in
32
+ * painting order — a stability statement, since two elements sharing an index
33
+ * sort by nanoid — and the surface's own ids, which a group's `children` is
34
+ * rewritten to name. The group is LAST in a builder's list, so every member
35
+ * already has its id when the group is written.
36
+ *
37
+ * Returns the group's id when there is one, else the first element's: what a
38
+ * gesture selects.
39
+ */
40
+ function place(gfx, elements) {
41
+ const surface = gfx.surface;
42
+ if (!surface)
43
+ return undefined;
44
+ const minted = new Map();
45
+ let first;
46
+ let group;
47
+ for (const { id: local, ...props } of elements) {
48
+ if (props.type === 'group') {
49
+ // A plain record is a legal `children`: the group's own `propsToY` takes
50
+ // the KEYS and forces every value to `true`.
51
+ props.children = Object.fromEntries(Object.keys(props.children).map(child => [
52
+ minted.get(child) ?? child,
53
+ true,
54
+ ]));
55
+ }
56
+ const id = surface.addElement({
57
+ ...props,
58
+ index: gfx.layer.generateIndex(),
59
+ });
60
+ minted.set(local, id);
61
+ first ??= id;
62
+ if (props.type === 'group')
63
+ group = id;
64
+ }
65
+ return group ?? first;
66
+ }
67
+ /** A frame centred on the viewport, written and selected. */
68
+ function createUmlFrame(std, type, name, options) {
69
+ const gfx = gfxOf(std);
70
+ const surface = gfx.surface;
71
+ if (!surface)
72
+ return;
73
+ const { w, h } = UML_FRAME_BOX[type];
74
+ const { centerX: cx, centerY: cy } = gfx.viewport;
75
+ const { id: _local, ...props } = umlFrameElement(type, { x: cx - w / 2, y: cy - h / 2 }, name, type, options);
76
+ finish(gfx, surface.addElement(props));
77
+ }
78
+ /**
79
+ * A frame's default name, resolved through the translation seam at placement
80
+ * and never again: content from the moment it lands (#183).
81
+ */
82
+ function frameName(std, type) {
83
+ const [key, english] = UML_FRAME_SEED[type];
84
+ return translateKey(std, key, english);
37
85
  }
38
86
  /* ── The sheet and the frame on it ─────────────────────────────────────── */
39
87
  /**
@@ -54,22 +102,10 @@ function addTier(surface, index, role, text, box, options) {
54
102
  * once and never re-resolved.
55
103
  */
56
104
  export function createUmlDiagram(std, kind = 'class') {
57
- const gfx = gfxOf(std);
58
- const surface = gfx.surface;
59
- if (!surface)
60
- return;
61
- const { w, h } = UML_DIAGRAM_BOX;
62
- const { centerX: cx, centerY: cy } = gfx.viewport;
63
- const id = surface.addElement({
64
- type: 'umlDiagram',
65
- // The FRAME the elements are drawn on, and a role of its own: a rule written
66
- // on the artefacts must never fall on the sheet holding them.
67
- role: UML_ROLE.diagram,
68
- name: translateKey(std, umlDiagramRoleKey, 'Diagram'),
69
- kind,
70
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
71
- });
72
- finish(gfx, id);
105
+ // The FRAME the elements are drawn on, with a role of its own
106
+ // (`umlFrameProps`): a rule written on the artefacts must never fall on the
107
+ // sheet holding them.
108
+ createUmlFrame(std, 'umlDiagram', frameName(std, 'umlDiagram'), { kind });
73
109
  }
74
110
  /**
75
111
  * Create a use case SUBJECT centred on the viewport (§18.1.4) — the rectangle
@@ -79,30 +115,8 @@ export function createUmlDiagram(std, kind = 'class') {
79
115
  * ONE name, so there is nothing for a discriminant to discriminate.
80
116
  */
81
117
  export function createUmlSubject(std) {
82
- const gfx = gfxOf(std);
83
- const surface = gfx.surface;
84
- if (!surface)
85
- return;
86
- const { w, h } = UML_SUBJECT_BOX;
87
- const { centerX: cx, centerY: cy } = gfx.viewport;
88
- const id = surface.addElement({
89
- type: 'umlSubject',
90
- role: UML_ROLE.subject,
91
- name: translateKey(std, umlSubjectRoleKey, 'Subject'),
92
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
93
- });
94
- finish(gfx, id);
118
+ createUmlFrame(std, 'umlSubject', frameName(std, 'umlSubject'));
95
119
  }
96
- /**
97
- * The two behaviour BACKGROUNDS, named through the same seam the subject is.
98
- *
99
- * `umlRoleKey` rather than a `umlPartitionRoleKey` export, and it is the same
100
- * string either way (`roles.ts` derives both from the role's local name): a
101
- * default name is asked for ONCE, at creation, and it is the author's content
102
- * from that moment on — so the call site needs the key and nothing else.
103
- */
104
- const umlPartitionRoleKey = umlRoleKey('partition');
105
- const umlRegionRoleKey = umlRoleKey('region');
106
120
  /**
107
121
  * Create an activity PARTITION — a swimlane (§15.6.4), the band an activity
108
122
  * diagram draws round the steps one participant is responsible for.
@@ -127,19 +141,7 @@ const umlRegionRoleKey = umlRoleKey('region');
127
141
  * turns a column into a row.
128
142
  */
129
143
  export function createUmlPartition(std) {
130
- const gfx = gfxOf(std);
131
- const surface = gfx.surface;
132
- if (!surface)
133
- return;
134
- const { w, h } = UML_PARTITION_BOX;
135
- const { centerX: cx, centerY: cy } = gfx.viewport;
136
- const id = surface.addElement({
137
- type: 'umlPartition',
138
- role: UML_ROLE.partition,
139
- name: translateKey(std, umlPartitionRoleKey, 'Partition'),
140
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
141
- });
142
- finish(gfx, id);
144
+ createUmlFrame(std, 'umlPartition', frameName(std, 'umlPartition'));
143
145
  }
144
146
  /**
145
147
  * Create a REGION — the rounded, named rectangle a composite state is drawn as
@@ -155,19 +157,7 @@ export function createUmlPartition(std) {
155
157
  * into.
156
158
  */
157
159
  export function createUmlRegion(std) {
158
- const gfx = gfxOf(std);
159
- const surface = gfx.surface;
160
- if (!surface)
161
- return;
162
- const { w, h } = UML_REGION_BOX;
163
- const { centerX: cx, centerY: cy } = gfx.viewport;
164
- const id = surface.addElement({
165
- type: 'umlRegion',
166
- role: UML_ROLE.region,
167
- name: translateKey(std, umlRegionRoleKey, 'Region'),
168
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
169
- });
170
- finish(gfx, id);
160
+ createUmlFrame(std, 'umlRegion', frameName(std, 'umlRegion'));
171
161
  }
172
162
  /* ── Phase 3: the combined fragment (§17.6.4) and the interaction use ───── */
173
163
  /**
@@ -209,19 +199,7 @@ export function createUmlRegion(std) {
209
199
  * putting a branch on the sheet nobody asked for.
210
200
  */
211
201
  export function createUmlFragment(std, operator = 'alt') {
212
- const gfx = gfxOf(std);
213
- const surface = gfx.surface;
214
- if (!surface)
215
- return;
216
- const { w, h } = UML_FRAGMENT_BOX;
217
- const { centerX: cx, centerY: cy } = gfx.viewport;
218
- const id = surface.addElement({
219
- type: 'umlFragment',
220
- role: UML_ROLE.fragment,
221
- operator,
222
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
223
- });
224
- finish(gfx, id);
202
+ createUmlFrame(std, 'umlFragment', undefined, { operator });
225
203
  }
226
204
  /**
227
205
  * Create an INTERACTION USE — §17.7.4's `ref` frame, the box that says "and
@@ -245,42 +223,37 @@ export function createUmlInteractionUse(std) {
245
223
  createUmlFragment(std, 'ref');
246
224
  }
247
225
  /**
248
- * The glyph kinds whose one tier is a `uml:name` rather than a `uml:label`.
249
- *
250
- * The line between the two is drawn by `roles.ts` and it is not about having a
251
- * rectangle: `uml:name` is the tier a KEYWORD may be written on and the grammar
252
- * reads back, `uml:label` is a name and nothing else. So:
253
- *
254
- * - a **package** name is a namespace, parseable and qualified (§12.2.4), and a
255
- * **note**'s text is what the exporter writes as the comment's body;
256
- * - the three **deployment cubes** are named INSIDE their front face, over a
257
- * keyword line — `«device»`, `«executionEnvironment»` (§19.4.4) — which is
258
- * exactly what a name compartment is for. It is also load-bearing rather than
259
- * tidy: the morph's `afterMorph` rewrites `uml:name` and only that, so a cube
260
- * labelled instead of named would morph into a device that never said so.
261
- *
262
- * Everything else here carries a LABEL: a port's word sits beside a 16-unit
263
- * square and the two interface marks' beside a ball and a socket, none of them
264
- * inside anything, and none of them ever carrying a keyword.
265
- */
266
- const NAMED_GLYPHS = new Set([
267
- 'package',
268
- 'note',
269
- 'node',
270
- 'device',
271
- 'execution-environment',
272
- ]);
273
- /**
274
- * Where the viewport centre puts a node of this kind.
275
- *
276
- * Shared by the two creation functions so a class and a use case are centred by
277
- * the same arithmetic — the geometry is the only thing the two gestures have in
278
- * common, and it is worth not writing twice.
226
+ * Place an artefact centred on the viewport and select what the gesture
227
+ * produced: the GROUP when the kind has words, the SHAPE when it is a mark.
228
+ *
229
+ * The elements come from the ONE assembly a host with no editor and every
230
+ * importer also call (`umlNodeElements`, `toolbox.ts`, over
231
+ * `umlArtefactElements`, `presets.ts`, `docs/adr/0045`). What only an editor
232
+ * has stays here: the viewport centre, and every seed resolved through the
233
+ * translation SEAM — the words a gesture writes are persisted, so a host that
234
+ * has a catalogue must be asked before they land: a package dropped in a
235
+ * French editor is never called "Package" afterwards (R30). Resolved once,
236
+ * here; content from that moment on.
279
237
  */
280
- function centredBox(gfx, kind) {
238
+ function createUmlArtefact(std, kind) {
239
+ const gfx = gfxOf(std);
240
+ if (!gfx.surface)
241
+ return;
281
242
  const { w, h } = UML_NODE_BOX[kind];
282
243
  const { centerX: cx, centerY: cy } = gfx.viewport;
283
- return { x: cx - w / 2, y: cy - h / 2, w, h };
244
+ const [bodyKey, bodyText] = umlBodySeed(kind);
245
+ const elements = umlNodeElements(kind, { x: cx - w / 2, y: cy - h / 2 },
246
+ // A mark asks for no key: there is no word to translate.
247
+ UML_UNLABELLED_KINDS.has(kind)
248
+ ? {}
249
+ : {
250
+ name: translateKey(std, umlSeedKey(kind), UML_NAME_SEED[kind]),
251
+ attributes: bodyKey ? translateKey(std, bodyKey, bodyText) : bodyText,
252
+ operations: translateKey(std, UML_OPERATIONS_SEED_KEY, UML_OPERATIONS_SEED),
253
+ });
254
+ const id = place(gfx, elements);
255
+ if (id)
256
+ finish(gfx, id);
284
257
  }
285
258
  /**
286
259
  * Create one of the SINGLE-LABEL artefacts — a package, a note, an actor, a use
@@ -299,14 +272,14 @@ function centredBox(gfx, kind) {
299
272
  *
300
273
  * ## `uml:name` for five of them, `uml:label` for the rest
301
274
  *
302
- * See {@link NAMED_GLYPHS}: a package, a note and the three cubes carry a NAME
303
- * the grammar parses a keyword off; a port, a ball and a socket carry a LABEL.
304
- * Keeping them apart is what lets the exporter read a tier without first asking
305
- * what shape it is under.
275
+ * See `NAMED_GLYPH_KINDS` (`presets.ts`): a package, a note and the three
276
+ * cubes carry a NAME the grammar parses a keyword off; a port, a ball and a
277
+ * socket carry a LABEL. Keeping them apart is what lets the exporter read a
278
+ * tier without first asking what shape it is under.
306
279
  *
307
280
  * ## The label is where the notation puts it, and that is not this file's call
308
281
  *
309
- * The box comes out of {@link umlCompartmentBoxes}, which is where every
282
+ * The box comes out of `umlCompartmentBoxes`, which is where every
310
283
  * "beside the glyph" and "inside the front face" decision is written down: a
311
284
  * port's label sits next to the square rather than on it (the square is 16
312
285
  * units, and words do not fit in it), an interface mark's sits beside the ball
@@ -342,83 +315,8 @@ function centredBox(gfx, kind) {
342
315
  * rules off and the exporter reads.
343
316
  */
344
317
  export function createUmlNode(std, kind) {
345
- const gfx = gfxOf(std);
346
- const surface = gfx.surface;
347
- if (!surface)
348
- return;
349
- const { x, y, w, h } = centredBox(gfx, kind);
350
- const shapeId = surface.addElement({
351
- // Every prop the kind is worth, from the ONE table the morph also reads
352
- // (`presets.ts`). NO `text`: the words are the child below, and the shape is
353
- // a body and nothing else.
354
- ...umlNodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
355
- index: gfx.layer.generateIndex(),
356
- });
357
- // The marks the notation draws without a word on them: the shape IS the
358
- // artefact, so there is nothing to group it with and nothing to seed.
359
- if (UML_UNLABELLED_KINDS.has(kind)) {
360
- finish(gfx, shapeId);
361
- return;
362
- }
363
- const { name: box } = umlCompartmentBoxes(kind, x, y, w, h);
364
- const isLabel = !NAMED_GLYPHS.has(kind);
365
- const textId = addTier(surface, gfx.layer.generateIndex(),
366
- // WHICH label role is the kind's own answer, not this file's: a lifeline's
367
- // head carries `uml:lifeline-ident` and every other single word carries
368
- // `uml:label` (`component.ts`, {@link umlLabelRoleOf}).
369
- isLabel ? umlLabelRoleOf(kind) : UML_ROLE.name,
370
- // Through the SEAM, like the frames above: the words a gesture writes are
371
- // persisted, so a host that has a catalogue must be asked before they land
372
- // — a package dropped in a French editor is never called "Package"
373
- // afterwards (R30). Resolved once, here; content from that moment on.
374
- translateKey(std, umlSeedKey(kind), UML_NAME_SEED[kind]), box, {
375
- fontSize: UML_NAME_FONT_SIZE,
376
- align: kind === 'note' ? TextAlign.Left : TextAlign.Center,
377
- });
378
- // The group LAST, so it is written after everything it holds: the elements are
379
- // created in painting order — the shape first and the words above it — and the
380
- // group's own `xywh` is derived from its children.
381
- const groupId = surface.addElement({
382
- type: 'group',
383
- index: gfx.layer.generateIndex(),
384
- // A plain record is a legal `children`: the group's own `propsToY` takes the
385
- // KEYS and forces every value to `true`.
386
- children: { [shapeId]: true, [textId]: true },
387
- // No title, deliberately. The group renderer paints one only while the
388
- // artefact is selected, and a package announcing itself as "Group 3" above
389
- // its own name is a label nobody wrote.
390
- });
391
- finish(gfx, groupId);
318
+ createUmlArtefact(std, kind);
392
319
  }
393
- /**
394
- * What the SECOND compartment says on a fresh artefact, where it is not the
395
- * §9.5.4 attribute line every classifier gets.
396
- *
397
- * Two exceptions and a default, rather than three entries, because the default
398
- * is the rule: `+ attribute : Type` is what a divided rectangle's body tier
399
- * holds unless the kind's own clause says otherwise.
400
- *
401
- * - an **object**'s tier holds SLOTS (§9.8.4) — `attribute = value`, an
402
- * instance giving its classifier's features values rather than redeclaring
403
- * them;
404
- * - a **state**'s holds INTERNAL ACTIVITIES (§14.2.4.4) and is seeded EMPTY.
405
- * That is the one place in this pack where a blank tier is the right
406
- * stencil, and it is the exporter that decides it: `model.ts` reads
407
- * `entry / …`, `do / …` and `exit / …` off these lines and treats a label
408
- * with nothing after it as no behaviour at all, so a seeded `entry / ` would
409
- * be a prompt that reads as content and exports as nothing. §14.2.4's own
410
- * figures draw exactly this — a named state with a ruled-off compartment
411
- * waiting to be filled — and the author types the first line the grammar
412
- * then reads back.
413
- *
414
- * Each is paired with the key it is the English default of, and the state's is
415
- * `null` for the reason an unlabelled kind contributes none: there is no word
416
- * to translate.
417
- */
418
- const BODY_SEED = {
419
- object: [UML_SLOTS_SEED_KEY, UML_SLOTS_SEED],
420
- state: [null, ''],
421
- };
422
320
  /**
423
321
  * Create one of the COMPARTMENTED artefacts — a class, an interface, an
424
322
  * enumeration, an object, and since phase 2 a component, an artifact or a
@@ -426,12 +324,13 @@ const BODY_SEED = {
426
324
  *
427
325
  * ## How many tiers is the LAYOUT's answer, not this file's
428
326
  *
429
- * The walk below writes a tier for each box `umlCompartmentBoxes` returns and
430
- * for no other, which is what lets two kinds be added to the notation without a
431
- * line changing here: a component and an artifact arrive with the name tier and
432
- * the one body tier §11.6.4 and §19.3.4 draw them with, because that is what the
433
- * layout hands over. The alternative — a table of tier counts in this file —
434
- * would be a second statement of the geometry, and the first one to drift.
327
+ * The assembly (`umlArtefactElements`) writes a tier for each box
328
+ * `umlCompartmentBoxes` returns and for no other, which is what lets two kinds
329
+ * be added to the notation without a line changing here: a component and an
330
+ * artifact arrive with the name tier and the one body tier §11.6.4 and §19.3.4
331
+ * draw them with, because that is what the layout hands over. The alternative
332
+ * — a table of tier counts in this file — would be a second statement of the
333
+ * geometry, and the first one to drift.
435
334
  *
436
335
  * ## Four or five elements, and the object is the four
437
336
  *
@@ -460,41 +359,7 @@ const BODY_SEED = {
460
359
  * visual cue UML gives for visibility (§9.5.4).
461
360
  */
462
361
  export function createUmlClassifier(std, kind) {
463
- const gfx = gfxOf(std);
464
- const surface = gfx.surface;
465
- if (!surface)
466
- return;
467
- const { x, y, w, h } = centredBox(gfx, kind);
468
- const shapeId = surface.addElement({
469
- ...umlNodeProps(kind, { xywh: new Bound(x, y, w, h).serialize() }),
470
- index: gfx.layer.generateIndex(),
471
- });
472
- const boxes = umlCompartmentBoxes(kind, x, y, w, h);
473
- const children = { [shapeId]: true };
474
- children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.name,
475
- // Through the seam, exactly as {@link createUmlNode} writes its one tier.
476
- translateKey(std, umlSeedKey(kind), UML_NAME_SEED[kind]), boxes.name, {
477
- fontSize: UML_NAME_FONT_SIZE,
478
- align: TextAlign.Center,
479
- bold: true,
480
- decoration: umlNameDecoration(kind),
481
- })] = true;
482
- if (boxes.attributes) {
483
- const [bodyKey, bodyText] = BODY_SEED[kind] ?? [
484
- UML_ATTRIBUTES_SEED_KEY,
485
- UML_ATTRIBUTES_SEED,
486
- ];
487
- children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.attributes, bodyKey ? translateKey(std, bodyKey, bodyText) : bodyText, boxes.attributes, { fontSize: UML_BODY_FONT_SIZE, align: TextAlign.Left })] = true;
488
- }
489
- if (boxes.operations) {
490
- children[addTier(surface, gfx.layer.generateIndex(), UML_ROLE.operations, translateKey(std, UML_OPERATIONS_SEED_KEY, UML_OPERATIONS_SEED), boxes.operations, { fontSize: UML_BODY_FONT_SIZE, align: TextAlign.Left })] = true;
491
- }
492
- const groupId = surface.addElement({
493
- type: 'group',
494
- index: gfx.layer.generateIndex(),
495
- children,
496
- });
497
- finish(gfx, groupId);
362
+ createUmlArtefact(std, kind);
498
363
  }
499
364
  /**
500
365
  * Arm the native connector tool, pre-styled for one UML relationship.
@@ -504,7 +369,7 @@ export function createUmlClassifier(std, kind) {
504
369
  * Because UML has twelve of them and they differ on three props out of a table
505
370
  * that is otherwise identical — the same mode, the same ink, the same weight. A
506
371
  * dozen near-identical functions would be a dozen places for a stroke width to
507
- * drift, and {@link UML_EDGE_STYLE} is the notation itself, which is the thing
372
+ * drift, and `UML_EDGE_STYLE` is the notation itself, which is the thing
508
373
  * worth reading. It is also what made phase 2 free: `deploy`, `manifest` and
509
374
  * `communication-path` arm through this function without a line changing in it.
510
375
  *
@@ -522,15 +387,10 @@ export function createUmlClassifier(std, kind) {
522
387
  * keep the user's own style (#144 M1).
523
388
  */
524
389
  export function activateUmlEdge(std, role) {
525
- gfxOf(std).tool.setTool(ConnectorTool, {
526
- mode: ConnectorMode.Straight,
527
- role: UML_ROLE[role],
528
- style: {
529
- stroke: UML_INK,
530
- strokeWidth: UML_EDGE_WIDTH,
531
- ...UML_EDGE_STYLE[role],
532
- },
533
- });
390
+ // The line a host's connector and an imported one are drawn in too
391
+ // (`umlConnectorProps`, `docs/adr/0045`): one table, read three ways.
392
+ const { type: _type, role: roleId, mode, ...style } = umlConnectorProps(role);
393
+ gfxOf(std).tool.setTool(ConnectorTool, { mode, role: roleId, style });
534
394
  // Keep the palette open (native sub-menu behaviour).
535
395
  }
536
396
  /* ── The legend ────────────────────────────────────────────────────────── */
@@ -309,10 +309,10 @@ export declare const UML_HEAD_LABEL_KINDS: ReadonlySet<UmlNodeKind>;
309
309
  * single role could only ever have asked them the same question (`roles.ts`).
310
310
  *
311
311
  * It does NOT answer for the compartmented kinds or for the five glyphs named
312
- * rather than labelled (`actions.ts`, `NAMED_GLYPHS`): those carry `uml:name`,
313
- * which is a different statement — a tier a keyword may be written over — and a
314
- * helper that folded all three answers together would be a second place to
315
- * decide what the grammar reads.
312
+ * rather than labelled (`presets.ts`, `NAMED_GLYPH_KINDS`): those carry
313
+ * `uml:name`, which is a different statement — a tier a keyword may be written
314
+ * over — and a helper that folded all three answers together would be a second
315
+ * place to decide what the grammar reads.
316
316
  */
317
317
  export declare function umlLabelRoleOf(kind: UmlNodeKind): UmlRoleId;
318
318
  /**
package/dist/component.js CHANGED
@@ -330,10 +330,10 @@ export const UML_HEAD_LABEL_KINDS = new Set(['lifeline']);
330
330
  * single role could only ever have asked them the same question (`roles.ts`).
331
331
  *
332
332
  * It does NOT answer for the compartmented kinds or for the five glyphs named
333
- * rather than labelled (`actions.ts`, `NAMED_GLYPHS`): those carry `uml:name`,
334
- * which is a different statement — a tier a keyword may be written over — and a
335
- * helper that folded all three answers together would be a second place to
336
- * decide what the grammar reads.
333
+ * rather than labelled (`presets.ts`, `NAMED_GLYPH_KINDS`): those carry
334
+ * `uml:name`, which is a different statement — a tier a keyword may be written
335
+ * over — and a helper that folded all three answers together would be a second
336
+ * place to decide what the grammar reads.
337
337
  */
338
338
  export function umlLabelRoleOf(kind) {
339
339
  return UML_HEAD_LABEL_KINDS.has(kind)
package/dist/import.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { InterchangeNote, SerializedElementProps } from '@formicoidea/labre-core/blocks/surface';
2
- import { type UmlBox } from './component.js';
2
+ import type { UmlBox } from './component.js';
3
3
  import { type UmlModel, type UmlNodeBase, type UmlRelation, type UmlRelationKind } from './model.js';
4
4
  /**
5
5
  * The **one materializer** every UML importer ends on — a model in, the
@@ -23,11 +23,12 @@ import { type UmlModel, type UmlNodeBase, type UmlRelation, type UmlRelationKind
23
23
  *
24
24
  * ## Never a size, never a colour, never a seed
25
25
  *
26
- * Every prop below comes out of {@link umlNodeProps}, {@link umlTextProps} and
27
- * {@link umlCompartmentBoxes} — the very three functions the creation site calls
28
- * (R17). Not one measurement is restated here, so an imported class and a drawn
29
- * one are the same element down to the stroke width, and a restyle that moves
30
- * the pack moves the import with it.
26
+ * Every artefact below comes out of {@link umlArtefactElements}, every frame
27
+ * out of {@link umlFrameProps} and every line out of {@link umlConnectorProps}
28
+ * — the very assembly the toolbar and the toolbox call (R17,
29
+ * `docs/adr/0045`). Not one measurement is restated here, so an imported class
30
+ * and a drawn one are the same element down to the stroke width, and a restyle
31
+ * that moves the pack moves the import with it.
31
32
  *
32
33
  * ## The two names an element carries, and they are not the same name
33
34
  *