bpmn-auto-layout 0.4.0 → 1.0.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/README.md +36 -17
- package/dist/index.cjs +228 -29
- package/dist/index.cjs.map +1 -1
- package/dist/{index.esm.js → index.js} +229 -30
- package/dist/index.js.map +1 -0
- package/package.json +19 -11
- package/dist/index.esm.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,46 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bpmn-io/bpmn-auto-layout/actions/workflows/CI.yml)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Create and layout the graphical representation of a BPMN diagram.
|
|
7
6
|
|
|
8
7
|
## Usage
|
|
9
8
|
|
|
10
|
-
This library works
|
|
11
|
-
|
|
12
|
-
To layout diagrams these must have __exactly one single start event__.
|
|
9
|
+
This library works with [Node.js](https://nodejs.org/) and in the browser.
|
|
13
10
|
|
|
14
11
|
```javascript
|
|
15
12
|
import { layoutProcess } from 'bpmn-auto-layout';
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
import diagramXML from './diagram.bpmn';
|
|
18
15
|
|
|
19
|
-
const
|
|
16
|
+
const diagramWithLayoutXML = await layoutProcess(diagramXML);
|
|
20
17
|
|
|
21
|
-
console.log(
|
|
18
|
+
console.log(diagramWithLayoutXML);
|
|
22
19
|
```
|
|
23
|
-
## Unsupported Concepts and elements
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
- Pools
|
|
27
|
-
- Data/Message Flows and Objects, Data Stores
|
|
28
|
-
- event sub-processes
|
|
21
|
+
## Limitations
|
|
29
22
|
|
|
23
|
+
* Given a collaboration only the first participant's process will be laid out
|
|
24
|
+
* Sub-processes will be laid out as collapsed sub-processes
|
|
25
|
+
* The following elements are not laid out:
|
|
26
|
+
* Groups
|
|
27
|
+
* Text annotations
|
|
28
|
+
* Associations
|
|
29
|
+
* Message flows
|
|
30
30
|
|
|
31
31
|
## Resources
|
|
32
32
|
|
|
33
|
-
*
|
|
34
|
-
|
|
33
|
+
* [Issues](https://github.com/bpmn-io/bpmn-auto-layout/issues)
|
|
35
34
|
|
|
36
|
-
##
|
|
35
|
+
## Build and Run
|
|
37
36
|
|
|
38
37
|
```sh
|
|
38
|
+
# install dependencies
|
|
39
39
|
npm install
|
|
40
|
+
|
|
41
|
+
# build and run tests
|
|
40
42
|
npm run all
|
|
43
|
+
|
|
44
|
+
# run example
|
|
45
|
+
npm start
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
## Test
|
|
49
|
+
|
|
50
|
+
We use snapshot testing to verify old and new layout attempts. A mismatch is indicated as a test failure.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# run tests
|
|
54
|
+
npm test
|
|
55
|
+
|
|
56
|
+
# inspect the results
|
|
57
|
+
npm run test:inspect
|
|
58
|
+
|
|
59
|
+
# run update snapshots
|
|
60
|
+
npm run test:update-snapshots
|
|
61
|
+
```
|
|
44
62
|
|
|
63
|
+
Add new test cases to [`test/fixtures`](./test/fixures) and they will be picked up automatically.
|
|
45
64
|
|
|
46
65
|
## License
|
|
47
66
|
|
package/dist/index.cjs
CHANGED
|
@@ -11,6 +11,27 @@ function isBoundaryEvent(element) {
|
|
|
11
11
|
return !!element.attachedToRef;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function findElementInTree(currentElement, targetElement, visited = new Set()) {
|
|
15
|
+
|
|
16
|
+
if (currentElement === targetElement) return true;
|
|
17
|
+
|
|
18
|
+
if (visited.has(currentElement)) return false;
|
|
19
|
+
|
|
20
|
+
visited.add(currentElement);
|
|
21
|
+
|
|
22
|
+
// If currentElement has no outgoing connections, return false
|
|
23
|
+
if (!currentElement.outgoing || currentElement.outgoing.length === 0) return false;
|
|
24
|
+
|
|
25
|
+
// Recursively check each outgoing element
|
|
26
|
+
for (let nextElement of currentElement.outgoing.map(out => out.targetRef)) {
|
|
27
|
+
if (findElementInTree(nextElement, targetElement, visited)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
class Grid {
|
|
15
36
|
constructor() {
|
|
16
37
|
this.grid = [];
|
|
@@ -54,7 +75,6 @@ class Grid {
|
|
|
54
75
|
if (!element) {
|
|
55
76
|
this._addStart(newElement);
|
|
56
77
|
}
|
|
57
|
-
|
|
58
78
|
const [ row, col ] = this.find(element);
|
|
59
79
|
this.grid[row].splice(col + 1, 0, newElement);
|
|
60
80
|
}
|
|
@@ -124,6 +144,84 @@ class Grid {
|
|
|
124
144
|
return elements;
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
adjustGridPosition(element) {
|
|
148
|
+
let [ row, col ] = this.find(element);
|
|
149
|
+
const [ , maxCol ] = this.getGridDimensions();
|
|
150
|
+
|
|
151
|
+
if (col < maxCol - 1) {
|
|
152
|
+
|
|
153
|
+
// add element in next column
|
|
154
|
+
this.grid[row].length = maxCol;
|
|
155
|
+
this.grid[row][maxCol] = element;
|
|
156
|
+
this.grid[row][col] = null;
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
adjustRowForMultipleIncoming(elements, currentElement) {
|
|
162
|
+
const results = elements.map(element => this.find(element));
|
|
163
|
+
|
|
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));
|
|
168
|
+
|
|
169
|
+
const [ row , col ] = this.find(currentElement);
|
|
170
|
+
|
|
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;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
adjustColumnForMultipleIncoming(elements, currentElement) {
|
|
179
|
+
const results = elements.map(element => this.find(element));
|
|
180
|
+
|
|
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));
|
|
185
|
+
|
|
186
|
+
const [ row , col ] = this.find(currentElement);
|
|
187
|
+
|
|
188
|
+
// add to the next column
|
|
189
|
+
if (maxCol + 1 > col) {
|
|
190
|
+
this.grid[row][maxCol + 1] = currentElement;
|
|
191
|
+
this.grid[row][col] = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getAllElements() {
|
|
196
|
+
const elements = [];
|
|
197
|
+
|
|
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);
|
|
201
|
+
|
|
202
|
+
if (element) {
|
|
203
|
+
elements.push(element);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return elements;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getGridDimensions() {
|
|
212
|
+
const numRows = this.grid.length;
|
|
213
|
+
let maxCols = 0;
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < numRows; i++) {
|
|
216
|
+
const currentRowLength = this.grid[i].length;
|
|
217
|
+
if (currentRowLength > maxCols) {
|
|
218
|
+
maxCols = currentRowLength;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return [ numRows , maxCols ];
|
|
223
|
+
}
|
|
224
|
+
|
|
127
225
|
elementsByPosition() {
|
|
128
226
|
const elements = [];
|
|
129
227
|
|
|
@@ -142,6 +240,12 @@ class Grid {
|
|
|
142
240
|
|
|
143
241
|
return elements;
|
|
144
242
|
}
|
|
243
|
+
|
|
244
|
+
getElementsTotal() {
|
|
245
|
+
const flattenedGrid = this.grid.flat();
|
|
246
|
+
const uniqueElements = new Set(flattenedGrid.filter(value => value));
|
|
247
|
+
return uniqueElements.size;
|
|
248
|
+
}
|
|
145
249
|
}
|
|
146
250
|
|
|
147
251
|
class DiFactory {
|
|
@@ -323,12 +427,12 @@ function connectElements(source, target, layoutGrid) {
|
|
|
323
427
|
if (dY === 0) {
|
|
324
428
|
if (isDirectPathBlocked(source, target, layoutGrid)) {
|
|
325
429
|
|
|
326
|
-
// Route on
|
|
430
|
+
// Route on bottom
|
|
327
431
|
return [
|
|
328
|
-
getDockingPoint(sourceMid, sourceBounds, '
|
|
329
|
-
{ x: sourceMid.x, y: sourceMid.y
|
|
330
|
-
{ x: targetMid.x, y: sourceMid.y
|
|
331
|
-
getDockingPoint(targetMid, targetBounds, '
|
|
432
|
+
getDockingPoint(sourceMid, sourceBounds, 'b'),
|
|
433
|
+
{ x: sourceMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
|
|
434
|
+
{ x: targetMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
|
|
435
|
+
getDockingPoint(targetMid, targetBounds, 'b')
|
|
332
436
|
];
|
|
333
437
|
} else {
|
|
334
438
|
|
|
@@ -362,6 +466,15 @@ function connectElements(source, target, layoutGrid) {
|
|
|
362
466
|
}
|
|
363
467
|
}
|
|
364
468
|
|
|
469
|
+
// negative dX indicates connection from future to past
|
|
470
|
+
if (dX < 0 && dY <= 0) {
|
|
471
|
+
return [
|
|
472
|
+
getDockingPoint(sourceMid, sourceBounds, 'b'),
|
|
473
|
+
{ x: sourceMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
|
|
474
|
+
{ x: targetMid.x, y: sourceMid.y + DEFAULT_CELL_HEIGHT / 2 },
|
|
475
|
+
getDockingPoint(targetMid, targetBounds, 'b')
|
|
476
|
+
];
|
|
477
|
+
}
|
|
365
478
|
const directManhattan = directManhattanConnect(source, target, layoutGrid);
|
|
366
479
|
|
|
367
480
|
if (directManhattan) {
|
|
@@ -477,7 +590,7 @@ var attacherHandler = {
|
|
|
477
590
|
const nextElements = [];
|
|
478
591
|
|
|
479
592
|
const attachedOutgoing = (element.attachers || [])
|
|
480
|
-
.map(
|
|
593
|
+
.map(attacher => (attacher.outgoing || []).reverse())
|
|
481
594
|
.flat()
|
|
482
595
|
.map(out => out.targetRef);
|
|
483
596
|
|
|
@@ -550,11 +663,6 @@ function insertIntoGrid(newElement, host, grid) {
|
|
|
550
663
|
grid.createRow(row);
|
|
551
664
|
}
|
|
552
665
|
|
|
553
|
-
// Host has element directly after, add space
|
|
554
|
-
if (grid.get(row, col + 1)) {
|
|
555
|
-
grid.addAfter(host, null);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
666
|
grid.add(newElement, [ row + 1, col + 1 ]);
|
|
559
667
|
}
|
|
560
668
|
|
|
@@ -614,8 +722,8 @@ var elementHandler = {
|
|
|
614
722
|
};
|
|
615
723
|
|
|
616
724
|
var outgoingHandler = {
|
|
617
|
-
'addToGrid': ({ element, grid, visited }) => {
|
|
618
|
-
|
|
725
|
+
'addToGrid': ({ element, grid, visited, stack }) => {
|
|
726
|
+
let nextElements = [];
|
|
619
727
|
|
|
620
728
|
// Handle outgoing paths
|
|
621
729
|
const outgoing = (element.outgoing || [])
|
|
@@ -623,14 +731,28 @@ var outgoingHandler = {
|
|
|
623
731
|
.filter(el => el);
|
|
624
732
|
|
|
625
733
|
let previousElement = null;
|
|
734
|
+
|
|
735
|
+
if (outgoing.length > 1 && isNextElementTasks(outgoing)) {
|
|
736
|
+
grid.adjustGridPosition(element);
|
|
737
|
+
}
|
|
738
|
+
|
|
626
739
|
outgoing.forEach((nextElement, index, arr) => {
|
|
627
740
|
if (visited.has(nextElement)) {
|
|
628
741
|
return;
|
|
629
742
|
}
|
|
630
743
|
|
|
744
|
+
// Prevents revisiting future incoming elements and ensures proper traversal without early exit.
|
|
745
|
+
if ((previousElement || stack.length > 0) && isFutureIncoming(nextElement, visited) && !checkForLoop(nextElement, visited)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
631
749
|
if (!previousElement) {
|
|
632
750
|
grid.addAfter(element, nextElement);
|
|
633
751
|
}
|
|
752
|
+
|
|
753
|
+
else if (is(element, 'bpmn:ExclusiveGateway') && is(nextElement, 'bpmn:ExclusiveGateway')) {
|
|
754
|
+
grid.addAfter(previousElement, nextElement);
|
|
755
|
+
}
|
|
634
756
|
else {
|
|
635
757
|
grid.addBelow(arr[index - 1], nextElement);
|
|
636
758
|
}
|
|
@@ -644,8 +766,11 @@ var outgoingHandler = {
|
|
|
644
766
|
visited.add(nextElement);
|
|
645
767
|
});
|
|
646
768
|
|
|
769
|
+
// Sort elements by priority to ensure proper stack placement
|
|
770
|
+
nextElements = sortByType(nextElements, 'bpmn:ExclusiveGateway'); // TODO: sort by priority
|
|
647
771
|
return nextElements;
|
|
648
772
|
},
|
|
773
|
+
|
|
649
774
|
'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
|
|
650
775
|
const outgoing = element.outgoing || [];
|
|
651
776
|
|
|
@@ -663,7 +788,59 @@ var outgoingHandler = {
|
|
|
663
788
|
}
|
|
664
789
|
};
|
|
665
790
|
|
|
666
|
-
|
|
791
|
+
|
|
792
|
+
// helpers /////
|
|
793
|
+
|
|
794
|
+
function sortByType(arr, type) {
|
|
795
|
+
const nonMatching = arr.filter(item => !is(item,type));
|
|
796
|
+
const matching = arr.filter(item => is(item,type));
|
|
797
|
+
|
|
798
|
+
return [ ...matching, ...nonMatching ];
|
|
799
|
+
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function checkForLoop(element, visited) {
|
|
803
|
+
for (const incomingElement of element.incoming) {
|
|
804
|
+
if (!visited.has(incomingElement.sourceRef)) {
|
|
805
|
+
return findElementInTree(element, incomingElement.sourceRef);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
function isFutureIncoming(element, visited) {
|
|
812
|
+
if (element.incoming.length > 1) {
|
|
813
|
+
for (const incomingElement of element.incoming) {
|
|
814
|
+
if (!visited.has(incomingElement.sourceRef)) {
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function isNextElementTasks(elements) {
|
|
823
|
+
return elements.every(element => is(element, 'bpmn:Task'));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
var incomingHandler = {
|
|
827
|
+
'addToGrid': ({ element, grid, visited }) => {
|
|
828
|
+
const nextElements = [];
|
|
829
|
+
|
|
830
|
+
const incoming = (element.incoming || [])
|
|
831
|
+
.map(out => out.sourceRef)
|
|
832
|
+
.filter(el => el);
|
|
833
|
+
|
|
834
|
+
// adjust the row if it is empty
|
|
835
|
+
if (incoming.length > 1) {
|
|
836
|
+
grid.adjustColumnForMultipleIncoming(incoming, element);
|
|
837
|
+
grid.adjustRowForMultipleIncoming(incoming, element);
|
|
838
|
+
}
|
|
839
|
+
return nextElements;
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const handlers = [ elementHandler, incomingHandler, outgoingHandler, attacherHandler ];
|
|
667
844
|
|
|
668
845
|
class Layouter {
|
|
669
846
|
constructor() {
|
|
@@ -686,8 +863,10 @@ class Layouter {
|
|
|
686
863
|
|
|
687
864
|
const root = this.getProcess();
|
|
688
865
|
|
|
689
|
-
|
|
690
|
-
|
|
866
|
+
if (root) {
|
|
867
|
+
this.cleanDi();
|
|
868
|
+
this.handlePlane(root);
|
|
869
|
+
}
|
|
691
870
|
|
|
692
871
|
return (await this.moddle.toXML(this.diagram, { format: true })).xml;
|
|
693
872
|
}
|
|
@@ -704,7 +883,13 @@ class Layouter {
|
|
|
704
883
|
createGridLayout(root) {
|
|
705
884
|
const grid = new Grid();
|
|
706
885
|
|
|
707
|
-
const flowElements = root.flowElements;
|
|
886
|
+
const flowElements = root.flowElements || [];
|
|
887
|
+
const elements = flowElements.filter(el => !is(el,'bpmn:SequenceFlow'));
|
|
888
|
+
|
|
889
|
+
// check for empty process/subprocess
|
|
890
|
+
if (!flowElements) {
|
|
891
|
+
return grid;
|
|
892
|
+
}
|
|
708
893
|
|
|
709
894
|
const startingElements = flowElements.filter(el => {
|
|
710
895
|
return !isConnection(el) && !isBoundaryEvent(el) && (!el.incoming || el.length === 0);
|
|
@@ -727,19 +912,17 @@ class Layouter {
|
|
|
727
912
|
visited.add(el);
|
|
728
913
|
});
|
|
729
914
|
|
|
730
|
-
|
|
731
|
-
const currentElement = stack.pop();
|
|
915
|
+
this.handleGrid(grid,visited,stack);
|
|
732
916
|
|
|
733
|
-
|
|
734
|
-
|
|
917
|
+
if (grid.getElementsTotal() != elements.length) {
|
|
918
|
+
const gridElements = grid.getAllElements();
|
|
919
|
+
const missingElements = elements.filter(el => !gridElements.includes(el) && !isBoundaryEvent(el));
|
|
920
|
+
if (missingElements.length > 1) {
|
|
921
|
+
stack.push(missingElements[0]);
|
|
922
|
+
grid.add(missingElements[0]);
|
|
923
|
+
visited.add(missingElements[0]);
|
|
924
|
+
this.handleGrid(grid,visited,stack);
|
|
735
925
|
}
|
|
736
|
-
|
|
737
|
-
const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited });
|
|
738
|
-
|
|
739
|
-
nextElements.flat().forEach(el => {
|
|
740
|
-
stack.push(el);
|
|
741
|
-
visited.add(el);
|
|
742
|
-
});
|
|
743
926
|
}
|
|
744
927
|
|
|
745
928
|
return grid;
|
|
@@ -784,6 +967,22 @@ class Layouter {
|
|
|
784
967
|
});
|
|
785
968
|
}
|
|
786
969
|
|
|
970
|
+
handleGrid(grid, visited, stack) {
|
|
971
|
+
while (stack.length > 0) {
|
|
972
|
+
const currentElement = stack.pop();
|
|
973
|
+
|
|
974
|
+
if (is(currentElement, 'bpmn:SubProcess')) {
|
|
975
|
+
this.handlePlane(currentElement);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited, stack });
|
|
979
|
+
|
|
980
|
+
nextElements.flat().forEach(el => {
|
|
981
|
+
stack.push(el);
|
|
982
|
+
visited.add(el);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
787
986
|
|
|
788
987
|
getProcess() {
|
|
789
988
|
return this.diagram.get('rootElements').find(el => el.$type === 'bpmn:Process');
|