bpmn-auto-layout 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var BPMNModdle = require('bpmn-moddle');
3
+ var bpmnModdle = require('bpmn-moddle');
4
4
  var minDash = require('min-dash');
5
5
 
6
6
  function isConnection(element) {
@@ -32,576 +32,656 @@ function findElementInTree(currentElement, targetElement, visited = new Set()) {
32
32
  return false;
33
33
  }
34
34
 
35
- class Grid {
36
- constructor() {
37
- this.grid = [];
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 };
38
41
  }
39
42
 
40
- add(element, position) {
41
- if (!position) {
42
- this._addStart(element);
43
- return;
44
- }
43
+ if (is(element, 'bpmn:Task')) {
44
+ return { width: DEFAULT_TASK_WIDTH, height: DEFAULT_TASK_HEIGHT };
45
+ }
45
46
 
46
- const [ row, col ] = position;
47
- if (!row && !col) {
48
- this._addStart(element);
49
- }
47
+ if (is(element, 'bpmn:Gateway')) {
48
+ return { width: 50, height: 50 };
49
+ }
50
50
 
51
- if (!this.grid[row]) {
52
- this.grid[row] = [];
53
- }
51
+ if (is(element, 'bpmn:Event')) {
52
+ return { width: 36, height: 36 };
53
+ }
54
54
 
55
- if (this.grid[row][col]) {
56
- throw new Error('Grid is occupied please ensure the place you insert at is not occupied');
57
- }
55
+ if (is(element, 'bpmn:Participant')) {
56
+ return { width: 400, height: 100 };
57
+ }
58
58
 
59
- this.grid[row][col] = element;
59
+ if (is(element, 'bpmn:Lane')) {
60
+ return { width: 400, height: 100 };
60
61
  }
61
62
 
62
- createRow(afterIndex) {
63
- if (!afterIndex && !Number.isInteger(afterIndex)) {
64
- this.grid.push([]);
65
- } else {
66
- this.grid.splice(afterIndex + 1, 0, []);
67
- }
63
+ if (is(element, 'bpmn:DataObjectReference')) {
64
+ return { width: 36, height: 50 };
68
65
  }
69
66
 
70
- _addStart(element) {
71
- this.grid.push([ element ]);
67
+ if (is(element, 'bpmn:DataStoreReference')) {
68
+ return { width: 50, height: 50 };
72
69
  }
73
70
 
74
- addAfter(element, newElement) {
75
- if (!element) {
76
- this._addStart(newElement);
77
- }
78
- const [ row, col ] = this.find(element);
79
- this.grid[row].splice(col + 1, 0, newElement);
71
+ if (is(element, 'bpmn:TextAnnotation')) {
72
+ return { width: DEFAULT_TASK_WIDTH, height: 30 };
80
73
  }
81
74
 
82
- addBelow(element, newElement) {
83
- if (!element) {
84
- this._addStart(newElement);
85
- }
75
+ return { width: DEFAULT_TASK_WIDTH, height: DEFAULT_TASK_HEIGHT };
76
+ }
86
77
 
87
- const [ row, col ] = this.find(element);
78
+ function is(element, type) {
79
+ return element.$instanceOf(type);
80
+ }
88
81
 
89
- // We are at the bottom of the current grid - add empty row below
90
- if (!this.grid[row + 1]) {
91
- this.grid[row + 1] = [];
92
- }
82
+ const DEFAULT_CELL_WIDTH = 150;
83
+ const DEFAULT_CELL_HEIGHT = 140;
93
84
 
94
- // The element below is already occupied - insert new row
95
- if (this.grid[row + 1][col]) {
96
- this.grid.splice(row + 1, 0, []);
97
- }
85
+ function getMid(bounds) {
86
+ return {
87
+ x: bounds.x + bounds.width / 2,
88
+ y: bounds.y + bounds.height / 2
89
+ };
90
+ }
98
91
 
99
- if (this.grid[row + 1][col]) {
100
- throw new Error('Grid is occupied and we could not find a place - this should not happen');
101
- }
92
+ function getDockingPoint(point, rectangle, dockingDirection = 'r', targetOrientation = 'top-left') {
102
93
 
103
- this.grid[row + 1][col] = newElement;
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';
104
98
  }
105
99
 
106
- find(element) {
107
- let row, col;
108
- row = this.grid.findIndex((row) => {
109
- col = row.findIndex((el) => {
110
- return el === element;
111
- });
100
+ if (dockingDirection === 'v') {
101
+ dockingDirection = /top/.test(targetOrientation) ? 't' : 'b';
102
+ }
112
103
 
113
- return col !== -1;
114
- });
104
+ if (dockingDirection === 't') {
105
+ return { original: point, x: point.x, y: rectangle.y };
106
+ }
115
107
 
116
- return [ row, col ];
108
+ if (dockingDirection === 'r') {
109
+ return { original: point, x: rectangle.x + rectangle.width, y: point.y };
117
110
  }
118
111
 
119
- get(row, col) {
120
- return (this.grid[row] || [])[col];
112
+ if (dockingDirection === 'b') {
113
+ return { original: point, x: point.x, y: rectangle.y + rectangle.height };
121
114
  }
122
115
 
123
- getElementsInRange({ row: startRow, col: startCol }, { row: endRow, col: endCol }) {
124
- const elements = [];
116
+ if (dockingDirection === 'l') {
117
+ return { original: point, x: rectangle.x, y: point.y };
118
+ }
125
119
 
126
- if (startRow > endRow) {
127
- [ startRow, endRow ] = [ endRow, startRow ];
128
- }
120
+ throw new Error('unexpected dockingDirection: <' + dockingDirection + '>');
121
+ }
129
122
 
130
- if (startCol > endCol) {
131
- [ startCol, endCol ] = [ endCol, startCol ];
132
- }
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;
133
134
 
134
- for (let row = startRow; row <= endRow; row++) {
135
- for (let col = startCol; col <= endCol; col++) {
136
- const element = this.get(row, col);
135
+ const sourceBounds = sourceDi.get('bounds');
136
+ const targetBounds = targetDi.get('bounds');
137
137
 
138
- if (element) {
139
- elements.push(element);
140
- }
141
- }
142
- }
138
+ const sourceMid = getMid(sourceBounds);
139
+ const targetMid = getMid(targetBounds);
143
140
 
144
- return elements;
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
+ ];
145
160
  }
146
161
 
147
- adjustGridPosition(element) {
148
- let [ row, col ] = this.find(element);
149
- const [ , maxCol ] = this.getGridDimensions();
162
+ // negative dX indicates connection from future to past
163
+ if (dX < 0) {
150
164
 
151
- if (col < maxCol - 1) {
165
+ const { y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
152
166
 
153
- // add element in next column
154
- this.grid[row].length = maxCol;
155
- this.grid[row][maxCol] = element;
156
- this.grid[row][col] = null;
167
+ const offsetY = DEFAULT_CELL_HEIGHT / 2;
157
168
 
158
- }
159
- }
169
+ if (sourceMid.y >= targetMid.y) {
160
170
 
161
- adjustRowForMultipleIncoming(elements, currentElement) {
162
- const results = elements.map(element => this.find(element));
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
+ }
163
182
 
164
- // filter only rows that currently exist, excluding any future or non-existent rows
165
- const lowestRow = Math.min(...results
166
- .map(result => result[0])
167
- .filter(row => row >= 0));
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 {
168
190
 
169
- const [ row , col ] = this.find(currentElement);
191
+ // edge goes above
192
+ const bendY = sourceMid.y - offsetY;
170
193
 
171
- // if element doesn't already exist in current row, add element
172
- if (lowestRow < row && !this.grid[lowestRow][col]) {
173
- this.grid[lowestRow][col] = currentElement;
174
- this.grid[row][col] = null;
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
+ ];
175
200
  }
176
201
  }
177
202
 
178
- adjustColumnForMultipleIncoming(elements, currentElement) {
179
- const results = elements.map(element => this.find(element));
203
+ // connect horizontally
204
+ if (dY === 0) {
205
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
206
+ const { y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
180
207
 
181
- // filter only col that currently exist, excluding any future or non-existent col
182
- const maxCol = Math.max(...results
183
- .map(result => result[1])
184
- .filter(col => col >= 0));
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 {
185
216
 
186
- const [ row , col ] = this.find(currentElement);
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
+ }
187
223
 
188
- // add to the next column
189
- if (maxCol + 1 > col) {
190
- this.grid[row][maxCol + 1] = currentElement;
191
- this.grid[row][col] = null;
224
+ if (baseTargetGrid) {
225
+ lastPoint.y = targetBounds.y + DEFAULT_TASK_HEIGHT / 2 ;
226
+ }
227
+ return [
228
+ firstPoint,
229
+ lastPoint
230
+ ];
192
231
  }
193
232
  }
194
233
 
195
- getAllElements() {
196
- const elements = [];
234
+ // connect vertically
235
+ if (dX === 0) {
236
+ if (isDirectPathBlocked(source, target, layoutGrid)) {
197
237
 
198
- for (let row = 0; row < this.grid.length; row++) {
199
- for (let col = 0; col < this.grid[row].length; col++) {
200
- const element = this.get(row, col);
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 {
201
248
 
202
- if (element) {
203
- elements.push(element);
204
- }
205
- }
249
+ // if space is clear, connect directly
250
+ return [ getDockingPoint(sourceMid, sourceBounds, 'v', dockingSource),
251
+ getDockingPoint(targetMid, targetBounds, 'v', dockingTarget)
252
+ ];
206
253
  }
207
-
208
- return elements;
209
254
  }
210
255
 
211
- getGridDimensions() {
212
- const numRows = this.grid.length;
213
- let maxCols = 0;
256
+ const directManhattan = directManhattanConnect(source, target, layoutGrid);
214
257
 
215
- for (let i = 0; i < numRows; i++) {
216
- const currentRowLength = this.grid[i].length;
217
- if (currentRowLength > maxCols) {
218
- maxCols = currentRowLength;
219
- }
220
- }
258
+ if (directManhattan) {
259
+ const startPoint = getDockingPoint(sourceMid, sourceBounds, directManhattan[0], dockingSource);
260
+ const endPoint = getDockingPoint(targetMid, targetBounds, directManhattan[1], dockingTarget);
221
261
 
222
- return [ numRows , maxCols ];
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
+ ];
223
269
  }
270
+ const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
224
271
 
225
- elementsByPosition() {
226
- const elements = [];
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
+ }
227
281
 
228
- this.grid.forEach((row, rowIndex) => {
229
- row.forEach((element, colIndex) => {
230
- if (!element) {
231
- return;
232
- }
233
- elements.push({
234
- element,
235
- row: rowIndex,
236
- col: colIndex
237
- });
238
- });
239
- });
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
+ }
240
291
 
241
- return elements;
242
- }
292
+ function getBounds(element, row, col, shift, attachedTo) {
293
+ let { width, height } = getDefaultSize(element);
294
+ const { x, y } = shift;
243
295
 
244
- getElementsTotal() {
245
- const flattenedGrid = this.grid.flat();
246
- const uniqueElements = new Set(flattenedGrid.filter(value => value));
247
- return uniqueElements.size;
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
+ };
248
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
+ };
249
313
  }
250
314
 
251
- class DiFactory {
252
- constructor(moddle) {
253
- this.moddle = moddle;
254
- }
315
+ function isDirectPathBlocked(source, target, layoutGrid) {
316
+ const { row: sourceRow, col: sourceCol } = source.gridPosition;
317
+ const { row: targetRow, col: targetCol } = target.gridPosition;
255
318
 
256
- create(type, attrs) {
257
- return this.moddle.create(type, attrs || {});
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;
258
326
  }
259
327
 
260
- createDiBounds(bounds) {
261
- return this.create('dc:Bounds', bounds);
328
+ if (dY) {
329
+ totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: targetCol }, { row: targetRow, col: targetCol }).length;
262
330
  }
263
331
 
264
- createDiLabel() {
265
- return this.create('bpmndi:BPMNLabel', {
266
- bounds: this.createDiBounds()
267
- });
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;
268
345
  }
269
346
 
270
- createDiShape(semantic, bounds, attrs) {
271
- return this.create('bpmndi:BPMNShape', minDash.assign({
272
- bpmnElement: semantic,
273
- bounds: this.createDiBounds(bounds)
274
- }, attrs));
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' ];
275
365
  }
366
+ }
276
367
 
277
- createDiWaypoints(waypoints) {
278
- var self = this;
279
368
 
280
- return minDash.map(waypoints, function(pos) {
281
- return self.createDiWaypoint(pos);
282
- });
283
- }
369
+ function getMaxExpandedBetween(source, target, layoutGrid) {
284
370
 
285
- createDiWaypoint(point) {
286
- return this.create('dc:Point', minDash.pick(point, [ 'x', 'y' ]));
287
- }
371
+ const hostSource = source.attachedToRef ? source.attachedToRef : source;
372
+ const hostTarget = target.attachedToRef ? target.attachedToRef : target;
288
373
 
289
- createDiEdge(semantic, waypoints, attrs) {
290
- return this.create('bpmndi:BPMNEdge', minDash.assign({
291
- bpmnElement: semantic,
292
- waypoint: this.createDiWaypoints(waypoints)
293
- }, attrs));
294
- }
295
374
 
296
- createDiPlane(attrs) {
297
- return this.create('bpmndi:BPMNPlane', attrs);
298
- }
375
+ const [ sourceRow, sourceCol ] = layoutGrid.find(hostSource);
376
+ const [ , targetCol ] = layoutGrid.find(hostTarget);
299
377
 
300
- createDiDiagram(attrs) {
301
- return this.create('bpmndi:BPMNDiagram', attrs);
302
- }
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);
303
386
  }
304
387
 
305
- function getDefaultSize(element) {
306
- if (is(element, 'bpmn:SubProcess')) {
307
- return { width: 100, height: 80 };
388
+ class Grid {
389
+ constructor() {
390
+ this.grid = [];
308
391
  }
309
392
 
310
- if (is(element, 'bpmn:Task')) {
311
- return { width: 100, height: 80 };
312
- }
393
+ add(element, position) {
394
+ if (!position) {
395
+ this._addStart(element);
396
+ return;
397
+ }
313
398
 
314
- if (is(element, 'bpmn:Gateway')) {
315
- return { width: 50, height: 50 };
316
- }
399
+ const [ row, col ] = position;
400
+ if (!row && !col) {
401
+ this._addStart(element);
402
+ }
317
403
 
318
- if (is(element, 'bpmn:Event')) {
319
- return { width: 36, height: 36 };
320
- }
404
+ if (!this.grid[row]) {
405
+ this.grid[row] = [];
406
+ }
321
407
 
322
- if (is(element, 'bpmn:Participant')) {
323
- return { width: 400, height: 100 };
324
- }
408
+ if (this.grid[row][col]) {
409
+ throw new Error('Grid is occupied please ensure the place you insert at is not occupied');
410
+ }
325
411
 
326
- if (is(element, 'bpmn:Lane')) {
327
- return { width: 400, height: 100 };
412
+ this.grid[row][col] = element;
328
413
  }
329
414
 
330
- if (is(element, 'bpmn:DataObjectReference')) {
331
- return { width: 36, height: 50 };
415
+ createRow(afterIndex) {
416
+ if (!afterIndex && !Number.isInteger(afterIndex)) {
417
+ this.grid.push([]);
418
+ } else {
419
+ this.grid.splice(afterIndex + 1, 0, []);
420
+ }
332
421
  }
333
422
 
334
- if (is(element, 'bpmn:DataStoreReference')) {
335
- return { width: 50, height: 50 };
423
+ _addStart(element) {
424
+ this.grid.push([ element ]);
336
425
  }
337
426
 
338
- if (is(element, 'bpmn:TextAnnotation')) {
339
- return { width: 100, height: 30 };
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);
340
433
  }
341
434
 
342
- return { width: 100, height: 80 };
343
- }
344
-
345
- function is(element, type) {
346
- return element.$instanceOf(type);
347
- }
435
+ addBelow(element, newElement) {
436
+ if (!element) {
437
+ this._addStart(newElement);
438
+ }
348
439
 
349
- const DEFAULT_CELL_WIDTH = 150;
350
- const DEFAULT_CELL_HEIGHT = 140;
440
+ const [ row, col ] = this.find(element);
351
441
 
352
- function getMid(bounds) {
353
- return {
354
- x: bounds.x + bounds.width / 2,
355
- y: bounds.y + bounds.height / 2
356
- };
357
- }
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
+ }
358
446
 
359
- function getDockingPoint(point, rectangle, dockingDirection = 'r', targetOrientation = 'top-left') {
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
+ }
360
451
 
361
- // ensure we end up with a specific docking direction
362
- // based on the targetOrientation, if <h|v> is being passed
363
- if (dockingDirection === 'h') {
364
- dockingDirection = /left/.test(targetOrientation) ? 'l' : 'r';
365
- }
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
+ }
366
455
 
367
- if (dockingDirection === 'v') {
368
- dockingDirection = /top/.test(targetOrientation) ? 't' : 'b';
456
+ this.grid[row + 1][col] = newElement;
369
457
  }
370
458
 
371
- if (dockingDirection === 't') {
372
- return { original: point, x: point.x, y: rectangle.y };
373
- }
459
+ find(element) {
460
+ let row, col;
461
+ row = this.grid.findIndex((row) => {
462
+ col = row.findIndex((el) => {
463
+ return el === element;
464
+ });
374
465
 
375
- if (dockingDirection === 'r') {
376
- return { original: point, x: rectangle.x + rectangle.width, y: point.y };
377
- }
466
+ return col !== -1;
467
+ });
378
468
 
379
- if (dockingDirection === 'b') {
380
- return { original: point, x: point.x, y: rectangle.y + rectangle.height };
469
+ return [ row, col ];
381
470
  }
382
471
 
383
- if (dockingDirection === 'l') {
384
- return { original: point, x: rectangle.x, y: point.y };
472
+ get(row, col) {
473
+ return (this.grid[row] || [])[col];
385
474
  }
386
475
 
387
- throw new Error('unexpected dockingDirection: <' + dockingDirection + '>');
388
- }
389
-
390
- /**
391
- * Modified Manhattan layout: Uses space between grid coloumns to route connections
392
- * if direct connection is not possible.
393
- * @param {*} source
394
- * @param {*} target
395
- * @returns waypoints
396
- */
397
- function connectElements(source, target, layoutGrid) {
398
- const sourceDi = source.di;
399
- const targetDi = target.di;
476
+ getElementsInRange({ row: startRow, col: startCol }, { row: endRow, col: endCol }) {
477
+ const elements = [];
400
478
 
401
- const sourceBounds = sourceDi.get('bounds');
402
- const targetBounds = targetDi.get('bounds');
479
+ if (startRow > endRow) {
480
+ [ startRow, endRow ] = [ endRow, startRow ];
481
+ }
403
482
 
404
- const sourceMid = getMid(sourceBounds);
405
- const targetMid = getMid(targetBounds);
483
+ if (startCol > endCol) {
484
+ [ startCol, endCol ] = [ endCol, startCol ];
485
+ }
406
486
 
407
- const dX = target.gridPosition.col - source.gridPosition.col;
408
- const dY = target.gridPosition.row - source.gridPosition.row;
487
+ for (let row = startRow; row <= endRow; row++) {
488
+ for (let col = startCol; col <= endCol; col++) {
489
+ const element = this.get(row, col);
409
490
 
410
- const dockingSource = `${(dY > 0 ? 'bottom' : 'top')}-${dX > 0 ? 'right' : 'left'}`;
411
- const dockingTarget = `${(dY > 0 ? 'top' : 'bottom')}-${dX > 0 ? 'left' : 'right'}`;
491
+ if (element) {
492
+ elements.push(element);
493
+ }
494
+ }
495
+ }
412
496
 
413
- // Source === Target ==> Build loop
414
- if (dX === 0 && dY === 0) {
415
- const { x, y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
416
- return [
417
- getDockingPoint(sourceMid, sourceBounds, 'r', dockingSource),
418
- { x: x + DEFAULT_CELL_WIDTH, y: sourceMid.y },
419
- { x: x + DEFAULT_CELL_WIDTH, y: y },
420
- { x: targetMid.x, y: y },
421
- getDockingPoint(targetMid, targetBounds, 't', dockingTarget)
422
- ];
497
+ return elements;
423
498
  }
424
499
 
425
- // negative dX indicates connection from future to past
426
- if (dX < 0) {
427
-
428
- const { y } = coordinatesToPosition(source.gridPosition.row, source.gridPosition.col);
429
-
430
- const offsetY = DEFAULT_CELL_HEIGHT / 2;
431
-
432
- if (sourceMid.y >= targetMid.y) {
500
+ adjustGridPosition(element) {
501
+ let [ row, col ] = this.find(element);
502
+ const [ , maxCol ] = this.getGridDimensions();
433
503
 
434
- // edge goes below
435
- return [
436
- getDockingPoint(sourceMid, sourceBounds, 'b'),
437
- { x: sourceMid.x, y: y + DEFAULT_CELL_HEIGHT },
438
- { x: targetMid.x, y: y + DEFAULT_CELL_HEIGHT },
439
- getDockingPoint(targetMid, targetBounds, 'b')
440
- ];
441
- } else {
504
+ if (col < maxCol - 1) {
442
505
 
443
- // edge goes above
444
- const bendY = sourceMid.y - offsetY;
506
+ // add element in next column
507
+ this.grid[row].length = maxCol;
508
+ this.grid[row][maxCol] = element;
509
+ this.grid[row][col] = null;
445
510
 
446
- return [
447
- getDockingPoint(sourceMid, sourceBounds, 't'),
448
- { x: sourceMid.x, y: bendY },
449
- { x: targetMid.x, y: bendY },
450
- getDockingPoint(targetMid, targetBounds, 't')
451
- ];
452
511
  }
453
512
  }
454
513
 
455
- // connect horizontally
456
- if (dY === 0) {
457
- if (isDirectPathBlocked(source, target, layoutGrid)) {
514
+ adjustRowForMultipleIncoming(elements, currentElement) {
515
+ const results = elements.map(element => this.find(element));
458
516
 
459
- // Route on bottom
460
- return [
461
- getDockingPoint(sourceMid, sourceBounds, 'b'),
462
- { x: sourceMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
463
- { x: targetMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
464
- getDockingPoint(targetMid, targetBounds, 'b')
465
- ];
466
- } else {
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));
467
521
 
468
- // if space is clear, connect directly
469
- return [
470
- getDockingPoint(sourceMid, sourceBounds, 'h', dockingSource),
471
- getDockingPoint(targetMid, targetBounds, 'h', dockingTarget)
472
- ];
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;
473
528
  }
474
529
  }
475
530
 
476
- // connect vertically
477
- if (dX === 0) {
478
- if (isDirectPathBlocked(source, target, layoutGrid)) {
531
+ adjustColumnForMultipleIncoming(elements, currentElement) {
532
+ const results = elements.map(element => this.find(element));
479
533
 
480
- // Route parallel
481
- const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
482
- return [
483
- getDockingPoint(sourceMid, sourceBounds, 'r'),
484
- { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y }, // out right
485
- { x: targetMid.x + DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset },
486
- { x: targetMid.x, y: targetMid.y + yOffset },
487
- getDockingPoint(targetMid, targetBounds, Math.sign(yOffset) > 0 ? 'b' : 't')
488
- ];
489
- } else {
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));
490
538
 
491
- // if space is clear, connect directly
492
- return [ getDockingPoint(sourceMid, sourceBounds, 'v', dockingSource),
493
- getDockingPoint(targetMid, targetBounds, 'v', dockingTarget)
494
- ];
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;
495
545
  }
496
546
  }
497
547
 
498
- const directManhattan = directManhattanConnect(source, target, layoutGrid);
548
+ getAllElements() {
549
+ const elements = [];
499
550
 
500
- if (directManhattan) {
501
- const startPoint = getDockingPoint(sourceMid, sourceBounds, directManhattan[0], dockingSource);
502
- const endPoint = getDockingPoint(targetMid, targetBounds, directManhattan[1], dockingTarget);
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);
503
554
 
504
- const midPoint = directManhattan[0] === 'h' ? { x: endPoint.x, y: startPoint.y } : { x: startPoint.x, y: endPoint.y };
555
+ if (element) {
556
+ elements.push(element);
557
+ }
558
+ }
559
+ }
505
560
 
506
- return [
507
- startPoint,
508
- midPoint,
509
- endPoint
510
- ];
561
+ return elements;
511
562
  }
512
- const yOffset = -Math.sign(dY) * DEFAULT_CELL_HEIGHT / 2;
513
563
 
514
- return [
515
- getDockingPoint(sourceMid, sourceBounds, 'r', dockingSource),
516
- { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: sourceMid.y }, // out right
517
- { x: sourceMid.x + DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset }, // to target row
518
- { x: targetMid.x - DEFAULT_CELL_WIDTH / 2, y: targetMid.y + yOffset }, // to target column
519
- { x: targetMid.x - DEFAULT_CELL_WIDTH / 2, y: targetMid.y }, // to mid
520
- getDockingPoint(targetMid, targetBounds, 'l', dockingTarget)
521
- ];
522
- }
564
+ getGridDimensions() {
565
+ const numRows = this.grid.length;
566
+ let maxCols = 0;
523
567
 
524
- // helpers /////
525
- function coordinatesToPosition(row, col) {
526
- return {
527
- width: DEFAULT_CELL_WIDTH,
528
- height: DEFAULT_CELL_HEIGHT,
529
- x: col * DEFAULT_CELL_WIDTH,
530
- y: row * DEFAULT_CELL_HEIGHT
531
- };
532
- }
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
+ }
533
577
 
534
- function getBounds(element, row, col, attachedTo) {
535
- const { width, height } = getDefaultSize(element);
578
+ elementsByPosition() {
579
+ const elements = [];
536
580
 
537
- // Center in cell
538
- if (!attachedTo) {
539
- return {
540
- width, height,
541
- x: (col * DEFAULT_CELL_WIDTH) + (DEFAULT_CELL_WIDTH - width) / 2,
542
- y: row * DEFAULT_CELL_HEIGHT + (DEFAULT_CELL_HEIGHT - height) / 2
543
- };
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;
544
595
  }
545
596
 
546
- const hostBounds = getBounds(attachedTo, row, col);
597
+ getElementsTotal() {
598
+ const flattenedGrid = this.grid.flat();
599
+ const uniqueElements = new Set(flattenedGrid.filter(value => value));
600
+ return uniqueElements.size;
601
+ }
547
602
 
548
- return {
549
- width, height,
550
- x: Math.round(hostBounds.x + hostBounds.width / 2 - width / 2),
551
- y: Math.round(hostBounds.y + hostBounds.height - height / 2)
552
- };
553
- }
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
+ }
554
613
 
555
- function isDirectPathBlocked(source, target, layoutGrid) {
556
- const { row: sourceRow, col: sourceCol } = source.gridPosition;
557
- const { row: targetRow, col: targetCol } = target.gridPosition;
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.rowCount - 1) return;
558
621
 
559
- const dX = targetCol - sourceCol;
560
- const dY = targetRow - sourceRow;
622
+ const placeholder = Number.isInteger(colCount) && colCount > 0 ? Array(colCount) : Array(1);
561
623
 
562
- let totalElements = 0;
624
+ const row = this.grid[rowIndex];
563
625
 
564
- if (dX) {
565
- totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, { row: sourceRow, col: targetCol }).length;
626
+ if (!afterIndex && !Number.isInteger(afterIndex)) {
627
+ row.splice(row.length, 0, ...placeholder);
628
+ } else {
629
+ row.splice(afterIndex + 1, 0, ...placeholder);
630
+ }
566
631
  }
632
+ }
567
633
 
568
- if (dY) {
569
- totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: targetCol }, { row: targetRow, col: targetCol }).length;
634
+ class DiFactory {
635
+ constructor(moddle) {
636
+ this.moddle = moddle;
570
637
  }
571
638
 
572
- return totalElements > 2;
573
- }
639
+ create(type, attrs) {
640
+ return this.moddle.create(type, attrs || {});
641
+ }
574
642
 
575
- function directManhattanConnect(source, target, layoutGrid) {
576
- const { row: sourceRow, col: sourceCol } = source.gridPosition;
577
- const { row: targetRow, col: targetCol } = target.gridPosition;
643
+ createDiBounds(bounds) {
644
+ return this.create('dc:Bounds', bounds);
645
+ }
578
646
 
579
- const dX = targetCol - sourceCol;
580
- const dY = targetRow - sourceRow;
647
+ createDiLabel() {
648
+ return this.create('bpmndi:BPMNLabel', {
649
+ bounds: this.createDiBounds()
650
+ });
651
+ }
581
652
 
582
- // Only directly connect left-to-right flow
583
- if (!(dX > 0 && dY !== 0)) {
584
- return;
653
+ createDiShape(semantic, bounds, attrs) {
654
+ return this.create('bpmndi:BPMNShape', minDash.assign({
655
+ bpmnElement: semantic,
656
+ bounds: this.createDiBounds(bounds)
657
+ }, attrs));
585
658
  }
586
659
 
587
- // If below, go down then horizontal
588
- if (dY > 0) {
589
- let totalElements = 0;
590
- const bendPoint = { row: targetRow, col: sourceCol };
591
- totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
592
- totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
660
+ createDiWaypoints(waypoints) {
661
+ var self = this;
593
662
 
594
- return totalElements > 2 ? false : [ 'v', 'h' ];
595
- } else {
663
+ return minDash.map(waypoints, function(pos) {
664
+ return self.createDiWaypoint(pos);
665
+ });
666
+ }
596
667
 
597
- // If above, go horizontal than vertical
598
- let totalElements = 0;
599
- const bendPoint = { row: sourceRow, col: targetCol };
668
+ createDiWaypoint(point) {
669
+ return this.create('dc:Point', minDash.pick(point, [ 'x', 'y' ]));
670
+ }
600
671
 
601
- totalElements += layoutGrid.getElementsInRange({ row: sourceRow, col: sourceCol }, bendPoint).length;
602
- totalElements += layoutGrid.getElementsInRange(bendPoint, { row: targetRow, col: targetCol }).length;
672
+ createDiEdge(semantic, waypoints, attrs) {
673
+ return this.create('bpmndi:BPMNEdge', minDash.assign({
674
+ bpmnElement: semantic,
675
+ waypoint: this.createDiWaypoints(waypoints)
676
+ }, attrs));
677
+ }
603
678
 
604
- return totalElements > 2 ? false : [ 'h', 'v' ];
679
+ createDiPlane(attrs) {
680
+ return this.create('bpmndi:BPMNPlane', attrs);
681
+ }
682
+
683
+ createDiDiagram(attrs) {
684
+ return this.create('bpmndi:BPMNDiagram', attrs);
605
685
  }
606
686
  }
607
687
 
@@ -629,13 +709,13 @@ var attacherHandler = {
629
709
  return nextElements;
630
710
  },
631
711
 
632
- 'createElementDi': ({ element, row, col, diFactory }) => {
633
- const hostBounds = getBounds(element, row, col);
712
+ 'createElementDi': ({ element, row, col, diFactory, shift }) => {
713
+ const hostBounds = getBounds(element, row, col, shift);
634
714
 
635
715
  const DIs = [];
636
716
  (element.attachers || []).forEach((att, i, arr) => {
637
717
  att.gridPosition = { row, col };
638
- const bounds = getBounds(att, row, col, element);
718
+ const bounds = getBounds(att, row, col, shift, element);
639
719
 
640
720
  // distribute along lower edge
641
721
  bounds.x = hostBounds.x + (i + 1) * (hostBounds.width / (arr.length + 1)) - bounds.width / 2;
@@ -652,7 +732,7 @@ var attacherHandler = {
652
732
  return DIs;
653
733
  },
654
734
 
655
- 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
735
+ 'createConnectionDi': ({ element, row, col, layoutGrid, diFactory, shift }) => {
656
736
  const attachers = element.attachers || [];
657
737
 
658
738
  return attachers.flatMap(att => {
@@ -665,11 +745,9 @@ var attacherHandler = {
665
745
  // Correct waypoints if they don't automatically attach to the bottom
666
746
  ensureExitBottom(att, waypoints, [ row, col ]);
667
747
 
668
- const connectionDi = diFactory.createDiEdge(out, waypoints, {
748
+ return diFactory.createDiEdge(out, waypoints, {
669
749
  id: out.id + '_di'
670
750
  });
671
-
672
- return connectionDi;
673
751
  });
674
752
  });
675
753
  }
@@ -698,12 +776,14 @@ function ensureExitBottom(source, waypoints, [ row, col ]) {
698
776
  return;
699
777
  }
700
778
 
779
+ const baseSourceGrid = source.grid || source.attachedToRef?.grid;
780
+
701
781
  if (waypoints.length === 2) {
702
782
  const newStart = [
703
783
  dockingPoint,
704
- { x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
705
- { x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 1) * DEFAULT_CELL_HEIGHT },
706
- { x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 0.5) * DEFAULT_CELL_HEIGHT },
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 },
707
787
  ];
708
788
 
709
789
  waypoints.splice(0, 1, ...newStart);
@@ -713,18 +793,17 @@ function ensureExitBottom(source, waypoints, [ row, col ]) {
713
793
  // add waypoints to exit bottom and connect to existing path
714
794
  const newStart = [
715
795
  dockingPoint,
716
- { x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
717
- { x: waypoints[1].x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
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 },
718
798
  ];
719
799
 
720
800
  waypoints.splice(0, 1, ...newStart);
721
- return;
722
801
  }
723
802
 
724
803
  var elementHandler = {
725
- 'createElementDi': ({ element, row, col, diFactory }) => {
804
+ 'createElementDi': ({ element, row, col, diFactory, shift }) => {
726
805
 
727
- const bounds = getBounds(element, row, col);
806
+ const bounds = getBounds(element, row, col, shift);
728
807
 
729
808
  const options = {
730
809
  id: element.id + '_di'
@@ -734,6 +813,10 @@ var elementHandler = {
734
813
  options.isMarkerVisible = true;
735
814
  }
736
815
 
816
+ if (element.isExpanded) {
817
+ options.isExpanded = true;
818
+ }
819
+
737
820
  const shapeDi = diFactory.createDiShape(element, bounds, options);
738
821
  element.di = shapeDi;
739
822
  element.gridPosition = { row, col };
@@ -865,7 +948,7 @@ const handlers = [ elementHandler, incomingHandler, outgoingHandler, attacherHan
865
948
 
866
949
  class Layouter {
867
950
  constructor() {
868
- this.moddle = new BPMNModdle();
951
+ this.moddle = new bpmnModdle.BpmnModdle();
869
952
  this.diFactory = new DiFactory(this.moddle);
870
953
  this._handlers = handlers;
871
954
  }
@@ -878,23 +961,76 @@ class Layouter {
878
961
  }
879
962
 
880
963
  async layoutProcess(xml) {
881
- const { rootElement } = await this.moddle.fromXML(xml);
964
+ const moddleObj = await this.moddle.fromXML(xml);
965
+ const { rootElement } = moddleObj;
882
966
 
883
967
  this.diagram = rootElement;
884
968
 
885
- const root = this.getProcess();
969
+ const firstRootProcess = this.getProcess();
970
+
971
+ if (firstRootProcess) {
972
+
973
+ this.setExpandedPropertyToModdleElements(moddleObj);
974
+
975
+ this.setExecutedProcesses(firstRootProcess);
976
+
977
+ this.createGridsForProcesses();
886
978
 
887
- if (root) {
888
979
  this.cleanDi();
889
- this.handlePlane(root);
980
+
981
+ this.createRootDi(firstRootProcess);
982
+
983
+ this.drawProcesses();
890
984
  }
891
985
 
892
986
  return (await this.moddle.toXML(this.diagram, { format: true })).xml;
893
987
  }
894
988
 
895
- handlePlane(planeElement) {
896
- const layout = this.createGridLayout(planeElement);
897
- this.generateDi(planeElement, layout);
989
+ createGridsForProcesses() {
990
+ const processes = this.layoutedProcesses.sort((a, b) => b.level - a.level);
991
+
992
+ // create and add grids for each process
993
+ // root processes should be processed last for element expanding
994
+ for (const process of processes) {
995
+
996
+ // add base grid with collapsed elements
997
+ process.grid = this.createGridLayout(process);
998
+
999
+ expandGridHorizontally(process.grid);
1000
+ expandGridVertically(process.grid);
1001
+
1002
+ if (process.isExpanded) {
1003
+ const [ rowCount, colCount ] = process.grid.getGridDimensions();
1004
+ if (rowCount === 0) process.grid.createRow();
1005
+ if (colCount === 0) process.grid.createCol();
1006
+ }
1007
+
1008
+ }
1009
+ }
1010
+
1011
+ setExpandedPropertyToModdleElements(bpmnModel) {
1012
+ const allElements = bpmnModel.elementsById;
1013
+ if (allElements) {
1014
+ for (const element of Object.values(allElements)) {
1015
+ if (element.$type === 'bpmndi:BPMNShape' && element.isExpanded === true) element.bpmnElement.isExpanded = true;
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ setExecutedProcesses(firstRootProcess) {
1021
+ this.layoutedProcesses = [];
1022
+
1023
+ const executionStack = [ firstRootProcess ];
1024
+
1025
+ while (executionStack.length > 0) {
1026
+ const executedProcess = executionStack.pop();
1027
+ this.layoutedProcesses.push(executedProcess);
1028
+ executedProcess.level = executedProcess.$parent === this.diagram ? 0 : executedProcess.$parent.level + 1;
1029
+
1030
+ const nextProcesses = executedProcess.flowElements?.filter(flowElement => flowElement.$type === 'bpmn:SubProcess') || [];
1031
+
1032
+ executionStack.splice(executionStack.length, 0, ...nextProcesses);
1033
+ }
898
1034
  }
899
1035
 
900
1036
  cleanDi() {
@@ -912,20 +1048,16 @@ class Layouter {
912
1048
  return grid;
913
1049
  }
914
1050
 
915
- const boundaryEvents = flowElements.filter(el => isBoundaryEvent(el));
916
- boundaryEvents.forEach(boundaryEvent => {
917
- const attachedTask = boundaryEvent.attachedToRef;
918
- const attachers = attachedTask.attachers || [];
919
- attachers.push(boundaryEvent);
920
- attachedTask.attachers = attachers;
921
- });
1051
+ bindBoundaryEventsWithHosts (flowElements);
922
1052
 
923
1053
  // Depth-first-search
924
1054
  const visited = new Set();
925
1055
  while (visited.size < elements.filter(element => !element.attachedToRef).length) {
926
-
927
1056
  const startingElements = flowElements.filter(el => {
928
- return !isConnection(el) && !isBoundaryEvent(el) && (!el.incoming || el.length === 0) && !visited.has(el);
1057
+ return !isConnection(el) &&
1058
+ !isBoundaryEvent(el) &&
1059
+ (!el.incoming || !hasOtherIncoming(el)) &&
1060
+ !visited.has(el);
929
1061
  });
930
1062
 
931
1063
  const stack = [ ...startingElements ];
@@ -937,7 +1069,7 @@ class Layouter {
937
1069
 
938
1070
  this.handleGrid(grid,visited,stack);
939
1071
 
940
- if (grid.getElementsTotal() != elements.length) {
1072
+ if (grid.getElementsTotal() !== elements.length) {
941
1073
  const gridElements = grid.getAllElements();
942
1074
  const missingElements = elements.filter(el => !gridElements.includes(el) && !isBoundaryEvent(el));
943
1075
  if (missingElements.length > 0) {
@@ -951,30 +1083,17 @@ class Layouter {
951
1083
  return grid;
952
1084
  }
953
1085
 
954
- generateDi(root, layoutGrid) {
1086
+ generateDi(layoutGrid , shift, procDi) {
955
1087
  const diFactory = this.diFactory;
956
1088
 
957
- // Step 0: Create Root element
958
- const diagram = this.diagram;
959
-
960
- var planeDi = diFactory.createDiPlane({
961
- id: 'BPMNPlane_' + root.id,
962
- bpmnElement: root
963
- });
964
- var diagramDi = diFactory.createDiDiagram({
965
- id: 'BPMNDiagram_' + root.id,
966
- plane: planeDi
967
- });
968
-
969
- // deepest subprocess is added first - insert at the front
970
- diagram.diagrams.unshift(diagramDi);
1089
+ const prePlaneElement = procDi ? procDi : this.diagram.diagrams[0];
971
1090
 
972
- const planeElement = planeDi.get('planeElement');
1091
+ const planeElement = prePlaneElement.plane.get('planeElement');
973
1092
 
974
1093
  // Step 1: Create DI for all elements
975
1094
  layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
976
1095
  const dis = this
977
- .handle('createElementDi', { element, row, col, layoutGrid, diFactory })
1096
+ .handle('createElementDi', { element, row, col, layoutGrid, diFactory, shift })
978
1097
  .flat();
979
1098
 
980
1099
  planeElement.push(...dis);
@@ -983,7 +1102,7 @@ class Layouter {
983
1102
  // Step 2: Create DI for all connections
984
1103
  layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
985
1104
  const dis = this
986
- .handle('createConnectionDi', { element, row, col, layoutGrid, diFactory })
1105
+ .handle('createConnectionDi', { element, row, col, layoutGrid, diFactory, shift })
987
1106
  .flat();
988
1107
 
989
1108
  planeElement.push(...dis);
@@ -994,10 +1113,6 @@ class Layouter {
994
1113
  while (stack.length > 0) {
995
1114
  const currentElement = stack.pop();
996
1115
 
997
- if (is(currentElement, 'bpmn:SubProcess')) {
998
- this.handlePlane(currentElement);
999
- }
1000
-
1001
1116
  const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited, stack });
1002
1117
 
1003
1118
  nextElements.flat().forEach(el => {
@@ -1010,6 +1125,146 @@ class Layouter {
1010
1125
  getProcess() {
1011
1126
  return this.diagram.get('rootElements').find(el => el.$type === 'bpmn:Process');
1012
1127
  }
1128
+
1129
+ createRootDi(processes) {
1130
+ this.createProcessDi(processes);
1131
+ }
1132
+
1133
+ createProcessDi(element) {
1134
+ const diFactory = this.diFactory;
1135
+
1136
+ const planeDi = diFactory.createDiPlane({
1137
+ id: 'BPMNPlane_' + element.id,
1138
+ bpmnElement: element
1139
+ });
1140
+ const diagramDi = diFactory.createDiDiagram({
1141
+ id: 'BPMNDiagram_' + element.id,
1142
+ plane: planeDi
1143
+ });
1144
+
1145
+ const diagram = this.diagram;
1146
+
1147
+ diagram.diagrams.push(diagramDi);
1148
+
1149
+ return diagramDi;
1150
+ }
1151
+
1152
+ /**
1153
+ * Draw processes.
1154
+ * Root processes should be processed first for element expanding
1155
+ */
1156
+ drawProcesses() {
1157
+ const sortedProcesses = this.layoutedProcesses.sort((a, b) => a.level - b.level);
1158
+
1159
+ for (const process of sortedProcesses) {
1160
+
1161
+ // draw processes in expanded elements
1162
+ if (process.isExpanded) {
1163
+ const baseProcDi = this.getElementDi(process);
1164
+ const diagram = this.getProcDi(baseProcDi);
1165
+ let { x, y } = baseProcDi.bounds;
1166
+ const { width, height } = getDefaultSize(process);
1167
+ x += DEFAULT_CELL_WIDTH / 2 - width / 4;
1168
+ y += DEFAULT_CELL_HEIGHT - height - height / 4;
1169
+ this.generateDi(process.grid, { x, y }, diagram);
1170
+ continue;
1171
+ }
1172
+
1173
+ // draw other processes
1174
+ const diagram = this.diagram.diagrams.find(diagram => diagram.plane.bpmnElement === process);
1175
+ this.generateDi(process.grid, { x: 0, y: 0 }, diagram);
1176
+ }
1177
+ }
1178
+
1179
+ getElementDi(element) {
1180
+ return this.diagram.diagrams
1181
+ .map(diagram => diagram.plane.planeElement).flat()
1182
+ .find(item => item.bpmnElement === element);
1183
+ }
1184
+
1185
+ getProcDi(element) {
1186
+ return this.diagram.diagrams.find(diagram => diagram.plane.planeElement.includes(element));
1187
+ }
1188
+ }
1189
+
1190
+ function bindBoundaryEventsWithHosts(elements) {
1191
+ const boundaryEvents = elements.filter(element => isBoundaryEvent(element));
1192
+ boundaryEvents.forEach(boundaryEvent => {
1193
+ const attachedTask = boundaryEvent.attachedToRef;
1194
+ const attachers = attachedTask.attachers || [];
1195
+ attachers.push(boundaryEvent);
1196
+ attachedTask.attachers = attachers;
1197
+ });
1198
+ }
1199
+
1200
+ /**
1201
+ * Check grid by columns.
1202
+ * If column has elements with isExpanded === true,
1203
+ * find the maximum size of elements grids and expand the parent grid horizontally.
1204
+ * @param grid
1205
+ */
1206
+ function expandGridHorizontally(grid) {
1207
+ const [ numRows , maxCols ] = grid.getGridDimensions();
1208
+ for (let i = maxCols - 1 ; i >= 0; i--) {
1209
+ const elementsInCol = [];
1210
+ for (let j = 0; j < numRows; j++) {
1211
+ const candidate = grid.get(j, i);
1212
+ if (candidate && candidate.isExpanded) elementsInCol.push(candidate);
1213
+ }
1214
+
1215
+ if (elementsInCol.length === 0) continue;
1216
+
1217
+ const maxColCount = elementsInCol.reduce((acc,cur) => {
1218
+ const [ ,curCols ] = cur.grid.getGridDimensions();
1219
+ if (acc === undefined || curCols > acc) return curCols;
1220
+ return acc;
1221
+ }, undefined);
1222
+
1223
+ const shift = !maxColCount ? 2 : maxColCount;
1224
+ grid.createCol(i, shift);
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Check grid by rows.
1230
+ * If row has elements with isExpanded === true,
1231
+ * find the maximum size of elements grids and expand the parent grid vertically.
1232
+ * @param grid
1233
+ */
1234
+ function expandGridVertically(grid) {
1235
+ const [ numRows , maxCols ] = grid.getGridDimensions();
1236
+
1237
+ for (let i = numRows - 1 ; i >= 0; i--) {
1238
+ const elementsInRow = [];
1239
+ for (let j = 0; j < maxCols; j++) {
1240
+ const candidate = grid.get(i, j);
1241
+ if (candidate && candidate.isExpanded) elementsInRow.push(candidate);
1242
+ }
1243
+
1244
+ if (elementsInRow.length === 0) continue;
1245
+
1246
+ const maxRowCount = elementsInRow.reduce((acc,cur) => {
1247
+ const [ curRows ] = cur.grid.getGridDimensions();
1248
+ if (acc === undefined || curRows > acc) return curRows;
1249
+ return acc;
1250
+ }, undefined);
1251
+
1252
+ const shift = !maxRowCount ? 1 : maxRowCount;
1253
+
1254
+ // expand the parent grid vertically
1255
+ for (let index = 0; index < shift; index++) {
1256
+ grid.createRow(i);
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ function hasOtherIncoming(element) {
1262
+ const fromHost = element.incoming?.filter(edge => edge.sourceRef !== element && edge.sourceRef.attachedToRef === undefined) || [];
1263
+
1264
+ const fromAttached = element.incoming?.filter(edge => edge.sourceRef !== element
1265
+ && edge.sourceRef.attachedToRef !== element);
1266
+
1267
+ return fromHost?.length > 0 || fromAttached?.length > 0;
1013
1268
  }
1014
1269
 
1015
1270
  function layoutProcess(xml) {