@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,89 @@
1
+ import { mountShapeTextEditor } from '@formicoidea/labre-core/gfx/shape';
2
+ import { mountTextElementEditor } from '@formicoidea/labre-core/gfx/text';
3
+ import { GroupElementModel, ShapeElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
4
+ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
5
+ import { umlComponentSiblings, umlGroupOf } from '../component.js';
6
+ /**
7
+ * View for a UML node. Registering it ensures `gfx.view.get(model)` returns a
8
+ * view (required so move / select / connector interactions work).
9
+ *
10
+ * ## Double-clicking the body edits the NAME — which is not on this element
11
+ *
12
+ * A UML artefact is a GROUP: the shape, plus the canvas TEXT elements that carry
13
+ * its words, every one of them edited in place exactly like any other words on
14
+ * the canvas (R16, and the same arrangement C4's recette of 28/08/2026 settled).
15
+ * The shape itself carries no text at all. Which leaves a gesture with nowhere
16
+ * obvious to go: the body is the biggest target in the component, double-clicking
17
+ * a shape is how everybody in this editor starts typing, and the shape's own
18
+ * inline editor would write into the one field that must stay empty — a "shadow"
19
+ * name, invisible under the real one, disagreeing with it, exported by neither.
20
+ *
21
+ * So the gesture is ROUTED: a double-click on the body opens the NAME child's
22
+ * own editor, `mountTextElementEditor` — the very same call `TextElementView`
23
+ * makes when you double-click those words directly. The author gets one editor
24
+ * for one name whichever half of the component they aimed at.
25
+ *
26
+ * ## `uml:name`, `uml:label` or `uml:lifeline-ident`, and why all three
27
+ *
28
+ * A compartmented artefact's first tier is `uml:name` — it is the NAME
29
+ * COMPARTMENT of §11.4.4, a rectangle with a rule under it. An actor's and a use
30
+ * case's single word is `uml:label`: the same tier, named differently because
31
+ * there is no compartment for it to be a compartment OF. A lifeline's head is
32
+ * `uml:lifeline-ident`, a third time: §17.3.4 prints a grammar for what goes in
33
+ * it and the validation pack reads that grammar and no other (`roles.ts`). The
34
+ * gesture is one gesture, so it accepts any of the three and takes whichever the
35
+ * group holds — the kind is not consulted, which means an artefact regrouped by
36
+ * hand still opens on the words it actually has.
37
+ *
38
+ * ## …except on an element that has no text child
39
+ *
40
+ * A node whose group was released and whose texts were deleted lands on
41
+ * `mountShapeTextEditor` and gets the native behaviour rather than nothing at
42
+ * all. Same fallback C4's node view keeps, and for the same reason: a gesture
43
+ * that silently does nothing reads as a broken editor.
44
+ *
45
+ * Hit testing is `UmlNodeElementModel.includesPoint`'s doing: the glyph-bodied
46
+ * kinds are created unfilled, and an empty unfilled shape would normally be a
47
+ * target only near its border. The override forces the interior test, so the
48
+ * whole body answers this gesture on all eight kinds.
49
+ */
50
+ export class UmlNodeView extends GfxElementModelView {
51
+ static { this.type = 'umlNode'; }
52
+ onCreated() {
53
+ super.onCreated();
54
+ this.on('dblclick', () => {
55
+ const edgeless = this.std.view.getBlock(this.std.store.root.id);
56
+ if (!edgeless || this.model.isLocked())
57
+ return;
58
+ const name = this.#name();
59
+ if (name) {
60
+ if (!name.isLocked())
61
+ mountTextElementEditor(name, edgeless);
62
+ return;
63
+ }
64
+ // No text child: the words really are the shape's own inner text.
65
+ if (this.model instanceof ShapeElementModel) {
66
+ mountShapeTextEditor(this.model, edgeless);
67
+ }
68
+ });
69
+ }
70
+ /**
71
+ * The `uml:name` (or `uml:label`, or `uml:lifeline-ident`) text grouped with
72
+ * this shape, if any.
73
+ */
74
+ #name() {
75
+ const surface = this.gfx.surface;
76
+ if (!surface)
77
+ return null;
78
+ const groups = surface.elementModels.filter((element) => element instanceof GroupElementModel);
79
+ const group = umlGroupOf(this.model.id, groups);
80
+ if (!group)
81
+ return null;
82
+ // The same pure resolution the exporter uses — group membership, then roles
83
+ // — so the words this gesture opens are the words the file comes out with.
84
+ const component = umlComponentSiblings(group, surface.elementModels);
85
+ const tier = component.name ?? component.label ?? component.ident;
86
+ const element = tier && surface.getElementById(tier.id);
87
+ return element instanceof TextElementModel ? element : null;
88
+ }
89
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The painting primitives every UML glyph is built out of, and the box they are
3
+ * all measured against.
4
+ *
5
+ * Lifted out of `node-renderer.ts` when the behaviour artefacts of phase 2
6
+ * arrived: a disc, a ring, a bar and a cross are drawn by the activity glyphs
7
+ * and by the state machine glyphs alike, and three files each with their own
8
+ * `line()` is three places for one of them to grow a rounded cap.
9
+ *
10
+ * Nothing here knows what a UML artefact is. Each function takes an already-
11
+ * configured context — `fillStyle`, `strokeStyle` and `lineWidth` set from the
12
+ * MODEL, never from a table — and a rectangle in the element-local frame.
13
+ */
14
+ export declare const TAU: number;
15
+ /**
16
+ * The rectangle a glyph is drawn inside: the element box, inset by half the
17
+ * stroke width so an outline lands within the element's bounds rather than
18
+ * straddling them.
19
+ *
20
+ * Both the inset box (`x0…y1`, `bw`, `bh`) and the RAW element size (`w`, `h`)
21
+ * are carried, because glyphs need both: an outline is drawn inside the inset,
22
+ * while every proportion `component.ts` also reads — the package's tab, the
23
+ * cube's depth, the pentagon's point — is a fraction of the element, so that
24
+ * the renderer and the layout cannot disagree about where a label goes.
25
+ */
26
+ export interface GlyphBox {
27
+ x0: number;
28
+ y0: number;
29
+ x1: number;
30
+ y1: number;
31
+ /** The inset box's own width and height. */
32
+ bw: number;
33
+ bh: number;
34
+ /** The element's width and height, before the stroke inset. */
35
+ w: number;
36
+ h: number;
37
+ }
38
+ /**
39
+ * Draw the four sides of a rectangle as an explicit path.
40
+ *
41
+ * `ctx.rect` would be one call — and would record nothing a test about WHERE a
42
+ * glyph was drawn can read, on this pack's canvas recorder or on a reviewer's
43
+ * screen. The four segments are the shape.
44
+ */
45
+ export declare function rectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void;
46
+ /** …and paint it as a body: the element's fill, then the element's outline. */
47
+ export declare function solidRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void;
48
+ /** Draw one straight segment, skipping a degenerate one. */
49
+ export declare function line(ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number): void;
50
+ /** A closed polygon, filled with the element's paper and outlined in its ink. */
51
+ export declare function solidPolygon(ctx: CanvasRenderingContext2D, points: readonly (readonly [number, number])[]): void;
52
+ /**
53
+ * A circle filled with the element's paper and outlined in its ink — the hollow
54
+ * mark: an activity final's outer ring, a flow final's rim, an entry point.
55
+ */
56
+ export declare function hollowDisc(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number): void;
57
+ /**
58
+ * A circle filled with the element's INK — the solid mark: an initial node, a
59
+ * junction, the bull of a bullseye.
60
+ *
61
+ * The fill is swapped to the stroke colour and put back, rather than the caller
62
+ * doing it: a glyph that forgot to restore it would paint every later mark in
63
+ * the wrong colour, and the bug would show up two shapes away from its cause.
64
+ * The colour is still the MODEL's — a node recoloured from the shape toolbar
65
+ * recolours its solid marks with it.
66
+ */
67
+ export declare function inkDisc(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number): void;
68
+ /** The same swap, for a solid mark that is not a circle (the fork's bar). */
69
+ export declare function withInkFill(ctx: CanvasRenderingContext2D, draw: () => void): void;
70
+ /**
71
+ * The two diagonals of a square centred on `(cx, cy)` with half-diagonal `r` —
72
+ * the X of a flow final, an exit point and a terminate.
73
+ */
74
+ export declare function cross(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number): void;
75
+ /**
76
+ * One centred word, in the element's ink — the `H` and `H*` of §14.2.4's history
77
+ * pseudostates, and the only text this pack's glyph layer writes.
78
+ *
79
+ * Text and not a drawn letterform, because it IS a letter: §14.2.4 writes `H`
80
+ * inside the circle, and a path traced by hand would be this pack drawing a
81
+ * typeface. Sized off the mark rather than off a constant, so a history dragged
82
+ * bigger carries a bigger letter.
83
+ */
84
+ export declare function glyphText(ctx: CanvasRenderingContext2D, text: string, cx: number, cy: number, size: number, fontFamily: string): void;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The painting primitives every UML glyph is built out of, and the box they are
3
+ * all measured against.
4
+ *
5
+ * Lifted out of `node-renderer.ts` when the behaviour artefacts of phase 2
6
+ * arrived: a disc, a ring, a bar and a cross are drawn by the activity glyphs
7
+ * and by the state machine glyphs alike, and three files each with their own
8
+ * `line()` is three places for one of them to grow a rounded cap.
9
+ *
10
+ * Nothing here knows what a UML artefact is. Each function takes an already-
11
+ * configured context — `fillStyle`, `strokeStyle` and `lineWidth` set from the
12
+ * MODEL, never from a table — and a rectangle in the element-local frame.
13
+ */
14
+ export const TAU = Math.PI * 2;
15
+ /**
16
+ * Draw the four sides of a rectangle as an explicit path.
17
+ *
18
+ * `ctx.rect` would be one call — and would record nothing a test about WHERE a
19
+ * glyph was drawn can read, on this pack's canvas recorder or on a reviewer's
20
+ * screen. The four segments are the shape.
21
+ */
22
+ export function rectPath(ctx, x, y, w, h) {
23
+ ctx.beginPath();
24
+ ctx.moveTo(x, y);
25
+ ctx.lineTo(x + w, y);
26
+ ctx.lineTo(x + w, y + h);
27
+ ctx.lineTo(x, y + h);
28
+ ctx.closePath();
29
+ }
30
+ /** …and paint it as a body: the element's fill, then the element's outline. */
31
+ export function solidRect(ctx, x, y, w, h) {
32
+ if (!(w > 0) || !(h > 0))
33
+ return;
34
+ rectPath(ctx, x, y, w, h);
35
+ ctx.fill();
36
+ ctx.stroke();
37
+ }
38
+ /** Draw one straight segment, skipping a degenerate one. */
39
+ export function line(ctx, x1, y1, x2, y2) {
40
+ ctx.beginPath();
41
+ ctx.moveTo(x1, y1);
42
+ ctx.lineTo(x2, y2);
43
+ ctx.stroke();
44
+ }
45
+ /** A closed polygon, filled with the element's paper and outlined in its ink. */
46
+ export function solidPolygon(ctx, points) {
47
+ if (points.length < 3)
48
+ return;
49
+ ctx.beginPath();
50
+ ctx.moveTo(points[0][0], points[0][1]);
51
+ for (let i = 1; i < points.length; i++) {
52
+ ctx.lineTo(points[i][0], points[i][1]);
53
+ }
54
+ ctx.closePath();
55
+ ctx.fill();
56
+ ctx.stroke();
57
+ }
58
+ /**
59
+ * A circle filled with the element's paper and outlined in its ink — the hollow
60
+ * mark: an activity final's outer ring, a flow final's rim, an entry point.
61
+ */
62
+ export function hollowDisc(ctx, cx, cy, r) {
63
+ if (!(r > 0))
64
+ return;
65
+ ctx.beginPath();
66
+ ctx.arc(cx, cy, r, 0, TAU);
67
+ ctx.fill();
68
+ ctx.stroke();
69
+ }
70
+ /**
71
+ * A circle filled with the element's INK — the solid mark: an initial node, a
72
+ * junction, the bull of a bullseye.
73
+ *
74
+ * The fill is swapped to the stroke colour and put back, rather than the caller
75
+ * doing it: a glyph that forgot to restore it would paint every later mark in
76
+ * the wrong colour, and the bug would show up two shapes away from its cause.
77
+ * The colour is still the MODEL's — a node recoloured from the shape toolbar
78
+ * recolours its solid marks with it.
79
+ */
80
+ export function inkDisc(ctx, cx, cy, r) {
81
+ if (!(r > 0))
82
+ return;
83
+ withInkFill(ctx, () => {
84
+ ctx.beginPath();
85
+ ctx.arc(cx, cy, r, 0, TAU);
86
+ ctx.fill();
87
+ ctx.stroke();
88
+ });
89
+ }
90
+ /** The same swap, for a solid mark that is not a circle (the fork's bar). */
91
+ export function withInkFill(ctx, draw) {
92
+ const paper = ctx.fillStyle;
93
+ ctx.fillStyle = ctx.strokeStyle;
94
+ try {
95
+ draw();
96
+ }
97
+ finally {
98
+ ctx.fillStyle = paper;
99
+ }
100
+ }
101
+ /**
102
+ * The two diagonals of a square centred on `(cx, cy)` with half-diagonal `r` —
103
+ * the X of a flow final, an exit point and a terminate.
104
+ */
105
+ export function cross(ctx, cx, cy, r) {
106
+ if (!(r > 0))
107
+ return;
108
+ // 45°, so the arms reach the rim of the circle they are drawn inside rather
109
+ // than a square inscribed in it: the cross of §15.3.4 touches its own rim.
110
+ const d = r * Math.SQRT1_2;
111
+ line(ctx, cx - d, cy - d, cx + d, cy + d);
112
+ line(ctx, cx + d, cy - d, cx - d, cy + d);
113
+ }
114
+ /**
115
+ * One centred word, in the element's ink — the `H` and `H*` of §14.2.4's history
116
+ * pseudostates, and the only text this pack's glyph layer writes.
117
+ *
118
+ * Text and not a drawn letterform, because it IS a letter: §14.2.4 writes `H`
119
+ * inside the circle, and a path traced by hand would be this pack drawing a
120
+ * typeface. Sized off the mark rather than off a constant, so a history dragged
121
+ * bigger carries a bigger letter.
122
+ */
123
+ export function glyphText(ctx, text, cx, cy, size, fontFamily) {
124
+ if (!(size > 0))
125
+ return;
126
+ const paper = ctx.fillStyle;
127
+ const font = ctx.font;
128
+ const align = ctx.textAlign;
129
+ const baseline = ctx.textBaseline;
130
+ ctx.fillStyle = ctx.strokeStyle;
131
+ ctx.font = `600 ${size}px ${fontFamily}`;
132
+ ctx.textAlign = 'center';
133
+ ctx.textBaseline = 'middle';
134
+ try {
135
+ ctx.fillText(text, cx, cy);
136
+ }
137
+ finally {
138
+ ctx.fillStyle = paper;
139
+ ctx.font = font;
140
+ ctx.textAlign = align;
141
+ ctx.textBaseline = baseline;
142
+ }
143
+ }
@@ -0,0 +1,36 @@
1
+ import { type UmlNodeKind } from '@formicoidea/labre-core/model';
2
+ import type { GlyphBox } from './paint.js';
3
+ /**
4
+ * The INTERACTION glyphs (§17.2.4, §17.3.4) — the two marks a sequence diagram
5
+ * is drawn out of that the shape layer cannot draw.
6
+ *
7
+ * A third module beside `act-glyphs.ts` and `stm-glyphs.ts`, for the reason
8
+ * those two exist: the renderer owns the BRANCH — which kind gets which picture
9
+ * — and the drawing lives beside the other drawings of its own family.
10
+ *
11
+ * The `execution` of §17.2.4 is deliberately NOT here. It is a thin FILLED
12
+ * RECTANGLE on a lifeline's spine, which is exactly what a native rect is, so
13
+ * the shape layer fills it, strokes it, hit-tests it and re-themes it and this
14
+ * file paints nothing over it — the same call `object-node` and `action` make.
15
+ *
16
+ * ## The lifeline is the one glyph bigger than its own element
17
+ *
18
+ * Everywhere else in this pack a picture is drawn inside the box it belongs to.
19
+ * §17.3.4 makes that impossible: what a message attaches to is the dashed
20
+ * SPINE, so the element has to be the narrow column the spine runs down (16
21
+ * units — see `UML_NODE_BOX.lifeline`), and the named head over its top is 160.
22
+ * The head therefore overflows the element by 72 units on each side, which is
23
+ * why `UmlNodeElementModel` overrides `elementBound` and `includesPoint` and
24
+ * why both of them, the label layout in `component.ts` and the drawing below
25
+ * all read ONE function — {@link umlLifelineHeadRect}, in the model, where the
26
+ * overrides can reach it.
27
+ */
28
+ /** The kinds this module draws. */
29
+ export type UmlSdGlyphKind = 'lifeline' | 'destruction';
30
+ /**
31
+ * Paint one interaction mark, in the element-local frame, with `fillStyle` /
32
+ * `strokeStyle` / `lineWidth` already set from the model.
33
+ */
34
+ export declare function paintSdGlyph(kind: UmlSdGlyphKind, ctx: CanvasRenderingContext2D, box: GlyphBox): void;
35
+ /** Whether the node renderer should hand this kind to {@link paintSdGlyph}. */
36
+ export declare function isSdGlyphKind(kind: UmlNodeKind): kind is UmlSdGlyphKind;
@@ -0,0 +1,79 @@
1
+ import { umlLifelineHeadRect } from '@formicoidea/labre-core/model';
2
+ import { UML_LIFELINE_DASH, UML_LIFELINE_SPINE_WIDTH } from '../consts.js';
3
+ import { cross, line, solidRect } from './paint.js';
4
+ /**
5
+ * The reach of a destruction's cross, as a fraction of the box's half-diagonal.
6
+ *
7
+ * The whole box, near enough: §17.2.4 draws the X as a bare cross with nothing
8
+ * round it, so — unlike a flow final, whose arms stop short of a rim they are
9
+ * drawn inside — there is nothing here for it to stay clear of.
10
+ */
11
+ const DESTRUCTION_REACH = 0.98;
12
+ /**
13
+ * Paint one interaction mark, in the element-local frame, with `fillStyle` /
14
+ * `strokeStyle` / `lineWidth` already set from the model.
15
+ */
16
+ export function paintSdGlyph(kind, ctx, box) {
17
+ const { x0, y0, bw, bh, w, h } = box;
18
+ switch (kind) {
19
+ // ── The lifeline: a named head over a dashed spine (§17.3.4) ──────────
20
+ case 'lifeline': {
21
+ const head = umlLifelineHeadRect({ deserializedXYWH: [0, 0, w, h] });
22
+ if (!head)
23
+ return;
24
+ // The head is the BODY: a lifeline is created unfilled and unstroked
25
+ // (`presets.ts`), so the glyph fills with the element's own `fillColor`
26
+ // and outlines with its `strokeColor` — which is what keeps it
27
+ // recolourable from the ordinary shape toolbar. Drawn at the head's own
28
+ // rectangle and not at the inset one: the inset belongs to the COLUMN,
29
+ // which is 16 units wide and is not what is being outlined here.
30
+ solidRect(ctx, head.x, head.y, head.w, head.h);
31
+ // …and the spine, down the column's centre from the head's bottom edge.
32
+ // DASHED, and that is the whole notation: an unbroken line would read as
33
+ // a relationship rather than as the passage of time (§17.3.4). Lighter
34
+ // than the head, because it is a line and the head is a box.
35
+ const cx = w / 2;
36
+ const top = head.y + head.h;
37
+ //
38
+ // The dash and the width are scoped with `save`/`restore` rather than
39
+ // read back and re-set: both are part of the 2D drawing state, so the
40
+ // caller's pattern comes back whatever it was, and the glyph never calls
41
+ // `getLineDash` — which the SVG export's context (svgcanvas 2.6.0) does
42
+ // not implement. The pair is balanced inside this block, `finally`
43
+ // included, so the renderer's own save stack is left as it found it.
44
+ if (h > top) {
45
+ ctx.save();
46
+ try {
47
+ ctx.setLineDash([...UML_LIFELINE_DASH]);
48
+ ctx.lineWidth = UML_LIFELINE_SPINE_WIDTH;
49
+ line(ctx, cx, top, cx, h);
50
+ }
51
+ finally {
52
+ ctx.restore();
53
+ }
54
+ }
55
+ return;
56
+ }
57
+ // ── The destruction: a bare X (§17.2.4) ───────────────────────────────
58
+ // Two strokes and nothing else — the mark that says this participant's
59
+ // life ends here. The same drawing as `terminate` (§14.2.4) and kept a
60
+ // separate kind for the reason the roles are separate: a terminate kills a
61
+ // state MACHINE, a destruction ends one participant in one conversation.
62
+ case 'destruction':
63
+ if (bw > 0 && bh > 0) {
64
+ cross(ctx, x0 + bw / 2, y0 + bh / 2, (Math.min(bw, bh) / 2) * DESTRUCTION_REACH);
65
+ }
66
+ return;
67
+ }
68
+ /**
69
+ * Every kind this module claims is drawn above — narrowed to `never` only if
70
+ * the switch is exhaustive over {@link UmlSdGlyphKind}, so a kind added to
71
+ * the union without a picture stops the build rather than painting nothing.
72
+ */
73
+ const unhandled = kind;
74
+ void unhandled;
75
+ }
76
+ /** Whether the node renderer should hand this kind to {@link paintSdGlyph}. */
77
+ export function isSdGlyphKind(kind) {
78
+ return kind === 'lifeline' || kind === 'destruction';
79
+ }
@@ -0,0 +1,22 @@
1
+ import type { UmlNodeKind } from '@formicoidea/labre-core/model';
2
+ import type { GlyphBox } from './paint.js';
3
+ /**
4
+ * The STATE MACHINE glyphs (§14.2.4) — the final state and the seven
5
+ * pseudostates a machine's transitions are routed through.
6
+ *
7
+ * The counterpart of `act-glyphs.ts`, and split out for the same reason. Two of
8
+ * the family are NOT here: a `state` is the native rounded rect with a
9
+ * compartment rule (drawn by the renderer's compartment branch, like a class),
10
+ * and a `choice` is the native diamond. And one more is elsewhere entirely —
11
+ * the INITIAL pseudostate is `act-glyphs.ts`'s filled disc, because §14.2.4 and
12
+ * §15.3.4 draw the same disc and mean the same thing by it.
13
+ */
14
+ /** The kinds this module draws. */
15
+ export type UmlStmGlyphKind = 'final-state' | 'junction' | 'shallow-history' | 'deep-history' | 'entry-point' | 'exit-point' | 'terminate';
16
+ /**
17
+ * Paint one state machine mark, in the element-local frame, with `fillStyle` /
18
+ * `strokeStyle` / `lineWidth` already set from the model.
19
+ */
20
+ export declare function paintStmGlyph(kind: UmlStmGlyphKind, ctx: CanvasRenderingContext2D, box: GlyphBox): void;
21
+ /** Whether the node renderer should hand this kind to {@link paintStmGlyph}. */
22
+ export declare function isStmGlyphKind(kind: UmlNodeKind): kind is UmlStmGlyphKind;
@@ -0,0 +1,111 @@
1
+ import { UML_FONT_FAMILY } from '../consts.js';
2
+ import { cross, glyphText, hollowDisc, inkDisc } from './paint.js';
3
+ /**
4
+ * The bull inside a final state's ring, as a fraction of the outer radius.
5
+ *
6
+ * The activity final's own number, and deliberately the same one: §14.2.4 and
7
+ * §15.3.4 draw the identical bullseye, and a reader who sees two different
8
+ * proportions on two diagrams will look for a meaning in the difference.
9
+ * The ROLES keep them apart for the audit; the picture must not.
10
+ */
11
+ const BULLSEYE_BULL = 0.52;
12
+ /** The reach of an exit point's and a terminate's cross, against the mark. */
13
+ const CROSS_REACH = 0.66;
14
+ /**
15
+ * How big the `H` of a history pseudostate is drawn, as a fraction of the
16
+ * circle's radius.
17
+ *
18
+ * Sized off the mark rather than off a font constant, so a history dragged
19
+ * bigger carries a bigger letter — and sized to sit INSIDE the rim: `H*` is two
20
+ * characters wide, so the number is the one that keeps the deeper of the two
21
+ * from touching the circle it is written in.
22
+ */
23
+ const HISTORY_LETTER = 0.95;
24
+ /**
25
+ * Paint one state machine mark, in the element-local frame, with `fillStyle` /
26
+ * `strokeStyle` / `lineWidth` already set from the model.
27
+ */
28
+ export function paintStmGlyph(kind, ctx, box) {
29
+ const { x0, y0, x1, y1, bw, bh, w, h } = box;
30
+ const cx = w / 2;
31
+ const cy = h / 2;
32
+ const radius = Math.min(bw, bh) / 2;
33
+ switch (kind) {
34
+ // ── The final state: a bullseye (§14.2.4) ─────────────────────────────
35
+ // Drawn exactly as an activity final is, and a different metaclass all the
36
+ // same: this one is a vertex a transition lands on, that one stops every
37
+ // flow in an activity. The picture is shared; the role is not.
38
+ case 'final-state':
39
+ hollowDisc(ctx, cx, cy, radius);
40
+ inkDisc(ctx, cx, cy, radius * BULLSEYE_BULL);
41
+ return;
42
+ // ── The junction: a small filled dot (§14.2.4) ────────────────────────
43
+ // The smallest mark in the pack, and the plainest: it merely joins
44
+ // transitions, so it is a disc and nothing else. Told from an INITIAL — the
45
+ // same filled disc — by its size and by the edges running into it, which is
46
+ // exactly how §14.2.4 distinguishes them.
47
+ case 'junction':
48
+ inkDisc(ctx, cx, cy, radius);
49
+ return;
50
+ // ── The two histories: a circle with H, or with H* (§14.2.4) ──────────
51
+ // Shallow remembers which sub-state the machine was in; deep remembers the
52
+ // whole nesting, and the star is the entire difference on the page.
53
+ case 'shallow-history':
54
+ case 'deep-history':
55
+ hollowDisc(ctx, cx, cy, radius);
56
+ glyphText(ctx, kind === 'deep-history' ? 'H*' : 'H', cx, cy, radius * HISTORY_LETTER, UML_FONT_FAMILY);
57
+ return;
58
+ // ── The entry point: a small hollow circle (§14.2.4) ──────────────────
59
+ // Drawn ON the border of a composite state, saying "a transition may enter
60
+ // the sub-machine here". Hollow, and that is the whole of it: an entry
61
+ // point is a door, and a filled one would read as an initial node.
62
+ case 'entry-point':
63
+ hollowDisc(ctx, cx, cy, radius);
64
+ return;
65
+ // ── The exit point: the same circle with a cross in it (§14.2.4) ──────
66
+ case 'exit-point':
67
+ hollowDisc(ctx, cx, cy, radius);
68
+ cross(ctx, cx, cy, radius * CROSS_REACH);
69
+ return;
70
+ // ── The terminate: a bare X (§14.2.4) ─────────────────────────────────
71
+ // No circle, no box: §14.2.4 draws two crossed strokes and nothing else,
72
+ // and a rim round them would make it an exit point. The mark spans the
73
+ // whole element rather than a circle inscribed in it, so it reads at the
74
+ // small sizes a pseudostate is drawn at.
75
+ case 'terminate':
76
+ if (bw > 0 && bh > 0) {
77
+ // Corner to corner of the inset box, which is what "a bare X" is — and
78
+ // it stays an X at any aspect ratio, unlike a cross measured from one
79
+ // radius.
80
+ crossBox(ctx, x0, y0, x1, y1);
81
+ }
82
+ return;
83
+ }
84
+ /**
85
+ * Every kind this module claims is drawn above — `never` here only if the
86
+ * switch is exhaustive over {@link UmlStmGlyphKind}.
87
+ */
88
+ const unhandled = kind;
89
+ void unhandled;
90
+ }
91
+ /** The two diagonals of a rectangle, corner to corner. */
92
+ function crossBox(ctx, x0, y0, x1, y1) {
93
+ ctx.beginPath();
94
+ ctx.moveTo(x0, y0);
95
+ ctx.lineTo(x1, y1);
96
+ ctx.stroke();
97
+ ctx.beginPath();
98
+ ctx.moveTo(x1, y0);
99
+ ctx.lineTo(x0, y1);
100
+ ctx.stroke();
101
+ }
102
+ /** Whether the node renderer should hand this kind to {@link paintStmGlyph}. */
103
+ export function isStmGlyphKind(kind) {
104
+ return (kind === 'final-state' ||
105
+ kind === 'junction' ||
106
+ kind === 'shallow-history' ||
107
+ kind === 'deep-history' ||
108
+ kind === 'entry-point' ||
109
+ kind === 'exit-point' ||
110
+ kind === 'terminate');
111
+ }
@@ -0,0 +1,38 @@
1
+ import type { UmlTierWrap } from '../component.js';
2
+ /** The type face a canvas text element is painted in. */
3
+ export interface UmlTierFace {
4
+ fontFamily?: string;
5
+ fontSize?: number;
6
+ fontStyle?: string;
7
+ fontWeight?: string;
8
+ }
9
+ /**
10
+ * How the canvas renderer would break ONE line of this tier at `width`.
11
+ *
12
+ * The one DOM-aware measurement in the UML pack, and it is deliberately the
13
+ * renderer's own: `getFontString` builds the very string the text renderer sets
14
+ * on the canvas context and `wrapText` is the very function it wraps with
15
+ * (`gfx/text`'s `element-renderer`), so a compartment sized from this holds
16
+ * exactly the lines the reader ends up seeing. A re-implementation here — a
17
+ * word-break of our own, a line height of our own — would agree with the picture
18
+ * on the day it was written and drift on the first change to either.
19
+ *
20
+ * ## Why it is a function of the LINE and not of the tier
21
+ *
22
+ * Because that is the seam {@link umlTierLineCount} needs to stay pure: it owns
23
+ * "the author's newlines are lines too", this owns "and this one is painted as
24
+ * three". Splitting them is what lets the counting be tested against a fake
25
+ * wrapper in a `happy-dom` unit run, where `measureText` answers nothing useful.
26
+ *
27
+ * `width` is the COMPARTMENT's width — `w - 2 × inset` on the node — and not the
28
+ * tier element's current one: the text editor shrink-wraps the element it is
29
+ * mounted on while the author types, so the box on the model at commit time is
30
+ * the size of the words rather than of the compartment they are going back into.
31
+ *
32
+ * `undefined` for an element that states no face and for a width no text could
33
+ * be laid out in, and that is the honest answer rather than a guard: an element
34
+ * with no font is one nothing has painted, so there is no wrapping it HAS, and
35
+ * {@link umlTierLineCount} falls back to the author's own newlines — which is
36
+ * exactly what a pure fixture wants to be measured by.
37
+ */
38
+ export declare function umlTierWrapper(face: UmlTierFace, width: number): UmlTierWrap | undefined;
@@ -0,0 +1,45 @@
1
+ import { getFontString, wrapText } from '@formicoidea/labre-core/gfx/text';
2
+ /**
3
+ * How the canvas renderer would break ONE line of this tier at `width`.
4
+ *
5
+ * The one DOM-aware measurement in the UML pack, and it is deliberately the
6
+ * renderer's own: `getFontString` builds the very string the text renderer sets
7
+ * on the canvas context and `wrapText` is the very function it wraps with
8
+ * (`gfx/text`'s `element-renderer`), so a compartment sized from this holds
9
+ * exactly the lines the reader ends up seeing. A re-implementation here — a
10
+ * word-break of our own, a line height of our own — would agree with the picture
11
+ * on the day it was written and drift on the first change to either.
12
+ *
13
+ * ## Why it is a function of the LINE and not of the tier
14
+ *
15
+ * Because that is the seam {@link umlTierLineCount} needs to stay pure: it owns
16
+ * "the author's newlines are lines too", this owns "and this one is painted as
17
+ * three". Splitting them is what lets the counting be tested against a fake
18
+ * wrapper in a `happy-dom` unit run, where `measureText` answers nothing useful.
19
+ *
20
+ * `width` is the COMPARTMENT's width — `w - 2 × inset` on the node — and not the
21
+ * tier element's current one: the text editor shrink-wraps the element it is
22
+ * mounted on while the author types, so the box on the model at commit time is
23
+ * the size of the words rather than of the compartment they are going back into.
24
+ *
25
+ * `undefined` for an element that states no face and for a width no text could
26
+ * be laid out in, and that is the honest answer rather than a guard: an element
27
+ * with no font is one nothing has painted, so there is no wrapping it HAS, and
28
+ * {@link umlTierLineCount} falls back to the author's own newlines — which is
29
+ * exactly what a pure fixture wants to be measured by.
30
+ */
31
+ export function umlTierWrapper(face, width) {
32
+ const { fontFamily, fontSize, fontStyle, fontWeight } = face;
33
+ if (!fontFamily || typeof fontSize !== 'number' || !fontWeight) {
34
+ return undefined;
35
+ }
36
+ if (!(width > 0))
37
+ return undefined;
38
+ const font = getFontString({
39
+ fontFamily,
40
+ fontSize,
41
+ fontStyle: fontStyle ?? 'normal',
42
+ fontWeight,
43
+ });
44
+ return line => wrapText(line, font, width).split('\n').length;
45
+ }