feat(modeling): add support for custom elements

This commit adds part of the infrastructure that allows
the coexistence between bpmn elements and custom ones.

Closes #348
This commit is contained in:
Ricardo Matias 2015-09-02 15:13:18 +02:00 committed by Nico Rehwaldt
parent 6482273aa4
commit 31f0ea1ec0
8 changed files with 257 additions and 226 deletions

View File

@ -1,7 +1,6 @@
'use strict';
var inherits = require('inherits'),
isArray = require('lodash/lang/isArray'),
isObject = require('lodash/lang/isObject'),
assign = require('lodash/object/assign'),
forEach = require('lodash/collection/forEach'),
@ -9,24 +8,30 @@ var inherits = require('inherits'),
includes = require('lodash/collection/includes'),
some = require('lodash/collection/some');
var DefaultRenderer = require('diagram-js/lib/draw/Renderer'),
var BaseRenderer = require('diagram-js/lib/draw/BaseRenderer'),
TextUtil = require('diagram-js/lib/util/Text'),
DiUtil = require('../util/DiUtil');
var createLine = DefaultRenderer.createLine;
var is = require('../util/ModelUtil').is;
var RenderUtil = require('diagram-js/lib/util/RenderUtil');
var componentsToPath = RenderUtil.componentsToPath,
createLine = RenderUtil.createLine;
function BpmnRenderer(events, styles, pathMap) {
var TASK_BORDER_RADIUS = 10;
var INNER_OUTER_DIST = 3;
DefaultRenderer.call(this, styles);
var LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '12px'
};
var TASK_BORDER_RADIUS = 10;
var INNER_OUTER_DIST = 3;
var LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '12px'
};
function BpmnRenderer(eventBus, styles, pathMap) {
BaseRenderer.call(this, eventBus, 1500);
var textUtil = new TextUtil({
style: LABEL_STYLE,
@ -35,6 +40,8 @@ function BpmnRenderer(events, styles, pathMap) {
var markers = {};
var computeStyle = styles.computeStyle;
function addMarker(id, element) {
markers[id] = element;
}
@ -130,15 +137,6 @@ function BpmnRenderer(events, styles, pathMap) {
});
}
function computeStyle(custom, traits, defaultStyles) {
if (!isArray(traits)) {
defaultStyles = traits;
traits = [];
}
return styles.style(traits || [], assign(defaultStyles, custom || {}));
}
function drawCircle(p, width, height, offset, attrs) {
if (isObject(offset)) {
@ -324,7 +322,7 @@ function BpmnRenderer(events, styles, pathMap) {
return pathData;
}
var handlers = {
var handlers = this.handlers = {
'bpmn:Event': function(p, element, attrs) {
return drawCircle(p, element.width, element.height, attrs);
},
@ -1423,30 +1421,6 @@ function BpmnRenderer(events, styles, pathMap) {
}
}
function drawShape(parent, element) {
var type = element.type;
var h = handlers[type];
/* jshint -W040 */
if (!h) {
return DefaultRenderer.prototype.drawShape.apply(this, [ parent, element ]);
} else {
return h(parent, element);
}
}
function drawConnection(parent, element) {
var type = element.type;
var h = handlers[type];
/* jshint -W040 */
if (!h) {
return DefaultRenderer.prototype.drawConnection.apply(this, [ parent, element ]);
} else {
return h(parent, element);
}
}
function renderDataItemCollection(p, element) {
var yPosition = (element.height - 16) / element.height;
@ -1467,164 +1441,177 @@ function BpmnRenderer(events, styles, pathMap) {
});
}
function isCollection(element, filter) {
return element.isCollection ||
(element.elementObjectRef && element.elementObjectRef.isCollection);
}
function getDi(element) {
return element.businessObject.di;
}
function getSemantic(element) {
return element.businessObject;
}
/**
* Checks if eventDefinition of the given element matches with semantic type.
*
* @return {boolean} true if element is of the given semantic type
*/
function isTypedEvent(event, eventDefinitionType, filter) {
function matches(definition, filter) {
return every(filter, function(val, key) {
// we want a == conversion here, to be able to catch
// undefined == false and friends
/* jshint -W116 */
return definition[key] == val;
});
}
return some(event.eventDefinitions, function(definition) {
return definition.$type === eventDefinitionType && matches(event, filter);
});
}
function isThrowEvent(event) {
return (event.$type === 'bpmn:IntermediateThrowEvent') || (event.$type === 'bpmn:EndEvent');
}
/////// cropping path customizations /////////////////////////
function componentsToPath(elements) {
return elements.join(',').replace(/,?([A-z]),?/g, '$1');
}
function getCirclePath(shape) {
var cx = shape.x + shape.width / 2,
cy = shape.y + shape.height / 2,
radius = shape.width / 2;
var circlePath = [
['M', cx, cy],
['m', 0, -radius],
['a', radius, radius, 0, 1, 1, 0, 2 * radius],
['a', radius, radius, 0, 1, 1, 0, -2 * radius],
['z']
];
return componentsToPath(circlePath);
}
function getRoundRectPath(shape) {
var radius = TASK_BORDER_RADIUS,
x = shape.x,
y = shape.y,
width = shape.width,
height = shape.height;
var roundRectPath = [
['M', x + radius, y],
['l', width - radius * 2, 0],
['a', radius, radius, 0, 0, 1, radius, radius],
['l', 0, height - radius * 2],
['a', radius, radius, 0, 0, 1, -radius, radius],
['l', radius * 2 - width, 0],
['a', radius, radius, 0, 0, 1, -radius, -radius],
['l', 0, radius * 2 - height],
['a', radius, radius, 0, 0, 1, radius, -radius],
['z']
];
return componentsToPath(roundRectPath);
}
function getDiamondPath(shape) {
var width = shape.width,
height = shape.height,
x = shape.x,
y = shape.y,
halfWidth = width / 2,
halfHeight = height / 2;
var diamondPath = [
['M', x + halfWidth, y],
['l', halfWidth, halfHeight],
['l', -halfWidth, halfHeight],
['l', -halfWidth, -halfHeight],
['z']
];
return componentsToPath(diamondPath);
}
function getRectPath(shape) {
var x = shape.x,
y = shape.y,
width = shape.width,
height = shape.height;
var rectPath = [
['M', x, y],
['l', width, 0],
['l', 0, height],
['l', -width, 0],
['z']
];
return componentsToPath(rectPath);
}
function getShapePath(element) {
var obj = getSemantic(element);
if (obj.$instanceOf('bpmn:Event')) {
return getCirclePath(element);
}
if (obj.$instanceOf('bpmn:Activity')) {
return getRoundRectPath(element);
}
if (obj.$instanceOf('bpmn:Gateway')) {
return getDiamondPath(element);
}
return getRectPath(element);
}
// hook onto canvas init event to initialize
// connection start/end markers on svg
events.on('canvas.init', function(event) {
eventBus.on('canvas.init', function(event) {
initMarkers(event.svg);
});
this.drawShape = drawShape;
this.drawConnection = drawConnection;
this.getShapePath = getShapePath;
}
inherits(BpmnRenderer, DefaultRenderer);
inherits(BpmnRenderer, BaseRenderer);
BpmnRenderer.$inject = [ 'eventBus', 'styles', 'pathMap' ];
module.exports = BpmnRenderer;
BpmnRenderer.prototype.canRender = function(element) {
return is(element, 'bpmn:BaseElement');
};
BpmnRenderer.prototype.drawShape = function(visuals, element) {
var type = element.type;
var h = this.handlers[type];
/* jshint -W040 */
return h(visuals, element);
};
BpmnRenderer.prototype.drawConnection = function(visuals, element) {
var type = element.type;
var h = this.handlers[type];
/* jshint -W040 */
return h(visuals, element);
};
BpmnRenderer.prototype.getShapePath = function(element) {
if (is(element, 'bpmn:Event')) {
return getCirclePath(element);
}
if (is(element, 'bpmn:Activity')) {
return getRoundRectPath(element, TASK_BORDER_RADIUS);
}
if (is(element, 'bpmn:Gateway')) {
return getDiamondPath(element);
}
return getRectPath(element);
};
///////// helper functions /////////////////////////////
/**
* Checks if eventDefinition of the given element matches with semantic type.
*
* @return {boolean} true if element is of the given semantic type
*/
function isTypedEvent(event, eventDefinitionType, filter) {
function matches(definition, filter) {
return every(filter, function(val, key) {
// we want a == conversion here, to be able to catch
// undefined == false and friends
/* jshint -W116 */
return definition[key] == val;
});
}
return some(event.eventDefinitions, function(definition) {
return definition.$type === eventDefinitionType && matches(event, filter);
});
}
function isThrowEvent(event) {
return (event.$type === 'bpmn:IntermediateThrowEvent') || (event.$type === 'bpmn:EndEvent');
}
function isCollection(element) {
return element.isCollection ||
(element.elementObjectRef && element.elementObjectRef.isCollection);
}
function getDi(element) {
return element.businessObject.di;
}
function getSemantic(element) {
return element.businessObject;
}
/////// cropping path customizations /////////////////////////
function getCirclePath(shape) {
var cx = shape.x + shape.width / 2,
cy = shape.y + shape.height / 2,
radius = shape.width / 2;
var circlePath = [
['M', cx, cy],
['m', 0, -radius],
['a', radius, radius, 0, 1, 1, 0, 2 * radius],
['a', radius, radius, 0, 1, 1, 0, -2 * radius],
['z']
];
return componentsToPath(circlePath);
}
function getRoundRectPath(shape, borderRadius) {
var x = shape.x,
y = shape.y,
width = shape.width,
height = shape.height;
var roundRectPath = [
['M', x + borderRadius, y],
['l', width - borderRadius * 2, 0],
['a', borderRadius, borderRadius, 0, 0, 1, borderRadius, borderRadius],
['l', 0, height - borderRadius * 2],
['a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, borderRadius],
['l', borderRadius * 2 - width, 0],
['a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, -borderRadius],
['l', 0, borderRadius * 2 - height],
['a', borderRadius, borderRadius, 0, 0, 1, borderRadius, -borderRadius],
['z']
];
return componentsToPath(roundRectPath);
}
function getDiamondPath(shape) {
var width = shape.width,
height = shape.height,
x = shape.x,
y = shape.y,
halfWidth = width / 2,
halfHeight = height / 2;
var diamondPath = [
['M', x + halfWidth, y],
['l', halfWidth, halfHeight],
['l', -halfWidth, halfHeight],
['l', -halfWidth, -halfHeight],
['z']
];
return componentsToPath(diamondPath);
}
function getRectPath(shape) {
var x = shape.x,
y = shape.y,
width = shape.width,
height = shape.height;
var rectPath = [
['M', x, y],
['l', width, 0],
['l', 0, height],
['l', -width, 0],
['z']
];
return componentsToPath(rectPath);
}

View File

@ -1,4 +1,5 @@
module.exports = {
renderer: [ 'type', require('./BpmnRenderer') ],
__init__: [ 'bpmnRenderer' ],
bpmnRenderer: [ 'type', require('./BpmnRenderer') ],
pathMap: [ 'type', require('./PathMap') ]
};
};

View File

@ -3,7 +3,8 @@
var assign = require('lodash/object/assign'),
inherits = require('inherits');
var LabelUtil = require('../../util/LabelUtil');
var LabelUtil = require('../../util/LabelUtil'),
is = require('../../util/ModelUtil').is;
var hasExternalLabel = LabelUtil.hasExternalLabel,
getExternalLabelMid = LabelUtil.getExternalLabelMid;
@ -35,18 +36,25 @@ function LabelSupport(eventBus, modeling, bpmnFactory) {
});
// update di information on label movement and creation
this.executed([ 'label.create', 'shape.moved' ], function(e) {
var element = e.context.shape,
businessObject = element.businessObject,
di = businessObject.di;
businessObject,
di;
// we want to trigger on real labels only
if (!element.labelTarget) {
return;
}
// we want to trigger on BPMN elements only
if (!is(element.labelTarget || element, 'bpmn:BaseElement')) {
return;
}
businessObject = element.businessObject,
di = businessObject.di;
if (!di.label) {
di.label = bpmnFactory.create('bpmndi:BPMNLabel', {
bounds: bpmnFactory.create('dc:Bounds')

View File

@ -79,14 +79,14 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
'shape.delete',
'connection.create',
'connection.move',
'connection.delete' ], updateParent);
'connection.delete' ], ifBpmn(updateParent));
this.reverted([ 'shape.move',
'shape.create',
'shape.delete',
'connection.create',
'connection.move',
'connection.delete' ], reverseUpdateParent);
'connection.delete' ], ifBpmn(reverseUpdateParent));
/*
* ## Updating Parent
@ -112,11 +112,17 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
// update bounds
function updateBounds(e) {
self.updateBounds(e.context.shape);
var shape = e.context.shape;
if (!is(shape, 'bpmn:BaseElement')) {
return;
}
self.updateBounds(shape);
}
this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], updateBounds);
this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], updateBounds);
this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(updateBounds));
this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(updateBounds));
// attach / detach connection
@ -130,7 +136,7 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
'connection.delete',
'connection.reconnectEnd',
'connection.reconnectStart'
], updateConnection);
], ifBpmn(updateConnection));
this.reverted([
'connection.create',
@ -138,7 +144,7 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
'connection.delete',
'connection.reconnectEnd',
'connection.reconnectStart'
], updateConnection);
], ifBpmn(updateConnection));
// update waypoints
@ -152,7 +158,7 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
'connection.updateWaypoints',
'connection.reconnectEnd',
'connection.reconnectStart'
], updateConnectionWaypoints);
], ifBpmn(updateConnectionWaypoints));
this.reverted([
'connection.layout',
@ -160,15 +166,15 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking) {
'connection.updateWaypoints',
'connection.reconnectEnd',
'connection.reconnectStart'
], updateConnectionWaypoints);
], ifBpmn(updateConnectionWaypoints));
// update attachments
function updateAttachment(e) {
self.updateAttachment(e.context);
}
this.executed([ 'element.updateAttachment' ], updateAttachment);
this.reverted([ 'element.updateAttachment' ], updateAttachment);
this.executed([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
this.reverted([ 'element.updateAttachment' ], ifBpmn(updateAttachment));
}
inherits(BpmnUpdater, CommandInterceptor);
@ -466,3 +472,24 @@ BpmnUpdater.prototype._getLabel = function(di) {
return di.label;
};
/**
* Make sure the event listener is only called
* if the touched element is a BPMN element.
*
* @param {Function} fn
* @return {Function} guarded function
*/
function ifBpmn(fn) {
return function(event) {
var context = event.context,
element = context.shape || context.connection;
if (is(element, 'bpmn:BaseElement')) {
fn(event);
}
};
}

View File

@ -27,7 +27,6 @@ module.exports = ElementFactory;
ElementFactory.prototype.baseCreate = BaseElementFactory.prototype.create;
ElementFactory.prototype.create = function(elementType, attrs) {
// no special magic for labels,
// we assume their businessObjects have already been created
// and wired via attrs
@ -35,10 +34,15 @@ ElementFactory.prototype.create = function(elementType, attrs) {
return this.baseCreate(elementType, assign({ type: 'label' }, LabelUtil.DEFAULT_LABEL_SIZE, attrs));
}
return this.createBpmnElement(elementType, attrs);
};
ElementFactory.prototype.createBpmnElement = function(elementType, attrs) {
var size;
attrs = attrs || {};
var businessObject = attrs.businessObject,
size;
var businessObject = attrs.businessObject;
if (!businessObject) {
if (!attrs.type) {

View File

@ -16,6 +16,10 @@ var DEFAULT_LABEL_SIZE = module.exports.DEFAULT_LABEL_SIZE = {
* @return {Boolean} true if has label
*/
module.exports.hasExternalLabel = function(semantic) {
// custom elements
if (typeof semantic.$instanceOf !== 'function') {
return false;
}
return semantic.$instanceOf('bpmn:Event') ||
semantic.$instanceOf('bpmn:Gateway') ||
@ -96,4 +100,4 @@ module.exports.getExternalLabelBounds = function(semantic, element) {
x: mid.x - size.width / 2,
y: mid.y - size.height / 2
}, size);
};
};

View File

@ -161,39 +161,39 @@ describe('path - bpmn renderer', function () {
describe('circle', function () {
it('should return a circle path', inject(function(canvas, elementRegistry, renderer) {
it('should return a circle path', inject(function(canvas, elementRegistry, graphicsFactory) {
// given
var eventElement = elementRegistry.get('StartEvent_1');
// when
var startPath = renderer.getShapePath(eventElement);
var startPath = graphicsFactory.getShapePath(eventElement);
// then
expect(startPath).to.equal('M247,343m0,-18a18,18,0,1,1,0,36a18,18,0,1,1,0,-36z');
}));
it('should return a diamond path', inject(function(canvas, elementRegistry, renderer) {
it('should return a diamond path', inject(function(canvas, elementRegistry, graphicsFactory) {
// given
var gatewayElement = elementRegistry.get('ExclusiveGateway_1');
// when
var gatewayPath = renderer.getShapePath(gatewayElement);
var gatewayPath = graphicsFactory.getShapePath(gatewayElement);
// then
expect(gatewayPath).to.equal('M418,318l25,25l-25,25l-25,-25z');
}));
it('should return a rounded rectangular path', inject(function(canvas, elementRegistry, renderer) {
it('should return a rounded rectangular path', inject(function(canvas, elementRegistry, graphicsFactory) {
// given
var subProcessElement = elementRegistry.get('SubProcess_1');
// when
var subProcessPath = renderer.getShapePath(subProcessElement);
var subProcessPath = graphicsFactory.getShapePath(subProcessElement);
// then
expect(subProcessPath).to.equal('M584,243l330,0a10,10,0,0,1,10,10l0,180a10,10,0,0,1,-10,10' +
@ -201,13 +201,13 @@ describe('path - bpmn renderer', function () {
}));
it('should return a rectangular path', inject(function(canvas, elementRegistry, renderer) {
it('should return a rectangular path', inject(function(canvas, elementRegistry, graphicsFactory) {
// given
var TextAnnotationElement = elementRegistry.get('TextAnnotation_1');
// when
var TextAnnotationPath = renderer.getShapePath(TextAnnotationElement);
var TextAnnotationPath = graphicsFactory.getShapePath(TextAnnotationElement);
// then
expect(TextAnnotationPath).to.equal('M368,156l100,0l0,80l-100,0z');

View File

@ -323,4 +323,4 @@ describe('features/modeling - layout', function() {
});
});
});