@bpmnkit/core 0.0.23 → 0.0.25

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.
@@ -105,10 +105,12 @@ interface BpmnFlowNodeBase {
105
105
  documentation?: string;
106
106
  extensionElements: XmlElement[];
107
107
  unknownAttributes: Record<string, string>;
108
+ isForCompensation?: boolean;
108
109
  }
109
110
  export interface BpmnStartEvent extends BpmnFlowNodeBase {
110
111
  type: "startEvent";
111
112
  eventDefinitions: BpmnEventDefinition[];
113
+ isInterrupting?: boolean;
112
114
  }
113
115
  export interface BpmnEndEvent extends BpmnFlowNodeBase {
114
116
  type: "endEvent";
@@ -42,6 +42,7 @@ const KNOWN_ATTRS = new Set([
42
42
  "default",
43
43
  "attachedToRef",
44
44
  "cancelActivity",
45
+ "isForCompensation",
45
46
  "sourceRef",
46
47
  "targetRef",
47
48
  "associationDirection",
@@ -275,7 +276,15 @@ function parseFlowElement(element) {
275
276
  unknownAttributes: unknownAttrs(element),
276
277
  };
277
278
  switch (ln) {
278
- case "startEvent":
279
+ case "startEvent": {
280
+ const isInterruptingAttr = attr(element, "isInterrupting");
281
+ return {
282
+ ...base,
283
+ type: "startEvent",
284
+ eventDefinitions: parseEventDefinitions(element),
285
+ ...(isInterruptingAttr === "false" ? { isInterrupting: false } : {}),
286
+ };
287
+ }
279
288
  case "endEvent":
280
289
  case "intermediateCatchEvent":
281
290
  case "intermediateThrowEvent":
@@ -299,7 +308,12 @@ function parseFlowElement(element) {
299
308
  case "businessRuleTask":
300
309
  case "manualTask":
301
310
  case "callActivity":
302
- return { ...base, type: ln, loopCharacteristics: parseLoopCharacteristics(element) };
311
+ return {
312
+ ...base,
313
+ type: ln,
314
+ loopCharacteristics: parseLoopCharacteristics(element),
315
+ isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
316
+ };
303
317
  case "adHocSubProcess":
304
318
  return {
305
319
  ...base,
@@ -157,6 +157,8 @@ function serializeFlowElement(fe, ns) {
157
157
  const attrs = { id: fe.id, ...fe.unknownAttributes };
158
158
  if (fe.name !== undefined)
159
159
  attrs.name = fe.name;
160
+ if (fe.isForCompensation)
161
+ attrs.isForCompensation = "true";
160
162
  const children = [];
161
163
  // Documentation
162
164
  if (fe.documentation !== undefined) {
@@ -169,6 +171,10 @@ function serializeFlowElement(fe, ns) {
169
171
  children.push(...flowRefs(fe.outgoing, "outgoing", bp));
170
172
  switch (fe.type) {
171
173
  case "startEvent":
174
+ if (fe.isInterrupting === false)
175
+ attrs.isInterrupting = "false";
176
+ children.push(...serializeEventDefinitions(fe.eventDefinitions, bp));
177
+ break;
172
178
  case "endEvent":
173
179
  case "intermediateCatchEvent":
174
180
  case "intermediateThrowEvent":
@@ -1,4 +1,4 @@
1
- import { ProcessBuilder } from "./bpmn-builder.js";
1
+ import { DiagramBuilder, ProcessBuilder } from "./bpmn-builder.js";
2
2
  import type { BpmnDefinitions } from "./bpmn-model.js";
3
3
  /** A minimal 3-element BPMN diagram useful for first-launch or "New Diagram" defaults. */
4
4
  export declare const SAMPLE_BPMN_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<bpmn:definitions xmlns:bpmn=\"http://www.omg.org/spec/BPMN/20100524/MODEL\"\n xmlns:bpmndi=\"http://www.omg.org/spec/BPMN/20100524/DI\"\n xmlns:dc=\"http://www.omg.org/spec/DD/20100524/DC\"\n xmlns:di=\"http://www.omg.org/spec/DD/20100524/DI\"\n id=\"Definitions_1\" targetNamespace=\"http://bpmn.io/schema/bpmn\">\n <bpmn:process id=\"proc\" isExecutable=\"true\">\n <bpmn:startEvent id=\"start\" name=\"Start\">\n <bpmn:outgoing>flow1</bpmn:outgoing>\n </bpmn:startEvent>\n <bpmn:serviceTask id=\"task1\" name=\"Process Order\">\n <bpmn:incoming>flow1</bpmn:incoming>\n <bpmn:outgoing>flow2</bpmn:outgoing>\n </bpmn:serviceTask>\n <bpmn:endEvent id=\"end\" name=\"End\">\n <bpmn:incoming>flow2</bpmn:incoming>\n </bpmn:endEvent>\n <bpmn:sequenceFlow id=\"flow1\" sourceRef=\"start\" targetRef=\"task1\"/>\n <bpmn:sequenceFlow id=\"flow2\" sourceRef=\"task1\" targetRef=\"end\"/>\n </bpmn:process>\n <bpmndi:BPMNDiagram id=\"diagram1\">\n <bpmndi:BPMNPlane id=\"plane1\" bpmnElement=\"proc\">\n <bpmndi:BPMNShape id=\"start_di\" bpmnElement=\"start\">\n <dc:Bounds x=\"152\" y=\"202\" width=\"36\" height=\"36\"/>\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"task1_di\" bpmnElement=\"task1\">\n <dc:Bounds x=\"260\" y=\"180\" width=\"100\" height=\"80\"/>\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"end_di\" bpmnElement=\"end\">\n <dc:Bounds x=\"432\" y=\"202\" width=\"36\" height=\"36\"/>\n </bpmndi:BPMNShape>\n <bpmndi:BPMNEdge id=\"flow1_di\" bpmnElement=\"flow1\">\n <di:waypoint x=\"188\" y=\"220\"/>\n <di:waypoint x=\"260\" y=\"220\"/>\n </bpmndi:BPMNEdge>\n <bpmndi:BPMNEdge id=\"flow2_di\" bpmnElement=\"flow2\">\n <di:waypoint x=\"360\" y=\"220\"/>\n <di:waypoint x=\"432\" y=\"220\"/>\n </bpmndi:BPMNEdge>\n </bpmndi:BPMNPlane>\n </bpmndi:BPMNDiagram>\n</bpmn:definitions>";
@@ -41,6 +41,20 @@ export declare const Bpmn: {
41
41
  * ```
42
42
  */
43
43
  readonly createProcess: (processId: string) => ProcessBuilder;
44
+ /**
45
+ * Create a multi-process BPMN definitions document using the fluent builder API.
46
+ *
47
+ * @param id - Unique identifier for the definitions element (defaults to `"Definitions_1"`).
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const defs = Bpmn.createDiagram("OrderSystem")
52
+ * .process("caller", (p) => p.startEvent("s").callActivity("call", { processId: "callee" }).endEvent("e"))
53
+ * .process("callee", (p) => p.startEvent("s2").serviceTask("work", { name: "Work", taskType: "work" }).endEvent("e2"))
54
+ * .build()
55
+ * ```
56
+ */
57
+ readonly createDiagram: (id?: string) => DiagramBuilder;
44
58
  /**
45
59
  * Parse a BPMN 2.0 XML string into a typed {@link BpmnDefinitions} model.
46
60
  *
@@ -1,5 +1,5 @@
1
1
  import { applyAutoLayout } from "./auto-layout.js";
2
- import { ProcessBuilder } from "./bpmn-builder.js";
2
+ import { DiagramBuilder, ProcessBuilder } from "./bpmn-builder.js";
3
3
  import { parseBpmn } from "./bpmn-parser.js";
4
4
  import { serializeBpmn } from "./bpmn-serializer.js";
5
5
  /** A minimal 3-element BPMN diagram useful for first-launch or "New Diagram" defaults. */
@@ -86,6 +86,22 @@ export const Bpmn = {
86
86
  createProcess(processId) {
87
87
  return new ProcessBuilder(processId);
88
88
  },
89
+ /**
90
+ * Create a multi-process BPMN definitions document using the fluent builder API.
91
+ *
92
+ * @param id - Unique identifier for the definitions element (defaults to `"Definitions_1"`).
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const defs = Bpmn.createDiagram("OrderSystem")
97
+ * .process("caller", (p) => p.startEvent("s").callActivity("call", { processId: "callee" }).endEvent("e"))
98
+ * .process("callee", (p) => p.startEvent("s2").serviceTask("work", { name: "Work", taskType: "work" }).endEvent("e2"))
99
+ * .build()
100
+ * ```
101
+ */
102
+ createDiagram(id = "Definitions_1") {
103
+ return new DiagramBuilder(id);
104
+ },
89
105
  /**
90
106
  * Parse a BPMN 2.0 XML string into a typed {@link BpmnDefinitions} model.
91
107
  *
@@ -64,6 +64,8 @@ export interface ZeebeExtensions {
64
64
  formDefinition?: ZeebeFormDefinition;
65
65
  /** DMN decision invoked by a business rule task (zeebe:calledDecision). */
66
66
  calledDecision?: ZeebeCalledDecision;
67
+ /** Marks this as a Camunda 8 native user task (zeebe:userTask). */
68
+ userTask?: true;
67
69
  /** Unrecognized extension elements preserved for roundtrip. */
68
70
  unknownElements?: XmlElement[];
69
71
  }
@@ -71,6 +71,9 @@ export function zeebeExtensionsToXmlElements(extensions) {
71
71
  attrs.activeElementsCollection = activeElementsCollection;
72
72
  elements.push({ name: "zeebe:adHoc", attributes: attrs, children: [] });
73
73
  }
74
+ if (extensions.userTask) {
75
+ elements.push({ name: "zeebe:userTask", attributes: {}, children: [] });
76
+ }
74
77
  if (extensions.formDefinition) {
75
78
  elements.push({
76
79
  name: "zeebe:formDefinition",
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@ export { isBpmnActivity, isBpmnAdHocSubProcess, isBpmnBoundaryEvent, isBpmnBusin
4
4
  export { findElement, findElementInProcess, findProcess, findSequenceFlow, getAllElements, getElementType, getZeebeExtensions, } from "./bpmn/utils.js";
5
5
  export { Bpmn, SAMPLE_BPMN_XML } from "./bpmn/index.js";
6
6
  export { applyAutoLayout } from "./bpmn/auto-layout.js";
7
- export type { ProcessBuilder, BranchBuilder, SubProcessContentBuilder, ServiceTaskOptions, ScriptTaskOptions, UserTaskOptions, CallActivityOptions, BusinessRuleTaskOptions, ElementOptions, GatewayOptions, MultiInstanceOptions, SubProcessOptions, StartEventOptions, IntermediateCatchEventOptions, IntermediateThrowEventOptions, BoundaryEventOptions, AdHocSubProcessOptions, } from "./bpmn/bpmn-builder.js";
7
+ export { DiagramBuilder } from "./bpmn/bpmn-builder.js";
8
+ export type { ProcessBuilder, BranchBuilder, SubProcessContentBuilder, ServiceTaskOptions, ScriptTaskOptions, UserTaskOptions, CallActivityOptions, BusinessRuleTaskOptions, ElementOptions, GatewayOptions, MultiInstanceOptions, SubProcessOptions, StartEventOptions, IntermediateCatchEventOptions, IntermediateThrowEventOptions, EndEventOptions, BoundaryEventOptions, AdHocSubProcessOptions, } from "./bpmn/bpmn-builder.js";
8
9
  export type { BpmnDefinitions, BpmnProcess, BpmnFlowNode, BpmnFlowElement, BpmnSequenceFlow, BpmnBoundaryEvent, BpmnElementType, BpmnStartEvent, BpmnEndEvent, BpmnIntermediateCatchEvent, BpmnIntermediateThrowEvent, BpmnTask, BpmnServiceTask, BpmnScriptTask, BpmnUserTask, BpmnSendTask, BpmnReceiveTask, BpmnBusinessRuleTask, BpmnManualTask, BpmnCallActivity, BpmnSubProcess, BpmnAdHocSubProcess, BpmnEventSubProcess, BpmnTransaction, BpmnExclusiveGateway, BpmnParallelGateway, BpmnInclusiveGateway, BpmnEventBasedGateway, BpmnComplexGateway, BpmnCollaboration, BpmnParticipant, BpmnMessageFlow, BpmnLane, BpmnLaneSet, BpmnError, BpmnEscalation, BpmnMessage, BpmnSignal, BpmnTextAnnotation, BpmnAssociation, BpmnConditionExpression, BpmnEventDefinition, BpmnTimerEventDefinition, BpmnErrorEventDefinition, BpmnEscalationEventDefinition, BpmnMessageEventDefinition, BpmnSignalEventDefinition, BpmnConditionalEventDefinition, BpmnLinkEventDefinition, BpmnCancelEventDefinition, BpmnTerminateEventDefinition, BpmnCompensateEventDefinition, BpmnMultiInstanceLoopCharacteristics, BpmnDiagram, BpmnDiPlane, BpmnDiShape, BpmnDiEdge, BpmnDiLabel, BpmnBounds, BpmnWaypoint, } from "./bpmn/bpmn-model.js";
9
10
  export type { RestConnectorConfig, RestAuthentication, HttpMethod, } from "./bpmn/rest-connector.js";
10
11
  export type { ZeebeExtensions, ZeebeTaskDefinition, ZeebeIoMapping, ZeebeIoMappingEntry, ZeebeTaskHeaders, ZeebeTaskHeaderEntry, ZeebeProperties, ZeebePropertyEntry, ZeebeFormDefinition, ZeebeCalledDecision, } from "./bpmn/zeebe-extensions.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { isBpmnActivity, isBpmnAdHocSubProcess, isBpmnBoundaryEvent, isBpmnBusin
3
3
  export { findElement, findElementInProcess, findProcess, findSequenceFlow, getAllElements, getElementType, getZeebeExtensions, } from "./bpmn/utils.js";
4
4
  export { Bpmn, SAMPLE_BPMN_XML } from "./bpmn/index.js";
5
5
  export { applyAutoLayout } from "./bpmn/auto-layout.js";
6
+ export { DiagramBuilder } from "./bpmn/bpmn-builder.js";
6
7
  export { zeebeExtensionsToXmlElements } from "./bpmn/zeebe-extensions.js";
7
8
  export { Dmn, layoutDmn, benchmarkDmnLayout, compactifyDmn, expandDmn } from "./dmn/index.js";
8
9
  export { Form, compactifyForm, expandForm } from "./form/index.js";
@@ -10,7 +10,8 @@ import type { LayoutResult } from "./types.js";
10
10
  * 4. Coordinate assignment — Fixed element sizes with spacing
11
11
  * 5. Sub-process layout — Recursive nested passes
12
12
  * 6. Edge routing — Orthogonal waypoints
13
- * 7. Overlap assertionPost-condition validation
13
+ * 7. Boundary event repositioning place events on host border
14
+ * 8. Overlap assertion — Post-condition validation
14
15
  */
15
16
  export declare function layoutProcess(process: BpmnProcess): LayoutResult;
16
17
  /**
@@ -7,6 +7,133 @@ import { assignLayers, groupByLayer } from "./layers.js";
7
7
  import { assertNoOverlap } from "./overlap.js";
8
8
  import { routeEdges } from "./routing.js";
9
9
  import { layoutSubProcesses } from "./subprocess.js";
10
+ const CHAIN_GAP = 30;
11
+ const CHAIN_V_GAP = 20;
12
+ /**
13
+ * Reposition boundary events to the bottom edge of their host task, then walk
14
+ * each boundary event's exclusive downstream chain and place those nodes
15
+ * horizontally to the right of the host task. Re-routes all affected edges.
16
+ */
17
+ function repositionBoundaryEvents(flowElements, result) {
18
+ const boundaryMap = new Map();
19
+ for (const el of flowElements) {
20
+ if (el.type !== "boundaryEvent")
21
+ continue;
22
+ const list = boundaryMap.get(el.attachedToRef) ?? [];
23
+ list.push(el.id);
24
+ boundaryMap.set(el.attachedToRef, list);
25
+ }
26
+ if (boundaryMap.size === 0)
27
+ return;
28
+ const nodeById = new Map(result.nodes.map((n) => [n.id, n]));
29
+ const succIds = new Map();
30
+ const predIds = new Map();
31
+ for (const edge of result.edges) {
32
+ const se = succIds.get(edge.sourceRef) ?? [];
33
+ se.push(edge.targetRef);
34
+ succIds.set(edge.sourceRef, se);
35
+ const ps = predIds.get(edge.targetRef) ?? new Set();
36
+ ps.add(edge.sourceRef);
37
+ predIds.set(edge.targetRef, ps);
38
+ }
39
+ for (const [hostId, beIds] of boundaryMap) {
40
+ const hostNode = nodeById.get(hostId);
41
+ if (!hostNode)
42
+ continue;
43
+ for (let i = 0; i < beIds.length; i++) {
44
+ const beId = beIds[i];
45
+ if (!beId)
46
+ continue;
47
+ const beNode = nodeById.get(beId);
48
+ if (!beNode)
49
+ continue;
50
+ const bW = beNode.bounds.width;
51
+ const bH = beNode.bounds.height;
52
+ // Place boundary event on the bottom edge of the host task, stacking leftward
53
+ const rightEdge = hostNode.bounds.x + hostNode.bounds.width;
54
+ beNode.bounds.x = Math.round(rightEdge - bW / 2 - i * (bW + 4));
55
+ beNode.bounds.y = Math.round(hostNode.bounds.y + hostNode.bounds.height - bH / 2);
56
+ if (beNode.labelBounds) {
57
+ beNode.labelBounds.x = beNode.bounds.x + Math.round(bW / 2 - beNode.labelBounds.width / 2);
58
+ beNode.labelBounds.y = beNode.bounds.y + bH + 4;
59
+ }
60
+ // Collect nodes exclusively reachable from this boundary event (BFS)
61
+ const chainSet = new Set([beId]);
62
+ const chainOrder = [];
63
+ const queue = [...(succIds.get(beId) ?? [])];
64
+ while (queue.length > 0) {
65
+ const id = queue.shift();
66
+ if (!id || chainSet.has(id))
67
+ continue;
68
+ const preds = predIds.get(id) ?? new Set();
69
+ if ([...preds].every((p) => chainSet.has(p))) {
70
+ chainSet.add(id);
71
+ chainOrder.push(id);
72
+ queue.push(...(succIds.get(id) ?? []));
73
+ }
74
+ }
75
+ // Each boundary event's chain gets its own vertical lane
76
+ let maxChainH = 0;
77
+ for (const id of chainOrder) {
78
+ const n = nodeById.get(id);
79
+ if (n)
80
+ maxChainH = Math.max(maxChainH, n.bounds.height);
81
+ }
82
+ const laneOffset = i * (maxChainH + CHAIN_V_GAP + 10);
83
+ const chainCenterY = Math.round(beNode.bounds.y + bH + CHAIN_V_GAP + maxChainH / 2 + laneOffset);
84
+ const chainStartX = Math.max(Math.round(beNode.bounds.x + bW / 2) + CHAIN_GAP, hostNode.bounds.x + hostNode.bounds.width + CHAIN_GAP);
85
+ let curX = chainStartX;
86
+ for (const id of chainOrder) {
87
+ const n = nodeById.get(id);
88
+ if (!n)
89
+ continue;
90
+ n.bounds.x = curX;
91
+ n.bounds.y = chainCenterY - Math.round(n.bounds.height / 2);
92
+ if (n.labelBounds) {
93
+ // Center label on node, but clamp so it never extends left of the node —
94
+ // wide event labels would otherwise overlap the preceding chain element.
95
+ const labelX = n.bounds.x + Math.round(n.bounds.width / 2 - n.labelBounds.width / 2);
96
+ n.labelBounds.x = Math.max(labelX, n.bounds.x);
97
+ n.labelBounds.y = n.bounds.y + n.bounds.height + 4;
98
+ }
99
+ // Advance past the node AND its label so the next element doesn't overlap.
100
+ const nodeRight = n.bounds.x + n.bounds.width;
101
+ const labelRight = n.labelBounds ? n.labelBounds.x + n.labelBounds.width : 0;
102
+ curX = Math.max(nodeRight, labelRight) + CHAIN_GAP;
103
+ }
104
+ // Re-route edges touching the boundary event or its chain
105
+ for (const edge of result.edges) {
106
+ if (!chainSet.has(edge.sourceRef))
107
+ continue;
108
+ const src = nodeById.get(edge.sourceRef);
109
+ const tgt = nodeById.get(edge.targetRef);
110
+ if (!src || !tgt)
111
+ continue;
112
+ if (edge.sourceRef === beId) {
113
+ const srcX = Math.round(src.bounds.x + bW / 2);
114
+ const srcY = Math.round(src.bounds.y + bH);
115
+ const tgtX = Math.round(tgt.bounds.x);
116
+ const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
117
+ edge.waypoints = [
118
+ { x: srcX, y: srcY },
119
+ { x: srcX, y: tgtY },
120
+ { x: tgtX, y: tgtY },
121
+ ];
122
+ }
123
+ else {
124
+ const srcX = Math.round(src.bounds.x + src.bounds.width);
125
+ const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
126
+ const tgtX = Math.round(tgt.bounds.x);
127
+ const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
128
+ edge.waypoints = [
129
+ { x: srcX, y: srcY },
130
+ { x: tgtX, y: tgtY },
131
+ ];
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
10
137
  /**
11
138
  * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
12
139
  *
@@ -17,10 +144,12 @@ import { layoutSubProcesses } from "./subprocess.js";
17
144
  * 4. Coordinate assignment — Fixed element sizes with spacing
18
145
  * 5. Sub-process layout — Recursive nested passes
19
146
  * 6. Edge routing — Orthogonal waypoints
20
- * 7. Overlap assertionPost-condition validation
147
+ * 7. Boundary event repositioning place events on host border
148
+ * 8. Overlap assertion — Post-condition validation
21
149
  */
22
150
  export function layoutProcess(process) {
23
151
  const result = layoutFlowNodes(process.flowElements, process.sequenceFlows);
152
+ repositionBoundaryEvents(process.flowElements, result);
24
153
  assertNoOverlap(result);
25
154
  return result;
26
155
  }
@@ -8,6 +8,9 @@
8
8
  export function assertNoOverlap(result) {
9
9
  const allBounds = [];
10
10
  for (const node of result.nodes) {
11
+ // Boundary events intentionally overlap their host activity — skip them.
12
+ if (node.type === "boundaryEvent")
13
+ continue;
11
14
  allBounds.push({ id: node.id, kind: "element", bounds: node.bounds });
12
15
  if (node.labelBounds) {
13
16
  allBounds.push({ id: `${node.id}-label`, kind: "label", bounds: node.labelBounds });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/core",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "dist/**/*.d.ts"
18
18
  ],
19
19
  "dependencies": {
20
- "@bpmnkit/feel": "0.0.18"
20
+ "@bpmnkit/feel": "0.0.19"
21
21
  },
22
22
  "description": "TypeScript-first BPMN 2.0 SDK — parse, build, layout, and optimize diagrams",
23
23
  "keywords": [