feat(grid-snapping): integrate with connection layout

Closes #1010
Related to #973
This commit is contained in:
Philipp Fromme 2019-05-02 09:55:11 +02:00 committed by Maciej Barelkowski
parent e4fe8c239b
commit 13f1e05ee7
4 changed files with 341 additions and 1 deletions

View File

@ -0,0 +1,133 @@
import inherits from 'inherits';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import { pointsAligned } from 'diagram-js/lib/util/Geometry';
var HIGH_PRIORITY = 3000;
/**
* Snaps connections with Manhattan layout.
*/
export default function LayoutConnectionBehavior(eventBus, gridSnapping, modeling) {
CommandInterceptor.call(this, eventBus);
this._gridSnapping = gridSnapping;
var self = this;
this.postExecuted([
'connection.create',
'connection.layout'
], HIGH_PRIORITY, function(event) {
var context = event.context,
connection = context.connection,
waypoints = connection.waypoints;
if (hasMiddleSegments(waypoints)) {
modeling.updateProperties(connection, {
waypoints: self.snapMiddleSegments(waypoints)
});
}
});
}
LayoutConnectionBehavior.$inject = [
'eventBus',
'gridSnapping',
'modeling'
];
inherits(LayoutConnectionBehavior, CommandInterceptor);
/**
* Snap middle segments of a given connection.
*
* @param {Array<Point>} waypoints
*
* @returns {Array<Point>}
*/
LayoutConnectionBehavior.prototype.snapMiddleSegments = function(waypoints) {
var gridSnapping = this._gridSnapping;
var middleSegments = getMiddleSegments(waypoints);
middleSegments.forEach(function(middleSegment) {
var segmentStart = middleSegment.start,
segmentEnd = middleSegment.end;
var aligned = pointsAligned(segmentStart, segmentEnd);
if (horizontallyAligned(aligned)) {
// snap horizontally
segmentStart.x = segmentEnd.x = gridSnapping.snapValue(segmentStart.x);
}
if (verticallyAligned(aligned)) {
// snap vertically
segmentStart.y = segmentEnd.y = gridSnapping.snapValue(segmentStart.y);
}
});
return waypoints;
};
// helpers //////////
/**
* Check wether a connection has a middle segments.
*
* @param {Array} waypoints
*
* @returns {boolean}
*/
function hasMiddleSegments(waypoints) {
return waypoints.length > 3;
}
/**
* Check wether an alignment is horizontal.
*
* @param {string} aligned
*
* @returns {boolean}
*/
function horizontallyAligned(aligned) {
return aligned === 'h';
}
/**
* Check wether an alignment is vertical.
*
* @param {string} aligned
*
* @returns {boolean}
*/
function verticallyAligned(aligned) {
return aligned === 'v';
}
/**
* Get middle segments from a given connection.
*
* @param {Array} waypoints
*
* @returns {Array}
*/
function getMiddleSegments(waypoints) {
var middleSegments = [];
for (var i = 1; i < waypoints.length - 2; i++) {
middleSegments.push({
start: waypoints[i],
end: waypoints[i + 1]
});
}
return middleSegments;
}

View File

@ -1,8 +1,11 @@
import AutoPlaceBehavior from './AutoPlaceBehavior';
import LayoutConnectionBehavior from './LayoutConnectionBehavior';
export default {
__init__: [
'gridSnappingLayoutConnectionBehavior',
'gridSnappingAutoPlaceBehavior'
],
gridSnappingAutoPlaceBehavior: [ 'type', AutoPlaceBehavior ]
gridSnappingAutoPlaceBehavior: [ 'type', AutoPlaceBehavior ],
gridSnappingLayoutConnectionBehavior: [ 'type', LayoutConnectionBehavior ]
};

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0">
<bpmn:process id="Process_1" isExecutable="true">
<bpmn:task id="Task_1" />
<bpmn:task id="Task_2" />
<bpmn:task id="Task_3" />
<bpmn:task id="Task_4" />
<bpmn:boundaryEvent id="BoundaryEvent_1" attachedToRef="Task_3" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="100" y="100" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="300" y="200" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_3_di" bpmnElement="Task_3">
<dc:Bounds x="100" y="400" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_4_di" bpmnElement="Task_4">
<dc:Bounds x="300" y="400" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1_di" bpmnElement="BoundaryEvent_1">
<dc:Bounds x="132" y="462" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,175 @@
import {
bootstrapModeler,
inject
} from 'test/TestHelper';
import coreModule from 'lib/core';
import gridSnappingModule from 'lib/features/grid-snapping';
import modelingModule from 'lib/features/modeling';
import moveModule from 'diagram-js/lib/features/move';
describe('features/grid-snapping - layout connection', function() {
var diagramXML = require('./LayoutConnectionBehavior.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: [
coreModule,
gridSnappingModule,
modelingModule,
moveModule
]
}));
describe('on connection create', function() {
it('should snap 3 segment connection (1 middle segment)', inject(
function(elementRegistry, modeling) {
// given
var task1 = elementRegistry.get('Task_1'),
task2 = elementRegistry.get('Task_2');
// when
var connection = modeling.connect(task1, task2);
// then
expect(connection.waypoints[1]).to.eql({ x: 250, y: 140 });
expect(connection.waypoints[2]).to.eql({ x: 250, y: 240 });
})
);
it('should snap 4 segment connection (2 middle segments)', inject(
function(elementRegistry, modeling) {
// given
var boundaryEvent1 = elementRegistry.get('BoundaryEvent_1'),
task4 = elementRegistry.get('Task_4');
// when
var connection = modeling.connect(boundaryEvent1, task4);
// then
expect(connection.waypoints[1]).to.eql({ x: 150, y: 520 });
expect(connection.waypoints[2]).to.eql({ x: 230, y: 520 });
expect(connection.waypoints[3]).to.eql({ x: 230, y: 440 });
}
));
});
describe('on connection layout', function() {
describe('should snap 3 segment connection (1 middle segment)', function() {
var connection;
beforeEach(inject(function(elementRegistry, modeling) {
// given
var task1 = elementRegistry.get('Task_1'),
task2 = elementRegistry.get('Task_2');
connection = modeling.connect(task1, task2);
// when
modeling.moveElements([ task2 ], { x: 50, y: 50 });
}));
it('should do', function() {
// then
expect(connection.waypoints[1]).to.eql({ x: 250, y: 140 });
expect(connection.waypoints[2]).to.eql({ x: 250, y: 290 });
});
it('should undo', inject(function(commandStack) {
// when
commandStack.undo();
// then
expect(connection.waypoints[1]).to.eql({ x: 250, y: 140 });
expect(connection.waypoints[2]).to.eql({ x: 250, y: 240 });
}));
it('should redo', inject(function(commandStack) {
// given
commandStack.undo();
// when
commandStack.redo();
// then
expect(connection.waypoints[1]).to.eql({ x: 250, y: 140 });
expect(connection.waypoints[2]).to.eql({ x: 250, y: 290 });
}));
});
describe('should snap 4 segment connection (2 middle segments)', function() {
var connection;
beforeEach(inject(function(elementRegistry, modeling) {
// given
var boundaryEvent1 = elementRegistry.get('BoundaryEvent_1'),
task4 = elementRegistry.get('Task_4');
connection = modeling.connect(boundaryEvent1, task4);
// when
modeling.moveElements([ task4 ], { x: 50, y: 50 });
}));
it('should do', function() {
// then
expect(connection.waypoints[1]).to.eql({ x: 150, y: 520 });
expect(connection.waypoints[2]).to.eql({ x: 230, y: 520 });
expect(connection.waypoints[3]).to.eql({ x: 230, y: 490 });
});
it('should undo', inject(function(commandStack) {
// when
commandStack.undo();
// then
expect(connection.waypoints[1]).to.eql({ x: 150, y: 520 });
expect(connection.waypoints[2]).to.eql({ x: 230, y: 520 });
expect(connection.waypoints[3]).to.eql({ x: 230, y: 440 });
}));
it('should redo', inject(function(commandStack) {
// given
commandStack.undo();
// when
commandStack.redo();
// then
expect(connection.waypoints[1]).to.eql({ x: 150, y: 520 });
expect(connection.waypoints[2]).to.eql({ x: 230, y: 520 });
expect(connection.waypoints[3]).to.eql({ x: 230, y: 490 });
}));
});
});
});