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:
parent
718836f53e
commit
22d2b97bbe
|
@ -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);
|
||||
|
@ -316,3 +373,41 @@ function deconflictPosition(source, element, position, escapeDelta) {
|
|||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue