Skip to content

Commit

Permalink
feat(grid-snapping): integrate with connection layout
Browse files Browse the repository at this point in the history
Closes #1010
Related to #973
  • Loading branch information
philippfromme authored and barmac committed May 7, 2019
1 parent 867b41c commit 1ac801a
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 1 deletion.
133 changes: 133 additions & 0 deletions lib/features/grid-snapping/behavior/LayoutConnectionBehavior.js
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 4 additions & 1 deletion lib/features/grid-snapping/behavior/index.js
Original file line number Diff line number Diff line change
@@ -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 ]
};
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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 });
}));

});

});

});

0 comments on commit 1ac801a

Please sign in to comment.