@formicoidea/labre-framework-bpmn 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +202 -6
- package/dist/actions.js +421 -43
- package/dist/background.d.ts +2 -0
- package/dist/background.js +158 -0
- package/dist/commands.js +496 -5
- package/dist/consts.d.ts +157 -3
- package/dist/consts.js +192 -3
- package/dist/element-renderer.d.ts +10 -4
- package/dist/element-renderer.js +14 -55
- package/dist/element-view.d.ts +100 -8
- package/dist/element-view.js +249 -30
- package/dist/export.d.ts +277 -0
- package/dist/export.js +1802 -0
- package/dist/facts.d.ts +48 -0
- package/dist/facts.js +127 -0
- package/dist/import.d.ts +44 -0
- package/dist/import.js +1440 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +44 -0
- package/dist/interchange.d.ts +109 -0
- package/dist/interchange.js +191 -0
- package/dist/morph.d.ts +61 -0
- package/dist/morph.js +118 -0
- package/dist/node/node-renderer.d.ts +0 -9
- package/dist/node/node-renderer.js +294 -17
- package/dist/pool-hit.d.ts +98 -0
- package/dist/pool-hit.js +130 -0
- package/dist/presets.d.ts +114 -0
- package/dist/presets.js +232 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +189 -0
- package/dist/roles.d.ts +96 -0
- package/dist/roles.js +410 -0
- package/dist/rules.d.ts +199 -0
- package/dist/rules.js +1539 -0
- package/dist/templates/index.js +116 -9
- package/dist/toolbar/bpmn-senior-button.js +8 -2
- package/dist/toolbar/config.d.ts +27 -2
- package/dist/toolbar/config.js +86 -2
- package/dist/toolbar/icons.d.ts +67 -0
- package/dist/toolbar/icons.js +141 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/translations.d.ts +3 -1
- package/dist/translations.js +8 -3
- package/dist/view.d.ts +6 -2
- package/dist/view.js +68 -5
- package/package.json +2 -2
|
@@ -1,15 +1,70 @@
|
|
|
1
1
|
import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
3
|
-
import { DefaultTheme } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { DefaultTheme, } from '@formicoidea/labre-core/model';
|
|
4
4
|
/**
|
|
5
|
-
* Renderer for a BPMN flow-object node.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Renderer for a BPMN flow-object node.
|
|
6
|
+
*
|
|
7
|
+
* The shape body (ellipse / rounded rect / diamond) is drawn by REUSING the
|
|
8
|
+
* native shape renderer — so stroke width, colors, inner text and theme behave
|
|
9
|
+
* exactly like a native shape. On top of it, this file paints the MARKERS the
|
|
10
|
+
* notation asks for, all of them stroke-based, scale-aware and drawn in the
|
|
11
|
+
* node's own (editable) stroke colour:
|
|
12
|
+
*
|
|
13
|
+
* - events — envelope (message), clock (timer), solid disc (terminate);
|
|
14
|
+
* - tasks — a person (user) or a gear (service) in the top-left corner;
|
|
15
|
+
* - activity — the `+` box at the bottom edge (sub-process, call activity);
|
|
16
|
+
* - gateway — the X (exclusive) or the `+` (parallel);
|
|
17
|
+
* - data — folded page, cylinder, open bracket.
|
|
18
|
+
*
|
|
19
|
+
* The `group` is the one artefact of the profile this file does NOT touch: its
|
|
20
|
+
* dashed, rounded, unfilled rectangle is entirely a native shape's own doing.
|
|
21
|
+
*
|
|
22
|
+
* The last three are different in kind from the rest: their silhouette is not a
|
|
23
|
+
* native shape, so the glyph draws the BODY too — fill and outline — and the
|
|
24
|
+
* native rect underneath is created unfilled and unstroked (see `NODE_PRESETS`
|
|
25
|
+
* in `actions.ts`). It is still what carries the inner text, the selection
|
|
26
|
+
* bounds and the connector anchors.
|
|
27
|
+
*
|
|
28
|
+
* ## Simplifications against bpmn.io, deliberately
|
|
29
|
+
*
|
|
30
|
+
* - **Message start vs message end**: the spec fills the end event's envelope
|
|
31
|
+
* solid and leaves the start event's hollow. Both are drawn hollow here, and
|
|
32
|
+
* the distinction is carried by the ring weight the two already have — thin
|
|
33
|
+
* green for a start, thick red for an end — which is the louder signal of the
|
|
34
|
+
* two and the one that is legible zoomed out.
|
|
35
|
+
* - The **timer** has no hour ticks and the **data store** no shelf lines: at
|
|
36
|
+
* the sizes this canvas draws them, both read as noise around the shape.
|
|
10
37
|
*
|
|
11
38
|
* Mirrors the EDGY node renderer.
|
|
12
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* The artefacts BPMN draws BARE — a plain native shape with nothing on it.
|
|
42
|
+
* Everything else in the union is decorated here.
|
|
43
|
+
*
|
|
44
|
+
* `group` is bare for a different reason from the other three. They are
|
|
45
|
+
* undecorated because the notation puts no marker on them; the group has a
|
|
46
|
+
* distinctive look — a dashed, rounded, unfilled rectangle — and it is here
|
|
47
|
+
* because that look is expressible as a native shape's own properties
|
|
48
|
+
* (`strokeStyle: dash`, `radius`, `filled: false`). Drawing it by hand would
|
|
49
|
+
* have meant re-implementing dashes the shape renderer already does, and losing
|
|
50
|
+
* the editability that comes free with them.
|
|
51
|
+
*
|
|
52
|
+
* Written as the short list rather than the long one, so that the glyph kinds
|
|
53
|
+
* are DERIVED from the model's union instead of restated beside it: a kind
|
|
54
|
+
* added to `BpmnNodeKind` is a glyph kind by default, and the exhaustiveness
|
|
55
|
+
* check at the bottom of this file then refuses to compile until it is drawn.
|
|
56
|
+
* The alternative — a hand-maintained set of the thirteen decorated kinds — is
|
|
57
|
+
* the one per-kind table in this pack that could not be made compile-total, and
|
|
58
|
+
* a kind missing from it paints the WRONG picture rather than none.
|
|
59
|
+
*/
|
|
60
|
+
const UNDECORATED_KINDS = {
|
|
61
|
+
startEvent: true,
|
|
62
|
+
endEvent: true,
|
|
63
|
+
task: true,
|
|
64
|
+
group: true,
|
|
65
|
+
};
|
|
66
|
+
const isUndecorated = (kind) => Object.hasOwn(UNDECORATED_KINDS, kind);
|
|
67
|
+
const TAU = Math.PI * 2;
|
|
13
68
|
export const bpmnNode = (model, ctx, matrix, renderer, rc, bound) => {
|
|
14
69
|
const [, , w, h] = model.deserializedXYWH;
|
|
15
70
|
const cx = w / 2;
|
|
@@ -22,21 +77,243 @@ export const bpmnNode = (model, ctx, matrix, renderer, rc, bound) => {
|
|
|
22
77
|
.translateSelf(-cx, -cy);
|
|
23
78
|
// Native shape (fill / stroke / inner text / theme handled natively).
|
|
24
79
|
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
25
|
-
|
|
80
|
+
const kind = model.kind;
|
|
81
|
+
if (isUndecorated(kind))
|
|
26
82
|
return;
|
|
27
83
|
const color = renderer.getColorValue(model.strokeColor, DefaultTheme.shapeStrokeColor, true);
|
|
28
|
-
|
|
29
|
-
|
|
84
|
+
const strokeWidth = model.strokeWidth || 1;
|
|
85
|
+
/**
|
|
86
|
+
* The smaller half-extent, the unit every glyph is sized against — and the
|
|
87
|
+
* floor under every radius derived from it.
|
|
88
|
+
*
|
|
89
|
+
* An element can be dragged to nothing: the resize manager takes the absolute
|
|
90
|
+
* value of the dragged extents but sets no minimum size. `arc` and `ellipse`
|
|
91
|
+
* THROW on a negative radius (`IndexSizeError`) rather than clamping, and the
|
|
92
|
+
* surface render loop wraps no renderer in a `try`, so one such throw aborts
|
|
93
|
+
* the rest of the frame with an unbalanced save stack. Clamping at the source
|
|
94
|
+
* covers every arc in this file at once; the two radii that do not come from
|
|
95
|
+
* here — the data store's, which subtract the stroke first — are clamped
|
|
96
|
+
* where they are computed. Same guard, same reason, as the native ellipse
|
|
97
|
+
* renderer's (`gfx/shape/src/element-renderer/shape/ellipse.ts`), the one
|
|
98
|
+
* native shape that draws a real ellipse and the one that already does this.
|
|
99
|
+
*/
|
|
100
|
+
const unit = Math.max(0, Math.min(w, h));
|
|
30
101
|
ctx.setTransform(glyphMatrix);
|
|
31
|
-
ctx.translate(cx, cy);
|
|
32
102
|
ctx.strokeStyle = color;
|
|
33
|
-
ctx.lineWidth = Math.max(2, Math.min(w, h) * 0.06);
|
|
34
103
|
ctx.lineCap = 'round';
|
|
35
|
-
ctx.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
104
|
+
ctx.lineJoin = 'round';
|
|
105
|
+
// ── Gateways: X (exclusive) or + (parallel), centred on the diamond ──
|
|
106
|
+
if (kind === 'gatewayExclusive' || kind === 'gatewayParallel') {
|
|
107
|
+
const r = unit * 0.2;
|
|
108
|
+
ctx.translate(cx, cy);
|
|
109
|
+
ctx.lineWidth = Math.max(2, unit * 0.06);
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
if (kind === 'gatewayExclusive') {
|
|
112
|
+
ctx.moveTo(-r, -r);
|
|
113
|
+
ctx.lineTo(r, r);
|
|
114
|
+
ctx.moveTo(-r, r);
|
|
115
|
+
ctx.lineTo(r, -r);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
ctx.moveTo(-r, 0);
|
|
119
|
+
ctx.lineTo(r, 0);
|
|
120
|
+
ctx.moveTo(0, -r);
|
|
121
|
+
ctx.lineTo(0, r);
|
|
122
|
+
}
|
|
123
|
+
ctx.stroke();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// ── Events ───────────────────────────────────────────────────────────
|
|
127
|
+
if (kind === 'startEventMessage' || kind === 'endEventMessage') {
|
|
128
|
+
// Envelope: a rectangle a little under half the ring's diameter, with the
|
|
129
|
+
// flap folded down to just past its middle.
|
|
130
|
+
const ew = unit * 0.44;
|
|
131
|
+
const eh = ew * 0.7;
|
|
132
|
+
const x = cx - ew / 2;
|
|
133
|
+
const y = cy - eh / 2;
|
|
134
|
+
ctx.lineWidth = Math.max(1, unit * 0.04);
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.moveTo(x, y);
|
|
137
|
+
ctx.lineTo(x + ew, y);
|
|
138
|
+
ctx.lineTo(x + ew, y + eh);
|
|
139
|
+
ctx.lineTo(x, y + eh);
|
|
140
|
+
ctx.lineTo(x, y);
|
|
141
|
+
ctx.moveTo(x, y);
|
|
142
|
+
ctx.lineTo(cx, y + eh * 0.62);
|
|
143
|
+
ctx.lineTo(x + ew, y);
|
|
144
|
+
ctx.stroke();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (kind === 'startEventTimer') {
|
|
148
|
+
// Clock: a rim and two hands, at twelve and at four.
|
|
149
|
+
const r = unit * 0.24;
|
|
150
|
+
ctx.lineWidth = Math.max(1, unit * 0.04);
|
|
151
|
+
ctx.beginPath();
|
|
152
|
+
ctx.arc(cx, cy, r, 0, TAU);
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
ctx.beginPath();
|
|
155
|
+
ctx.moveTo(cx, cy);
|
|
156
|
+
ctx.lineTo(cx, cy - r * 0.72);
|
|
157
|
+
ctx.moveTo(cx, cy);
|
|
158
|
+
ctx.lineTo(cx + r * 0.52, cy + r * 0.38);
|
|
159
|
+
ctx.stroke();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (kind === 'endEventTerminate') {
|
|
163
|
+
// Terminate: a solid disc — the process stops here and nothing else runs.
|
|
164
|
+
ctx.fillStyle = color;
|
|
165
|
+
ctx.beginPath();
|
|
166
|
+
ctx.arc(cx, cy, unit * 0.28, 0, TAU);
|
|
167
|
+
ctx.fill();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// ── Activities ───────────────────────────────────────────────────────
|
|
171
|
+
if (kind === 'taskUser' || kind === 'taskService') {
|
|
172
|
+
// Both markers sit in the top-left corner, inside a square of the same
|
|
173
|
+
// side, so a row of tasks reads as a column of markers down the left.
|
|
174
|
+
const side = unit * 0.24;
|
|
175
|
+
const inset = unit * 0.1;
|
|
176
|
+
const ox = inset + side / 2;
|
|
177
|
+
const oy = inset + side / 2;
|
|
178
|
+
ctx.lineWidth = Math.max(1, unit * 0.028);
|
|
179
|
+
if (kind === 'taskUser') {
|
|
180
|
+
// A head over a pair of shoulders.
|
|
181
|
+
ctx.beginPath();
|
|
182
|
+
ctx.arc(ox, oy - side * 0.2, side * 0.2, 0, TAU);
|
|
183
|
+
ctx.stroke();
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.arc(ox, oy + side * 0.42, side * 0.36, Math.PI, TAU);
|
|
186
|
+
ctx.stroke();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// A gear: hub, body and eight teeth.
|
|
190
|
+
const rOuter = side * 0.5;
|
|
191
|
+
const rBody = rOuter * 0.72;
|
|
192
|
+
ctx.beginPath();
|
|
193
|
+
ctx.arc(ox, oy, rBody, 0, TAU);
|
|
194
|
+
ctx.stroke();
|
|
195
|
+
ctx.beginPath();
|
|
196
|
+
ctx.arc(ox, oy, rOuter * 0.28, 0, TAU);
|
|
197
|
+
ctx.stroke();
|
|
198
|
+
ctx.beginPath();
|
|
199
|
+
for (let i = 0; i < 8; i++) {
|
|
200
|
+
const a = (i * Math.PI) / 4;
|
|
201
|
+
ctx.moveTo(ox + Math.cos(a) * rBody, oy + Math.sin(a) * rBody);
|
|
202
|
+
ctx.lineTo(ox + Math.cos(a) * rOuter, oy + Math.sin(a) * rOuter);
|
|
203
|
+
}
|
|
204
|
+
ctx.stroke();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (kind === 'subProcess' || kind === 'callActivity') {
|
|
208
|
+
// The collapsed marker: a small boxed `+` on the bottom edge, saying
|
|
209
|
+
// "there is a whole process folded up in here". The call activity carries
|
|
210
|
+
// the SAME marker — what tells the two apart is its thick border, which is
|
|
211
|
+
// a creation-time preset rather than anything drawn here.
|
|
212
|
+
const side = unit * 0.2;
|
|
213
|
+
const bx = cx;
|
|
214
|
+
const by = h - unit * 0.1 - side / 2;
|
|
215
|
+
const half = side / 2;
|
|
216
|
+
const arm = side * 0.3;
|
|
217
|
+
ctx.lineWidth = Math.max(1, unit * 0.028);
|
|
218
|
+
ctx.beginPath();
|
|
219
|
+
ctx.moveTo(bx - half, by - half);
|
|
220
|
+
ctx.lineTo(bx + half, by - half);
|
|
221
|
+
ctx.lineTo(bx + half, by + half);
|
|
222
|
+
ctx.lineTo(bx - half, by + half);
|
|
223
|
+
ctx.lineTo(bx - half, by - half);
|
|
224
|
+
ctx.moveTo(bx - arm, by);
|
|
225
|
+
ctx.lineTo(bx + arm, by);
|
|
226
|
+
ctx.moveTo(bx, by - arm);
|
|
227
|
+
ctx.lineTo(bx, by + arm);
|
|
228
|
+
ctx.stroke();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// ── Data and artifacts: the glyph IS the body ────────────────────────
|
|
232
|
+
// The native rect under these three is unfilled and unstroked, so both the
|
|
233
|
+
// fill and the outline are drawn here — off the model's own colours, which
|
|
234
|
+
// keeps them editable from the shape toolbar like every other node's.
|
|
235
|
+
const fill = renderer.getColorValue(model.fillColor, DefaultTheme.shapeFillColor, true);
|
|
236
|
+
const half = strokeWidth / 2;
|
|
237
|
+
const x0 = half;
|
|
238
|
+
const y0 = half;
|
|
239
|
+
const x1 = w - half;
|
|
240
|
+
const y1 = h - half;
|
|
241
|
+
ctx.lineWidth = strokeWidth;
|
|
242
|
+
ctx.fillStyle = fill;
|
|
243
|
+
if (kind === 'dataObject') {
|
|
244
|
+
// A page with its top-right corner turned down.
|
|
245
|
+
const fold = Math.min(w, h) * 0.28;
|
|
246
|
+
ctx.beginPath();
|
|
247
|
+
ctx.moveTo(x0, y0);
|
|
248
|
+
ctx.lineTo(x1 - fold, y0);
|
|
249
|
+
ctx.lineTo(x1, y0 + fold);
|
|
250
|
+
ctx.lineTo(x1, y1);
|
|
251
|
+
ctx.lineTo(x0, y1);
|
|
252
|
+
ctx.lineTo(x0, y0);
|
|
253
|
+
ctx.fill();
|
|
254
|
+
ctx.stroke();
|
|
255
|
+
// The fold itself, drawn after the body so it is not painted over.
|
|
256
|
+
ctx.beginPath();
|
|
257
|
+
ctx.moveTo(x1 - fold, y0);
|
|
258
|
+
ctx.lineTo(x1 - fold, y0 + fold);
|
|
259
|
+
ctx.lineTo(x1, y0 + fold);
|
|
260
|
+
ctx.stroke();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (kind === 'dataStore') {
|
|
264
|
+
// A cylinder: an elliptical lid, two straight sides and a bulging floor.
|
|
265
|
+
// Clamped: both subtract the stroke width first, so both go NEGATIVE on an
|
|
266
|
+
// element dragged narrower (or shorter) than its own border — and a
|
|
267
|
+
// negative radius is the one thing `ellipse` throws on. See the note on
|
|
268
|
+
// `unit` above.
|
|
269
|
+
const rx = Math.max(0, (x1 - x0) / 2);
|
|
270
|
+
const ry = Math.max(0, (y1 - y0) * 0.16);
|
|
271
|
+
const mx = (x0 + x1) / 2;
|
|
272
|
+
const top = y0 + ry;
|
|
273
|
+
const bottom = y1 - ry;
|
|
274
|
+
ctx.beginPath();
|
|
275
|
+
ctx.moveTo(x0, top);
|
|
276
|
+
ctx.lineTo(x0, bottom);
|
|
277
|
+
// Floor, left to right through the lowest point.
|
|
278
|
+
ctx.ellipse(mx, bottom, rx, ry, 0, Math.PI, 0, true);
|
|
279
|
+
ctx.lineTo(x1, top);
|
|
280
|
+
// Back up the front of the lid, right to left.
|
|
281
|
+
ctx.ellipse(mx, top, rx, ry, 0, 0, Math.PI, false);
|
|
282
|
+
ctx.closePath();
|
|
283
|
+
ctx.fill();
|
|
284
|
+
ctx.stroke();
|
|
285
|
+
// The lid's own far edge, which the body path does not include.
|
|
286
|
+
ctx.beginPath();
|
|
287
|
+
ctx.ellipse(mx, top, rx, ry, 0, 0, TAU);
|
|
288
|
+
ctx.stroke();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (kind === 'textAnnotation') {
|
|
292
|
+
// An open bracket down the leading edge and nothing else — no fill, no
|
|
293
|
+
// closing edge. A note is attached to the picture, not framed in it, and
|
|
294
|
+
// the three missing sides are what say so.
|
|
295
|
+
const arm = Math.min(w * 0.18, h * 0.35);
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.moveTo(x0 + arm, y0);
|
|
298
|
+
ctx.lineTo(x0, y0);
|
|
299
|
+
ctx.lineTo(x0, y1);
|
|
300
|
+
ctx.lineTo(x0 + arm, y1);
|
|
301
|
+
ctx.stroke();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Every glyph kind is drawn above, and this is what keeps that true: `kind`
|
|
306
|
+
* is narrowed to `never` here only if the branches are exhaustive over
|
|
307
|
+
* {@link BpmnGlyphKind}, so a kind added to the model's union without a
|
|
308
|
+
* marker of its own stops the build.
|
|
309
|
+
*
|
|
310
|
+
* Which is the whole point of closing the last branch rather than letting it
|
|
311
|
+
* fall through. A renderer that silently paints an annotation bracket on
|
|
312
|
+
* somebody's new artefact is worse than one that does not paint it at all:
|
|
313
|
+
* the first is a wrong picture nobody is told about, the second is a missing
|
|
314
|
+
* one everybody can see.
|
|
315
|
+
*/
|
|
316
|
+
const unhandled = kind;
|
|
317
|
+
void unhandled;
|
|
41
318
|
};
|
|
42
319
|
export const BpmnNodeRendererExtension = ElementRendererExtension('bpmnNode', bpmnNode);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
/**
|
|
3
|
+
* Where a pointer is on a pool, in ELEMENT-LOCAL model units.
|
|
4
|
+
*
|
|
5
|
+
* Pure, and lifted out of `element-view.ts` so the answers can be asserted
|
|
6
|
+
* without an editor, a viewport or a canvas around them. The view converts the
|
|
7
|
+
* pointer and hands it here; everything about WHICH gesture a point means is
|
|
8
|
+
* decided in this file.
|
|
9
|
+
*
|
|
10
|
+
* Every box is derived from `backgroundInstanceZones` /
|
|
11
|
+
* `backgroundInstanceZoneBand` — the same two functions the renderer paints
|
|
12
|
+
* from and the audit reports from. Nothing here restates a coordinate, so a
|
|
13
|
+
* target cannot drift away from the thing it is a target for.
|
|
14
|
+
*
|
|
15
|
+
* ponytail: a ROTATED pool is not accounted for — the caller converts by
|
|
16
|
+
* subtraction, so every box assumes an upright pool. Same reserve
|
|
17
|
+
* `backgroundAxisFacts` documents, for the same reason: nothing rotates a
|
|
18
|
+
* framework background today. Upgrade: rotate the local point by
|
|
19
|
+
* `-model.rotate` about the element centre, in the caller that has the element.
|
|
20
|
+
*/
|
|
21
|
+
/** What these functions need of a pool: its box and its lanes. */
|
|
22
|
+
export interface BpmnPoolGeometry {
|
|
23
|
+
deserializedXYWH: readonly number[];
|
|
24
|
+
lanes?: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** A lane band and, when the declaration asks for one, its title strip. */
|
|
27
|
+
export interface BpmnLaneBand {
|
|
28
|
+
top: number;
|
|
29
|
+
height: number;
|
|
30
|
+
strip: BackgroundRect | null;
|
|
31
|
+
}
|
|
32
|
+
export interface BpmnPoolBands {
|
|
33
|
+
plot: ReturnType<typeof backgroundPlot>;
|
|
34
|
+
bands: BpmnLaneBand[];
|
|
35
|
+
}
|
|
36
|
+
/** The lane bands of this pool, or `null` when it has no usable partition. */
|
|
37
|
+
export declare function bpmnPoolBands(model: BpmnPoolGeometry): BpmnPoolBands | null;
|
|
38
|
+
/**
|
|
39
|
+
* A hit box's width, grown to stay reachable when the board is zoomed out.
|
|
40
|
+
*
|
|
41
|
+
* 44 view pixels is the touch-target floor, converted to model units so the box
|
|
42
|
+
* is at least a fingertip wide however far out the pool is drawn; the painted
|
|
43
|
+
* width wins once the pool is large enough on screen for it to. The growth is
|
|
44
|
+
* CAPPED, because a target that swallows the thing it sits next to is its own
|
|
45
|
+
* kind of broken — at 0.2 zoom an uncapped floor would make a lane's title band
|
|
46
|
+
* wider than the flow area it titles.
|
|
47
|
+
*/
|
|
48
|
+
export declare function bpmnReachable(painted: number, cap: number, zoom: number): number;
|
|
49
|
+
/**
|
|
50
|
+
* The INTERNAL lane boundary the point is on, as the index of the lane BELOW
|
|
51
|
+
* it — so `i` separates lane `i - 1` from lane `i`. `null` for anywhere else.
|
|
52
|
+
*
|
|
53
|
+
* Internal only: the outer edges belong to the plot, and dragging one would be
|
|
54
|
+
* a resize of the pool, which the handles already do.
|
|
55
|
+
*/
|
|
56
|
+
export declare function bpmnLaneBoundaryAt(model: BpmnPoolGeometry, local: readonly [number, number]): number | null;
|
|
57
|
+
/**
|
|
58
|
+
* The lane whose TITLE BAND the point is in, or `null`.
|
|
59
|
+
*
|
|
60
|
+
* The band, not a corner box (PO recette, 2026-08-26): a lane name is written
|
|
61
|
+
* down a strip at the lane's leading edge, so that strip is what you aim at to
|
|
62
|
+
* change it. Its whole height is live — the name is centred in it, and a band
|
|
63
|
+
* you may only click the top of would be a target that lies about where it is.
|
|
64
|
+
*/
|
|
65
|
+
export declare function bpmnLaneTitleBandAt(model: BpmnPoolGeometry, local: readonly [number, number], zoom: number): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the point is in the POOL's own title band — the left margin strip the
|
|
68
|
+
* participant name is written up.
|
|
69
|
+
*
|
|
70
|
+
* The whole pool used to open the participant editor. That was right while a
|
|
71
|
+
* pool held one name; with a name per lane it would mean a double-click in the
|
|
72
|
+
* middle of the flow area renames the participant, which is neither of the two
|
|
73
|
+
* things a user double-clicking there could have meant.
|
|
74
|
+
*/
|
|
75
|
+
export declare function bpmnInPoolTitleBand(model: BpmnPoolGeometry, local: readonly [number, number], zoom: number): boolean;
|
|
76
|
+
/** What a double-click at this point would rename. */
|
|
77
|
+
export type BpmnPoolTarget = {
|
|
78
|
+
kind: 'lane';
|
|
79
|
+
index: number;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'participant';
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* THE arbiter: which name, if any, a point on a pool is aiming at.
|
|
85
|
+
*
|
|
86
|
+
* The two title bands are adjacent, and both grow when the board is zoomed out
|
|
87
|
+
* (`bpmnReachable`), so on a small pool they OVERLAP — at zoom 1 a 560-unit
|
|
88
|
+
* pool already has a 28-unit participant band grown to 44, which reaches into
|
|
89
|
+
* the lane strip beside it. Something has to arbitrate, and it is this
|
|
90
|
+
* function rather than the view, so the answer can be asserted without an
|
|
91
|
+
* editor and so there is exactly one of it.
|
|
92
|
+
*
|
|
93
|
+
* The LANE wins the overlap. The strip is painted with the lane's name written
|
|
94
|
+
* down it, so it is the name a user is looking at when they aim there; handing
|
|
95
|
+
* their double-click to the participant instead would rename the one thing they
|
|
96
|
+
* demonstrably were not pointing at.
|
|
97
|
+
*/
|
|
98
|
+
export declare function bpmnPoolTargetAt(model: BpmnPoolGeometry, local: readonly [number, number], zoom: number): BpmnPoolTarget | null;
|
package/dist/pool-hit.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { backgroundInstanceZoneBand, backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { BPMN_POOL_BACKGROUND } from './background.js';
|
|
3
|
+
import { POOL_LANE_GRAB } from './consts.js';
|
|
4
|
+
/** The lane bands of this pool, or `null` when it has no usable partition. */
|
|
5
|
+
export function bpmnPoolBands(model) {
|
|
6
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
7
|
+
const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h);
|
|
8
|
+
if (!(plot.width > 0) || !(plot.height > 0))
|
|
9
|
+
return null;
|
|
10
|
+
const zones = backgroundInstanceZones(BPMN_POOL_BACKGROUND, model);
|
|
11
|
+
if (zones.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
plot,
|
|
15
|
+
bands: zones.map(zone => ({
|
|
16
|
+
top: plot.y0 + zone.rect.y * plot.height,
|
|
17
|
+
height: zone.rect.h * plot.height,
|
|
18
|
+
strip: backgroundInstanceZoneBand(BPMN_POOL_BACKGROUND, zone, plot),
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A hit box's width, grown to stay reachable when the board is zoomed out.
|
|
24
|
+
*
|
|
25
|
+
* 44 view pixels is the touch-target floor, converted to model units so the box
|
|
26
|
+
* is at least a fingertip wide however far out the pool is drawn; the painted
|
|
27
|
+
* width wins once the pool is large enough on screen for it to. The growth is
|
|
28
|
+
* CAPPED, because a target that swallows the thing it sits next to is its own
|
|
29
|
+
* kind of broken — at 0.2 zoom an uncapped floor would make a lane's title band
|
|
30
|
+
* wider than the flow area it titles.
|
|
31
|
+
*/
|
|
32
|
+
export function bpmnReachable(painted, cap, zoom) {
|
|
33
|
+
return Math.max(painted, Math.min(44 / (zoom || 1), cap));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The INTERNAL lane boundary the point is on, as the index of the lane BELOW
|
|
37
|
+
* it — so `i` separates lane `i - 1` from lane `i`. `null` for anywhere else.
|
|
38
|
+
*
|
|
39
|
+
* Internal only: the outer edges belong to the plot, and dragging one would be
|
|
40
|
+
* a resize of the pool, which the handles already do.
|
|
41
|
+
*/
|
|
42
|
+
export function bpmnLaneBoundaryAt(model, local) {
|
|
43
|
+
const geometry = bpmnPoolBands(model);
|
|
44
|
+
if (!geometry)
|
|
45
|
+
return null;
|
|
46
|
+
const { plot, bands } = geometry;
|
|
47
|
+
// The strip on the left is the participant's name, not the flow area: a
|
|
48
|
+
// separator does not run through it, so neither does its grab zone.
|
|
49
|
+
if (local[0] < plot.x0 || local[0] > plot.x1)
|
|
50
|
+
return null;
|
|
51
|
+
for (let i = 1; i < bands.length; i++) {
|
|
52
|
+
if (Math.abs(local[1] - bands[i].top) <= POOL_LANE_GRAB)
|
|
53
|
+
return i;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The lane whose TITLE BAND the point is in, or `null`.
|
|
59
|
+
*
|
|
60
|
+
* The band, not a corner box (PO recette, 2026-08-26): a lane name is written
|
|
61
|
+
* down a strip at the lane's leading edge, so that strip is what you aim at to
|
|
62
|
+
* change it. Its whole height is live — the name is centred in it, and a band
|
|
63
|
+
* you may only click the top of would be a target that lies about where it is.
|
|
64
|
+
*/
|
|
65
|
+
export function bpmnLaneTitleBandAt(model, local, zoom) {
|
|
66
|
+
const geometry = bpmnPoolBands(model);
|
|
67
|
+
if (!geometry)
|
|
68
|
+
return null;
|
|
69
|
+
const { plot, bands } = geometry;
|
|
70
|
+
for (let i = 0; i < bands.length; i++) {
|
|
71
|
+
const strip = bands[i].strip;
|
|
72
|
+
if (!strip)
|
|
73
|
+
continue;
|
|
74
|
+
// Grown across the strip only; along it, a lane is already as tall as it
|
|
75
|
+
// is. Capped at half the plot so the target never covers the flow area.
|
|
76
|
+
const width = bpmnReachable(strip.w, plot.width / 2, zoom);
|
|
77
|
+
if (local[0] >= strip.x &&
|
|
78
|
+
local[0] <= strip.x + width &&
|
|
79
|
+
local[1] >= strip.y &&
|
|
80
|
+
local[1] <= strip.y + strip.h) {
|
|
81
|
+
return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether the point is in the POOL's own title band — the left margin strip the
|
|
88
|
+
* participant name is written up.
|
|
89
|
+
*
|
|
90
|
+
* The whole pool used to open the participant editor. That was right while a
|
|
91
|
+
* pool held one name; with a name per lane it would mean a double-click in the
|
|
92
|
+
* middle of the flow area renames the participant, which is neither of the two
|
|
93
|
+
* things a user double-clicking there could have meant.
|
|
94
|
+
*/
|
|
95
|
+
export function bpmnInPoolTitleBand(model, local, zoom) {
|
|
96
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
97
|
+
const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h);
|
|
98
|
+
// The band IS the margin, clamped to a pool narrower than its own margin —
|
|
99
|
+
// the same degenerate case the renderer clamps.
|
|
100
|
+
const painted = Math.min(plot.x0, w);
|
|
101
|
+
if (!(painted > 0))
|
|
102
|
+
return false;
|
|
103
|
+
// Grown rightwards, capped at twice the margin: past that lies the lane
|
|
104
|
+
// strip, which has its own claim on those units — see `bpmnPoolTargetAt`.
|
|
105
|
+
const width = bpmnReachable(painted, painted * 2, zoom);
|
|
106
|
+
return local[0] >= 0 && local[0] <= width && local[1] >= 0 && local[1] <= h;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* THE arbiter: which name, if any, a point on a pool is aiming at.
|
|
110
|
+
*
|
|
111
|
+
* The two title bands are adjacent, and both grow when the board is zoomed out
|
|
112
|
+
* (`bpmnReachable`), so on a small pool they OVERLAP — at zoom 1 a 560-unit
|
|
113
|
+
* pool already has a 28-unit participant band grown to 44, which reaches into
|
|
114
|
+
* the lane strip beside it. Something has to arbitrate, and it is this
|
|
115
|
+
* function rather than the view, so the answer can be asserted without an
|
|
116
|
+
* editor and so there is exactly one of it.
|
|
117
|
+
*
|
|
118
|
+
* The LANE wins the overlap. The strip is painted with the lane's name written
|
|
119
|
+
* down it, so it is the name a user is looking at when they aim there; handing
|
|
120
|
+
* their double-click to the participant instead would rename the one thing they
|
|
121
|
+
* demonstrably were not pointing at.
|
|
122
|
+
*/
|
|
123
|
+
export function bpmnPoolTargetAt(model, local, zoom) {
|
|
124
|
+
const index = bpmnLaneTitleBandAt(model, local, zoom);
|
|
125
|
+
if (index !== null)
|
|
126
|
+
return { kind: 'lane', index };
|
|
127
|
+
if (bpmnInPoolTitleBand(model, local, zoom))
|
|
128
|
+
return { kind: 'participant' };
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { StrokeStyle, TextAlign, TextVerticalAlign } from '@formicoidea/labre-core/model';
|
|
3
|
+
/**
|
|
4
|
+
* What a BPMN artefact is BORN as — the one description of a node's props, read
|
|
5
|
+
* by every site that creates one.
|
|
6
|
+
*
|
|
7
|
+
* Lifted out of `actions.ts` when the importer arrived, and lifted rather than
|
|
8
|
+
* copied for the reason the copy would have failed: a task read out of a
|
|
9
|
+
* `.bpmn` file and a task drawn from the palette must be the same element in
|
|
10
|
+
* the document, down to the stroke width and the text fit mode. Two builders
|
|
11
|
+
* would agree on the day they were written and drift on the first restyle,
|
|
12
|
+
* and the drift would show up as an imported board that looks subtly unlike a
|
|
13
|
+
* drawn one — the kind of difference nobody reports and everybody notices.
|
|
14
|
+
*
|
|
15
|
+
* Pure data and one pure function: no `BlockStdScope`, no surface, no viewport.
|
|
16
|
+
* The creation gesture supplies the box, the importer supplies the box the file
|
|
17
|
+
* gave it, and neither knows anything the other does not.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The native shape and accent a kind is born with (style C).
|
|
21
|
+
*
|
|
22
|
+
* `glyphBody` is the one non-obvious field: the three data/artifact shapes have
|
|
23
|
+
* a silhouette a native rect cannot make — a folded page, a cylinder, an open
|
|
24
|
+
* bracket — so for those the renderer's glyph paints the BODY as well as the
|
|
25
|
+
* decoration, and the native shape is created unfilled and unstroked. It still
|
|
26
|
+
* earns its keep: it is what carries the inner text, the selection bounds, the
|
|
27
|
+
* resize handles and the connector anchors.
|
|
28
|
+
*/
|
|
29
|
+
export interface BpmnNodePreset {
|
|
30
|
+
shapeType: 'ellipse' | 'rect' | 'diamond';
|
|
31
|
+
stroke: string;
|
|
32
|
+
width: number;
|
|
33
|
+
/** Corner radius, `rect` only. Absent means a square corner. */
|
|
34
|
+
radius?: number;
|
|
35
|
+
/** Border style. Absent means a solid line, which is what BPMN mostly draws. */
|
|
36
|
+
strokeStyle?: StrokeStyle;
|
|
37
|
+
/**
|
|
38
|
+
* No fill — an OUTLINE, not a body. It also decides what the artefact does to
|
|
39
|
+
* a click: an unfilled shape is hit near its border and on its label only, so
|
|
40
|
+
* the group never steals a click from the work it encloses.
|
|
41
|
+
*/
|
|
42
|
+
hollow?: true;
|
|
43
|
+
/** The glyph draws the body; the native shape paints nothing. */
|
|
44
|
+
glyphBody?: true;
|
|
45
|
+
/**
|
|
46
|
+
* Where the inner text sits. Absent means centred, which is what every
|
|
47
|
+
* artefact whose label names the artefact itself wants. The group is the
|
|
48
|
+
* exception: its label names a region, so it goes in the corner of it.
|
|
49
|
+
*/
|
|
50
|
+
textAlign?: TextAlign;
|
|
51
|
+
textVerticalAlign?: TextVerticalAlign;
|
|
52
|
+
}
|
|
53
|
+
/** Per-kind native shape + accent presets (style C). */
|
|
54
|
+
export declare const NODE_PRESETS: Record<BpmnNodeKind, BpmnNodePreset>;
|
|
55
|
+
/**
|
|
56
|
+
* One BPMN node, as the props `surface.addElement` takes.
|
|
57
|
+
*
|
|
58
|
+
* The single description of what a `bpmnNode` IS in a document: the palette
|
|
59
|
+
* hands it a box centred on the viewport, the importer hands it the box the
|
|
60
|
+
* file's `dc:Bounds` gave, and the element that lands is the same either way.
|
|
61
|
+
*
|
|
62
|
+
* `text` is passed through unchanged, `undefined` included — an artefact whose
|
|
63
|
+
* label the source left empty gets no `text` key at all rather than an empty
|
|
64
|
+
* one, which is what keeps an imported node byte-comparable with a drawn one
|
|
65
|
+
* that was never typed into.
|
|
66
|
+
*/
|
|
67
|
+
export declare function bpmnNodeProps(kind: BpmnNodeKind, box: {
|
|
68
|
+
xywh: string;
|
|
69
|
+
text?: string;
|
|
70
|
+
}): Record<string, unknown> & {
|
|
71
|
+
type: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* What a node's kind is worth to an element that ALREADY EXISTS — the same
|
|
75
|
+
* description as {@link bpmnNodeProps}, minus the three things a morph has no
|
|
76
|
+
* business touching.
|
|
77
|
+
*
|
|
78
|
+
* Derived from the creation builder rather than restated beside it, and that is
|
|
79
|
+
* the whole point of the function.
|
|
80
|
+
*
|
|
81
|
+
* ## Why a `{kind, role}` patch is not enough
|
|
82
|
+
*
|
|
83
|
+
* Because the appearance of a BPMN artefact lives in props the CREATING kind's
|
|
84
|
+
* preset wrote, and nothing else ever rewrites them. One shipped morph pair
|
|
85
|
+
* shows it today: `subProcess` and `callActivity` are the same rounded
|
|
86
|
+
* rectangle and differ only in `strokeWidth` — 2 against 4 — and that thick
|
|
87
|
+
* border IS how a reader tells "this box stands for a process defined
|
|
88
|
+
* elsewhere" from "this box stands for one defined inline". Morph between them
|
|
89
|
+
* with two keys and the call activity arrives wearing the sub-process's thin
|
|
90
|
+
* border, which is a drawing that says the wrong thing.
|
|
91
|
+
*
|
|
92
|
+
* Every other family declared in `./morph.ts` currently shares one preset
|
|
93
|
+
* across its members, so for those the full patch changes nothing — and that is
|
|
94
|
+
* the second reason to write it this way rather than to trim it. A family is
|
|
95
|
+
* DATA (`BPMN_MORPH_FAMILIES`) and grows by declaration, with no code change to
|
|
96
|
+
* prompt anyone to ask whether the presets still agree; deriving the patch from
|
|
97
|
+
* the creation builder means the answer is right in advance. It is also what
|
|
98
|
+
* guarantees that a morphed artefact and one drawn fresh from the palette are
|
|
99
|
+
* the same element — two builders would agree the day they were written and
|
|
100
|
+
* drift on the first restyle, which is the argument this file already makes for
|
|
101
|
+
* having one creation builder at all.
|
|
102
|
+
*/
|
|
103
|
+
export declare function bpmnMorphProps(kind: BpmnNodeKind): Record<string, unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* The fields to DELETE from an element after morphing it to `kind` — the keys
|
|
106
|
+
* some other kind writes and this one does not.
|
|
107
|
+
*
|
|
108
|
+
* A patch cannot express absence. `textVerticalAlign` is spread conditionally
|
|
109
|
+
* (see {@link bpmnNodeProps}), so morphing away from the group would leave
|
|
110
|
+
* `Top` sitting in the Y.Map and silently in force over a preset that means
|
|
111
|
+
* "centred". `clearField` removes the key, which is the same call `writeLanes`
|
|
112
|
+
* makes when a pool loses its last lane.
|
|
113
|
+
*/
|
|
114
|
+
export declare function bpmnMorphClears(kind: BpmnNodeKind): readonly string[];
|