@formicoidea/labre-framework-bpmn 0.32.0 → 0.34.1
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 +427 -43
- package/dist/background.d.ts +2 -0
- package/dist/background.js +158 -0
- package/dist/commands-manifest.d.ts +18 -0
- package/dist/commands-manifest.js +226 -0
- package/dist/commands.js +496 -5
- package/dist/consts.d.ts +195 -4
- package/dist/consts.js +230 -4
- package/dist/element-renderer.d.ts +10 -4
- package/dist/element-renderer.js +14 -55
- package/dist/element-view.d.ts +119 -8
- package/dist/element-view.js +274 -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 +69 -0
- package/dist/import.js +1476 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +47 -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 +168 -0
- package/dist/presets.js +327 -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 +28 -2
- package/dist/toolbar/config.js +93 -4
- 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 +38 -3
- package/dist/view.d.ts +6 -2
- package/dist/view.js +68 -5
- package/package.json +6 -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
|
+
}
|