From 4bb270f19279db40f9cc3c179e09ee3a9a114e7c Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Thu, 24 May 2018 13:42:40 +0200 Subject: [PATCH] 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. --- lib/draw/BpmnRenderer.js | 43 +++++----- lib/draw/TextRenderer.js | 83 ++++++++++++++++++ lib/draw/index.js | 3 + .../label-editing/cmd/UpdateLabelHandler.js | 42 ++------- .../modeling/behavior/LabelBehavior.js | 48 ++--------- .../modeling/cmd/UpdatePropertiesHandler.js | 44 ++-------- lib/import/BpmnImporter.js | 42 ++------- test/spec/draw/TextRenderer.bpmn | 20 +++++ test/spec/draw/TextRendererSpec.js | 86 +++++++++++++++++++ 9 files changed, 245 insertions(+), 166 deletions(-) create mode 100644 lib/draw/TextRenderer.js create mode 100644 test/spec/draw/TextRenderer.bpmn create mode 100644 test/spec/draw/TextRendererSpec.js diff --git a/lib/draw/BpmnRenderer.js b/lib/draw/BpmnRenderer.js index 858593a0..c29e5cd7 100644 --- a/lib/draw/BpmnRenderer.js +++ b/lib/draw/BpmnRenderer.js @@ -7,7 +7,6 @@ import { } from 'min-dash'; import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; -import TextUtil from 'diagram-js/lib/util/Text'; import { isExpanded, @@ -58,18 +57,13 @@ var RENDERER_IDS = new Ids(); var TASK_BORDER_RADIUS = 10; var INNER_OUTER_DIST = 3; -var LABEL_STYLE = { - fontFamily: 'Arial, sans-serif', - fontSize: 12 -}; - var DEFAULT_FILL_OPACITY = .95, HIGH_FILL_OPACITY = .35; export default function BpmnRenderer( config, eventBus, styles, pathMap, - canvas, priority) { + canvas, textRenderer, priority) { BaseRenderer.call(this, eventBus, priority); @@ -78,11 +72,6 @@ export default function BpmnRenderer( var rendererId = RENDERER_IDS.next(); - var textUtil = new TextUtil({ - style: LABEL_STYLE, - size: { width: 100 } - }); - var markers = {}; var computeStyle = styles.computeStyle; @@ -450,7 +439,14 @@ export default function BpmnRenderer( } 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'); @@ -484,16 +480,22 @@ export default function BpmnRenderer( return renderLabel(parentGfx, semantic.name, { box: box, fitBox: true, - style: { - fontSize: '11px', - fill: getStrokeColor(element, defaultStrokeColor) - } + style: assign( + {}, + textRenderer.getExternalStyle(), + { + fill: getStrokeColor(element, defaultStrokeColor) + } + ) }); } function renderLaneLabel(parentGfx, text, element) { var textBox = renderLabel(parentGfx, text, { - box: { height: 30, width: element.height }, + box: { + height: 30, + width: element.height + }, align: 'center-middle', style: { fill: getStrokeColor(element, defaultStrokeColor) @@ -594,7 +596,7 @@ export default function BpmnRenderer( stroke: getStrokeColor(element, defaultStrokeColor) }); - for (var i = 0;i < 12;i++) { + for (var i = 0;i < 12; i++) { var linePathData = pathMap.getScaledPath('EVENT_TIMER_LINE', { xScaleFactor: 0.75, @@ -1846,7 +1848,8 @@ BpmnRenderer.$inject = [ 'eventBus', 'styles', 'pathMap', - 'canvas' + 'canvas', + 'textRenderer' ]; diff --git a/lib/draw/TextRenderer.js b/lib/draw/TextRenderer.js new file mode 100644 index 00000000..8f74f108 --- /dev/null +++ b/lib/draw/TextRenderer.js @@ -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' +]; \ No newline at end of file diff --git a/lib/draw/index.js b/lib/draw/index.js index 530988e4..5902fde1 100644 --- a/lib/draw/index.js +++ b/lib/draw/index.js @@ -1,8 +1,11 @@ import BpmnRenderer from './BpmnRenderer'; +import TextRenderer from './TextRenderer'; + import PathMap from './PathMap'; export default { __init__: [ 'bpmnRenderer' ], bpmnRenderer: [ 'type', BpmnRenderer ], + textRenderer: [ 'type', TextRenderer ], pathMap: [ 'type', PathMap ] }; diff --git a/lib/features/label-editing/cmd/UpdateLabelHandler.js b/lib/features/label-editing/cmd/UpdateLabelHandler.js index 3b6fded6..7ef85dc1 100644 --- a/lib/features/label-editing/cmd/UpdateLabelHandler.js +++ b/lib/features/label-editing/cmd/UpdateLabelHandler.js @@ -3,8 +3,6 @@ import { getLabel } from '../LabelUtil'; -import TextUtil from 'diagram-js/lib/util/Text'; - import { getExternalLabelMid, isLabelExternal, @@ -26,9 +24,7 @@ var NULL_DIMENSIONS = { /** * A handler that updates the text of a BPMN element. */ -export default function UpdateLabelHandler(modeling) { - - var textUtil = new TextUtil(); +export default function UpdateLabelHandler(modeling, textRenderer) { /** * 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 if (typeof newBounds === 'undefined') { - newBounds = getLayoutedBounds(label, text, textUtil); + newBounds = textRenderer.getLayoutedBounds(label, text); } // setting newBounds to false or _null_ will @@ -132,33 +128,7 @@ export default function UpdateLabelHandler(modeling) { this.postExecute = postExecute; } -UpdateLabelHandler.$inject = [ 'modeling' ]; - - -// TODO(nikku): repeating code (search for ) - -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) - }; -} \ No newline at end of file +UpdateLabelHandler.$inject = [ + 'modeling', + 'textRenderer' +]; \ No newline at end of file diff --git a/lib/features/modeling/behavior/LabelBehavior.js b/lib/features/modeling/behavior/LabelBehavior.js index 38c12a49..5f72dc3b 100644 --- a/lib/features/modeling/behavior/LabelBehavior.js +++ b/lib/features/modeling/behavior/LabelBehavior.js @@ -20,8 +20,6 @@ import { import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; -import TextUtil from 'diagram-js/lib/util/Text'; - var DEFAULT_LABEL_DIMENSIONS = { width: 90, height: 20 @@ -36,13 +34,14 @@ var DEFAULT_LABEL_DIMENSIONS = { * @param {EventBus} eventBus * @param {Modeling} modeling * @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); - var textUtil = new TextUtil(); - // update label if name property was updated this.postExecute('element.updateProperties', function(e) { var context = e.context, @@ -73,10 +72,9 @@ export default function LabelBehavior(eventBus, modeling, bpmnFactory) { var labelCenter = getExternalLabelMid(element); // we don't care about x and y - var labelDimensions = getLayoutedBounds( + var labelDimensions = textRenderer.getLayoutedBounds( DEFAULT_LABEL_DIMENSIONS, - businessObject.name || '', - textUtil + businessObject.name || '' ); modeling.createLabel(element, labelCenter, { @@ -200,34 +198,6 @@ inherits(LabelBehavior, CommandInterceptor); LabelBehavior.$inject = [ 'eventBus', 'modeling', - 'bpmnFactory' -]; - - -// TODO(nikku): repeating code (search for ) - -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) - }; -} + 'bpmnFactory', + 'textRenderer' +]; \ No newline at end of file diff --git a/lib/features/modeling/cmd/UpdatePropertiesHandler.js b/lib/features/modeling/cmd/UpdatePropertiesHandler.js index 8b265d8d..10de4bf5 100644 --- a/lib/features/modeling/cmd/UpdatePropertiesHandler.js +++ b/lib/features/modeling/cmd/UpdatePropertiesHandler.js @@ -9,8 +9,6 @@ import { getBusinessObject } from '../../../util/ModelUtil'; -import TextUtil from 'diagram-js/lib/util/Text'; - var DEFAULT_FLOW = 'default', ID = 'id', DI = 'di'; @@ -29,20 +27,23 @@ var NULL_DIMENSIONS = { * Use respective diagram-js provided handlers if you would * 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._moddle = moddle; this._translate = translate; this._modeling = modeling; - - this._textUtil = new TextUtil(); + this._textRenderer = textRenderer; } UpdatePropertiesHandler.$inject = [ 'elementRegistry', 'moddle', 'translate', - 'modeling' + 'modeling', + 'textRenderer' ]; @@ -119,7 +120,7 @@ UpdatePropertiesHandler.prototype.postExecute = function(context) { // get layouted text bounds and resize external // external label accordingly - var newLabelBounds = getLayoutedBounds(label, text, this._textUtil); + var newLabelBounds = this._textRenderer.getLayoutedBounds(label, text); this._modeling.resizeShape(label, newLabelBounds, NULL_DIMENSIONS); }; @@ -230,33 +231,4 @@ function unwrapBusinessObjects(properties) { }); return unwrappedProps; -} - - -// TODO(nikku): repeating code (search for ) - -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) - }; } \ No newline at end of file diff --git a/lib/import/BpmnImporter.js b/lib/import/BpmnImporter.js index ccf04562..18d84233 100644 --- a/lib/import/BpmnImporter.js +++ b/lib/import/BpmnImporter.js @@ -3,8 +3,6 @@ import { map } from 'min-dash'; -import TextUtil from 'diagram-js/lib/util/Text'; - import { is } from '../util/ModelUtil'; import { @@ -56,19 +54,18 @@ function notYetDrawn(translate, semantic, refSemantic, property) { * @param {ElementFactory} elementFactory * @param {ElementRegistry} elementRegistry * @param {Function} translate + * @param {TextRenderer} textRenderer */ export default function BpmnImporter( eventBus, canvas, elementFactory, - elementRegistry, translate) { + elementRegistry, translate, textRenderer) { this._eventBus = eventBus; this._canvas = canvas; - this._elementFactory = elementFactory; this._elementRegistry = elementRegistry; this._translate = translate; - - this._textUtil = new TextUtil(); + this._textRenderer = textRenderer; } BpmnImporter.$inject = [ @@ -76,7 +73,8 @@ BpmnImporter.$inject = [ 'canvas', 'elementFactory', 'elementRegistry', - 'translate' + 'translate', + 'textRenderer' ]; @@ -239,7 +237,7 @@ BpmnImporter.prototype.addLabel = function(semantic, element) { if (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, { @@ -312,33 +310,7 @@ BpmnImporter.prototype._getElement = function(semantic) { }; -// TODO(nikku): repeating code (search for ) - -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) - }; -} +// helpers //////////////////// function isPointInsideBBox(bbox, point) { var x = point.x, diff --git a/test/spec/draw/TextRenderer.bpmn b/test/spec/draw/TextRenderer.bpmn new file mode 100644 index 00000000..36441f82 --- /dev/null +++ b/test/spec/draw/TextRenderer.bpmn @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/draw/TextRendererSpec.js b/test/spec/draw/TextRendererSpec.js new file mode 100644 index 00000000..d16e6791 --- /dev/null +++ b/test/spec/draw/TextRendererSpec.js @@ -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'); + })); + + }); + +}); \ No newline at end of file