@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.
Files changed (47) hide show
  1. package/dist/actions.d.ts +202 -6
  2. package/dist/actions.js +421 -43
  3. package/dist/background.d.ts +2 -0
  4. package/dist/background.js +158 -0
  5. package/dist/commands.js +496 -5
  6. package/dist/consts.d.ts +157 -3
  7. package/dist/consts.js +192 -3
  8. package/dist/element-renderer.d.ts +10 -4
  9. package/dist/element-renderer.js +14 -55
  10. package/dist/element-view.d.ts +100 -8
  11. package/dist/element-view.js +249 -30
  12. package/dist/export.d.ts +277 -0
  13. package/dist/export.js +1802 -0
  14. package/dist/facts.d.ts +48 -0
  15. package/dist/facts.js +127 -0
  16. package/dist/import.d.ts +44 -0
  17. package/dist/import.js +1440 -0
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.js +44 -0
  20. package/dist/interchange.d.ts +109 -0
  21. package/dist/interchange.js +191 -0
  22. package/dist/morph.d.ts +61 -0
  23. package/dist/morph.js +118 -0
  24. package/dist/node/node-renderer.d.ts +0 -9
  25. package/dist/node/node-renderer.js +294 -17
  26. package/dist/pool-hit.d.ts +98 -0
  27. package/dist/pool-hit.js +130 -0
  28. package/dist/presets.d.ts +114 -0
  29. package/dist/presets.js +232 -0
  30. package/dist/profiles.d.ts +2 -0
  31. package/dist/profiles.js +189 -0
  32. package/dist/roles.d.ts +96 -0
  33. package/dist/roles.js +410 -0
  34. package/dist/rules.d.ts +199 -0
  35. package/dist/rules.js +1539 -0
  36. package/dist/templates/index.js +116 -9
  37. package/dist/toolbar/bpmn-senior-button.js +8 -2
  38. package/dist/toolbar/config.d.ts +27 -2
  39. package/dist/toolbar/config.js +86 -2
  40. package/dist/toolbar/icons.d.ts +67 -0
  41. package/dist/toolbar/icons.js +141 -0
  42. package/dist/toolbar/senior-tool.js +1 -0
  43. package/dist/translations.d.ts +3 -1
  44. package/dist/translations.js +8 -3
  45. package/dist/view.d.ts +6 -2
  46. package/dist/view.js +68 -5
  47. 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. The shape body (ellipse / rounded rect
6
- * / diamond) is drawn by REUSING the native shape renderer — so stroke width,
7
- * colors, inner text and theme behave exactly like a native shape. Only
8
- * `gatewayExclusive` is decorated: an X drawn on top in the node's (editable)
9
- * stroke color. Events and task are plain native shapes.
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
- if (model.kind !== 'gatewayExclusive')
80
+ const kind = model.kind;
81
+ if (isUndecorated(kind))
26
82
  return;
27
83
  const color = renderer.getColorValue(model.strokeColor, DefaultTheme.shapeStrokeColor, true);
28
- // ── Exclusive-gateway X, centred and sized to the diamond ───────────
29
- const r = Math.min(w, h) * 0.2;
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.beginPath();
36
- ctx.moveTo(-r, -r);
37
- ctx.lineTo(r, r);
38
- ctx.moveTo(-r, r);
39
- ctx.lineTo(r, -r);
40
- ctx.stroke();
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;
@@ -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[];