Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better boundary event handling #38

Merged
merged 9 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 28 additions & 2 deletions lib/Grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,34 @@ export class Grid {
this.grid = [];
}

add(element) {
this._addStart(element);
add(element, position) {
if (!position) {
this._addStart(element);
return;
}

const [ row, col ] = position;
if (!row && !col) {
this._addStart(element);
}

if (!this.grid[row]) {
this.grid[row] = [];
}

if (this.grid[row][col]) {
throw new Error('Grid is occupied please ensure the place you insert at is not occupied');
}

this.grid[row][col] = element;
}

createRow(afterIndex) {
if (!afterIndex) {
this.grid.push([]);
}

this.grid.splice(afterIndex + 1, 0, []);
}

_addStart(element) {
Expand Down
119 changes: 21 additions & 98 deletions lib/Layouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import BPMNModdle from 'bpmn-moddle';
import { isBoundaryEvent, isConnection } from './utils/elementUtils';
import { Grid } from './Grid';
import { DiFactory } from './di/DiFactory';
import { connectElements, getBounds } from './utils/layoutUtil';
import { is } from './di/DiUtil';
import { handlers } from './handler';
import { isFunction } from 'min-dash';

export class Layouter {
constructor() {
this.moddle = new BPMNModdle();
this.diFactory = new DiFactory(this.moddle);
this._handlers = handlers;
}

handle(operation, options) {
return this._handlers
.filter(handler => isFunction(handler[operation]))
.map(handler => handler[operation](options));

}

async layoutProcess(xml) {
Expand Down Expand Up @@ -36,7 +45,6 @@ export class Layouter {
createGridLayout(root) {
const grid = new Grid();

// const process = this.getProcess();
const flowElements = root.flowElements;

const startingElements = flowElements.filter(el => {
Expand Down Expand Up @@ -67,54 +75,11 @@ export class Layouter {
this.handlePlane(currentElement);
}

// Handle outgoing paths
const outgoing = (currentElement.outgoing || [])
.map(out => out.targetRef)
.filter(el => el);

let previousElement = null;
outgoing.forEach((nextElement, index, arr) => {
if (visited.has(nextElement)) {
return;
}

if (!previousElement) {
grid.addAfter(currentElement, nextElement);
}
else {
grid.addBelow(arr[index - 1], nextElement);
}

// Is self-looping
if (nextElement !== currentElement) {
previousElement = nextElement;
}
});

const attachedOutgoing = (currentElement.attachers || [])
.map(att => att.outgoing)
.flat()
.map(out => out.targetRef);

// handle boundary events
attachedOutgoing.forEach((nextElement, index, arr) => {
if (visited.has(nextElement)) {
return;
}

const below = arr[index - 1] || currentElement;
grid.addBelow(below, nextElement);
stack.push(nextElement);
visited.add(nextElement);
});
const nextElements = this.handle('addToGrid', { element: currentElement, grid, visited });

// add to stack in reverse order: first element should be first of the stack
outgoing.reverse().forEach(el => {
if (visited.has(el)) {
return;
}
visited.add(el);
nextElements.flat().forEach(el => {
stack.push(el);
visited.add(el);
});
}

Expand Down Expand Up @@ -143,62 +108,20 @@ export class Layouter {

// Step 1: Create DI for all elements
layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
const bounds = getBounds(element, row, col);

const shapeDi = diFactory.createDiShape(element, bounds, {
id: element.id + '_di'
});
element.di = shapeDi;
element.gridPosition = { row, col };

planeElement.push(shapeDi);

// handle attachers
(element.attachers || []).forEach(att => {
att.gridPosition = { row, col };
const attacherBounds = getBounds(att, row, col, element);
const dis = this
.handle('createElementDi', { element, row, col, layoutGrid, diFactory })
.flat();

const attacherDi = diFactory.createDiShape(att, attacherBounds, {
id: att.id + '_di'
});
att.di = attacherDi;
att.gridPosition = { row, col };

planeElement.push(attacherDi);
});
planeElement.push(...dis);
});

// Step 2: Create DI for all connections
layoutGrid.elementsByPosition().forEach(({ element, row, col }) => {
const outgoing = element.outgoing || [];

outgoing.forEach(out => {
const target = out.targetRef;
const waypoints = connectElements(element, target, layoutGrid);

const connectionDi = diFactory.createDiEdge(out, waypoints, {
id: out.id + '_di'
});

planeElement.push(connectionDi);
});

// handle attachers
(element.attachers || []).forEach(att => {
const outgoing = att.outgoing || [];

outgoing.forEach(out => {
const target = out.targetRef;
const waypoints = connectElements(att, target, layoutGrid);

const connectionDi = diFactory.createDiEdge(out, waypoints, {
id: out.id + '_di'
});

planeElement.push(connectionDi);
});
});
const dis = this
.handle('createConnectionDi', { element, row, col, layoutGrid, diFactory })
.flat();

planeElement.push(...dis);
});
}

Expand Down
128 changes: 128 additions & 0 deletions lib/handler/attachersHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
DEFAULT_CELL_HEIGHT,
DEFAULT_CELL_WIDTH,
connectElements,
getBounds,
getDockingPoint,
getMid
} from '../utils/layoutUtil';

export default {
'addToGrid': ({ element, grid, visited }) => {
const nextElements = [];

const attachedOutgoing = (element.attachers || [])
.map(att => att.outgoing.reverse())
.flat()
.map(out => out.targetRef);

// handle boundary events
attachedOutgoing.forEach((nextElement, index, arr) => {
if (visited.has(nextElement)) {
return;
}

// Add below and to the right of the element
insertIntoGrid(nextElement, element, grid);
nextElements.push(nextElement);
});

return nextElements;
},

'createElementDi': ({ element, row, col, diFactory }) => {
const hostBounds = getBounds(element, row, col);

const DIs = [];
(element.attachers || []).forEach((att, i, arr) => {
att.gridPosition = { row, col };
const bounds = getBounds(att, row, col, element);

// distribute along lower edge
bounds.x = hostBounds.x + (i + 1) * (hostBounds.width / (arr.length + 1)) - bounds.width / 2;

const attacherDi = diFactory.createDiShape(att, bounds, {
id: att.id + '_di'
});
att.di = attacherDi;
att.gridPosition = { row, col };

DIs.push(attacherDi);
});

return DIs;
},

'createConnectionDi': ({ element, row, col, layoutGrid, diFactory }) => {
const attachers = element.attachers || [];

return attachers.flatMap(att => {
const outgoing = att.outgoing || [];

return outgoing.map(out => {
const target = out.targetRef;
const waypoints = connectElements(att, target, layoutGrid);

// Correct waypoints if they don't automatically attach to the bottom
ensureExitBottom(att, waypoints, [ row, col ]);

const connectionDi = diFactory.createDiEdge(out, waypoints, {
id: out.id + '_di'
});

return connectionDi;
});
});
}
};


function insertIntoGrid(newElement, host, grid) {
const [ row, col ] = grid.find(host);

// Grid is occupied
if (grid.get(row + 1, col) || grid.get(row + 1, col + 1)) {
grid.createRow(row);
}

// Host has element directly after, add space
if (grid.get(row, col + 1)) {
grid.addAfter(host, null);
}

grid.add(newElement, [ row + 1, col + 1 ]);
}

function ensureExitBottom(source, waypoints, [ row, col ]) {

const sourceDi = source.di;
const sourceBounds = sourceDi.get('bounds');
const sourceMid = getMid(sourceBounds);

const dockingPoint = getDockingPoint(sourceMid, sourceBounds, 'b');
if (waypoints[0].x === dockingPoint.x && waypoints[0].y === dockingPoint.y) {
return;
}

if (waypoints.length === 2) {
const newStart = [
dockingPoint,
{ x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
{ x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 1) * DEFAULT_CELL_HEIGHT },
{ x: (col + 1) * DEFAULT_CELL_WIDTH, y: (row + 0.5) * DEFAULT_CELL_HEIGHT },
];

waypoints.splice(0, 1, ...newStart);
return;
}

// add waypoints to exit bottom and connect to existing path
const newStart = [
dockingPoint,
{ x: dockingPoint.x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
{ x: waypoints[1].x, y: (row + 1) * DEFAULT_CELL_HEIGHT },
];

waypoints.splice(0, 1, ...newStart);
return;
}
23 changes: 23 additions & 0 deletions lib/handler/elementHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { is } from '../di/DiUtil';
import { getBounds } from '../utils/layoutUtil';

export default {
'createElementDi': ({ element, row, col, diFactory }) => {

const bounds = getBounds(element, row, col);

const options = {
id: element.id + '_di'
};

if (is(element, 'bpmn:ExclusiveGateway')) {
options.isMarkerVisible = true;
}

const shapeDi = diFactory.createDiShape(element, bounds, options);
element.di = shapeDi;
element.gridPosition = { row, col };

return shapeDi;
}
};
5 changes: 5 additions & 0 deletions lib/handler/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { default as attacherHandler } from './attachersHandler';
import { default as elementHandler } from './elementHandler';
import { default as outgoingHandler } from './outgoingHandler';

export const handlers = [ elementHandler, outgoingHandler, attacherHandler ];