From 220c0a73f3deda29345450ac1e0715a79ddea815 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Wed, 4 Jul 2018 09:13:38 +0200 Subject: [PATCH] feat(modeling/BpmnLayouter): handle boundary events This adds proper connection layouting for sequence flows leaving from boundary events. If needed, such connections will be layoute with an U-turn. Closes #467 --- lib/features/modeling/BpmnLayouter.js | 202 ++++++++++++--- ...LayoutSequenceFlowSpec.boundaryEvents.bpmn | 74 ++---- .../modeling/layout/LayoutSequenceFlowSpec.js | 240 ++++++++++-------- 3 files changed, 316 insertions(+), 200 deletions(-) diff --git a/lib/features/modeling/BpmnLayouter.js b/lib/features/modeling/BpmnLayouter.js index 8ecae7f9..9c3cf900 100644 --- a/lib/features/modeling/BpmnLayouter.js +++ b/lib/features/modeling/BpmnLayouter.js @@ -7,7 +7,8 @@ import { import BaseLayouter from 'diagram-js/lib/layout/BaseLayouter'; import { - repairConnection + repairConnection, + withoutRedundantPoints } from 'diagram-js/lib/layout/ManhattanLayout'; import { @@ -15,10 +16,6 @@ import { getOrientation } from 'diagram-js/lib/layout/LayoutUtil'; -import { - pointsOnLine -} from 'diagram-js/lib/util/Geometry'; - import { isExpanded } from '../../util/DiUtil'; @@ -32,7 +29,6 @@ inherits(BpmnLayouter, BaseLayouter); BpmnLayouter.prototype.layoutConnection = function(connection, hints) { - hints = hints || {}; var source = connection.source, @@ -107,29 +103,18 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { // // except for // - // (1) outgoing of BoundaryEvents -> layout h:v or v:h based on attach orientation + // (1) outgoing of BoundaryEvents -> layout based on attach orientation and target orientation // (2) incoming / outgoing of Gateway -> v:h (outgoing), h:v (incoming) // if (is(connection, 'bpmn:SequenceFlow') || isCompensationAssociation(connection)) { - // make sure boundary event connections do - // not look ugly =:> if (is(source, 'bpmn:BoundaryEvent')) { - var orientation = getAttachOrientation(source); + manhattanOptions = { + preferredLayouts: getBoundaryEventPreferredLayouts(source, target) + }; - if (/left|right/.test(orientation)) { - manhattanOptions = { - preferredLayouts: [ 'h:v' ] - }; - } else - - if (/top|bottom/.test(orientation)) { - manhattanOptions = { - preferredLayouts: [ 'v:h' ] - }; - } } else if (is(source, 'bpmn:Gateway')) { @@ -146,7 +131,6 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { }; } - // apply horizontal love <3 else { manhattanOptions = { preferredLayouts: [ 'h:h' ] @@ -159,25 +143,14 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { manhattanOptions = assign(manhattanOptions, hints); updatedWaypoints = - repairConnection( - source, target, - start, end, - waypoints, - manhattanOptions); - - // filter un-needed waypoints that may be the result of - // bundle collapsing - updatedWaypoints = updatedWaypoints && updatedWaypoints.reduce(function(points, p, idx) { - - var previous = points[points.length - 1], - next = updatedWaypoints[idx + 1]; - - if (!pointsOnLine(previous, next, p, 0)) { - points.push(p); - } - - return points; - }, []); + withoutRedundantPoints( + repairConnection( + source, target, + start, end, + waypoints, + manhattanOptions + ) + ); } return updatedWaypoints || [ start, end ]; @@ -210,4 +183,151 @@ function isCompensationAssociation(connection) { function isExpandedSubProcess(element) { return is(element, 'bpmn:SubProcess') && isExpanded(element); +} + +function isSame(a, b) { + return a === b; +} + +function isAnyOrientation(orientation, orientations) { + return orientations.indexOf(orientation) !== -1; +} + +var oppositeOrientationMapping = { + 'top': 'bottom', + 'top-right': 'bottom-left', + 'top-left': 'bottom-right', + 'right': 'left', + 'bottom': 'top', + 'bottom-right': 'top-left', + 'bottom-left': 'top-right', + 'left': 'right' +}; + +var orientationDirectionMapping = { + top: 't', + right: 'r', + bottom: 'b', + left: 'l' +}; + +function getHorizontalOrientation(orientation) { + var matches = /right|left/.exec(orientation); + + return matches && matches[0]; +} + +function getVerticalOrientation(orientation) { + var matches = /top|bottom/.exec(orientation); + + return matches && matches[0]; +} + +function isOppositeOrientation(a, b) { + return oppositeOrientationMapping[a] === b; +} + +function isOppositeHorizontalOrientation(a, b) { + var horizontalOrientation = getHorizontalOrientation(a); + + var oppositeHorizontalOrientation = oppositeOrientationMapping[horizontalOrientation]; + + return b.indexOf(oppositeHorizontalOrientation) !== -1; +} + +function isOppositeVerticalOrientation(a, b) { + var verticalOrientation = getVerticalOrientation(a); + + var oppositeVerticalOrientation = oppositeOrientationMapping[verticalOrientation]; + + return b.indexOf(oppositeVerticalOrientation) !== -1; +} + +function isHorizontalOrientation(orientation) { + return orientation === 'right' || orientation === 'left'; +} + +function getBoundaryEventPreferredLayouts(source, target) { + var sourceMid = getMid(source), + targetMid = getMid(target), + attachOrientation = getAttachOrientation(source), + sourceLayout, + targetlayout; + + var isLoop = isSame(source.host, target); + + var attachedToSide = isAnyOrientation(attachOrientation, [ 'top', 'right', 'bottom', 'left' ]); + + var isHorizontalAttachOrientation = isHorizontalOrientation(attachOrientation); + + var targetOrientation = getOrientation(targetMid, sourceMid, { + x: source.width / 2 + target.width / 2, + y: source.height / 2 + target.height / 2 + }); + + // source layout + + // attached to either top, right, bottom or left side + if (attachedToSide) { + + sourceLayout = orientationDirectionMapping[ + isHorizontalAttachOrientation ? + getHorizontalOrientation(attachOrientation) : + getVerticalOrientation(attachOrientation) + ]; + + } else + + // attached to either top-right, top-left, bottom-right or bottom-left corner + { + + // loop, same vertical or opposite horizontal orientation + if (isLoop || + isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) || + isOppositeOrientation(getHorizontalOrientation(attachOrientation), getHorizontalOrientation(targetOrientation)) + ) { + sourceLayout = orientationDirectionMapping[getVerticalOrientation(attachOrientation)]; + } else { + sourceLayout = orientationDirectionMapping[getHorizontalOrientation(attachOrientation)]; + } + + } + + // target layout + + // attached to either top, right, bottom or left side + if (attachedToSide) { + + // loop or opposite horizontal/vertical orientation + if ( + isLoop || + (isHorizontalAttachOrientation ? + isOppositeHorizontalOrientation(attachOrientation, targetOrientation) : + isOppositeVerticalOrientation(attachOrientation, targetOrientation)) + ) { + targetlayout = isHorizontalAttachOrientation ? 'h' : 'v'; + } else { + targetlayout = isHorizontalAttachOrientation ? 'v' : 'h'; + } + + } else + + // attached to either top-right, top-left, bottom-right or bottom-left corner + { + + // orientation is 'right', 'left' + // or same vertical orientation but also 'right' or 'left' + if ( + isHorizontalOrientation(targetOrientation) || + (isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) && + getHorizontalOrientation(targetOrientation)) + ) { + targetlayout = 'h'; + } else { + targetlayout = 'v'; + } + + } + + return [ sourceLayout + ':' + targetlayout ]; } \ No newline at end of file diff --git a/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.boundaryEvents.bpmn b/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.boundaryEvents.bpmn index a6fc9a86..47f14697 100644 --- a/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.boundaryEvents.bpmn +++ b/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.boundaryEvents.bpmn @@ -1,64 +1,44 @@ - + + - - - - - - - - - - + + + + + + + - - + + - - - - - + + - - - - - + + - - - - - + + - - - - - + + - - + + - - + + - - + + - - - - - - - - + + diff --git a/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.js b/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.js index 26de6d35..018ede39 100644 --- a/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.js +++ b/test/spec/features/modeling/layout/LayoutSequenceFlowSpec.js @@ -31,14 +31,139 @@ describe('features/modeling - layout', function() { }); - describe.skip('overall experience, boundary events', function() { + describe('boundary events', function() { var diagramXML = require('./LayoutSequenceFlowSpec.boundaryEvents.bpmn'); - beforeEach(bootstrapModeler(diagramXML, { modules: Modeler.prototype._modules })); + var testModules = [ coreModule, modelingModule ]; + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); - it('should feel awesome', inject(function() { })); + describe('loops', function() { + + it('attached top right', function() { + + // when + var connection = connect('BoundaryEvent_TopRight', 'SubProcess'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 300 }, x: 650, y: 282 }, + { x: 650, y: 262 }, + { x: 475, y: 262 }, + { original: { x: 475, y: 400 }, x: 475, y: 300 } + ]); + }); + + + it('attached bottom right', function() { + + // when + var connection = connect('BoundaryEvent_BottomRight', 'SubProcess'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 468 }, x: 668, y: 468 }, + { x: 688, y: 468 }, + { x: 688, y: 400 }, + { original: { x: 475, y: 400 }, x: 650, y: 400 } + ]); + }); + + + it('attached bottom left', function() { + + // when + var connection = connect('BoundaryEvent_BottomLeft', 'SubProcess'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 300, y: 500 }, x: 300, y: 518 }, + { x: 300, y: 538 }, + { x: 475, y: 538 }, + { original: { x: 475, y: 400 }, x: 475, y: 500 } + ]); + }); + + + it('attached top left', function() { + + // when + var connection = connect('BoundaryEvent_TopLeft', 'SubProcess'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 300, y: 338 }, x: 282, y: 338 }, + { x: 262, y: 338 }, + { x: 262, y: 400 }, + { original: { x: 475, y: 400 }, x: 300, y: 400 } + ]); + }); + + }); + + + describe('non-loops', function() { + + it('attached top right, orientation top', function() { + + // when + var connection = connect('BoundaryEvent_TopRight', 'Task_Top'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 300 }, x: 650, y: 282 }, + { x: 650, y: 40 }, + { original: { x: 450, y: 40 }, x: 500, y: 40 } + ]); + }); + + + it('attached top right, orientation right', function() { + + // when + var connection = connect('BoundaryEvent_TopRight', 'Task_Right'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 300 }, x: 668, y: 300 }, + { x: 900, y: 300 }, + { original: { x: 900, y: 390 }, x: 900, y: 350 } + ]); + }); + + + it('attached top right, orientation bottom', function() { + + // when + var connection = connect('BoundaryEvent_TopRight', 'Task_Bottom'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 300 }, x: 650, y: 282 }, + { x: 650, y: 262 }, + { x: 450, y: 262 }, + { original: { x: 450, y: 690 }, x: 450, y: 650 } + ]); + }); + + + it('attached top right, orientation left', function() { + + // when + var connection = connect('BoundaryEvent_TopRight', 'Task_Left'); + + // then + expect(connection).to.have.waypoints([ + { original: { x: 650, y: 300 }, x: 650, y: 282 }, + { x: 650, y: 262 }, + { x: 50, y: 262 }, + { original: { x: 50, y: 390 }, x: 50, y: 350 } + ]); + }); + + }); }); @@ -210,113 +335,4 @@ describe('features/modeling - layout', function() { }); - - describe('boundary events', function() { - - var diagramXML = require('./LayoutSequenceFlowSpec.boundaryEvents.bpmn'); - - var testModules = [ coreModule, modelingModule ]; - - beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); - - - it('should layout h:h connecting BoundaryEvent -> left Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_A', 'Task_1'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 505, y: 417 }, x: 487, y: 417 }, - { x: 437, y: 417 }, - { x: 437, y: 394 }, - { original: { x: 337, y: 394 }, x: 387, y: 394 } - ]); - })); - - - it('should layout h:v connecting BoundaryEvent -> bottom-left Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_A', 'Task_2'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 505, y: 417 }, x: 487, y: 417 }, - { x: 412, y: 417 }, - { original: { x: 412, y: 543 }, x: 412, y: 503 } - ]); - })); - - - it('should layout h:v connecting BoundaryEvent -> top-right Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_A', 'Task_5'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 505, y: 417 }, x: 523, y: 417 }, - { x: 1016, y: 417 }, - { original: { x: 1016, y: 215 }, x: 1016, y: 255 } - ]); - })); - - - it('should layout v:v connecting BoundaryEvent -> top Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_B', 'Task_4'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 586, y: 258 }, x: 586, y: 240 }, - { original: { x: 586, y: 121 }, x: 586, y: 161 } - ]); - })); - - - it('should layout v:h connecting BoundaryEvent -> top-left Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_B', 'Task_3'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 586, y: 258 }, x: 586, y: 240 }, - { x: 586, y: 162 }, - { original: { x: 428, y: 162 }, x: 478, y: 162 } - ]); - })); - - - it('should layout h:v connecting BoundaryEvent -> bottom-right Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_C', 'Task_6'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 855, y: 293 }, x: 873, y: 293 }, - { x: 1041, y: 293 }, - { original: { x: 1041, y: 483 }, x: 1041, y: 443 } - ]); - })); - - - it('should layout v:h connecting BoundaryEvent -> bottom-left Task', inject(function() { - - // when - var connection = connect('BoundaryEvent_D', 'Task_2'); - - // then - expect(connection).to.have.waypoints([ - { original: { x: 815, y: 458 }, x: 815, y: 476 }, - { x: 815, y: 543 }, - { original: { x: 412, y: 543 }, x: 462, y: 543 } - ]); - })); - - }); - });