bpmn-auto-layout-extended 1.0.2 → 1.0.5
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 +3 -2
- package/dist/index.cjs +217 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +217 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/krasucki/bpmn-auto-layout-extended/actions/workflows/CI.yml)
|
|
4
4
|
|
|
5
|
-
Extended fork of [bpmn-auto-layout](https://github.com/bpmn-io/bpmn-auto-layout) with support for collaborations, message flows, text annotations, groups, and sub-process expansion.
|
|
5
|
+
Extended fork of [bpmn-auto-layout](https://github.com/bpmn-io/bpmn-auto-layout) with support for collaborations, message flows, text annotations, groups, lanes, and sub-process expansion.
|
|
6
6
|
|
|
7
7
|
Create and layout the graphical representation of a BPMN diagram.
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ Create and layout the graphical representation of a BPMN diagram.
|
|
|
11
11
|
This library works with [Node.js](https://nodejs.org/) and in the browser.
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import { layoutProcess } from 'bpmn-auto-layout';
|
|
14
|
+
import { layoutProcess } from 'bpmn-auto-layout-extended';
|
|
15
15
|
|
|
16
16
|
import diagramXML from './diagram.bpmn';
|
|
17
17
|
|
|
@@ -36,6 +36,7 @@ await layoutProcess(diagramXML, options);
|
|
|
36
36
|
* **Message flows** — message flows between participants are routed as orthogonal edges.
|
|
37
37
|
* **Text annotations and associations** — process-level and collaboration-level annotations are positioned above their associated element.
|
|
38
38
|
* **Groups** — group shapes are emitted around their member elements.
|
|
39
|
+
* **Lanes (swimlanes)** — when a process defines a `laneSet`, elements are reorganized into lane-specific row bands and `BPMNShape` DI entries are emitted for each lane. Works both inside collaborations and for standalone processes (a collaboration+participant wrapper is synthesized automatically). Elements not explicitly assigned to a lane are inferred from their connections.
|
|
39
40
|
* **Original participant gap preserved** — the gap between pool lanes from the input is carried over (capped at 100 px); annotations are accommodated by expanding the pool height.
|
|
40
41
|
* **Sub-process expand support** — collapsed sub-processes retain their inner layout so they can be expanded; use `expandSubProcesses: true` to expand them inline during layout.
|
|
41
42
|
|
package/dist/index.cjs
CHANGED
|
@@ -947,6 +947,7 @@ var incomingHandler = {
|
|
|
947
947
|
const handlers = [ elementHandler, incomingHandler, outgoingHandler, attacherHandler ];
|
|
948
948
|
|
|
949
949
|
const PARTICIPANT_LABEL_WIDTH = 30;
|
|
950
|
+
const LANE_LABEL_WIDTH = 30;
|
|
950
951
|
|
|
951
952
|
class Layouter {
|
|
952
953
|
constructor() {
|
|
@@ -979,19 +980,43 @@ class Layouter {
|
|
|
979
980
|
const firstRootProcess = this.getProcess();
|
|
980
981
|
|
|
981
982
|
if (firstRootProcess) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
this.
|
|
994
|
-
|
|
983
|
+
const hasLanes = firstRootProcess.laneSets?.[0]?.lanes?.length > 0;
|
|
984
|
+
|
|
985
|
+
if (hasLanes) {
|
|
986
|
+
|
|
987
|
+
// Standalone process with lanes — synthesize a collaboration wrapper
|
|
988
|
+
// so layoutCollaboration() handles lane grid reorganization and DI.
|
|
989
|
+
const participant = this.moddle.create('bpmn:Participant', {
|
|
990
|
+
id: 'Participant_' + firstRootProcess.id,
|
|
991
|
+
name: firstRootProcess.name || firstRootProcess.id,
|
|
992
|
+
processRef: firstRootProcess
|
|
993
|
+
});
|
|
994
|
+
const collaboration = this.moddle.create('bpmn:Collaboration', {
|
|
995
|
+
id: 'Collaboration_' + firstRootProcess.id,
|
|
996
|
+
participants: [ participant ]
|
|
997
|
+
});
|
|
998
|
+
participant.$parent = collaboration;
|
|
999
|
+
collaboration.$parent = this.diagram;
|
|
1000
|
+
this.diagram.get('rootElements').push(collaboration);
|
|
1001
|
+
|
|
1002
|
+
this.setExpandedPropertyToModdleElements(moddleObj, options);
|
|
1003
|
+
this.cleanDi();
|
|
1004
|
+
this.layoutCollaboration(collaboration, options);
|
|
1005
|
+
} else {
|
|
1006
|
+
this.setExpandedPropertyToModdleElements(moddleObj, options);
|
|
1007
|
+
this.setExecutedProcesses(firstRootProcess);
|
|
1008
|
+
this.createGridsForProcesses();
|
|
1009
|
+
this.cleanDi();
|
|
1010
|
+
this.createRootDi(firstRootProcess);
|
|
1011
|
+
this.drawProcesses();
|
|
1012
|
+
|
|
1013
|
+
// Draw artifacts and data associations for each laid out process
|
|
1014
|
+
for (const process of this.layoutedProcesses) {
|
|
1015
|
+
const diagram = this.diagram.diagrams.find(d => d.plane.bpmnElement === process)
|
|
1016
|
+
|| this.diagram.diagrams[0];
|
|
1017
|
+
this.generateArtifactsDi(process, diagram);
|
|
1018
|
+
this.generateDataAssociationsDi(process, diagram);
|
|
1019
|
+
}
|
|
995
1020
|
}
|
|
996
1021
|
}
|
|
997
1022
|
}
|
|
@@ -1005,17 +1030,26 @@ class Layouter {
|
|
|
1005
1030
|
// Build grids per participant's process
|
|
1006
1031
|
const participantLayouts = collaboration.participants.map(participant => {
|
|
1007
1032
|
const process = participant.processRef;
|
|
1008
|
-
if (!process) return { participant, process: null, layoutedProcesses: [], grid: null };
|
|
1033
|
+
if (!process) return { participant, process: null, layoutedProcesses: [], grid: null, laneInfo: null };
|
|
1009
1034
|
|
|
1010
1035
|
this.layoutedProcesses = [];
|
|
1011
1036
|
this.setExecutedProcesses(process);
|
|
1012
1037
|
this.createGridsForProcesses();
|
|
1013
1038
|
|
|
1039
|
+
// Detect lanes and reorganize grid so each lane occupies distinct row bands
|
|
1040
|
+
let laneInfo = null;
|
|
1041
|
+
const laneMapping = this.getLaneMapping(process);
|
|
1042
|
+
if (laneMapping && process.grid) {
|
|
1043
|
+
const laneRowRanges = this.reorganizeGridByLanes(process.grid, laneMapping);
|
|
1044
|
+
laneInfo = { lanes: laneMapping.lanes, laneRowRanges };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1014
1047
|
return {
|
|
1015
1048
|
participant,
|
|
1016
1049
|
process,
|
|
1017
1050
|
layoutedProcesses: [ ...this.layoutedProcesses ],
|
|
1018
|
-
grid: process.grid
|
|
1051
|
+
grid: process.grid,
|
|
1052
|
+
laneInfo
|
|
1019
1053
|
};
|
|
1020
1054
|
});
|
|
1021
1055
|
|
|
@@ -1025,17 +1059,18 @@ class Layouter {
|
|
|
1025
1059
|
const participantFloors = new Map();
|
|
1026
1060
|
let currentY = 0;
|
|
1027
1061
|
|
|
1028
|
-
for (const { participant, process, layoutedProcesses, grid } of participantLayouts) {
|
|
1062
|
+
for (const { participant, process, layoutedProcesses, grid, laneInfo } of participantLayouts) {
|
|
1029
1063
|
if (process) participantFloors.set(process, currentY);
|
|
1030
1064
|
|
|
1031
1065
|
let participantWidth, participantHeight;
|
|
1066
|
+
const hasLanes = !!laneInfo;
|
|
1032
1067
|
|
|
1033
1068
|
if (!grid) {
|
|
1034
1069
|
participantWidth = 400;
|
|
1035
1070
|
participantHeight = DEFAULT_CELL_HEIGHT;
|
|
1036
1071
|
} else {
|
|
1037
1072
|
const [ rows, cols ] = grid.getGridDimensions();
|
|
1038
|
-
participantWidth = PARTICIPANT_LABEL_WIDTH + Math.max(cols, 1) * DEFAULT_CELL_WIDTH;
|
|
1073
|
+
participantWidth = PARTICIPANT_LABEL_WIDTH + (hasLanes ? LANE_LABEL_WIDTH : 0) + Math.max(cols, 1) * DEFAULT_CELL_WIDTH;
|
|
1039
1074
|
participantHeight = Math.max(rows, 1) * DEFAULT_CELL_HEIGHT;
|
|
1040
1075
|
}
|
|
1041
1076
|
|
|
@@ -1061,8 +1096,20 @@ class Layouter {
|
|
|
1061
1096
|
|
|
1062
1097
|
if (grid) {
|
|
1063
1098
|
|
|
1099
|
+
// Emit lane shapes BEFORE element shapes — bpmn-js uses DI order to
|
|
1100
|
+
// establish parent-child containment (participant → lanes → elements → edges)
|
|
1101
|
+
if (hasLanes) {
|
|
1102
|
+
this.generateLanesDi(
|
|
1103
|
+
laneInfo.lanes,
|
|
1104
|
+
laneInfo.laneRowRanges,
|
|
1105
|
+
{ x: 0, y: currentY, width: participantWidth, height: participantHeight },
|
|
1106
|
+
annotationPadding,
|
|
1107
|
+
collaborationDi
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1064
1111
|
// Draw flow elements with participant offset (shifted down by annotation padding)
|
|
1065
|
-
const shift = { x: PARTICIPANT_LABEL_WIDTH, y: currentY + annotationPadding };
|
|
1112
|
+
const shift = { x: PARTICIPANT_LABEL_WIDTH + (hasLanes ? LANE_LABEL_WIDTH : 0), y: currentY + annotationPadding };
|
|
1066
1113
|
this.generateDi(grid, shift, collaborationDi);
|
|
1067
1114
|
|
|
1068
1115
|
// Draw expanded sub-processes within this participant
|
|
@@ -1468,8 +1515,10 @@ class Layouter {
|
|
|
1468
1515
|
const participants = collaboration.participants;
|
|
1469
1516
|
if (participants.length < 2) return 0;
|
|
1470
1517
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1518
|
+
// Guard: when input XML for some reason has no DI section diagrams and planeElement are undefined
|
|
1519
|
+
// until cleanDi() rebuilds them.
|
|
1520
|
+
const shapes = (this.diagram.diagrams || [])
|
|
1521
|
+
.flatMap(d => d.plane.planeElement || [])
|
|
1473
1522
|
.filter(el => el.$type === 'bpmndi:BPMNShape' && participants.includes(el.bpmnElement));
|
|
1474
1523
|
|
|
1475
1524
|
if (shapes.length < 2) return 0;
|
|
@@ -1560,6 +1609,154 @@ class Layouter {
|
|
|
1560
1609
|
getProcDi(element) {
|
|
1561
1610
|
return this.diagram.diagrams.find(diagram => diagram.plane.planeElement.includes(element));
|
|
1562
1611
|
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Detect lanes in a process and build element-to-lane mapping.
|
|
1615
|
+
* @param {Object} process - BPMN process moddle element
|
|
1616
|
+
* @returns {{ lanes: Object[], elementToLaneIdx: Map }|null}
|
|
1617
|
+
*/
|
|
1618
|
+
getLaneMapping(process) {
|
|
1619
|
+
const laneSet = (process.laneSets || [])[0];
|
|
1620
|
+
if (!laneSet) return null;
|
|
1621
|
+
|
|
1622
|
+
const lanes = laneSet.lanes || [];
|
|
1623
|
+
if (lanes.length === 0) return null;
|
|
1624
|
+
|
|
1625
|
+
// bpmn-moddle stores flowNodeRef as string IDs (not object references)
|
|
1626
|
+
const idToLaneIdx = new Map();
|
|
1627
|
+
lanes.forEach((lane, idx) => {
|
|
1628
|
+
(lane.flowNodeRef || []).forEach(ref => {
|
|
1629
|
+
const id = typeof ref === 'string' ? ref : ref.id;
|
|
1630
|
+
idToLaneIdx.set(id, idx);
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
return { lanes, idToLaneIdx };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Reorganize grid rows so each lane's elements occupy distinct, non-overlapping
|
|
1639
|
+
* row bands. Elements keep their column positions; only rows are remapped.
|
|
1640
|
+
*
|
|
1641
|
+
* @param {Grid} grid - The process grid (modified in place)
|
|
1642
|
+
* @param {{ lanes: Object[], elementToLaneIdx: Map }} laneMapping
|
|
1643
|
+
* @returns {Array<{ startRow: number, rowCount: number }>} Row range per lane
|
|
1644
|
+
*/
|
|
1645
|
+
reorganizeGridByLanes(grid, laneMapping) {
|
|
1646
|
+
const { lanes, idToLaneIdx } = laneMapping;
|
|
1647
|
+
const positioned = grid.elementsByPosition();
|
|
1648
|
+
|
|
1649
|
+
// Group elements by lane index (lookup by element ID)
|
|
1650
|
+
const byLane = new Map();
|
|
1651
|
+
positioned.forEach(({ element, row, col }) => {
|
|
1652
|
+
let laneIdx = idToLaneIdx.get(element.id);
|
|
1653
|
+
|
|
1654
|
+
// Infer lane for unassigned elements from connected elements
|
|
1655
|
+
if (laneIdx === undefined) {
|
|
1656
|
+
laneIdx = this.inferLane(element, idToLaneIdx) ?? 0;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (!byLane.has(laneIdx)) byLane.set(laneIdx, []);
|
|
1660
|
+
byLane.get(laneIdx).push({ element, origRow: row, col });
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
// Build new grid data with lanes in separate row bands
|
|
1664
|
+
const newGridData = [];
|
|
1665
|
+
const laneRowRanges = [];
|
|
1666
|
+
|
|
1667
|
+
for (let i = 0; i < lanes.length; i++) {
|
|
1668
|
+
const group = byLane.get(i) || [];
|
|
1669
|
+
const startRow = newGridData.length;
|
|
1670
|
+
|
|
1671
|
+
if (group.length === 0) {
|
|
1672
|
+
|
|
1673
|
+
// Empty lane still gets one row for visual presence
|
|
1674
|
+
newGridData.push([]);
|
|
1675
|
+
laneRowRanges.push({ startRow, rowCount: 1 });
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Map original rows to consecutive new rows within this lane band
|
|
1680
|
+
const origRows = [ ...new Set(group.map(g => g.origRow)) ].sort((a, b) => a - b);
|
|
1681
|
+
const rowMap = new Map();
|
|
1682
|
+
origRows.forEach((r, idx) => rowMap.set(r, startRow + idx));
|
|
1683
|
+
|
|
1684
|
+
// Initialize empty rows
|
|
1685
|
+
for (let j = 0; j < origRows.length; j++) newGridData.push([]);
|
|
1686
|
+
|
|
1687
|
+
// Place elements in their new positions
|
|
1688
|
+
group.forEach(({ element, origRow, col }) => {
|
|
1689
|
+
const newRow = rowMap.get(origRow);
|
|
1690
|
+
while (newGridData[newRow].length <= col) newGridData[newRow].push(undefined);
|
|
1691
|
+
newGridData[newRow][col] = element;
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
laneRowRanges.push({ startRow, rowCount: origRows.length });
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Replace grid's internal data
|
|
1698
|
+
grid.grid = newGridData;
|
|
1699
|
+
return laneRowRanges;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* Infer lane index for an element not explicitly assigned to any lane,
|
|
1704
|
+
* by checking its connected elements.
|
|
1705
|
+
*/
|
|
1706
|
+
inferLane(element, idToLaneIdx) {
|
|
1707
|
+
const outgoing = (element.outgoing || []).map(e => e.targetRef).filter(Boolean);
|
|
1708
|
+
const incoming = (element.incoming || []).map(e => e.sourceRef).filter(Boolean);
|
|
1709
|
+
|
|
1710
|
+
for (const connected of [ ...outgoing, ...incoming ]) {
|
|
1711
|
+
const idx = idToLaneIdx.get(connected.id);
|
|
1712
|
+
if (idx !== undefined) return idx;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return undefined;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Generate BPMNShape DI entries for lanes within a participant.
|
|
1720
|
+
*
|
|
1721
|
+
* @param {Object[]} lanes - Lane moddle elements
|
|
1722
|
+
* @param {Array<{ startRow: number, rowCount: number }>} laneRowRanges
|
|
1723
|
+
* @param {{ x: number, y: number, width: number, height: number }} participantBounds
|
|
1724
|
+
* @param {number} annotationPadding - Extra top padding for annotations
|
|
1725
|
+
* @param {Object} collaborationDi - Target diagram for DI shapes
|
|
1726
|
+
*/
|
|
1727
|
+
generateLanesDi(lanes, laneRowRanges, participantBounds, annotationPadding, collaborationDi) {
|
|
1728
|
+
const planeElement = collaborationDi.plane.get('planeElement');
|
|
1729
|
+
|
|
1730
|
+
const laneX = participantBounds.x + PARTICIPANT_LABEL_WIDTH;
|
|
1731
|
+
const laneWidth = participantBounds.width - PARTICIPANT_LABEL_WIDTH;
|
|
1732
|
+
|
|
1733
|
+
let cumulativeY = participantBounds.y;
|
|
1734
|
+
|
|
1735
|
+
lanes.forEach((lane, idx) => {
|
|
1736
|
+
const range = laneRowRanges[idx];
|
|
1737
|
+
let laneHeight = range.rowCount * DEFAULT_CELL_HEIGHT;
|
|
1738
|
+
|
|
1739
|
+
// First lane absorbs annotation padding
|
|
1740
|
+
if (idx === 0) {
|
|
1741
|
+
laneHeight += annotationPadding;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const shape = this.diFactory.createDiShape(lane, {
|
|
1745
|
+
x: laneX,
|
|
1746
|
+
y: cumulativeY,
|
|
1747
|
+
width: laneWidth,
|
|
1748
|
+
height: laneHeight
|
|
1749
|
+
}, {
|
|
1750
|
+
id: lane.id + '_di',
|
|
1751
|
+
isHorizontal: true
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
lane.di = shape;
|
|
1755
|
+
planeElement.push(shape);
|
|
1756
|
+
|
|
1757
|
+
cumulativeY += laneHeight;
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1563
1760
|
}
|
|
1564
1761
|
|
|
1565
1762
|
function containsElement(process, element) {
|