feat(auto-place): handle boundary events

Add basic auto-placement of boundary events:

* handle top aligned events
* handle bottom aligned events
* take boundary events into account when placing
  host elements and vice versa

Related to #563
This commit is contained in:
Nico Rehwaldt 2017-12-15 14:34:05 +01:00
parent 718836f53e
commit 22d2b97bbe
3 changed files with 276 additions and 68 deletions

View File

@ -7,8 +7,7 @@ var getMid = require('diagram-js/lib/layout/LayoutUtil').getMid,
getOrientation = require('diagram-js/lib/layout/LayoutUtil').getOrientation;
var find = require('lodash/collection/find'),
reduce = require('lodash/collection/reduce'),
filter = require('lodash/collection/filter');
reduce = require('lodash/collection/reduce');
var DEFAULT_HORIZONTAL_DISTANCE = 50;
@ -29,15 +28,34 @@ function getFlowNodePosition(source, element) {
var horizontalDistance = getFlowNodeDistance(source, element);
var orientation = 'left',
rowSize = 80,
margin = 30;
if (is(source, 'bpmn:BoundaryEvent')) {
orientation = getOrientation(source, source.host, -25);
if (orientation === 'top') {
margin *= -1;
}
}
var verticalDistances = {
left: 0,
right: 0,
top: -1 * rowSize,
bottom: rowSize
};
var position = {
x: sourceTrbl.right + horizontalDistance + element.width / 2,
y: sourceMid.y
y: sourceMid.y + verticalDistances[orientation]
};
var escapeDirection = {
y: {
margin: 30,
rowSize: 80
margin: margin,
rowSize: rowSize
}
};
@ -61,9 +79,38 @@ function getFlowNodeDistance(source, element) {
var sourceTrbl = asTRBL(source);
// is connection a reference to consider?
var isReference = function(c) {
function isReference(c) {
return is(c, 'bpmn:SequenceFlow');
};
}
function toTargetNode(weight) {
return function(shape) {
return {
shape: shape,
weight: weight,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return shapeTrbl.left - sourceTrbl.right;
}
};
};
}
function toSourceNode(weight) {
return function(shape) {
return {
shape: shape,
weight: weight,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return sourceTrbl.left - shapeTrbl.right;
}
};
};
}
// we create a list of nodes to take into consideration
// for calculating the optimal flow node distance
@ -72,28 +119,8 @@ function getFlowNodeDistance(source, element) {
// * only take into account individual nodes once
//
var nodes = reduce([].concat(
filter(source.outgoing, isReference).map(function(c) {
return {
shape: c.target,
weight: 5,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return shapeTrbl.left - sourceTrbl.right;
}
};
}),
filter(source.incoming, isReference).map(function(c) {
return {
shape: c.source,
weight: 1,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return sourceTrbl.left - shapeTrbl.right;
}
};
})
getTargets(source, isReference).map(toTargetNode(5)),
getSources(source, isReference).map(toSourceNode(1))
), function(nodes, node) {
// filter out shapes connected twice via source or target
nodes[node.shape.id + '__weight_' + node.weight] = node;
@ -219,8 +246,42 @@ function getDefaultPosition(source, element) {
module.exports.getDefaultPosition = getDefaultPosition;
/**
* Returns all connected elements around the given source.
*
* This includes:
*
* - connected elements
* - host connected elements
* - attachers connected elements
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Array<djs.model.Shape>}
*/
function getAutoPlaceClosure(source, element) {
var allConnected = getConnected(source);
if (source.host) {
allConnected = allConnected.concat(getConnected(source.host));
}
if (source.attachers) {
allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
return shapes.concat(getConnected(attacher));
}, []));
}
return allConnected;
}
/**
* Return target at given position, if defined.
*
* This takes connected elements from host and attachers
* into account, too.
*/
function getConnectedAtPosition(source, position, element) {
@ -231,17 +292,14 @@ function getConnectedAtPosition(source, position, element) {
height: element.height
};
var targets = source.outgoing.map(function(c) {
return c.target;
});
var closure = getAutoPlaceClosure(source, element);
var sources = source.incoming.map(function(c) {
return c.source;
});
return find(closure, function(target) {
var allConnected = [].concat(targets, sources);
if (target === element) {
return false;
}
return find(allConnected, function(target) {
var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
return orientation === 'intersect';
@ -306,7 +364,6 @@ function deconflictPosition(source, element, position, escapeDelta) {
var existingTarget;
// deconflict position until free slot is found
while ((existingTarget = getConnectedAtPosition(source, position, element))) {
position = nextPosition(existingTarget);
@ -315,4 +372,42 @@ function deconflictPosition(source, element, position, escapeDelta) {
return position;
}
module.exports.deconflictPosition = deconflictPosition;
module.exports.deconflictPosition = deconflictPosition;
///////// helpers /////////////////////
function noneFilter() {
return true;
}
function getConnected(element, connectionFilter) {
return [].concat(
getTargets(element, connectionFilter),
getSources(element, connectionFilter)
);
}
function getSources(shape, connectionFilter) {
if (!connectionFilter) {
connectionFilter = noneFilter;
}
return shape.incoming.filter(connectionFilter).map(function(c) {
return c.source;
});
}
function getTargets(shape, connectionFilter) {
if (!connectionFilter) {
connectionFilter = noneFilter;
}
return shape.outgoing.filter(connectionFilter).map(function(c) {
return c.target;
});
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="sid-38422fae-e03e-43a3-bef4-bd33b32041b2" targetNamespace="http://bpmn.io/bpmn" exporter="http://bpmn.io" exporterVersion="0.10.1">
<process id="Process_1" isExecutable="false">
<task id="TASK_1" name="TASK_1">
<outgoing>SequenceFlow_0o6gp3o</outgoing>
</task>
<boundaryEvent id="BOUNDARY_BOTTOM" name="BOUNDARY_BOTTOM" attachedToRef="TASK_1" />
<task id="TASK_2" name="TASK_2">
<incoming>SequenceFlow_0o6gp3o</incoming>
</task>
<sequenceFlow id="SequenceFlow_0o6gp3o" sourceRef="TASK_1" targetRef="TASK_2" />
<task id="TASK_3" name="TASK_3" />
<boundaryEvent id="BOUNDARY_TOP" name="BOUNDARY_TOP" attachedToRef="TASK_1" />
<subProcess id="SUBPROCESS" name="SUBPROCESS" />
<boundaryEvent id="BOUNDARY_SUBPROCESS_BOTTOM" name="BOUNDARY_SUBPROCESS_BOTTOM" attachedToRef="SUBPROCESS" />
<boundaryEvent id="BOUNDARY_SUBPROCESS_TOP" name="BOUNDARY_SUBPROCESS_TOP" attachedToRef="SUBPROCESS" />
</process>
<bpmndi:BPMNDiagram id="BpmnDiagram_1">
<bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="TASK_1_di" bpmnElement="TASK_1">
<omgdc:Bounds x="121" y="93" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BOUNDARY_BOTTOM_di" bpmnElement="BOUNDARY_BOTTOM">
<omgdc:Bounds x="155" y="155" width="36" height="36" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="72" y="188" width="87" height="24" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TASK_2_di" bpmnElement="TASK_2">
<omgdc:Bounds x="305" y="93" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0o6gp3o_di" bpmnElement="TASK_1">
<omgdi:waypoint xsi:type="omgdc:Point" x="221" y="133" />
<omgdi:waypoint xsi:type="omgdc:Point" x="305" y="133" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="263" y="112" width="0" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_3_di" bpmnElement="TASK_3">
<omgdc:Bounds x="305" y="203" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BOUNDARY_TOP_di" bpmnElement="BOUNDARY_TOP">
<omgdc:Bounds x="156" y="75" width="36" height="36" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="78" y="52" width="86" height="24" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SUBPROCESS_di" bpmnElement="SUBPROCESS" isExpanded="true">
<omgdc:Bounds x="142" y="314" width="258" height="141" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BOUNDARY_SUBPROCESS_BOTTOM_di" bpmnElement="BOUNDARY_SUBPROCESS_BOTTOM">
<omgdc:Bounds x="192" y="437" width="36" height="36" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="110" y="473" width="86" height="36" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BOUNDARY_SUBPROCESS_TOP_di" bpmnElement="BOUNDARY_SUBPROCESS_TOP">
<omgdc:Bounds x="189" y="296" width="36" height="36" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="98" y="259" width="86" height="36" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

View File

@ -27,34 +27,6 @@ describe('features/auto-place', function() {
}));
function autoPlace(cfg) {
var element = cfg.element,
behind = cfg.behind,
expectedBounds = cfg.expectedBounds;
return inject(function(autoPlace, elementRegistry, elementFactory) {
var sourceEl = elementRegistry.get(behind);
// assume
expect(sourceEl).to.exist;
if (typeof element === 'string') {
element = { type: element };
}
var shape = elementFactory.createShape(element);
// when
var placedShape = autoPlace.append(sourceEl, shape);
// then
expect(placedShape).to.have.bounds(expectedBounds);
});
}
describe('should place bpmn:FlowNode', function() {
it('at default distance after START_EVENT_1', autoPlace({
@ -209,4 +181,80 @@ describe('features/auto-place', function() {
});
});
describe('boundary event connection handling', function() {
var diagramXML = require('./AutoPlace.boundary-events.bpmn');
before(bootstrapModeler(diagramXML, {
modules: [
coreModule,
modelingModule,
autoPlaceModule,
selectionModule
]
}));
it('should place bottom right of BOUNDARY_BOTTOM', autoPlace({
element: 'bpmn:Task',
behind: 'BOUNDARY_BOTTOM',
expectedBounds: { x: 241, y: 213, width: 100, height: 80 }
}));
it('should place bottom right of BOUNDARY_SUBPROCESS_BOTTOM', autoPlace({
element: 'bpmn:Task',
behind: 'BOUNDARY_SUBPROCESS_BOTTOM',
expectedBounds: { x: 278, y: 495, width: 100, height: 80 }
}));
it('should place top right of BOUNDARY_TOP', autoPlace({
element: 'bpmn:Task',
behind: 'BOUNDARY_TOP',
expectedBounds: { x: 242, y: -27, width: 100, height: 80 }
}));
it('should place top right of BOUNDARY_SUBPROCESS_TOP', autoPlace({
element: 'bpmn:Task',
behind: 'BOUNDARY_SUBPROCESS_TOP',
expectedBounds: { x: 275, y: 194, width: 100, height: 80 }
}));
});
});
////////// helpers /////////////////////////////////////////
function autoPlace(cfg) {
var element = cfg.element,
behind = cfg.behind,
expectedBounds = cfg.expectedBounds;
return inject(function(autoPlace, elementRegistry, elementFactory) {
var sourceEl = elementRegistry.get(behind);
// assume
expect(sourceEl).to.exist;
if (typeof element === 'string') {
element = { type: element };
}
var shape = elementFactory.createShape(element);
// when
var placedShape = autoPlace.append(sourceEl, shape);
// then
expect(placedShape).to.have.bounds(expectedBounds);
});
}