@bpmnkit/core 0.0.27 → 0.1.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.
Files changed (56) hide show
  1. package/dist/bpmn/auto-layout.js +39 -126
  2. package/dist/bpmn/bpmn-builder.d.ts +30 -0
  3. package/dist/bpmn/bpmn-builder.js +350 -31
  4. package/dist/bpmn/bpmn-model.d.ts +35 -2
  5. package/dist/bpmn/bpmn-parser.js +39 -1
  6. package/dist/bpmn/bpmn-serializer.js +27 -0
  7. package/dist/bpmn/compact.js +11 -1
  8. package/dist/bpmn/di-check.d.ts +14 -0
  9. package/dist/bpmn/di-check.js +51 -0
  10. package/dist/bpmn/di-planes.d.ts +13 -0
  11. package/dist/bpmn/di-planes.js +20 -0
  12. package/dist/bpmn/optimize/tasks.js +1 -0
  13. package/dist/bpmn/svg.js +36 -4
  14. package/dist/bpmn/type-guards.d.ts +7 -1
  15. package/dist/bpmn/type-guards.js +13 -0
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.js +3 -1
  18. package/dist/layout/annotations.d.ts +27 -0
  19. package/dist/layout/annotations.js +251 -0
  20. package/dist/layout/grid/edge-labels.d.ts +8 -0
  21. package/dist/layout/grid/edge-labels.js +126 -0
  22. package/dist/layout/grid/flow-graph.d.ts +25 -0
  23. package/dist/layout/grid/flow-graph.js +99 -0
  24. package/dist/layout/grid/grid-engine.d.ts +4 -0
  25. package/dist/layout/grid/grid-engine.js +214 -0
  26. package/dist/layout/grid/grid-router.d.ts +36 -0
  27. package/dist/layout/grid/grid-router.js +190 -0
  28. package/dist/layout/grid/grid.d.ts +43 -0
  29. package/dist/layout/grid/grid.js +174 -0
  30. package/dist/layout/grid/walker.d.ts +11 -0
  31. package/dist/layout/grid/walker.js +126 -0
  32. package/dist/layout/index.d.ts +2 -5
  33. package/dist/layout/index.js +1 -4
  34. package/dist/layout/layout-engine.d.ts +5 -15
  35. package/dist/layout/layout-engine.js +8 -486
  36. package/dist/layout/types.js +4 -0
  37. package/dist/xml/xml-parser.js +5 -0
  38. package/package.json +1 -1
  39. package/dist/layout/astar.d.ts +0 -15
  40. package/dist/layout/astar.js +0 -191
  41. package/dist/layout/block-builder.d.ts +0 -37
  42. package/dist/layout/block-builder.js +0 -154
  43. package/dist/layout/block-layout.d.ts +0 -9
  44. package/dist/layout/block-layout.js +0 -163
  45. package/dist/layout/coordinates.d.ts +0 -85
  46. package/dist/layout/coordinates.js +0 -1392
  47. package/dist/layout/crossing.d.ts +0 -8
  48. package/dist/layout/crossing.js +0 -60
  49. package/dist/layout/graph.d.ts +0 -28
  50. package/dist/layout/graph.js +0 -126
  51. package/dist/layout/layers.d.ts +0 -13
  52. package/dist/layout/layers.js +0 -49
  53. package/dist/layout/routing.d.ts +0 -33
  54. package/dist/layout/routing.js +0 -622
  55. package/dist/layout/subprocess.d.ts +0 -14
  56. package/dist/layout/subprocess.js +0 -115
@@ -1,12 +1,9 @@
1
+ import { associationWaypoints, packAnnotations } from "../layout/annotations.js";
1
2
  import { layoutProcess } from "../layout/layout-engine.js";
2
- import { resolveEdgeCrossings } from "../layout/routing.js";
3
3
  const POOL_HEADER = 30;
4
4
  const LANE_HEADER = 30;
5
5
  const PADDING = 20;
6
6
  const POOL_GAP = 30;
7
- const ANN_H = 50;
8
- const ANN_GAP = 60;
9
- const ANN_PADDING = 20;
10
7
  function contentBbox(nodes, extra) {
11
8
  let minX = Number.POSITIVE_INFINITY;
12
9
  let minY = Number.POSITIVE_INFINITY;
@@ -34,97 +31,6 @@ function contentBbox(nodes, extra) {
34
31
  }
35
32
  return { minX, minY, maxX, maxY };
36
33
  }
37
- /** Pre-compute annotation positions in layout space (before dx/dy shift). */
38
- function computeAnnotationLocalBounds(process, layoutNodes) {
39
- const nodeById = new Map(layoutNodes.map((n) => [n.id, n]));
40
- const placements = new Map();
41
- // Occupied regions for overlap checks (node bounds + label bounds)
42
- const occupied = [];
43
- for (const n of layoutNodes) {
44
- occupied.push({ ...n.bounds });
45
- if (n.labelBounds)
46
- occupied.push({ ...n.labelBounds });
47
- }
48
- // Static obstacles for crossing detection (nodes + labels only, not annotations)
49
- const obstacles = [...occupied];
50
- for (const ta of process.textAnnotations) {
51
- const assoc = process.associations.find((a) => a.sourceRef === ta.id || a.targetRef === ta.id);
52
- const connId = assoc
53
- ? assoc.sourceRef === ta.id
54
- ? assoc.targetRef
55
- : assoc.sourceRef
56
- : undefined;
57
- const connNode = connId ? nodeById.get(connId) : undefined;
58
- const annW = Math.min(200, Math.max(100, (ta.text?.length ?? 10) * 5));
59
- if (!connNode) {
60
- const candidate = { x: 0, y: 0, width: annW, height: ANN_H };
61
- occupied.push({ ...candidate });
62
- placements.set(ta.id, candidate);
63
- continue;
64
- }
65
- const localX = connNode.bounds.x + connNode.bounds.width / 2 - annW / 2;
66
- const anchorX = connNode.bounds.x + connNode.bounds.width / 2;
67
- const pushStep = ANN_H + ANN_PADDING * 2 + 10;
68
- // Try below: start below connected element, push down for overlaps
69
- const belowY = connNode.bounds.y + connNode.bounds.height + ANN_GAP;
70
- const below = { x: localX, y: belowY, width: annW, height: ANN_H };
71
- for (let i = 0; i < 30 && hasOverlapPadded(below, occupied, ANN_PADDING); i++)
72
- below.y += pushStep;
73
- // Try above: gap scales with text length so longer annotations have more breathing room
74
- const aboveGap = ANN_GAP + Math.round(annW * 0.2);
75
- const aboveY = connNode.bounds.y - aboveGap - ANN_H;
76
- const above = { x: localX, y: aboveY, width: annW, height: ANN_H };
77
- for (let i = 0; i < 30 && hasOverlapPadded(above, occupied, ANN_PADDING); i++)
78
- above.y -= pushStep;
79
- // Count how many obstacles the association line would cross for each candidate
80
- const belowCrossings = countLineCrossings(anchorX, connNode.bounds, below, obstacles);
81
- const aboveCrossings = countLineCrossings(anchorX, connNode.bounds, above, obstacles);
82
- const candidate = belowCrossings <= aboveCrossings ? below : above;
83
- occupied.push({ ...candidate });
84
- obstacles.push({ ...candidate });
85
- placements.set(ta.id, candidate);
86
- }
87
- return placements;
88
- }
89
- /** Count how many obstacles the vertical association line from connNode to annotation crosses. */
90
- function countLineCrossings(lineX, connBounds, annBounds, obstacles) {
91
- const annCY = annBounds.y + annBounds.height / 2;
92
- const connCY = connBounds.y + connBounds.height / 2;
93
- const top = Math.min(annCY, connCY);
94
- const bottom = Math.max(annCY, connCY);
95
- const tolerance = 20;
96
- let crossings = 0;
97
- for (const b of obstacles) {
98
- // Skip the connected element itself
99
- if (b.x === connBounds.x && b.y === connBounds.y && b.width === connBounds.width)
100
- continue;
101
- // Obstacle must overlap with the line's X corridor
102
- if (b.x + b.width < lineX - tolerance || b.x > lineX + tolerance)
103
- continue;
104
- // Obstacle must be between connNode and annotation vertically
105
- if (b.y + b.height <= top || b.y >= bottom)
106
- continue;
107
- crossings++;
108
- }
109
- return crossings;
110
- }
111
- function hasOverlap(a, others) {
112
- for (const b of others) {
113
- if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y)
114
- return true;
115
- }
116
- return false;
117
- }
118
- function hasOverlapPadded(a, others, padding) {
119
- for (const b of others) {
120
- if (a.x - padding < b.x + b.width &&
121
- a.x + a.width + padding > b.x &&
122
- a.y - padding < b.y + b.height &&
123
- a.y + a.height + padding > b.y)
124
- return true;
125
- }
126
- return false;
127
- }
128
34
  function nodeToShape(node, dx, dy) {
129
35
  const shape = {
130
36
  id: `${node.id}_di`,
@@ -263,33 +169,17 @@ function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, al
263
169
  const elId = annId === assoc.sourceRef ? assoc.targetRef : assoc.sourceRef;
264
170
  const annB = annId ? annLocalBounds.get(annId) : undefined;
265
171
  const elNode = nodeById.get(elId);
266
- if (!annB || !elNode)
172
+ if (!annB || !annId || !elNode)
267
173
  continue;
268
- const elB = elNode.bounds;
269
- const annCx = Math.round(annB.x + annB.width / 2 + dx);
270
- const elCx = Math.round(elB.x + elB.width / 2 + dx);
271
- let waypoints;
272
- if (annB.y >= elB.y + elB.height) {
273
- // annotation below: element bottom-center → annotation top-center
274
- waypoints = [
275
- { x: elCx, y: Math.round(elB.y + elB.height + dy) },
276
- { x: annCx, y: Math.round(annB.y + dy) },
277
- ];
278
- }
279
- else if (annB.y + annB.height <= elB.y) {
280
- // annotation above: element top-center → annotation bottom-center
281
- waypoints = [
282
- { x: elCx, y: Math.round(elB.y + dy) },
283
- { x: annCx, y: Math.round(annB.y + annB.height + dy) },
284
- ];
285
- }
286
- else {
287
- // side-by-side: center-to-center
288
- waypoints = [
289
- { x: Math.round(elB.x + elB.width / 2 + dx), y: Math.round(elB.y + elB.height / 2 + dy) },
290
- { x: annCx, y: Math.round(annB.y + annB.height / 2 + dy) },
291
- ];
292
- }
174
+ const { pElem, pAnn } = associationWaypoints(elNode.bounds, annB);
175
+ // Shift into diagram space and honour the original sourceRef→targetRef order.
176
+ const shifted = {
177
+ pElem: { x: Math.round(pElem.x + dx), y: Math.round(pElem.y + dy) },
178
+ pAnn: { x: Math.round(pAnn.x + dx), y: Math.round(pAnn.y + dy) },
179
+ };
180
+ const wp1 = assoc.sourceRef === annId ? shifted.pAnn : shifted.pElem;
181
+ const wp2 = assoc.sourceRef === annId ? shifted.pElem : shifted.pAnn;
182
+ const waypoints = [wp1, wp2];
293
183
  allEdges.push({
294
184
  id: `${assoc.id}_di`,
295
185
  bpmnElement: assoc.id,
@@ -324,15 +214,11 @@ export function applyAutoLayout(defs) {
324
214
  const participantId = processToParticipant.get(process.id);
325
215
  const lanes = process.laneSet?.lanes ?? [];
326
216
  const hasLanes = lanes.length > 0;
327
- // layoutProcess now handles boundary event repositioning internally.
328
217
  const result = layoutProcess(process);
329
- // Re-resolve edge crossings after boundary events moved shapes
330
- const nodeMap = new Map(result.nodes.map((n) => [n.id, n]));
331
- resolveEdgeCrossings(result.edges, nodeMap);
332
218
  if (result.nodes.length === 0)
333
219
  continue;
334
220
  // Pre-compute annotation positions in layout space so they're included in the bbox
335
- const annBounds = computeAnnotationLocalBounds(process, result.nodes);
221
+ const annBounds = packAnnotations(process, result.nodes);
336
222
  const { minX, minY, maxX, maxY } = contentBbox(result.nodes, annBounds.values());
337
223
  const contentW = maxX - minX;
338
224
  const contentH = maxY - minY;
@@ -371,6 +257,33 @@ export function applyAutoLayout(defs) {
371
257
  poolY += innerH + POOL_GAP;
372
258
  }
373
259
  }
260
+ if (collab && collab.messageFlows.length > 0) {
261
+ const shapeByElement = new Map(allShapes.map((s) => [s.bpmnElement, s.bounds]));
262
+ for (const mf of collab.messageFlows) {
263
+ const src = shapeByElement.get(mf.sourceRef);
264
+ const tgt = shapeByElement.get(mf.targetRef);
265
+ if (!src || !tgt)
266
+ continue;
267
+ const srcBelow = src.y + src.height / 2 > tgt.y + tgt.height / 2;
268
+ const sx = Math.round(src.x + src.width / 2);
269
+ const tx = Math.round(tgt.x + tgt.width / 2);
270
+ const sy = srcBelow ? src.y : src.y + src.height;
271
+ const ty = srcBelow ? tgt.y + tgt.height : tgt.y;
272
+ const midY = Math.round((sy + ty) / 2);
273
+ const waypoints = sx === tx
274
+ ? [
275
+ { x: sx, y: sy },
276
+ { x: tx, y: ty },
277
+ ]
278
+ : [
279
+ { x: sx, y: sy },
280
+ { x: sx, y: midY },
281
+ { x: tx, y: midY },
282
+ { x: tx, y: ty },
283
+ ];
284
+ allEdges.push({ id: `${mf.id}_di`, bpmnElement: mf.id, waypoints, unknownAttributes: {} });
285
+ }
286
+ }
374
287
  const planeBpmnElement = collab?.id ?? defs.processes[0]?.id ?? "plane";
375
288
  const existingDiagram = defs.diagrams[0];
376
289
  return {
@@ -321,6 +321,12 @@ export declare class BranchBuilder {
321
321
  * @param handler - Callback that chains elements from the boundary event.
322
322
  */
323
323
  withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: BranchBuilder) => void): this;
324
+ /** Add a sub-process (mirrors `ProcessBuilder.subProcess`). */
325
+ subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
326
+ /** Add an ad-hoc sub-process (mirrors `ProcessBuilder.adHocSubProcess`). */
327
+ adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
328
+ /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
329
+ eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
324
330
  /**
325
331
  * Create a named branch from the last gateway added inside this branch.
326
332
  *
@@ -372,6 +378,30 @@ export declare class SubProcessContentBuilder {
372
378
  branch(name: string, callback: (b: BranchBuilder) => void): this;
373
379
  connectTo(targetId: string): this;
374
380
  element(elementId: string): this;
381
+ /**
382
+ * Add a boundary event attached to an existing activity in this sub-process.
383
+ *
384
+ * The boundary event is NOT connected by a sequence flow — it attaches via
385
+ * `attachedToRef`. The builder cursor advances to the boundary event so
386
+ * subsequent elements chain from it. Use `withBoundary()` if you want the
387
+ * cursor to return to the task afterward.
388
+ */
389
+ boundaryEvent(id: string, options: BoundaryEventOptions): this;
390
+ /**
391
+ * Attach a boundary event to the preceding task and build its outgoing path,
392
+ * then restore the builder cursor to the preceding task so the main flow continues.
393
+ *
394
+ * @param id - ID for the boundary event element.
395
+ * @param options - Boundary event options (without `attachedTo` — inferred from cursor).
396
+ * @param handler - Callback that chains elements from the boundary event.
397
+ */
398
+ withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: SubProcessContentBuilder) => void): this;
399
+ /** Add a sub-process nested inside this sub-process's content. */
400
+ subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
401
+ /** Add an ad-hoc sub-process nested inside this sub-process's content. */
402
+ adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
403
+ /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
404
+ eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
375
405
  }
376
406
  /** Fluent builder for constructing BPMN processes. */
377
407
  export declare class ProcessBuilder {