From 5814b72e952be5caa95e8f4cb8eb9bf9e7f3a9df Mon Sep 17 00:00:00 2001 From: Martin Stamm Date: Wed, 18 May 2022 16:04:32 +0200 Subject: [PATCH] feat(modeling): update associations on connection change --- .../behavior/LayoutConnectionBehavior.js | 117 ++++++++++++++++++ lib/features/modeling/behavior/index.js | 3 + .../behavior/util/ConnectionLayoutUtil.js | 16 +++ .../behavior/LayoutConnectionBehavior.bpmn | 44 +++++++ .../behavior/LayoutConnectionBehaviorSpec.js | 113 +++++++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 lib/features/modeling/behavior/LayoutConnectionBehavior.js create mode 100644 lib/features/modeling/behavior/util/ConnectionLayoutUtil.js create mode 100644 test/spec/features/modeling/behavior/LayoutConnectionBehavior.bpmn create mode 100644 test/spec/features/modeling/behavior/LayoutConnectionBehaviorSpec.js diff --git a/lib/features/modeling/behavior/LayoutConnectionBehavior.js b/lib/features/modeling/behavior/LayoutConnectionBehavior.js new file mode 100644 index 00000000..b8829724 --- /dev/null +++ b/lib/features/modeling/behavior/LayoutConnectionBehavior.js @@ -0,0 +1,117 @@ +import { + assign +} from 'min-dash'; + +import inherits from 'inherits'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { getConnectionAdjustment as getConnectionAnchorPoint } from './util/ConnectionLayoutUtil'; + +/** + * A component that makes sure that Associations connected to Connections + * are updated together with the Connection. + * + * @param {EventBus} eventBus + * @param {Modeling} modeling + */ +export default function LayoutConnectionBehavior( + eventBus, modeling) { + + CommandInterceptor.call(this, eventBus); + + function getnewAnchorPoint(event, point) { + + var context = event.context, + connection = context.connection, + hints = assign({}, context.hints), + newWaypoints = context.newWaypoints || connection.waypoints, + oldWaypoints = context.oldWaypoints; + + + if (typeof hints.startChanged === 'undefined') { + hints.startChanged = !!hints.connectionStart; + } + + if (typeof hints.endChanged === 'undefined') { + hints.endChanged = !!hints.connectionEnd; + } + + return getConnectionAnchorPoint(point, newWaypoints, oldWaypoints, hints); + } + + this.postExecute([ + 'connection.layout', + 'connection.updateWaypoints' + ], function(event) { + var context = event.context; + + var connection = context.connection, + outgoing = connection.outgoing, + incoming = connection.incoming; + + incoming.forEach(function(connection) { + var endPoint = connection.waypoints[connection.waypoints.length - 1]; + var newEndpoint = getnewAnchorPoint(event, endPoint); + + var newWaypoints = [].concat(connection.waypoints.slice(0, -1), [ newEndpoint ]); + + modeling.updateWaypoints(connection, newWaypoints); + }); + + outgoing.forEach(function(connection) { + var startpoint = connection.waypoints[0]; + var newStartpoint = getnewAnchorPoint(event, startpoint); + + var newWaypoints = [].concat([ newStartpoint ], connection.waypoints.slice(0, -1)); + + modeling.updateWaypoints(connection, newWaypoints); + }); + + }); + + + this.postExecute([ + 'connection.move' + ], function(event) { + var context = event.context; + + var connection = context.connection, + outgoing = connection.outgoing, + incoming = connection.incoming, + delta = context.delta; + + incoming.forEach(function(connection) { + var endPoint = connection.waypoints[connection.waypoints.length - 1]; + var newEndpoint = { + x: endPoint.x + delta.x, + y: endPoint.y + delta.y + }; + + var newWaypoints = [].concat(connection.waypoints.slice(0, -1), [ newEndpoint ]); + + modeling.updateWaypoints(connection, newWaypoints); + }); + + outgoing.forEach(function(connection) { + var startpoint = connection.waypoints[0]; + var newStartpoint = { + x: startpoint.x + delta.x, + y: startpoint.y + delta.y + }; + + var newWaypoints = [].concat([ newStartpoint ], connection.waypoints.slice(0, -1)); + + modeling.updateWaypoints(connection, newWaypoints); + }); + + }); + +} + +inherits(LayoutConnectionBehavior, CommandInterceptor); + +LayoutConnectionBehavior.$inject = [ + 'eventBus', + 'modeling' +]; diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 1bdfaf86..acceab57 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -17,6 +17,7 @@ import GroupBehavior from './GroupBehavior'; import ImportDockingFix from './ImportDockingFix'; import IsHorizontalFix from './IsHorizontalFix'; import LabelBehavior from './LabelBehavior'; +import LayoutConnectionBehavior from './LayoutConnectionBehavior'; import MessageFlowBehavior from './MessageFlowBehavior'; import ModelingFeedback from './ModelingFeedback'; import RemoveEmbeddedLabelBoundsBehavior from './RemoveEmbeddedLabelBoundsBehavior'; @@ -57,6 +58,7 @@ export default { 'importDockingFix', 'isHorizontalFix', 'labelBehavior', + 'layoutConnectionBehavior', 'messageFlowBehavior', 'modelingFeedback', 'removeElementBehavior', @@ -95,6 +97,7 @@ export default { importDockingFix: [ 'type', ImportDockingFix ], isHorizontalFix: [ 'type', IsHorizontalFix ], labelBehavior: [ 'type', LabelBehavior ], + layoutConnectionBehavior: [ 'type', LayoutConnectionBehavior ], messageFlowBehavior: [ 'type', MessageFlowBehavior ], modelingFeedback: [ 'type', ModelingFeedback ], removeElementBehavior: [ 'type', RemoveElementBehavior ], diff --git a/lib/features/modeling/behavior/util/ConnectionLayoutUtil.js b/lib/features/modeling/behavior/util/ConnectionLayoutUtil.js new file mode 100644 index 00000000..20300f02 --- /dev/null +++ b/lib/features/modeling/behavior/util/ConnectionLayoutUtil.js @@ -0,0 +1,16 @@ +import { getAnchorPointAdjustment } from './LayoutUtil'; + +/** + * Calculate the new point after the connection waypoints got updated. + * + * @param {djs.model.Label} label + * @param {Array} newWaypoints + * @param {Array} oldWaypoints + * @param {Object} hints + * + * @return {Point} point + */ +export function getConnectionAdjustment(position, newWaypoints, oldWaypoints, hints) { + return getAnchorPointAdjustment(position, newWaypoints, oldWaypoints, hints).point; +} + diff --git a/test/spec/features/modeling/behavior/LayoutConnectionBehavior.bpmn b/test/spec/features/modeling/behavior/LayoutConnectionBehavior.bpmn new file mode 100644 index 00000000..fa0001e3 --- /dev/null +++ b/test/spec/features/modeling/behavior/LayoutConnectionBehavior.bpmn @@ -0,0 +1,44 @@ + + + + + SequenceFlow_1 + + + SequenceFlow_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/LayoutConnectionBehaviorSpec.js b/test/spec/features/modeling/behavior/LayoutConnectionBehaviorSpec.js new file mode 100644 index 00000000..9d63f420 --- /dev/null +++ b/test/spec/features/modeling/behavior/LayoutConnectionBehaviorSpec.js @@ -0,0 +1,113 @@ +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + + +import { + assign, + map, +} from 'min-dash'; + +import modelingModule from 'lib/features/modeling'; +import coreModule from 'lib/core'; + +describe('behavior - LayoutConnectionBehavior', function() { + + var diagramXML = require('./LayoutConnectionBehavior.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { + modules: [ + modelingModule, + coreModule + ] + })); + + + describe('association', function() { + + it('should reconnect on bendpoint move', inject(function(elementRegistry, modeling) { + + // given + var sequenceFlow = elementRegistry.get('SequenceFlow_1'); + var association = elementRegistry.get('Association_1'); + + // when + var newWaypoints = copyWaypoints(sequenceFlow); + newWaypoints.splice(1, 0, + { x: 500, y: 300 } + ); + + var hints = { + bendpointMove: { + bendpointIndex: 1, + insert: true + } + }; + + modeling.updateWaypoints(sequenceFlow, newWaypoints, hints); + + // then + expectWaypoints(association, [ + { x: 525, y: 110 }, + { x: 355, y: 229 }, + ]); + })); + + + it('should reconnect on connection move', inject(function(elementRegistry, modeling) { + + // given + var startEvent = elementRegistry.get('StartEvent_1'); + var endEvent = elementRegistry.get('EndEvent_1'); + var association = elementRegistry.get('Association_1'); + + // when + modeling.moveElements([ startEvent, endEvent ], { x: 0, y: 200 }); + + // then + expectWaypoints(association, [ + { x: 525, y: 110 }, + { x: 460, y: 350 }, + ]); + + })); + + }); + +}); + + +// helpers ////////// + +function copyWaypoint(waypoint) { + return assign({}, waypoint); +} + +function copyWaypoints(connection) { + return map(connection.waypoints, function(waypoint) { + + waypoint = copyWaypoint(waypoint); + + if (waypoint.original) { + waypoint.original = copyWaypoint(waypoint.original); + } + + return waypoint; + }); +} + +function expectWaypoints(connection, expectedWaypoints) { + + var actualWaypoints = connection.waypoints; + + expect(actualWaypoints).to.exist; + expect(expectedWaypoints).to.exist; + + expect(connection.waypoints.length).to.eql(expectedWaypoints.length); + + for (var i in actualWaypoints) { + expect(actualWaypoints[i].x).to.eql(expectedWaypoints[i].x); + expect(actualWaypoints[i].y).to.eql(expectedWaypoints[i].y); + } +} \ No newline at end of file