@formicoidea/labre-framework-bpmn 0.58.0 → 0.60.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.
@@ -1,16 +1,80 @@
1
- import { createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
3
  import { BPMN_POOL_BACKGROUND } from './background.js';
4
+ import { bpmnPoolLayout, cachePaintedBands } from './pool-hit.js';
3
5
  /**
4
- * Canvas renderer for the BPMN pool.
6
+ * The pool as the framework-background primitive paints it.
5
7
  *
6
8
  * There is no BPMN drawing code any more: the pool is an INSTANTIATION of the
7
9
  * framework-background primitive, configured by the `BPMN_POOL_BACKGROUND`
8
10
  * declaration. What used to be ninety lines of hand-traced rounded rectangle,
9
11
  * filled band and rotated name is now a declaration any other framework can
10
12
  * write for itself.
13
+ */
14
+ const paintPool = createFrameworkBackgroundRenderer(BPMN_POOL_BACKGROUND, {
15
+ // The model cannot measure text, so picking would select by the DECLARED
16
+ // band widths while a long name paints them wider. Cache what was painted
17
+ // on the model's `@local()` `paintedBands` (never stored), and only when
18
+ // it changed: a local write is an `elementUpdated`, which repaints. The
19
+ // view writes the same cache before any paint (`element-view.ts`).
20
+ onBands: (model, layout) => {
21
+ cachePaintedBands(model, layout);
22
+ },
23
+ });
24
+ /** Width of the focused lane's outline, in model units. */
25
+ const FOCUS_OUTLINE_WIDTH = 2;
26
+ /**
27
+ * Canvas renderer for the BPMN pool: the declared background, then the outline
28
+ * of the FOCUSED lane, if this editor has one (`BpmnLaneFocusHolder`, held by
29
+ * the pool's view and never stored).
30
+ *
31
+ * The outline is the editor talking about the drawing — "this is the lane the
32
+ * reorder commands will move" — not part of it, so it is the theme's accent
33
+ * (`--affine-primary-color`, resolved through the renderer) rather than a
34
+ * notation colour. It is painted only onto the editor's OWN canvases: the SVG
35
+ * and PNG exports paint through this same renderer into a context of their
36
+ * own, and a file must not carry an on-screen focus. A renderer with no editor
37
+ * behind it (a bare unit test) paints none either, and a host that defines no
38
+ * accent gets none.
39
+ *
40
+ * Painted in the transform the primitive left on the context — element-local
41
+ * units, rotated about the centre like the rest of the pool.
11
42
  *
12
43
  * Exported as a function as well as an extension because the fidelity suite
13
44
  * drives it directly with a canvas stub.
14
45
  */
15
- export const bpmnPool = createFrameworkBackgroundRenderer(BPMN_POOL_BACKGROUND);
46
+ export const bpmnPool = (model, ctx, matrix, renderer, rc, viewportBound) => {
47
+ paintPool(model, ctx, matrix, renderer, rc, viewportBound);
48
+ const canvas = renderer;
49
+ if (!canvas?.std)
50
+ return;
51
+ const onScreen = ctx.canvas === canvas.canvas ||
52
+ canvas.stackingCanvas.includes(ctx.canvas);
53
+ if (!onScreen)
54
+ return;
55
+ const holder = canvas.std
56
+ .getOptional(GfxControllerIdentifier)
57
+ ?.view?.get(model.id);
58
+ const id = holder?.focusedLaneId;
59
+ if (!id)
60
+ return;
61
+ const [, , w, h] = model.deserializedXYWH;
62
+ // The outline hugs the flow area as PAINTED, i.e. right of a participant
63
+ // band a long name may have widened.
64
+ const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h, bpmnPoolLayout(model).margin);
65
+ const prefix = BPMN_POOL_BACKGROUND.instanceZones?.idPrefix ?? 'lane';
66
+ const zone = backgroundInstanceZones(BPMN_POOL_BACKGROUND, model).find(candidate => candidate.id === `${prefix}:${id}`);
67
+ if (!zone)
68
+ return;
69
+ const accent = canvas.getPropertyValue('--affine-primary-color').trim();
70
+ if (!accent)
71
+ return;
72
+ const inset = FOCUS_OUTLINE_WIDTH / 2;
73
+ ctx.save();
74
+ ctx.strokeStyle = accent;
75
+ ctx.lineWidth = FOCUS_OUTLINE_WIDTH;
76
+ ctx.setLineDash([]);
77
+ ctx.strokeRect(plot.x0 + inset, plot.y0 + zone.rect.y * plot.height + inset, plot.width - FOCUS_OUTLINE_WIDTH, zone.rect.h * plot.height - FOCUS_OUTLINE_WIDTH);
78
+ ctx.restore();
79
+ };
16
80
  export const BpmnPoolRendererExtension = ElementRendererExtension(BPMN_POOL_BACKGROUND.type, bpmnPool);
@@ -2,6 +2,7 @@ import type { BpmnPoolElementModel } from '@formicoidea/labre-core/model';
2
2
  import type { EditorHost } from '@formicoidea/labre-core/std';
3
3
  import type { PointTestOptions } from '@formicoidea/labre-core/std/gfx';
4
4
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
5
+ import { type BpmnLaneFocusHolder } from './actions.js';
5
6
  /**
6
7
  * View for a BPMN pool. Three direct gestures live here:
7
8
  *
@@ -14,7 +15,11 @@ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
14
15
  * renderer paints the name in, rather than a second set of metrics that would
15
16
  * one day disagree with it;
16
17
  * - **drag on an internal lane boundary** moves the separator, taking from one
17
- * lane and giving to the other.
18
+ * lane and giving to the other;
19
+ * - **click in a lane's title band of a SELECTED pool** focuses that lane —
20
+ * the lane "Move lane up / down" and "Remove lane" act on. On-screen state
21
+ * only ({@link BpmnLaneFocusHolder}): never stored, outlined on the canvas
22
+ * by the renderer, dropped when the pool leaves the selection.
18
23
  *
19
24
  * Both renames are ZONED to their band (PO recette, 2026-08-26). The whole pool
20
25
  * used to open the participant editor, which was right while a pool had one
@@ -55,14 +60,18 @@ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
55
60
  * local point by `-model.rotate` about the element centre, at the one function
56
61
  * that has the element in hand (`_localPoint`), not a new declared field.
57
62
  */
58
- export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementModel> {
63
+ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementModel> implements BpmnLaneFocusHolder {
59
64
  static type: string;
60
65
  private _nameEditor;
66
+ private _focusedLaneId;
67
+ /** The focused lane's id — see {@link BpmnLaneFocusHolder}. */
68
+ get focusedLaneId(): string | null;
61
69
  /** The boundary the pointer is over, and the handlers armed for it. */
62
70
  private _armed;
63
71
  /** Everything a separator drag needs, frozen at `dragstart`. */
64
72
  private _drag;
65
73
  onCreated(): void;
74
+ private _refreshPaintedBands;
66
75
  onDestroyed(): void;
67
76
  /** Hand the cursor back on the way out; nothing here owns it for long. */
68
77
  private _leave;
@@ -90,9 +99,14 @@ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementMod
90
99
  * - the internal LANE SEPARATORS, twelve-unit strips in the middle of the
91
100
  * flow area, which arm the drag that moves them;
92
101
  * - the zoom-GROWN rename targets, which reach past the painted bands when
93
- * the board is small on screen so a fingertip can still land on one.
102
+ * the board is small on screen so a fingertip can still land on one;
103
+ * - the part of a title band a long name has WIDENED past its declared
104
+ * width (the name wraps onto more lines and the band grows with it,
105
+ * `backgroundBandWidths`). The model selects by the declared widths, since
106
+ * it cannot measure text; the rename double-click must still land on every
107
+ * line painted, so `pool-hit.ts` aims at the band as painted.
94
108
  *
95
- * Neither is a selection zone — a click in the flow area still goes to
109
+ * None is a selection zone — a click in the flow area still goes to
96
110
  * whatever the user dropped there, because picking asks the MODEL. This
97
111
  * override only decides where the pool's own gestures are heard, which is why
98
112
  * it is declared here, beside the code that draws and handles them, rather
@@ -115,6 +129,18 @@ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementMod
115
129
  private _onDragStart;
116
130
  private _onDragMove;
117
131
  private _onDragEnd;
132
+ /**
133
+ * A press in a lane's title band of a SELECTED pool focuses that lane; a
134
+ * press in the participant band clears the focus. Anywhere else leaves it.
135
+ *
136
+ * On `pointerdown`, which this view already listens to, and NOT on `click`:
137
+ * a view that registers a handler reports the event as handled, and a
138
+ * registered `click` would take click-to-select away from the default tool.
139
+ * Selected first, for the reason the separator drag gives: a first click on
140
+ * a pool is "work on this pool", not "pick this lane".
141
+ */
142
+ private _focusAt;
143
+ private _setFocus;
118
144
  /**
119
145
  * A double-click renames whatever TITLE BAND it landed in, and nothing
120
146
  * otherwise (PO recette, 2026-08-26).
@@ -1,8 +1,15 @@
1
- import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
1
+ import { EdgelessCRUDIdentifier, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ToolbarFlag, ToolbarRegistryIdentifier, } from '@formicoidea/labre-core/shared/services';
2
3
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
- import { bpmnLanesOf, renameBpmnLane } from './actions.js';
4
+ import { bpmnLanesOf, renameBpmnLane, } from './actions.js';
4
5
  import { POOL_LANE_MIN_HEIGHT } from './consts.js';
5
- import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolTargetAt, } from './pool-hit.js';
6
+ import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolMeasuredLayout, bpmnPoolTargetAt, cachePaintedBands, } from './pool-hit.js';
7
+ /**
8
+ * The props whose change moves a title band: the names it is sized by, and the
9
+ * box whose height a name wraps along. `paintedBands` itself is NOT here — the
10
+ * cache's own write must never re-enter the code that writes it.
11
+ */
12
+ const BAND_PROPS = new Set(['name', 'lanes', 'xywh']);
6
13
  /**
7
14
  * View for a BPMN pool. Three direct gestures live here:
8
15
  *
@@ -15,7 +22,11 @@ import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolTargetAt, } from './pool-hit
15
22
  * renderer paints the name in, rather than a second set of metrics that would
16
23
  * one day disagree with it;
17
24
  * - **drag on an internal lane boundary** moves the separator, taking from one
18
- * lane and giving to the other.
25
+ * lane and giving to the other;
26
+ * - **click in a lane's title band of a SELECTED pool** focuses that lane —
27
+ * the lane "Move lane up / down" and "Remove lane" act on. On-screen state
28
+ * only ({@link BpmnLaneFocusHolder}): never stored, outlined on the canvas
29
+ * by the renderer, dropped when the pool leaves the selection.
19
30
  *
20
31
  * Both renames are ZONED to their band (PO recette, 2026-08-26). The whole pool
21
32
  * used to open the participant editor, which was right while a pool had one
@@ -60,18 +71,49 @@ export class BpmnPoolView extends GfxElementModelView {
60
71
  constructor() {
61
72
  super(...arguments);
62
73
  this._nameEditor = null;
74
+ this._focusedLaneId = null;
63
75
  /** The boundary the pointer is over, and the handlers armed for it. */
64
76
  this._armed = null;
65
77
  /** Everything a separator drag needs, frozen at `dragstart`. */
66
78
  this._drag = null;
67
79
  }
68
80
  static { this.type = 'bpmnPool'; }
81
+ /** The focused lane's id — see {@link BpmnLaneFocusHolder}. */
82
+ get focusedLaneId() {
83
+ return this._focusedLaneId;
84
+ }
69
85
  onCreated() {
70
86
  super.onCreated();
87
+ // Picking asks the MODEL, which selects by `paintedBands`. The renderer
88
+ // writes it after each paint, but a hidden tab never paints and the first
89
+ // click can come before the first frame: the view measures it now, and
90
+ // again whenever a band can have moved, so picking never waits for a
91
+ // frame. One measure per update; the renderer's write after the next
92
+ // paint lands on the same numbers and writes nothing.
93
+ this._refreshPaintedBands();
94
+ this.disposable.add(this.model.propsUpdated.subscribe(({ key }) => {
95
+ if (BAND_PROPS.has(key))
96
+ this._refreshPaintedBands();
97
+ }));
71
98
  this.on('dblclick', e => this._onDblClick(e));
72
99
  this.on('pointermove', e => this._updateHover(e));
73
- this.on('pointerdown', e => this._updateHover(e));
100
+ this.on('pointerdown', e => {
101
+ this._updateHover(e);
102
+ this._focusAt(e);
103
+ });
74
104
  this.on('pointerleave', () => this._leave());
105
+ // The focus belongs to a pool the user is working on: once it leaves the
106
+ // selection, there is no lane to act on and nothing to outline.
107
+ this.disposable.add(this.gfx.selection.slots.updated.subscribe(() => {
108
+ if (this._focusedLaneId === null)
109
+ return;
110
+ if (this.gfx.selection.selectedIds.includes(this.model.id))
111
+ return;
112
+ this._setFocus(null);
113
+ }));
114
+ }
115
+ _refreshPaintedBands() {
116
+ cachePaintedBands(this.model, bpmnPoolMeasuredLayout(this.model));
75
117
  }
76
118
  onDestroyed() {
77
119
  this._closeEditor();
@@ -121,9 +163,14 @@ export class BpmnPoolView extends GfxElementModelView {
121
163
  * - the internal LANE SEPARATORS, twelve-unit strips in the middle of the
122
164
  * flow area, which arm the drag that moves them;
123
165
  * - the zoom-GROWN rename targets, which reach past the painted bands when
124
- * the board is small on screen so a fingertip can still land on one.
166
+ * the board is small on screen so a fingertip can still land on one;
167
+ * - the part of a title band a long name has WIDENED past its declared
168
+ * width (the name wraps onto more lines and the band grows with it,
169
+ * `backgroundBandWidths`). The model selects by the declared widths, since
170
+ * it cannot measure text; the rename double-click must still land on every
171
+ * line painted, so `pool-hit.ts` aims at the band as painted.
125
172
  *
126
- * Neither is a selection zone — a click in the flow area still goes to
173
+ * None is a selection zone — a click in the flow area still goes to
127
174
  * whatever the user dropped there, because picking asks the MODEL. This
128
175
  * override only decides where the pool's own gestures are heard, which is why
129
176
  * it is declared here, beside the code that draws and handles them, rather
@@ -255,6 +302,37 @@ export class BpmnPoolView extends GfxElementModelView {
255
302
  this.model.pop('lanes');
256
303
  this._disarm();
257
304
  }
305
+ /* ── Lane focus ────────────────────────────────────────────────────── */
306
+ /**
307
+ * A press in a lane's title band of a SELECTED pool focuses that lane; a
308
+ * press in the participant band clears the focus. Anywhere else leaves it.
309
+ *
310
+ * On `pointerdown`, which this view already listens to, and NOT on `click`:
311
+ * a view that registers a handler reports the event as handled, and a
312
+ * registered `click` would take click-to-select away from the default tool.
313
+ * Selected first, for the reason the separator drag gives: a first click on
314
+ * a pool is "work on this pool", not "pick this lane".
315
+ */
316
+ _focusAt(e) {
317
+ if (!this.gfx.selection.selectedIds.includes(this.model.id))
318
+ return;
319
+ const target = this._targetAt(this._localPoint(e));
320
+ if (target === null)
321
+ return;
322
+ const lane = target.kind === 'lane' ? bpmnLanesOf(this.model)[target.index] : null;
323
+ this._setFocus(lane?.id ?? null);
324
+ }
325
+ _setFocus(id) {
326
+ if (this._focusedLaneId === id)
327
+ return;
328
+ this._focusedLaneId = id;
329
+ // The outline is painted by the canvas renderer, and the toolbar decides
330
+ // whether to offer the reorder commands: both have to be told.
331
+ this.gfx.surfaceComponent?.refresh();
332
+ this.gfx.std
333
+ .getOptional(ToolbarRegistryIdentifier)
334
+ ?.flags.refresh(ToolbarFlag.Surface);
335
+ }
258
336
  /* ── In-place naming ───────────────────────────────────────────────── */
259
337
  /**
260
338
  * A double-click renames whatever TITLE BAND it landed in, and nothing
package/dist/import.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { InterchangeImportError, readXmlElement, } from '@formicoidea/labre-core/interchange';
2
- import { ConnectorMode, PointStyle, StrokeStyle, } from '@formicoidea/labre-core/model/headless';
3
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
4
- import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, bpmnLabelMode, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
3
+ import { bpmnLabelMode, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, } from './consts.js';
5
4
  import { BPMN_FORMAT_ID, BPMN_NS, BPMN_OWN_DECLARATIONS, BPMN_SCOPE, BPMN_XML_OF_KIND, escapeAttr, escapeText, } from './export.js';
6
- import { bpmnLabelBoxUnder, bpmnLabelProps, bpmnNodeProps } from './presets.js';
5
+ import { bpmnConnectorProps, bpmnLabelBoxUnder, bpmnLabelProps, bpmnNodeProps, } from './presets.js';
7
6
  import { BPMN_ROLE } from './roles.js';
8
7
  /**
9
8
  * The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
@@ -637,39 +636,18 @@ const SWEEP_STEP = 200;
637
636
  const SWEEP_COLUMNS = 4;
638
637
  /** The margin between a minted pool's plot and the work inside it. */
639
638
  const MINTED_POOL_PADDING = 40;
640
- /** The connector a role is drawn as — the styles the tools arm, one table. */
639
+ /**
640
+ * The connector a role is drawn as — the styles the tools arm, one table
641
+ * (`bpmnConnectorProps`). A role this reader does not know keeps its own name
642
+ * and takes the sequence flow's line.
643
+ */
641
644
  function connectorProps(role) {
642
- const base = { type: 'connector', role, mode: ConnectorMode.Orthogonal };
643
- if (role === BPMN_ROLE.messageFlow) {
644
- return {
645
- ...base,
646
- stroke: MESSAGE_STROKE,
647
- strokeWidth: MESSAGE_WIDTH,
648
- strokeStyle: StrokeStyle.Dash,
649
- frontEndpointStyle: PointStyle.Circle,
650
- rearEndpointStyle: PointStyle.Arrow,
651
- };
652
- }
653
- if (role === BPMN_ROLE.association) {
654
- return {
655
- ...base,
656
- stroke: ASSOCIATION_STROKE,
657
- strokeWidth: ASSOCIATION_WIDTH,
658
- strokeStyle: StrokeStyle.Dash,
659
- // No head at either end: an association claims no direction, and an
660
- // arrowhead would be the picture claiming one (`docs/adr/0010`).
661
- frontEndpointStyle: PointStyle.None,
662
- rearEndpointStyle: PointStyle.None,
663
- };
664
- }
665
- return {
666
- ...base,
667
- stroke: SEQUENCE_STROKE,
668
- strokeWidth: SEQUENCE_WIDTH,
669
- strokeStyle: StrokeStyle.Solid,
670
- frontEndpointStyle: PointStyle.None,
671
- rearEndpointStyle: PointStyle.Triangle,
672
- };
645
+ const edge = role === BPMN_ROLE.messageFlow
646
+ ? 'messageFlow'
647
+ : role === BPMN_ROLE.association
648
+ ? 'association'
649
+ : 'sequenceFlow';
650
+ return { ...bpmnConnectorProps(edge), role };
673
651
  }
674
652
  /**
675
653
  * The version of the format this file declares (ADR 0012, P2 as amended).
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { bpmnCommandIcons, bpmnCommands } from './commands.js';
5
5
  export { BPMN_FORMAT_ID, BPMN_NS, BPMN_XML_OF_KIND, type BpmnExportBoard, type BpmnExportOptions, type BpmnExportOutcome, type BpmnXmlMapping, exportBpmnXml, exportBpmnXmlWithWarnings, isNcName, toNcName, } from './export.js';
6
6
  export { BPMN_KIND_OF_XML, BPMN_QUARANTINE_REASON, importBpmnXml, } from './import.js';
7
7
  export { bpmnLabelFit, type BpmnLabelFit, bpmnMorphClears, bpmnMorphProps, bpmnNodeProps, type BpmnNodePreset, NODE_PRESETS, } from './presets.js';
8
+ export { BPMN_TOOLBOX, bpmnFlowElement, bpmnNodeElements, type BpmnNodeTexts, bpmnPoolElement, type BpmnPoolTexts, } from './toolbox.js';
8
9
  export { BPMN_MORPH_FAMILIES, BPMN_MORPH_SPEC } from './morph.js';
9
10
  export { BPMN_INTERCHANGE, BPMN_SVG_FORMAT, BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_EXTENSION, BPMN_XML_IMPORT, BPMN_XML_FORMAT, BPMN_XML_MIME, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
10
11
  export { bpmnLaneOf, bpmnPoolOf } from './facts.js';
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ export { BPMN_KIND_OF_XML, BPMN_QUARANTINE_REASON, importBpmnXml, } from './impo
30
30
  // geometry of its own — a file, a generator, another editor's board — fits their
31
31
  // labels the way the importer does, rather than reinventing the arithmetic.
32
32
  export { bpmnLabelFit, bpmnMorphClears, bpmnMorphProps, bpmnNodeProps, NODE_PRESETS, } from './presets.js';
33
+ // The toolbar as pure builders (`docs/adr/0045`), also on `./interchange`.
34
+ export { BPMN_TOOLBOX, bpmnFlowElement, bpmnNodeElements, bpmnPoolElement, } from './toolbox.js';
33
35
  // What an artefact may BECOME: the declared families, and the spec the generic
34
36
  // morph module is registered with. Data a host can read without an editor.
35
37
  export { BPMN_MORPH_FAMILIES, BPMN_MORPH_SPEC } from './morph.js';
@@ -72,3 +72,4 @@ export declare const BPMN_SVG_FORMAT: InterchangeFormat;
72
72
  * would be a re-render (P2).
73
73
  */
74
74
  export declare const BPMN_SVG_IMPORT: InterchangeImportCapability;
75
+ export { BPMN_TOOLBOX, bpmnFlowElement, bpmnNodeElements, type BpmnNodeTexts, bpmnPoolElement, type BpmnPoolTexts, } from './toolbox.js';
@@ -98,3 +98,6 @@ export const BPMN_SVG_IMPORT = {
98
98
  direction: 'import',
99
99
  run: parseSvgSketch,
100
100
  };
101
+ // The toolbar as pure builders (`docs/adr/0045`): what a host with no editor
102
+ // places to draw the very elements a user places from the toolbar.
103
+ export { BPMN_TOOLBOX, bpmnFlowElement, bpmnNodeElements, bpmnPoolElement, } from './toolbox.js';
@@ -1,4 +1,4 @@
1
- import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blocks/surface';
1
+ import { type BackgroundInset, type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blocks/surface';
2
2
  /**
3
3
  * Where a pointer is on a pool, in ELEMENT-LOCAL model units.
4
4
  *
@@ -9,8 +9,18 @@ import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blo
9
9
  *
10
10
  * Every box is derived from `backgroundInstanceZones` /
11
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.
12
+ * from and the audit reports from — at the band widths `backgroundBandWidths`
13
+ * resolves, the one resolver the renderer sizes the bands with. A long name
14
+ * WIDENS its band (it wraps onto more lines), and the rename target widens with
15
+ * it: a double-click on the second line of a participant name is a
16
+ * double-click on that name. Measured with `measureBackgroundText`, the same
17
+ * metrics the canvas paints with wherever a document exists. Nothing here
18
+ * restates a coordinate, so a target cannot drift away from the thing it is a
19
+ * target for.
20
+ *
21
+ * The model's own `includesPoint` (`affine-model`, headless, no text
22
+ * measurement) still selects by the DECLARED widths; the grown part of a band
23
+ * answers the pool's gestures through the view, which asks these functions.
14
24
  *
15
25
  * ponytail: a ROTATED pool is not accounted for — the caller converts by
16
26
  * subtraction, so every box assumes an upright pool. Same reserve
@@ -18,11 +28,60 @@ import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blo
18
28
  * framework background today. Upgrade: rotate the local point by
19
29
  * `-model.rotate` about the element centre, in the caller that has the element.
20
30
  */
21
- /** What these functions need of a pool: its box and its lanes. */
31
+ /**
32
+ * What these functions need of a pool: its box, its lanes and its name — the
33
+ * name and the lane names size the bands.
34
+ */
22
35
  export interface BpmnPoolGeometry {
23
36
  deserializedXYWH: readonly number[];
24
37
  lanes?: unknown;
38
+ name?: unknown;
39
+ /** What the renderer last painted — `BpmnPoolElementModel.paintedBands`. */
40
+ paintedBands?: {
41
+ margin: number;
42
+ zoneBandWidth: number;
43
+ };
25
44
  }
45
+ /**
46
+ * The pool's bands as painted: the margin and strip width the names need.
47
+ *
48
+ * The widths the renderer cached on the model when it has painted the pool —
49
+ * the very numbers the model's `includesPoint` selects by, so a rename target
50
+ * and a selection click agree to the unit; measured here otherwise (a pool not
51
+ * painted yet, a headless caller).
52
+ */
53
+ export declare function bpmnPoolLayout(model: BpmnPoolGeometry): {
54
+ margin: BackgroundInset;
55
+ zoneBandWidth: number | undefined;
56
+ };
57
+ /**
58
+ * The pool's bands MEASURED now, from its name and lanes, ignoring any cache:
59
+ * what the view writes into `paintedBands` before the first paint and after
60
+ * every change that moves a band.
61
+ */
62
+ export declare function bpmnPoolMeasuredLayout(model: BpmnPoolGeometry): {
63
+ margin: BackgroundInset;
64
+ zoneBandWidth: number | undefined;
65
+ };
66
+ /**
67
+ * Write the band widths into the model's `paintedBands` cache — the one place
68
+ * that does, for both writers: the renderer after each paint (the measured
69
+ * truth on the canvas) and the view on creation and on every change that moves
70
+ * a band, so picking never waits for a frame (a hidden tab never paints).
71
+ *
72
+ * Writes ONLY when a width changed and returns whether it wrote: a `@local()`
73
+ * write emits `elementUpdated`, which repaints, so writing an equal value on
74
+ * every paint would repaint forever.
75
+ */
76
+ export declare function cachePaintedBands(model: {
77
+ paintedBands?: {
78
+ margin: number;
79
+ zoneBandWidth: number;
80
+ };
81
+ }, layout: {
82
+ margin: BackgroundInset;
83
+ zoneBandWidth: number | undefined;
84
+ }): boolean;
26
85
  /** A lane band and, when the declaration asks for one, its title strip. */
27
86
  export interface BpmnLaneBand {
28
87
  top: number;
package/dist/pool-hit.js CHANGED
@@ -1,10 +1,59 @@
1
- import { backgroundInstanceZoneBand, backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundBandWidths, backgroundInstanceZoneBand, backgroundInstanceZones, backgroundPlot, measureBackgroundText, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { BPMN_POOL_BACKGROUND } from './background.js';
3
- import { POOL_LANE_GRAB } from './consts.js';
3
+ import { POOL_LANE_BAND_WIDTH, POOL_LANE_GRAB } from './consts.js';
4
+ /**
5
+ * The pool's bands as painted: the margin and strip width the names need.
6
+ *
7
+ * The widths the renderer cached on the model when it has painted the pool —
8
+ * the very numbers the model's `includesPoint` selects by, so a rename target
9
+ * and a selection click agree to the unit; measured here otherwise (a pool not
10
+ * painted yet, a headless caller).
11
+ */
12
+ export function bpmnPoolLayout(model) {
13
+ const declared = BPMN_POOL_BACKGROUND.geometry.margin;
14
+ const painted = model.paintedBands;
15
+ if (painted) {
16
+ return {
17
+ margin: { ...declared, left: painted.margin },
18
+ zoneBandWidth: painted.zoneBandWidth,
19
+ };
20
+ }
21
+ return bpmnPoolMeasuredLayout(model);
22
+ }
23
+ /**
24
+ * The pool's bands MEASURED now, from its name and lanes, ignoring any cache:
25
+ * what the view writes into `paintedBands` before the first paint and after
26
+ * every change that moves a band.
27
+ */
28
+ export function bpmnPoolMeasuredLayout(model) {
29
+ const [, , w, h] = model.deserializedXYWH;
30
+ return backgroundBandWidths(BPMN_POOL_BACKGROUND, model, w, h, measureBackgroundText);
31
+ }
32
+ /**
33
+ * Write the band widths into the model's `paintedBands` cache — the one place
34
+ * that does, for both writers: the renderer after each paint (the measured
35
+ * truth on the canvas) and the view on creation and on every change that moves
36
+ * a band, so picking never waits for a frame (a hidden tab never paints).
37
+ *
38
+ * Writes ONLY when a width changed and returns whether it wrote: a `@local()`
39
+ * write emits `elementUpdated`, which repaints, so writing an equal value on
40
+ * every paint would repaint forever.
41
+ */
42
+ export function cachePaintedBands(model, layout) {
43
+ const margin = layout.margin.left;
44
+ const zoneBandWidth = layout.zoneBandWidth ?? POOL_LANE_BAND_WIDTH;
45
+ const previous = model.paintedBands;
46
+ if (previous?.margin === margin && previous.zoneBandWidth === zoneBandWidth) {
47
+ return false;
48
+ }
49
+ model.paintedBands = { margin, zoneBandWidth };
50
+ return true;
51
+ }
4
52
  /** The lane bands of this pool, or `null` when it has no usable partition. */
5
53
  export function bpmnPoolBands(model) {
6
54
  const [, , w, h] = model.deserializedXYWH;
7
- const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h);
55
+ const layout = bpmnPoolLayout(model);
56
+ const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h, layout.margin);
8
57
  if (!(plot.width > 0) || !(plot.height > 0))
9
58
  return null;
10
59
  const zones = backgroundInstanceZones(BPMN_POOL_BACKGROUND, model);
@@ -15,7 +64,7 @@ export function bpmnPoolBands(model) {
15
64
  bands: zones.map(zone => ({
16
65
  top: plot.y0 + zone.rect.y * plot.height,
17
66
  height: zone.rect.h * plot.height,
18
- strip: backgroundInstanceZoneBand(BPMN_POOL_BACKGROUND, zone, plot),
67
+ strip: backgroundInstanceZoneBand(BPMN_POOL_BACKGROUND, zone, plot, layout.zoneBandWidth),
19
68
  })),
20
69
  };
21
70
  }
@@ -94,10 +143,10 @@ export function bpmnLaneTitleBandAt(model, local, zoom) {
94
143
  */
95
144
  export function bpmnInPoolTitleBand(model, local, zoom) {
96
145
  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);
146
+ // The band IS the margin — as wide as the wrapped name needs — clamped to a
147
+ // pool narrower than its own margin, the same degenerate case the renderer
148
+ // clamps.
149
+ const painted = Math.min(bpmnPoolLayout(model).margin.left, w);
101
150
  if (!(painted > 0))
102
151
  return false;
103
152
  // Grown rightwards, capped at twice the margin: past that lies the lane
package/dist/presets.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
2
2
  import { StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign } from '@formicoidea/labre-core/model/headless';
3
+ import { type BpmnEdgeKey } from './consts.js';
3
4
  /**
4
5
  * What a BPMN artefact is BORN as — the one description of a node's props, read
5
6
  * by every site that creates one.
@@ -174,6 +175,17 @@ export declare function bpmnPoolProps(box: {
174
175
  }): Record<string, unknown> & {
175
176
  type: string;
176
177
  };
178
+ /**
179
+ * One BPMN connecting object, as the props `surface.addElement` takes, ends
180
+ * left to the caller: the role, the orthogonal mode the tools arm the
181
+ * connector with, and the line {@link BPMN_EDGE_STYLE} says it looks like.
182
+ *
183
+ * The importer and the toolbox both write a flow through here, so a flow read
184
+ * out of a `.bpmn` file and one a host places are the line the tool draws.
185
+ */
186
+ export declare function bpmnConnectorProps(edge: BpmnEdgeKey): Record<string, unknown> & {
187
+ type: string;
188
+ };
177
189
  /**
178
190
  * What a node's kind is worth to an element that ALREADY EXISTS — the same
179
191
  * description as {@link bpmnNodeProps}, minus the three things a morph has no
package/dist/presets.js CHANGED
@@ -1,6 +1,6 @@
1
- import { FontFamily, SHAPE_TEXT_PADDING, SHAPE_TEXT_VERTICAL_PADDING, ShapeStyle, StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign, } from '@formicoidea/labre-core/model/headless';
1
+ import { ConnectorMode, FontFamily, SHAPE_TEXT_PADDING, SHAPE_TEXT_VERTICAL_PADDING, ShapeStyle, StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { ACTIVITY_FIT_REF, BPMN_LABEL_GAP, BPMN_LABEL_H, BPMN_LABEL_W, CALL_ACTIVITY_WIDTH, END_WIDTH, EVENT_END, EVENT_START, GROUP_RADIUS, GROUP_STROKE, INNER_FONT_SIZE, isBpmnActivityKind, LABEL_FONT_SIZE, LABEL_INSET_RATIO, LABEL_MIN_FONT_SIZE, NEUTRAL_STROKE, NODE_FILL, NODE_SIZE, NODE_STROKE_WIDTH, START_WIDTH, TASK_RADIUS, } from './consts.js';
3
+ import { ACTIVITY_FIT_REF, BPMN_EDGE_STYLE, BPMN_LABEL_GAP, BPMN_LABEL_H, BPMN_LABEL_W, CALL_ACTIVITY_WIDTH, END_WIDTH, EVENT_END, EVENT_START, GROUP_RADIUS, GROUP_STROKE, INNER_FONT_SIZE, isBpmnActivityKind, LABEL_FONT_SIZE, LABEL_INSET_RATIO, LABEL_MIN_FONT_SIZE, NEUTRAL_STROKE, NODE_FILL, NODE_SIZE, NODE_STROKE_WIDTH, START_WIDTH, TASK_RADIUS, } from './consts.js';
4
4
  import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
5
5
  /** Per-kind native shape + accent presets (style C). */
6
6
  export const NODE_PRESETS = {
@@ -320,6 +320,22 @@ export function bpmnPoolProps(box) {
320
320
  xywh: box.xywh,
321
321
  };
322
322
  }
323
+ /**
324
+ * One BPMN connecting object, as the props `surface.addElement` takes, ends
325
+ * left to the caller: the role, the orthogonal mode the tools arm the
326
+ * connector with, and the line {@link BPMN_EDGE_STYLE} says it looks like.
327
+ *
328
+ * The importer and the toolbox both write a flow through here, so a flow read
329
+ * out of a `.bpmn` file and one a host places are the line the tool draws.
330
+ */
331
+ export function bpmnConnectorProps(edge) {
332
+ return {
333
+ type: 'connector',
334
+ role: BPMN_ROLE[edge],
335
+ mode: ConnectorMode.Orthogonal,
336
+ ...BPMN_EDGE_STYLE[edge],
337
+ };
338
+ }
323
339
  /**
324
340
  * The box {@link bpmnMorphProps} hands {@link bpmnNodeProps} and then throws
325
341
  * away. Never written to a document: a morph keeps the geometry the element