chore(draw): unify text rendering into service

A newly introduced TextRenderer is responsible for text
rendering and text related bounds computation.

This removes a bunch of code duplication, too.
This commit is contained in:
Nico Rehwaldt 2018-05-24 13:42:40 +02:00
parent 4cd0a01df6
commit 4bb270f192
9 changed files with 245 additions and 166 deletions

View File

@ -7,7 +7,6 @@ import {
} from 'min-dash'; } from 'min-dash';
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
import TextUtil from 'diagram-js/lib/util/Text';
import { import {
isExpanded, isExpanded,
@ -58,18 +57,13 @@ var RENDERER_IDS = new Ids();
var TASK_BORDER_RADIUS = 10; var TASK_BORDER_RADIUS = 10;
var INNER_OUTER_DIST = 3; var INNER_OUTER_DIST = 3;
var LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: 12
};
var DEFAULT_FILL_OPACITY = .95, var DEFAULT_FILL_OPACITY = .95,
HIGH_FILL_OPACITY = .35; HIGH_FILL_OPACITY = .35;
export default function BpmnRenderer( export default function BpmnRenderer(
config, eventBus, styles, pathMap, config, eventBus, styles, pathMap,
canvas, priority) { canvas, textRenderer, priority) {
BaseRenderer.call(this, eventBus, priority); BaseRenderer.call(this, eventBus, priority);
@ -78,11 +72,6 @@ export default function BpmnRenderer(
var rendererId = RENDERER_IDS.next(); var rendererId = RENDERER_IDS.next();
var textUtil = new TextUtil({
style: LABEL_STYLE,
size: { width: 100 }
});
var markers = {}; var markers = {};
var computeStyle = styles.computeStyle; var computeStyle = styles.computeStyle;
@ -450,7 +439,14 @@ export default function BpmnRenderer(
} }
function renderLabel(parentGfx, label, options) { function renderLabel(parentGfx, label, options) {
var text = textUtil.createText(label || '', options);
options = assign({
size: {
width: 100
}
}, options);
var text = textRenderer.createText(label || '', options);
svgClasses(text).add('djs-label'); svgClasses(text).add('djs-label');
@ -484,16 +480,22 @@ export default function BpmnRenderer(
return renderLabel(parentGfx, semantic.name, { return renderLabel(parentGfx, semantic.name, {
box: box, box: box,
fitBox: true, fitBox: true,
style: { style: assign(
fontSize: '11px', {},
textRenderer.getExternalStyle(),
{
fill: getStrokeColor(element, defaultStrokeColor) fill: getStrokeColor(element, defaultStrokeColor)
} }
)
}); });
} }
function renderLaneLabel(parentGfx, text, element) { function renderLaneLabel(parentGfx, text, element) {
var textBox = renderLabel(parentGfx, text, { var textBox = renderLabel(parentGfx, text, {
box: { height: 30, width: element.height }, box: {
height: 30,
width: element.height
},
align: 'center-middle', align: 'center-middle',
style: { style: {
fill: getStrokeColor(element, defaultStrokeColor) fill: getStrokeColor(element, defaultStrokeColor)
@ -1846,7 +1848,8 @@ BpmnRenderer.$inject = [
'eventBus', 'eventBus',
'styles', 'styles',
'pathMap', 'pathMap',
'canvas' 'canvas',
'textRenderer'
]; ];

83
lib/draw/TextRenderer.js Normal file
View File

@ -0,0 +1,83 @@
import { assign } from 'min-dash';
import TextUtil from 'diagram-js/lib/util/Text';
export default function TextRenderer(config) {
var defaultStyle = assign({
fontFamily: 'Arial, sans-serif',
fontSize: 12
}, config && config.defaultStyle || {});
var externalStyle = assign({}, defaultStyle, {
fontSize: parseInt(defaultStyle.fontSize, 10) - 1
}, config && config.externalStyle || {});
var textUtil = new TextUtil({
style: defaultStyle
});
/**
* Get the new bounds of an externally rendered,
* layouted label.
*
* @param {Bounds} bounds
* @param {String} text
*
* @return {Bounds}
*/
this.getLayoutedBounds = function(bounds, text) {
var layoutedDimensions = textUtil.getDimensions(text, {
box: {
width: 90,
height: 30,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y
},
style: externalStyle
});
// resize label shape to fit label text
return {
x: Math.round(bounds.x + bounds.width / 2 - layoutedDimensions.width / 2),
y: Math.round(bounds.y),
width: Math.ceil(layoutedDimensions.width),
height: Math.ceil(layoutedDimensions.height)
};
};
/**
* Create a layouted text element.
*
* @param {String} text
* @param {Object} [options]
*
* @return {SVGElement} rendered text
*/
this.createText = function(text, options) {
return textUtil.createText(text, options || {});
};
/**
* Get default text style.
*/
this.getDefaultStyle = function() {
return defaultStyle;
};
/**
* Get the external text style.
*/
this.getExternalStyle = function() {
return externalStyle;
};
}
TextRenderer.$inject = [
'config.textRenderer'
];

View File

@ -1,8 +1,11 @@
import BpmnRenderer from './BpmnRenderer'; import BpmnRenderer from './BpmnRenderer';
import TextRenderer from './TextRenderer';
import PathMap from './PathMap'; import PathMap from './PathMap';
export default { export default {
__init__: [ 'bpmnRenderer' ], __init__: [ 'bpmnRenderer' ],
bpmnRenderer: [ 'type', BpmnRenderer ], bpmnRenderer: [ 'type', BpmnRenderer ],
textRenderer: [ 'type', TextRenderer ],
pathMap: [ 'type', PathMap ] pathMap: [ 'type', PathMap ]
}; };

View File

@ -3,8 +3,6 @@ import {
getLabel getLabel
} from '../LabelUtil'; } from '../LabelUtil';
import TextUtil from 'diagram-js/lib/util/Text';
import { import {
getExternalLabelMid, getExternalLabelMid,
isLabelExternal, isLabelExternal,
@ -26,9 +24,7 @@ var NULL_DIMENSIONS = {
/** /**
* A handler that updates the text of a BPMN element. * A handler that updates the text of a BPMN element.
*/ */
export default function UpdateLabelHandler(modeling) { export default function UpdateLabelHandler(modeling, textRenderer) {
var textUtil = new TextUtil();
/** /**
* Set the label and return the changed elements. * Set the label and return the changed elements.
@ -114,7 +110,7 @@ export default function UpdateLabelHandler(modeling) {
// resize element based on label _or_ pre-defined bounds // resize element based on label _or_ pre-defined bounds
if (typeof newBounds === 'undefined') { if (typeof newBounds === 'undefined') {
newBounds = getLayoutedBounds(label, text, textUtil); newBounds = textRenderer.getLayoutedBounds(label, text);
} }
// setting newBounds to false or _null_ will // setting newBounds to false or _null_ will
@ -132,33 +128,7 @@ export default function UpdateLabelHandler(modeling) {
this.postExecute = postExecute; this.postExecute = postExecute;
} }
UpdateLabelHandler.$inject = [ 'modeling' ]; UpdateLabelHandler.$inject = [
'modeling',
'textRenderer'
// TODO(nikku): repeating code (search for <getLayoutedBounds>) ];
var EXTERNAL_LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '11px'
};
function getLayoutedBounds(bounds, text, textUtil) {
var layoutedLabelDimensions = textUtil.getDimensions(text, {
box: {
width: 90,
height: 30,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y
},
style: EXTERNAL_LABEL_STYLE
});
// resize label shape to fit label text
return {
x: Math.round(bounds.x + bounds.width / 2 - layoutedLabelDimensions.width / 2),
y: Math.round(bounds.y),
width: Math.ceil(layoutedLabelDimensions.width),
height: Math.ceil(layoutedLabelDimensions.height)
};
}

View File

@ -20,8 +20,6 @@ import {
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import TextUtil from 'diagram-js/lib/util/Text';
var DEFAULT_LABEL_DIMENSIONS = { var DEFAULT_LABEL_DIMENSIONS = {
width: 90, width: 90,
height: 20 height: 20
@ -36,13 +34,14 @@ var DEFAULT_LABEL_DIMENSIONS = {
* @param {EventBus} eventBus * @param {EventBus} eventBus
* @param {Modeling} modeling * @param {Modeling} modeling
* @param {BpmnFactory} bpmnFactory * @param {BpmnFactory} bpmnFactory
* @param {TextRenderer} textRenderer
*/ */
export default function LabelBehavior(eventBus, modeling, bpmnFactory) { export default function LabelBehavior(
eventBus, modeling, bpmnFactory,
textRenderer) {
CommandInterceptor.call(this, eventBus); CommandInterceptor.call(this, eventBus);
var textUtil = new TextUtil();
// update label if name property was updated // update label if name property was updated
this.postExecute('element.updateProperties', function(e) { this.postExecute('element.updateProperties', function(e) {
var context = e.context, var context = e.context,
@ -73,10 +72,9 @@ export default function LabelBehavior(eventBus, modeling, bpmnFactory) {
var labelCenter = getExternalLabelMid(element); var labelCenter = getExternalLabelMid(element);
// we don't care about x and y // we don't care about x and y
var labelDimensions = getLayoutedBounds( var labelDimensions = textRenderer.getLayoutedBounds(
DEFAULT_LABEL_DIMENSIONS, DEFAULT_LABEL_DIMENSIONS,
businessObject.name || '', businessObject.name || ''
textUtil
); );
modeling.createLabel(element, labelCenter, { modeling.createLabel(element, labelCenter, {
@ -200,34 +198,6 @@ inherits(LabelBehavior, CommandInterceptor);
LabelBehavior.$inject = [ LabelBehavior.$inject = [
'eventBus', 'eventBus',
'modeling', 'modeling',
'bpmnFactory' 'bpmnFactory',
'textRenderer'
]; ];
// TODO(nikku): repeating code (search for <getLayoutedBounds>)
var EXTERNAL_LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '11px'
};
function getLayoutedBounds(bounds, text, textUtil) {
var layoutedLabelDimensions = textUtil.getDimensions(text, {
box: {
width: 90,
height: 30,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y
},
style: EXTERNAL_LABEL_STYLE
});
// resize label shape to fit label text
return {
x: Math.round(bounds.x + bounds.width / 2 - layoutedLabelDimensions.width / 2),
y: Math.round(bounds.y),
width: Math.ceil(layoutedLabelDimensions.width),
height: Math.ceil(layoutedLabelDimensions.height)
};
}

View File

@ -9,8 +9,6 @@ import {
getBusinessObject getBusinessObject
} from '../../../util/ModelUtil'; } from '../../../util/ModelUtil';
import TextUtil from 'diagram-js/lib/util/Text';
var DEFAULT_FLOW = 'default', var DEFAULT_FLOW = 'default',
ID = 'id', ID = 'id',
DI = 'di'; DI = 'di';
@ -29,20 +27,23 @@ var NULL_DIMENSIONS = {
* Use respective diagram-js provided handlers if you would * Use respective diagram-js provided handlers if you would
* like to perform automated modeling. * like to perform automated modeling.
*/ */
export default function UpdatePropertiesHandler(elementRegistry, moddle, translate, modeling) { export default function UpdatePropertiesHandler(
elementRegistry, moddle, translate,
modeling, textRenderer) {
this._elementRegistry = elementRegistry; this._elementRegistry = elementRegistry;
this._moddle = moddle; this._moddle = moddle;
this._translate = translate; this._translate = translate;
this._modeling = modeling; this._modeling = modeling;
this._textRenderer = textRenderer;
this._textUtil = new TextUtil();
} }
UpdatePropertiesHandler.$inject = [ UpdatePropertiesHandler.$inject = [
'elementRegistry', 'elementRegistry',
'moddle', 'moddle',
'translate', 'translate',
'modeling' 'modeling',
'textRenderer'
]; ];
@ -119,7 +120,7 @@ UpdatePropertiesHandler.prototype.postExecute = function(context) {
// get layouted text bounds and resize external // get layouted text bounds and resize external
// external label accordingly // external label accordingly
var newLabelBounds = getLayoutedBounds(label, text, this._textUtil); var newLabelBounds = this._textRenderer.getLayoutedBounds(label, text);
this._modeling.resizeShape(label, newLabelBounds, NULL_DIMENSIONS); this._modeling.resizeShape(label, newLabelBounds, NULL_DIMENSIONS);
}; };
@ -231,32 +232,3 @@ function unwrapBusinessObjects(properties) {
return unwrappedProps; return unwrappedProps;
} }
// TODO(nikku): repeating code (search for <getLayoutedBounds>)
var EXTERNAL_LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '11px'
};
function getLayoutedBounds(bounds, text, textUtil) {
var layoutedLabelDimensions = textUtil.getDimensions(text, {
box: {
width: 90,
height: 30,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y
},
style: EXTERNAL_LABEL_STYLE
});
// resize label shape to fit label text
return {
x: Math.round(bounds.x + bounds.width / 2 - layoutedLabelDimensions.width / 2),
y: Math.round(bounds.y),
width: Math.ceil(layoutedLabelDimensions.width),
height: Math.ceil(layoutedLabelDimensions.height)
};
}

View File

@ -3,8 +3,6 @@ import {
map map
} from 'min-dash'; } from 'min-dash';
import TextUtil from 'diagram-js/lib/util/Text';
import { is } from '../util/ModelUtil'; import { is } from '../util/ModelUtil';
import { import {
@ -56,19 +54,18 @@ function notYetDrawn(translate, semantic, refSemantic, property) {
* @param {ElementFactory} elementFactory * @param {ElementFactory} elementFactory
* @param {ElementRegistry} elementRegistry * @param {ElementRegistry} elementRegistry
* @param {Function} translate * @param {Function} translate
* @param {TextRenderer} textRenderer
*/ */
export default function BpmnImporter( export default function BpmnImporter(
eventBus, canvas, elementFactory, eventBus, canvas, elementFactory,
elementRegistry, translate) { elementRegistry, translate, textRenderer) {
this._eventBus = eventBus; this._eventBus = eventBus;
this._canvas = canvas; this._canvas = canvas;
this._elementFactory = elementFactory; this._elementFactory = elementFactory;
this._elementRegistry = elementRegistry; this._elementRegistry = elementRegistry;
this._translate = translate; this._translate = translate;
this._textRenderer = textRenderer;
this._textUtil = new TextUtil();
} }
BpmnImporter.$inject = [ BpmnImporter.$inject = [
@ -76,7 +73,8 @@ BpmnImporter.$inject = [
'canvas', 'canvas',
'elementFactory', 'elementFactory',
'elementRegistry', 'elementRegistry',
'translate' 'translate',
'textRenderer'
]; ];
@ -239,7 +237,7 @@ BpmnImporter.prototype.addLabel = function(semantic, element) {
if (text) { if (text) {
// get corrected bounds from actual layouted text // get corrected bounds from actual layouted text
bounds = getLayoutedBounds(bounds, text, this._textUtil); bounds = this._textRenderer.getLayoutedBounds(bounds, text);
} }
label = this._elementFactory.createLabel(elementData(semantic, { label = this._elementFactory.createLabel(elementData(semantic, {
@ -312,33 +310,7 @@ BpmnImporter.prototype._getElement = function(semantic) {
}; };
// TODO(nikku): repeating code (search for <getLayoutedBounds>) // helpers ////////////////////
var EXTERNAL_LABEL_STYLE = {
fontFamily: 'Arial, sans-serif',
fontSize: '11px'
};
function getLayoutedBounds(bounds, text, textUtil) {
var layoutedLabelDimensions = textUtil.getDimensions(text, {
box: {
width: 90,
height: 30,
x: bounds.width / 2 + bounds.x,
y: bounds.height / 2 + bounds.y
},
style: EXTERNAL_LABEL_STYLE
});
// resize label shape to fit label text
return {
x: Math.round(bounds.x + bounds.width / 2 - layoutedLabelDimensions.width / 2),
y: Math.round(bounds.y),
width: Math.ceil(layoutedLabelDimensions.width),
height: Math.ceil(layoutedLabelDimensions.height)
};
}
function isPointInsideBBox(bbox, point) { function isPointInsideBBox(bbox, point) {
var x = point.x, var x = point.x,

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="sid-38422fae-e03e-43a3-bef4-bd33b32041b2" targetNamespace="http://bpmn.io/bpmn" exporter="http://bpmn.io" exporterVersion="0.0.0">
<process id="Process_1" isExecutable="false">
<startEvent id="StartEvent_1" name="EVENT" />
<task id="Task_1bjzik3" name="TASK" />
</process>
<bpmndi:BPMNDiagram id="BpmnDiagram_1">
<bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="StartEvent_16g5oa3_di" bpmnElement="StartEvent_1">
<omgdc:Bounds x="239" y="184" width="36" height="36" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="237" y="226" width="39" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1bjzik3_di" bpmnElement="Task_1bjzik3">
<omgdc:Bounds x="347" y="162" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

View File

@ -0,0 +1,86 @@
import {
bootstrapViewer,
inject
} from 'test/TestHelper';
describe('draw - TextRenderer', function() {
var diagramXML = require('./TextRenderer.bpmn');
describe('API', function() {
beforeEach(bootstrapViewer(diagramXML));
it('should expose #createText', inject(function(textRenderer) {
// when
var text = textRenderer.createText('FOO');
// then
expect(text).to.exist;
}));
it('should expose #getLayoutedBounds', inject(function(textRenderer) {
// given
var bounds = {
x: 0,
y: 0,
width: 100,
height: 100
};
// when
var layoutedBounds = textRenderer.getLayoutedBounds(
bounds,
'FOO\nBar\nFOOBAR'
);
// then
expect(layoutedBounds).to.exist;
expect(layoutedBounds.x).to.exist;
expect(layoutedBounds.y).to.exist;
expect(layoutedBounds.width).to.exist;
expect(layoutedBounds.height).to.exist;
}));
});
describe('style override', function() {
beforeEach(bootstrapViewer(diagramXML, {
textRenderer: {
defaultStyle: {
fontFamily: 'monospace',
fontSize: '15px'
},
externalStyle: {
fontWeight: 'bold'
}
}
}));
it('should render', inject(function(textRenderer) {
// when
var defaultStyle = textRenderer.getDefaultStyle();
var externalStyle = textRenderer.getExternalStyle();
// then
expect(defaultStyle.fontFamily).to.eql('monospace');
expect(defaultStyle.fontSize).to.eql('15px');
expect(externalStyle.fontFamily).to.eql('monospace');
expect(externalStyle.fontSize).to.eql(14);
expect(externalStyle.fontWeight).to.eql('bold');
}));
});
});