@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
package/dist/morph.js
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { GroupElementModel, TextElementModel, UmlNodeElementModel, } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { umlCommandIcons, umlCommands } from './commands.js';
|
|
4
|
+
import { GUILLEMET_CLOSE, GUILLEMET_OPEN, UML_NAME_SEED } from './keywords.js';
|
|
5
|
+
import { umlFitComponent } from './node/fit.js';
|
|
6
|
+
import { umlMorphClears, umlMorphProps } from './presets.js';
|
|
7
|
+
import { UML_ROLE, UML_ROLE_OF_KIND } from './roles.js';
|
|
8
|
+
/**
|
|
9
|
+
* What a UML artefact may BECOME — the declaration behind the "Change type"
|
|
10
|
+
* dropdown on a selected component's contextual toolbar.
|
|
11
|
+
*
|
|
12
|
+
* ## Six families, and the kinds that are alone
|
|
13
|
+
*
|
|
14
|
+
* A class, an interface and an enumeration are THE SAME PICTURE: §9.5.4 draws
|
|
15
|
+
* one divided rectangle for all three and tells them apart by the keyword
|
|
16
|
+
* written above the name — `«interface»`, `«enumeration»`, nothing at all. That
|
|
17
|
+
* is the strongest statement of "the same artefact, said more precisely" any
|
|
18
|
+
* notation in this library makes.
|
|
19
|
+
*
|
|
20
|
+
* Phase 2 adds the second, and it is the same statement in a different clause:
|
|
21
|
+
* a **node**, a **device** and an **execution environment** are ONE 3D box
|
|
22
|
+
* (§19.4.4), told apart by `«device»` / `«executionEnvironment»` above the name
|
|
23
|
+
* and by nothing else. A deployment diagram is drawn coarse and then made
|
|
24
|
+
* precise — "this runs on a server", then "on that server's application
|
|
25
|
+
* container" — so the swap is the modelling act itself rather than a repair.
|
|
26
|
+
*
|
|
27
|
+
* Everything else is declared alone, and each singleton is a REFUSAL written
|
|
28
|
+
* down rather than an omission to be discovered later:
|
|
29
|
+
*
|
|
30
|
+
* - an **object** is an InstanceSpecification (§11.6), a thing that exists at
|
|
31
|
+
* run time. A class is a type. Offering the swap would invite a diagram that
|
|
32
|
+
* confuses the two levels, and its name compartment is `object : Class`
|
|
33
|
+
* underlined — different words as well as a different meaning;
|
|
34
|
+
* - a **package** is a namespace (§12.2), not a classifier in it;
|
|
35
|
+
* - a **note** is a Comment (Annex A) and carries no semantics at all;
|
|
36
|
+
* - an **actor** and a **use case** belong to a behaviour diagram (§18.1) and
|
|
37
|
+
* are not structural classifiers;
|
|
38
|
+
* - a **component** is a Class (§11.6) and would in principle join the first
|
|
39
|
+
* family, but it is the one classifier the notation draws with a MARK of its
|
|
40
|
+
* own — the two-tab icon in the corner — rather than with a keyword, and a
|
|
41
|
+
* component diagram is a different conversation from a class diagram. Left
|
|
42
|
+
* alone until a PO asks for the swap;
|
|
43
|
+
* - an **artifact** is a physical file (§19.3) — a `.war`, a script, a
|
|
44
|
+
* document. Offering to turn one into a class would invite a diagram that
|
|
45
|
+
* confuses what a thing IS with what it is deployed AS, which is the very
|
|
46
|
+
* distinction the `«manifest»` arrow exists to draw;
|
|
47
|
+
* - a **port** is a property of its owner (§11.3), not a classifier standing
|
|
48
|
+
* beside it, and it is the only artefact in the pack whose position is part
|
|
49
|
+
* of its meaning: it sits ON a border;
|
|
50
|
+
* - a **provided interface** and a **required interface** are the ball and the
|
|
51
|
+
* socket of §10.4.4 — two DIFFERENT drawings, unlike every family above, and
|
|
52
|
+
* a phase-2 pack should not decide by itself that a modeller who drew one
|
|
53
|
+
* meant the other. The pair is the obvious candidate for a third family and
|
|
54
|
+
* is deliberately left for a product decision.
|
|
55
|
+
*
|
|
56
|
+
* The declaration is therefore TOTAL over `UmlNodeKind` — every kind is named
|
|
57
|
+
* exactly once — which is what lets the unit suite prove that a kind added to
|
|
58
|
+
* the pack cannot be silently left out of this decision. The MENU is a different
|
|
59
|
+
* question, and {@link UML_MORPH_SPEC} answers it: a family of one has nothing
|
|
60
|
+
* to offer, so it is not offered.
|
|
61
|
+
*
|
|
62
|
+
* Families are DATA. Nothing derives them, and the role tree would have been the
|
|
63
|
+
* wrong source in the same way it is for BPMN and C4: `roleIsA` makes
|
|
64
|
+
* `uml:interface` a `uml:classifier` and thence reaches `uml:class`, but it says
|
|
65
|
+
* nothing about whether a reader would ACCEPT the swap — which is the only
|
|
66
|
+
* question this table answers.
|
|
67
|
+
*/
|
|
68
|
+
export const UML_MORPH_FAMILIES = [
|
|
69
|
+
// Declaration order is menu order, and the family opens on the plain class:
|
|
70
|
+
// an undecorated rectangle is the honest first draft, the keyword is the
|
|
71
|
+
// refinement — the same call `commands.ts` makes about the senior sub-menu.
|
|
72
|
+
['class', 'interface', 'enumeration'],
|
|
73
|
+
['object'],
|
|
74
|
+
['package'],
|
|
75
|
+
['note'],
|
|
76
|
+
['actor'],
|
|
77
|
+
['use-case'],
|
|
78
|
+
/* ── Phase 2: components and deployment ──────────────────────────────── */
|
|
79
|
+
// The second family, and it opens on the plain NODE for the same reason the
|
|
80
|
+
// first opens on the plain class: §19.4.4's keyword is the refinement.
|
|
81
|
+
['node', 'device', 'execution-environment'],
|
|
82
|
+
['component'],
|
|
83
|
+
['artifact'],
|
|
84
|
+
['port'],
|
|
85
|
+
['provided-interface'],
|
|
86
|
+
['required-interface'],
|
|
87
|
+
/* ── Phase 2: activities and state machines ──────────────────────────── */
|
|
88
|
+
// Four families, and every one of them is a PAIR the notation draws as one
|
|
89
|
+
// picture with one mark changed — which is the only test a family has to
|
|
90
|
+
// pass here, because a swap inside one must move nothing (the suite proves
|
|
91
|
+
// the footprint and the silhouette are shared).
|
|
92
|
+
//
|
|
93
|
+
// - §16.3.4 / §16.10.4: a send signal and an accept event are the SAME
|
|
94
|
+
// pentagon with its angled edge on the other side, and "did this step send
|
|
95
|
+
// the message or wait for it" is the single most common thing to get
|
|
96
|
+
// backwards on an activity diagram;
|
|
97
|
+
// - §15.3.4: an activity final ends the whole activity, a flow final ends
|
|
98
|
+
// ONE token — two circles, a bullseye and a cross, and the distinction is
|
|
99
|
+
// a refinement an author makes after the fact;
|
|
100
|
+
// - §14.2.4: a shallow history restores the region's last sub-state, a deep
|
|
101
|
+
// history restores the whole nested configuration — `H` and `H*`, one
|
|
102
|
+
// asterisk apart;
|
|
103
|
+
// - §14.2.4: an entry point and an exit point are the circle on a composite
|
|
104
|
+
// state's border, hollow or crossed.
|
|
105
|
+
['send-signal', 'accept-event'],
|
|
106
|
+
['activity-final', 'flow-final'],
|
|
107
|
+
['shallow-history', 'deep-history'],
|
|
108
|
+
['entry-point', 'exit-point'],
|
|
109
|
+
// The rest are declared alone, and each refusal is an argument:
|
|
110
|
+
//
|
|
111
|
+
// - an **action** is a rounded rectangle the shape layer draws (§15.2.4),
|
|
112
|
+
// where the two signal shapes are pentagons this pack paints itself: a
|
|
113
|
+
// family must be silhouette-preserving, and this pair is not. It is the
|
|
114
|
+
// obvious candidate for a fifth family the day the three share a drawing;
|
|
115
|
+
// - an **initial** node is the one control node with no counterpart — there
|
|
116
|
+
// is exactly one per region (§15.3.4), and the swap a modeller wants from
|
|
117
|
+
// it is a deletion;
|
|
118
|
+
// - a **decision** and a **fork** are a diamond and a bar. §15.3.4 draws
|
|
119
|
+
// them differently because they MEAN differently — one token down one
|
|
120
|
+
// branch, versus one token down every branch — and a dropdown between two
|
|
121
|
+
// different pictures would be this pack inventing an equivalence;
|
|
122
|
+
// - a **choice** is the decision's diamond on a state machine and a
|
|
123
|
+
// **junction** its filled dot (§14.2.4); neither belongs with the activity
|
|
124
|
+
// node it is drawn like, because they are never on the same sheet;
|
|
125
|
+
// - an **object node** (§15.4.4) is data, not a step; a **time event**
|
|
126
|
+
// (§16.10.4) is the hourglass and nothing else is drawn as one;
|
|
127
|
+
// - a **state** is the rounded box of §14.2.4 and a **final state** the
|
|
128
|
+
// bullseye that ends it — a named thing and an unnamed one;
|
|
129
|
+
// - a **terminate** pseudostate is the cross that kills the machine
|
|
130
|
+
// (§14.2.4): there is nothing it is a more precise version of.
|
|
131
|
+
['action'],
|
|
132
|
+
['initial'],
|
|
133
|
+
['decision'],
|
|
134
|
+
['fork'],
|
|
135
|
+
['object-node'],
|
|
136
|
+
['time-event'],
|
|
137
|
+
['state'],
|
|
138
|
+
['final-state'],
|
|
139
|
+
['choice'],
|
|
140
|
+
['junction'],
|
|
141
|
+
['terminate'],
|
|
142
|
+
/* ── Phase 3: the sequence vocabulary (§17.2.4) ──────────────────────── */
|
|
143
|
+
// Three more singletons, and the three refusals are the plainest in the
|
|
144
|
+
// table: these are not variations on one picture, they are the three
|
|
145
|
+
// different things an interaction is drawn with.
|
|
146
|
+
//
|
|
147
|
+
// - a **lifeline** is the participant itself — a head with a spine, and the
|
|
148
|
+
// only element on the sheet a message may attach to. There is nothing it
|
|
149
|
+
// is a more precise version of, and turning one into anything else would
|
|
150
|
+
// orphan every message drawn on it;
|
|
151
|
+
// - an **execution** is a bar ON a spine and a **destruction** is a cross on
|
|
152
|
+
// one. Both are marks, both are positioned by geometry, and what they mean
|
|
153
|
+
// is where they sit (§17.2.4) — "this participant is busy here" and "this
|
|
154
|
+
// participant ends here". A dropdown between them would be offering to
|
|
155
|
+
// turn a stretch of activity into an ending, which is not a refinement of
|
|
156
|
+
// anything: it is a different fact about a different moment.
|
|
157
|
+
['lifeline'],
|
|
158
|
+
['execution'],
|
|
159
|
+
['destruction'],
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* The families a MENU can be made of.
|
|
163
|
+
*
|
|
164
|
+
* A one-member family is a statement about the notation, not an affordance: a
|
|
165
|
+
* dropdown offering a package the choice of becoming a package is a control that
|
|
166
|
+
* cannot do anything, drawn on every package a user selects. The generic module
|
|
167
|
+
* already refuses a kind in NO family, and this gives it the same answer for a
|
|
168
|
+
* kind alone in one — so the table above stays total and readable while the
|
|
169
|
+
* toolbar stays honest.
|
|
170
|
+
*/
|
|
171
|
+
const OFFERED_FAMILIES = UML_MORPH_FAMILIES.filter(family => family.length > 1);
|
|
172
|
+
/**
|
|
173
|
+
* The creation command that draws each kind, keyed BY that kind.
|
|
174
|
+
*
|
|
175
|
+
* Derived from `telemetry.element`, which is where the kind is already written
|
|
176
|
+
* down (`node:interface`) and is documented as a historical value that must not
|
|
177
|
+
* be renamed. Deriving rather than restating is what stops a second table of
|
|
178
|
+
* labels and icons drifting from the one the sub-menu and the catalogue read.
|
|
179
|
+
*/
|
|
180
|
+
const NODE_COMMANDS = new Map(umlCommands.flatMap(command => {
|
|
181
|
+
const element = command.telemetry?.element;
|
|
182
|
+
return element?.startsWith('node:')
|
|
183
|
+
? [[element.slice('node:'.length), command]]
|
|
184
|
+
: [];
|
|
185
|
+
}));
|
|
186
|
+
/** A kind's wording: the creation command's own key and English. */
|
|
187
|
+
function labelOf(kind) {
|
|
188
|
+
const command = NODE_COMMANDS.get(kind);
|
|
189
|
+
return {
|
|
190
|
+
key: command?.labelKey,
|
|
191
|
+
fallback: command?.labelFallback ?? kind,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** A kind's icon: the creation command's own, reused rather than redrawn. */
|
|
195
|
+
function iconOf(kind) {
|
|
196
|
+
const iconKey = NODE_COMMANDS.get(kind)?.iconKey;
|
|
197
|
+
return (iconKey && umlCommandIcons[iconKey]) || umlCommandIcons['uml.class'];
|
|
198
|
+
}
|
|
199
|
+
/* ── Resolving the composite ───────────────────────────────────────────── */
|
|
200
|
+
/**
|
|
201
|
+
* The `umlNode` shape a selected GROUP is the component of — `undefined` when
|
|
202
|
+
* the group is not one.
|
|
203
|
+
*
|
|
204
|
+
* A UML artefact is a native `group` holding the shape and its tiers of words
|
|
205
|
+
* (`actions.ts`), so what a click selects is the group and what carries `kind`
|
|
206
|
+
* is the shape. This is the whole of the indirection, and it is also the gate: a
|
|
207
|
+
* plain group somebody lassoed round three rectangles, a C4 component (a group
|
|
208
|
+
* of a shape and three lines) and a group of two UML classifiers all answer
|
|
209
|
+
* `undefined`, so none of them is ever offered the menu.
|
|
210
|
+
*
|
|
211
|
+
* TWO shapes is a refusal rather than a first-wins pick. A group holding two
|
|
212
|
+
* classifiers is a group somebody made of two classifiers — morphing "it" would
|
|
213
|
+
* mean picking one of them by document order, and the honest answer to an
|
|
214
|
+
* ambiguous selection is the same one the generic module gives to a mixed
|
|
215
|
+
* family: nothing.
|
|
216
|
+
*
|
|
217
|
+
* ## Why this is not `component.ts`'s `umlNodeOfComponent`
|
|
218
|
+
*
|
|
219
|
+
* Because that one answers a different question with a different vocabulary: it
|
|
220
|
+
* takes a RESOLVED `UmlComponent` — the pure, Yjs-free shape the exporter
|
|
221
|
+
* and the renderer build out of ids and roles — and reads a field off it. This
|
|
222
|
+
* one takes a live model and walks `childElements`, because that is what the
|
|
223
|
+
* toolbar hands over and `instanceof` is what makes the refusals above real. The
|
|
224
|
+
* two are deliberately not merged: one is the pure half of the pack and must
|
|
225
|
+
* stay free of the model classes, and this one exists to touch them.
|
|
226
|
+
*/
|
|
227
|
+
export function umlNodeOfGroup(model) {
|
|
228
|
+
if (!(model instanceof GroupElementModel))
|
|
229
|
+
return undefined;
|
|
230
|
+
let found;
|
|
231
|
+
for (const child of model.childElements) {
|
|
232
|
+
if (!(child instanceof UmlNodeElementModel))
|
|
233
|
+
continue;
|
|
234
|
+
if (found)
|
|
235
|
+
return undefined;
|
|
236
|
+
found = child;
|
|
237
|
+
}
|
|
238
|
+
return found;
|
|
239
|
+
}
|
|
240
|
+
/** One tier of a component, by the role it carries. */
|
|
241
|
+
function tierOf(model, role) {
|
|
242
|
+
if (!(model instanceof GroupElementModel))
|
|
243
|
+
return undefined;
|
|
244
|
+
return model.childElements.find((child) => child instanceof TextElementModel && child.role === role);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Write one tier, in place, if the decision says to.
|
|
248
|
+
*
|
|
249
|
+
* `decide` reads the tier as it is STORED and answers with the whole text to
|
|
250
|
+
* write, or `null` for "this is the author's, leave it". The comparison that
|
|
251
|
+
* decides whether anything happens is against the TRIMMED text — the same string
|
|
252
|
+
* `decide` was given — because a decision made on `'Payments '` returns
|
|
253
|
+
* `'«interface»\nPayments'`, and comparing that against the untrimmed original
|
|
254
|
+
* would call a no-op a change and spend a transaction rewriting the padding
|
|
255
|
+
* away.
|
|
256
|
+
*
|
|
257
|
+
* In place, in one transaction, and inside the caller's `captureSync`: the
|
|
258
|
+
* `Y.Text` instance is what any bound editor holds, so it is mutated rather than
|
|
259
|
+
* replaced, and the rewrite is part of the same single ctrl+z as the kind that
|
|
260
|
+
* made it necessary.
|
|
261
|
+
*/
|
|
262
|
+
function rewriteTier(model, role, decide) {
|
|
263
|
+
const tier = tierOf(model, role);
|
|
264
|
+
if (!tier || tier.isLocked())
|
|
265
|
+
return;
|
|
266
|
+
const text = tier.text.toString().trim();
|
|
267
|
+
const next = decide(text);
|
|
268
|
+
if (next === null || next === text)
|
|
269
|
+
return;
|
|
270
|
+
tier.surface.store.transact(() => {
|
|
271
|
+
tier.text.delete(0, tier.text.length);
|
|
272
|
+
tier.text.insert(0, next);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/* ── The keyword line ──────────────────────────────────────────────────── */
|
|
276
|
+
/**
|
|
277
|
+
* The Annex C keyword a kind is ANNOUNCED by, or `undefined` for the kinds the
|
|
278
|
+
* notation announces by their silhouette.
|
|
279
|
+
*
|
|
280
|
+
* Derived from {@link UML_NAME_SEED} rather than restated, for the reason every
|
|
281
|
+
* derived table in this pack exists: the seed is what a fresh artefact actually
|
|
282
|
+
* says, and a second list of keywords would agree with it on the day it was
|
|
283
|
+
* written and drift on the first one that changed. A seed whose first line is a
|
|
284
|
+
* guillemet pair IS the keyword line — that is what `«interface»\nInterface`
|
|
285
|
+
* means — and a seed that has none declares a kind with none.
|
|
286
|
+
*/
|
|
287
|
+
const KEYWORD_OF_KIND = Object.fromEntries(Object.keys(UML_NAME_SEED).flatMap(kind => {
|
|
288
|
+
const first = UML_NAME_SEED[kind].split('\n')[0];
|
|
289
|
+
return first.startsWith(GUILLEMET_OPEN) && first.endsWith(GUILLEMET_CLOSE)
|
|
290
|
+
? [[kind, first]]
|
|
291
|
+
: [];
|
|
292
|
+
}));
|
|
293
|
+
/**
|
|
294
|
+
* The NAME line a fresh artefact of this kind carries — the seed minus its
|
|
295
|
+
* keyword line.
|
|
296
|
+
*
|
|
297
|
+
* `Class`, `Interface`, `Enumeration`, `object : Class`. This is the string a
|
|
298
|
+
* component NOBODY HAS NAMED still says, and the only name a morph is allowed to
|
|
299
|
+
* rewrite.
|
|
300
|
+
*
|
|
301
|
+
* ENGLISH, and knowingly so: since the creation sites resolve their seeds
|
|
302
|
+
* through the translation seam (`umlSeedKey`), an artefact placed in a
|
|
303
|
+
* translated host arrives carrying that host's word, which this table does not
|
|
304
|
+
* recognise. The morph then treats the name as the author's and leaves it —
|
|
305
|
+
* the timid half of the rule below, never the destructive one. Resolving it
|
|
306
|
+
* would mean handing `afterMorph` the `std` it is not given, which is the
|
|
307
|
+
* generic `MorphSpec`'s shape and not this pack's; C4's `c4MorphedName` carries
|
|
308
|
+
* exactly the same limitation, against the same seam.
|
|
309
|
+
*/
|
|
310
|
+
const SEEDED_NAME = Object.fromEntries(Object.keys(UML_NAME_SEED).map(kind => {
|
|
311
|
+
const lines = UML_NAME_SEED[kind].split('\n');
|
|
312
|
+
return [kind, KEYWORD_OF_KIND[kind] ? lines.slice(1).join('\n') : lines[0]];
|
|
313
|
+
}));
|
|
314
|
+
/**
|
|
315
|
+
* What a name compartment should say once its shape has morphed — or `null` when
|
|
316
|
+
* nothing about it is the notation's to change.
|
|
317
|
+
*
|
|
318
|
+
* ## Two rewrites, and the one rule they share
|
|
319
|
+
*
|
|
320
|
+
* The rule is the same for both and it is deliberately timid: rewrite ONLY what
|
|
321
|
+
* the notation itself wrote, and never what an author typed. A morph is not
|
|
322
|
+
* allowed to take away words somebody put on the picture.
|
|
323
|
+
*
|
|
324
|
+
* - **The keyword line.** `«interface»` is not a name, it is what makes the same
|
|
325
|
+
* rectangle mean a different metaclass (§9.5.4) — so a class morphed to an
|
|
326
|
+
* interface that kept an unmarked name compartment would be an interface drawn
|
|
327
|
+
* exactly as a class, which is to say not drawn at all. The SOURCE kind's own
|
|
328
|
+
* keyword is dropped and the target's is put in its place; a first line an
|
|
329
|
+
* author wrote themselves (`«service»`, `«entity»` — Annex C is explicit that
|
|
330
|
+
* not every word in guillemets is a keyword) is not the source kind's, so it
|
|
331
|
+
* survives and the new keyword is written above it.
|
|
332
|
+
* - **The name.** Placeholder→placeholder and nothing else: the name is
|
|
333
|
+
* rewritten only when it is EXACTLY the source kind's own seed, which is what
|
|
334
|
+
* a classifier nobody has named still carries. A class called `Payments` is
|
|
335
|
+
* called that whatever keyword it ends up wearing, which is the PO's "the
|
|
336
|
+
* label is intact" for every name that is real content.
|
|
337
|
+
*
|
|
338
|
+
* Pure and total over every string, because the input is a canvas text element
|
|
339
|
+
* somebody may have typed anything into.
|
|
340
|
+
*/
|
|
341
|
+
export function umlMorphedName(from, to, rawText) {
|
|
342
|
+
const text = (rawText ?? '').trim();
|
|
343
|
+
const lines = text.length ? text.split('\n') : [];
|
|
344
|
+
// The keyword line(s), if the SOURCE kind wrote one and it is still there.
|
|
345
|
+
// Plural for the same reason the target's loop below is: a document may
|
|
346
|
+
// arrive with the word already stacked.
|
|
347
|
+
const fromKeyword = KEYWORD_OF_KIND[from];
|
|
348
|
+
let body = lines;
|
|
349
|
+
while (fromKeyword && body[0]?.trim() === fromKeyword)
|
|
350
|
+
body = body.slice(1);
|
|
351
|
+
const toKeyword = KEYWORD_OF_KIND[to];
|
|
352
|
+
// …and the TARGET's, if it is ALREADY there. A keyword is written once: the
|
|
353
|
+
// line below puts `toKeyword` back on top, and without this a compartment that
|
|
354
|
+
// already opened with it would come out wearing it twice —
|
|
355
|
+
// `«interface»\n«interface»\nLigne`, then three, then four. The PO's recette of
|
|
356
|
+
// 14/09/2026 saw the stack; it is reachable whenever the shape's `kind` and
|
|
357
|
+
// its name compartment disagree, which an import, a paste from a tool that
|
|
358
|
+
// writes the keyword itself, or an author typing it by hand all produce.
|
|
359
|
+
//
|
|
360
|
+
// Only the target's own keyword is dropped. A first line the author wrote —
|
|
361
|
+
// `«service»`, `«entity»`, which Annex C is explicit are not keywords — is not
|
|
362
|
+
// one of ours to take away, so it survives and the new keyword goes above it.
|
|
363
|
+
// A LOOP, not one slice: a document that already stacked the keyword twice —
|
|
364
|
+
// an import, a paste from a tool that writes it itself, a morph run before
|
|
365
|
+
// this line existed — has two to take off, and taking one off left the other
|
|
366
|
+
// in place. The rewrite then came out identical to the text it started from
|
|
367
|
+
// and `umlMorphedName` answered `null`, so the stack was permanent.
|
|
368
|
+
while (toKeyword && body[0]?.trim() === toKeyword)
|
|
369
|
+
body = body.slice(1);
|
|
370
|
+
// The name, if it is still the source kind's own prompt.
|
|
371
|
+
const named = body.join('\n').trim() === SEEDED_NAME[from] ? [SEEDED_NAME[to]] : body;
|
|
372
|
+
const next = (toKeyword ? [toKeyword, ...named] : named).join('\n');
|
|
373
|
+
return next === text ? null : next;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Keep the component's own words saying what the shape now is — the `afterMorph`
|
|
377
|
+
* half of the UML spec.
|
|
378
|
+
*
|
|
379
|
+
* One tier, and it is the one the reader looks at: `uml:name`. The attributes
|
|
380
|
+
* and operations compartments are the author's whole content and say nothing
|
|
381
|
+
* about the metaclass, so nothing here touches them — an enumeration that used
|
|
382
|
+
* to be a class keeps every feature somebody wrote on it, which is exactly what
|
|
383
|
+
* makes the morph worth having.
|
|
384
|
+
*
|
|
385
|
+
* ## …and then re-fit the box, because the keyword is a LINE
|
|
386
|
+
*
|
|
387
|
+
* §9.5.4 writes `«interface»` ABOVE the name, so morphing a class called `Ligne`
|
|
388
|
+
* turns a one-line name compartment into a two-line one. Nothing used to re-lay
|
|
389
|
+
* the component for it — `UmlCompartmentWatcher` fires on an editing selection
|
|
390
|
+
* closing, and a morph opens no editor — so the keyword was written and the name
|
|
391
|
+
* under it was painted straight through the rule into the attributes
|
|
392
|
+
* compartment. That is the PO's "le titre va accumuler «interface» \n Class
|
|
393
|
+
* ligne" of 14/09/2026, measured: two lines needing 38 units in a 22.4-unit
|
|
394
|
+
* compartment, on a node that stayed 120 tall.
|
|
395
|
+
*
|
|
396
|
+
* So the same fit the watcher runs is run here, INSIDE the caller's checkpoint
|
|
397
|
+
* (`capture: false`): `applyMorph` has already opened one, and the re-layout
|
|
398
|
+
* belongs in the same single ctrl+z as the kind that made it necessary — which
|
|
399
|
+
* is exactly what {@link rewriteTier} promises about the words themselves.
|
|
400
|
+
*/
|
|
401
|
+
function rewriteName(model, from, to) {
|
|
402
|
+
rewriteTier(model, UML_ROLE.name, text => umlMorphedName(from, to, text));
|
|
403
|
+
const node = umlNodeOfGroup(model);
|
|
404
|
+
if (!node)
|
|
405
|
+
return;
|
|
406
|
+
umlFitComponent(node.surface, node.surface.store, node.id,
|
|
407
|
+
// The morph's own checkpoint, not a second one.
|
|
408
|
+
{ capture: false });
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* UML's morph declaration, handed to the generic `morphToolbarConfig`.
|
|
412
|
+
*
|
|
413
|
+
* `modelType` is the native `GroupElementModel` and not `UmlNodeElementModel`,
|
|
414
|
+
* because the group is what a click on a component selects. Everything that
|
|
415
|
+
* makes that safe is in {@link umlNodeOfGroup}: the toolbar's own homogeneity
|
|
416
|
+
* test only proves that every selected element is A group, and the resolution
|
|
417
|
+
* proves that every one of them is one of OURS.
|
|
418
|
+
*
|
|
419
|
+
* `propsOf` is the shipped creation builder minus `type` / `xywh` / `text`
|
|
420
|
+
* ({@link umlMorphProps}), so the toolbox and the morph cannot disagree about
|
|
421
|
+
* what an interface looks like; `clearOf` is empty on today's table and derived
|
|
422
|
+
* rather than assumed so (`presets.ts`).
|
|
423
|
+
*/
|
|
424
|
+
export const UML_MORPH_SPEC = {
|
|
425
|
+
framework: 'uml',
|
|
426
|
+
families: OFFERED_FAMILIES,
|
|
427
|
+
modelType: GroupElementModel,
|
|
428
|
+
resolveTarget: umlNodeOfGroup,
|
|
429
|
+
kindOf: (model) => model instanceof UmlNodeElementModel ? model.kind : undefined,
|
|
430
|
+
// ↓ everything from here down is SHARED with {@link UML_BARE_MORPH_SPEC}.
|
|
431
|
+
// The SHAPE's role, which is the only role a UML artefact has: the group
|
|
432
|
+
// carries none by design, and the tiers carry roles of their own (`uml:name`,
|
|
433
|
+
// `uml:attributes`, `uml:operations`) that say which COMPARTMENT they are and
|
|
434
|
+
// not what kind of box they belong to — so a morph never rewrites them.
|
|
435
|
+
roleOf: kind => UML_ROLE_OF_KIND[kind],
|
|
436
|
+
propsOf: umlMorphProps,
|
|
437
|
+
clearOf: umlMorphClears,
|
|
438
|
+
afterMorph: rewriteName,
|
|
439
|
+
labelOf,
|
|
440
|
+
iconOf,
|
|
441
|
+
label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* The SAME dropdown, on a UML shape that is not inside a group.
|
|
445
|
+
*
|
|
446
|
+
* ## Why a second spec exists at all
|
|
447
|
+
*
|
|
448
|
+
* Because phase 2 made the pack's own rule about composites untrue for a third
|
|
449
|
+
* of its artefacts. Until then every UML artefact was a `group` — a shape and
|
|
450
|
+
* the canvas texts grouped with it — so `modelType: GroupElementModel` was the
|
|
451
|
+
* whole selection filter. The behaviour vocabulary is not: §15.3.4 and §14.2.4
|
|
452
|
+
* name none of their routing marks, so a bullseye, a bar, a diamond and a
|
|
453
|
+
* crossed circle are created as the SHAPE alone (`actions.ts`), and what a
|
|
454
|
+
* click selects for them is a `umlNode`, never a group.
|
|
455
|
+
*
|
|
456
|
+
* `umlNodeOfGroup` refuses a bare shape by design — it is the gate that keeps a
|
|
457
|
+
* plain lasso and another framework's component out of the menu — so the group
|
|
458
|
+
* spec resolves nothing for them and three whole families (`activity-final` /
|
|
459
|
+
* `flow-final`, `shallow-history` / `deep-history`, `entry-point` /
|
|
460
|
+
* `exit-point`) had a dropdown nobody could ever open.
|
|
461
|
+
*
|
|
462
|
+
* ## Why not one spec with a widened `modelType`
|
|
463
|
+
*
|
|
464
|
+
* Because the selection filter is not the only thing that differs: the two
|
|
465
|
+
* specs are registered under two DIFFERENT toolbar flavours. A bare `umlNode`'s
|
|
466
|
+
* contextual row is `affine:surface:umlNode`'s (the widget derives the flavour
|
|
467
|
+
* from `model.type`), and a grouped artefact's is `affine:surface:group`'s.
|
|
468
|
+
* Widening `modelType` to the abstract base would leave the group registration
|
|
469
|
+
* answering for shapes it will never be handed, and would make its homogeneity
|
|
470
|
+
* gate — the thing that refuses a group holding two classifiers — apply to a
|
|
471
|
+
* selection it was not written about. Two registrations, two keys, one table.
|
|
472
|
+
*
|
|
473
|
+
* Everything that decides what the menu SAYS and what a pick WRITES is shared
|
|
474
|
+
* with {@link UML_MORPH_SPEC} by reference, so the two can never offer
|
|
475
|
+
* different families or write different props.
|
|
476
|
+
*
|
|
477
|
+
* `resolveTarget` is absent, which is identity: a bare mark is one element —
|
|
478
|
+
* what the user selects, what carries the kind and what the patch lands on are
|
|
479
|
+
* the same object, exactly as they are for a connector. `afterMorph` is absent
|
|
480
|
+
* too, and that is the notation's doing rather than an omission: `rewriteName`
|
|
481
|
+
* exists to keep a `uml:name` tier saying what the shape now is, and these
|
|
482
|
+
* kinds have no tier at all — there is nothing to rewrite, and reaching for one
|
|
483
|
+
* would be looking for words the specification says are not there.
|
|
484
|
+
*/
|
|
485
|
+
export const UML_BARE_MORPH_SPEC = {
|
|
486
|
+
framework: UML_MORPH_SPEC.framework,
|
|
487
|
+
families: UML_MORPH_SPEC.families,
|
|
488
|
+
modelType: UmlNodeElementModel,
|
|
489
|
+
kindOf: UML_MORPH_SPEC.kindOf,
|
|
490
|
+
roleOf: UML_MORPH_SPEC.roleOf,
|
|
491
|
+
propsOf: UML_MORPH_SPEC.propsOf,
|
|
492
|
+
clearOf: UML_MORPH_SPEC.clearOf,
|
|
493
|
+
labelOf: UML_MORPH_SPEC.labelOf,
|
|
494
|
+
iconOf: UML_MORPH_SPEC.iconOf,
|
|
495
|
+
label: UML_MORPH_SPEC.label,
|
|
496
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { UmlNodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
import type { GlyphBox } from './paint.js';
|
|
3
|
+
/**
|
|
4
|
+
* The ACTIVITY glyphs (§15.3.4 control nodes, §16.3.4 signals, §16.10.4 time
|
|
5
|
+
* events) — the marks an activity diagram is routed by.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `node-renderer.ts` because the behaviour families roughly
|
|
8
|
+
* doubled the pack's glyph count, and a 900-line renderer is a file nobody
|
|
9
|
+
* reads the middle of. The renderer still owns the branch — which kind gets
|
|
10
|
+
* which picture, and the `never` that proves every kind gets one — and this
|
|
11
|
+
* file owns the drawing.
|
|
12
|
+
*
|
|
13
|
+
* Three kinds of the activity family are NOT here, and that is the same
|
|
14
|
+
* distinction `presets.ts` draws: an `action` is the native rounded rect of
|
|
15
|
+
* §15.3.4, a `decision` the native diamond, an `object-node` the plain native
|
|
16
|
+
* rect. The shape layer draws all three, and a glyph painted over them would be
|
|
17
|
+
* inventing a notation.
|
|
18
|
+
*
|
|
19
|
+
* Every proportion below is a fraction of the glyph's own box, so a mark dragged
|
|
20
|
+
* to any size stays the mark it is — the rule the whole pack follows.
|
|
21
|
+
*/
|
|
22
|
+
/** The kinds this module draws. */
|
|
23
|
+
export type UmlActGlyphKind = 'initial' | 'activity-final' | 'flow-final' | 'fork' | 'send-signal' | 'accept-event' | 'time-event';
|
|
24
|
+
/**
|
|
25
|
+
* Paint one activity mark, in the element-local frame, with `fillStyle` /
|
|
26
|
+
* `strokeStyle` / `lineWidth` already set from the model.
|
|
27
|
+
*/
|
|
28
|
+
export declare function paintActGlyph(kind: UmlActGlyphKind, ctx: CanvasRenderingContext2D, box: GlyphBox): void;
|
|
29
|
+
/** Whether the node renderer should hand this kind to {@link paintActGlyph}. */
|
|
30
|
+
export declare function isActGlyphKind(kind: UmlNodeKind): kind is UmlActGlyphKind;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { UML_SIGNAL_POINT } from '../component.js';
|
|
2
|
+
import { cross, hollowDisc, inkDisc, solidPolygon, withInkFill, } from './paint.js';
|
|
3
|
+
/**
|
|
4
|
+
* The bull inside an activity final's ring (§15.3.4), as a fraction of the outer
|
|
5
|
+
* radius.
|
|
6
|
+
*
|
|
7
|
+
* A touch over half: §15.3.4's figure draws a solid disc with a clear moat round
|
|
8
|
+
* it, and a bull much larger than this closes the moat up at the sizes a control
|
|
9
|
+
* node is actually drawn at (28 units).
|
|
10
|
+
*/
|
|
11
|
+
const BULLSEYE_BULL = 0.52;
|
|
12
|
+
/**
|
|
13
|
+
* The reach of a flow final's cross (§15.3.4), as a fraction of the rim radius.
|
|
14
|
+
*
|
|
15
|
+
* Short of the rim rather than touching it: the X is drawn INSIDE the circle,
|
|
16
|
+
* and arms that met the rim would read as a circle quartered rather than as a
|
|
17
|
+
* circle with a cross in it.
|
|
18
|
+
*/
|
|
19
|
+
const FLOW_FINAL_CROSS = 0.62;
|
|
20
|
+
/**
|
|
21
|
+
* Paint one activity mark, in the element-local frame, with `fillStyle` /
|
|
22
|
+
* `strokeStyle` / `lineWidth` already set from the model.
|
|
23
|
+
*/
|
|
24
|
+
export function paintActGlyph(kind, ctx, box) {
|
|
25
|
+
const { x0, y0, x1, y1, bw, bh, w, h } = box;
|
|
26
|
+
const cx = w / 2;
|
|
27
|
+
const cy = h / 2;
|
|
28
|
+
// One radius from whichever dimension is the tighter, so every round mark
|
|
29
|
+
// stays ROUND at any aspect ratio the element is dragged to — the rule the
|
|
30
|
+
// actor's head and the interface ball already follow.
|
|
31
|
+
const radius = Math.min(bw, bh) / 2;
|
|
32
|
+
switch (kind) {
|
|
33
|
+
// ── The initial node: a filled disc (§15.3.4) ──────────────────────────
|
|
34
|
+
// Shared with the state machine family, which draws the identical disc and
|
|
35
|
+
// means the identical thing by it (§14.2.4) — one kind, one role, one
|
|
36
|
+
// picture, and the role def records the sharing.
|
|
37
|
+
case 'initial':
|
|
38
|
+
inkDisc(ctx, cx, cy, radius);
|
|
39
|
+
return;
|
|
40
|
+
// ── The activity final: a bullseye (§15.3.4) ──────────────────────────
|
|
41
|
+
// A ring with a solid bull in it. Drawn rim first so the bull lands ON the
|
|
42
|
+
// paper the rim encloses rather than under it.
|
|
43
|
+
case 'activity-final':
|
|
44
|
+
hollowDisc(ctx, cx, cy, radius);
|
|
45
|
+
inkDisc(ctx, cx, cy, radius * BULLSEYE_BULL);
|
|
46
|
+
return;
|
|
47
|
+
// ── The flow final: a circle with an X in it (§15.3.4) ────────────────
|
|
48
|
+
// The one final that stops a single flow rather than the whole activity,
|
|
49
|
+
// and the cross is the entire difference on the page.
|
|
50
|
+
case 'flow-final':
|
|
51
|
+
hollowDisc(ctx, cx, cy, radius);
|
|
52
|
+
cross(ctx, cx, cy, radius * FLOW_FINAL_CROSS);
|
|
53
|
+
return;
|
|
54
|
+
// ── The fork / join: a filled bar (§15.3.4) ───────────────────────────
|
|
55
|
+
// The WHOLE inset box, because the element IS the bar: its default
|
|
56
|
+
// footprint is 120 × 10 and dragging it longer is how an author fits more
|
|
57
|
+
// outgoing flows onto it. Ink-filled, since §15.3.4 draws a solid segment
|
|
58
|
+
// and a hollow one would read as a very flat box.
|
|
59
|
+
case 'fork':
|
|
60
|
+
if (bw > 0 && bh > 0) {
|
|
61
|
+
withInkFill(ctx, () => {
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.moveTo(x0, y0);
|
|
64
|
+
ctx.lineTo(x1, y0);
|
|
65
|
+
ctx.lineTo(x1, y1);
|
|
66
|
+
ctx.lineTo(x0, y1);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
ctx.fill();
|
|
69
|
+
ctx.stroke();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
// ── The send signal: a convex pentagon (§16.3.4) ──────────────────────
|
|
74
|
+
// A rectangle with its right edge pulled out to a POINT — an arrow head
|
|
75
|
+
// saying the signal leaves here. The point's depth is `UML_SIGNAL_POINT`,
|
|
76
|
+
// the same number `component.ts` pulls the label box back by, so the name
|
|
77
|
+
// can never run out through the tip.
|
|
78
|
+
case 'send-signal': {
|
|
79
|
+
const point = w * UML_SIGNAL_POINT;
|
|
80
|
+
solidPolygon(ctx, [
|
|
81
|
+
[x0, y0],
|
|
82
|
+
[Math.max(x0, x1 - point), y0],
|
|
83
|
+
[x1, cy],
|
|
84
|
+
[Math.max(x0, x1 - point), y1],
|
|
85
|
+
[x0, y1],
|
|
86
|
+
]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// ── The accept event: a concave pentagon (§16.3.4) ────────────────────
|
|
90
|
+
// The same rectangle with its left edge pushed IN to a notch — a socket
|
|
91
|
+
// saying the event arrives here. The mirror of the shape above, which is
|
|
92
|
+
// exactly how a reader tells a send from a receive at a glance.
|
|
93
|
+
case 'accept-event': {
|
|
94
|
+
const point = w * UML_SIGNAL_POINT;
|
|
95
|
+
solidPolygon(ctx, [
|
|
96
|
+
[x0, y0],
|
|
97
|
+
[x1, y0],
|
|
98
|
+
[x1, y1],
|
|
99
|
+
[x0, y1],
|
|
100
|
+
[Math.min(x1, x0 + point), cy],
|
|
101
|
+
]);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// ── The time event: an hourglass (§16.10.4) ───────────────────────────
|
|
105
|
+
// Two triangles meeting at a point in the middle of the box. Drawn as two
|
|
106
|
+
// closed paths rather than one crossed one, because a single bow-tie path
|
|
107
|
+
// fills by the even-odd rule on some canvases and by the non-zero rule on
|
|
108
|
+
// others — and an hourglass that is sometimes hollow is not a notation.
|
|
109
|
+
case 'time-event':
|
|
110
|
+
if (bw > 0 && bh > 0) {
|
|
111
|
+
solidPolygon(ctx, [
|
|
112
|
+
[x0, y0],
|
|
113
|
+
[x1, y0],
|
|
114
|
+
[cx, cy],
|
|
115
|
+
]);
|
|
116
|
+
solidPolygon(ctx, [
|
|
117
|
+
[cx, cy],
|
|
118
|
+
[x1, y1],
|
|
119
|
+
[x0, y1],
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every kind this module claims is drawn above. Narrowed to `never` only if
|
|
126
|
+
* the switch is exhaustive over {@link UmlActGlyphKind}, so a kind added to
|
|
127
|
+
* the union without a picture of its own stops the build rather than silently
|
|
128
|
+
* painting nothing.
|
|
129
|
+
*/
|
|
130
|
+
const unhandled = kind;
|
|
131
|
+
void unhandled;
|
|
132
|
+
}
|
|
133
|
+
/** Whether the node renderer should hand this kind to {@link paintActGlyph}. */
|
|
134
|
+
export function isActGlyphKind(kind) {
|
|
135
|
+
return (kind === 'initial' ||
|
|
136
|
+
kind === 'activity-final' ||
|
|
137
|
+
kind === 'flow-final' ||
|
|
138
|
+
kind === 'fork' ||
|
|
139
|
+
kind === 'send-signal' ||
|
|
140
|
+
kind === 'accept-event' ||
|
|
141
|
+
kind === 'time-event');
|
|
142
|
+
}
|