@formicoidea/labre-framework-uml 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +384 -0
- package/dist/actions.d.ts +430 -0
- package/dist/actions.js +724 -0
- package/dist/background.d.ts +153 -0
- package/dist/background.js +557 -0
- package/dist/board-hit.d.ts +137 -0
- package/dist/board-hit.js +191 -0
- package/dist/commands-manifest.d.ts +19 -0
- package/dist/commands-manifest.js +571 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +983 -0
- package/dist/component.d.ts +521 -0
- package/dist/component.js +708 -0
- package/dist/consts.d.ts +365 -0
- package/dist/consts.js +463 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/drawio-decode.d.ts +32 -0
- package/dist/drawio-decode.js +96 -0
- package/dist/drawio-import.d.ts +133 -0
- package/dist/drawio-import.js +885 -0
- package/dist/edge-morph.d.ts +154 -0
- package/dist/edge-morph.js +153 -0
- package/dist/edge-styles.d.ts +78 -0
- package/dist/edge-styles.js +210 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +64 -0
- package/dist/element-renderer.js +114 -0
- package/dist/element-view.d.ts +234 -0
- package/dist/element-view.js +430 -0
- package/dist/export.d.ts +61 -0
- package/dist/export.js +41 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +26 -0
- package/dist/frame-tag.d.ts +70 -0
- package/dist/frame-tag.js +78 -0
- package/dist/grammar.d.ts +695 -0
- package/dist/grammar.js +1436 -0
- package/dist/import.d.ts +306 -0
- package/dist/import.js +1270 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +67 -0
- package/dist/interchange.d.ts +125 -0
- package/dist/interchange.js +372 -0
- package/dist/keywords.d.ts +204 -0
- package/dist/keywords.js +361 -0
- package/dist/kinds.d.ts +73 -0
- package/dist/kinds.js +91 -0
- package/dist/legend.d.ts +30 -0
- package/dist/legend.js +255 -0
- package/dist/model.d.ts +862 -0
- package/dist/model.js +1316 -0
- package/dist/morph.d.ts +178 -0
- package/dist/morph.js +496 -0
- package/dist/node/act-glyphs.d.ts +30 -0
- package/dist/node/act-glyphs.js +142 -0
- package/dist/node/compartment-watcher.d.ts +107 -0
- package/dist/node/compartment-watcher.js +163 -0
- package/dist/node/fit.d.ts +85 -0
- package/dist/node/fit.js +129 -0
- package/dist/node/node-renderer.d.ts +49 -0
- package/dist/node/node-renderer.js +611 -0
- package/dist/node/node-view.d.ts +51 -0
- package/dist/node/node-view.js +89 -0
- package/dist/node/paint.d.ts +84 -0
- package/dist/node/paint.js +143 -0
- package/dist/node/sd-glyphs.d.ts +36 -0
- package/dist/node/sd-glyphs.js +79 -0
- package/dist/node/stm-glyphs.d.ts +22 -0
- package/dist/node/stm-glyphs.js +111 -0
- package/dist/node/tier-metrics.d.ts +38 -0
- package/dist/node/tier-metrics.js +45 -0
- package/dist/operators.d.ts +45 -0
- package/dist/operators.js +75 -0
- package/dist/plantuml-import.d.ts +113 -0
- package/dist/plantuml-import.js +1723 -0
- package/dist/plantuml.d.ts +94 -0
- package/dist/plantuml.js +985 -0
- package/dist/presets.d.ts +149 -0
- package/dist/presets.js +302 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +358 -0
- package/dist/reading.d.ts +160 -0
- package/dist/reading.js +648 -0
- package/dist/roles.d.ts +257 -0
- package/dist/roles.js +997 -0
- package/dist/rules.d.ts +267 -0
- package/dist/rules.js +3090 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +197 -0
- package/dist/toolbar/config.d.ts +228 -0
- package/dist/toolbar/config.js +833 -0
- package/dist/toolbar/icons.d.ts +440 -0
- package/dist/toolbar/icons.js +711 -0
- package/dist/toolbar/senior-tool.d.ts +11 -0
- package/dist/toolbar/senior-tool.js +21 -0
- package/dist/toolbar/uml-menu.d.ts +17 -0
- package/dist/toolbar/uml-menu.js +20 -0
- package/dist/toolbar/uml-senior-button.d.ts +18 -0
- package/dist/toolbar/uml-senior-button.js +22 -0
- package/dist/translations.d.ts +27 -0
- package/dist/translations.js +117 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +236 -0
- package/dist/xmi-import.d.ts +174 -0
- package/dist/xmi-import.js +2268 -0
- package/dist/xmi.d.ts +61 -0
- package/dist/xmi.js +1860 -0
- package/dist/xml-reader.d.ts +157 -0
- package/dist/xml-reader.js +430 -0
- package/dist/xml.d.ts +72 -0
- package/dist/xml.js +96 -0
- package/package.json +38 -0
|
@@ -0,0 +1,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
|
+
}
|