@bpmnkit/core 0.0.26 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/bpmn/auto-layout.js +71 -139
  2. package/dist/bpmn/bpmn-builder.d.ts +30 -0
  3. package/dist/bpmn/bpmn-builder.js +343 -31
  4. package/dist/bpmn/di-check.d.ts +14 -0
  5. package/dist/bpmn/di-check.js +51 -0
  6. package/dist/bpmn/svg.js +14 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +1 -0
  9. package/dist/layout/annotations.d.ts +27 -0
  10. package/dist/layout/annotations.js +251 -0
  11. package/dist/layout/grid/edge-labels.d.ts +8 -0
  12. package/dist/layout/grid/edge-labels.js +126 -0
  13. package/dist/layout/grid/flow-graph.d.ts +25 -0
  14. package/dist/layout/grid/flow-graph.js +99 -0
  15. package/dist/layout/grid/grid-engine.d.ts +4 -0
  16. package/dist/layout/grid/grid-engine.js +214 -0
  17. package/dist/layout/grid/grid-router.d.ts +36 -0
  18. package/dist/layout/grid/grid-router.js +190 -0
  19. package/dist/layout/grid/grid.d.ts +43 -0
  20. package/dist/layout/grid/grid.js +174 -0
  21. package/dist/layout/grid/walker.d.ts +11 -0
  22. package/dist/layout/grid/walker.js +126 -0
  23. package/dist/layout/index.d.ts +2 -5
  24. package/dist/layout/index.js +1 -4
  25. package/dist/layout/layout-engine.d.ts +5 -15
  26. package/dist/layout/layout-engine.js +8 -478
  27. package/dist/layout/types.d.ts +2 -2
  28. package/dist/layout/types.js +6 -2
  29. package/dist/xml/xml-parser.js +5 -0
  30. package/package.json +1 -1
  31. package/dist/layout/astar.d.ts +0 -15
  32. package/dist/layout/astar.js +0 -191
  33. package/dist/layout/block-builder.d.ts +0 -37
  34. package/dist/layout/block-builder.js +0 -154
  35. package/dist/layout/block-layout.d.ts +0 -9
  36. package/dist/layout/block-layout.js +0 -163
  37. package/dist/layout/coordinates.d.ts +0 -85
  38. package/dist/layout/coordinates.js +0 -1392
  39. package/dist/layout/crossing.d.ts +0 -8
  40. package/dist/layout/crossing.js +0 -60
  41. package/dist/layout/graph.d.ts +0 -28
  42. package/dist/layout/graph.js +0 -126
  43. package/dist/layout/layers.d.ts +0 -13
  44. package/dist/layout/layers.js +0 -49
  45. package/dist/layout/routing.d.ts +0 -33
  46. package/dist/layout/routing.js +0 -622
  47. package/dist/layout/subprocess.d.ts +0 -14
  48. package/dist/layout/subprocess.js +0 -77
@@ -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`,
@@ -201,19 +107,38 @@ function buildLaneShapes(lanes, nodes, dx, dy, poolY, poolHeaderWidth, laneConte
201
107
  const mB = accB && accB.count > 0 ? accB.sum / accB.count : Number.POSITIVE_INFINITY;
202
108
  return mA - mB;
203
109
  });
204
- const tileH = Math.round(poolHeight / sortedLanes.length);
205
- return sortedLanes.map((lane, i) => ({
206
- id: `${lane.id}_di`,
207
- bpmnElement: lane.id,
208
- isHorizontal: true,
209
- bounds: {
210
- x: Math.round(poolHeaderWidth),
211
- y: Math.round(poolY + i * tileH),
212
- width: Math.round(laneContentWidth),
213
- height: Math.round(i === sortedLanes.length - 1 ? poolHeight - i * tileH : tileH),
214
- },
215
- unknownAttributes: {},
216
- }));
110
+ // Compute proportional weight per lane: node count × row height, minimum 1 row
111
+ const MIN_LANE_H = 80;
112
+ const weights = sortedLanes.map((lane) => {
113
+ const count = nodes.filter((n) => elemToLane.get(n.id) === lane.id).length;
114
+ return Math.max(count, 1);
115
+ });
116
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
117
+ // Scale proportionally to poolHeight so all lanes fill the pool exactly
118
+ const scaledHeights = weights.map((w) => Math.round((w / totalWeight) * poolHeight));
119
+ // Fix last lane for rounding drift
120
+ if (scaledHeights.length > 0) {
121
+ scaledHeights[scaledHeights.length - 1] =
122
+ poolHeight - scaledHeights.slice(0, -1).reduce((a, b) => a + b, 0);
123
+ }
124
+ let cumulativeY = 0;
125
+ return sortedLanes.map((lane, i) => {
126
+ const laneH = scaledHeights[i] ?? MIN_LANE_H;
127
+ const shape = {
128
+ id: `${lane.id}_di`,
129
+ bpmnElement: lane.id,
130
+ isHorizontal: true,
131
+ bounds: {
132
+ x: Math.round(poolHeaderWidth),
133
+ y: Math.round(poolY + cumulativeY),
134
+ width: Math.round(laneContentWidth),
135
+ height: Math.round(laneH),
136
+ },
137
+ unknownAttributes: {},
138
+ };
139
+ cumulativeY += laneH;
140
+ return shape;
141
+ });
217
142
  }
218
143
  function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, allEdges, dx, dy) {
219
144
  if (process.textAnnotations.length === 0 && process.associations.length === 0)
@@ -244,33 +169,17 @@ function addAnnotationShapes(process, layoutNodes, annLocalBounds, allShapes, al
244
169
  const elId = annId === assoc.sourceRef ? assoc.targetRef : assoc.sourceRef;
245
170
  const annB = annId ? annLocalBounds.get(annId) : undefined;
246
171
  const elNode = nodeById.get(elId);
247
- if (!annB || !elNode)
172
+ if (!annB || !annId || !elNode)
248
173
  continue;
249
- const elB = elNode.bounds;
250
- const annCx = Math.round(annB.x + annB.width / 2 + dx);
251
- const elCx = Math.round(elB.x + elB.width / 2 + dx);
252
- let waypoints;
253
- if (annB.y >= elB.y + elB.height) {
254
- // annotation below: element bottom-center → annotation top-center
255
- waypoints = [
256
- { x: elCx, y: Math.round(elB.y + elB.height + dy) },
257
- { x: annCx, y: Math.round(annB.y + dy) },
258
- ];
259
- }
260
- else if (annB.y + annB.height <= elB.y) {
261
- // annotation above: element top-center → annotation bottom-center
262
- waypoints = [
263
- { x: elCx, y: Math.round(elB.y + dy) },
264
- { x: annCx, y: Math.round(annB.y + annB.height + dy) },
265
- ];
266
- }
267
- else {
268
- // side-by-side: center-to-center
269
- waypoints = [
270
- { x: Math.round(elB.x + elB.width / 2 + dx), y: Math.round(elB.y + elB.height / 2 + dy) },
271
- { x: annCx, y: Math.round(annB.y + annB.height / 2 + dy) },
272
- ];
273
- }
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];
274
183
  allEdges.push({
275
184
  id: `${assoc.id}_di`,
276
185
  bpmnElement: assoc.id,
@@ -305,15 +214,11 @@ export function applyAutoLayout(defs) {
305
214
  const participantId = processToParticipant.get(process.id);
306
215
  const lanes = process.laneSet?.lanes ?? [];
307
216
  const hasLanes = lanes.length > 0;
308
- // layoutProcess now handles boundary event repositioning internally.
309
217
  const result = layoutProcess(process);
310
- // Re-resolve edge crossings after boundary events moved shapes
311
- const nodeMap = new Map(result.nodes.map((n) => [n.id, n]));
312
- resolveEdgeCrossings(result.edges, nodeMap);
313
218
  if (result.nodes.length === 0)
314
219
  continue;
315
220
  // Pre-compute annotation positions in layout space so they're included in the bbox
316
- const annBounds = computeAnnotationLocalBounds(process, result.nodes);
221
+ const annBounds = packAnnotations(process, result.nodes);
317
222
  const { minX, minY, maxX, maxY } = contentBbox(result.nodes, annBounds.values());
318
223
  const contentW = maxX - minX;
319
224
  const contentH = maxY - minY;
@@ -352,6 +257,33 @@ export function applyAutoLayout(defs) {
352
257
  poolY += innerH + POOL_GAP;
353
258
  }
354
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
+ }
355
287
  const planeBpmnElement = collab?.id ?? defs.processes[0]?.id ?? "plane";
356
288
  const existingDiagram = defs.diagrams[0];
357
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 {