Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ Then, we will work on BPMN extensions, library extension points.
# Development

To build the project, see the [Contributing guide](CONTRIBUTING.md#Build) :slightly_smiling_face:


# License

`bpmn-visualization-js` is released under the Apache 2.0 license.

Some BPMN icons used by `bpmn-visualization-js` are derived from existing projects. See the [BPMN Support page](docs/bpmn-support.adoc)
for more details:
- [draw.io](https://github.com/jgraph/drawio) (Apache-2.0)
7 changes: 4 additions & 3 deletions docs/bpmn-support.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ The default rendering uses `white` as fill color and `black` as stroke color.
|

|Service Task
|icon:flask[]
|Stroke color and icon are subject to change
|icon:check-circle-o[]
|Icon may be subject to change +
*icon*: the task icon is derived from the https://github.com/jgraph/drawio/blob/9394fb0f1430d2c869865827b2bbef5639f63478/src/main/webapp/stencils/bpmn.xml#L898[draw.io bpmn mxgraph stencil]

|User Task
|icon:flask[]
Expand Down Expand Up @@ -128,4 +129,4 @@ The default rendering uses `white` as fill color and `black` as stroke color.
|Parallel
|
|
|===
|===
3 changes: 2 additions & 1 deletion src/component/mxgraph/ShapeConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { mxgraph } from 'ts-mxgraph';
import { ShapeBpmnElementKind } from '../../model/bpmn/shape/ShapeBpmnElementKind';
import { EndEventShape, StartEventShape } from './shape/event-shapes';
import { ExclusiveGatewayShape } from './shape/gateway-shapes';
import { TaskShape } from './shape/task-shapes';
import { ServiceTaskShape, TaskShape } from './shape/task-shapes';

export default class ShapeConfigurator {
private mxClient: typeof mxgraph.mxClient = MxGraphFactoryService.getMxGraphProperty('mxClient');
Expand All @@ -35,6 +35,7 @@ export default class ShapeConfigurator {
this.mxCellRenderer.registerShape(ShapeBpmnElementKind.EVENT_START, StartEventShape);
this.mxCellRenderer.registerShape(ShapeBpmnElementKind.GATEWAY_EXCLUSIVE, ExclusiveGatewayShape);
this.mxCellRenderer.registerShape(ShapeBpmnElementKind.TASK, TaskShape);
this.mxCellRenderer.registerShape(ShapeBpmnElementKind.TASK_SERVICE, ServiceTaskShape);
}

private initMxShapePrototype(isFF: boolean): void {
Expand Down
23 changes: 10 additions & 13 deletions src/component/mxgraph/StyleConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ export default class StyleConfigurator {
// events
this.configureEventsStyle();
// tasks
this.configureTasksStyle();
this.configureUserTaskStyle();
this.configureServiceTaskStyle();
this.configureTaskStyle();
// gateways
this.configureGatewaysStyle();
this.configureParallelGatewayStyle();
Expand Down Expand Up @@ -110,12 +109,17 @@ export default class StyleConfigurator {
});
}

private configureServiceTaskStyle(): void {
const style = this.mxUtils.clone(this.getStylesheet().getCellStyle(ShapeBpmnElementKind.TASK_USER), this.getDefaultVertexStyle());
style[this.mxConstants.STYLE_STROKECOLOR] = 'red';
this.putCellStyle(ShapeBpmnElementKind.TASK_SERVICE, style);
private configureTasksStyle(): void {
ShapeUtil.taskKinds().forEach(kind => {
const style = this.cloneDefaultVertexStyle();
style[this.mxConstants.STYLE_SHAPE] = kind;
style[this.mxConstants.STYLE_VERTICAL_ALIGN] = 'middle';
this.putCellStyle(kind, style);
});
}

// TODO: to be removed as it will be configured in configureTasksStyle
// left just to not break current rendering
private configureUserTaskStyle(): void {
const style = this.cloneDefaultVertexStyle();
style[this.mxConstants.STYLE_SHAPE] = this.mxConstants.SHAPE_RECTANGLE;
Expand All @@ -126,13 +130,6 @@ export default class StyleConfigurator {
this.putCellStyle(ShapeBpmnElementKind.TASK_USER, style);
}

private configureTaskStyle(): void {
const style = this.cloneDefaultVertexStyle();
style[this.mxConstants.STYLE_SHAPE] = ShapeBpmnElementKind.TASK;
style[this.mxConstants.STYLE_VERTICAL_ALIGN] = 'middle';
this.putCellStyle(ShapeBpmnElementKind.TASK, style);
}

// TODO: to be removed as it will be configured in configureGatewaysStyle
// left just to not break current rendering
private configureParallelGatewayStyle(): void {
Expand Down
57 changes: 57 additions & 0 deletions src/component/mxgraph/extension/MxScaleFactorCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright 2020 Bonitasoft S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mxgraph } from 'ts-mxgraph';

/**
* Scale dimensions passed to the method of the original {@link mxgraph.mxXmlCanvas2D}
*
* @example vanilla canvas calls when a scale factor must be applied to positions
* const scaleFactor = 0.26;
* c.moveTo(8 * scaleFactor, 39 * scaleFactor);
* c.lineTo(12 * scaleFactor, 25 * scaleFactor);
*
* @example with `MxScaleFactorCanvas`
* const canvas = new MxScaleFactorCanvas(c, 0.26);
* canvas.moveTo(8, 39);
* canvas.lineTo(12, 25);
*/
export default class MxScaleFactorCanvas {
constructor(readonly c: mxgraph.mxXmlCanvas2D, readonly scaleFactor: number) {}

arcTo(rx: number, ry: number, angle: number, largeArcFlag: number, sweepFlag: number, x: number, y: number): void {
this.c.arcTo(rx * this.scaleFactor, ry * this.scaleFactor, angle, largeArcFlag, sweepFlag, x * this.scaleFactor, y * this.scaleFactor);
}

begin(): void {
this.c.begin();
}

close(): void {
this.c.close();
}

fillAndStroke(): void {
this.c.fillAndStroke();
}

lineTo(x: number, y: number): void {
this.c.lineTo(x * this.scaleFactor, y * this.scaleFactor);
}

moveTo(x: number, y: number): void {
this.c.moveTo(x * this.scaleFactor, y * this.scaleFactor);
}
}
153 changes: 151 additions & 2 deletions src/component/mxgraph/shape/task-shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,164 @@
import { MxGraphFactoryService } from '../../../service/MxGraphFactoryService';
import { mxgraph } from 'ts-mxgraph';
import { StyleConstant } from '../StyleConfigurator';
import MxScaleFactorCanvas from '../extension/MxScaleFactorCanvas';

const mxRectangleShape: typeof mxgraph.mxRectangleShape = MxGraphFactoryService.getMxGraphProperty('mxRectangleShape');

export class TaskShape extends mxRectangleShape {
abstract class BaseTaskShape extends mxRectangleShape {
// TODO we need to declare this field here because it is missing in the current mxShape type definition
isRounded: boolean;

protected constructor(bounds: mxgraph.mxRectangle, fill: string, stroke: string, strokewidth: number = StyleConstant.STROKE_WIDTH_THIN) {
super(bounds, fill, stroke, strokewidth);

// enforced by BPMN
this.isRounded = true;
}

public paintForeground(c: mxgraph.mxXmlCanvas2D, x: number, y: number, w: number, h: number): void {
super.paintForeground(c, x, y, w, h);
this.paintTaskIcon(c, x, y, w, h);
}

protected abstract paintTaskIcon(c: mxgraph.mxXmlCanvas2D, x: number, y: number, w: number, h: number): void;
}

export class TaskShape extends BaseTaskShape {
public constructor(bounds: mxgraph.mxRectangle, fill: string, stroke: string, strokewidth: number) {
super(bounds, fill, stroke, strokewidth);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected paintTaskIcon(c: mxgraph.mxXmlCanvas2D, x: number, y: number, w: number, h: number): void {
// No symbol for the BPMN Task
}
}

export class ServiceTaskShape extends BaseTaskShape {
public constructor(bounds: mxgraph.mxRectangle, fill: string, stroke: string, strokewidth: number) {
super(bounds, fill, stroke, strokewidth);
}

// this implementation is adapted from the draw.io BPMN 'Service Task' stencil
// https://github.com/jgraph/drawio/blob/9394fb0f1430d2c869865827b2bbef5639f63478/src/main/webapp/stencils/bpmn.xml#L898
protected paintTaskIcon(c: mxgraph.mxXmlCanvas2D, x: number, y: number, w: number, h: number): void {
const xTranslation = x + w / 20;
const yTranslation = y + h / 20;
c.translate(xTranslation, yTranslation);

// ensure we are not impacted by the configured shape stroke width
c.setStrokeWidth(1);

const parentSize = Math.min(w, h);
const ratioFromParent = 0.25;
// coordinates below fill a box of 100x100 (approximately: 90x90 + foreground translation)
const scaleFactor = (parentSize / 100) * ratioFromParent;

const canvas = new MxScaleFactorCanvas(c, scaleFactor);
// background
this.drawIconBackground(canvas);

// foreground
const foregroundTranslation = 14 * scaleFactor;
c.translate(foregroundTranslation, foregroundTranslation);
this.drawIconForeground(canvas);

// hack for translation that will be needed when managing task markers
// c.translate(-xTranslation, -yTranslation);
}

private drawIconBackground(canvas: MxScaleFactorCanvas): void {
canvas.begin();
canvas.moveTo(2.06, 24.62);
canvas.lineTo(10.17, 30.95);
canvas.lineTo(9.29, 37.73);
canvas.lineTo(0, 41.42);
canvas.lineTo(2.95, 54.24);
canvas.lineTo(13.41, 52.92);
canvas.lineTo(17.39, 58.52);
canvas.lineTo(13.56, 67.66);
canvas.lineTo(24.47, 74.44);
canvas.lineTo(30.81, 66.33);
canvas.lineTo(37.88, 67.21);
canvas.lineTo(41.57, 76.5);
canvas.lineTo(54.24, 73.55);
canvas.lineTo(53.06, 62.94);
canvas.lineTo(58.52, 58.52);
canvas.lineTo(67.21, 63.09);
canvas.lineTo(74.58, 51.88);
canvas.lineTo(66.03, 45.25);
canvas.lineTo(66.92, 38.62);
canvas.lineTo(76.5, 34.93);
canvas.lineTo(73.7, 22.26);
canvas.lineTo(62.64, 23.44);
canvas.lineTo(58.81, 18.42);
canvas.lineTo(62.79, 8.7);
canvas.lineTo(51.74, 2.21);
canvas.lineTo(44.81, 10.47);
canvas.lineTo(38.03, 9.43);
canvas.lineTo(33.75, 0);
canvas.lineTo(21.52, 3.24);
canvas.lineTo(22.7, 13.56);
canvas.lineTo(18.13, 17.54);
canvas.lineTo(8.7, 13.56);
canvas.close();

const arcStartX = 24.8;
const arcStartY = 39;
this.drawInnerCircle(canvas, arcStartX, arcStartY);
}

private drawIconForeground(canvas: MxScaleFactorCanvas): void {
canvas.begin();
canvas.moveTo(16.46, 41.42);
canvas.lineTo(24.57, 47.75);
canvas.lineTo(23.69, 54.53);
canvas.lineTo(14.4, 58.22);
canvas.lineTo(17.35, 71.04);
canvas.lineTo(27.81, 69.72);
canvas.lineTo(31.79, 75.32);
canvas.lineTo(27.96, 84.46);
canvas.lineTo(38.87, 91.24);
canvas.lineTo(45.21, 83.13);
canvas.lineTo(52.28, 84.01);
canvas.lineTo(55.97, 93.3);
canvas.lineTo(68.64, 90.35);
canvas.lineTo(67.46, 79.74);
canvas.lineTo(72.92, 75.32);
canvas.lineTo(81.61, 79.89);
canvas.lineTo(88.98, 68.68);
canvas.lineTo(80.43, 62.05);
canvas.lineTo(81.32, 55.42);
canvas.lineTo(90.9, 51.73);
canvas.lineTo(88.1, 39.06);
canvas.lineTo(77.04, 40.24);
canvas.lineTo(73.21, 35.22);
canvas.lineTo(77.19, 25.5);
canvas.lineTo(66.14, 19.01);
canvas.lineTo(59.21, 27.27);
canvas.lineTo(52.43, 26.23);
canvas.lineTo(48.15, 16.8);
canvas.lineTo(35.92, 20.04);
canvas.lineTo(37.1, 30.36);
canvas.lineTo(32.53, 34.34);
canvas.lineTo(23.1, 30.36);
canvas.close();

const arcStartX = 39.2;
const arcStartY = 55.8;
this.drawInnerCircle(canvas, arcStartX, arcStartY);

// fill the inner circle to mask the background
canvas.begin();
this.drawInnerCircle(canvas, arcStartX, arcStartY);
}

private drawInnerCircle(canvas: MxScaleFactorCanvas, arcStartX: number, arcStartY: number): void {
const arcRay = 13.5;
canvas.moveTo(arcStartX, arcStartY);
canvas.arcTo(arcRay, arcRay, 0, 1, 1, arcStartX + 2 * arcRay, arcStartY);
canvas.arcTo(arcRay, arcRay, 0, 0, 1, arcStartX, arcStartY);
canvas.close();
canvas.fillAndStroke();
}
}
17 changes: 13 additions & 4 deletions src/model/bpmn/shape/ShapeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,33 @@ import { ShapeBpmnElementKind } from './ShapeBpmnElementKind';
// TODO move to ShapeBpmnElementKind? and rename into ShapeBpmnElementKindUtil?
// TODO bpmnEventKinds and flowNodeKinds currently hosted in ProcessConverter may be hosted here
export default class ShapeUtil {
private static readonly EVENTS_KIND = ShapeUtil.filterKind('Event');
private static readonly EVENT_KINDS = ShapeUtil.filterKind('Event');
private static TASK_KINDS = ShapeUtil.filterKind('Task', true);
private static readonly GATEWAY_KINDS = ShapeUtil.filterKind('Gateway');

private static filterKind(suffix: string): ShapeBpmnElementKind[] {
private static filterKind(suffix: string, ignoreCase = false): ShapeBpmnElementKind[] {
return Object.values(ShapeBpmnElementKind).filter(kind => {
if (ignoreCase) {
return kind.endsWith(suffix) || kind.toLowerCase().endsWith(suffix.toLowerCase());
}
return kind.endsWith(suffix);
});
}

public static isEvent(kind: ShapeBpmnElementKind): boolean {
return this.EVENTS_KIND.includes(kind);
return this.EVENT_KINDS.includes(kind);
}

// TODO should we clone the array to avoid modifications of this ref array by client code?
// topLevelBpmnEventKinds to not mixed with the bpmnEventKinds that currently are the list of non None event subtypes
public static topLevelBpmnEventKinds(): ShapeBpmnElementKind[] {
return this.EVENTS_KIND;
return this.EVENT_KINDS;
}

public static taskKinds(): ShapeBpmnElementKind[] {
return this.TASK_KINDS;
}

public static gatewayKinds(): ShapeBpmnElementKind[] {
return this.GATEWAY_KINDS;
}
Expand Down
9 changes: 5 additions & 4 deletions test/e2e/View.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('BPMN Visualization JS', () => {
<semantic:incoming>_e16564d7-0c4c-413e-95f6-f668a3f851fb</semantic:incoming>
<semantic:outgoing>_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599</semantic:outgoing>
</semantic:task>
<semantic:task completionQuantity="1" isForCompensation="false" startQuantity="1" name="Task 2" id="_820c21c0-45f3-473b-813f-06381cc637cd">
<semantic:serviceTask implementation="##WebService" completionQuantity="1" isForCompensation="false" startQuantity="1" name="Service Task 2" id="serviceTask_2">
<semantic:incoming>_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599</semantic:incoming>
<semantic:outgoing>_2aa47410-1b0e-4f8b-ad54-d6f798080cb4</semantic:outgoing>
</semantic:task>
Expand All @@ -61,8 +61,8 @@ describe('BPMN Visualization JS', () => {
<semantic:terminateEventDefinition/>
</semantic:endEvent>
<semantic:sequenceFlow sourceRef="startEvent_1" targetRef="task_1" name="" id="_e16564d7-0c4c-413e-95f6-f668a3f851fb"/>
<semantic:sequenceFlow sourceRef="task_1" targetRef="_820c21c0-45f3-473b-813f-06381cc637cd" name="" id="_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599"/>
<semantic:sequenceFlow sourceRef="_820c21c0-45f3-473b-813f-06381cc637cd" targetRef="_e70a6fcb-913c-4a7b-a65d-e83adc73d69c" name="" id="_2aa47410-1b0e-4f8b-ad54-d6f798080cb4"/>
<semantic:sequenceFlow sourceRef="task_1" targetRef="serviceTask_2" name="" id="_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599"/>
<semantic:sequenceFlow sourceRef="serviceTask_2" targetRef="_e70a6fcb-913c-4a7b-a65d-e83adc73d69c" name="" id="_2aa47410-1b0e-4f8b-ad54-d6f798080cb4"/>
<semantic:sequenceFlow sourceRef="_e70a6fcb-913c-4a7b-a65d-e83adc73d69c" targetRef="endEvent_1" name="" id="_8e8fe679-eb3b-4c43-a4d6-891e7087ff80"/>
</semantic:process>
<bpmndi:BPMNDiagram documentation="" id="Trisotech_Visio-_6" name="A.1.0" resolution="96.00000267028808">
Expand All @@ -82,7 +82,7 @@ describe('BPMN Visualization JS', () => {
<dc:Bounds height="12.804751171875008" width="72.48293963254594" x="263.3333333333333" y="344.5818763825664"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_820c21c0-45f3-473b-813f-06381cc637cd" id="S1373649849860__820c21c0-45f3-473b-813f-06381cc637cd">
<bpmndi:BPMNShape bpmnElement="serviceTask_2" id="shape_serviceTask_2">
<dc:Bounds height="68.0" width="83.0" x="390.0" y="317.0"/>
<bpmndi:BPMNLabel labelStyle="LS1373649849858">
<dc:Bounds height="12.804751171875008" width="72.48293963254594" x="395.3333333333333" y="344.5818763825664"/>
Expand Down Expand Up @@ -170,6 +170,7 @@ describe('BPMN Visualization JS', () => {
expectModelContainsBpmnEvent('startEvent_2_timer', ShapeBpmnElementKind.EVENT_START, ShapeBpmnEventKind.TIMER);
expectModelContainsBpmnEvent('endEvent_1', ShapeBpmnElementKind.EVENT_END, ShapeBpmnEventKind.TERMINATE);
expectModelContainsCell('task_1', ShapeBpmnElementKind.TASK);
expectModelContainsCell('serviceTask_2', ShapeBpmnElementKind.TASK_SERVICE);
});

function expectModelContainsCellWithGeometry(cellId: string, parentId: string, geometry: mxgraph.mxGeometry): void {
Expand Down
Loading