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