diff --git a/lib/features/modeling/behavior/LabelBehavior.js b/lib/features/modeling/behavior/LabelBehavior.js index 15cd1e93..725c68ff 100644 --- a/lib/features/modeling/behavior/LabelBehavior.js +++ b/lib/features/modeling/behavior/LabelBehavior.js @@ -12,6 +12,7 @@ import { import { isLabelExternal, getExternalLabelMid, + hasExternalLabel } from '../../../util/LabelUtil'; import { @@ -20,6 +21,28 @@ import { import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; +import { + getNewAttachPoint +} from 'diagram-js/lib/util/AttachUtil'; + +import { + getMid, + roundPoint +} from 'diagram-js/lib/layout/LayoutUtil'; + +import { + delta +} from 'diagram-js/lib/util/PositionUtil'; + +import { + sortBy +} from 'min-dash'; + +import { + getDistancePointLine, + perpendicularFoot +} from './util/GeometricUtil'; + var DEFAULT_LABEL_DIMENSIONS = { width: 90, height: 20 @@ -207,6 +230,32 @@ export default function LabelBehavior( } }); + + // move external label after resizing + this.postExecute('shape.resize', function(event) { + + var context = event.context, + shape = context.shape, + newBounds = context.newBounds, + oldBounds = context.oldBounds; + + if (hasExternalLabel(shape)) { + + var label = shape.label, + labelMid = getMid(label), + edges = asEdges(oldBounds); + + // get nearest border point to label as reference point + var referencePoint = getReferencePoint(labelMid, edges); + + var delta = getReferencePointDelta(referencePoint, oldBounds, newBounds); + + modeling.moveShape(label, delta); + + } + + }); + } inherits(LabelBehavior, CommandInterceptor); @@ -216,4 +265,116 @@ LabelBehavior.$inject = [ 'modeling', 'bpmnFactory', 'textRenderer' -]; \ No newline at end of file +]; + +// helpers ////////////////////// + +/** + * Calculates a reference point delta relative to a new position + * of a certain element's bounds + * + * @param {Point} point + * @param {Bounds} oldBounds + * @param {Bounds} newBounds + * + * @return {Delta} delta + */ +export function getReferencePointDelta(referencePoint, oldBounds, newBounds) { + + var newReferencePoint = getNewAttachPoint(referencePoint, oldBounds, newBounds); + + return roundPoint(delta(newReferencePoint, referencePoint)); +} + +/** + * Generates the nearest point (reference point) for a given point + * onto given set of lines + * + * @param {Array} lines + * @param {Point} point + * + * @param {Point} + */ +export function getReferencePoint(point, lines) { + + if (!lines.length) { + return; + } + + var nearestLine = getNearestLine(point, lines); + + return perpendicularFoot(point, nearestLine); +} + +/** + * Convert the given bounds to a lines array containing all edges + * + * @param {Bounds|Point} bounds + * + * @return Array + */ +export function asEdges(bounds) { + return [ + [ // top + { + x: bounds.x, + y: bounds.y + }, + { + x: bounds.x + (bounds.width || 0), + y: bounds.y + } + ], + [ // right + { + x: bounds.x + (bounds.width || 0), + y: bounds.y + }, + { + x: bounds.x + (bounds.width || 0), + y: bounds.y + (bounds.height || 0) + } + ], + [ // bottom + { + x: bounds.x, + y: bounds.y + (bounds.height || 0) + }, + { + x: bounds.x + (bounds.width || 0), + y: bounds.y + (bounds.height || 0) + } + ], + [ // left + { + x: bounds.x, + y: bounds.y + }, + { + x: bounds.x, + y: bounds.y + (bounds.height || 0) + } + ] + ]; +} + +/** + * Returns the nearest line for a given point by distance + * @param {Point} point + * @param Array lines + * + * @return Array + */ +function getNearestLine(point, lines) { + + var distances = lines.map(function(l) { + return { + line: l, + distance: getDistancePointLine(point, l) + }; + }); + + var sorted = sortBy(distances, 'distance'); + + return sorted[0].line; +} \ No newline at end of file diff --git a/test/spec/features/modeling/behavior/LabelBehavior.bpmn b/test/spec/features/modeling/behavior/LabelBehavior.bpmn index 9f4a13b2..c8e4f239 100644 --- a/test/spec/features/modeling/behavior/LabelBehavior.bpmn +++ b/test/spec/features/modeling/behavior/LabelBehavior.bpmn @@ -1,5 +1,8 @@ - + + + + @@ -8,6 +11,11 @@ foo + + + + + @@ -30,6 +38,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/LabelBehaviorSpec.js b/test/spec/features/modeling/behavior/LabelBehaviorSpec.js index 8a2bf224..47821346 100644 --- a/test/spec/features/modeling/behavior/LabelBehaviorSpec.js +++ b/test/spec/features/modeling/behavior/LabelBehaviorSpec.js @@ -5,6 +5,14 @@ import { inject } from 'test/TestHelper'; +import { + resizeBounds +} from 'diagram-js/lib/features/resize/ResizeUtil'; + +import { + pick +} from 'min-dash'; + import modelingModule from 'lib/features/modeling'; import coreModule from 'lib/core'; @@ -370,4 +378,251 @@ describe('behavior - LabelBehavior', function() { }); + + describe('resize label target', function() { + + describe('should move label (outside)', function() { + + it('to the top', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_2'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'se', { + x: 0, + y: -50 + }) + ); + + // then + expect(label.x).to.equal(labelBounds.x); + expect(label.y).to.be.below(labelBounds.y); + + })); + + + it('to the right', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_1'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'se', { + x: 50, + y: 0 + }) + ); + + // then + expect(label.x).to.be.above(labelBounds.x); + expect(label.y).to.equal(labelBounds.y); + + })); + + + it('to the bottom', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_2'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'se', { + x: 0, + y: 50 + }) + ); + + // then + expect(label.x).to.equal(labelBounds.x); + expect(label.y).to.be.above(labelBounds.y); + + })); + + + it('to the left', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_3'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'sw', { + x: -50, + y: 0 + }) + ); + + // then + expect(label.x).to.be.below(labelBounds.x); + expect(label.y).to.equal(labelBounds.y); + + })); + + + it('NOT if reference point not affected', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_2'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'ne', { + x: 0, + y: -50 + }) + ); + + // then + expect(getBounds(label)).to.eql(labelBounds); + + })); + + }); + + + describe('should move label (inside)', function() { + + it('to the top', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_4'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'nw', { + x: 0, + y: -50 + }) + ); + + // then + expect(label.x).to.equal(labelBounds.x); + expect(label.y).to.be.below(labelBounds.y); + + })); + + + it('to the right', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_4'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'ne', { + x: 50, + y: 0 + }) + ); + + // then + expect(label.x).to.be.above(labelBounds.x); + expect(label.y).to.equal(labelBounds.y); + + })); + + + it('to the bottom', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_5'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'sw', { + x: 0, + y: 50 + }) + ); + + // then + expect(label.x).to.equal(labelBounds.x); + expect(label.y).to.be.above(labelBounds.y); + + })); + + + + it('to the left', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_4'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'sw', { + x: -50, + y: 0 + }) + ); + + // then + expect(label.x).to.be.below(labelBounds.x); + expect(label.y).to.equal(labelBounds.y); + + })); + + + it('NOT if reference point not affected', inject(function(elementRegistry, modeling) { + + // given + var groupShape = elementRegistry.get('Group_4'), + label = groupShape.label, + labelBounds = getBounds(label); + + // when + modeling.resizeShape( + groupShape, + resizeBounds(groupShape, 'se', { + x: 0, + y: 50 + }) + ); + + // then + expect(getBounds(label)).to.eql(labelBounds); + + })); + + }); + + }); + }); + +// helper ////////// + +function getBounds(element) { + return pick(element, [ 'x', 'y', 'width', 'height' ]); +}