diff --git a/lib/features/modeling/behavior/LabelBehavior.js b/lib/features/modeling/behavior/LabelBehavior.js index be11c09b..3e174d12 100644 --- a/lib/features/modeling/behavior/LabelBehavior.js +++ b/lib/features/modeling/behavior/LabelBehavior.js @@ -4,12 +4,14 @@ var assign = require('lodash/object/assign'), inherits = require('inherits'); var LabelUtil = require('../../../util/LabelUtil'), + LabelLayoutUtil = require('./util/LabelLayoutUtil'), ModelUtil = require('../../../util/ModelUtil'), is = ModelUtil.is, getBusinessObject = ModelUtil.getBusinessObject; var hasExternalLabel = LabelUtil.hasExternalLabel, - getExternalLabelMid = LabelUtil.getExternalLabelMid; + getExternalLabelMid = LabelUtil.getExternalLabelMid, + getLabelAdjustment = LabelLayoutUtil.getLabelAdjustment; var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor'); @@ -70,6 +72,55 @@ function LabelSupport(eventBus, modeling, bpmnFactory) { } }); + + + this.postExecute([ + 'connection.layout', + 'connection.reconnectEnd', + 'connection.reconnectStart' + ], function(e) { + + var context = e.context, + oldWaypoints = context.oldWaypoints, + newWaypoints = context.newWaypoints || context.connection.waypoints, + label = context.connection.label, + hints = context.hints || {}, + delta; + + if (e.command != 'connection.layout') { + hints.startChanged = e.command == 'connection.reconnectStart' ? true : false; + hints.endChanged = e.command == 'connection.reconnectEnd' ? true : false; + } + + if (!label) { + return; + } + + delta = getLabelAdjustment(label, newWaypoints, oldWaypoints, hints); + + modeling.moveShape(label, delta); + }); + + this.postExecute([ + 'connection.updateWaypoints' + ], function(e) { + + var context = e.context, + oldWaypoints = context.oldWaypoints, + newWaypoints = context.newWaypoints, + label = context.connection.label, + hints = context.hints || {}, + delta; + + if (!label) { + return; + } + + delta = getLabelAdjustment(label, newWaypoints, oldWaypoints, hints); + + modeling.moveShape(label, delta); + }); + this.postExecute([ 'shape.replace' ], function(e) { var context = e.context, newShape = context.newShape, diff --git a/lib/features/modeling/behavior/util/GeometricUtil.js b/lib/features/modeling/behavior/util/GeometricUtil.js new file mode 100644 index 00000000..1c61e171 --- /dev/null +++ b/lib/features/modeling/behavior/util/GeometricUtil.js @@ -0,0 +1,129 @@ +'use strict'; + +/** + * Returns the length of a vector + * + * @param {Vector} + * @return {Float} + */ +function vectorLength(v) { + return Math.sqrt( Math.pow(v.x, 2) + Math.pow(v.y, 2) ); +} + +module.exports.vectorLength = vectorLength; + +/** + * Calculates the angle between a line a the yAxis + * + * @param {Array} + * @return {Float} + */ +function getAngle(line) { + // return value is between 0, 180 and -180, -0 + // @janstuemmel: maybe replace return a/b with b/a + return Math.atan( (line[1].y - line[0].y) / (line[1].x - line[0].x) ); +} + +module.exports.getAngle = getAngle; + +/** + * Rotates a vector by a given angle + * + * @param {Vector} + * @param {Float} Angle in radians + * @return {Vector} + */ +function rotateVector(vector, angle) { + return (!angle) ? vector : { + x: Math.cos(angle) * vector.x - Math.sin(angle) * vector.y, + y: Math.sin(angle) * vector.x + Math.cos(angle) * vector.y + }; +} + +module.exports.rotateVector = rotateVector; + +/** + * Solves a 2D equation system + * a + r*b = c, where a,b,c are 2D vectors + * + * @param {Vector} + * @param {Vector} + * @param {Vector} + * @return {Float} + */ +function solveLambaSystem(a, b, c) { + + // the 2d system + var system = [ + { n: a[0] - c[0], lambda: b[0] }, + { n: a[1] - c[1], lambda: b[1] } + ]; + + // solve + var n = system[0].n * b[0] + system[1].n * b[1], + l = system[0].lambda * b[0] + system[1].lambda * b[1]; + + return -n/l; +} + +/** + * Position of perpendicular foot + * + * @param {Point} + * @param [ {Point}, {Point} ] line defined throug two points + * @return {Point} the perpendicular foot position + */ +function perpendicularFoot(point, line) { + + var a = line[0], b = line[1]; + + // relative position of b from a + var bd = { x: b.x - a.x, y: b.y - a.y }; + + // solve equation system to the parametrized vectors param real value + var r = solveLambaSystem( [ a.x, a.y ], [ bd.x, bd.y ], [ point.x, point.y ] ); + + return { x: a.x + r*bd.x, y: a.y + r*bd.y }; + +} + +module.exports.perpendicularFoot = perpendicularFoot; + +/** + * Calculates the distance between a point and a line + * + * @param {Point} + * @param [ {Point}, {Point} ] line defined throug two points + * @return {Float} distance + */ +function getDistancePointLine(point, line) { + + var pfPoint = perpendicularFoot(point, line); + + // distance vector + var connectionVector = { + x: pfPoint.x - point.x, + y: pfPoint.y - point.y + }; + + return vectorLength(connectionVector); +} + +module.exports.getDistancePointLine = getDistancePointLine; + +/** + * Calculates the distance between two points + * + * @param {Point} + * @param {Point} + * @return {Float} distance + */ +function getDistancePointPoint(point1, point2) { + + return vectorLength({ + x: point1.x - point2.x, + y: point1.y - point2.y + }); +} + +module.exports.getDistancePointPoint = getDistancePointPoint; diff --git a/lib/features/modeling/behavior/util/LabelLayoutUtil.js b/lib/features/modeling/behavior/util/LabelLayoutUtil.js new file mode 100644 index 00000000..44930025 --- /dev/null +++ b/lib/features/modeling/behavior/util/LabelLayoutUtil.js @@ -0,0 +1,216 @@ +'use strict'; + +var GeometricUtil = require('./GeometricUtil'); + +var getDistance = require('./GeometricUtil').getDistancePointPoint; + +var getAttachment = require('./LineAttachmentUtil').getAttachment; + + +function findNewLabelLineStartIndex(oldWaypoints, newWaypoints, attachment, hints) { + + var index = attachment.segmentIndex; + + var offset = newWaypoints.length - oldWaypoints.length; + + // segmentMove happend + if (hints.segmentMove) { + + var oldSegmentStartIndex = hints.segmentMove.segmentStartIndex, + newSegmentStartIndex = hints.segmentMove.newSegmentStartIndex; + + // if label was on moved segment return new segment index + if (index === oldSegmentStartIndex) { + return newSegmentStartIndex; + } + + // label is after new segment index + if (index >= newSegmentStartIndex) { + return (index+offset < newSegmentStartIndex) ? newSegmentStartIndex : index+offset; + } + + // if label is before new segment index + return index; + } + + // bendpointMove happend + if (hints.bendpointMove) { + + var insert = hints.bendpointMove.insert, + bendpointIndex = hints.bendpointMove.bendpointIndex, + newIndex; + + // waypoints length didnt change + if (offset === 0) { + return index; + } + + // label behind new/removed bendpoint + if (index >= bendpointIndex) { + newIndex = insert ? index + 1 : index - 1; + } + + // label before new/removed bendpoint + if (index < bendpointIndex) { + + newIndex = index; + + // decide label should take right or left segment + if (insert && attachment.type !== 'bendpoint' && bendpointIndex-1 === index) { + + var rel = relativePositionMidWaypoint(newWaypoints, bendpointIndex); + + if (rel < attachment.relativeLocation) { + newIndex++; + } + } + } + + return newIndex; + } + + // start/end changed + if (offset === 0) { + return index; + } + + if (hints.connectionStart) { + return (index === 0) ? 0 : null; + } + + if (hints.connectionEnd) { + return (index === oldWaypoints.length - 2) ? newWaypoints.length - 2 : null; + } + + // if nothing fits, return null + return null; +} + +module.exports.findNewLabelLineStartIndex = findNewLabelLineStartIndex; + + +/** + * Calculate the required adjustment (move delta) for the given label + * after the connection waypoints got updated. + * + * @param {djs.model.Label} label + * @param {Array} newWaypoints + * @param {Array} oldWaypoints + * @param {Object} hints + * + * @return {Point} delta + */ +function getLabelAdjustment(label, newWaypoints, oldWaypoints, hints) { + + var x = 0, + y = 0; + + var labelPosition = getLabelMid(label); + + // get closest attachment + var attachment = getAttachment(labelPosition, oldWaypoints), + oldLabelLineIndex = attachment.segmentIndex, + newLabelLineIndex = findNewLabelLineStartIndex(oldWaypoints, newWaypoints, attachment, hints); + + if ( newLabelLineIndex === null ) { + return { x: x, y: y }; + } + + // should never happen + // TODO(@janstuemmel): throw an error here when connectionSegmentMove is refactored + if (newLabelLineIndex < 0 || + newLabelLineIndex > newWaypoints.length - 2) { + return { x: x, y: y }; + } + + var oldLabelLine = getLine(oldWaypoints, oldLabelLineIndex), + newLabelLine = getLine(newWaypoints, newLabelLineIndex), + oldFoot = attachment.position; + + var relativeFootPosition = getRelativeFootPosition(oldLabelLine, oldFoot), + angleDelta = getAngleDelta(oldLabelLine, newLabelLine); + + // special rule if label on bendpoint + if (attachment.type === 'bendpoint') { + + var offset = newWaypoints.length - oldWaypoints.length, + oldBendpointIndex = attachment.bendpointIndex, + oldBendpoint = oldWaypoints[oldBendpointIndex]; + + // bendpoint position hasnt changed, return same position + if (newWaypoints.indexOf(oldBendpoint) !== -1) { + return { x: x, y: y }; + } + + // new bendpoint and old bendpoint have same index, then just return the offset + if (offset === 0) { + var newBendpoint = newWaypoints[oldBendpointIndex]; + + return { + x: newBendpoint.x - attachment.position.x, + y: newBendpoint.y - attachment.position.y + }; + } + + // if bendpoints get removed + if (offset < 0 && oldBendpointIndex !== 0 && oldBendpointIndex < oldWaypoints.length - 1) { + relativeFootPosition = relativePositionMidWaypoint(oldWaypoints, oldBendpointIndex); + } + } + + var newFoot = { + x: (newLabelLine[1].x - newLabelLine[0].x) * relativeFootPosition + newLabelLine[0].x, + y: (newLabelLine[1].y - newLabelLine[0].y) * relativeFootPosition + newLabelLine[0].y + }; + + // the rotated vector to label + var newLabelVector = GeometricUtil.rotateVector({ + x: labelPosition.x - oldFoot.x, + y: labelPosition.y - oldFoot.y + }, angleDelta); + + // the new relative position + x = newFoot.x + newLabelVector.x - labelPosition.x; + y = newFoot.y + newLabelVector.y - labelPosition.y; + + return { x: x, y: y }; +} + +module.exports.getLabelAdjustment = getLabelAdjustment; + + +//// HELPERS /////// + +function relativePositionMidWaypoint(waypoints, idx) { + + var distanceSegment1 = getDistance(waypoints[idx-1], waypoints[idx]), + distanceSegment2 = getDistance(waypoints[idx], waypoints[idx+1]); + + var relativePosition = distanceSegment1 / ( distanceSegment1 + distanceSegment2 ); + + return relativePosition; + +} + +function getLabelMid(label) { + return { + x: label.x + label.width / 2, + y: label.y + label.height / 2 + }; +} + +function getAngleDelta(l1, l2) { + var a1 = GeometricUtil.getAngle(l1), + a2 = GeometricUtil.getAngle(l2); + return a2 - a1; +} + +function getLine(waypoints, idx) { + return [ waypoints[idx], waypoints[idx+1] ]; +} + +function getRelativeFootPosition(line, foot) { + var length = GeometricUtil.getDistancePointPoint(line[0], line[1]), + lengthToFoot = GeometricUtil.getDistancePointPoint(line[0], foot); + return lengthToFoot / length; +} diff --git a/lib/features/modeling/behavior/util/LineAttachmentUtil.js b/lib/features/modeling/behavior/util/LineAttachmentUtil.js new file mode 100644 index 00000000..4b36ac5c --- /dev/null +++ b/lib/features/modeling/behavior/util/LineAttachmentUtil.js @@ -0,0 +1,214 @@ +'use strict'; + +var sqrt = Math.sqrt, + min = Math.min, + max = Math.max; + +/** + * Calculate the square (power to two) of a number. + * + * @param {Number} n + * + * @return {Number} + */ +function sq(n) { + return Math.pow(n, 2); +} + +/** + * Get distance between two points. + * + * @param {Point} p1 + * @param {Point} p2 + * + * @return {Number} + */ +function getDistance(p1, p2) { + return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y)); +} + +/** + * Return the attachment of the given point on the specified line. + * + * The attachment is either a bendpoint (attached to the given point) + * or segment (attached to a location on a line segment) attachment: + * + * ```javascript + * var pointAttachment = { + * type: 'bendpoint', + * bendpointIndex: 3, + * position: { x: 10, y: 10 } // the attach point on the line + * }; + * + * var segmentAttachment = { + * type: 'segment', + * segmentIndex: 2, + * relativeLocation: 0.31, // attach point location between 0 (at start) and 1 (at end) + * position: { x: 10, y: 10 } // the attach point on the line + * }; + * ``` + * + * @param {Point} point + * @param {Array} line + * + * @return {Object} attachment + */ +function getAttachment(point, line) { + + var idx = 0, + segmentStart, + segmentEnd, + segmentStartDistance, + segmentEndDistance, + attachmentPosition, + minDistance, + intersections, + attachment, + attachmentDistance, + closestAttachmentDistance, + closestAttachment; + + for (idx = 0; idx < line.length - 1; idx++) { + + segmentStart = line[idx]; + segmentEnd = line[idx + 1]; + + if (pointsEqual(segmentStart, segmentEnd)) { + continue; + } + + segmentStartDistance = getDistance(point, segmentStart); + segmentEndDistance = getDistance(point, segmentEnd); + + minDistance = min(segmentStartDistance, segmentEndDistance); + + intersections = getCircleSegmentIntersections(segmentStart, segmentEnd, point, minDistance); + + if (intersections.length < 1) { + throw new Error('expected between [1, 2] circle -> line intersections'); + } + + // one intersection -> bendpoint attachment + if (intersections.length === 1) { + attachment = { + type: 'bendpoint', + position: intersections[0], + segmentIndex: idx, + bendpointIndex: pointsEqual(segmentStart, intersections[0]) ? idx : idx + 1 + }; + } + + // two intersections -> segment attachment + if (intersections.length === 2) { + + attachmentPosition = mid(intersections[0], intersections[1]); + + attachment = { + type: 'segment', + position: attachmentPosition, + segmentIndex: idx, + relativeLocation: getDistance(segmentStart, attachmentPosition) / getDistance(segmentStart, segmentEnd) + }; + } + + attachmentDistance = getDistance(attachment.position, point); + + if (!closestAttachment || closestAttachmentDistance > attachmentDistance) { + closestAttachment = attachment; + closestAttachmentDistance = attachmentDistance; + } + } + + return closestAttachment; +} + +module.exports.getAttachment = getAttachment; + +/** + * Gets the intersection between a circle and a line segment. + * + * @param {Point} s1 segment start + * @param {Point} s2 segment end + * @param {Point} cc circle center + * @param {Number} cr circle radius + * + * @return {Array} intersections + */ +function getCircleSegmentIntersections(s1, s2, cc, cr) { + + var baX = s2.x - s1.x; + var baY = s2.y - s1.y; + var caX = cc.x - s1.x; + var caY = cc.y - s1.y; + + var a = baX * baX + baY * baY; + var bBy2 = baX * caX + baY * caY; + var c = caX * caX + caY * caY - cr * cr; + + var pBy2 = bBy2 / a; + var q = c / a; + + var disc = pBy2 * pBy2 - q; + if (disc < 0) { + return []; + } + + // if disc == 0 ... dealt with later + var tmpSqrt = sqrt(disc); + var abScalingFactor1 = -pBy2 + tmpSqrt; + var abScalingFactor2 = -pBy2 - tmpSqrt; + + var i1 = { + x: round(s1.x - baX * abScalingFactor1), + y: round(s1.y - baY * abScalingFactor1) + }; + + if (disc === 0) { // abScalingFactor1 == abScalingFactor2 + return [ i1 ]; + } + + var i2 = { + x: round(s1.x - baX * abScalingFactor2), + y: round(s1.y - baY * abScalingFactor2) + }; + + return [ i1, i2 ].filter(function(p) { + return isPointInSegment(p, s1, s2); + }); +} + + +function isPointInSegment(p, segmentStart, segmentEnd) { + return ( + fenced(p.x, segmentStart.x, segmentEnd.x) && + fenced(p.y, segmentStart.y, segmentEnd.y) + ); +} + +function fenced(n, rangeStart, rangeEnd) { + return min(rangeStart, rangeEnd) <= n && n <= max(rangeStart, rangeEnd); +} + +/** + * Calculate mid of two points. + * + * @param {Point} p1 + * @param {Point} p2 + * + * @return {Point} + */ +function mid(p1, p2) { + + return { + x: (p1.x + p2.x) / 2, + y: (p1.y + p2.y) / 2 + }; +} + +function round(n) { + return Math.round(n * 1000) / 1000; +} + +function pointsEqual(p1, p2) { + return p1.x === p2.x && p1.y === p2.y; +} diff --git a/test/spec/util/LabelUtil.bpmn b/test/spec/features/modeling/LabelLayouting.initial.bpmn similarity index 100% rename from test/spec/util/LabelUtil.bpmn rename to test/spec/features/modeling/LabelLayouting.initial.bpmn diff --git a/test/spec/features/modeling/LabelLayouting.move.bpmn b/test/spec/features/modeling/LabelLayouting.move.bpmn new file mode 100644 index 00000000..d13e3556 --- /dev/null +++ b/test/spec/features/modeling/LabelLayouting.move.bpmn @@ -0,0 +1,99 @@ + + + + + SequenceFlow_A + + + SequenceFlow_A + SequenceFlow_B + + + + SequenceFlow_B + SequenceFlow_C + SequenceFlow_D + + + + SequenceFlow_C + + + + SequenceFlow_D + SequenceFlow_E + + + + SequenceFlow_E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/LabelLayoutingSpec.js b/test/spec/features/modeling/LabelLayoutingSpec.js new file mode 100644 index 00000000..f3c64e4f --- /dev/null +++ b/test/spec/features/modeling/LabelLayoutingSpec.js @@ -0,0 +1,559 @@ +'use strict'; + +require('../../../TestHelper'); + +/* global bootstrapModeler, inject */ + + +var coreModule = require('lib/core'), + bendpointsModule = require('diagram-js/lib/features/bendpoints'), + modelingModule = require('lib/features/modeling'), + labelEditingModule = require('lib/features/label-editing'); + +var canvasEvent = require('../../../util/MockEvents').createCanvasEvent; + +var testModules = [ + coreModule, + modelingModule, + labelEditingModule, + bendpointsModule +]; + + +describe('modeling - label layouting', function() { + + describe('should create label', function() { + + var diagramXML = require('./LabelLayouting.initial.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { + modules: testModules + })); + + + it('horizontal', inject(function(modeling, elementRegistry) { + + // given + var element1 = elementRegistry.get('StartEvent_1'), + element2 = elementRegistry.get('ExclusiveGateway_2'); + + // when + var connection = modeling.connect(element1, element2); + + // then + expect(connection.label.x).to.be.equal(427); + expect(connection.label.y).to.be.equal(332); + })); + + + it('vertical', inject(function(modeling, elementRegistry) { + + // given + var element1 = elementRegistry.get('StartEvent_1'), + element2 = elementRegistry.get('ExclusiveGateway_1'); + + // when + var connection = modeling.connect(element1, element2); + + // then + expect(connection.label.x).to.be.equal(292); + expect(connection.label.y).to.be.equal(219.5); + })); + + }); + + + describe('should move label', function() { + + var diagramXML = require('./LabelLayouting.move.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { + modules: testModules + })); + + describe('on segment move', function() { + + it('left - no relayout', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: -30, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: -30, y: 0 }); + })); + + + it('left - remove bendpoint', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: -70, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: -70, y: 23 }); + })); + + + it('right - no relayout', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 30, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 30, y: 0 }); + })); + + + it('right - remove bendpoint', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 70, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 70, y: -17 }); + })); + + + it('down', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 0, y: 70 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 0, y: 70 }); + + })); + + + it('up - remove two bendpoints', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 0, y: -90 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: -39, y: -85 }); + + })); + + // TODO(@janstuemmel): solve by connectionSegmentMove refactoring + it.skip('up - remove two bendpoints - redundant waypoints', inject(function(elementRegistry, connectionSegmentMove, dragging, bendpointMove) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + dragging.move(canvasEvent({ x: 620, y: 435 })); + dragging.end(); + + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + dragging.move(canvasEvent({ x: 300, y: 435 })); + dragging.end(); + + var labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + dragging.move(canvasEvent({ x: 0, y: -160 })); + dragging.end(); + + // then + expect(getLabelPosition(connection)).to.not.eql(labelPosition); + + })); + + }); + + describe('on reconnect', function() { + + it('start', inject(function(elementRegistry, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_D'), + shape = elementRegistry.get('Task_1'); + + // when + modeling.reconnectStart(connection, shape, { x: 0, y: 0 }); + + // then + expect(Math.round(connection.label.x)).to.be.equal(528); + expect(Math.round(connection.label.y)).to.be.equal(137); + + })); + + + it('end', inject(function(elementRegistry, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_A'), + shape = elementRegistry.get('Task_2'); + + // when + modeling.reconnectEnd(connection, shape, { x: 294, y: 270 }); + + // then + expect(Math.round(connection.label.x)).to.be.equal(215); + expect(Math.round(connection.label.y)).to.be.equal(184); + + })); + + }); + + describe('on shape move', function() { + + it('down', inject(function(elementRegistry, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_E'), + shape = elementRegistry.get('Task_4'), + labelPosition = getLabelPosition(connection); + + // when + modeling.moveShape(shape, { x: 0, y: 100 }); + + // then + expectLabelMoved(connection, labelPosition, { x: 0, y: 100 }); + + })); + + }); + + describe('on bendpoint add/delete/moving', function() { + + + it('move, label on segment', inject(function(elementRegistry, bendpointMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + + dragging.move(canvasEvent({ x: 455 + 50, y: 120 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(425); + expect(Math.round(connection.label.y)).to.be.equal(170); + + })); + + + it('move, label on bendpoint', inject(function(elementRegistry, bendpointMove, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + // label out of segments, on a bendpoint (idx=1) + modeling.moveShape(connection.label, { x: 40, y: 0 }); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + + dragging.move(canvasEvent({ x: 455 + 50, y: 500 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(477); + expect(Math.round(connection.label.y)).to.be.equal(507); + + })); + + + it('remove bendpoint when label on segment', inject(function(elementRegistry, bendpointMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_B'); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + + dragging.move(canvasEvent({ x: 455, y: 120 + 160 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(380); + expect(Math.round(connection.label.y)).to.be.equal(190); + + })); + + + it('add bendpoint, label on right segment', inject(function(elementRegistry, bendpointMove, dragging, canvas) { + + // given + var connection = elementRegistry.get('SequenceFlow_A'); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1, true); + + dragging.hover({ + element: connection, + gfx: canvas.getGraphics(connection) + }); + + dragging.move(canvasEvent({ x: 220, y: 200 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(211); + expect(Math.round(connection.label.y)).to.be.equal(152); + + })); + + + it('add bendpoint, label on left segment', inject(function(elementRegistry, bendpointMove, dragging, canvas) { + + // given + var connection = elementRegistry.get('SequenceFlow_A'); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1, true); + + dragging.hover({ + element: connection, + gfx: canvas.getGraphics(connection) + }); + + dragging.move(canvasEvent({ x: 260, y: 200 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(197); + expect(Math.round(connection.label.y)).to.be.equal(147); + + + })); + + + it('remove bendpoint when label on bendpoint', inject(function(elementRegistry, bendpointMove, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + // label out of segments, on a bendpoint + modeling.moveShape(connection.label, { x: 40, y: 0 }); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + + dragging.move(canvasEvent({ x: 455, y: 320 })); + + dragging.end(); + + // then + expect(Math.round(connection.label.x)).to.be.equal(426); + expect(Math.round(connection.label.y)).to.be.equal(287); + + })); + + + it('add benpoint, label on segment, should not move', inject(function(elementRegistry, bendpointMove, canvas, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + // label out of segments, on a bendpoint + modeling.moveShape(connection.label, { x: 40, y: -60 }); + var position = getLabelPosition(connection); + + // when + bendpointMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2, true); + + dragging.hover({ + element: connection, + gfx: canvas.getGraphics(connection) + }); + + dragging.move(canvasEvent({ x: 400, y: 350 })); + + dragging.end(); + + // then + expectLabelMoved(connection, position, { x: 0, y: 0 }); + + })); + + }); + + describe('special cases', function() { + + it('should behave properly, right after importing', inject(function(elementRegistry, connectionSegmentMove, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'), + labelPosition = getLabelPosition(connection), + label = connection.label; + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 0, y: 70 })); + + dragging.end(); + + // move label + modeling.moveShape(label, { x: 40, y: -30 }); + + // drag again + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 1); + + dragging.move(canvasEvent({ x: -20, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 20, y: 40 }); + + })); + + + it('should reposition on right segment', inject(function(elementRegistry, connectionSegmentMove, dragging) { + + // given + var connection = elementRegistry.get('SequenceFlow_E'), + labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: -100, y: 0 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: -45, y: -70 }); + + })); + + + describe.skip('label out of bounds', function() { + + + it('should not move label that is out of bounds', inject(function(elementRegistry, connectionSegmentMove, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + // move shape away + modeling.moveShape(connection.label, { x: 0, y: 140 }); + + var labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 0, y: 30 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 0, y: 0 }); + })); + + + it('should not move label that is out of bounds in corner', inject(function(elementRegistry, connectionSegmentMove, dragging, modeling) { + + // given + var connection = elementRegistry.get('SequenceFlow_C'); + + // move shape away + modeling.moveShape(connection.label, { x: 50, y: 0 }); + + var labelPosition = getLabelPosition(connection); + + // when + connectionSegmentMove.start(canvasEvent({ x: 0, y: 0 }), connection, 2); + + dragging.move(canvasEvent({ x: 0, y: 30 })); + + dragging.end(); + + // then + expectLabelMoved(connection, labelPosition, { x: 0, y: 0 }); + })); + + }); + + }); + + }); + +}); + + + +function getLabelPosition(connection) { + + var label = connection.label; + + var mid = { + x: label.x + (label.width / 2), + y: label.y + (label.height / 2) + }; + + return mid; +} + + +function expectLabelMoved(connection, oldPosition, expectedDelta) { + + var newPosition = getLabelPosition(connection); + + var delta = { + x: Math.round(newPosition.x - oldPosition.x), + y: Math.round(newPosition.y - oldPosition.y) + }; + + expect(delta).to.eql(expectedDelta); +} diff --git a/test/spec/features/modeling/behavior/util/GeometricUtilSpec.js b/test/spec/features/modeling/behavior/util/GeometricUtilSpec.js new file mode 100644 index 00000000..2ca14f7b --- /dev/null +++ b/test/spec/features/modeling/behavior/util/GeometricUtilSpec.js @@ -0,0 +1,127 @@ +'use strict'; + +require('../../../../../TestHelper'); + +var GeometricUtil = require('lib/features/modeling/behavior/util/GeometricUtil'); + + +describe('modeling/behavior/util - GeometricUtil', function() { + + it('should calculate right horizontal-line/point distance', function() { + + // given + var testData = [ + { point: { x: 2, y: 4 }, line: [ { x: 1, y: 1 }, { x: 4, y: 1 } ], distance: 3 }, + { point: { x: 2, y: 2 }, line: [ { x: 1, y: 1 }, { x: 1, y: 4 } ], distance: 1 }, + { point: { x: 0, y: 0 }, line: [ { x: 0, y: 4 }, { x: 4, y: 0 } ], distance: 3 } + ]; + + for (var i=0; i