@formicoidea/labre-framework-bpmn 0.37.0 → 0.38.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.js CHANGED
@@ -8,7 +8,7 @@ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
8
8
  import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_LABEL, nodeLabelKey, NODE_SIZE, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
9
9
  import { BPMN_FORMAT_ID } from './export.js';
10
10
  import { BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_FORMAT, BPMN_XML_IMPORT, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
11
- import { bpmnNodeProps } from './presets.js';
11
+ import { bpmnNodeProps, bpmnPoolProps } from './presets.js';
12
12
  import { BPMN_ROLE } from './roles.js';
13
13
  /**
14
14
  * Standalone creation/activation actions for the BPMN toolbox — lifted out of
@@ -56,13 +56,11 @@ export function createBpmnPool(std) {
56
56
  const w = 560;
57
57
  const h = 200;
58
58
  const { centerX: cx, centerY: cy } = gfx.viewport;
59
- const id = surface.addElement({
60
- type: 'bpmnPool',
61
- // The FRAME the flow objects are drawn in, and a role of its own: a rule
62
- // written on the artefacts must never fall on the lane that holds them.
63
- role: BPMN_ROLE.pool,
64
- xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
65
- });
59
+ // What a pool IS lives in one place (`./presets.ts`), beside what a node is
60
+ // and for the same reason: the shipped cards build their participants from it
61
+ // too, so a pool laid out by a template and one dropped from the toolbox
62
+ // cannot say different things. The gesture owns the BOX and nothing else.
63
+ const id = surface.addElement(bpmnPoolProps({ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize() }));
66
64
  finish(gfx, id);
67
65
  }
68
66
  /**
package/dist/export.d.ts CHANGED
@@ -213,7 +213,7 @@ export declare const BPMN_SCOPE: {
213
213
  * decides the FILENAME and nothing else.
214
214
  */
215
215
  export interface BpmnExportBoard {
216
- /** In document order — which is the tie-break `bpmnPoolOf` breaks on. */
216
+ /** In document order; `bpmnPoolOf` breaks a tie on the smaller id, not on it. */
217
217
  pools: readonly BpmnPoolElementModel[];
218
218
  nodes: readonly BpmnNodeElementModel[];
219
219
  connectors: readonly ConnectorElementModel[];
@@ -258,10 +258,11 @@ export interface BpmnExportOutcome {
258
258
  * process; with a collaboration it must point at the collaboration, or most
259
259
  * tools draw the flow and none of the pools (spec §12.3.2).
260
260
  *
261
- * Attribution is {@link bpmnPoolOf} and {@link bpmnLaneOf} — the CENTRE against
262
- * the pool's PLOT, containment only, no nearest-pool fallback. Deliberately the
263
- * same arithmetic the audit and the validation rules read, so a task the audit
264
- * reports in "Back office" is in the `lane` named "Back office" here.
261
+ * Attribution is {@link bpmnPoolOf} and {@link bpmnLaneOf} — WHOLE containment
262
+ * in the pool, then the CENTRE against the pool's plot for the lane, no
263
+ * nearest-pool fallback. Deliberately the same arithmetic the audit and the
264
+ * validation rules read, so a task the audit reports in "Back office" is in the
265
+ * `lane` named "Back office" here.
265
266
  */
266
267
  export declare function exportBpmnXml(board: BpmnExportBoard, options?: BpmnExportOptions): string;
267
268
  /**
package/dist/export.js CHANGED
@@ -929,10 +929,11 @@ function edgeScope(element, source, target, ctx) {
929
929
  * process; with a collaboration it must point at the collaboration, or most
930
930
  * tools draw the flow and none of the pools (spec §12.3.2).
931
931
  *
932
- * Attribution is {@link bpmnPoolOf} and {@link bpmnLaneOf} — the CENTRE against
933
- * the pool's PLOT, containment only, no nearest-pool fallback. Deliberately the
934
- * same arithmetic the audit and the validation rules read, so a task the audit
935
- * reports in "Back office" is in the `lane` named "Back office" here.
932
+ * Attribution is {@link bpmnPoolOf} and {@link bpmnLaneOf} — WHOLE containment
933
+ * in the pool, then the CENTRE against the pool's plot for the lane, no
934
+ * nearest-pool fallback. Deliberately the same arithmetic the audit and the
935
+ * validation rules read, so a task the audit reports in "Back office" is in the
936
+ * `lane` named "Back office" here.
936
937
  */
937
938
  export function exportBpmnXml(board, options = {}) {
938
939
  return exportBpmnXmlWithWarnings(board, options).text;
package/dist/facts.d.ts CHANGED
@@ -1,31 +1,19 @@
1
1
  import type { BpmnLane, BpmnPoolElementModel } from '@formicoidea/labre-core/model';
2
2
  import type { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  /**
4
- * The pool whose plot contains the bound's centre, or `null`.
4
+ * The pool an artefact belongs to: the one that WHOLLY CONTAINS its bound
5
+ * (PF2.4), or `null`.
5
6
  *
6
- * ## Which attribution rule this is, and why it is not the audit's whole one
7
+ * The pool's ELEMENT bound and not its plot: membership is one question with one
8
+ * answer across the product, and `containingFrame` is where it is answered — the
9
+ * same function `containingBackground` uses inside the validation engine, so a
10
+ * task the engine indicts for hanging off a pool is not reported as being in it.
11
+ * The name band therefore counts as part of the pool for MEMBERSHIP; it is only
12
+ * the lane arithmetic below that still subtracts it.
7
13
  *
8
- * The audit attributes in two halves (`attribute()` in `audit.ts`, and
9
- * `attributeBackground()` in `validation.ts`): the frame that CONTAINS the
10
- * element, failing that the NEAREST by edge-to-edge gap. This is the containment
11
- * half only, with the audit's own first-match-in-document-order tie-break —
12
- * which is the sanctioned reduction, and it is the right one here for two
13
- * reasons.
14
- *
15
- * - The nearest-fallback never returns `null` while a single pool exists on the
16
- * board. That is correct for an audit, which has to say something about every
17
- * role-carrying element it reports; it is wrong for a fact query, whose whole
18
- * value is telling "in this pool" apart from "beside it". A task dropped on
19
- * bare canvas is not in a pool, and saying so is the answer.
20
- * - Containment is tested on the CENTRE against the plot, not on the full
21
- * element box against the element box, so this function and {@link bpmnLaneOf}
22
- * are the same test at two scales. A laned pool therefore cannot answer "yes,
23
- * this pool" and "no lane" for a lane set that covers the plot — the two are
24
- * congruent by construction rather than by agreement.
25
- *
26
- * Ties — a centre inside two overlapping pools — go to the first pool in the
27
- * order given, which for a surface is document order. The audit's `attribute()`
28
- * returns on its first containing frame the same way.
14
+ * No nearest-pool fallback, unlike the audit's frame attribution: a task dropped
15
+ * beside a pool is not in it, and inventing one here would put a rule's finding
16
+ * on a participant the author never drew it on.
29
17
  */
30
18
  export declare function bpmnPoolOf(pools: readonly BpmnPoolElementModel[], bound: Bound): BpmnPoolElementModel | null;
31
19
  /**
@@ -41,6 +29,10 @@ export declare function bpmnPoolOf(pools: readonly BpmnPoolElementModel[], bound
41
29
  * not paint. The `BpmnLane` handed back is the model's own row, matched by id,
42
30
  * so a caller gets the thing it can rename or resize.
43
31
  *
32
+ * The CENTRE, deliberately unlike {@link bpmnPoolOf}: a task must be wholly in a
33
+ * pool, but may straddle two lanes and is then read in the one its centre is in.
34
+ * See the module comment — a board is a container, a band is a reading.
35
+ *
44
36
  * First match in declaration order, so a centre landing exactly ON a divider
45
37
  * belongs to the band ABOVE it. Arbitrary in isolation and deliberate together:
46
38
  * it is the tie `zoneAt` breaks, and the two must break it the same way.
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, containingFrame, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { BPMN_POOL_BACKGROUND } from './background.js';
3
3
  /**
4
4
  * Where a BPMN artefact SITS — the facts, answerable without a `BlockStdScope`.
@@ -16,15 +16,21 @@ import { BPMN_POOL_BACKGROUND } from './background.js';
16
16
  * call them, a test can call them with a stub, and the answer cannot depend on
17
17
  * which extensions happen to be registered.
18
18
  *
19
- * ## One convention, copied rather than reinvented
19
+ * ## Two conventions, and the asymmetry between them is the point (PF2.4)
20
20
  *
21
- * Both read the element's CENTRE against ratios of the pool's PLOT, and take the
22
- * FIRST matching zone in declaration order — the convention `plotRatios` and
23
- * `zoneAt` already use inside the audit. Copied deliberately and stated here so
24
- * it stays copied: the day these two disagree with the audit, one component gets
25
- * two answers about which lane it is in, and no user can be told which is right.
26
- * The centre also has the property the geometry needs — a task wider than a lane
27
- * still belongs to exactly one of them.
21
+ * {@link bpmnPoolOf} asks about MEMBERSHIP and answers with WHOLE containment in
22
+ * the pool's element bound — the shared `containingFrame`, the same test the
23
+ * validation engine and the audit use, so one artefact never gets two answers
24
+ * about which participant it is on.
25
+ *
26
+ * {@link bpmnLaneOf} asks about POSITION inside a pool it is already in, and
27
+ * keeps the CENTRE against ratios of the PLOT, first matching zone in
28
+ * declaration order — the convention `plotRatios` and `zoneAt` use in the audit.
29
+ * A lane is a band drawn across a pool, and a task taller than a band still has
30
+ * to be in exactly one of them.
31
+ *
32
+ * So a task must be WHOLLY inside a pool, but may straddle two lanes. That is
33
+ * deliberate: a board is a container, a band is a reading.
28
34
  */
29
35
  /**
30
36
  * `lane` — the namespace the pool's instance zones report under.
@@ -59,42 +65,23 @@ function within(at, rect) {
59
65
  at[1] >= rect.y &&
60
66
  at[1] <= rect.y + rect.h);
61
67
  }
62
- /** The whole plot, as the ratios every zone rectangle is expressed in. */
63
- const WHOLE_PLOT = { x: 0, y: 0, w: 1, h: 1 };
64
68
  /**
65
- * The pool whose plot contains the bound's centre, or `null`.
66
- *
67
- * ## Which attribution rule this is, and why it is not the audit's whole one
68
- *
69
- * The audit attributes in two halves (`attribute()` in `audit.ts`, and
70
- * `attributeBackground()` in `validation.ts`): the frame that CONTAINS the
71
- * element, failing that the NEAREST by edge-to-edge gap. This is the containment
72
- * half only, with the audit's own first-match-in-document-order tie-break —
73
- * which is the sanctioned reduction, and it is the right one here for two
74
- * reasons.
69
+ * The pool an artefact belongs to: the one that WHOLLY CONTAINS its bound
70
+ * (PF2.4), or `null`.
75
71
  *
76
- * - The nearest-fallback never returns `null` while a single pool exists on the
77
- * board. That is correct for an audit, which has to say something about every
78
- * role-carrying element it reports; it is wrong for a fact query, whose whole
79
- * value is telling "in this pool" apart from "beside it". A task dropped on
80
- * bare canvas is not in a pool, and saying so is the answer.
81
- * - Containment is tested on the CENTRE against the plot, not on the full
82
- * element box against the element box, so this function and {@link bpmnLaneOf}
83
- * are the same test at two scales. A laned pool therefore cannot answer "yes,
84
- * this pool" and "no lane" for a lane set that covers the plot — the two are
85
- * congruent by construction rather than by agreement.
72
+ * The pool's ELEMENT bound and not its plot: membership is one question with one
73
+ * answer across the product, and `containingFrame` is where it is answered — the
74
+ * same function `containingBackground` uses inside the validation engine, so a
75
+ * task the engine indicts for hanging off a pool is not reported as being in it.
76
+ * The name band therefore counts as part of the pool for MEMBERSHIP; it is only
77
+ * the lane arithmetic below that still subtracts it.
86
78
  *
87
- * Ties — a centre inside two overlapping pools — go to the first pool in the
88
- * order given, which for a surface is document order. The audit's `attribute()`
89
- * returns on its first containing frame the same way.
79
+ * No nearest-pool fallback, unlike the audit's frame attribution: a task dropped
80
+ * beside a pool is not in it, and inventing one here would put a rule's finding
81
+ * on a participant the author never drew it on.
90
82
  */
91
83
  export function bpmnPoolOf(pools, bound) {
92
- for (const pool of pools) {
93
- const at = plotRatios(pool, bound);
94
- if (at !== null && within(at, WHOLE_PLOT))
95
- return pool;
96
- }
97
- return null;
84
+ return containingFrame(bound, pools, pool => pool.elementBound);
98
85
  }
99
86
  /**
100
87
  * The lane of `pool` the bound's centre falls in — `null` when the pool carries
@@ -109,6 +96,10 @@ export function bpmnPoolOf(pools, bound) {
109
96
  * not paint. The `BpmnLane` handed back is the model's own row, matched by id,
110
97
  * so a caller gets the thing it can rename or resize.
111
98
  *
99
+ * The CENTRE, deliberately unlike {@link bpmnPoolOf}: a task must be wholly in a
100
+ * pool, but may straddle two lanes and is then read in the one its centre is in.
101
+ * See the module comment — a board is a container, a band is a reading.
102
+ *
112
103
  * First match in declaration order, so a centre landing exactly ON a divider
113
104
  * belongs to the band ABOVE it. Arbitrary in isolation and deliberate together:
114
105
  * it is the tie `zoneAt` breaks, and the two must break it the same way.
package/dist/presets.d.ts CHANGED
@@ -124,6 +124,25 @@ export declare function bpmnNodeProps(kind: BpmnNodeKind, box: {
124
124
  }): Record<string, unknown> & {
125
125
  type: string;
126
126
  };
127
+ /**
128
+ * One BPMN pool, as the props `surface.addElement` takes.
129
+ *
130
+ * The same argument {@link bpmnNodeProps} makes, one artefact over: the palette
131
+ * hands it a box centred on the viewport and a worked scene hands it the box the
132
+ * scene lays out, and the element that lands is the same either way. It used to
133
+ * be spelled out at each site, which is how the shipped cards came to write a
134
+ * `name` the creation gesture leaves to the model's own default.
135
+ *
136
+ * `name` is spread and never defaulted, for the reason `textVerticalAlign` is on
137
+ * a node: a pool drawn from the toolbox puts no participant name in its Y.Map,
138
+ * and a scene that names its participants says so explicitly.
139
+ */
140
+ export declare function bpmnPoolProps(box: {
141
+ xywh: string;
142
+ name?: string;
143
+ }): Record<string, unknown> & {
144
+ type: string;
145
+ };
127
146
  /**
128
147
  * What a node's kind is worth to an element that ALREADY EXISTS — the same
129
148
  * description as {@link bpmnNodeProps}, minus the three things a morph has no
package/dist/presets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FontFamily, SHAPE_TEXT_PADDING, SHAPE_TEXT_VERTICAL_PADDING, ShapeStyle, StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign, } from '@formicoidea/labre-core/model';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  import { CALL_ACTIVITY_WIDTH, END_WIDTH, EVENT_END, EVENT_START, GROUP_RADIUS, GROUP_STROKE, INNER_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
- import { BPMN_ROLE_OF_KIND } from './roles.js';
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 = {
7
7
  // Events: one ellipse, and the ring weight says start or end. The message and
@@ -255,6 +255,29 @@ export function bpmnNodeProps(kind, box) {
255
255
  xywh: box.xywh,
256
256
  };
257
257
  }
258
+ /**
259
+ * One BPMN pool, as the props `surface.addElement` takes.
260
+ *
261
+ * The same argument {@link bpmnNodeProps} makes, one artefact over: the palette
262
+ * hands it a box centred on the viewport and a worked scene hands it the box the
263
+ * scene lays out, and the element that lands is the same either way. It used to
264
+ * be spelled out at each site, which is how the shipped cards came to write a
265
+ * `name` the creation gesture leaves to the model's own default.
266
+ *
267
+ * `name` is spread and never defaulted, for the reason `textVerticalAlign` is on
268
+ * a node: a pool drawn from the toolbox puts no participant name in its Y.Map,
269
+ * and a scene that names its participants says so explicitly.
270
+ */
271
+ export function bpmnPoolProps(box) {
272
+ return {
273
+ type: 'bpmnPool',
274
+ // The FRAME the flow objects are drawn in, and a role of its own: a rule
275
+ // written on the artefacts must never fall on the lane that holds them.
276
+ role: BPMN_ROLE.pool,
277
+ ...(box.name === undefined ? {} : { name: box.name }),
278
+ xywh: box.xywh,
279
+ };
280
+ }
258
281
  /**
259
282
  * The box {@link bpmnMorphProps} hands {@link bpmnNodeProps} and then throws
260
283
  * away. Never written to a document: a morph keeps the geometry the element
package/dist/rules.js CHANGED
@@ -882,11 +882,11 @@ const activityDeadEnd = {
882
882
  *
883
883
  * ## What it needs the pool DECLARATION for
884
884
  *
885
- * "Inside the pool" is read against the pool's PLOT, not its element box, and the
886
- * plot is where the declaration says the flow area starts — the left margin is
887
- * the participant name band, and a task lying on the band is not in the flow area
888
- * at all. Carrying `BPMN_POOL_BACKGROUND` here is what makes this rule and
889
- * `bpmnPoolOf` give one answer to "which pool is this on" instead of two.
885
+ * "Inside the pool" is WHOLE containment in the pool's element bound (PF2.4) —
886
+ * the same `containingFrame` `bpmnPoolOf` calls, so this rule and the fact query
887
+ * give one answer to "which pool is this on" instead of two. The declaration is
888
+ * carried all the same: it is what the zone half of the family reads, and it is
889
+ * where the lane geometry lives.
890
890
  *
891
891
  * ## Silence
892
892
  *
@@ -912,8 +912,8 @@ const sequenceFlowStaysHome = {
912
912
  reference: 'OMG BPMN 2.0.2 (ISO/IEC 19510) p.40, restated p.502 — Sequence Flows cannot cross a Pool boundary',
913
913
  },
914
914
  backgroundRole: BPMN_ROLE.pool,
915
- // The declaration, so "inside the pool" means inside its FLOW AREA — the
916
- // participant name band is not part of it. Same plot `bpmnPoolOf` reads.
915
+ // The declaration, for the zone half of the family and the lane geometry.
916
+ // Membership itself is whole containment in the element bound (PF2.4).
917
917
  background: BPMN_POOL_BACKGROUND,
918
918
  locality: {
919
919
  edgeRole: BPMN_ROLE.sequenceFlow,
@@ -1,74 +1,59 @@
1
- import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
2
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
3
- import { END_WIDTH, EVENT_END, EVENT_START, INNER_FONT_SIZE, MESSAGE_STROKE, MESSAGE_WIDTH, NEUTRAL_STROKE, NODE_FILL, NODE_SIZE, NODE_STROKE_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, START_WIDTH, TASK_RADIUS, } from '../consts.js';
4
- import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from '../roles.js';
1
+ import { makeTemplateSnapshot, surfaceText, templateFromCommand, } from '@formicoidea/labre-core/gfx/template';
2
+ import { ConnectorMode, PointStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
3
+ import { bpmnCommands } from '../commands.js';
4
+ import { MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from '../consts.js';
5
+ import { bpmnNodeProps, bpmnPoolProps } from '../presets.js';
6
+ import { BPMN_ROLE } from '../roles.js';
5
7
  /**
6
- * One BPMN flow-object node, as a surface-element JSON entry.
8
+ * The BPMN palette — DERIVED from the toolbox, one template per artefact
9
+ * command.
7
10
  *
8
- * Every kind that {@link NodeKind} admits paints its own body natively, as an
9
- * ellipse, a rounded rect or a diamond. The GLYPH-BODIED kinds (`dataObject`,
10
- * `dataStore`, `textAnnotation`) do not: they are created unfilled and
11
- * unstroked and the renderer draws their silhouette, so a card that spelled one
12
- * out here would insert an invisible rectangle. `group` is out for a neighbouring
13
- * reason — it is `hollow`, and the branches below all fill. When any of them
14
- * earns a card, this helper reads `NODE_PRESETS` instead of branching.
11
+ * Every single-artefact entry below is what its command actually draws, run once
12
+ * against a recording surface. It used to be a hand-written restatement of the
13
+ * same artefacts through a private `node()` builder, and it had drifted exactly
14
+ * as far as a copy drifts: none of the eleven nodes carried the
15
+ * `textFitMode: Overflow` the pack has written since #160, the events and
16
+ * gateways had lost their typography on an early return, the labels were English
17
+ * literals outside the translation seam (#192), and six kinds out of seventeen
18
+ * had a template at all. Derived, none of that can happen again — and
19
+ * `templates-parity.unit.spec.ts` re-runs each command and compares, so the day
20
+ * a creation site changes the palette changes with it.
21
+ *
22
+ * The two worked SCENES stay hand-composed: a process end to end and a
23
+ * two-party message exchange are arrangements of a dozen artefacts, which no
24
+ * single command draws. They are built on the same presets
25
+ * ({@link bpmnNodeProps}, {@link bpmnPoolProps}), and the same test checks their
26
+ * composition AND runs the rule pack over them — which is what caught the
27
+ * "Message exchange" card violating `bpmn.pool-start-without-end` at insertion.
28
+ */
29
+ /** The command a derived template is the picture of. Throws rather than skips. */
30
+ function byId(id) {
31
+ const command = bpmnCommands.find(entry => entry.id === id);
32
+ if (!command)
33
+ throw new Error(`[bpmn] templates: no command "${id}"`);
34
+ return command;
35
+ }
36
+ /**
37
+ * A scene node, from the same description the toolbox draws from.
38
+ *
39
+ * The scene owns the BOX and the words and nothing else — every visual prop
40
+ * comes from {@link bpmnNodeProps}. Undefined keys are dropped because that is
41
+ * what `surface.addElement` stores, so a scene artefact stays comparable with a
42
+ * derived one.
15
43
  */
16
44
  function node(kind, x, y, text) {
17
45
  const { w, h } = NODE_SIZE[kind];
18
- const base = {
19
- type: 'bpmnNode',
20
- kind,
21
- // A template must produce the same typed artefacts as the toolbox, or a
22
- // process started from a preset would read differently from a hand-drawn
23
- // one.
24
- role: BPMN_ROLE_OF_KIND[kind],
25
- filled: true,
26
- fillColor: NODE_FILL,
27
- shapeStyle: ShapeStyle.General,
28
- roughness: 0,
29
- xywh: `[${x},${y},${w},${h}]`,
30
- };
31
- if (kind === 'startEvent')
32
- return {
33
- ...base,
34
- shapeType: 'ellipse',
35
- strokeColor: EVENT_START,
36
- strokeWidth: START_WIDTH,
37
- };
38
- if (kind === 'endEvent')
39
- return {
40
- ...base,
41
- shapeType: 'ellipse',
42
- strokeColor: EVENT_END,
43
- strokeWidth: END_WIDTH,
44
- };
45
- if (kind === 'gatewayExclusive')
46
- return {
47
- ...base,
48
- shapeType: 'diamond',
49
- strokeColor: NEUTRAL_STROKE,
50
- strokeWidth: NODE_STROKE_WIDTH,
51
- };
52
- return {
53
- ...base,
54
- shapeType: 'rect',
55
- radius: TASK_RADIUS,
56
- strokeColor: NEUTRAL_STROKE,
57
- strokeWidth: NODE_STROKE_WIDTH,
58
- text: surfaceText(text ?? 'Task'),
59
- color: NEUTRAL_STROKE,
60
- fontFamily: FontFamily.Inter,
61
- fontSize: INNER_FONT_SIZE,
62
- textAlign: 'center',
63
- };
46
+ return defined({
47
+ ...bpmnNodeProps(kind, { xywh: `[${x},${y},${w},${h}]` }),
48
+ // A snapshot carries a serialized `Y.Text`, never a bare string.
49
+ ...(text === undefined ? {} : { text: surfaceText(text) }),
50
+ });
64
51
  }
65
- function pool(x, y, w, h, name = 'Pool') {
66
- return {
67
- type: 'bpmnPool',
68
- role: BPMN_ROLE.pool,
69
- name,
70
- xywh: `[${x},${y},${w},${h}]`,
71
- };
52
+ /** A scene participant, from the same description the toolbox draws from. */
53
+ const pool = (x, y, w, h, name) => defined(bpmnPoolProps({ xywh: `[${x},${y},${w},${h}]`, name }));
54
+ /** Drop the keys a preset left undefined — what `addElement` would store. */
55
+ function defined(props) {
56
+ return Object.fromEntries(Object.entries(props).filter(([, value]) => value !== undefined));
72
57
  }
73
58
  /** A sequence-flow connector; ids are remapped on insert. */
74
59
  function seq(source, target) {
@@ -121,6 +106,10 @@ function msg(source, target) {
121
106
  * that", and a stroke attached to neither end has no this and no that to say it
122
107
  * about. Drawing it with the sequence-flow tool, or dropping this one and then
123
108
  * attaching both of its ends, is what makes it a statement.
109
+ *
110
+ * Hand-written, and the one artefact card that cannot derive:
111
+ * `bpmn.sequenceFlowTool` ACTIVATES a tool rather than drawing anything, so
112
+ * there is nothing to record — the user draws it.
124
113
  */
125
114
  function freeSeq() {
126
115
  return {
@@ -135,86 +124,139 @@ function freeSeq() {
135
124
  target: { position: [140, 0] },
136
125
  };
137
126
  }
138
- const single = (el) => ({
139
- a: el,
140
- });
141
- const PREVIEW_ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
127
+ /* ── Previews ─────────────────────────────────────────────────────────────
128
+ *
129
+ * One 135×80 sketch per card, in the toolbar glyphs' own shapes
130
+ * (`../toolbar/icons.ts`). A preview has one job, which is to look like what
131
+ * lands on the board.
132
+ */
133
+ const A = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
134
+ /** An event: the ring weight says start or end, the glyph says what triggers it. */
135
+ const eventPreview = (stroke, width, glyph = '') => `<svg ${A} fill="none"><circle cx="67" cy="40" r="20" stroke="${stroke}" stroke-width="${width}"/>${glyph}</svg>`;
136
+ /** An activity: one rounded rectangle, and a marker tells the kinds apart. */
137
+ const taskPreview = (glyph = '', width = 2.4) => `<svg ${A} fill="none"><rect x="34" y="24" width="66" height="32" rx="6" stroke="#262626" stroke-width="${width}"/>${glyph}</svg>`;
138
+ /** A gateway: one diamond, one marker each. */
139
+ const gatewayPreview = (marker) => `<svg ${A} fill="none"><path d="M67 16 L92 40 L67 64 L42 40 Z" stroke="#262626" stroke-width="2.4" stroke-linejoin="round"/><path d="${marker}" stroke="#262626" stroke-width="2.2" stroke-linecap="round"/></svg>`;
140
+ const ENVELOPE = '<rect x="59" y="35" width="16" height="11" stroke="#262626" stroke-width="1.4"/><path d="M59 35 L67 42 L75 35" stroke="#262626" stroke-width="1.4" stroke-linejoin="round"/>';
141
+ const CLOCK = '<circle cx="67" cy="40" r="9" stroke="#262626" stroke-width="1.4"/><path d="M67 40 V33 M67 40 L72 43" stroke="#262626" stroke-width="1.4" stroke-linecap="round"/>';
142
+ /** The collapsed `[+]`, on the activity's bottom edge — sub-process and call alike. */
143
+ const COLLAPSED = '<rect x="61" y="44" width="12" height="12" stroke="#262626" stroke-width="1.4"/><path d="M67 47 V53 M64 50 H70" stroke="#262626" stroke-width="1.4" stroke-linecap="round"/>';
142
144
  const previews = {
143
- process: `<svg ${PREVIEW_ATTRS} fill="none"><circle cx="16" cy="40" r="8" stroke="#43a06b" stroke-width="2"/><rect x="34" y="31" width="26" height="18" rx="3" stroke="#262626" stroke-width="1.6"/><path d="M78 31 L88 40 L78 49 L68 40 Z" stroke="#262626" stroke-width="1.4"/><path d="M73 37 L83 43 M83 37 L73 43" stroke="#262626" stroke-width="1.2"/><circle cx="118" cy="40" r="8" stroke="#cf5648" stroke-width="3"/><path d="M24 40 H34 M60 40 H68 M88 40 H110" stroke="#262626" stroke-width="1.2"/></svg>`,
144
- startEvent: `<svg ${PREVIEW_ATTRS} fill="none"><circle cx="67" cy="40" r="20" stroke="#43a06b" stroke-width="3"/></svg>`,
145
- endEvent: `<svg ${PREVIEW_ATTRS} fill="none"><circle cx="67" cy="40" r="20" stroke="#cf5648" stroke-width="5"/></svg>`,
146
- task: `<svg ${PREVIEW_ATTRS} fill="none"><rect x="34" y="24" width="66" height="32" rx="6" stroke="#262626" stroke-width="2.4"/></svg>`,
147
- gateway: `<svg ${PREVIEW_ATTRS} fill="none"><path d="M67 16 L92 40 L67 64 L42 40 Z" stroke="#262626" stroke-width="2.4" stroke-linejoin="round"/><path d="M58 31 L76 49 M76 31 L58 49" stroke="#262626" stroke-width="2.2" stroke-linecap="round"/></svg>`,
148
- sequence: `<svg ${PREVIEW_ATTRS} fill="none"><path d="M24 40 H96" stroke="#262626" stroke-width="2.4" stroke-linecap="round"/><path d="M94 33 L108 40 L94 47 Z" fill="#262626"/></svg>`,
149
- pool: `<svg ${PREVIEW_ATTRS} fill="none"><rect x="14" y="20" width="107" height="40" rx="3" stroke="#262626" stroke-width="2"/><path d="M30 20 V60" stroke="#262626" stroke-width="1.8"/><rect x="14" y="20" width="16" height="40" fill="#f4f4f5"/><path d="M30 20 V60" stroke="#262626" stroke-width="1.8"/></svg>`,
145
+ process: `<svg ${A} fill="none"><circle cx="16" cy="40" r="8" stroke="#43a06b" stroke-width="2"/><rect x="34" y="31" width="26" height="18" rx="3" stroke="#262626" stroke-width="1.6"/><path d="M78 31 L88 40 L78 49 L68 40 Z" stroke="#262626" stroke-width="1.4"/><path d="M73 37 L83 43 M83 37 L73 43" stroke="#262626" stroke-width="1.2"/><circle cx="118" cy="40" r="8" stroke="#cf5648" stroke-width="3"/><path d="M24 40 H34 M60 40 H68 M88 40 H110" stroke="#262626" stroke-width="1.2"/></svg>`,
146
+ startEvent: eventPreview('#43a06b', 3),
147
+ startEventMessage: eventPreview('#43a06b', 3, ENVELOPE),
148
+ startEventTimer: eventPreview('#43a06b', 3, CLOCK),
149
+ endEvent: eventPreview('#cf5648', 5),
150
+ endEventMessage: eventPreview('#cf5648', 5, ENVELOPE),
151
+ endEventTerminate: eventPreview('#cf5648', 5, '<circle cx="67" cy="40" r="8" fill="#262626"/>'),
152
+ task: taskPreview(),
153
+ taskUser: taskPreview('<circle cx="41" cy="31" r="2.6" stroke="#262626" stroke-width="1.2"/><path d="M37.4 38 a3.6 3.6 0 0 1 7.2 0" stroke="#262626" stroke-width="1.2"/>'),
154
+ taskService: taskPreview('<circle cx="41" cy="32" r="3.4" stroke="#262626" stroke-width="1.2"/><circle cx="41" cy="32" r="1" fill="#262626"/><path d="M41 27.4 V29 M41 35 V36.6 M36.4 32 H38 M44 32 H45.6" stroke="#262626" stroke-width="1.2" stroke-linecap="round"/>'),
155
+ subProcess: taskPreview(COLLAPSED),
156
+ // The thick border IS the distinction: this box stands for a process defined
157
+ // elsewhere (`presets.ts`, `CALL_ACTIVITY_WIDTH`).
158
+ callActivity: taskPreview(COLLAPSED, 4.5),
159
+ gateway: gatewayPreview('M58 31 L76 49 M76 31 L58 49'),
160
+ gatewayParallel: gatewayPreview('M67 30 V50 M57 40 H77'),
161
+ dataObject: `<svg ${A} fill="none"><path d="M52 14 H76 L88 26 V66 H52 Z" stroke="#262626" stroke-width="2.2" stroke-linejoin="round"/><path d="M76 14 V26 H88" stroke="#262626" stroke-width="1.8" stroke-linejoin="round"/></svg>`,
162
+ dataStore: `<svg ${A} fill="none"><path d="M48 22 V58 C48 62 56 65 67 65 C78 65 86 62 86 58 V22" stroke="#262626" stroke-width="2.2" stroke-linejoin="round"/><ellipse cx="67" cy="22" rx="19" ry="6" stroke="#262626" stroke-width="2.2"/></svg>`,
163
+ textAnnotation: `<svg ${A} fill="none"><path d="M46 16 H36 V64 H46" stroke="#262626" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M54 28 H100 M54 40 H100 M54 52 H86" stroke="#262626" stroke-width="1.8" stroke-linecap="round"/></svg>`,
164
+ group: `<svg ${A} fill="none"><rect x="18" y="12" width="99" height="56" rx="10" stroke="#8e8d91" stroke-width="2.2" stroke-dasharray="7 5"/></svg>`,
165
+ sequence: `<svg ${A} fill="none"><path d="M24 40 H96" stroke="#262626" stroke-width="2.4" stroke-linecap="round"/><path d="M94 33 L108 40 L94 47 Z" fill="#262626"/></svg>`,
166
+ pool: `<svg ${A} fill="none"><rect x="14" y="20" width="107" height="40" rx="3" stroke="#262626" stroke-width="2"/><path d="M30 20 V60" stroke="#262626" stroke-width="1.8"/><rect x="14" y="20" width="16" height="40" fill="#f4f4f5"/><path d="M30 20 V60" stroke="#262626" stroke-width="1.8"/></svg>`,
150
167
  // Two participants stacked, and the dashed line between them is the whole
151
168
  // point of the card: a message flow is the one arrow that crosses a pool.
152
169
  // Its source terminator is a FILLED disc, matching what `renderCircle`
153
170
  // actually paints rather than the hollow ring the norm asks for — a preview
154
171
  // has one job, which is to look like what lands on the board.
155
- messageExchange: `<svg ${PREVIEW_ATTRS} fill="none"><rect x="14" y="8" width="107" height="27" rx="3" stroke="#262626" stroke-width="1.6"/><rect x="14" y="8" width="11" height="27" fill="#f4f4f5"/><path d="M25 8 V35" stroke="#262626" stroke-width="1.4"/><rect x="14" y="45" width="107" height="27" rx="3" stroke="#262626" stroke-width="1.6"/><rect x="14" y="45" width="11" height="27" fill="#f4f4f5"/><path d="M25 45 V72" stroke="#262626" stroke-width="1.4"/><circle cx="36" cy="21.5" r="4.5" stroke="#43a06b" stroke-width="1.6"/><rect x="52" y="14" width="30" height="15" rx="3" stroke="#262626" stroke-width="1.6"/><rect x="52" y="51" width="30" height="15" rx="3" stroke="#262626" stroke-width="1.6"/><path d="M45 21.5 H52" stroke="#262626" stroke-width="1.4"/><circle cx="67" cy="31.5" r="2.2" fill="#262626"/><path d="M67 34 V48" stroke="#262626" stroke-width="1.4" stroke-dasharray="3 2.4"/><path d="M64 46 L67 50 L70 46" stroke="#262626" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
172
+ messageExchange: `<svg ${A} fill="none"><rect x="14" y="8" width="107" height="27" rx="3" stroke="#262626" stroke-width="1.6"/><rect x="14" y="8" width="11" height="27" fill="#f4f4f5"/><path d="M25 8 V35" stroke="#262626" stroke-width="1.4"/><rect x="14" y="45" width="107" height="27" rx="3" stroke="#262626" stroke-width="1.6"/><rect x="14" y="45" width="11" height="27" fill="#f4f4f5"/><path d="M25 45 V72" stroke="#262626" stroke-width="1.4"/><circle cx="33" cy="21.5" r="4.5" stroke="#43a06b" stroke-width="1.6"/><rect x="46" y="14" width="26" height="15" rx="3" stroke="#262626" stroke-width="1.6"/><circle cx="88" cy="21.5" r="4.5" stroke="#cf5648" stroke-width="2.6"/><rect x="46" y="51" width="26" height="15" rx="3" stroke="#262626" stroke-width="1.6"/><path d="M41 21.5 H46 M72 21.5 H83" stroke="#262626" stroke-width="1.4"/><circle cx="59" cy="31.5" r="2.2" fill="#262626"/><path d="M59 34 V48" stroke="#262626" stroke-width="1.4" stroke-dasharray="3 2.4"/><path d="M56 46 L59 50 L62 46" stroke="#262626" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
173
+ };
174
+ /* ── The two worked scenes ────────────────────────────────────────────────── */
175
+ /** A hand-composed card — what is left once the artefacts are derived. */
176
+ const scene = (name, preview, elements) => ({
177
+ name,
178
+ type: 'template',
179
+ preview,
180
+ content: makeTemplateSnapshot(elements, name),
181
+ });
182
+ const process = {
183
+ pool: pool(0, 0, 640, 200, 'Process'),
184
+ start: node('startEvent', 40, 72),
185
+ task1: node('task', 116, 64, 'Submit request'),
186
+ gw: node('gatewayExclusive', 272, 64),
187
+ task2: node('task', 376, 20, 'Fulfil'),
188
+ task3: node('task', 376, 124, 'Reject'),
189
+ end: node('endEvent', 556, 72),
190
+ c1: seq('start', 'task1'),
191
+ c2: seq('task1', 'gw'),
192
+ c3: seq('gw', 'task2'),
193
+ c4: seq('gw', 'task3'),
194
+ c5: seq('task2', 'end'),
195
+ c6: seq('task3', 'end'),
156
196
  };
157
197
  /**
158
- * The BPMN basics: two worked scenes — a process end to end and a two-party
159
- * message exchange — plus a prefab for each of the lean artefacts.
198
+ * Two participants, and the one arrow that is allowed to cross between them.
199
+ *
200
+ * The card exists because the message flow is the piece of BPMN people get
201
+ * wrong first: a sequence flow may never leave its pool, and the thing that
202
+ * does leave is a different statement with a different line. Laying the two
203
+ * pools out one above the other and drawing both flows once is the shortest
204
+ * way to show that — the solid arrow stays home, the dashed one crosses.
205
+ *
206
+ * Stacked vertically with a 40-unit gutter, so the message flow is a straight
207
+ * orthogonal drop between two tasks that already line up.
208
+ *
209
+ * ## The end event, which is not decoration
210
+ *
211
+ * The customer pool shipped with a start event and no end, which is a verbatim
212
+ * violation of BPMN 2.0.2 p.246 and of `bpmn.pool-start-without-end` — a
213
+ * `warning` the moment anybody switched the card to the descriptive profile.
214
+ * Factory content teaching the mistake the rule pack reports is the worst
215
+ * possible first BPMN diagram, and nothing in the repository compared the two
216
+ * until this slice ran the rules over the shipped cards. The customer's process
217
+ * now closes where it should: order placed, then done.
160
218
  */
161
- function bpmnTemplates() {
162
- const process = {
163
- pool: pool(0, 0, 640, 200, 'Process'),
164
- start: node('startEvent', 40, 72),
165
- task1: node('task', 116, 64, 'Submit request'),
166
- gw: node('gatewayExclusive', 272, 64),
167
- task2: node('task', 376, 20, 'Fulfil'),
168
- task3: node('task', 376, 124, 'Reject'),
169
- end: node('endEvent', 556, 72),
170
- c1: seq('start', 'task1'),
171
- c2: seq('task1', 'gw'),
172
- c3: seq('gw', 'task2'),
173
- c4: seq('gw', 'task3'),
174
- c5: seq('task2', 'end'),
175
- c6: seq('task3', 'end'),
176
- };
177
- /**
178
- * Two participants, and the one arrow that is allowed to cross between them.
179
- *
180
- * The card exists because the message flow is the piece of BPMN people get
181
- * wrong first: a sequence flow may never leave its pool, and the thing that
182
- * does leave is a different statement with a different line. Laying the two
183
- * pools out one above the other and drawing both flows once is the shortest
184
- * way to show that — the solid arrow stays home, the dashed one crosses.
185
- *
186
- * Stacked vertically with a 40-unit gutter, so the message flow is a straight
187
- * orthogonal drop between two tasks that already line up.
188
- */
189
- const messageExchange = {
190
- customer: pool(0, 0, 640, 200, 'Customer'),
191
- supplier: pool(0, 240, 640, 200, 'Supplier'),
192
- start: node('startEvent', 40, 72),
193
- ask: node('taskUser', 140, 64, 'Place order'),
194
- answer: node('taskService', 140, 304, 'Confirm order'),
195
- // Inside the first participant: what happens, and in what order.
196
- inside: seq('start', 'ask'),
197
- // Across the two: who told whom.
198
- across: msg('ask', 'answer'),
199
- };
200
- const t = (name, preview, elements) => ({
201
- name,
202
- type: 'template',
203
- preview,
204
- content: makeTemplateSnapshot(elements, name),
205
- });
206
- return [
207
- t('Simple process', previews.process, process),
208
- t('Message exchange', previews.messageExchange, messageExchange),
209
- t('Start event', previews.startEvent, single(node('startEvent', 0, 0))),
210
- t('End event', previews.endEvent, single(node('endEvent', 0, 0))),
211
- t('Task', previews.task, single(node('task', 0, 0))),
212
- t('Exclusive gateway', previews.gateway, single(node('gatewayExclusive', 0, 0))),
213
- t('Sequence flow', previews.sequence, single(freeSeq())),
214
- t('Pool', previews.pool, single(pool(0, 0, 560, 200))),
215
- ];
216
- }
219
+ const messageExchange = {
220
+ customer: pool(0, 0, 640, 200, 'Customer'),
221
+ supplier: pool(0, 240, 640, 200, 'Supplier'),
222
+ start: node('startEvent', 40, 72),
223
+ ask: node('taskUser', 140, 64, 'Place order'),
224
+ done: node('endEvent', 320, 72),
225
+ answer: node('taskService', 140, 304, 'Confirm order'),
226
+ // Inside the first participant: what happens, and in what order.
227
+ inside: seq('start', 'ask'),
228
+ closes: seq('ask', 'done'),
229
+ // Across the two: who told whom.
230
+ across: msg('ask', 'answer'),
231
+ };
217
232
  export const bpmnTemplateCategory = {
218
233
  name: 'BPMN',
219
- templates: bpmnTemplates(),
234
+ templates: [
235
+ scene('Simple process', previews.process, process),
236
+ scene('Message exchange', previews.messageExchange, messageExchange),
237
+ /* ── The core: a drawable process, from the first click ─────────────── */
238
+ templateFromCommand(byId('bpmn.addStartEvent'), previews.startEvent),
239
+ templateFromCommand(byId('bpmn.addEndEvent'), previews.endEvent),
240
+ templateFromCommand(byId('bpmn.addTask'), previews.task),
241
+ templateFromCommand(byId('bpmn.addExclusiveGateway'), previews.gateway),
242
+ scene('Sequence flow', previews.sequence, { a: freeSeq() }),
243
+ templateFromCommand(byId('bpmn.addPool'), previews.pool),
244
+ /* ── Activities ─────────────────────────────────────────────────────── */
245
+ templateFromCommand(byId('bpmn.addUserTask'), previews.taskUser),
246
+ templateFromCommand(byId('bpmn.addServiceTask'), previews.taskService),
247
+ templateFromCommand(byId('bpmn.addSubProcess'), previews.subProcess),
248
+ templateFromCommand(byId('bpmn.addCallActivity'), previews.callActivity),
249
+ /* ── The other gateway ──────────────────────────────────────────────── */
250
+ templateFromCommand(byId('bpmn.addParallelGateway'), previews.gatewayParallel),
251
+ /* ── Event variants ─────────────────────────────────────────────────── */
252
+ templateFromCommand(byId('bpmn.addMessageStartEvent'), previews.startEventMessage),
253
+ templateFromCommand(byId('bpmn.addTimerStartEvent'), previews.startEventTimer),
254
+ templateFromCommand(byId('bpmn.addMessageEndEvent'), previews.endEventMessage),
255
+ templateFromCommand(byId('bpmn.addTerminateEndEvent'), previews.endEventTerminate),
256
+ /* ── Data, and what an author writes ON the picture ──────────────────── */
257
+ templateFromCommand(byId('bpmn.addDataObject'), previews.dataObject),
258
+ templateFromCommand(byId('bpmn.addDataStore'), previews.dataStore),
259
+ templateFromCommand(byId('bpmn.addTextAnnotation'), previews.textAnnotation),
260
+ templateFromCommand(byId('bpmn.addGroup'), previews.group),
261
+ ],
220
262
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-bpmn",
3
3
  "description": "Labre bpmn framework for @formicoidea/labre-core.",
4
- "version": "0.37.0",
4
+ "version": "0.38.1",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -31,7 +31,7 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@formicoidea/labre-core": "0.37.0",
34
+ "@formicoidea/labre-core": "0.38.1",
35
35
  "lit": "^3.2.0"
36
36
  }
37
37
  }