feat(modeling): move labels with connections

* move on segment dragging
* move on reconnect
* move on bendpoint add / remove

Closes #331
This commit is contained in:
Jan Stümmel 2016-04-22 15:44:56 +02:00 committed by Nico Rehwaldt
parent 713021ecff
commit fd198b6059
10 changed files with 1585 additions and 48 deletions

View File

@ -4,12 +4,14 @@ var assign = require('lodash/object/assign'),
inherits = require('inherits'); inherits = require('inherits');
var LabelUtil = require('../../../util/LabelUtil'), var LabelUtil = require('../../../util/LabelUtil'),
LabelLayoutUtil = require('./util/LabelLayoutUtil'),
ModelUtil = require('../../../util/ModelUtil'), ModelUtil = require('../../../util/ModelUtil'),
is = ModelUtil.is, is = ModelUtil.is,
getBusinessObject = ModelUtil.getBusinessObject; getBusinessObject = ModelUtil.getBusinessObject;
var hasExternalLabel = LabelUtil.hasExternalLabel, var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelMid = LabelUtil.getExternalLabelMid; getExternalLabelMid = LabelUtil.getExternalLabelMid,
getLabelAdjustment = LabelLayoutUtil.getLabelAdjustment;
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor'); 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) { this.postExecute([ 'shape.replace' ], function(e) {
var context = e.context, var context = e.context,
newShape = context.newShape, newShape = context.newShape,

View File

@ -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;

View File

@ -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<Point>} newWaypoints
* @param {Array<Point>} 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;
}

View File

@ -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<Point>} 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<Point>} 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;
}

View File

@ -0,0 +1,99 @@
<?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:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_A</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_1" name="1">
<bpmn:incoming>SequenceFlow_A</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_B</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_A" name="A" sourceRef="StartEvent_1" targetRef="Task_1" />
<bpmn:task id="Task_3" name="3">
<bpmn:incoming>SequenceFlow_B</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_C</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_D</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_B" name="B" sourceRef="Task_1" targetRef="Task_3" />
<bpmn:task id="Task_2" name="2">
<bpmn:incoming>SequenceFlow_C</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_C" name="C" sourceRef="Task_3" targetRef="Task_2" />
<bpmn:task id="Task_4" name="4">
<bpmn:incoming>SequenceFlow_D</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_E</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_D" name="D" sourceRef="Task_3" targetRef="Task_4" />
<bpmn:endEvent id="EndEvent_1">
<bpmn:incoming>SequenceFlow_E</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_E" name="E" sourceRef="Task_4" targetRef="EndEvent_1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="307" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_A_di" bpmnElement="SequenceFlow_A">
<di:waypoint xsi:type="dc:Point" x="209" y="120" />
<di:waypoint xsi:type="dc:Point" x="307" y="120" />
<bpmndi:BPMNLabel>
<dc:Bounds x="213" y="95" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_3_di" bpmnElement="Task_3">
<dc:Bounds x="504" y="235" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_B_di" bpmnElement="SequenceFlow_B">
<di:waypoint xsi:type="dc:Point" x="407" y="120" />
<di:waypoint xsi:type="dc:Point" x="455" y="120" />
<di:waypoint xsi:type="dc:Point" x="455" y="275" />
<di:waypoint xsi:type="dc:Point" x="504" y="275" />
<bpmndi:BPMNLabel>
<dc:Bounds x="425" y="175" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="294" y="260" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_C_di" bpmnElement="SequenceFlow_C">
<di:waypoint xsi:type="dc:Point" x="554" y="315" />
<di:waypoint xsi:type="dc:Point" x="554" y="385" />
<di:waypoint xsi:type="dc:Point" x="344" y="385" />
<di:waypoint xsi:type="dc:Point" x="344" y="340" />
<bpmndi:BPMNLabel>
<dc:Bounds x="486" y="392" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_4_di" bpmnElement="Task_4">
<dc:Bounds x="717" y="353" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_D_di" bpmnElement="SequenceFlow_D">
<di:waypoint xsi:type="dc:Point" x="602" y="243" />
<di:waypoint xsi:type="dc:Point" x="716" y="166" />
<di:waypoint xsi:type="dc:Point" x="661" y="393" />
<di:waypoint xsi:type="dc:Point" x="717" y="393" />
<bpmndi:BPMNLabel>
<dc:Bounds x="626.5" y="201.5" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0za5rat_di" bpmnElement="EndEvent_1">
<dc:Bounds x="886" y="175" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="859" y="211" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1ni843l_di" bpmnElement="SequenceFlow_E">
<di:waypoint xsi:type="dc:Point" x="817" y="393" />
<di:waypoint xsi:type="dc:Point" x="904" y="393" />
<di:waypoint xsi:type="dc:Point" x="904" y="211" />
<bpmndi:BPMNLabel>
<dc:Bounds x="794" y="372.5" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -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);
}

View File

@ -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<testData.length; i++) {
// when
var d = GeometricUtil.getDistancePointLine(testData[i].point, testData[i].line);
// then
expect(Math.round(d)).to.be.equal(testData[i].distance);
}
});
it('should calculate right perpendicular foot', function() {
// given
var testData = [
{ point: { x: 2, y: 2 }, line: [ { x: 1, y: 1 }, { x: 1, y: 3 } ], foot: { x: 1, y: 2 } },
{ point: { x: 2, y: 4 }, line: [ { x: 1, y: 1 }, { x: 4, y: 1 } ], foot: { x: 2, y: 1 } }
];
for (var i=0; i<testData.length; i++) {
// when
var foot = GeometricUtil.perpendicularFoot(testData[i].point, testData[i].line);
// then
expect(rounded(foot)).to.be.eql(testData[i].foot);
}
});
it('should calculate right distance', function() {
var testData = [
{ p1: { x: 1, y: 1 }, p2: { x: 5, y: 1 }, distance: 4 },
{ p1: { x: 1, y: 1 }, p2: { x: 1, y: 5 }, distance: 4 },
{ p1: { x: 1, y: 1 }, p2: { x: 5, y: 5 }, distance: 6 },
{ p1: { x: 1, y: 1 }, p2: { x: -5, y: -5 }, distance: 8 }
];
for (var i=0; i<testData.length; i++) {
// when
var d = GeometricUtil.getDistancePointPoint(testData[i].p1, testData[i].p2);
// then
expect(Math.round(d)).to.be.eql(testData[i].distance);
}
});
it('should calculate right line angle', function() {
// given
var testLines = [
{ line: [ { x: 0, y: 0 }, { x: 10, y: 10 } ], angle: 45 },
{ line: [ { x: 0, y: 0 }, { x: 0, y: 10 } ], angle: 90 },
{ line: [ { x: 0, y: 0 }, { x: -10, y: 10 } ], angle: -45 },
{ line: [ { x: 0, y: 0 }, { x: 10, y: 0 } ], angle: 0 },
{ line: [ { x: 0, y: 0 }, { x: 0, y: -10 } ], angle: -90 },
{ line: [ { x: 0, y: 0 }, { x: -10, y: 0 } ], angle: 0 }
];
for (var i=0; i<testLines.length; i++) {
// when
var angle = GeometricUtil.getAngle(testLines[i].line);
// to degree
angle = angle * ( 180 / Math.PI );
//then
expect(angle).to.be.equal(testLines[i].angle);
}
});
it('should rotate vector', function() {
// given
var testVectors = [
// x=-10 because we have system with flipped y axis
{ vector: { x: 0, y: 10 }, angle: 90, rotated: { x: -10, y: 0 } },
{ vector: { x: 10, y: 0 }, angle: 90, rotated: { x: 0, y: 10 } },
{ vector: { x: 10, y: 0 }, angle: 45, rotated: { x: 7, y: 7 } }
];
for (var i=0; i<testVectors.length; i++) {
// degree to radian
var angle = testVectors[i].angle * ( Math.PI / 180 );
// when
var rotatedVector = GeometricUtil.rotateVector(testVectors[i].vector, angle);
// then
expect(rounded(rotatedVector)).to.be.eql(testVectors[i].rotated);
}
});
});
function rounded(v) {
return {
x: Math.round(v.x),
y: Math.round(v.y)
};
}

View File

@ -0,0 +1,189 @@
'use strict';
var getAttachment = require('lib/features/modeling/behavior/util/LineAttachmentUtil').getAttachment;
describe('modeling/behavior/util - LineAttachmentUtil#getAttachment', function() {
// test line
//
// *--*
// |
// *
// \
// *
//
var line = [
{ x: 10, y: 10 },
// -
{ x: 30, y: 10 },
// |
{ x: 30, y: 30 },
// \
{ x: 130, y: 130 }
];
describe('should recognize segment', function() {
it('horizontal', function() {
// when
var attachment = getAttachment({ x: 20, y: 5 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 20, y: 10 },
segmentIndex: 0,
relativeLocation: 0.5
});
});
it('horizontal (on line)', function() {
// when
var attachment = getAttachment({ x: 20, y: 10 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 20, y: 10 },
segmentIndex: 0,
relativeLocation: 0.5
});
});
it('vertical', function() {
// when
var attachment = getAttachment({ x: 40, y: 20 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 30, y: 20 },
segmentIndex: 1,
relativeLocation: 0.5
});
});
it('diagonal', function() {
// when
var attachment = getAttachment({ x: 30, y: 40 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 35, y: 35 },
segmentIndex: 2,
relativeLocation: 0.05
});
});
it('diagonal (on line)', function() {
// when
var attachment = getAttachment({ x: 50, y: 50 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 50, y: 50 },
segmentIndex: 2,
relativeLocation: 0.2
});
});
it('horizontal (conflicting with vertical)', function() {
// when
var attachment = getAttachment({ x: 25, y: 15 }, line);
// then
expect(attachment).to.eql({
type: 'segment',
position: { x: 25, y: 10 },
segmentIndex: 0,
relativeLocation: 0.75
});
});
});
describe('should recognize bendpoint', function() {
it('horizontal', function() {
// when
var attachment = getAttachment({ x: 35, y: 5 }, line);
// then
expect(attachment).to.eql({
type: 'bendpoint',
position: { x: 30, y: 10 },
bendpointIndex: 1,
segmentIndex: 0
});
});
it('horizontal (segment start)', function() {
// when
var attachment = getAttachment({ x: 5, y: 5 }, line);
// then
expect(attachment).to.eql({
type: 'bendpoint',
position: { x: 10, y: 10 },
bendpointIndex: 0,
segmentIndex: 0
});
});
it('vertical', function() {
// when
var attachment = getAttachment({ x: 35, y: 10 }, line);
// then
expect(attachment).to.eql({
type: 'bendpoint',
position: { x: 30, y: 10 },
bendpointIndex: 1,
segmentIndex: 0
});
});
it('vertical (segment start)', function() {
var otherLine = [
{ x: 10, y: 10 },
{ x: 10, y: 50 }
];
// when
var attachment = getAttachment({ x: 5, y: 5 }, otherLine);
// then
expect(attachment).to.eql({
type: 'bendpoint',
position: { x: 10, y: 10 },
bendpointIndex: 0,
segmentIndex: 0
});
});
});
});

View File

@ -1,47 +0,0 @@
'use strict';
require('../../TestHelper');
/* global bootstrapModeler, inject */
var coreModule = require('../../../lib/core'),
modelingModule = require('../../../lib/features/modeling');
describe('LabelUtil', function() {
var diagramXML = require('./LabelUtil.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: [ coreModule, modelingModule ] }));
it('should correctly place horizontal label', 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('should correctly place vertical label', 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);
}));
});