@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/import.js CHANGED
@@ -1,13 +1,10 @@
1
- import { connectorEndLabelBox, ConnectorMode, TextAlign, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
1
+ import { connectorEndLabelBox, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
4
- 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';
5
- import { UML_EDGE_STYLE } from './edge-styles.js';
3
+ import { UML_DIAGRAM_BOX, UML_FRAGMENT_BOX, UML_NODE_BOX } from './consts.js';
6
4
  import { formatActivityEdgeLabel, formatEndLabel, formatTransitionLabel, } from './grammar.js';
7
- import { guillemets, UML_UNLABELLED_KINDS } from './keywords.js';
5
+ import { guillemets } from './keywords.js';
8
6
  import { umlGuardText, } from './model.js';
9
- import { umlNameDecoration, umlNodeProps, umlTextProps } from './presets.js';
10
- import { UML_ROLE } from './roles.js';
7
+ import { UML_FRAME_BOX, umlArtefactElements, umlConnectorProps, umlFrameProps, } from './presets.js';
11
8
  /**
12
9
  * The **one materializer** every UML importer ends on — a model in, the
13
10
  * serialized props of a board out (`docs/adr/0012`, P3).
@@ -30,11 +27,12 @@ import { UML_ROLE } from './roles.js';
30
27
  *
31
28
  * ## Never a size, never a colour, never a seed
32
29
  *
33
- * Every prop below comes out of {@link umlNodeProps}, {@link umlTextProps} and
34
- * {@link umlCompartmentBoxes} — the very three functions the creation site calls
35
- * (R17). Not one measurement is restated here, so an imported class and a drawn
36
- * one are the same element down to the stroke width, and a restyle that moves
37
- * the pack moves the import with it.
30
+ * Every artefact below comes out of {@link umlArtefactElements}, every frame
31
+ * out of {@link umlFrameProps} and every line out of {@link umlConnectorProps}
32
+ * — the very assembly the toolbar and the toolbox call (R17,
33
+ * `docs/adr/0045`). Not one measurement is restated here, so an imported class
34
+ * and a drawn one are the same element down to the stroke width, and a restyle
35
+ * that moves the pack moves the import with it.
38
36
  *
39
37
  * ## The two names an element carries, and they are not the same name
40
38
  *
@@ -362,38 +360,6 @@ function stateBodyText(state) {
362
360
  ...state.lines,
363
361
  ].join('\n');
364
362
  }
365
- /* ── What a draft is ──────────────────────────────────────────────────── */
366
- /**
367
- * The glyph kinds whose one tier is a `uml:name` rather than a `uml:label`.
368
- *
369
- * A transcription of `actions.ts`'s own `NAMED_GLYPHS`, kept here rather than
370
- * imported because that module reaches for the editor (`BlockStdScope`, the gfx
371
- * controller, the surface) and a reader must not: P3 is what lets labre-mcp call
372
- * this file with no DI in sight. `import.unit.spec.ts` re-runs the creation
373
- * commands against a recording surface and compares the tier roles, so the two
374
- * statements cannot drift in silence.
375
- */
376
- const NAMED_GLYPH_KINDS = new Set([
377
- 'package',
378
- 'note',
379
- 'node',
380
- 'device',
381
- 'execution-environment',
382
- ]);
383
- /** The four background frames, and the box each is created at. */
384
- const FRAME_BOX = {
385
- umlSubject: UML_SUBJECT_BOX,
386
- umlPartition: UML_PARTITION_BOX,
387
- umlRegion: UML_REGION_BOX,
388
- umlFragment: UML_FRAGMENT_BOX,
389
- };
390
- /** The role each background frame carries — one table, read once below. */
391
- const FRAME_ROLE = {
392
- umlSubject: UML_ROLE.subject,
393
- umlPartition: UML_ROLE.partition,
394
- umlRegion: UML_ROLE.region,
395
- umlFragment: UML_ROLE.fragment,
396
- };
397
363
  /** Every artefact one model draws, in the order a reader should meet them. */
398
364
  function draftsOf(model) {
399
365
  const drafts = [];
@@ -413,7 +379,7 @@ function draftsOf(model) {
413
379
  element,
414
380
  name: record.name,
415
381
  ...(orientation ? { orientation } : {}),
416
- size: FRAME_BOX[element],
382
+ size: UML_FRAME_BOX[element],
417
383
  ...(record.bounds ? { bounds: record.bounds } : {}),
418
384
  });
419
385
  };
@@ -591,7 +557,7 @@ function draftsOf(model) {
591
557
  })),
592
558
  }
593
559
  : {}),
594
- size: FRAME_BOX.umlFragment,
560
+ size: UML_FRAME_BOX.umlFragment,
595
561
  ...(fragment.bounds ? { bounds: fragment.bounds } : {}),
596
562
  });
597
563
  }
@@ -940,12 +906,8 @@ export function umlElementsFromModel(models, options) {
940
906
  const dx = frame.x + UML_IMPORT_MARGIN - left;
941
907
  const dy = frame.y + UML_FRAME_BAND_HEIGHT + UML_IMPORT_CONTAINER_INSET - top;
942
908
  elements.push({
943
- type: 'umlDiagram',
909
+ ...umlFrameProps('umlDiagram', { xywh: new Bound(frame.x, frame.y, frame.w, frame.h).serialize() }, { name: model.diagram.name, kind: model.diagram.kind }),
944
910
  id: mint(),
945
- role: UML_ROLE.diagram,
946
- name: model.diagram.name,
947
- kind: model.diagram.kind,
948
- xywh: new Bound(frame.x, frame.y, frame.w, frame.h).serialize(),
949
911
  ...payload(formatId, model.diagram.id),
950
912
  });
951
913
  /** The provisional name a relation's end resolves to. */
@@ -968,99 +930,44 @@ export function umlElementsFromModel(models, options) {
968
930
  boxOf.set(draft.sourceId, placed);
969
931
  }
970
932
  elements.push({
971
- type: draft.element,
933
+ ...umlFrameProps(draft.element, { xywh }, {
934
+ name: draft.name ?? '',
935
+ // The model defaults a band to vertical, and a creation that
936
+ // restated the default would be a second place for it to be
937
+ // changed.
938
+ ...(draft.orientation === 'horizontal'
939
+ ? { orientation: 'horizontal' }
940
+ : {}),
941
+ // §17.6.4's operator and its operand bands. `alt` is the model's
942
+ // own default and is written all the same: unlike an
943
+ // orientation, the operator IS the fragment, and a document that
944
+ // left it out would be one whose pentagon depends on a default
945
+ // nobody can see.
946
+ ...(draft.operator ? { operator: draft.operator } : {}),
947
+ ...(draft.operands ? { operands: draft.operands } : {}),
948
+ }),
972
949
  id,
973
- role: FRAME_ROLE[draft.element],
974
- name: draft.name ?? '',
975
- // The model defaults a band to vertical, and a creation that restated
976
- // the default would be a second place for it to be changed.
977
- ...(draft.orientation === 'horizontal'
978
- ? { orientation: 'horizontal' }
979
- : {}),
980
- // §17.6.4's operator and its operand bands. `alt` is the model's own
981
- // default and is written all the same: unlike an orientation, the
982
- // operator IS the fragment, and a document that left it out would be
983
- // one whose pentagon depends on a default nobody can see.
984
- ...(draft.operator ? { operator: draft.operator } : {}),
985
- ...(draft.operands ? { operands: draft.operands } : {}),
986
- xywh,
987
950
  ...payload(formatId, draft.sourceId),
988
951
  });
989
952
  continue;
990
953
  }
991
- const kind = draft.kind;
992
- const shapeId = mint();
954
+ // The artefact the toolbar draws, through the ONE assembly the toolbar
955
+ // and a host with no editor call too (`umlArtefactElements`,
956
+ // `docs/adr/0045`): the shape, the tiers the kind is worth, the group
957
+ // last — with the FILE's words in the tiers, never a seed, and this
958
+ // reader's own names minted in painting order.
959
+ const [shape, ...rest] = umlArtefactElements(draft.kind, placed, draft.tiers ?? { name: '' }, mint);
960
+ if (!shape)
961
+ continue;
993
962
  // An interface MARK answers for nothing: §10.4.4 draws it as the notation
994
963
  // for a relationship rather than as an element, which is exactly why
995
964
  // `model.ts` keeps it out of `artefactOf`. A connector must never resolve
996
965
  // to a lollipop.
997
966
  if (draft.sourceId && draft.connectable !== false) {
998
- shapeOf.set(draft.sourceId, shapeId);
967
+ shapeOf.set(draft.sourceId, shape.id);
999
968
  boxOf.set(draft.sourceId, placed);
1000
969
  }
1001
- elements.push({
1002
- ...umlNodeProps(kind, { xywh }),
1003
- id: shapeId,
1004
- ...payload(formatId, draft.sourceId),
1005
- });
1006
- const children = [shapeId];
1007
- const boxes = umlCompartmentBoxes(kind, placed.x, placed.y, placed.w, placed.h);
1008
- const tiers = draft.tiers ?? { name: '' };
1009
- // A mark the notation draws no words on is the SHAPE and nothing else —
1010
- // and therefore no group either, because a group of one element is a
1011
- // wrapper a user would have to descend through (`actions.ts`).
1012
- if (!UML_UNLABELLED_KINDS.has(kind)) {
1013
- const compartmented = boxes.attributes !== undefined;
1014
- const tierId = mint();
1015
- children.push(tierId);
1016
- elements.push({
1017
- ...umlTextProps(compartmented || NAMED_GLYPH_KINDS.has(kind)
1018
- ? UML_ROLE.name
1019
- : // A lifeline's head is `uml:lifeline-ident`, every other single
1020
- // word is `uml:label` — the creation path's own table, read
1021
- // rather than restated (`component.ts`).
1022
- umlLabelRoleOf(kind), boxes.name, {
1023
- fontSize: UML_NAME_FONT_SIZE,
1024
- // A note holds a sentence rather than a title (Annex A), and a
1025
- // centred paragraph is the one thing on a UML diagram that reads
1026
- // as a poem.
1027
- align: kind === 'note' ? TextAlign.Left : TextAlign.Center,
1028
- // The classifier's name is the heading of a divided box; a
1029
- // glyph's one word is not.
1030
- ...(compartmented ? { bold: true } : {}),
1031
- // The toolbox's own preset: an instance's name is underlined.
1032
- decoration: umlNameDecoration(kind),
1033
- }),
1034
- id: tierId,
1035
- text: tiers.name,
1036
- });
1037
- for (const [role, box, words] of [
1038
- [UML_ROLE.attributes, boxes.attributes, tiers.attributes],
1039
- [UML_ROLE.operations, boxes.operations, tiers.operations],
1040
- ]) {
1041
- if (!box)
1042
- continue;
1043
- const bodyId = mint();
1044
- children.push(bodyId);
1045
- elements.push({
1046
- ...umlTextProps(role, box, {
1047
- fontSize: UML_BODY_FONT_SIZE,
1048
- align: TextAlign.Left,
1049
- }),
1050
- id: bodyId,
1051
- text: words ?? '',
1052
- });
1053
- }
1054
- }
1055
- if (children.length > 1) {
1056
- // LAST, so it is written after everything it holds — and so the caller's
1057
- // map already answers for every child by the time this row is created.
1058
- elements.push({
1059
- type: 'group',
1060
- id: mint(),
1061
- children: Object.fromEntries(children.map(child => [child, true])),
1062
- });
1063
- }
970
+ elements.push({ ...shape, ...payload(formatId, draft.sourceId) }, ...rest);
1064
971
  }
1065
972
  // `relations` AND the behaviour edges that live on the Activity and the
1066
973
  // StateMachine — see {@link umlDrawnEdges} for why there are two places to
@@ -1085,16 +992,10 @@ export function umlElementsFromModel(models, options) {
1085
992
  ? undefined
1086
993
  : messageAnchors(edge.y + dy, boxOf.get(edge.sourceId), boxOf.get(edge.targetId));
1087
994
  elements.push({
1088
- type: 'connector',
995
+ // The line the edge tool draws (`umlConnectorProps`): STRAIGHT, the
996
+ // pack's ink, the relationship's own ends.
997
+ ...umlConnectorProps(edge.kind),
1089
998
  id: mint(),
1090
- role: UML_ROLE[edge.kind],
1091
- // STRAIGHT, always: UML's lines state a relationship between two
1092
- // classifiers, and the elbows of an orthogonal router would read as a
1093
- // route through the diagram that nobody drew (`actions.ts`).
1094
- mode: ConnectorMode.Straight,
1095
- stroke: UML_INK,
1096
- strokeWidth: UML_EDGE_WIDTH,
1097
- ...UML_EDGE_STYLE[edge.kind],
1098
999
  source: { id: source, position: anchors?.source ?? [0.5, 0.5] },
1099
1000
  target: { id: target, position: anchors?.target ?? [0.5, 0.5] },
1100
1001
  ...(edge.label ? { text: edge.label } : {}),
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export * from './operators.js';
29
29
  export * from './presets.js';
30
30
  export * from './reading.js';
31
31
  export * from './roles.js';
32
+ export * from './toolbox.js';
32
33
  export * from './profiles.js';
33
34
  export * from './rules.js';
34
35
  export * from './drawio-decode.js';
package/dist/index.js CHANGED
@@ -37,6 +37,9 @@ export * from './operators.js';
37
37
  export * from './presets.js';
38
38
  export * from './reading.js';
39
39
  export * from './roles.js';
40
+ // The toolbar as pure builders (`docs/adr/0045`), also on `./interchange`:
41
+ // every item that places elements, for a host with no editor.
42
+ export * from './toolbox.js';
40
43
  /* ── What a finished diagram is HELD TO: the rule pack and its two levels ─ */
41
44
  export * from './profiles.js';
42
45
  export * from './rules.js';
@@ -11,3 +11,4 @@
11
11
  */
12
12
  export * from './drawio-decode.js';
13
13
  export * from './interchange.js';
14
+ export { UML_TOOLBOX, umlFrameElement, type UmlFrameOptions, umlNodeElements, type UmlNodeTexts, umlRelationElement, } from './toolbox.js';
@@ -11,3 +11,6 @@
11
11
  */
12
12
  export * from './drawio-decode.js';
13
13
  export * from './interchange.js';
14
+ // The toolbar as pure builders (`docs/adr/0045`): what a host with no editor
15
+ // places to draw the very elements a user places from the toolbar.
16
+ export { UML_TOOLBOX, umlFrameElement, umlNodeElements, umlRelationElement, } from './toolbox.js';
@@ -164,6 +164,32 @@ export declare const UML_OPERATIONS_SEED_KEY = "com.labre.uml.seed.operations";
164
164
  export declare const UML_SLOTS_SEED = "attribute = value";
165
165
  /** Its own key, resolved at placement like the two above. */
166
166
  export declare const UML_SLOTS_SEED_KEY = "com.labre.uml.seed.slots";
167
+ /**
168
+ * What the SECOND compartment says on a fresh artefact, as `[key, English]`:
169
+ * the §9.5.4 attribute line every divided box gets, except two.
170
+ *
171
+ * Two exceptions and a default, rather than one entry per kind, because the
172
+ * default is the rule: `+ attribute : Type` is what a divided rectangle's body
173
+ * tier holds unless the kind's own clause says otherwise.
174
+ *
175
+ * - an **object**'s tier holds SLOTS (§9.8.4) — `attribute = value`, an
176
+ * instance giving its classifier's features values rather than redeclaring
177
+ * them;
178
+ * - a **state**'s holds INTERNAL ACTIVITIES (§14.2.4.4) and is seeded EMPTY.
179
+ * That is the one place in this pack where a blank tier is the right
180
+ * stencil, and it is the exporter that decides it: `model.ts` reads
181
+ * `entry / …`, `do / …` and `exit / …` off these lines and treats a label
182
+ * with nothing after it as no behaviour at all, so a seeded `entry / ` would
183
+ * be a prompt that reads as content and exports as nothing. §14.2.4's own
184
+ * figures draw exactly this — a named state with a ruled-off compartment
185
+ * waiting to be filled — and the author types the first line the grammar
186
+ * then reads back.
187
+ *
188
+ * The state's key is `null` for the reason an unlabelled kind contributes
189
+ * none: there is no word to translate. The toolbar resolves the key at
190
+ * placement; a host with no editor takes the English (`toolbox.ts`).
191
+ */
192
+ export declare function umlBodySeed(kind: UmlNodeKind): readonly [key: string | null, text: string];
167
193
  /** What a name compartment states, once its notation has been read off it. */
168
194
  export interface UmlStereotypes {
169
195
  /**
package/dist/keywords.js CHANGED
@@ -289,6 +289,38 @@ export const UML_OPERATIONS_SEED_KEY = 'com.labre.uml.seed.operations';
289
289
  export const UML_SLOTS_SEED = 'attribute = value';
290
290
  /** Its own key, resolved at placement like the two above. */
291
291
  export const UML_SLOTS_SEED_KEY = 'com.labre.uml.seed.slots';
292
+ /**
293
+ * What the SECOND compartment says on a fresh artefact, as `[key, English]`:
294
+ * the §9.5.4 attribute line every divided box gets, except two.
295
+ *
296
+ * Two exceptions and a default, rather than one entry per kind, because the
297
+ * default is the rule: `+ attribute : Type` is what a divided rectangle's body
298
+ * tier holds unless the kind's own clause says otherwise.
299
+ *
300
+ * - an **object**'s tier holds SLOTS (§9.8.4) — `attribute = value`, an
301
+ * instance giving its classifier's features values rather than redeclaring
302
+ * them;
303
+ * - a **state**'s holds INTERNAL ACTIVITIES (§14.2.4.4) and is seeded EMPTY.
304
+ * That is the one place in this pack where a blank tier is the right
305
+ * stencil, and it is the exporter that decides it: `model.ts` reads
306
+ * `entry / …`, `do / …` and `exit / …` off these lines and treats a label
307
+ * with nothing after it as no behaviour at all, so a seeded `entry / ` would
308
+ * be a prompt that reads as content and exports as nothing. §14.2.4's own
309
+ * figures draw exactly this — a named state with a ruled-off compartment
310
+ * waiting to be filled — and the author types the first line the grammar
311
+ * then reads back.
312
+ *
313
+ * The state's key is `null` for the reason an unlabelled kind contributes
314
+ * none: there is no word to translate. The toolbar resolves the key at
315
+ * placement; a host with no editor takes the English (`toolbox.ts`).
316
+ */
317
+ export function umlBodySeed(kind) {
318
+ if (kind === 'object')
319
+ return [UML_SLOTS_SEED_KEY, UML_SLOTS_SEED];
320
+ if (kind === 'state')
321
+ return [null, ''];
322
+ return [UML_ATTRIBUTES_SEED_KEY, UML_ATTRIBUTES_SEED];
323
+ }
292
324
  /**
293
325
  * Both spellings of a guillemet pair — the real characters and the ASCII
294
326
  * fallback Annex C's note tolerates.
package/dist/presets.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { TextAlign, TextDecoration } from '@formicoidea/labre-core/model/headless';
1
+ import type { SerializedElementProps } from '@formicoidea/labre-core/interchange';
2
+ import { ConnectorMode, StrokeStyle, TextAlign, TextDecoration } from '@formicoidea/labre-core/model/headless';
2
3
  import type { UmlNodeKind } from '@formicoidea/labre-core/model';
3
- import type { UmlBox } from './component.js';
4
+ import { type UmlBox } from './component.js';
5
+ import { type UmlEdgeRole } from './edge-styles.js';
4
6
  /**
5
7
  * What a UML shape IS, as props — the ONE description the creation site, the
6
8
  * morph and the templates all read (R17).
@@ -166,3 +168,90 @@ export declare function umlTextProps(role: string, box: UmlBox, options: {
166
168
  * never carried the field (`node/node-renderer.ts`).
167
169
  */
168
170
  export declare function umlNameDecoration(kind: UmlNodeKind): TextDecoration | undefined;
171
+ /** The words of an artefact, tier by tier, written exactly as given. */
172
+ export interface UmlTierTexts {
173
+ name: string;
174
+ /** The second compartment, one member per line; `''` when absent. */
175
+ attributes?: string;
176
+ /** The third compartment, one member per line; `''` when absent. */
177
+ operations?: string;
178
+ }
179
+ /** The elements of one artefact, as {@link umlArtefactElements} names them. */
180
+ export type UmlArtefactPart = 'shape' | 'name' | 'attributes' | 'operations' | 'group';
181
+ /**
182
+ * One UML artefact in `box`, as the elements a document holds: the shape, the
183
+ * tiers its kind is worth, and the group that makes them one thing, LAST.
184
+ *
185
+ * The ONE assembly of an artefact (`docs/adr/0045`): the toolbar's actions
186
+ * (through the toolbox, `toolbox.ts`), a host with no editor (the same
187
+ * toolbox) and every importer (`umlElementsFromModel`, `import.ts`) write
188
+ * through here, so a class drawn by hand, one placed by a host and one read
189
+ * out of a `.puml` file are the same five elements, down to the stroke width.
190
+ * What differs between them is what they pass: where the box is, the words
191
+ * (the toolbar's seeds, a host's texts, the file's own), and the ids.
192
+ *
193
+ * ## How many tiers is the LAYOUT's answer, not this function's
194
+ *
195
+ * A tier is written for each box {@link umlCompartmentBoxes} returns and for
196
+ * no other: a class gets name, attributes and operations; an object, a
197
+ * component, an artifact and a state a name over one body tier; a glyph its
198
+ * one word, placed against its picture. A mark the notation draws no word on
199
+ * ({@link UML_UNLABELLED_KINDS}) is the SHAPE alone, and therefore no group
200
+ * either: a group of one element is a wrapper a user would have to descend
201
+ * through to reach the mark it holds.
202
+ *
203
+ * ## The name tier
204
+ *
205
+ * `uml:name` on a divided box and on the five {@link NAMED_GLYPH_KINDS};
206
+ * otherwise the kind's label role ({@link umlLabelRoleOf}: a lifeline's head
207
+ * is `uml:lifeline-ident`, every other single word `uml:label`). Bold on a
208
+ * divided box only, where it is the heading; read LEFT on a note, which holds
209
+ * a sentence rather than a title (Annex A); underlined on an instance
210
+ * ({@link umlNameDecoration}). A compartment of features is a list of
211
+ * signatures and is read down its left edge (§9.5.4).
212
+ *
213
+ * ## Ids
214
+ *
215
+ * `id(part)` is asked once per element, in painting order — shape, name,
216
+ * attributes, operations, group — and only for the elements written: the
217
+ * importer mints its `uml-import-<n>` names in that order, the toolbox
218
+ * prefixes a local name.
219
+ */
220
+ export declare function umlArtefactElements(kind: UmlNodeKind, box: UmlBox, tiers: UmlTierTexts, id: (part: UmlArtefactPart) => string): SerializedElementProps[];
221
+ /** The five background elements a UML diagram is drawn on and in. */
222
+ export type UmlFrameType = 'umlDiagram' | 'umlSubject' | 'umlPartition' | 'umlRegion' | 'umlFragment';
223
+ /** The box each frame is created at, by the toolbar and the importers. */
224
+ export declare const UML_FRAME_BOX: Readonly<Record<UmlFrameType, {
225
+ readonly w: number;
226
+ readonly h: number;
227
+ }>>;
228
+ /**
229
+ * A frame's props: its type, its role, the fields the caller states (a name, a
230
+ * diagram's kind, a fragment's operator…) and its box. The toolbox and the
231
+ * importers both write a frame through here, so the role a frame is born with
232
+ * is the one table above.
233
+ */
234
+ export declare function umlFrameProps(type: UmlFrameType, box: {
235
+ xywh: string;
236
+ }, fields: Record<string, unknown>): SerializedElementProps;
237
+ /**
238
+ * One UML relationship, as the props `surface.addElement` takes, ends left to
239
+ * the caller: the role, the STRAIGHT mode, the ink and the line
240
+ * {@link UML_EDGE_STYLE} says the relationship looks like.
241
+ *
242
+ * The edge tools arm the connector with it (`activateUmlEdge`), the toolbox
243
+ * builds a host's connector from it and the importers draw a file's
244
+ * relations through it, so the three are one line. STRAIGHT, always: a class
245
+ * diagram is a graph, not a process laid out in lanes, and the elbows of an
246
+ * orthogonal router would read as a route through the diagram nobody drew.
247
+ */
248
+ export declare function umlConnectorProps(edge: UmlEdgeRole): {
249
+ strokeStyle: StrokeStyle;
250
+ frontEndpointStyle: import("@formicoidea/labre-core/model").PointStyle;
251
+ rearEndpointStyle: import("@formicoidea/labre-core/model").PointStyle;
252
+ type: "connector";
253
+ role: "uml:association" | "uml:aggregation" | "uml:composition" | "uml:generalization" | "uml:realization" | "uml:dependency" | "uml:anchor" | "uml:include" | "uml:extend" | "uml:deploy" | "uml:manifest" | "uml:communication-path" | "uml:control-flow" | "uml:object-flow" | "uml:transition" | "uml:message-sync" | "uml:message-async" | "uml:message-reply" | "uml:message-create" | "uml:message-delete";
254
+ mode: ConnectorMode;
255
+ stroke: string;
256
+ strokeWidth: number;
257
+ };
package/dist/presets.js CHANGED
@@ -1,7 +1,10 @@
1
- import { FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model/headless';
1
+ import { ConnectorMode, FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { UML_CARD, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_NODE_RADIUS, UML_NODE_STROKE_WIDTH, } from './consts.js';
4
- import { UML_ROLE_OF_KIND } from './roles.js';
3
+ import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
4
+ import { UML_BODY_FONT_SIZE, UML_CARD, UML_DIAGRAM_BOX, UML_EDGE_WIDTH, UML_FRAGMENT_BOX, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_NODE_RADIUS, UML_NODE_STROKE_WIDTH, UML_PARTITION_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
5
+ import { UML_EDGE_STYLE } from './edge-styles.js';
6
+ import { UML_UNLABELLED_KINDS } from './keywords.js';
7
+ import { UML_ROLE, UML_ROLE_OF_KIND } from './roles.js';
5
8
  /**
6
9
  * What a UML shape IS, as props — the ONE description the creation site, the
7
10
  * morph and the templates all read (R17).
@@ -320,3 +323,175 @@ export function umlTextProps(role, box, options) {
320
323
  export function umlNameDecoration(kind) {
321
324
  return kind === 'object' ? TextDecoration.Underline : undefined;
322
325
  }
326
+ /* ── The whole artefact: shape, tiers, group ──────────────────────────── */
327
+ /**
328
+ * The glyph kinds whose one tier is a `uml:name` rather than a `uml:label`.
329
+ *
330
+ * The line between the two is drawn by `roles.ts` and it is not about having a
331
+ * rectangle: `uml:name` is the tier a KEYWORD may be written on and the grammar
332
+ * reads back, `uml:label` is a name and nothing else. So:
333
+ *
334
+ * - a **package** name is a namespace, parseable and qualified (§12.2.4), and a
335
+ * **note**'s text is what the exporter writes as the comment's body;
336
+ * - the three **deployment cubes** are named INSIDE their front face, over a
337
+ * keyword line — `«device»`, `«executionEnvironment»` (§19.4.4) — which is
338
+ * exactly what a name compartment is for. It is also load-bearing rather than
339
+ * tidy: the morph's `afterMorph` rewrites `uml:name` and only that, so a cube
340
+ * labelled instead of named would morph into a device that never said so.
341
+ *
342
+ * Everything else here carries a LABEL: a port's word sits beside a 16-unit
343
+ * square and the two interface marks' beside a ball and a socket, none of them
344
+ * inside anything, and none of them ever carrying a keyword.
345
+ *
346
+ * Written once, here, since {@link umlArtefactElements} became the one assembly
347
+ * (`docs/adr/0045`): the toolbar and the importers each used to keep a copy.
348
+ */
349
+ const NAMED_GLYPH_KINDS = new Set([
350
+ 'package',
351
+ 'note',
352
+ 'node',
353
+ 'device',
354
+ 'execution-environment',
355
+ ]);
356
+ /**
357
+ * One UML artefact in `box`, as the elements a document holds: the shape, the
358
+ * tiers its kind is worth, and the group that makes them one thing, LAST.
359
+ *
360
+ * The ONE assembly of an artefact (`docs/adr/0045`): the toolbar's actions
361
+ * (through the toolbox, `toolbox.ts`), a host with no editor (the same
362
+ * toolbox) and every importer (`umlElementsFromModel`, `import.ts`) write
363
+ * through here, so a class drawn by hand, one placed by a host and one read
364
+ * out of a `.puml` file are the same five elements, down to the stroke width.
365
+ * What differs between them is what they pass: where the box is, the words
366
+ * (the toolbar's seeds, a host's texts, the file's own), and the ids.
367
+ *
368
+ * ## How many tiers is the LAYOUT's answer, not this function's
369
+ *
370
+ * A tier is written for each box {@link umlCompartmentBoxes} returns and for
371
+ * no other: a class gets name, attributes and operations; an object, a
372
+ * component, an artifact and a state a name over one body tier; a glyph its
373
+ * one word, placed against its picture. A mark the notation draws no word on
374
+ * ({@link UML_UNLABELLED_KINDS}) is the SHAPE alone, and therefore no group
375
+ * either: a group of one element is a wrapper a user would have to descend
376
+ * through to reach the mark it holds.
377
+ *
378
+ * ## The name tier
379
+ *
380
+ * `uml:name` on a divided box and on the five {@link NAMED_GLYPH_KINDS};
381
+ * otherwise the kind's label role ({@link umlLabelRoleOf}: a lifeline's head
382
+ * is `uml:lifeline-ident`, every other single word `uml:label`). Bold on a
383
+ * divided box only, where it is the heading; read LEFT on a note, which holds
384
+ * a sentence rather than a title (Annex A); underlined on an instance
385
+ * ({@link umlNameDecoration}). A compartment of features is a list of
386
+ * signatures and is read down its left edge (§9.5.4).
387
+ *
388
+ * ## Ids
389
+ *
390
+ * `id(part)` is asked once per element, in painting order — shape, name,
391
+ * attributes, operations, group — and only for the elements written: the
392
+ * importer mints its `uml-import-<n>` names in that order, the toolbox
393
+ * prefixes a local name.
394
+ */
395
+ export function umlArtefactElements(kind, box, tiers, id) {
396
+ const shape = {
397
+ // NO `text`: the words are the tiers below, and the shape is a body and
398
+ // nothing else (R16).
399
+ ...umlNodeProps(kind, {
400
+ xywh: new Bound(box.x, box.y, box.w, box.h).serialize(),
401
+ }),
402
+ id: id('shape'),
403
+ };
404
+ if (UML_UNLABELLED_KINDS.has(kind))
405
+ return [shape];
406
+ const boxes = umlCompartmentBoxes(kind, box.x, box.y, box.w, box.h);
407
+ const compartmented = boxes.attributes !== undefined;
408
+ const members = [
409
+ shape,
410
+ {
411
+ ...umlTextProps(compartmented || NAMED_GLYPH_KINDS.has(kind)
412
+ ? UML_ROLE.name
413
+ : umlLabelRoleOf(kind), boxes.name, {
414
+ fontSize: UML_NAME_FONT_SIZE,
415
+ align: kind === 'note' ? TextAlign.Left : TextAlign.Center,
416
+ ...(compartmented ? { bold: true } : {}),
417
+ decoration: umlNameDecoration(kind),
418
+ }),
419
+ id: id('name'),
420
+ text: tiers.name,
421
+ },
422
+ ];
423
+ for (const [part, role, tierBox] of [
424
+ ['attributes', UML_ROLE.attributes, boxes.attributes],
425
+ ['operations', UML_ROLE.operations, boxes.operations],
426
+ ]) {
427
+ if (!tierBox)
428
+ continue;
429
+ members.push({
430
+ ...umlTextProps(role, tierBox, {
431
+ fontSize: UML_BODY_FONT_SIZE,
432
+ align: TextAlign.Left,
433
+ }),
434
+ id: id(part),
435
+ text: tiers[part] ?? '',
436
+ });
437
+ }
438
+ return [
439
+ ...members,
440
+ {
441
+ type: 'group',
442
+ id: id('group'),
443
+ // A plain record is a legal `children`: the group's own `propsToY` takes
444
+ // the KEYS. No title, deliberately: the group renderer paints one only
445
+ // while the artefact is selected, and a package announcing itself as
446
+ // "Group 3" above its own name is a label nobody wrote.
447
+ children: Object.fromEntries(members.map(m => [m.id, true])),
448
+ },
449
+ ];
450
+ }
451
+ /** The box each frame is created at, by the toolbar and the importers. */
452
+ export const UML_FRAME_BOX = {
453
+ umlDiagram: UML_DIAGRAM_BOX,
454
+ umlSubject: UML_SUBJECT_BOX,
455
+ umlPartition: UML_PARTITION_BOX,
456
+ umlRegion: UML_REGION_BOX,
457
+ umlFragment: UML_FRAGMENT_BOX,
458
+ };
459
+ /** The role each frame carries — one table, read by every creation site. */
460
+ const UML_FRAME_ROLE = {
461
+ umlDiagram: UML_ROLE.diagram,
462
+ umlSubject: UML_ROLE.subject,
463
+ umlPartition: UML_ROLE.partition,
464
+ umlRegion: UML_ROLE.region,
465
+ umlFragment: UML_ROLE.fragment,
466
+ };
467
+ /**
468
+ * A frame's props: its type, its role, the fields the caller states (a name, a
469
+ * diagram's kind, a fragment's operator…) and its box. The toolbox and the
470
+ * importers both write a frame through here, so the role a frame is born with
471
+ * is the one table above.
472
+ */
473
+ export function umlFrameProps(type, box, fields) {
474
+ return { type, role: UML_FRAME_ROLE[type], ...fields, xywh: box.xywh };
475
+ }
476
+ /* ── The relationships ────────────────────────────────────────────────── */
477
+ /**
478
+ * One UML relationship, as the props `surface.addElement` takes, ends left to
479
+ * the caller: the role, the STRAIGHT mode, the ink and the line
480
+ * {@link UML_EDGE_STYLE} says the relationship looks like.
481
+ *
482
+ * The edge tools arm the connector with it (`activateUmlEdge`), the toolbox
483
+ * builds a host's connector from it and the importers draw a file's
484
+ * relations through it, so the three are one line. STRAIGHT, always: a class
485
+ * diagram is a graph, not a process laid out in lanes, and the elbows of an
486
+ * orthogonal router would read as a route through the diagram nobody drew.
487
+ */
488
+ export function umlConnectorProps(edge) {
489
+ return {
490
+ type: 'connector',
491
+ role: UML_ROLE[edge],
492
+ mode: ConnectorMode.Straight,
493
+ stroke: UML_INK,
494
+ strokeWidth: UML_EDGE_WIDTH,
495
+ ...UML_EDGE_STYLE[edge],
496
+ };
497
+ }