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