feat(draw): render sequence flows behind tasks

* ordering provider ensures sequence flows appear
  behind tasks
* importer makes sure we render elements in the order
  lanes > sequence flows > other flow elements
* consistent minimal opacity ensures elements in
  front of tasks don't look _connected_

Closes #727
This commit is contained in:
Nico Rehwaldt 2017-11-13 11:11:07 +01:00
parent 495ab7ee9d
commit 1e6186e3ce
8 changed files with 325 additions and 58 deletions

View File

@ -50,6 +50,9 @@ var LABEL_STYLE = {
fontSize: 12
};
var DEFAULT_FILL_OPACITY = .95,
HIGH_FILL_OPACITY = .35;
function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
@ -487,6 +490,8 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
var handlers = this.handlers = {
'bpmn:Event': function(parentGfx, element, attrs) {
attrs.fillOpacity = attrs.fillOpacity || DEFAULT_FILL_OPACITY;
return drawCircle(parentGfx, element.width, element.height, attrs);
},
'bpmn:StartEvent': function(parentGfx, element) {
@ -801,6 +806,10 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
'bpmn:IntermediateThrowEvent': as('bpmn:IntermediateEvent'),
'bpmn:Activity': function(parentGfx, element, attrs) {
attrs = attrs || {};
attrs.fillOpacity = attrs.fillOpacity || DEFAULT_FILL_OPACITY;
return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, attrs);
},
@ -1035,7 +1044,6 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
},
'bpmn:SubProcess': function(parentGfx, element, attrs) {
attrs = assign({
fillOpacity: 0.95,
fill: getFillColor(element),
stroke: getStrokeColor(element)
}, attrs);
@ -1084,7 +1092,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
'bpmn:Participant': function(parentGfx, element) {
var attrs = {
fillOpacity: 0.95,
fillOpacity: DEFAULT_FILL_OPACITY,
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
@ -1124,6 +1132,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
'bpmn:Lane': function(parentGfx, element, attrs) {
var rect = drawRect(parentGfx, element.width, element.height, 0, assign({
fill: getFillColor(element),
fillOpacity: HIGH_FILL_OPACITY,
stroke: getStrokeColor(element)
}, attrs));
@ -1137,12 +1146,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
return rect;
},
'bpmn:InclusiveGateway': function(parentGfx, element) {
var attrs = {
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
var diamond = drawDiamond(parentGfx, element.width, element.height, attrs);
var diamond = renderer('bpmn:Gateway')(parentGfx, element);
/* circle path */
drawCircle(parentGfx, element.width, element.height, element.height * 0.24, {
@ -1154,12 +1158,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
return diamond;
},
'bpmn:ExclusiveGateway': function(parentGfx, element) {
var attrs = {
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
var diamond = drawDiamond(parentGfx, element.width, element.height, attrs);
var diamond = renderer('bpmn:Gateway')(parentGfx, element);
var pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', {
xScaleFactor: 0.4,
@ -1183,12 +1182,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
return diamond;
},
'bpmn:ComplexGateway': function(parentGfx, element) {
var attrs = {
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
var diamond = drawDiamond(parentGfx, element.width, element.height, attrs);
var diamond = renderer('bpmn:Gateway')(parentGfx, element);
var pathData = pathMap.getScaledPath('GATEWAY_COMPLEX', {
xScaleFactor: 0.5,
@ -1210,12 +1204,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
return diamond;
},
'bpmn:ParallelGateway': function(parentGfx, element) {
var attrs = {
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
var diamond = drawDiamond(parentGfx, element.width, element.height, attrs);
var diamond = renderer('bpmn:Gateway')(parentGfx, element);
var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', {
xScaleFactor: 0.6,
@ -1240,12 +1229,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
var semantic = getSemantic(element);
var attrs = {
fill: getFillColor(element),
stroke: getStrokeColor(element)
};
var diamond = drawDiamond(parentGfx, element.width, element.height, attrs);
var diamond = renderer('bpmn:Gateway')(parentGfx, element);
/* outer circle path */ drawCircle(parentGfx, element.width, element.height, element.height * 0.20, {
strokeWidth: 1,
@ -1314,7 +1298,13 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
return diamond;
},
'bpmn:Gateway': function(parentGfx, element) {
return drawDiamond(parentGfx, element.width, element.height);
var attrs = {
fill: getFillColor(element),
fillOpacity: DEFAULT_FILL_OPACITY,
stroke: getStrokeColor(element)
};
return drawDiamond(parentGfx, element.width, element.height, attrs);
},
'bpmn:SequenceFlow': function(parentGfx, element) {
var pathData = createPathFromConnection(element);
@ -1452,6 +1442,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
var elementObject = drawPath(parentGfx, pathData, {
fill: getFillColor(element),
fillOpacity: DEFAULT_FILL_OPACITY,
stroke: getStrokeColor(element)
});
@ -1503,6 +1494,7 @@ function BpmnRenderer(eventBus, styles, pathMap, canvas, priority) {
var elementStore = drawPath(parentGfx, DATA_STORE_PATH, {
strokeWidth: 2,
fill: getFillColor(element),
fillOpacity: DEFAULT_FILL_OPACITY,
stroke: getStrokeColor(element)
});

View File

@ -26,7 +26,7 @@ function BpmnOrderingProvider(eventBus, translate) {
{
type: 'bpmn:SequenceFlow',
order: {
level: 5,
level: 3,
containers: [
'bpmn:Participant',
'bpmn:FlowElementsContainer'
@ -34,8 +34,22 @@ function BpmnOrderingProvider(eventBus, translate) {
}
},
// handle DataAssociation(s) like message flows and render them always on top
{ type: 'bpmn:DataAssociation', order: { level: 9, containers: [ 'bpmn:Collaboration', 'bpmn:Process' ] } },
{ type: 'bpmn:MessageFlow', order: { level: 9, containers: [ 'bpmn:Collaboration' ] } },
{
type: 'bpmn:DataAssociation',
order: {
level: 9,
containers: [
'bpmn:Collaboration',
'bpmn:Process'
]
}
},
{
type: 'bpmn:MessageFlow', order: {
level: 9,
containers: [ 'bpmn:Collaboration' ]
}
},
{
type: 'bpmn:Association',
order: {
@ -48,6 +62,7 @@ function BpmnOrderingProvider(eventBus, translate) {
}
},
{ type: 'bpmn:BoundaryEvent', order: { level: 8 } },
{ type: 'bpmn:FlowElement', order: { level: 5 } },
{ type: 'bpmn:Participant', order: { level: -2 } },
{ type: 'bpmn:Lane', order: { level: -1 } }
];

View File

@ -73,6 +73,8 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
translate = this._translate,
hidden;
var parentIndex;
// ROOT ELEMENT
// handle the special case that we deal with a
// invisible root element (process or collaboration)
@ -105,7 +107,12 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
this._attachBoundary(semantic, element);
}
this._canvas.addShape(element, parentElement);
// insert lanes behind other flow nodes (cf. #727)
if (is(semantic, 'bpmn:Lane')) {
parentIndex = 0;
}
this._canvas.addShape(element, parentElement, parentIndex);
}
// CONNECTION
@ -132,7 +139,12 @@ BpmnImporter.prototype.add = function(semantic, parentElement) {
parentElement = null;
}
this._canvas.addConnection(element, parentElement);
// insert sequence flows behind other flow nodes (cf. #727)
if (is(semantic, 'bpmn:SequenceFlow')) {
parentIndex = 0;
}
this._canvas.addConnection(element, parentElement, parentIndex);
} else {
throw new Error(translate('unknown di {di} for element {semantic}', {
di: elementToString(di),

View File

@ -229,7 +229,15 @@ function BpmnTreeWalker(handler, translate) {
}
function handleDeferred(deferred) {
forEach(deferred, function(d) { d(); });
var fn;
// drain deferred until empty
while (deferred.length) {
fn = deferred.shift();
fn();
}
}
function handleProcess(process, context) {
@ -348,13 +356,17 @@ function BpmnTreeWalker(handler, translate) {
}
function handleLane(lane, context) {
var newContext = visitIfDi(lane, context);
if (lane.childLaneSet) {
handleLaneSet(lane.childLaneSet, newContext || context);
}
deferred.push(function() {
wireFlowNodeRefs(lane);
var newContext = visitIfDi(lane, context);
if (lane.childLaneSet) {
handleLaneSet(lane.childLaneSet, newContext || context);
}
wireFlowNodeRefs(lane);
});
}
function handleLaneSet(laneSet, context) {
@ -366,11 +378,11 @@ function BpmnTreeWalker(handler, translate) {
}
function handleFlowElementsContainer(container, context) {
handleFlowElements(container.flowElements, context);
if (container.laneSets) {
handleLaneSets(container.laneSets, context);
}
handleFlowElements(container.flowElements, context);
}
function handleFlowElements(flowElements, context) {

View File

@ -6,6 +6,7 @@ var Helper = require('./Helper');
var move = Helper.move,
attach = Helper.attach,
connect = Helper.connect,
expectZOrder = Helper.expectZOrder;
var modelingModule = require('../../../../lib/features/modeling'),
@ -228,4 +229,23 @@ describe('features/modeling - ordering', function() {
});
describe('connections', function() {
var diagramXML = require('./ordering.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should render sequence flows behind tasks', inject(function() {
// when
var connection = connect('BoundaryEvent', 'Task');
// then
expectZOrder(connection, 'Task', 'BoundaryEvent', connection.label);
}));
});
});

View File

@ -85,6 +85,33 @@ function add(attrs, position, target, isAttach) {
module.exports.add = add;
function connect(source, target) {
return TestHelper.getBpmnJS().invoke(function(canvas, elementRegistry, modeling) {
function getElement(id) {
var element = elementRegistry.get(id);
expect(element).to.exist;
return element;
}
if (typeof target === 'string') {
target = getElement(target);
}
if (typeof source === 'string') {
source = getElement(source);
}
return modeling.connect(source, target);
});
}
module.exports.connect = connect;
function attach(attrs, position, target) {
return add(attrs, position, target, true);
}
@ -105,7 +132,7 @@ function getAncestors(element) {
}
function compareZOrder(aId, bId) {
function compareZOrder(a, b) {
var elementA,
elementB;
@ -120,8 +147,16 @@ function compareZOrder(aId, bId) {
return element;
}
elementA = getElement(aId);
elementB = getElement(bId);
if (typeof a === 'string') {
a = getElement(a);
}
if (typeof b === 'string') {
b = getElement(b);
}
elementA = a;
elementB = b;
});

View File

@ -1,8 +1,5 @@
'use strict';
require('../../TestHelper');
var TestContainer = require('mocha-test-container-support');
var Diagram = require('diagram-js/lib/Diagram'),
@ -10,8 +7,14 @@ var Diagram = require('diagram-js/lib/Diagram'),
importBpmnDiagram = require('../../../lib/import/Importer').importBpmnDiagram,
Viewer = require('../../../lib/Viewer');
var domMatches = require('min-dom/lib/matches');
var getChildrenGfx = require('diagram-js/lib/util/GraphicsUtil').getChildren;
var find = require('lodash/collection/find');
var is = require('../../../lib/util/ModelUtil').is;
describe('import - Importer', function() {
@ -164,12 +167,12 @@ describe('import - Importer', function() {
expect(events).to.eql([
{ type: 'add', semantic: '_Collaboration_2', di: 'BPMNPlane_1', diagramElement: '_Collaboration_2' },
{ type: 'add', semantic: 'Participant_2', di: '_BPMNShape_Participant_2', diagramElement: 'Participant_2' },
{ type: 'add', semantic: 'Lane_1', di: '_BPMNShape_Lane_2', diagramElement: 'Lane_1' },
{ type: 'add', semantic: 'Lane_2', di: '_BPMNShape_Lane_3', diagramElement: 'Lane_2' },
{ type: 'add', semantic: 'Lane_3', di: '_BPMNShape_Lane_4', diagramElement: 'Lane_3' },
{ type: 'add', semantic: 'Task_1', di: '_BPMNShape_Task_3', diagramElement: 'Task_1' },
{ type: 'add', semantic: 'Participant_1', di: '_BPMNShape_Participant_3', diagramElement: 'Participant_1' },
{ type: 'add', semantic: 'StartEvent_1', di: '_BPMNShape_StartEvent_3', diagramElement: 'StartEvent_1' }
{ type: 'add', semantic: 'StartEvent_1', di: '_BPMNShape_StartEvent_3', diagramElement: 'StartEvent_1' },
{ type: 'add', semantic: 'Lane_1', di: '_BPMNShape_Lane_2', diagramElement: 'Lane_1' },
{ type: 'add', semantic: 'Lane_2', di: '_BPMNShape_Lane_3', diagramElement: 'Lane_2' },
{ type: 'add', semantic: 'Lane_3', di: '_BPMNShape_Lane_4', diagramElement: 'Lane_3' }
]);
done(err);
@ -184,7 +187,7 @@ describe('import - Importer', function() {
var xml = require('../../fixtures/bpmn/import/position/position-testcase.bpmn');
it('should round shape\'s x and y coordinates', function(done) {
it('should round shape coordinates', function(done) {
// given
var events = {};
@ -210,7 +213,7 @@ describe('import - Importer', function() {
});
it('should round shape\'s height and width', function(done) {
it('should round shape dimensions', function(done) {
// given
var events = {};
@ -234,6 +237,42 @@ describe('import - Importer', function() {
});
describe('order', function() {
it('should import sequence flows and lanes behind other flow nodes', function(done) {
var xml = require('./sequenceFlow-ordering.bpmn');
// given
var elementRegistry = diagram.get('elementRegistry');
runImport(diagram, xml, function(err, warnings) {
// when
var processShape = elementRegistry.get('Participant_1jxpy8o');
var children = processShape.children;
// lanes
// connections
// other elements
var correctlyOrdered = [].concat(
children.filter(function(c) { return is(c, 'bpmn:Lane'); }),
children.filter(function(c) { return c.waypoints; }),
children.filter(function(c) { return !is(c, 'bpmn:Lane') && !c.waypoints; })
);
// then
expectChildren(diagram, processShape, correctlyOrdered);
done(err);
});
});
});
describe('elements', function() {
it('should import boundary events', function(done) {
@ -422,3 +461,35 @@ describe('import - Importer', function() {
});
});
////////// helpers /////////////////////////////////////
function expectChildren(diagram, parent, children) {
return diagram.invoke(function(elementRegistry) {
// verify model is consistent
expect(parent.children).to.eql(children);
// verify SVG is consistent
var parentGfx = elementRegistry.getGraphics(parent);
var expectedChildrenGfx = children.map(function(c) {
return elementRegistry.getGraphics(c);
});
var childrenContainerGfx =
domMatches(parentGfx, '[data-element-id="Process_1"]')
? parentGfx
: getChildrenGfx(parentGfx);
var existingChildrenGfx = Array.prototype.map.call(childrenContainerGfx.childNodes, function(c) {
return c.querySelector('.djs-element');
});
expect(existingChildrenGfx).to.eql(expectedChildrenGfx);
});
}

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:collaboration id="Collaboration_0mfxch0">
<bpmn:participant id="Participant_1jxpy8o" processRef="Process_1" />
</bpmn:collaboration>
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:laneSet>
<bpmn:lane id="Lane_0p1e6ph">
<bpmn:flowNodeRef>Task_16z77et</bpmn:flowNodeRef>
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Task_14uuigv</bpmn:flowNodeRef>
<bpmn:flowNodeRef>EndEvent_0exo24i</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_1onrzg4">
<bpmn:flowNodeRef>Task_1y8k07l</bpmn:flowNodeRef>
<bpmn:flowNodeRef>ExclusiveGateway_0xyt6ot</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:task id="Task_16z77et">
<bpmn:incoming>SequenceFlow_1q8max9</bpmn:incoming>
</bpmn:task>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1q8max9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_14uuigv" />
<bpmn:endEvent id="EndEvent_0exo24i">
<bpmn:incoming>SequenceFlow_01mqkl9</bpmn:incoming>
</bpmn:endEvent>
<bpmn:task id="Task_1y8k07l">
<bpmn:outgoing>SequenceFlow_01mqkl9</bpmn:outgoing>
</bpmn:task>
<bpmn:exclusiveGateway id="ExclusiveGateway_0xyt6ot" />
<bpmn:dataObjectReference id="DataObjectReference_0wub6uo" dataObjectRef="DataObject_06ep7y5" />
<bpmn:dataObject id="DataObject_06ep7y5" />
<bpmn:dataStoreReference id="DataStoreReference_1ioqvkf" />
<bpmn:sequenceFlow id="SequenceFlow_1q8max9" sourceRef="StartEvent_1" targetRef="Task_16z77et" />
<bpmn:sequenceFlow id="SequenceFlow_01mqkl9" sourceRef="Task_1y8k07l" targetRef="EndEvent_0exo24i" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0mfxch0">
<bpmndi:BPMNShape id="Participant_1jxpy8o_di" bpmnElement="Participant_1jxpy8o">
<dc:Bounds x="90" y="65.5" width="600" height="323" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_16z77et_di" bpmnElement="Task_16z77et">
<dc:Bounds x="473" y="88" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="140" y="206" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="113" y="176" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_14uuigv_di" bpmnElement="Task_14uuigv">
<dc:Bounds x="280" y="157" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0exo24i_di" bpmnElement="EndEvent_0exo24i">
<dc:Bounds x="181" y="86" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="154" y="64" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1y8k07l_di" bpmnElement="Task_1y8k07l">
<dc:Bounds x="321" y="289" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_0xyt6ot_di" bpmnElement="ExclusiveGateway_0xyt6ot" isMarkerVisible="true">
<dc:Bounds x="182" y="242" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="162" y="296" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DataObjectReference_0wub6uo_di" bpmnElement="DataObjectReference_0wub6uo">
<dc:Bounds x="461" y="242" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="434" y="296" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DataStoreReference_1ioqvkf_di" bpmnElement="DataStoreReference_1ioqvkf">
<dc:Bounds x="542" y="242" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="522" y="296" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1q8max9_di" bpmnElement="SequenceFlow_1q8max9">
<di:waypoint xsi:type="dc:Point" x="158" y="242" />
<di:waypoint xsi:type="dc:Point" x="158" y="265" />
<di:waypoint xsi:type="dc:Point" x="617" y="265" />
<di:waypoint xsi:type="dc:Point" x="617" y="128" />
<di:waypoint xsi:type="dc:Point" x="573" y="128" />
<bpmndi:BPMNLabel>
<dc:Bounds x="343" y="244" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_01mqkl9_di" bpmnElement="SequenceFlow_01mqkl9">
<di:waypoint xsi:type="dc:Point" x="371" y="289" />
<di:waypoint xsi:type="dc:Point" x="371" y="190" />
<di:waypoint xsi:type="dc:Point" x="199" y="190" />
<di:waypoint xsi:type="dc:Point" x="199" y="122" />
<bpmndi:BPMNLabel>
<dc:Bounds x="240" y="169" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Lane_0p1e6ph_di" bpmnElement="Lane_0p1e6ph">
<dc:Bounds x="120" y="65.5" width="570" height="162" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1onrzg4_di" bpmnElement="Lane_1onrzg4">
<dc:Bounds x="120" y="227.5" width="570" height="161" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>