Skip to content

Commit d340bae

Browse files
authored
feat: add support for 'BPMN in Color' (#2614)
Support the 'BPMN in Color' and use bpmn.io colors as fallback. The BPMN parser always extract the colors, but by default, they are ignored by the Renderer. It is possible to enable the support in the Renderer by setting an options at the library initialization. This introduces internal management of BPMN extensions for BPMNShape, BPMNEdge and BPMNLabel. The parsing tests are using diagrams taken from the 'BPMN in Color' specification sample (this ensure that the implementation is compliant with the specification) and from the original issue requesting support for modeling colors as an example of bpmn.io colors.
1 parent 48c678e commit d340bae

30 files changed

Lines changed: 1889 additions & 56 deletions

dev/ts/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,23 @@ function configurePoolsFilteringFromParameters(parameters: URLSearchParams): Mod
273273
export function startBpmnVisualization(config: BpmnVisualizationDemoConfiguration): void {
274274
const log = logStartup;
275275
log(`Initializing BpmnVisualization with container '${config.globalOptions.container}'...`);
276+
277+
const parameters = new URLSearchParams(window.location.search);
278+
const rendererIgnoreBpmnColors = parameters.get('renderer.ignore.bpmn.colors');
279+
if (rendererIgnoreBpmnColors) {
280+
const ignoreBpmnColors = rendererIgnoreBpmnColors === 'true';
281+
log('Ignore support for "BPMN in Color"?', ignoreBpmnColors);
282+
!config.globalOptions.renderer && (config.globalOptions.renderer = {});
283+
config.globalOptions.renderer.ignoreBpmnColors = ignoreBpmnColors;
284+
}
285+
276286
bpmnVisualization = new ThemedBpmnVisualization(config.globalOptions);
277287
log('Initialization completed');
278288
new DropFileUserInterface(window, 'drop-container', bpmnVisualization.graph.container, readAndLoadFile);
279289
log('Drag&Drop support initialized');
280290

281291
statusKoNotifier = config.statusKoNotifier ?? logOnlyStatusKoNotifier;
282292

283-
const parameters = new URLSearchParams(window.location.search);
284-
285293
log('Configuring Load Options');
286294
loadOptions = config.loadOptions || {};
287295
loadOptions.fit = getFitOptionsFromParameters(config, parameters);

docs/users/architecture/images/architecture/internal-model.drawio

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/users/architecture/images/architecture/internal-model.svg

Lines changed: 2 additions & 1 deletion
Loading
33 KB
Loading

docs/users/overview.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ image::images/bpmn-theme-custom-colors.png[Custom BPMN Theme]
129129
_Custom BPMN Theme_
130130

131131

132+
==== `BPMN in Color` Support
133+
134+
As of version 0.35.0, `bpmn-visualization` supports the https://github.com/bpmn-miwg/bpmn-in-color[BPMN in Color Specification] with a fallback to the
135+
https://github.com/bpmn-io/bpmn-moddle/blob/ea7fa6a94c55f49fe1da1f019dc9a40d62967252/resources/bpmn-io/json/bioc.json[bpmn.io specific BPMN extensions for colors] (a.k.a `bioc`).
136+
137+
This support is disabled by default, and it can be enabled at the library initialization.
138+
139+
image::images/bpmn-in-color-C.1.0.png[C.1.0 with "BPMN in Color"]
140+
141+
_miwg-test-suite C.1.0 reference diagram using "BPMN in Color"_
142+
143+
132144
[[diagram-navigation]]
133145
=== Diagram Navigation
134146

src/component/BpmnVisualization.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import GraphConfigurator from './mxgraph/GraphConfigurator';
1818
import { newBpmnRenderer } from './mxgraph/BpmnRenderer';
1919
import { newBpmnParser } from './parser/BpmnParser';
2020
import type { BpmnGraph } from './mxgraph/BpmnGraph';
21-
import type { GlobalOptions, LoadOptions, ParserOptions } from './options';
21+
import type { GlobalOptions, LoadOptions, ParserOptions, RendererOptions } from './options';
2222
import type { BpmnElementsRegistry } from './registry';
2323
import { newBpmnElementsRegistry } from './registry/bpmn-elements-registry';
2424
import { BpmnModelRegistry } from './registry/bpmn-model-registry';
@@ -69,7 +69,10 @@ export class BpmnVisualization {
6969

7070
private readonly parserOptions: ParserOptions;
7171

72+
private readonly rendererOptions: RendererOptions;
73+
7274
constructor(options: GlobalOptions) {
75+
this.rendererOptions = options?.renderer;
7376
// mxgraph configuration
7477
const configurator = new GraphConfigurator(htmlElement(options?.container));
7578
this.graph = configurator.configure(options);
@@ -89,7 +92,7 @@ export class BpmnVisualization {
8992
load(xml: string, options?: LoadOptions): void {
9093
const bpmnModel = newBpmnParser(this.parserOptions).parse(xml);
9194
const renderedModel = this.bpmnModelRegistry.load(bpmnModel, options?.modelFilter);
92-
newBpmnRenderer(this.graph).render(renderedModel, options?.fit);
95+
newBpmnRenderer(this.graph, this.rendererOptions).render(renderedModel, options?.fit);
9396
}
9497

9598
getVersion(): Version {

src/component/mxgraph/BpmnRenderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { MessageVisibleKind, ShapeUtil } from '../../model/bpmn/internal';
2323
import CoordinatesTranslator from './renderer/CoordinatesTranslator';
2424
import StyleComputer from './renderer/StyleComputer';
2525
import type { BpmnGraph } from './BpmnGraph';
26-
import type { FitOptions } from '../options';
26+
import type { FitOptions, RendererOptions } from '../options';
2727
import type { RenderedModel } from '../registry/bpmn-model-registry';
2828
import { mxgraph } from './initializer';
2929
import type { mxCell } from 'mxgraph';
@@ -139,8 +139,8 @@ export class BpmnRenderer {
139139
/**
140140
* @internal
141141
*/
142-
export function newBpmnRenderer(graph: BpmnGraph): BpmnRenderer {
143-
return new BpmnRenderer(graph, new CoordinatesTranslator(graph), new StyleComputer());
142+
export function newBpmnRenderer(graph: BpmnGraph, options: RendererOptions): BpmnRenderer {
143+
return new BpmnRenderer(graph, new CoordinatesTranslator(graph), new StyleComputer(options));
144144
}
145145

146146
/**

src/component/mxgraph/renderer/StyleComputer.ts

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,45 @@ import { BpmnStyleIdentifier } from '../style';
3131
import { MessageVisibleKind, ShapeBpmnCallActivityKind, ShapeBpmnElementKind, ShapeBpmnMarkerKind, ShapeUtil } from '../../../model/bpmn/internal';
3232
import { AssociationFlow, SequenceFlow } from '../../../model/bpmn/internal/edge/flows';
3333
import type { Font } from '../../../model/bpmn/internal/Label';
34+
import type { RendererOptions } from '../../options';
3435

3536
/**
3637
* @internal
3738
*/
3839
export default class StyleComputer {
40+
private readonly ignoreBpmnColors: boolean;
41+
42+
constructor(options?: RendererOptions) {
43+
this.ignoreBpmnColors = options?.ignoreBpmnColors ?? true;
44+
}
45+
3946
computeStyle(bpmnCell: Shape | Edge, labelBounds: Bounds): string {
4047
const styles: string[] = [bpmnCell.bpmnElement.kind as string];
4148

42-
let shapeStyleValues;
49+
let mainStyleValues;
4350
if (bpmnCell instanceof Shape) {
44-
shapeStyleValues = StyleComputer.computeShapeStyle(bpmnCell);
51+
mainStyleValues = this.computeShapeStyleValues(bpmnCell);
4552
} else {
46-
styles.push(...StyleComputer.computeEdgeStyle(bpmnCell));
47-
shapeStyleValues = new Map<string, string | number>();
53+
styles.push(...StyleComputer.computeEdgeBaseStyles(bpmnCell));
54+
mainStyleValues = this.computeEdgeStyleValues(bpmnCell);
4855
}
4956

50-
const fontStyleValues = StyleComputer.computeFontStyleValues(bpmnCell);
57+
const fontStyleValues = this.computeFontStyleValues(bpmnCell);
5158
const labelStyleValues = StyleComputer.computeLabelStyleValues(bpmnCell, labelBounds);
5259

53-
return [] //
54-
.concat([...styles])
55-
.concat([...shapeStyleValues, ...fontStyleValues, ...labelStyleValues].filter(([, v]) => v && v != 'undefined').map(([key, value]) => key + '=' + value))
60+
return styles //
61+
.concat(toArrayOfMxGraphStyleEntries([...mainStyleValues, ...fontStyleValues, ...labelStyleValues]))
5662
.join(';');
5763
}
5864

59-
private static computeShapeStyle(shape: Shape): Map<string, string | number> {
65+
private computeShapeStyleValues(shape: Shape): Map<string, string | number> {
6066
const styleValues = new Map<string, string | number>();
6167
const bpmnElement = shape.bpmnElement;
6268

6369
if (bpmnElement instanceof ShapeBpmnEvent) {
64-
this.computeEventShapeStyle(bpmnElement, styleValues);
70+
StyleComputer.computeEventShapeStyle(bpmnElement, styleValues);
6571
} else if (bpmnElement instanceof ShapeBpmnActivity) {
66-
this.computeActivityShapeStyle(bpmnElement, styleValues);
72+
StyleComputer.computeActivityShapeStyle(bpmnElement, styleValues);
6773
} else if (ShapeUtil.isPoolOrLane(bpmnElement.kind)) {
6874
// mxgraph.mxConstants.STYLE_HORIZONTAL is for the label
6975
// In BPMN, isHorizontal is for the Shape
@@ -74,6 +80,18 @@ export default class StyleComputer {
7480
styleValues.set(BpmnStyleIdentifier.EVENT_BASED_GATEWAY_KIND, String(bpmnElement.gatewayKind));
7581
}
7682

83+
if (!this.ignoreBpmnColors) {
84+
const extensions = shape.extensions;
85+
const fillColor = extensions.fillColor;
86+
if (fillColor) {
87+
styleValues.set(mxgraph.mxConstants.STYLE_FILLCOLOR, fillColor);
88+
if (ShapeUtil.isPoolOrLane(bpmnElement.kind)) {
89+
styleValues.set(mxgraph.mxConstants.STYLE_SWIMLANE_FILLCOLOR, fillColor);
90+
}
91+
}
92+
extensions.strokeColor && styleValues.set(mxgraph.mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
93+
}
94+
7795
return styleValues;
7896
}
7997

@@ -100,7 +118,7 @@ export default class StyleComputer {
100118
}
101119
}
102120

103-
private static computeEdgeStyle(edge: Edge): string[] {
121+
private static computeEdgeBaseStyles(edge: Edge): string[] {
104122
const styles: string[] = [];
105123

106124
const bpmnElement = edge.bpmnElement;
@@ -114,7 +132,18 @@ export default class StyleComputer {
114132
return styles;
115133
}
116134

117-
private static computeFontStyleValues(bpmnCell: Shape | Edge): Map<string, string | number> {
135+
private computeEdgeStyleValues(edge: Edge): Map<string, string | number> {
136+
const styleValues = new Map<string, string | number>();
137+
138+
if (!this.ignoreBpmnColors) {
139+
const extensions = edge.extensions;
140+
extensions.strokeColor && styleValues.set(mxgraph.mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
141+
}
142+
143+
return styleValues;
144+
}
145+
146+
private computeFontStyleValues(bpmnCell: Shape | Edge): Map<string, string | number> {
118147
const styleValues = new Map<string, string | number>();
119148

120149
const font = bpmnCell.label?.font;
@@ -124,6 +153,11 @@ export default class StyleComputer {
124153
styleValues.set(mxgraph.mxConstants.STYLE_FONTSTYLE, getFontStyleValue(font));
125154
}
126155

156+
if (!this.ignoreBpmnColors) {
157+
const extensions = bpmnCell.label?.extensions;
158+
extensions?.color && styleValues.set(mxgraph.mxConstants.STYLE_FONTCOLOR, extensions.color);
159+
}
160+
127161
return styleValues;
128162
}
129163

@@ -162,7 +196,14 @@ export default class StyleComputer {
162196
}
163197

164198
computeMessageFlowIconStyle(edge: Edge): string {
165-
return `shape=${BpmnStyleIdentifier.MESSAGE_FLOW_ICON};${BpmnStyleIdentifier.IS_INITIATING}=${edge.messageVisibleKind === MessageVisibleKind.INITIATING}`;
199+
const styleValues: Array<[string, string]> = [];
200+
styleValues.push(['shape', BpmnStyleIdentifier.MESSAGE_FLOW_ICON]);
201+
styleValues.push([BpmnStyleIdentifier.IS_INITIATING, String(edge.messageVisibleKind === MessageVisibleKind.INITIATING)]);
202+
if (!this.ignoreBpmnColors) {
203+
edge.extensions.strokeColor && styleValues.push([mxgraph.mxConstants.STYLE_STROKECOLOR, edge.extensions.strokeColor]);
204+
}
205+
206+
return toArrayOfMxGraphStyleEntries(styleValues).join(';');
166207
}
167208
}
168209

@@ -186,3 +227,7 @@ export function getFontStyleValue(font: Font): number {
186227
}
187228
return value;
188229
}
230+
231+
function toArrayOfMxGraphStyleEntries(styleValues: Array<[string, string | number]>): string[] {
232+
return styleValues.filter(([, v]) => v && v != 'undefined').map(([key, value]) => `${key}=${value}`);
233+
}

src/component/options.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface GlobalOptions {
2525
navigation?: NavigationConfiguration;
2626
/** Configure the BPMN parser. */
2727
parser?: ParserOptions;
28+
/** Configure how the BPMN diagram and its elements are rendered. */
29+
renderer?: RendererOptions;
2830
}
2931

3032
/**
@@ -180,3 +182,21 @@ export type ParserOptions = {
180182
*/
181183
additionalXmlAttributeProcessor?: (val: string) => string;
182184
};
185+
186+
/**
187+
* Global configuration for the rendering of the BPMN elements.
188+
*
189+
* @category Initialization & Configuration
190+
* @since 0.35.0
191+
*/
192+
export type RendererOptions = {
193+
/**
194+
* If set to `false`, support the "BPMN in Color" specification with a fallback with bpmn.io colors. For more details about the support, see
195+
* {@link https://github.com/process-analytics/bpmn-visualization-js/pull/2614}.
196+
*
197+
* Otherwise, disable the support.
198+
*
199+
* @default true
200+
*/
201+
ignoreBpmnColors?: boolean;
202+
};

src/component/parser/json/converter/DiagramConverter.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ import { Edge, Waypoint } from '../../../../model/bpmn/internal/edge/edge';
2222
import type { Shapes } from '../../../../model/bpmn/internal/BpmnModel';
2323
import type BpmnModel from '../../../../model/bpmn/internal/BpmnModel';
2424
import Label, { Font } from '../../../../model/bpmn/internal/Label';
25-
import { MessageVisibleKind } from '../../../../model/bpmn/internal/edge/kinds';
25+
import { MessageVisibleKind, ShapeBpmnCallActivityKind, ShapeBpmnMarkerKind, ShapeUtil } from '../../../../model/bpmn/internal';
2626
import type { BPMNDiagram, BPMNEdge, BPMNLabel, BPMNLabelStyle, BPMNShape } from '../../../../model/bpmn/json/BPMNDI';
2727
import type { Point } from '../../../../model/bpmn/json/DC';
2828
import type { ConvertedElements } from './utils';
29-
import { ShapeBpmnCallActivityKind, ShapeBpmnMarkerKind, ShapeUtil } from '../../../../model/bpmn/internal';
3029
import { ensureIsArray } from '../../../helpers/array-utils';
3130
import type { ParsingMessageCollector } from '../../parsing-messages';
3231
import { EdgeUnknownBpmnElementWarning, LabelStyleMissingFontWarning, ShapeUnknownBpmnElementWarning } from '../warnings';
@@ -104,26 +103,44 @@ export default class DiagramConverter {
104103
return false;
105104
}
106105

107-
private deserializeShape(shape: BPMNShape, findShapeElement: (bpmnElement: string) => ShapeBpmnElement): Shape | undefined {
108-
const bpmnElement = findShapeElement(shape.bpmnElement);
106+
private deserializeShape(bpmnShape: BPMNShape, findShapeElement: (bpmnElement: string) => ShapeBpmnElement): Shape | undefined {
107+
const bpmnElement = findShapeElement(bpmnShape.bpmnElement);
109108
if (bpmnElement) {
110-
const bounds = DiagramConverter.deserializeBounds(shape);
109+
const bounds = DiagramConverter.deserializeBounds(bpmnShape);
111110

112111
if (
113112
(bpmnElement instanceof ShapeBpmnSubProcess ||
114113
(bpmnElement instanceof ShapeBpmnCallActivity && bpmnElement.callActivityKind === ShapeBpmnCallActivityKind.CALLING_PROCESS)) &&
115-
!shape.isExpanded
114+
!bpmnShape.isExpanded
116115
) {
117116
bpmnElement.markers.push(ShapeBpmnMarkerKind.EXPAND);
118117
}
119118

120119
let isHorizontal;
121120
if (ShapeUtil.isPoolOrLane(bpmnElement.kind)) {
122-
isHorizontal = shape.isHorizontal ?? true;
121+
isHorizontal = bpmnShape.isHorizontal ?? true;
123122
}
124123

125-
const label = this.deserializeLabel(shape.BPMNLabel, shape.id);
126-
return new Shape(shape.id, bpmnElement, bounds, label, isHorizontal);
124+
const bpmnLabel = bpmnShape.BPMNLabel;
125+
const label = this.deserializeLabel(bpmnLabel, bpmnShape.id);
126+
const shape = new Shape(bpmnShape.id, bpmnElement, bounds, label, isHorizontal);
127+
DiagramConverter.setColorExtensionsOnShape(shape, bpmnShape);
128+
129+
return shape;
130+
}
131+
}
132+
133+
// 'BPMN in Color' extensions with fallback to bpmn.io colors
134+
private static setColorExtensionsOnShape(shape: Shape, bpmnShape: BPMNShape): void {
135+
if ('background-color' in bpmnShape) {
136+
shape.extensions.fillColor = <string>bpmnShape['background-color'];
137+
} else if ('fill' in bpmnShape) {
138+
shape.extensions.fillColor = <string>bpmnShape['fill'];
139+
}
140+
if ('border-color' in bpmnShape) {
141+
shape.extensions.strokeColor = <string>bpmnShape['border-color'];
142+
} else if ('stroke' in bpmnShape) {
143+
shape.extensions.strokeColor = <string>bpmnShape['stroke'];
127144
}
128145
}
129146

@@ -136,26 +153,37 @@ export default class DiagramConverter {
136153

137154
private deserializeEdges(edges: BPMNEdge | BPMNEdge[]): Edge[] {
138155
return ensureIsArray(edges)
139-
.map(edge => {
156+
.map(bpmnEdge => {
140157
const flow =
141-
this.convertedElements.findSequenceFlow(edge.bpmnElement) ||
142-
this.convertedElements.findMessageFlow(edge.bpmnElement) ||
143-
this.convertedElements.findAssociationFlow(edge.bpmnElement);
158+
this.convertedElements.findSequenceFlow(bpmnEdge.bpmnElement) ||
159+
this.convertedElements.findMessageFlow(bpmnEdge.bpmnElement) ||
160+
this.convertedElements.findAssociationFlow(bpmnEdge.bpmnElement);
144161

145162
if (!flow) {
146-
this.parsingMessageCollector.warning(new EdgeUnknownBpmnElementWarning(edge.bpmnElement));
163+
this.parsingMessageCollector.warning(new EdgeUnknownBpmnElementWarning(bpmnEdge.bpmnElement));
147164
return;
148165
}
149166

150-
const waypoints = this.deserializeWaypoints(edge.waypoint);
151-
const label = this.deserializeLabel(edge.BPMNLabel, edge.id);
152-
const messageVisibleKind = edge.messageVisibleKind ? (edge.messageVisibleKind as unknown as MessageVisibleKind) : MessageVisibleKind.NONE;
167+
const waypoints = this.deserializeWaypoints(bpmnEdge.waypoint);
168+
const label = this.deserializeLabel(bpmnEdge.BPMNLabel, bpmnEdge.id);
169+
const messageVisibleKind = bpmnEdge.messageVisibleKind ? (bpmnEdge.messageVisibleKind as unknown as MessageVisibleKind) : MessageVisibleKind.NONE;
153170

154-
return new Edge(edge.id, flow, waypoints, label, messageVisibleKind);
171+
const edge = new Edge(bpmnEdge.id, flow, waypoints, label, messageVisibleKind);
172+
DiagramConverter.setColorExtensionsOnEdge(edge, bpmnEdge);
173+
return edge;
155174
})
156175
.filter(Boolean);
157176
}
158177

178+
// 'BPMN in Color' extensions with fallback to bpmn.io colors
179+
private static setColorExtensionsOnEdge(edge: Edge, bpmnEdge: BPMNEdge): void {
180+
if ('border-color' in bpmnEdge) {
181+
edge.extensions.strokeColor = <string>bpmnEdge['border-color'];
182+
} else if ('stroke' in bpmnEdge) {
183+
edge.extensions.strokeColor = <string>bpmnEdge['stroke'];
184+
}
185+
}
186+
159187
private deserializeWaypoints(waypoints: Point[]): Waypoint[] {
160188
return ensureIsArray(waypoints).map(waypoint => new Waypoint(waypoint.x, waypoint.y));
161189
}
@@ -164,9 +192,13 @@ export default class DiagramConverter {
164192
if (bpmnLabel && typeof bpmnLabel === 'object') {
165193
const font = this.findFont(bpmnLabel.labelStyle, id);
166194
const bounds = DiagramConverter.deserializeBounds(bpmnLabel);
167-
195+
const label = new Label(font, bounds);
196+
if ('color' in bpmnLabel) {
197+
label.extensions.color = <string>bpmnLabel.color;
198+
return label;
199+
}
168200
if (font || bounds) {
169-
return new Label(font, bounds);
201+
return label;
170202
}
171203
}
172204
}

0 commit comments

Comments
 (0)