bpmn-auto-layout-extended 1.0.2

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/index.js ADDED
@@ -0,0 +1,1669 @@
1
+ import { BpmnModdle } from 'bpmn-moddle';
2
+ import { assign, map, pick, isFunction } from 'min-dash';
3
+
4
+ function isConnection(element) {
5
+ return !!element.sourceRef;
6
+ }
7
+
8
+ function isBoundaryEvent(element) {
9
+ return !!element.attachedToRef;
10
+ }
11
+
12
+ function findElementInTree(currentElement, targetElement, visited = new Set()) {
13
+
14
+ if (currentElement === targetElement) return true;
15
+
16
+ if (visited.has(currentElement)) return false;
17
+
18
+ visited.add(currentElement);
19
+
20
+ // If currentElement has no outgoing connections, return false
21
+ if (!currentElement.outgoing || currentElement.outgoing.length === 0) return false;
22
+
23
+ // Recursively check each outgoing element
24
+ for (let nextElement of currentElement.outgoing.map(out => out.targetRef)) {
25
+ if (findElementInTree(nextElement, targetElement, visited)) {
26
+ return true;
27
+ }
28
+ }
29
+
30
+ return false;
31
+ }
32
+
33
+ const DEFAULT_TASK_HEIGHT = 80;
34
+ const DEFAULT_TASK_WIDTH = 100;
35
+
36
+ function getDefaultSize(element) {
37
+ if (is(element, 'bpmn:SubProcess')) {
38
+ return { width: DEFAULT_TASK_WIDTH, height: DEFAULT_TASK_HEIGHT };
39
+ }
40
+
41
+ if (is(element, 'bpmn:Task')) {
42
+ return { width: DEFAULT_TASK_WIDTH, height: DEFAULT_TASK_HEIGHT };
43
+ }
44
+
45
+ if (is(element, 'bpmn:Gateway')) {
46
+ return { width: 50, height: 50 };
47
+ }
48
+
49
+ if (is(element, 'bpmn:Event')) {
50
+ return { width: 36, height: 36 };
51
+ }
52
+
53
+ if (is(element, 'bpmn:Participant')) {
54
+ return { width: 400, height: 100 };
55
+ }
56
+
57
+ if (is(element, 'bpmn:Lane')) {
58
+ return { width: 400, height: 100 };
59
+ }
60
+
61
+ if (is(element, 'bpmn:DataObjectReference')) {
62
+ return { width: 36, height: 50 };
63
+ }
64
+
65
+ if (is(element, 'bpmn:DataStoreReference')) {
66
+ return { width: 50, height: 50 };
67
+ }
68
+
69
+ if (is(element, 'bpmn:TextAnnotation')) {
70
+ return { width: DEFAULT_TASK_WIDTH, height: 30 };
71
+ }
72
+
73
+ return { width: DEFAULT_TASK_WIDTH, height: DEFAULT_TASK_HEIGHT };
74
+ }
75
+
76
+ function is(element, type) {
77
+ return element.$instanceOf(type);
78
+ }
79
+
80
+ const DEFAULT_CELL_WIDTH = 150;
81
+ const DEFAULT_CELL_HEIGHT = 140;
82
+
83
+ function getMid(bounds) {
84
+ return {
85
+ x: bounds.x + bounds.width / 2,
86
+ y: bounds.y + bounds.height / 2
87
+ };
88
+ }
89
+
90
+ function getDockingPoint(point, rectangle, dockingDirection = 'r', targetOrientation = 'top-left') {
91
+
92
+ // ensure we end up with a specific docking direction
93
+ // based on the targetOrientation, if <h|v> is being passed
94
+ if (dockingDirection === 'h') {
95
+ dockingDirection = /left/.test(targetOrientation) ? 'l' : 'r';
96
+ }
97
+
98
+ if (dockingDirection === 'v') {
99
+ dockingDirection = /top/.test(targetOrientation) ? 't' : 'b';
100
+ }
101
+
102
+ if (dockingDirection === 't') {
103
+ return { original: point, x: point.x, y: rectangle.y };
104
+ }
105
+
106
+ if (dockingDirection === 'r') {
107
+ return { original: point, x: rectangle.x + rectangle.width, y: point.y };
108
+ }
109
+
110
+ if (dockingDirection === 'b') {
111
+ return { original: point, x: point.x, y: rectangle.y + rectangle.height };
112
+ }
113
+
114
+ if (dockingDirection === 'l') {
115
+ return { original: point, x: rectangle.x, y: point.y };
116
+ }
117
+
118
+ throw new Error('unexpected dockingDirection: <' + dockingDirection + '>');
119
+ }
120
+
121
+ /**
122
+ * Modified Manhattan layout: Uses space between grid columns to route connections
123
+ * if direct connection is not possible.
124
+ * @param {*} source
125
+ * @param {*} target
126
+ * @param layoutGrid
127
+ * @returns waypoints
128
+ */
129
+ function connectElements(source, target, layoutGrid) {
130
+ const sourceDi = source.di;
131
+ const targetDi = target.di;
132
+
133
+ const sourceBounds = sourceDi.get('bounds');
134
+ const targetBounds = targetDi.get('bounds');
135
+
136
+ const sourceMid = getMid(sourceBounds);
137
+ const targetMid = getMid(targetBounds);
138
+
139
+ const dX = target.gridPosition.col - source.gridPosition.col;
140
+ const dY = target.gridPosition.row - source.gridPosition.row;
141
+
142
+ const dockingSource = `${(dY > 0 ? 'bottom' : 'top')}-${dX > 0 ? 'right' : 'left'}`;
143
+ const dockingTarget = `${(dY > 0 ? 'top' : 'bottom')}-${dX > 0 ? 'left' : 'right'}`;
144
+
145
+ const baseSourceGrid = source.grid || source.attachedToRef?.grid;
146
+ const baseTargetGrid = target.grid;
147
+
148
+ // Source === Target ==> Build loop
149
+ if (dX === 0 && dY === 0) {
150
+ const { x, y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
151
+ return [
152
+ getDockingPoint(sourceMid, sourceBounds, 'r', dockingSource),
153
+ { x: baseSourceGrid ? x + (baseSourceGrid.getGridDimensions()[1] + 1) * DEFAULT_CELL_WIDTH : x + DEFAULT_CELL_WIDTH, y: sourceMid.y },
154
+ { x: baseSourceGrid ? x + (baseSourceGrid.getGridDimensions()[1] + 1) * DEFAULT_CELL_WIDTH : x + DEFAULT_CELL_WIDTH, y: y },
155
+ { x: targetMid.x, y: y },
156
+ getDockingPoint(targetMid, targetBounds, 't', dockingTarget)
157
+ ];
158
+ }
159
+
160
+ // negative dX indicates connection from future to past
161
+ if (dX < 0) {
162
+
163
+ const { y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
164
+
165
+ const offsetY = DEFAULT_CELL_HEIGHT / 2;
166
+
167
+ if (sourceMid.y >= targetMid.y) {
168
+
169
+ // edge goes below
170
+ const maxExpanded = getMaxExpandedBetween(source, target, layoutGrid);
171
+
172
+ if (maxExpanded) {
173
+ return [
174
+ getDockingPoint(sourceMid, sourceBounds, 'b'),
175
+ { x: sourceMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT + maxExpanded * DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
176
+ { x: targetMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT + maxExpanded * DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
177
+ getDockingPoint(targetMid, targetBounds, 'b')
178
+ ];
179
+ }
180
+
181
+ return [
182
+ getDockingPoint(sourceMid, sourceBounds, 'b'),
183
+ { x: sourceMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
184
+ { x: targetMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
185
+ getDockingPoint(targetMid, targetBounds, 'b')
186
+ ];
187
+ } else {
188
+
189
+ // edge goes above
190
+ const bendY = sourceMid.y - offsetY;
191
+
192
+ return [
193
+ getDockingPoint(sourceMid, sourceBounds, 't'),
194
+ { x: sourceMid.x, y: bendY },
195
+ { x: targetMid.x, y: bendY },
196
+ getDockingPoint(targetMid, targetBounds, 't')
197
+ ];
198
+ }
199
+ }
200
+
201
+ // connect horizontally
202
+ if (dY === 0) {
203
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
204
+ const { y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
205
+
206
+ // Route on bottom
207
+ return [
208
+ getDockingPoint(sourceMid, sourceBounds, 'b'),
209
+ { x: sourceMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
210
+ { x: targetMid.x, y: !baseSourceGrid ? y + DEFAULT_CELL_HEIGHT : y + (baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
211
+ getDockingPoint(targetMid, targetBounds, 'b')
212
+ ];
213
+ } else {
214
+
215
+ // if space is clear, connect directly
216
+ const firstPoint = getDockingPoint(sourceMid, sourceBounds, 'h', dockingSource);
217
+ const lastPoint = getDockingPoint(targetMid, targetBounds, 'h', dockingTarget);
218
+ if (baseSourceGrid) {
219
+ firstPoint.y = sourceBounds.y + DEFAULT_TASK_HEIGHT / 2 ;
220
+ }
221
+
222
+ if (baseTargetGrid) {
223
+ lastPoint.y = targetBounds.y + DEFAULT_TASK_HEIGHT / 2 ;
224
+ }
225
+ return [
226
+ firstPoint,
227
+ lastPoint
228
+ ];
229
+ }
230
+ }
231
+
232
+ // connect vertically
233
+ if (dX === 0) {
234
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
235
+
236
+ // Route parallel
237
+ const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
238
+ return [
239
+ getDockingPoint(sourceMid, sourceBounds, 'r'),
240
+ { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y }, // out right
241
+ { x: targetMid.x + DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset },
242
+ { x: targetMid.x, y: targetMid.y + yOffset },
243
+ getDockingPoint(targetMid, targetBounds, Math.sign(yOffset) > 0 ? 'b' : 't')
244
+ ];
245
+ } else {
246
+
247
+ // if space is clear, connect directly
248
+ return [ getDockingPoint(sourceMid, sourceBounds, 'v', dockingSource),
249
+ getDockingPoint(targetMid, targetBounds, 'v', dockingTarget)
250
+ ];
251
+ }
252
+ }
253
+
254
+ const directManhattan = directManhattanConnect(source, target, layoutGrid);
255
+
256
+ if (directManhattan) {
257
+ const startPoint = getDockingPoint(sourceMid, sourceBounds, directManhattan[0], dockingSource);
258
+ const endPoint = getDockingPoint(targetMid, targetBounds, directManhattan[1], dockingTarget);
259
+
260
+ const midPoint = directManhattan[0] === 'h' ? { x: endPoint.x, y: startPoint.y } : { x: startPoint.x, y: endPoint.y };
261
+
262
+ return [
263
+ startPoint,
264
+ midPoint,
265
+ endPoint
266
+ ];
267
+ }
268
+ const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
269
+
270
+ return [
271
+ getDockingPoint(sourceMid, sourceBounds, 'r', dockingSource),
272
+ { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y }, // out right
273
+ { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset }, // to target row
274
+ { x: targetMid.x - DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset }, // to target column
275
+ { x: targetMid.x - DEFAULT_CELL_WIDTH / 2, y: targetMid.y }, // to mid
276
+ getDockingPoint(targetMid, targetBounds, 'l', dockingTarget)
277
+ ];
278
+ }
279
+
280
+ // helpers /////
281
+ function coordinatesToPosition(row, col) {
282
+ return {
283
+ width: DEFAULT_CELL_WIDTH,
284
+ height: DEFAULT_CELL_HEIGHT,
285
+ x: col * DEFAULT_CELL_WIDTH,
286
+ y: row * DEFAULT_CELL_HEIGHT
287
+ };
288
+ }
289
+
290
+ function getBounds(element, row, col, shift, attachedTo) {
291
+ let { width, height } = getDefaultSize(element);
292
+ const { x, y } = shift;
293
+
294
+ // Center in cell
295
+ if (!attachedTo) {
296
+ return {
297
+ width: element.isExpanded ? element.grid.getGridDimensions()[1] * DEFAULT_CELL_WIDTH + width : width,
298
+ height: element.isExpanded ? element.grid.getGridDimensions()[0] * DEFAULT_CELL_HEIGHT + height : height,
299
+ x: (col * DEFAULT_CELL_WIDTH) + (DEFAULT_CELL_WIDTH - width) / 2 + x,
300
+ y: row * DEFAULT_CELL_HEIGHT + (DEFAULT_CELL_HEIGHT - height) / 2 + y
301
+ };
302
+ }
303
+
304
+ const hostBounds = attachedTo.di.bounds;
305
+
306
+ return {
307
+ width, height,
308
+ x: Math.round(hostBounds.x + hostBounds.width / 2 - width / 2),
309
+ y: Math.round(hostBounds.y + hostBounds.height - height / 2)
310
+ };
311
+ }
312
+
313
+ function isDirectPathBlocked(source, target, layoutGrid) {
314
+ const { row: sourceRow, col: sourceCol } = source.gridPosition;
315
+ const { row: targetRow, col: targetCol } = target.gridPosition;
316
+
317
+ const dX = targetCol - sourceCol;
318
+ const dY = targetRow - sourceRow;
319
+
320
+ let totalElements = 0;
321
+
322
+ if (dX) {
323
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, { row: sourceRow, col: targetCol }).length;
324
+ }
325
+
326
+ if (dY) {
327
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: targetCol }, { row: targetRow, col: targetCol }).length;
328
+ }
329
+
330
+ return totalElements > 2;
331
+ }
332
+
333
+ function directManhattanConnect(source, target, layoutGrid) {
334
+ const { row: sourceRow, col: sourceCol } = source.gridPosition;
335
+ const { row: targetRow, col: targetCol } = target.gridPosition;
336
+
337
+ const dX = targetCol - sourceCol;
338
+ const dY = targetRow - sourceRow;
339
+
340
+ // Only directly connect left-to-right flow
341
+ if (!(dX > 0 && dY !== 0)) {
342
+ return;
343
+ }
344
+
345
+ // If below, go down then horizontal
346
+ if (dY > 0) {
347
+ let totalElements = 0;
348
+ const bendPoint = { row: targetRow, col: sourceCol };
349
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
350
+ totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
351
+
352
+ return totalElements > 2 ? false : [ 'v', 'h' ];
353
+ } else {
354
+
355
+ // If above, go horizontal than vertical
356
+ let totalElements = 0;
357
+ const bendPoint = { row: sourceRow, col: targetCol };
358
+
359
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
360
+ totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
361
+
362
+ return totalElements > 2 ? false : [ 'h', 'v' ];
363
+ }
364
+ }
365
+
366
+
367
+ function getMaxExpandedBetween(source, target, layoutGrid) {
368
+
369
+ const hostSource = source.attachedToRef ? source.attachedToRef : source;
370
+ const hostTarget = target.attachedToRef ? target.attachedToRef : target;
371
+
372
+
373
+ const [ sourceRow, sourceCol ] = layoutGrid.find(hostSource);
374
+ const [ , targetCol ] = layoutGrid.find(hostTarget);
375
+
376
+ const firstCol = sourceCol < targetCol ? sourceCol : targetCol;
377
+ const lastCol = sourceCol < targetCol ? targetCol : sourceCol;
378
+
379
+ const elementsInRange = layoutGrid.getAllElements().filter(element => element.gridPosition.row === sourceRow && element.gridPosition.col > firstCol && element.gridPosition.col < lastCol);
380
+
381
+ return elementsInRange.reduce((acc, cur) => {
382
+ if (cur.grid?.getGridDimensions()[0] > acc) return cur.grid?.getGridDimensions()[0];
383
+ }, 0);
384
+ }
385
+
386
+ class Grid {
387
+ constructor() {
388
+ this.grid = [];
389
+ }
390
+
391
+ add(element, position) {
392
+ if (!position) {
393
+ this._addStart(element);
394
+ return;
395
+ }
396
+
397
+ const [ row, col ] = position;
398
+ if (!row && !col) {
399
+ this._addStart(element);
400
+ }
401
+
402
+ if (!this.grid[row]) {
403
+ this.grid[row] = [];
404
+ }
405
+
406
+ if (this.grid[row][col]) {
407
+ throw new Error('Grid is occupied please ensure the place you insert at is not occupied');
408
+ }
409
+
410
+ this.grid[row][col] = element;
411
+ }
412
+
413
+ createRow(afterIndex) {
414
+ if (!afterIndex && !Number.isInteger(afterIndex)) {
415
+ this.grid.push([]);
416
+ } else {
417
+ this.grid.splice(afterIndex + 1, 0, []);
418
+ }
419
+ }
420
+
421
+ _addStart(element) {
422
+ this.grid.push([ element ]);
423
+ }
424
+
425
+ addAfter(element, newElement) {
426
+ if (!element) {
427
+ this._addStart(newElement);
428
+ }
429
+ const [ row, col ] = this.find(element);
430
+ this.grid[row].splice(col + 1, 0, newElement);
431
+ }
432
+
433
+ addBelow(element, newElement) {
434
+ if (!element) {
435
+ this._addStart(newElement);
436
+ }
437
+
438
+ const [ row, col ] = this.find(element);
439
+
440
+ // We are at the bottom of the current grid - add empty row below
441
+ if (!this.grid[row + 1]) {
442
+ this.grid[row + 1] = [];
443
+ }
444
+
445
+ // The element below is already occupied - insert new row
446
+ if (this.grid[row + 1][col]) {
447
+ this.grid.splice(row + 1, 0, []);
448
+ }
449
+
450
+ if (this.grid[row + 1][col]) {
451
+ throw new Error('Grid is occupied and we could not find a place - this should not happen');
452
+ }
453
+
454
+ this.grid[row + 1][col] = newElement;
455
+ }
456
+
457
+ find(element) {
458
+ let row, col;
459
+ row = this.grid.findIndex((row) => {
460
+ col = row.findIndex((el) => {
461
+ return el === element;
462
+ });
463
+
464
+ return col !== -1;
465
+ });
466
+
467
+ return [ row, col ];
468
+ }
469
+
470
+ get(row, col) {
471
+ return (this.grid[row] || [])[col];
472
+ }
473
+
474
+ getElementsInRange({ row: startRow, col: startCol }, { row: endRow, col: endCol }) {
475
+ const elements = [];
476
+
477
+ if (startRow > endRow) {
478
+ [ startRow, endRow ] = [ endRow, startRow ];
479
+ }
480
+
481
+ if (startCol > endCol) {
482
+ [ startCol, endCol ] = [ endCol, startCol ];
483
+ }
484
+
485
+ for (let row = startRow; row <= endRow; row++) {
486
+ for (let col = startCol; col <= endCol; col++) {
487
+ const element = this.get(row, col);
488
+
489
+ if (element) {
490
+ elements.push(element);
491
+ }
492
+ }
493
+ }
494
+
495
+ return elements;
496
+ }
497
+
498
+ adjustGridPosition(element) {
499
+ let [ row, col ] = this.find(element);
500
+ const [ , maxCol ] = this.getGridDimensions();
501
+
502
+ if (col < maxCol - 1) {
503
+
504
+ // add element in next column
505
+ this.grid[row].length = maxCol;
506
+ this.grid[row][maxCol] = element;
507
+ this.grid[row][col] = null;
508
+
509
+ }
510
+ }
511
+
512
+ adjustRowForMultipleIncoming(elements, currentElement) {
513
+ const results = elements.map(element => this.find(element));
514
+
515
+ // filter only rows that currently exist, excluding any future or non-existent rows
516
+ const lowestRow = Math.min(...results
517
+ .map(result => result[0])
518
+ .filter(row => row >= 0));
519
+
520
+ const [ row , col ] = this.find(currentElement);
521
+
522
+ // if element doesn't already exist in current row, add element
523
+ if (lowestRow < row && !this.grid[lowestRow][col]) {
524
+ this.grid[lowestRow][col] = currentElement;
525
+ this.grid[row][col] = null;
526
+ }
527
+ }
528
+
529
+ adjustColumnForMultipleIncoming(elements, currentElement) {
530
+ const results = elements.map(element => this.find(element));
531
+
532
+ // filter only col that currently exist, excluding any future or non-existent col
533
+ const maxCol = Math.max(...results
534
+ .map(result => result[1])
535
+ .filter(col => col >= 0));
536
+
537
+ const [ row , col ] = this.find(currentElement);
538
+
539
+ // add to the next column
540
+ if (maxCol + 1 > col) {
541
+ this.grid[row][maxCol + 1] = currentElement;
542
+ this.grid[row][col] = null;
543
+ }
544
+ }
545
+
546
+ getAllElements() {
547
+ const elements = [];
548
+
549
+ for (let row = 0; row < this.grid.length; row++) {
550
+ for (let col = 0; col < this.grid[row].length; col++) {
551
+ const element = this.get(row, col);
552
+
553
+ if (element) {
554
+ elements.push(element);
555
+ }
556
+ }
557
+ }
558
+
559
+ return elements;
560
+ }
561
+
562
+ getGridDimensions() {
563
+ const numRows = this.grid.length;
564
+ let maxCols = 0;
565
+
566
+ for (let i = 0; i < numRows; i++) {
567
+ const currentRowLength = this.grid[i].length;
568
+ if (currentRowLength > maxCols) {
569
+ maxCols = currentRowLength;
570
+ }
571
+ }
572
+
573
+ return [ numRows , maxCols ];
574
+ }
575
+
576
+ elementsByPosition() {
577
+ const elements = [];
578
+
579
+ this.grid.forEach((row, rowIndex) => {
580
+ row.forEach((element, colIndex) => {
581
+ if (!element) {
582
+ return;
583
+ }
584
+ elements.push({
585
+ element,
586
+ row: rowIndex,
587
+ col: colIndex
588
+ });
589
+ });
590
+ });
591
+
592
+ return elements;
593
+ }
594
+
595
+ getElementsTotal() {
596
+ const flattenedGrid = this.grid.flat();
597
+ const uniqueElements = new Set(flattenedGrid.filter(value => value));
598
+ return uniqueElements.size;
599
+ }
600
+
601
+ /**
602
+ *
603
+ * @param {number} afterIndex - number is integer
604
+ * @param {number=} colCount - number is positive integer
605
+ */
606
+ createCol(afterIndex, colCount) {
607
+ this.grid.forEach((row, rowIndex) => {
608
+ this.expandRow(rowIndex, afterIndex, colCount);
609
+ });
610
+ }
611
+
612
+ /**
613
+ * @param {number} rowIndex - is positive integer
614
+ * @param {number} afterIndex - is integer
615
+ * @param {number=} colCount - is positive integer
616
+ */
617
+ expandRow(rowIndex, afterIndex, colCount) {
618
+ if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex > this.grid.length - 1) return;
619
+
620
+ const placeholder = Number.isInteger(colCount) && colCount > 0 ? Array(colCount) : Array(1);
621
+
622
+ const row = this.grid[rowIndex];
623
+
624
+ if (!afterIndex && !Number.isInteger(afterIndex)) {
625
+ row.splice(row.length, 0, ...placeholder);
626
+ } else {
627
+ row.splice(afterIndex + 1, 0, ...placeholder);
628
+ }
629
+ }
630
+ }
631
+
632
+ class DiFactory {
633
+ constructor(moddle) {
634
+ this.moddle = moddle;
635
+ }
636
+
637
+ create(type, attrs) {
638
+ return this.moddle.create(type, attrs || {});
639
+ }
640
+
641
+ createDiBounds(bounds) {
642
+ return this.create('dc:Bounds', bounds);
643
+ }
644
+
645
+ createDiLabel() {
646
+ return this.create('bpmndi:BPMNLabel', {
647
+ bounds: this.createDiBounds()
648
+ });
649
+ }
650
+
651
+ createDiShape(semantic, bounds, attrs) {
652
+ return this.create('bpmndi:BPMNShape', assign({
653
+ bpmnElement: semantic,
654
+ bounds: this.createDiBounds(bounds)
655
+ }, attrs));
656
+ }
657
+
658
+ createDiWaypoints(waypoints) {
659
+ var self = this;
660
+
661
+ return map(waypoints, function(pos) {
662
+ return self.createDiWaypoint(pos);
663
+ });
664
+ }
665
+
666
+ createDiWaypoint(point) {
667
+ return this.create('dc:Point', pick(point, [ 'x', 'y' ]));
668
+ }
669
+
670
+ createDiEdge(semantic, waypoints, attrs) {
671
+ return this.create('bpmndi:BPMNEdge', assign({
672
+ bpmnElement: semantic,
673
+ waypoint: this.createDiWaypoints(waypoints)
674
+ }, attrs));
675
+ }
676
+
677
+ createDiPlane(attrs) {
678
+ return this.create('bpmndi:BPMNPlane', attrs);
679
+ }
680
+
681
+ createDiDiagram(attrs) {
682
+ return this.create('bpmndi:BPMNDiagram', attrs);
683
+ }
684
+ }
685
+
686
+ var attacherHandler = {
687
+ 'addToGrid': ({ element, grid, visited }) => {
688
+ const nextElements = [];
689
+
690
+ const attachedOutgoing = (element.attachers || [])
691
+ .map(attacher => (attacher.outgoing || []).reverse())
692
+ .flat()
693
+ .map(out => out.targetRef);
694
+
695
+ // handle boundary events
696
+ attachedOutgoing.forEach((nextElement, index, arr) => {
697
+ if (visited.has(nextElement)) {
698
+ return;
699
+ }
700
+
701
+ // Add below and to the right of the element
702
+ insertIntoGrid(nextElement, element, grid);
703
+ nextElements.push(nextElement);
704
+ visited.add(nextElement);
705
+ });
706
+
707
+ return nextElements;
708
+ },
709
+
710
+ 'createElementDi': ({ element, row, col, diFactory, shift }) => {
711
+ const hostBounds = getBounds(element, row, col, shift);
712
+
713
+ const DIs = [];
714
+ (element.attachers || []).forEach((att, i, arr) => {
715
+ att.gridPosition = { row, col };
716
+ const bounds = getBounds(att, row, col, shift, element);
717
+
718
+ // distribute along lower edge
719
+ bounds.x = hostBounds.x + (i + 1) * (hostBounds.width / (arr.length + 1)) - bounds.width / 2;
720
+
721
+ const attacherDi = diFactory.createDiShape(att, bounds, {
722
+ id: att.id + '_di'
723
+ });
724
+ att.di = attacherDi;
725
+ att.gridPosition = { row, col };
726
+
727
+ DIs.push(attacherDi);
728
+ });
729
+
730
+ return DIs;
731
+ },
732
+
733
+ 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory, shift }) => {
734
+ const attachers = element.attachers || [];
735
+
736
+ return attachers.flatMap(att => {
737
+ const outgoing = att.outgoing || [];
738
+
739
+ return outgoing.map(out => {
740
+ const target = out.targetRef;
741
+ const waypoints = connectElements(att, target, layoutGrid);
742
+
743
+ // Correct waypoints if they don't automatically attach to the bottom
744
+ ensureExitBottom(att, waypoints, [ row, col ]);
745
+
746
+ return diFactory.createDiEdge(out, waypoints, {
747
+ id: out.id + '_di'
748
+ });
749
+ });
750
+ });
751
+ }
752
+ };
753
+
754
+
755
+ function insertIntoGrid(newElement, host, grid) {
756
+ const [ row, col ] = grid.find(host);
757
+
758
+ // Grid is occupied
759
+ if (grid.get(row + 1, col) || grid.get(row + 1, col + 1)) {
760
+ grid.createRow(row);
761
+ }
762
+
763
+ grid.add(newElement, [ row + 1, col + 1 ]);
764
+ }
765
+
766
+ function ensureExitBottom(source, waypoints, [ row, col ]) {
767
+
768
+ const sourceDi = source.di;
769
+ const sourceBounds = sourceDi.get('bounds');
770
+ const sourceMid = getMid(sourceBounds);
771
+
772
+ const dockingPoint = getDockingPoint(sourceMid, sourceBounds, 'b');
773
+ if (waypoints[0].x === dockingPoint.x && waypoints[0].y === dockingPoint.y) {
774
+ return;
775
+ }
776
+
777
+ const baseSourceGrid = source.grid || source.attachedToRef?.grid;
778
+
779
+ if (waypoints.length === 2) {
780
+ const newStart = [
781
+ dockingPoint,
782
+ { x: dockingPoint.x, y: !baseSourceGrid ? (row + 1) * DEFAULT_CELL_HEIGHT : (row + baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
783
+ { x: !baseSourceGrid ? (col + 1) * DEFAULT_CELL_WIDTH : (col + baseSourceGrid.getGridDimensions()[1] + 1) * DEFAULT_CELL_WIDTH, y: !baseSourceGrid ? (row + 1) * DEFAULT_CELL_HEIGHT : (row + baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
784
+ { x: !baseSourceGrid ? (col + 1) * DEFAULT_CELL_WIDTH : (col + baseSourceGrid.getGridDimensions()[1] + 1) * DEFAULT_CELL_WIDTH, y: !baseSourceGrid ? (row + 0.5) * DEFAULT_CELL_HEIGHT : row * DEFAULT_CELL_HEIGHT + DEFAULT_CELL_HEIGHT / 2 },
785
+ ];
786
+
787
+ waypoints.splice(0, 1, ...newStart);
788
+ return;
789
+ }
790
+
791
+ // add waypoints to exit bottom and connect to existing path
792
+ const newStart = [
793
+ dockingPoint,
794
+ { x: dockingPoint.x, y: !baseSourceGrid ? (row + 1) * DEFAULT_CELL_HEIGHT : (row + baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
795
+ { x: waypoints[1].x, y: !baseSourceGrid ? (row + 1) * DEFAULT_CELL_HEIGHT : (row + baseSourceGrid.getGridDimensions()[0] + 1) * DEFAULT_CELL_HEIGHT },
796
+ ];
797
+
798
+ waypoints.splice(0, 1, ...newStart);
799
+ }
800
+
801
+ var elementHandler = {
802
+ 'createElementDi': ({ element, row, col, diFactory, shift }) => {
803
+
804
+ const bounds = getBounds(element, row, col, shift);
805
+
806
+ const options = {
807
+ id: element.id + '_di'
808
+ };
809
+
810
+ if (is(element, 'bpmn:ExclusiveGateway')) {
811
+ options.isMarkerVisible = true;
812
+ }
813
+
814
+ if (element.isExpanded) {
815
+ options.isExpanded = true;
816
+ }
817
+
818
+ const shapeDi = diFactory.createDiShape(element, bounds, options);
819
+ element.di = shapeDi;
820
+ element.gridPosition = { row, col };
821
+
822
+ return shapeDi;
823
+ }
824
+ };
825
+
826
+ var outgoingHandler = {
827
+ 'addToGrid': ({ element, grid, visited, stack }) => {
828
+ let nextElements = [];
829
+
830
+ // Handle outgoing paths
831
+ const outgoing = (element.outgoing || [])
832
+ .map(out => out.targetRef)
833
+ .filter(el => el);
834
+
835
+ let previousElement = null;
836
+
837
+ if (outgoing.length > 1 && isNextElementTasks(outgoing)) {
838
+ grid.adjustGridPosition(element);
839
+ }
840
+
841
+ outgoing.forEach((nextElement, index, arr) => {
842
+ if (visited.has(nextElement)) {
843
+ return;
844
+ }
845
+
846
+ // Prevents revisiting future incoming elements and ensures proper traversal without early exit.
847
+ if ((previousElement || stack.length > 0) && isFutureIncoming(nextElement, visited) && !checkForLoop(nextElement, visited)) {
848
+ return;
849
+ }
850
+
851
+ if (!previousElement) {
852
+ grid.addAfter(element, nextElement);
853
+ }
854
+
855
+ else if (is(element, 'bpmn:ExclusiveGateway') && is(nextElement, 'bpmn:ExclusiveGateway')) {
856
+ grid.addAfter(previousElement, nextElement);
857
+ }
858
+ else {
859
+ grid.addBelow(arr[index - 1], nextElement);
860
+ }
861
+
862
+ // Is self-looping
863
+ if (nextElement !== element) {
864
+ previousElement = nextElement;
865
+ }
866
+
867
+ nextElements.unshift(nextElement);
868
+ visited.add(nextElement);
869
+ });
870
+
871
+ // Sort elements by priority to ensure proper stack placement
872
+ nextElements = sortByType(nextElements, 'bpmn:ExclusiveGateway'); // TODO: sort by priority
873
+ return nextElements;
874
+ },
875
+
876
+ 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
877
+ const outgoing = element.outgoing || [];
878
+
879
+ return outgoing.map(out => {
880
+ const target = out.targetRef;
881
+ const waypoints = connectElements(element, target, layoutGrid);
882
+
883
+ const connectionDi = diFactory.createDiEdge(out, waypoints, {
884
+ id: out.id + '_di'
885
+ });
886
+
887
+ return connectionDi;
888
+ });
889
+
890
+ }
891
+ };
892
+
893
+
894
+ // helpers /////
895
+
896
+ function sortByType(arr, type) {
897
+ const nonMatching = arr.filter(item => !is(item,type));
898
+ const matching = arr.filter(item => is(item,type));
899
+
900
+ return [ ...matching, ...nonMatching ];
901
+
902
+ }
903
+
904
+ function checkForLoop(element, visited) {
905
+ for (const incomingElement of element.incoming) {
906
+ if (!visited.has(incomingElement.sourceRef)) {
907
+ return findElementInTree(element, incomingElement.sourceRef);
908
+ }
909
+ }
910
+ }
911
+
912
+
913
+ function isFutureIncoming(element, visited) {
914
+ if (element.incoming.length > 1) {
915
+ for (const incomingElement of element.incoming) {
916
+ if (!visited.has(incomingElement.sourceRef)) {
917
+ return true;
918
+ }
919
+ }
920
+ }
921
+ return false;
922
+ }
923
+
924
+ function isNextElementTasks(elements) {
925
+ return elements.every(element => is(element, 'bpmn:Task'));
926
+ }
927
+
928
+ var incomingHandler = {
929
+ 'addToGrid': ({ element, grid, visited }) => {
930
+ const nextElements = [];
931
+
932
+ const incoming = (element.incoming || [])
933
+ .map(out => out.sourceRef)
934
+ .filter(el => el);
935
+
936
+ // adjust the row if it is empty
937
+ if (incoming.length > 1) {
938
+ grid.adjustColumnForMultipleIncoming(incoming, element);
939
+ grid.adjustRowForMultipleIncoming(incoming, element);
940
+ }
941
+ return nextElements;
942
+ },
943
+ };
944
+
945
+ const handlers = [ elementHandler, incomingHandler, outgoingHandler, attacherHandler ];
946
+
947
+ const PARTICIPANT_LABEL_WIDTH = 30;
948
+
949
+ class Layouter {
950
+ constructor() {
951
+ this.moddle = new BpmnModdle();
952
+ this.diFactory = new DiFactory(this.moddle);
953
+ this._handlers = handlers;
954
+ }
955
+
956
+ handle(operation, options) {
957
+ return this._handlers
958
+ .filter(handler => isFunction(handler[operation]))
959
+ .map(handler => handler[operation](options));
960
+
961
+ }
962
+
963
+ async layoutProcess(xml, options = {}) {
964
+ const moddleObj = await this.moddle.fromXML(xml);
965
+ const { rootElement } = moddleObj;
966
+
967
+ this.diagram = rootElement;
968
+
969
+ const collaboration = this.getCollaboration();
970
+
971
+ if (collaboration) {
972
+ this.setExpandedPropertyToModdleElements(moddleObj, options);
973
+ const participantGap = this.readParticipantGap(collaboration);
974
+ this.cleanDi();
975
+ this.layoutCollaboration(collaboration, { ...options, participantGap });
976
+ } else {
977
+ const firstRootProcess = this.getProcess();
978
+
979
+ if (firstRootProcess) {
980
+ this.setExpandedPropertyToModdleElements(moddleObj, options);
981
+ this.setExecutedProcesses(firstRootProcess);
982
+ this.createGridsForProcesses();
983
+ this.cleanDi();
984
+ this.createRootDi(firstRootProcess);
985
+ this.drawProcesses();
986
+
987
+ // Draw artifacts and data associations for each laid out process
988
+ for (const process of this.layoutedProcesses) {
989
+ const diagram = this.diagram.diagrams.find(d => d.plane.bpmnElement === process)
990
+ || this.diagram.diagrams[0];
991
+ this.generateArtifactsDi(process, diagram);
992
+ this.generateDataAssociationsDi(process, diagram);
993
+ }
994
+ }
995
+ }
996
+
997
+ return (await this.moddle.toXML(this.diagram, { format: true })).xml;
998
+ }
999
+
1000
+ layoutCollaboration(collaboration, options = {}) {
1001
+ const PARTICIPANT_GAP = options.participantGap ?? 0;
1002
+
1003
+ // Build grids per participant's process
1004
+ const participantLayouts = collaboration.participants.map(participant => {
1005
+ const process = participant.processRef;
1006
+ if (!process) return { participant, process: null, layoutedProcesses: [], grid: null };
1007
+
1008
+ this.layoutedProcesses = [];
1009
+ this.setExecutedProcesses(process);
1010
+ this.createGridsForProcesses();
1011
+
1012
+ return {
1013
+ participant,
1014
+ process,
1015
+ layoutedProcesses: [ ...this.layoutedProcesses ],
1016
+ grid: process.grid
1017
+ };
1018
+ });
1019
+
1020
+ // Create single collaboration diagram
1021
+ const collaborationDi = this.createCollaborationDi(collaboration);
1022
+
1023
+ const participantFloors = new Map();
1024
+ let currentY = 0;
1025
+
1026
+ for (const { participant, process, layoutedProcesses, grid } of participantLayouts) {
1027
+ if (process) participantFloors.set(process, currentY);
1028
+
1029
+ let participantWidth, participantHeight;
1030
+
1031
+ if (!grid) {
1032
+ participantWidth = 400;
1033
+ participantHeight = DEFAULT_CELL_HEIGHT;
1034
+ } else {
1035
+ const [ rows, cols ] = grid.getGridDimensions();
1036
+ participantWidth = PARTICIPANT_LABEL_WIDTH + Math.max(cols, 1) * DEFAULT_CELL_WIDTH;
1037
+ participantHeight = Math.max(rows, 1) * DEFAULT_CELL_HEIGHT;
1038
+ }
1039
+
1040
+ // Compute extra top padding needed for collaboration-level annotations
1041
+ const annotationPadding = grid
1042
+ ? this.computeAnnotationPadding(collaboration, process, grid)
1043
+ : 0;
1044
+
1045
+ participantHeight += annotationPadding;
1046
+
1047
+ // Emit participant shape
1048
+ const participantShape = this.diFactory.createDiShape(participant, {
1049
+ x: 0,
1050
+ y: currentY,
1051
+ width: participantWidth,
1052
+ height: participantHeight
1053
+ }, {
1054
+ id: participant.id + '_di',
1055
+ isHorizontal: true
1056
+ });
1057
+ collaborationDi.plane.get('planeElement').push(participantShape);
1058
+ participant.di = participantShape;
1059
+
1060
+ if (grid) {
1061
+
1062
+ // Draw flow elements with participant offset (shifted down by annotation padding)
1063
+ const shift = { x: PARTICIPANT_LABEL_WIDTH, y: currentY + annotationPadding };
1064
+ this.generateDi(grid, shift, collaborationDi);
1065
+
1066
+ // Draw expanded sub-processes within this participant
1067
+ this.layoutedProcesses = layoutedProcesses;
1068
+ this.drawExpandedProcesses(collaborationDi);
1069
+
1070
+ // Draw artifacts (text annotations, associations, groups) and data associations
1071
+ this.generateArtifactsDi(process, collaborationDi);
1072
+ this.generateDataAssociationsDi(process, collaborationDi);
1073
+ }
1074
+
1075
+ currentY += participantHeight + PARTICIPANT_GAP;
1076
+ }
1077
+
1078
+ // Collaboration-level artifacts (annotations, associations, groups)
1079
+ this.generateArtifactsDi(collaboration, collaborationDi, {
1080
+ getAnnotationFloor: (peer) => {
1081
+ for (const [ proc, floorY ] of participantFloors) {
1082
+ if (containsElement(proc, peer)) return floorY;
1083
+ }
1084
+ return -Infinity;
1085
+ }
1086
+ });
1087
+
1088
+ // Message flows after all participants are positioned
1089
+ this.generateMessageFlowsDi(collaboration, collaborationDi);
1090
+ }
1091
+
1092
+ drawExpandedProcesses(targetDi) {
1093
+ const expandedProcesses = this.layoutedProcesses
1094
+ .filter(p => p.isExpanded)
1095
+ .sort((a, b) => a.level - b.level);
1096
+
1097
+ for (const process of expandedProcesses) {
1098
+ const baseProcDi = this.getElementDi(process);
1099
+ if (!baseProcDi) continue;
1100
+ const diagram = this.getProcDi(baseProcDi) || targetDi;
1101
+ let { x, y } = baseProcDi.bounds;
1102
+ const { width, height } = getDefaultSize(process);
1103
+ x += DEFAULT_CELL_WIDTH / 2 - width / 4;
1104
+ y += DEFAULT_CELL_HEIGHT - height - height / 4;
1105
+ this.generateDi(process.grid, { x, y }, diagram);
1106
+ }
1107
+ }
1108
+
1109
+ generateArtifactsDi(process, procDi, options = {}) {
1110
+ const { getAnnotationFloor = () => -Infinity } = options;
1111
+ const artifacts = process.artifacts || [];
1112
+ const planeElement = procDi.plane.get('planeElement');
1113
+
1114
+ const textAnnotations = artifacts.filter(a => is(a, 'bpmn:TextAnnotation'));
1115
+ const associations = artifacts.filter(a => is(a, 'bpmn:Association'));
1116
+ const groups = artifacts.filter(a => is(a, 'bpmn:Group'));
1117
+
1118
+ // Position text annotations above their associated source element
1119
+ textAnnotations.forEach(annotation => {
1120
+ const association = associations.find(
1121
+ assoc => assoc.targetRef === annotation || assoc.sourceRef === annotation
1122
+ );
1123
+
1124
+ if (!association) return;
1125
+
1126
+ const peer = association.sourceRef === annotation
1127
+ ? association.targetRef
1128
+ : association.sourceRef;
1129
+
1130
+ if (!peer || !peer.di) return;
1131
+
1132
+ const peerBounds = peer.di.get('bounds');
1133
+ const { width, height } = getDefaultSize(annotation);
1134
+ const candidateY = peerBounds.y - height - 20;
1135
+ const floor = getAnnotationFloor(peer);
1136
+ const x = peerBounds.x;
1137
+ const y = floor > -Infinity ? Math.max(candidateY, floor + 5) : candidateY;
1138
+
1139
+ const shapeDi = this.diFactory.createDiShape(annotation, { x, y, width, height }, {
1140
+ id: annotation.id + '_di'
1141
+ });
1142
+ annotation.di = shapeDi;
1143
+ planeElement.push(shapeDi);
1144
+ });
1145
+
1146
+ // Emit association edges
1147
+ associations.forEach(association => {
1148
+ const source = association.sourceRef;
1149
+ const target = association.targetRef;
1150
+
1151
+ if (!source || !target || !source.di || !target.di) return;
1152
+
1153
+ const sourceBounds = source.di.get('bounds');
1154
+ const targetBounds = target.di.get('bounds');
1155
+ const sourceMid = getMid(sourceBounds);
1156
+ const targetMid = getMid(targetBounds);
1157
+
1158
+ const annotationIsSource = is(source, 'bpmn:TextAnnotation');
1159
+ const edgeDi = this.diFactory.createDiEdge(association, [
1160
+ getDockingPoint(sourceMid, sourceBounds, annotationIsSource ? 'b' : 't'),
1161
+ getDockingPoint(targetMid, targetBounds, annotationIsSource ? 't' : 'b')
1162
+ ], {
1163
+ id: association.id + '_di'
1164
+ });
1165
+ planeElement.push(edgeDi);
1166
+ });
1167
+
1168
+ // Groups — handled after associations
1169
+ this.generateGroupsDi(groups, process, planeElement);
1170
+ }
1171
+
1172
+ generateGroupsDi(groups, process, planeElement) {
1173
+ const PADDING = 20;
1174
+ const flowElements = process.flowElements || [];
1175
+
1176
+ groups.forEach(group => {
1177
+ const categoryValue = group.categoryValueRef;
1178
+ if (!categoryValue) return;
1179
+
1180
+ // Find flow elements whose categoryValueRef array includes this group's categoryValue
1181
+ const members = flowElements.filter(el =>
1182
+ Array.isArray(el.categoryValueRef) && el.categoryValueRef.includes(categoryValue)
1183
+ );
1184
+ const memberBounds = members
1185
+ .filter(el => el.di)
1186
+ .map(el => el.di.get('bounds'));
1187
+
1188
+ if (memberBounds.length === 0) return;
1189
+
1190
+ const minX = Math.min(...memberBounds.map(b => b.x)) - PADDING;
1191
+ const minY = Math.min(...memberBounds.map(b => b.y)) - PADDING;
1192
+ const maxX = Math.max(...memberBounds.map(b => b.x + b.width)) + PADDING;
1193
+ const maxY = Math.max(...memberBounds.map(b => b.y + b.height)) + PADDING;
1194
+
1195
+ const shapeDi = this.diFactory.createDiShape(group, {
1196
+ x: minX,
1197
+ y: minY,
1198
+ width: maxX - minX,
1199
+ height: maxY - minY
1200
+ }, {
1201
+ id: group.id + '_di'
1202
+ });
1203
+ planeElement.push(shapeDi);
1204
+ });
1205
+ }
1206
+
1207
+ generateMessageFlowsDi(collaboration, collaborationDi) {
1208
+ const messageFlows = collaboration.messageFlows || [];
1209
+ const planeElement = collaborationDi.plane.get('planeElement');
1210
+
1211
+ messageFlows.forEach(messageFlow => {
1212
+ const source = messageFlow.sourceRef;
1213
+ const target = messageFlow.targetRef;
1214
+
1215
+ if (!source || !target || !source.di || !target.di) return;
1216
+
1217
+ const sourceBounds = source.di.get('bounds');
1218
+ const targetBounds = target.di.get('bounds');
1219
+ const sourceMid = getMid(sourceBounds);
1220
+ const targetMid = getMid(targetBounds);
1221
+
1222
+ const sourceIsAbove = sourceBounds.y < targetBounds.y;
1223
+
1224
+ const sourceExitY = sourceIsAbove
1225
+ ? sourceBounds.y + sourceBounds.height
1226
+ : sourceBounds.y;
1227
+ const targetEntryY = sourceIsAbove
1228
+ ? targetBounds.y
1229
+ : targetBounds.y + targetBounds.height;
1230
+ const midY = (sourceExitY + targetEntryY) / 2;
1231
+
1232
+ // Orthogonal routing: exit source vertically, jog horizontally at midpoint, enter target vertically
1233
+ const edgeDi = this.diFactory.createDiEdge(messageFlow, [
1234
+ { x: sourceMid.x, y: sourceExitY },
1235
+ { x: sourceMid.x, y: midY },
1236
+ { x: targetMid.x, y: midY },
1237
+ { x: targetMid.x, y: targetEntryY }
1238
+ ], {
1239
+ id: messageFlow.id + '_di'
1240
+ });
1241
+
1242
+ planeElement.push(edgeDi);
1243
+ });
1244
+ }
1245
+
1246
+ generateDataAssociationsDi(process, procDi) {
1247
+ const flowElements = process.flowElements || [];
1248
+ const planeElement = procDi.plane.get('planeElement');
1249
+
1250
+ flowElements.forEach(element => {
1251
+ (element.dataInputAssociations || []).forEach(association => {
1252
+ const sources = association.sourceRef || [];
1253
+ sources.forEach((source, i) => {
1254
+ if (!source.di || !element.di) return;
1255
+ const id = sources.length > 1
1256
+ ? `${association.id}_src${i}_di`
1257
+ : `${association.id}_di`;
1258
+ const edgeDi = this.diFactory.createDiEdge(association,
1259
+ orthogonalConnect(source.di.get('bounds'), element.di.get('bounds')),
1260
+ { id });
1261
+ planeElement.push(edgeDi);
1262
+ });
1263
+ });
1264
+
1265
+ (element.dataOutputAssociations || []).forEach(association => {
1266
+ const target = association.targetRef;
1267
+ if (!target || !target.di || !element.di) return;
1268
+ const edgeDi = this.diFactory.createDiEdge(association,
1269
+ orthogonalConnect(element.di.get('bounds'), target.di.get('bounds')),
1270
+ { id: association.id + '_di' });
1271
+ planeElement.push(edgeDi);
1272
+ });
1273
+ });
1274
+ }
1275
+
1276
+ createGridsForProcesses() {
1277
+ const processes = this.layoutedProcesses.sort((a, b) => b.level - a.level);
1278
+
1279
+ // create and add grids for each process
1280
+ // root processes should be processed last for element expanding
1281
+ for (const process of processes) {
1282
+
1283
+ // add base grid with collapsed elements
1284
+ process.grid = this.createGridLayout(process);
1285
+
1286
+ expandGridHorizontally(process.grid);
1287
+ expandGridVertically(process.grid);
1288
+
1289
+ if (process.isExpanded) {
1290
+ const [ rowCount, colCount ] = process.grid.getGridDimensions();
1291
+ if (rowCount === 0) process.grid.createRow();
1292
+ if (colCount === 0) process.grid.createCol();
1293
+ }
1294
+
1295
+ }
1296
+ }
1297
+
1298
+ setExpandedPropertyToModdleElements(bpmnModel, options = {}) {
1299
+ const allElements = bpmnModel.elementsById;
1300
+ if (allElements) {
1301
+ for (const element of Object.values(allElements)) {
1302
+ if (element.$type === 'bpmndi:BPMNShape' && element.isExpanded === true) element.bpmnElement.isExpanded = true;
1303
+ }
1304
+
1305
+ if (options.expandSubProcesses) {
1306
+ for (const element of Object.values(allElements)) {
1307
+ if (element.$type === 'bpmn:SubProcess') {
1308
+ element.isExpanded = true;
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+
1315
+ setExecutedProcesses(firstRootProcess) {
1316
+ this.layoutedProcesses = [];
1317
+
1318
+ const executionStack = [ firstRootProcess ];
1319
+
1320
+ while (executionStack.length > 0) {
1321
+ const executedProcess = executionStack.pop();
1322
+ this.layoutedProcesses.push(executedProcess);
1323
+ executedProcess.level = executedProcess.$parent === this.diagram ? 0 : executedProcess.$parent.level + 1;
1324
+
1325
+ const nextProcesses = executedProcess.flowElements?.filter(flowElement => flowElement.$type === 'bpmn:SubProcess') || [];
1326
+
1327
+ executionStack.splice(executionStack.length, 0, ...nextProcesses);
1328
+ }
1329
+ }
1330
+
1331
+ cleanDi() {
1332
+ this.diagram.diagrams = [];
1333
+ }
1334
+
1335
+ createGridLayout(root) {
1336
+ const grid = new Grid();
1337
+
1338
+ const flowElements = root.flowElements || [];
1339
+ const elements = flowElements.filter(el => !is(el,'bpmn:SequenceFlow'));
1340
+
1341
+ // check for empty process/subprocess
1342
+ if (!flowElements) {
1343
+ return grid;
1344
+ }
1345
+
1346
+ bindBoundaryEventsWithHosts (flowElements);
1347
+
1348
+ // Depth-first-search
1349
+ const visited = new Set();
1350
+ while (visited.size < elements.filter(element => !element.attachedToRef).length) {
1351
+ const startingElements = flowElements.filter(el => {
1352
+ return !isConnection(el) &&
1353
+ !isBoundaryEvent(el) &&
1354
+ (!el.incoming || !hasOtherIncoming(el)) &&
1355
+ !visited.has(el);
1356
+ });
1357
+
1358
+ const stack = [ ...startingElements ];
1359
+
1360
+ startingElements.forEach(el => {
1361
+ grid.add(el);
1362
+ visited.add(el);
1363
+ });
1364
+
1365
+ this.handleGrid(grid,visited,stack);
1366
+
1367
+ if (grid.getElementsTotal() !== elements.length) {
1368
+ const gridElements = grid.getAllElements();
1369
+ const missingElements = elements.filter(el => !gridElements.includes(el) && !isBoundaryEvent(el));
1370
+ if (missingElements.length > 0) {
1371
+ stack.push(missingElements[0]);
1372
+ grid.add(missingElements[0]);
1373
+ visited.add(missingElements[0]);
1374
+ this.handleGrid(grid,visited,stack);
1375
+ }
1376
+ }
1377
+ }
1378
+ return grid;
1379
+ }
1380
+
1381
+ generateDi(layoutGrid , shift, procDi) {
1382
+ const diFactory = this.diFactory;
1383
+
1384
+ const prePlaneElement = procDi ? procDi : this.diagram.diagrams[0];
1385
+
1386
+ const planeElement = prePlaneElement.plane.get('planeElement');
1387
+
1388
+ // Step 1: Create DI for all elements
1389
+ layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
1390
+ const dis = this
1391
+ .handle('createElementDi', { element, row, col, layoutGrid, diFactory, shift })
1392
+ .flat();
1393
+
1394
+ planeElement.push(...dis);
1395
+ });
1396
+
1397
+ // Step 2: Create DI for all connections
1398
+ layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
1399
+ const dis = this
1400
+ .handle('createConnectionDi', { element, row, col, layoutGrid, diFactory, shift })
1401
+ .flat();
1402
+
1403
+ planeElement.push(...dis);
1404
+ });
1405
+ }
1406
+
1407
+ handleGrid(grid, visited, stack) {
1408
+ while (stack.length > 0) {
1409
+ const currentElement = stack.pop();
1410
+
1411
+ const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited, stack });
1412
+
1413
+ nextElements.flat().forEach(el => {
1414
+ stack.push(el);
1415
+ visited.add(el);
1416
+ });
1417
+ }
1418
+ }
1419
+
1420
+ getProcess() {
1421
+ return this.diagram.get('rootElements').find(el => el.$type === 'bpmn:Process');
1422
+ }
1423
+
1424
+ getCollaboration() {
1425
+ return this.diagram.get('rootElements').find(el => el.$type === 'bpmn:Collaboration');
1426
+ }
1427
+
1428
+ computeAnnotationPadding(collaboration, process, grid) {
1429
+ const artifacts = collaboration.artifacts || [];
1430
+ const associations = artifacts.filter(a => is(a, 'bpmn:Association'));
1431
+ const textAnnotations = artifacts.filter(a => is(a, 'bpmn:TextAnnotation'));
1432
+ const processElements = new Set(process.flowElements || []);
1433
+ const elementsByPos = grid.elementsByPosition();
1434
+
1435
+ let padding = 0;
1436
+
1437
+ textAnnotations.forEach(annotation => {
1438
+ const association = associations.find(
1439
+ assoc => assoc.targetRef === annotation || assoc.sourceRef === annotation
1440
+ );
1441
+ if (!association) return;
1442
+
1443
+ const peer = association.sourceRef === annotation
1444
+ ? association.targetRef
1445
+ : association.sourceRef;
1446
+
1447
+ if (!peer || !processElements.has(peer)) return;
1448
+
1449
+ const pos = elementsByPos.find(({ element }) => element === peer);
1450
+ if (!pos) return;
1451
+
1452
+ const { height: annotHeight } = getDefaultSize(annotation);
1453
+ const { height: peerHeight } = getDefaultSize(peer);
1454
+ const relativePeerY = pos.row * DEFAULT_CELL_HEIGHT + (DEFAULT_CELL_HEIGHT - peerHeight) / 2;
1455
+ const relativeCandidateY = relativePeerY - annotHeight - 20;
1456
+
1457
+ if (relativeCandidateY < 5) {
1458
+ padding = Math.max(padding, 5 - relativeCandidateY);
1459
+ }
1460
+ });
1461
+
1462
+ return padding;
1463
+ }
1464
+
1465
+ readParticipantGap(collaboration) {
1466
+ const participants = collaboration.participants;
1467
+ if (participants.length < 2) return 0;
1468
+
1469
+ const shapes = this.diagram.diagrams
1470
+ .flatMap(d => d.plane.planeElement)
1471
+ .filter(el => el.$type === 'bpmndi:BPMNShape' && participants.includes(el.bpmnElement));
1472
+
1473
+ if (shapes.length < 2) return 0;
1474
+
1475
+ shapes.sort((a, b) => a.bounds.y - b.bounds.y);
1476
+
1477
+ let gap = 0;
1478
+ for (let i = 1; i < shapes.length; i++) {
1479
+ const prevBottom = shapes[i - 1].bounds.y + shapes[i - 1].bounds.height;
1480
+ gap = Math.max(gap, shapes[i].bounds.y - prevBottom);
1481
+ }
1482
+ return Math.min(gap, 100);
1483
+ }
1484
+
1485
+ createCollaborationDi(collaboration) {
1486
+ const diFactory = this.diFactory;
1487
+ const planeDi = diFactory.createDiPlane({
1488
+ id: 'BPMNPlane_' + collaboration.id,
1489
+ bpmnElement: collaboration
1490
+ });
1491
+ const diagramDi = diFactory.createDiDiagram({
1492
+ id: 'BPMNDiagram_' + collaboration.id,
1493
+ plane: planeDi
1494
+ });
1495
+ this.diagram.diagrams.push(diagramDi);
1496
+ return diagramDi;
1497
+ }
1498
+
1499
+ createRootDi(processes) {
1500
+ this.createProcessDi(processes);
1501
+ }
1502
+
1503
+ createProcessDi(element) {
1504
+ const diFactory = this.diFactory;
1505
+
1506
+ const planeDi = diFactory.createDiPlane({
1507
+ id: 'BPMNPlane_' + element.id,
1508
+ bpmnElement: element
1509
+ });
1510
+ const diagramDi = diFactory.createDiDiagram({
1511
+ id: 'BPMNDiagram_' + element.id,
1512
+ plane: planeDi
1513
+ });
1514
+
1515
+ const diagram = this.diagram;
1516
+
1517
+ diagram.diagrams.push(diagramDi);
1518
+
1519
+ return diagramDi;
1520
+ }
1521
+
1522
+ /**
1523
+ * Draw processes.
1524
+ * Root processes should be processed first for element expanding
1525
+ */
1526
+ drawProcesses() {
1527
+ const sortedProcesses = this.layoutedProcesses.sort((a, b) => a.level - b.level);
1528
+
1529
+ for (const process of sortedProcesses) {
1530
+
1531
+ // draw processes in expanded elements
1532
+ if (process.isExpanded) {
1533
+ const baseProcDi = this.getElementDi(process);
1534
+ const diagram = this.getProcDi(baseProcDi);
1535
+ let { x, y } = baseProcDi.bounds;
1536
+ const { width, height } = getDefaultSize(process);
1537
+ x += DEFAULT_CELL_WIDTH / 2 - width / 4;
1538
+ y += DEFAULT_CELL_HEIGHT - height - height / 4;
1539
+ this.generateDi(process.grid, { x, y }, diagram);
1540
+ continue;
1541
+ }
1542
+
1543
+ // draw other processes; if no separate diagram (collapsed sub-process),
1544
+ // generateDi falls back to diagrams[0] — bpmn-js uses those shapes on expand.
1545
+ // Use an offset so inner elements clear the breadcrumb nav when expanded.
1546
+ const diagram = this.diagram.diagrams.find(d => d.plane.bpmnElement === process);
1547
+ const shift = diagram ? { x: 0, y: 0 } : { x: 30, y: 80 };
1548
+ this.generateDi(process.grid, shift, diagram);
1549
+ }
1550
+ }
1551
+
1552
+ getElementDi(element) {
1553
+ return this.diagram.diagrams
1554
+ .map(diagram => diagram.plane.planeElement).flat()
1555
+ .find(item => item.bpmnElement === element);
1556
+ }
1557
+
1558
+ getProcDi(element) {
1559
+ return this.diagram.diagrams.find(diagram => diagram.plane.planeElement.includes(element));
1560
+ }
1561
+ }
1562
+
1563
+ function containsElement(process, element) {
1564
+ const flowElements = process.flowElements || [];
1565
+ if (flowElements.includes(element)) return true;
1566
+ return flowElements.some(el => el.flowElements && containsElement(el, element));
1567
+ }
1568
+
1569
+ function bindBoundaryEventsWithHosts(elements) {
1570
+ const boundaryEvents = elements.filter(element => isBoundaryEvent(element));
1571
+ boundaryEvents.forEach(boundaryEvent => {
1572
+ const attachedTask = boundaryEvent.attachedToRef;
1573
+ const attachers = attachedTask.attachers || [];
1574
+ attachers.push(boundaryEvent);
1575
+ attachedTask.attachers = attachers;
1576
+ });
1577
+ }
1578
+
1579
+ /**
1580
+ * Check grid by columns.
1581
+ * If column has elements with isExpanded === true,
1582
+ * find the maximum size of elements grids and expand the parent grid horizontally.
1583
+ * @param grid
1584
+ */
1585
+ function expandGridHorizontally(grid) {
1586
+ const [ numRows , maxCols ] = grid.getGridDimensions();
1587
+ for (let i = maxCols - 1 ; i >= 0; i--) {
1588
+ const elementsInCol = [];
1589
+ for (let j = 0; j < numRows; j++) {
1590
+ const candidate = grid.get(j, i);
1591
+ if (candidate && candidate.isExpanded) elementsInCol.push(candidate);
1592
+ }
1593
+
1594
+ if (elementsInCol.length === 0) continue;
1595
+
1596
+ const maxColCount = elementsInCol.reduce((acc,cur) => {
1597
+ const [ ,curCols ] = cur.grid.getGridDimensions();
1598
+ if (acc === undefined || curCols > acc) return curCols;
1599
+ return acc;
1600
+ }, undefined);
1601
+
1602
+ const shift = !maxColCount ? 2 : maxColCount;
1603
+ grid.createCol(i, shift);
1604
+ }
1605
+ }
1606
+
1607
+ /**
1608
+ * Check grid by rows.
1609
+ * If row has elements with isExpanded === true,
1610
+ * find the maximum size of elements grids and expand the parent grid vertically.
1611
+ * @param grid
1612
+ */
1613
+ function expandGridVertically(grid) {
1614
+ const [ numRows , maxCols ] = grid.getGridDimensions();
1615
+
1616
+ for (let i = numRows - 1 ; i >= 0; i--) {
1617
+ const elementsInRow = [];
1618
+ for (let j = 0; j < maxCols; j++) {
1619
+ const candidate = grid.get(i, j);
1620
+ if (candidate && candidate.isExpanded) elementsInRow.push(candidate);
1621
+ }
1622
+
1623
+ if (elementsInRow.length === 0) continue;
1624
+
1625
+ const maxRowCount = elementsInRow.reduce((acc,cur) => {
1626
+ const [ curRows ] = cur.grid.getGridDimensions();
1627
+ if (acc === undefined || curRows > acc) return curRows;
1628
+ return acc;
1629
+ }, undefined);
1630
+
1631
+ const shift = !maxRowCount ? 1 : maxRowCount;
1632
+
1633
+ // expand the parent grid vertically
1634
+ for (let index = 0; index < shift; index++) {
1635
+ grid.createRow(i);
1636
+ }
1637
+ }
1638
+ }
1639
+
1640
+ function orthogonalConnect(sourceBounds, targetBounds) {
1641
+ const sourceMid = getMid(sourceBounds);
1642
+ const targetMid = getMid(targetBounds);
1643
+ const sourceBelow = sourceBounds.y > targetBounds.y;
1644
+ const sourceExitY = sourceBelow ? sourceBounds.y : sourceBounds.y + sourceBounds.height;
1645
+ const targetEntryY = sourceBelow ? targetBounds.y + targetBounds.height : targetBounds.y;
1646
+ const midY = (sourceExitY + targetEntryY) / 2;
1647
+ return [
1648
+ { x: sourceMid.x, y: sourceExitY },
1649
+ { x: sourceMid.x, y: midY },
1650
+ { x: targetMid.x, y: midY },
1651
+ { x: targetMid.x, y: targetEntryY }
1652
+ ];
1653
+ }
1654
+
1655
+ function hasOtherIncoming(element) {
1656
+ const fromHost = element.incoming?.filter(edge => edge.sourceRef !== element && edge.sourceRef.attachedToRef === undefined) || [];
1657
+
1658
+ const fromAttached = element.incoming?.filter(edge => edge.sourceRef !== element
1659
+ && edge.sourceRef.attachedToRef !== element);
1660
+
1661
+ return fromHost?.length > 0 || fromAttached?.length > 0;
1662
+ }
1663
+
1664
+ function layoutProcess(xml, options = {}) {
1665
+ return new Layouter().layoutProcess(xml, options);
1666
+ }
1667
+
1668
+ export { layoutProcess };
1669
+ //# sourceMappingURL=index.js.map