Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 26 additions & 5 deletions src/component/mxgraph/BpmnRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,18 @@ import StyleComputer from './renderer/StyleComputer';
* @internal
*/
export class BpmnRenderer {
private readonly ignoreBpmnActivityLabelBounds: boolean;
private readonly ignoreBpmnTaskLabelBounds: boolean;

constructor(
readonly graph: BpmnGraph,
readonly coordinatesTranslator: CoordinatesTranslator,
readonly styleComputer: StyleComputer,
) {}
rendererOptions: RendererOptions,
) {
this.ignoreBpmnActivityLabelBounds = rendererOptions?.ignoreBpmnActivityLabelBounds ?? false;
this.ignoreBpmnTaskLabelBounds = rendererOptions?.ignoreBpmnTaskLabelBounds ?? false;
}

render(renderedModel: RenderedModel): void {
this.insertShapesAndEdges(renderedModel);
Expand Down Expand Up @@ -71,9 +78,7 @@ export class BpmnRenderer {
const bpmnElement = shape.bpmnElement;
const parent = this.getParent(bpmnElement);
const bounds = shape.bounds;
let labelBounds = shape.label?.bounds;
// pool/lane label bounds are not managed for now (use hard coded values)
labelBounds = ShapeUtil.isPoolOrLane(bpmnElement.kind) ? undefined : labelBounds;
const labelBounds = isLabelBoundsIgnored(shape, this.ignoreBpmnActivityLabelBounds, this.ignoreBpmnTaskLabelBounds) ? undefined : shape.label?.bounds;
const style = this.styleComputer.computeStyle(shape, labelBounds);

this.insertVertex(parent, bpmnElement.id, bpmnElement.name, bounds, labelBounds, style);
Expand Down Expand Up @@ -139,11 +144,27 @@ export class BpmnRenderer {
}
}

/**
* @internal
*/
export function isLabelBoundsIgnored(shape: Shape, ignoreBpmnActivityLabelBounds: boolean, ignoreBpmnTaskLabelBounds: boolean): boolean {
const kind = shape.bpmnElement.kind;
if (ShapeUtil.isPoolOrLane(kind)) {
return true;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: restore comment

Suggested change
return true;
// pool/lane label bounds are not managed for now (use hard coded values)
return true;

}

if (ignoreBpmnActivityLabelBounds && ShapeUtil.isActivity(kind)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep a single return including the all conditions

return true;
}

return ignoreBpmnTaskLabelBounds && ShapeUtil.isTask(kind);
}

/**
* @internal
*/
export function newBpmnRenderer(graph: BpmnGraph, options: RendererOptions): BpmnRenderer {
return new BpmnRenderer(graph, new CoordinatesTranslator(graph), new StyleComputer(options));
return new BpmnRenderer(graph, new CoordinatesTranslator(graph), new StyleComputer(options), options);
}

/**
Expand Down
49 changes: 3 additions & 46 deletions src/component/mxgraph/renderer/StyleComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,10 @@ import { BpmnStyleIdentifier } from '../style';
export default class StyleComputer {
private readonly ignoreBpmnColors: boolean;
private readonly ignoreBpmnLabelStyles: boolean;
private readonly ignoreBpmnActivityLabelBounds: boolean;
private readonly ignoreBpmnTaskLabelBounds: boolean;

constructor(options?: RendererOptions) {
this.ignoreBpmnColors = options?.ignoreBpmnColors ?? true;
this.ignoreBpmnLabelStyles = options?.ignoreBpmnLabelStyles ?? false;
this.ignoreBpmnActivityLabelBounds = options?.ignoreBpmnActivityLabelBounds ?? false;
this.ignoreBpmnTaskLabelBounds = options?.ignoreBpmnTaskLabelBounds ?? false;
}

computeStyle(bpmnCell: Shape | Edge, labelBounds: Bounds): string {
Expand All @@ -62,16 +58,12 @@ export default class StyleComputer {
}

const fontStyleValues = this.computeFontStyleValues(bpmnCell);
const labelStyleValues = this.computeLabelStyleValues(bpmnCell, labelBounds);
const labelStyleValues = computeLabelStyleValues(bpmnCell, labelBounds);

styles.push(...toArrayOfMxGraphStyleEntries([...mainStyleValues, ...fontStyleValues, ...labelStyleValues]));
return styles.join(';');
}

private computeLabelStyleValues(bpmnCell: Shape | Edge, labelBounds: Bounds): Map<string, string | number> {
return computeLabelStyleValues(bpmnCell, labelBounds, this.ignoreBpmnActivityLabelBounds, this.ignoreBpmnTaskLabelBounds);
}

private computeShapeStyleValues(shape: Shape): Map<string, string | number> {
const styleValues = new Map<string, string | number>();
const bpmnElement = shape.bpmnElement;
Expand Down Expand Up @@ -182,20 +174,11 @@ function computeEdgeBaseStyles(edge: Edge): string[] {
return styles;
}

function computeLabelStyleValues(
bpmnCell: Shape | Edge,
labelBounds: Bounds,
ignoreBpmnActivityLabelBounds: boolean,
ignoreBpmnTaskLabelBounds: boolean,
): Map<string, string | number> {
function computeLabelStyleValues(bpmnCell: Shape | Edge, labelBounds: Bounds): Map<string, string | number> {
const styleValues = new Map<string, string | number>();

const bpmnElement = bpmnCell.bpmnElement;

// Check if we should ignore label bounds for this element
const shouldIgnoreLabelBounds = shouldIgnoreBpmnLabelBounds(bpmnCell, ignoreBpmnActivityLabelBounds, ignoreBpmnTaskLabelBounds);

if (labelBounds && !shouldIgnoreLabelBounds) {
if (labelBounds) {
styleValues.set(mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_TOP);
if (bpmnCell.bpmnElement.kind != ShapeBpmnElementKind.TEXT_ANNOTATION) {
styleValues.set(mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER);
Expand Down Expand Up @@ -224,32 +207,6 @@ function computeLabelStyleValues(
return styleValues;
}

/**
* Determines if label bounds should be ignored based on the element type and options.
*/
function shouldIgnoreBpmnLabelBounds(bpmnCell: Shape | Edge, ignoreBpmnActivityLabelBounds: boolean, ignoreBpmnTaskLabelBounds: boolean): boolean {
// Only apply to shapes, not edges
if (!(bpmnCell instanceof Shape)) {
return false;
}

const bpmnElement = bpmnCell.bpmnElement;

// If ignoring all activity label bounds
if (ignoreBpmnActivityLabelBounds && bpmnElement instanceof ShapeBpmnActivity) {
return true;
}

// If ignoring task label bounds only, check if it's a task (but not subprocess or call activity)
if (ignoreBpmnTaskLabelBounds && bpmnElement instanceof ShapeBpmnActivity) {
// Activities include tasks, sub-processes, and call activities
// We only want to ignore bounds for tasks, not sub-processes or call activities
return !(bpmnElement instanceof ShapeBpmnSubProcess) && !(bpmnElement instanceof ShapeBpmnCallActivity);
}

return false;
}

/**
* @internal
* @private
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions test/e2e/bpmn.rendering.ignore.options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Copyright 2025 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 type { ImageSnapshotThresholdConfig } from './helpers/visu/image-snapshot-config';

import { ImageSnapshotConfigurator, MultiBrowserImageSnapshotThresholds } from './helpers/visu/image-snapshot-config';

import { AvailableTestPages, PageTester } from '@test/shared/visu/bpmn-page-utils';

class ImageSnapshotThresholdsActivityLabelBounds extends MultiBrowserImageSnapshotThresholds {
constructor() {
super({ chromium: 0 / 100, firefox: 0 / 100, webkit: 0 / 100 });
}

protected override getChromiumThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}
}

class ImageSnapshotThresholdsActivityLabelBoundsIgnored extends MultiBrowserImageSnapshotThresholds {
constructor() {
super({ chromium: 0 / 100, firefox: 0 / 100, webkit: 0 / 100 });
}

protected override getChromiumThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}
}

class ImageSnapshotThresholdsLabelStyles extends MultiBrowserImageSnapshotThresholds {
constructor() {
super({ chromium: 0 / 100, firefox: 0 / 100, webkit: 0 / 100 });
}

protected override getChromiumThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}
}

class ImageSnapshotThresholdsLabelStylesIgnored extends MultiBrowserImageSnapshotThresholds {
constructor() {
super({ chromium: 0 / 100, firefox: 0 / 100, webkit: 0 / 100 });
}

protected override getChromiumThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>();
}
}

describe('BPMN rendering - ignore options', () => {
const diagramSubfolder = 'bpmn-rendering-ignore-options';
const pageTester = new PageTester({ targetedPage: AvailableTestPages.BPMN_RENDERING, diagramSubfolder }, page);

describe('Ignore activity label bounds', () => {
const bpmnDiagramName = 'activities.with.wrongly.positioned.labels';

describe.each([false, true])('ignoreBpmnActivityLabelBounds: %s', (ignoreBpmnActivityLabelBounds: boolean) => {
const imageSnapshotConfigurator = ignoreBpmnActivityLabelBounds
? new ImageSnapshotConfigurator(new ImageSnapshotThresholdsActivityLabelBoundsIgnored(), 'bpmn-rendering-ignore-options/ignored')
: new ImageSnapshotConfigurator(new ImageSnapshotThresholdsActivityLabelBounds(), 'bpmn-rendering-ignore-options/not-ignored');
Comment on lines +103 to +105
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Use a single MultiBrowserImageSnapshotThresholds class to managed both ignored/not-ignored use case as done in https://github.com/process-analytics/bpmn-visualization-js/blob/v0.47.0/test/e2e/bpmn.theme.test.ts#L68
The key would be the use case and not the name of the diagram

The same applies to the other test


it(`${bpmnDiagramName}`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram(bpmnDiagramName, {
rendererIgnoreBpmnActivityLabelBounds: ignoreBpmnActivityLabelBounds,
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig(bpmnDiagramName);
expect(image).toMatchImageSnapshot(config);
});
});
});

describe('Ignore label styles', () => {
const bpmnDiagramName = 'labels.with.font.styles';

describe.each([false, true])('ignoreBpmnLabelStyles: %s', (ignoreBpmnLabelStyles: boolean) => {
const imageSnapshotConfigurator = ignoreBpmnLabelStyles
? new ImageSnapshotConfigurator(new ImageSnapshotThresholdsLabelStylesIgnored(), 'bpmn-rendering-ignore-options/ignored')
: new ImageSnapshotConfigurator(new ImageSnapshotThresholdsLabelStyles(), 'bpmn-rendering-ignore-options/not-ignored');

it(`${bpmnDiagramName}`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram(bpmnDiagramName, {
rendererIgnoreBpmnLabelStyles: ignoreBpmnLabelStyles,
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig(bpmnDiagramName);
expect(image).toMatchImageSnapshot(config);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_wrongly_positioned_labels" targetNamespace="http://example.bpmn.com/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:task id="Task_1" name="Task with label too high" />
<bpmn:userTask id="UserTask_1" name="User Task partly outside" />
<bpmn:serviceTask id="ServiceTask_1" name="Service Task label out of bounds" />
<bpmn:callActivity id="CallActivity_1" name="Call Activity top right" calledElement="global_task" />
<bpmn:subProcess id="SubProcess_1" name="SubProcess bottom left">
<bpmn:task id="InnerTask_1" />
</bpmn:subProcess>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="160" y="80" width="100" height="80" />
<bpmndi:BPMNLabel>
<dc:Bounds x="160" y="80" width="100" height="30" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1_di" bpmnElement="UserTask_1">
<dc:Bounds x="320" y="80" width="100" height="80" />
<bpmndi:BPMNLabel>
<dc:Bounds x="320" y="65" width="100" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ServiceTask_1_di" bpmnElement="ServiceTask_1">
<dc:Bounds x="470" y="80" width="100" height="80" />
<bpmndi:BPMNLabel>
<dc:Bounds x="510" y="140" width="100" height="50" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="CallActivity_1_di" bpmnElement="CallActivity_1" isExpanded="true">
<dc:Bounds x="160" y="220" width="140" height="100" />
<bpmndi:BPMNLabel>
<dc:Bounds x="220" y="225" width="80" height="25" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_1_di" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="360" y="200" width="250" height="150" />
<bpmndi:BPMNLabel>
<dc:Bounds x="365" y="320" width="120" height="25" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="InnerTask_1_di" bpmnElement="InnerTask_1">
<dc:Bounds x="430" y="250" width="100" height="60" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_labels_font_styles" targetNamespace="http://example.bpmn.com/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:task id="Task_1" name="Source Task">
<bpmn:outgoing>Flow_1</bpmn:outgoing>
</bpmn:task>
<bpmn:task id="Task_2" name="Target Task">
<bpmn:incoming>Flow_1</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_1" name="Bold and Underline" sourceRef="Task_1" targetRef="Task_2" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="160" y="100" width="100" height="80" />
<bpmndi:BPMNLabel labelStyle="BPMNLabelStyle_bold_underline" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="400" y="100" width="100" height="80" />
<bpmndi:BPMNLabel labelStyle="BPMNLabelStyle_bold_underline" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="260" y="140" />
<di:waypoint x="400" y="140" />
<bpmndi:BPMNLabel labelStyle="BPMNLabelStyle_bold_underline">
<dc:Bounds x="290" y="115" width="100" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
<bpmndi:BPMNLabelStyle id="BPMNLabelStyle_bold_underline">
<dc:Font name="Times,sans-serif" isBold="true" isItalic="false" isUnderline="true" isStrikeThrough="false" />
</bpmndi:BPMNLabelStyle>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_01pfi2w">
<bpmndi:BPMNShape id="Participant_1gtbp0n_di" bpmnElement="Participant_1gtbp0n" isHorizontal="true">
<dc:Bounds x="156" y="40" width="600" height="430" />
<!-- Label bounds are always ignored for pools -->
<bpmndi:BPMNLabel>
<dc:Bounds x="255" y="63" width="56" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0h8gy68_di" bpmnElement="Lane_0h8gy68" isHorizontal="true">
<dc:Bounds x="186" y="250" width="570" height="80" />
Expand Down
Loading
Loading