From 86e5d4d66bbc16638571c02db8dc892437620a4c Mon Sep 17 00:00:00 2001 From: "jan.ladleif" Date: Thu, 29 Oct 2020 17:32:39 +0100 Subject: [PATCH 1/4] Add support for XML coloring in the renderer --- lib/draw/ChoreoRenderer.js | 297 ++++++++++++++++++++----------------- 1 file changed, 160 insertions(+), 137 deletions(-) diff --git a/lib/draw/ChoreoRenderer.js b/lib/draw/ChoreoRenderer.js index d7b20be..140d20e 100644 --- a/lib/draw/ChoreoRenderer.js +++ b/lib/draw/ChoreoRenderer.js @@ -1,25 +1,9 @@ import inherits from 'inherits'; - import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; - -import { - translate -} from 'diagram-js/lib/util/SvgTransformUtil'; - -import { - componentsToPath -} from 'diagram-js/lib/util/RenderUtil'; - -import { - assign -} from 'min-dash'; - -import { - heightOfBottomBands, - heightOfTopBands, - hasBandMarker -} from '../util/BandUtil'; - +import { translate} from 'diagram-js/lib/util/SvgTransformUtil'; +import { componentsToPath } from 'diagram-js/lib/util/RenderUtil'; +import { assign } from 'min-dash'; +import { is } from 'bpmn-js/lib/util/ModelUtil'; import { append as svgAppend, attr as svgAttr, @@ -28,28 +12,46 @@ import { } from 'tiny-svg'; import { - MESSAGE_DISTANCE -} from '../util/MessageUtil'; -import { is } from 'bpmn-js/lib/util/ModelUtil'; + heightOfBottomBands, + heightOfTopBands, + hasBandMarker +} from '../util/BandUtil'; +import { MESSAGE_DISTANCE } from '../util/MessageUtil'; -// display specific constants that are not part of the BPMNDI standard +// Renderer configuration parameters const CHOREO_TASK_ROUNDING = 10; const MARKER_HEIGHT = 15; -const DEFAULT_FILL_OPACITY = .95; -const NON_INITIATING_OPACITY = .1725; +const DEFAULT_FILL_OPACITY = 0.95; +const NON_INITIATING_OPACITY = 0.1725; +const DEFAULT_NON_INITIATING_FILL = 'rgb(211,211,211)'; /** * A renderer for BPMN 2.0 choreography diagrams. - * @constructor - * @param {EventBus} eventBus - * @param {Styles} styles - * @param {TextRenderer} textRenderer - * @param {PathMap} pathMap */ -export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) { +export default function ChoreoRenderer(config, eventBus, textRenderer, pathMap) { + + // Because of the priority this does not work with the injector + BaseRenderer.call(this, eventBus, 2000); + + // Convenience functions to get the first color from a prioritized list. + // First, get the color from the XML DI. Then, get the color from the config. + // Then, take the given override color. If none of those are given, fills are + // white and strokes are black. + function getFillColor(di, override) { + return di.get('bioc:fill') || + (config && config.defaultFillColor) || + override || + 'white'; + } - BaseRenderer.call(this, eventBus, 2000); // Because of the priority this does not work with the injector + function getStrokeColor(di, override) { + return di.get('bioc:stroke') || + (config && config.defaultStrokeColor) || + override || + 'black'; + } + // Label convenience functions function getLabel(caption, options) { var text = textRenderer.createText(caption || '', options); svgClasses(text).add('djs-label'); @@ -71,49 +73,42 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) return label; } + // Message drawing function this.drawMessage = function(p, element) { let bandKind = element.parent.diBand.participantBandKind || 'top-initiating'; let isBottom = bandKind.startsWith('bottom'); let isInitiating = !bandKind.endsWith('non_initiating'); - // first, draw the connecting dotted line + // First, draw the connecting dotted line let connector = svgCreate('path'); svgAttr(connector, { d: componentsToPath([ ['M', element.width / 2, isBottom ? -MESSAGE_DISTANCE : element.height], ['l', 0, MESSAGE_DISTANCE] ]), - stroke: 'black', + stroke: getStrokeColor(element.parent.diBand), strokeWidth: 2, strokeDasharray: '0, 4', strokeLinecap: 'round' }); svgAppend(p, connector); - // draw background - let rect = svgCreate('rect'); - svgAttr(rect, { - x: 0, - y: 0, - width: element.width, - height: element.height, - fill: 'white', - fillOpacity: DEFAULT_FILL_OPACITY, - }); - svgAppend(p, rect); - - // then, draw the envelope + // Then, draw the envelope let envelope = svgCreate('path'); + let fillColorOverride; + if (!isInitiating) { + fillColorOverride = DEFAULT_NON_INITIATING_FILL; + } svgAttr(envelope, { d: getEnvelopePath(element.width, element.height), - stroke: 'black', + stroke: getStrokeColor(element.parent.diBand), strokeWidth: 2, - fill: isInitiating ? 'white' : 'black', - fillOpacity: isInitiating ? 0 : NON_INITIATING_OPACITY, + fill: getFillColor(element.parent.diBand, fillColorOverride), + fillOpacity: DEFAULT_FILL_OPACITY }); svgAppend(p, envelope); - // then, attach the label + // Then, attach the label if (element.businessObject.name) { let label = getBoxedLabel(element.businessObject.name, { x: - element.parent.width / 2 + element.width / 2, @@ -127,6 +122,7 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) return p; }; + // Participant band drawing function this.drawParticipantBand = function(p, element) { const bandKind = element.diBand.participantBandKind || 'top-initiating'; const isInitiating = !bandKind.endsWith('non_initiating'); @@ -134,43 +130,70 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) const isBottom = bandKind.startsWith('bottom'); const isMiddle = !isTop && !isBottom; - // draw the participant band - let bandShape = svgCreate('path'); - svgAttr(bandShape, { - d: getParticipantBandOutline(0, 0, element.width, element.height, bandKind), - fill: isInitiating ? 'white' : 'black', - fillOpacity: isInitiating ? 0 : NON_INITIATING_OPACITY - }); - svgAppend(p, bandShape); - attachMarkerToParticipant(p, element); - - // add the line(s) - if (isTop || isMiddle) { - let line = svgCreate('path'); - svgAttr(line, { - d: componentsToPath([ - ['M', 0, element.height], - ['l', element.width, 0] - ]), - stroke: 'black', - strokeWidth: 2 + const bandFill = element.diBand.get('bioc:fill'); + const bandStroke = element.diBand.get('bioc:stroke'); + const activityFill = element.parent.businessObject.di.get('bioc:fill'); + const activityStroke = element.parent.businessObject.di.get('bioc:stroke'); + + const needsSolidFill = bandFill || activityFill; + const needsAdditionalOutline = bandStroke || activityStroke; + + // Draw the participant band background if necessary, that is, if the band is + // non-initiating or has a custom fill + if (!isInitiating || needsSolidFill) { + let bandShape = svgCreate('path'); + svgAttr(bandShape, { + d: getParticipantBandOutline(0, 0, element.width, element.height, bandKind), + fill: getFillColor(element.diBand, isInitiating ? 'white' : 'black'), + fillOpacity: bandFill || isInitiating ? DEFAULT_FILL_OPACITY * (!bandFill ? 0.75 : 1) : NON_INITIATING_OPACITY }); - svgAppend(p, line); + svgAppend(p, bandShape); } - if (isBottom || isMiddle) { - let line = svgCreate('path'); - svgAttr(line, { - d: componentsToPath([ - ['M', 0, 0], - ['l', element.width, 0] - ]), - stroke: 'black', + attachMarkerToParticipant(p, element); + + // If we have a custom fill or stroke, we have to again place a stroke around the + // band to fix rendering artifacts around the task stroke + if (needsSolidFill || needsAdditionalOutline) { + let bandOutline = svgCreate('path'); + svgAttr(bandOutline, { + d: getParticipantBandOutline(0, 0, element.width, element.height, bandKind), + fill: 'none', + stroke: getStrokeColor( + element.diBand, + getStrokeColor(element.parent.businessObject.di) + ), strokeWidth: 2 }); - svgAppend(p, line); + svgAppend(p, bandOutline); + } else { + // Otherwise, a line below or above the band is enough + if (isTop || isMiddle) { + let line = svgCreate('path'); + svgAttr(line, { + d: componentsToPath([ + ['M', 0, element.height], + ['l', element.width, 0] + ]), + stroke: getStrokeColor(element.parent.businessObject.di), + strokeWidth: 2 + }); + svgAppend(p, line); + } + if (isBottom || isMiddle) { + let line = svgCreate('path'); + svgAttr(line, { + d: componentsToPath([ + ['M', 0, 0], + ['l', element.width, 0] + ]), + stroke: getStrokeColor(element.parent.businessObject.di), + strokeWidth: 2 + }); + svgAppend(p, line); + } } - // add the name of the participant + // Add the name of the participant let label = getBoxedLabel(element.businessObject.name, { x: 0, y: 0, @@ -182,19 +205,25 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) return p; }; + // Choreography activity drawing function this.drawChoreographyActivity = function(p, element) { + // Draw the outer stroke and background let shape = svgCreate('path'); svgAttr(shape, { - d: getTaskOutline(0, 0, element.width, element.height, is(element, 'bpmn:CallChoreography') ? 2 : 0), - fill: 'white', + d: getTaskOutline( + 0, 0, element.width, element.height,is(element, 'bpmn:CallChoreography') ? 2 : 0 + ), + fill: getFillColor(element.businessObject.di), fillOpacity: DEFAULT_FILL_OPACITY, - stroke: 'black', + stroke: getStrokeColor(element.businessObject.di), strokeWidth: is(element, 'bpmn:CallChoreography') ? 6 : 2 }); svgAppend(p, shape); + // Attach markers let hasMarkers = attachMarkerToChoreoActivity(p, element); + // Place the label correctly taking into account whether we have markers let top = heightOfTopBands(element); let bottom = element.height - heightOfBottomBands(element) - (hasMarkers ? 20 : 0); let align = 'center-middle'; @@ -209,12 +238,16 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) }, align); svgAppend(p, label); - return print; + return p; }; + // Marker attachment functions function attachMarkerToChoreoActivity(parentGfx, element) { - const defaultFillColor = 'transparent'; - const defaultStrokeColor = 'black'; + // Markers are styled according to the activity they are attached to + let style = { + fill: 'none', + stroke: getStrokeColor(element.businessObject.di) + }; const bottomBandHeight = heightOfBottomBands(element); let loopType = element.businessObject.loopType; @@ -226,13 +259,12 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) let isCollapsed = element.collapsed; let offset = (isCollapsed && hasLoopMarker) ? 10 : 0; - // draw sub choreography marker + // Draw sub choreography marker if (isCollapsed) { translate( - drawRect(parentGfx, 14, 14, { - fill: defaultFillColor, - stroke: defaultStrokeColor - }), + drawRect(parentGfx, 14, 14, assign({ + strokeWidth: 2 + }, style)), element.width / 2 - 7.5 + offset, element.height - bottomBandHeight - 20 ); @@ -246,13 +278,10 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) my: (element.height - bottomBandHeight - 20) / element.height } }); - drawMarker('sub-process', parentGfx, markerPath, { - fill: defaultFillColor, - stroke: defaultStrokeColor - }); + drawMarker('sub-process', parentGfx, markerPath, style); } - // draw loop markers + // Draw loop markers if (hasLoopMarker) { let loopName; let pathAttr = { @@ -261,10 +290,6 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) containerWidth: element.width, containerHeight: element.height }; - let drawAttr = { - fill: defaultFillColor, - stroke: defaultStrokeColor - }; if (loopType === 'Standard') { loopName = 'loop'; @@ -272,7 +297,7 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) mx: ((element.width / 2 - offset) / element.width), my: (element.height - 7 - bottomBandHeight) / element.height }; - assign(drawAttr, { + assign(style, { strokeWidth: 1, strokeLinecap: 'round', strokeMiterlimit: 0.5 @@ -292,15 +317,22 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) } let markerPath = pathMap.getScaledPath('MARKER_' + loopName.toUpperCase(), pathAttr); - drawMarker(loopName, parentGfx, markerPath, drawAttr); + drawMarker(loopName, parentGfx, markerPath, style); } return isCollapsed || hasLoopMarker; } function attachMarkerToParticipant(parentGfx, element) { - const defaultFillColor = 'transparent'; - const defaultStrokeColor = 'black'; + // Participant band markers are either styled by the band itself, or the + // activity the participant band is attached to + const style = { + fill: 'none', + stroke: getStrokeColor( + element.diBand, + getStrokeColor(element.parent.businessObject.di) + ), + }; if (hasBandMarker(element.businessObject)) { const markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { xScaleFactor: 1, @@ -312,20 +344,12 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) my: (element.height - MARKER_HEIGHT) / element.height } }); - drawMarker('participant-multiplicity', parentGfx, markerPath, { - strokeWidth: 1, - fill: defaultFillColor, - stroke: defaultStrokeColor - }); + drawMarker('participant-multiplicity', parentGfx, markerPath, style); } } function drawMarker(type, parentGfx, d, attrs) { attrs = assign({ 'data-marker': type }, attrs); - attrs = styles.computeStyle(attrs, ['no-fill'], { - strokeWidth: 2, - stroke: 'black' - }); const path = svgCreate('path'); svgAttr(path, { d: d }); @@ -335,32 +359,13 @@ export default function ChoreoRenderer(eventBus, styles, textRenderer, pathMap) return path; } - - function drawRect(parentGfx, width, height, attrs) { - assign(attrs, { - stroke: 'black', - strokeWidth: 2, - fill: 'white' - }); - - let rect = svgCreate('rect'); - svgAttr(rect, { - x: 0, - y: 0, - width: width, - height: height - }); - svgAttr(rect, attrs); - svgAppend(parentGfx, rect); - return rect; - } } inherits(ChoreoRenderer, BaseRenderer); ChoreoRenderer.$inject = [ + 'config', 'eventBus', - 'styles', 'textRenderer', 'pathMap' ]; @@ -391,6 +396,11 @@ ChoreoRenderer.prototype.getShapePath = function(shape) { } }; +/** + * The functions below are used to produce some of the more involved + * vector paths used in the renderer. + */ + function getEnvelopePath(width, height) { let flap = height * 0.6; let path = [ @@ -475,4 +485,17 @@ function getParticipantBandOutline(x, y, width, height, participantBandKind) { ]; } return componentsToPath(path); -} \ No newline at end of file +} + +function drawRect(parentGfx, width, height, attrs) { + let rect = svgCreate('rect'); + svgAttr(rect, { + x: 0, + y: 0, + width: width, + height: height + }); + svgAttr(rect, attrs); + svgAppend(parentGfx, rect); + return rect; +} From 40a7af2afba78a5401e7cc2cc1f51e7cf1d0ef5d Mon Sep 17 00:00:00 2001 From: "jan.ladleif" Date: Thu, 29 Oct 2020 17:37:21 +0100 Subject: [PATCH 2/4] Fix linter --- lib/draw/ChoreoRenderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/draw/ChoreoRenderer.js b/lib/draw/ChoreoRenderer.js index 140d20e..5842c5e 100644 --- a/lib/draw/ChoreoRenderer.js +++ b/lib/draw/ChoreoRenderer.js @@ -1,6 +1,6 @@ import inherits from 'inherits'; import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; -import { translate} from 'diagram-js/lib/util/SvgTransformUtil'; +import { translate } from 'diagram-js/lib/util/SvgTransformUtil'; import { componentsToPath } from 'diagram-js/lib/util/RenderUtil'; import { assign } from 'min-dash'; import { is } from 'bpmn-js/lib/util/ModelUtil'; From 9650da3fdc6f622d56458c73f323baf2ef93c5fa Mon Sep 17 00:00:00 2001 From: "jan.ladleif" Date: Tue, 3 Nov 2020 15:52:31 +0100 Subject: [PATCH 3/4] Override SetColorHandler to properly handle choreography elements --- lib/features/modeling/ChoreoModeling.js | 2 + .../modeling/cmd/ChoreoSetColorHandler.js | 109 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 lib/features/modeling/cmd/ChoreoSetColorHandler.js diff --git a/lib/features/modeling/ChoreoModeling.js b/lib/features/modeling/ChoreoModeling.js index 2a5eebc..eaa8ed4 100644 --- a/lib/features/modeling/ChoreoModeling.js +++ b/lib/features/modeling/ChoreoModeling.js @@ -14,6 +14,7 @@ import ChoreoAppendShapeHandler from './cmd/ChoreoAppendShapeHandler'; import LinkCallChoreoHandler from './cmd/LinkCallChoreoHandler'; import LinkCallChoreoParticipantHandler from './cmd/LinkCallChoreoParticipantHandler'; import ChoreoParticipantHandler from './cmd/ChoreoParticipantHandler.js'; +import ChoreoSetColorHandler from './cmd/ChoreoSetColorHandler'; /** * Component that manages choreography specific modeling moves that attach to the @@ -42,6 +43,7 @@ ChoreoModeling.prototype.getHandlers = function() { handlers['message.toggle'] = ToggleMessageVisibilityHandler; handlers['message.add'] = AddMessageHandler; handlers['element.updateLabel'] = UpdateMessageLabelHandler; + handlers['element.setColor'] = ChoreoSetColorHandler; handlers['participant.toggleMultiplicity'] = ParticipantMultiplicityHandler; handlers['shape.append'] = ChoreoAppendShapeHandler; handlers['link.callChoreo'] = LinkCallChoreoHandler; diff --git a/lib/features/modeling/cmd/ChoreoSetColorHandler.js b/lib/features/modeling/cmd/ChoreoSetColorHandler.js new file mode 100644 index 0000000..9c6d4d0 --- /dev/null +++ b/lib/features/modeling/cmd/ChoreoSetColorHandler.js @@ -0,0 +1,109 @@ +import inherits from 'inherits'; + +import SetColorHandler from 'bpmn-js/lib/features/modeling/cmd/SetColorHandler'; + +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import { + assign, + filter, + forEach, + pick +} from 'min-dash'; + +const DEFAULT_COLORS = { + fill: undefined, + stroke: undefined +}; + +/** + * The regular bpmn-js SetColorHandler uses the 'updateProperties' command to change the + * DI information. However, that one has hardcoded exceptions for DI properties, and we can + * not easily extend it with support for messages and participant bands which use the + * 'diBand' property. So we implement the command for them manually, and delegate everything + * else to the super class. + * + * @param {*} commandStack + */ +export default function ChoreoSetColorHandler(commandStack) { + this._commandStack = commandStack; +} + +ChoreoSetColorHandler.$inject = [ + 'commandStack' +]; + +inherits(ChoreoSetColorHandler, SetColorHandler); + +ChoreoSetColorHandler.prototype.execute = function(context) { + const elements = context.elements; + const colors = context.colors || DEFAULT_COLORS; + context.oldColors = {}; + + let changed = []; + + forEach(elements, function(element) { + if (is(element, 'bpmn:Message')) { + element = element.parent; + } + + if (is(element, 'bpmn:Participant')) { + context.oldColors[element.id] = pick(element.diBand, ['fill', 'stroke']); + + // This slightly complicated method makes sure that you can pass undefined to + // erase custom coloring, and pass 'nothing' to not change the color at all + if ('fill' in colors) { + element.diBand.fill = colors.fill; + } + if ('stroke' in colors) { + element.diBand.stroke = colors.stroke; + } + + changed.push(element); + changed.push(...element.children); + } else if (is(element, 'bpmn:ChoreographyActivity')) { + // Participant band rendering is also influenced by the activity color + changed.push(...element.children); + } + }); + + return changed; +}; + +ChoreoSetColorHandler.prototype.revert = function(context) { + const elements = context.elements; + const colors = context.colors || DEFAULT_COLORS; + + let changed = []; + + forEach(elements, function(element) { + if (is(element, 'bpmn:Message')) { + element = element.parent; + } + + if (is(element, 'bpmn:Participant')) { + if ('fill' in colors) { + element.diBand.fill = context.oldColors[element.id].fill; + } + if ('stroke' in colors) { + element.diBand.stroke = context.oldColors[element.id].stroke; + } + + changed.push(element); + changed.push(...element.children); + } else if (is(element, 'bpmn:ChoreographyActivity')) { + changed.push(...element.children); + } + }); + + return changed; +}; + +ChoreoSetColorHandler.prototype.postExecute = function(context) { + // Delegate all remaining elements to the super handler + let newContext = assign({}, context); + newContext.elements = filter(newContext.elements, function(element) { + return !is(element, 'bpmn:Participant') && !is(element, 'bpmn:Message'); + }); + SetColorHandler.prototype.postExecute.call(this, context); +}; From 65a2448ada708d2c4efe3596e16b2ef7c869ecf1 Mon Sep 17 00:00:00 2001 From: "jan.ladleif" Date: Tue, 3 Nov 2020 15:55:04 +0100 Subject: [PATCH 4/4] Fix small oversight --- lib/features/modeling/cmd/ChoreoSetColorHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/modeling/cmd/ChoreoSetColorHandler.js b/lib/features/modeling/cmd/ChoreoSetColorHandler.js index 9c6d4d0..388f7dd 100644 --- a/lib/features/modeling/cmd/ChoreoSetColorHandler.js +++ b/lib/features/modeling/cmd/ChoreoSetColorHandler.js @@ -105,5 +105,5 @@ ChoreoSetColorHandler.prototype.postExecute = function(context) { newContext.elements = filter(newContext.elements, function(element) { return !is(element, 'bpmn:Participant') && !is(element, 'bpmn:Message'); }); - SetColorHandler.prototype.postExecute.call(this, context); + SetColorHandler.prototype.postExecute.call(this, newContext); };