feat(snapping): snap boundary events
This adds immediate feedback when creating and moving boundary events. * During move, boundary events are snapped, if attachment is allowed * Boundary events snap to their siblings and not to elements inside the host Closes #320
This commit is contained in:
parent
8e9beeaae2
commit
ece7b7d597
|
@ -14,7 +14,7 @@ var getParents = require('../ModelingUtil').getParents,
|
|||
|
||||
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
|
||||
|
||||
var isOutside = require('diagram-js/lib/features/snapping/SnapUtil').isOutside;
|
||||
var isBoundaryAttachment = require('../../snapping/BpmnSnappingUtil').getBoundaryAttachment;
|
||||
|
||||
/**
|
||||
* BPMN specific modeling rule
|
||||
|
@ -72,9 +72,10 @@ BpmnRules.prototype.init = function() {
|
|||
this.addRule('shapes.move', function(context) {
|
||||
|
||||
var target = context.target,
|
||||
shapes = context.shapes;
|
||||
shapes = context.shapes,
|
||||
position = context.position;
|
||||
|
||||
return canAttach(shapes, target) || canMove(shapes, target);
|
||||
return canAttach(shapes, target, null, position) || canMove(shapes, target, position);
|
||||
});
|
||||
|
||||
this.addRule([ 'shape.create', 'shape.append' ], function(context) {
|
||||
|
@ -363,7 +364,7 @@ function canAttach(elements, target, source, position) {
|
|||
}
|
||||
|
||||
// only attach to subprocess border
|
||||
if (position && is(target, 'bpmn:SubProcess') && isOutside(position, target, 0.125)) {
|
||||
if (position && !isBoundaryAttachment(position, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,14 @@ var Snapping = require('diagram-js/lib/features/snapping/Snapping'),
|
|||
var is = require('../../util/ModelUtil').is;
|
||||
|
||||
|
||||
var round = Math.round;
|
||||
|
||||
var mid = SnapUtil.mid,
|
||||
topLeft = SnapUtil.topLeft,
|
||||
bottomRight = SnapUtil.bottomRight,
|
||||
calcSnapEdges = SnapUtil.calcSnapEdges,
|
||||
closestEdge = SnapUtil.closestEdge;
|
||||
isSnapped = SnapUtil.isSnapped,
|
||||
setSnapped = SnapUtil.setSnapped,
|
||||
getBoundaryAttachment = require('./BpmnSnappingUtil').getBoundaryAttachment;
|
||||
|
||||
/**
|
||||
* BPMN specific snapping functionality
|
||||
|
@ -29,75 +32,15 @@ var mid = SnapUtil.mid,
|
|||
* @param {EventBus} eventBus
|
||||
* @param {Canvas} canvas
|
||||
*/
|
||||
function BpmnSnapping(eventBus, canvas) {
|
||||
function BpmnSnapping(eventBus, canvas, bpmnRules) {
|
||||
|
||||
// instantiate super
|
||||
Snapping.call(this, eventBus, canvas);
|
||||
|
||||
|
||||
/**
|
||||
* Drop participant on process <> process elements snapping
|
||||
*/
|
||||
|
||||
function initParticipantSnapping(context, shape, elements) {
|
||||
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var snapBox = getBoundingBox(elements.filter(function(e) {
|
||||
return !e.labelTarget && !e.waypoints;
|
||||
}));
|
||||
|
||||
snapBox.x -= 50;
|
||||
snapBox.y -= 20;
|
||||
snapBox.width += 70;
|
||||
snapBox.height += 40;
|
||||
|
||||
// adjust shape height to include bounding box
|
||||
shape.width = Math.max(shape.width, snapBox.width);
|
||||
shape.height = Math.max(shape.height, snapBox.height);
|
||||
|
||||
context.participantSnapBox = snapBox;
|
||||
}
|
||||
|
||||
function snapParticipant(snapBox, shape, event, offset) {
|
||||
offset = offset || 0;
|
||||
|
||||
var shapeHalfWidth = shape.width / 2 - offset,
|
||||
shapeHalfHeight = shape.height / 2;
|
||||
|
||||
var currentTopLeft = {
|
||||
x: event.x - shapeHalfWidth - offset,
|
||||
y: event.y - shapeHalfHeight
|
||||
};
|
||||
|
||||
var currentBottomRight = {
|
||||
x: event.x + shapeHalfWidth + offset,
|
||||
y: event.y + shapeHalfHeight
|
||||
};
|
||||
|
||||
var snapTopLeft = snapBox,
|
||||
snapBottomRight = bottomRight(snapBox);
|
||||
|
||||
if (currentTopLeft.x >= snapTopLeft.x) {
|
||||
event.x = snapTopLeft.x + offset + shapeHalfWidth;
|
||||
event.snapped = true;
|
||||
} else
|
||||
if (currentBottomRight.x <= snapBottomRight.x) {
|
||||
event.x = snapBottomRight.x - offset - shapeHalfWidth;
|
||||
event.snapped = true;
|
||||
}
|
||||
|
||||
if (currentTopLeft.y >= snapTopLeft.y) {
|
||||
event.y = snapTopLeft.y + shapeHalfHeight;
|
||||
event.snapped = true;
|
||||
} else
|
||||
if (currentBottomRight.y <= snapBottomRight.y) {
|
||||
event.y = snapBottomRight.y - shapeHalfHeight;
|
||||
event.snapped = true;
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.on('create.start', function(event) {
|
||||
|
||||
var context = event.context,
|
||||
|
@ -116,20 +59,54 @@ function BpmnSnapping(eventBus, canvas) {
|
|||
shape = context.shape,
|
||||
participantSnapBox = context.participantSnapBox;
|
||||
|
||||
if (!event.snapped && participantSnapBox) {
|
||||
if (!isSnapped(event) && participantSnapBox) {
|
||||
snapParticipant(participantSnapBox, shape, event);
|
||||
}
|
||||
});
|
||||
|
||||
eventBus.on([ 'create.end', 'shape.move.end' ], 1500, function(event) {
|
||||
var context = event.context;
|
||||
eventBus.on('shape.move.start', function(event) {
|
||||
|
||||
if (context.canExecute === 'attach') {
|
||||
snapBoundaryEvents(event);
|
||||
var context = event.context,
|
||||
shape = context.shape,
|
||||
rootElement = canvas.getRootElement();
|
||||
|
||||
// snap participant around existing elements (if any)
|
||||
if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
|
||||
initParticipantSnapping(context, shape, rootElement.children);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function canAttach(shape, target, position) {
|
||||
return bpmnRules.canAttach([ shape ], target, null, position) === 'attach';
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap boundary events to elements border
|
||||
*/
|
||||
eventBus.on([ 'create.move', 'create.end' ], 1500, function(event) {
|
||||
|
||||
var context = event.context,
|
||||
target = context.target,
|
||||
shape = context.shape;
|
||||
|
||||
if (target && !isSnapped(event) && canAttach(shape, target, event)) {
|
||||
snapBoundaryEvent(event, shape, target);
|
||||
}
|
||||
});
|
||||
|
||||
eventBus.on([ 'shape.move.move', 'shape.move.end' ], 1500, function(event) {
|
||||
|
||||
var context = event.context,
|
||||
target = context.target,
|
||||
shape = context.shape;
|
||||
|
||||
if (target && !isSnapped(event) && canAttach(shape, target, event)) {
|
||||
snapBoundaryEvent(event, shape, target);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
eventBus.on('resize.start', 1500, function(event) {
|
||||
var context = event.context,
|
||||
shape = context.shape;
|
||||
|
@ -155,88 +132,20 @@ function BpmnSnapping(eventBus, canvas) {
|
|||
|
||||
inherits(BpmnSnapping, Snapping);
|
||||
|
||||
BpmnSnapping.$inject = [ 'eventBus', 'canvas' ];
|
||||
BpmnSnapping.$inject = [ 'eventBus', 'canvas', 'bpmnRules' ];
|
||||
|
||||
module.exports = BpmnSnapping;
|
||||
|
||||
|
||||
function whichAxis(edge) {
|
||||
if (/top|bottom/.test(edge)) {
|
||||
return 'y';
|
||||
}
|
||||
if (/left|right/.test(edge)) {
|
||||
return 'x';
|
||||
}
|
||||
}
|
||||
|
||||
function snapBoundaryEvents(event) {
|
||||
var context = event.context,
|
||||
target = context.target;
|
||||
|
||||
var snapLocationPoints = {
|
||||
'top': target.y,
|
||||
'bottom': target.y + target.height,
|
||||
'left': target.x,
|
||||
'right': target.x + target.width,
|
||||
};
|
||||
|
||||
var bounds = {
|
||||
x: event.x,
|
||||
y: event.y
|
||||
};
|
||||
|
||||
var edge = closestEdge(bounds, target);
|
||||
|
||||
var axis = whichAxis(edge);
|
||||
|
||||
var snapping = {};
|
||||
|
||||
var locationSnapping = snapLocationPoints[edge];
|
||||
|
||||
snapping[axis] = locationSnapping;
|
||||
|
||||
// adjust event { x, y, dx, dy } and mark as snapping
|
||||
var cx, cy;
|
||||
|
||||
if (snapping.x) {
|
||||
|
||||
cx = event.x - snapping.x;
|
||||
|
||||
event.x = snapping.x;
|
||||
event.dx = event.dx - cx;
|
||||
|
||||
if (context.delta) {
|
||||
cx = (snapping.x - 18) - event.shape.x;
|
||||
|
||||
context.delta.x = cx;
|
||||
}
|
||||
|
||||
event.snapped = true;
|
||||
}
|
||||
|
||||
if (snapping.y) {
|
||||
cy = event.y - snapping.y;
|
||||
|
||||
event.y = snapping.y;
|
||||
event.dy = event.dy - cy;
|
||||
|
||||
if (context.delta) {
|
||||
cy = (snapping.y - 18) - event.shape.y;
|
||||
|
||||
context.delta.y = cy;
|
||||
}
|
||||
|
||||
event.snapped = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BpmnSnapping.prototype.initSnap = function(event) {
|
||||
|
||||
var context = event.context,
|
||||
shape = event.shape,
|
||||
snapContext,
|
||||
snapEdges;
|
||||
shapeMid,
|
||||
shapeBounds,
|
||||
shapeTopLeft,
|
||||
shapeBottomRight,
|
||||
snapContext;
|
||||
|
||||
|
||||
snapContext = Snapping.prototype.initSnap.call(this, event);
|
||||
|
@ -246,12 +155,29 @@ BpmnSnapping.prototype.initSnap = function(event) {
|
|||
snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]);
|
||||
}
|
||||
|
||||
|
||||
if (shape) {
|
||||
|
||||
snapEdges = calcSnapEdges(event, shape);
|
||||
shapeMid = mid(shape, event);
|
||||
|
||||
forEach(snapEdges, function(bounds, location) {
|
||||
snapContext.setSnapOrigin(location, bounds);
|
||||
shapeBounds = {
|
||||
width: shape.width,
|
||||
height: shape.height,
|
||||
x: isNaN(shape.x) ? round(shapeMid.x - shape.width / 2) : shape.x,
|
||||
y: isNaN(shape.y) ? round(shapeMid.y - shape.height / 2) : shape.y,
|
||||
};
|
||||
|
||||
shapeTopLeft = topLeft(shapeBounds);
|
||||
shapeBottomRight = bottomRight(shapeBounds);
|
||||
|
||||
snapContext.setSnapOrigin('top-left', {
|
||||
x: shapeTopLeft.x - event.x,
|
||||
y: shapeTopLeft.y - event.y
|
||||
});
|
||||
|
||||
snapContext.setSnapOrigin('bottom-right', {
|
||||
x: shapeBottomRight.x - event.x,
|
||||
y: shapeBottomRight.y - event.y
|
||||
});
|
||||
|
||||
forEach(shape.outgoing, function(c) {
|
||||
|
@ -288,12 +214,18 @@ BpmnSnapping.prototype.initSnap = function(event) {
|
|||
|
||||
BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
|
||||
|
||||
var siblings = this.getSiblings(shape, target);
|
||||
// use target parent as snap target
|
||||
if (is(shape, 'bpmn:BoundaryEvent')) {
|
||||
target = target.parent;
|
||||
}
|
||||
|
||||
// add sequence flow parents as snap targets
|
||||
if (is(target, 'bpmn:SequenceFlow')) {
|
||||
this.addTargetSnaps(snapPoints, shape, target.parent);
|
||||
}
|
||||
|
||||
var siblings = this.getSiblings(shape, target) || [];
|
||||
|
||||
forEach(siblings, function(s) {
|
||||
snapPoints.add('mid', mid(s));
|
||||
|
||||
|
@ -303,14 +235,15 @@ BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
forEach(shape.incoming, function(c) {
|
||||
|
||||
if (siblings.indexOf(c.source) === -1) {
|
||||
snapPoints.add('mid', mid(c.source));
|
||||
}
|
||||
|
||||
var docking = c.waypoints[0];
|
||||
snapPoints.add(c.id + '-docking', docking.original || docking);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -318,10 +251,95 @@ BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
|
|||
|
||||
if (siblings.indexOf(c.target) === -1) {
|
||||
snapPoints.add('mid', mid(c.target));
|
||||
}
|
||||
|
||||
var docking = c.waypoints[c.waypoints.length - 1];
|
||||
snapPoints.add(c.id + '-docking', docking.original || docking);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
/////// participant snapping //////////////////
|
||||
|
||||
function initParticipantSnapping(context, shape, elements) {
|
||||
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var snapBox = getBoundingBox(elements.filter(function(e) {
|
||||
return !e.labelTarget && !e.waypoints;
|
||||
}));
|
||||
|
||||
snapBox.x -= 50;
|
||||
snapBox.y -= 20;
|
||||
snapBox.width += 70;
|
||||
snapBox.height += 40;
|
||||
|
||||
// adjust shape height to include bounding box
|
||||
shape.width = Math.max(shape.width, snapBox.width);
|
||||
shape.height = Math.max(shape.height, snapBox.height);
|
||||
|
||||
context.participantSnapBox = snapBox;
|
||||
}
|
||||
|
||||
function snapParticipant(snapBox, shape, event, offset) {
|
||||
offset = offset || 0;
|
||||
|
||||
var shapeHalfWidth = shape.width / 2 - offset,
|
||||
shapeHalfHeight = shape.height / 2;
|
||||
|
||||
var currentTopLeft = {
|
||||
x: event.x - shapeHalfWidth - offset,
|
||||
y: event.y - shapeHalfHeight
|
||||
};
|
||||
|
||||
var currentBottomRight = {
|
||||
x: event.x + shapeHalfWidth + offset,
|
||||
y: event.y + shapeHalfHeight
|
||||
};
|
||||
|
||||
var snapTopLeft = snapBox,
|
||||
snapBottomRight = bottomRight(snapBox);
|
||||
|
||||
if (currentTopLeft.x >= snapTopLeft.x) {
|
||||
setSnapped(event, 'x', snapTopLeft.x + offset + shapeHalfWidth);
|
||||
} else
|
||||
if (currentBottomRight.x <= snapBottomRight.x) {
|
||||
setSnapped(event, 'x', snapBottomRight.x - offset - shapeHalfWidth);
|
||||
}
|
||||
|
||||
if (currentTopLeft.y >= snapTopLeft.y) {
|
||||
setSnapped(event, 'y', snapTopLeft.y + shapeHalfHeight);
|
||||
} else
|
||||
if (currentBottomRight.y <= snapBottomRight.y) {
|
||||
setSnapped(event, 'y', snapBottomRight.y - shapeHalfHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////// boundary event snapping /////////////////////////
|
||||
|
||||
|
||||
var LayoutUtil = require('diagram-js/lib/layout/LayoutUtil');
|
||||
|
||||
|
||||
function snapBoundaryEvent(event, shape, target) {
|
||||
var targetTRBL = LayoutUtil.asTRBL(target);
|
||||
|
||||
var direction = getBoundaryAttachment(event, target);
|
||||
|
||||
if (/top/.test(direction)) {
|
||||
setSnapped(event, 'y', targetTRBL.top);
|
||||
} else
|
||||
if (/bottom/.test(direction)) {
|
||||
setSnapped(event, 'y', targetTRBL.bottom);
|
||||
}
|
||||
|
||||
if (/left/.test(direction)) {
|
||||
setSnapped(event, 'x', targetTRBL.left);
|
||||
} else
|
||||
if (/right/.test(direction)) {
|
||||
setSnapped(event, 'x', targetTRBL.right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var getOrientation = require('diagram-js/lib/layout/LayoutUtil').getOrientation;
|
||||
|
||||
|
||||
module.exports.getBoundaryAttachment = function(position, targetBounds) {
|
||||
|
||||
var orientation = getOrientation(position, targetBounds, -15);
|
||||
|
||||
if (orientation !== 'intersect') {
|
||||
return orientation;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -659,7 +659,7 @@ describe('features/modeling/rules - BpmnRules', function() {
|
|||
}));
|
||||
|
||||
|
||||
it('attach IntermediateEvent to SubProcess border', inject(function(elementFactory, elementRegistry, bpmnRules) {
|
||||
it('attach IntermediateEvent to SubProcess inner', inject(function(elementFactory, elementRegistry, bpmnRules) {
|
||||
|
||||
// given
|
||||
var subProcessElement = elementRegistry.get('SubProcess_1');
|
||||
|
@ -669,8 +669,30 @@ describe('features/modeling/rules - BpmnRules', function() {
|
|||
});
|
||||
|
||||
var position = {
|
||||
x: eventShape.x,
|
||||
y: eventShape.y
|
||||
x: subProcessElement.x + subProcessElement.width / 2,
|
||||
y: subProcessElement.y + subProcessElement.height / 2
|
||||
};
|
||||
|
||||
// when
|
||||
var canAttach = bpmnRules.canAttach([ eventShape ], subProcessElement, null, position);
|
||||
|
||||
// then
|
||||
expect(canAttach).to.equal(false);
|
||||
}));
|
||||
|
||||
|
||||
it('attach IntermediateEvent to SubProcess border', inject(function(elementFactory, elementRegistry, bpmnRules) {
|
||||
|
||||
// given
|
||||
var subProcessElement = elementRegistry.get('SubProcess_1');
|
||||
var eventShape = elementFactory.createShape({
|
||||
type: 'bpmn:IntermediateThrowEvent',
|
||||
x: 0, y: 0
|
||||
});
|
||||
|
||||
var position = {
|
||||
x: subProcessElement.x + subProcessElement.width / 2,
|
||||
y: subProcessElement.y + subProcessElement.height
|
||||
};
|
||||
|
||||
// when
|
||||
|
|
Loading…
Reference in New Issue