Skip to content
Merged
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
27 changes: 22 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,23 @@ export class BpmnRenderer {
}
}

/**
* @internal
*/
export function isLabelBoundsIgnored(shape: Shape, ignoreBpmnActivityLabelBounds: boolean, ignoreBpmnTaskLabelBounds: boolean): boolean {
const kind = shape.bpmnElement.kind;
return (
ShapeUtil.isPoolOrLane(kind) || // pool/lane label bounds are not managed for now (use hard coded values)
(ignoreBpmnActivityLabelBounds && ShapeUtil.isActivity(kind)) ||
(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.
194 changes: 194 additions & 0 deletions test/e2e/bpmn.rendering.ignore.options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
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>([
[
'activities.with.wrongly.positioned.labels.not-ignored',
{
macos: 0.19 / 100, // 0.18413140767182812%
windows: 0.22 / 100, // 0.21177195104159496%
},
],
[
'activities.with.wrongly.positioned.labels.ignored',
{
macos: 0.29 / 100, // 0.28115982788768923%
windows: 0.28 / 100, // 0.27545483944123594%
},
],
]);
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>([
[
'activities.with.wrongly.positioned.labels.not-ignored',
{
linux: 0.23 / 100, // 0.2236111574782207%
macos: 0.36 / 100, // 0.35012765743468455%
windows: 1.32 / 100, // 1.3103779470739596%
},
],
[
'activities.with.wrongly.positioned.labels.ignored',
{
linux: 0.85 / 100, // 0.8457215876566115%
macos: 0.64 / 100, // 0.634897664634726%
windows: 1.9 / 100, // 1.8900990794732508%
},
],
]);
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>([
[
'activities.with.wrongly.positioned.labels.not-ignored',
{
macos: 0.44 / 100, // 0.4382175377357411%
},
],
[
'activities.with.wrongly.positioned.labels.ignored',
{
macos: 1.5 / 100, // 1.4951298719464878%
},
],
]);
}
}

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>([
[
'labels.with.font.styles.not-ignored',
{
macos: 0.17 / 100, // 0.16518659366272503%
windows: 0.16 / 100, // 0.1578221549419112%
},
],
[
'labels.with.font.styles.ignored',
{
macos: 0.21 / 100, // 0.20961855005547925%
windows: 0.29 / 100, // 0.2864761242524328%
},
],
]);
}

protected override getFirefoxThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>([
[
'labels.with.font.styles.not-ignored',
{
// very large number because of firefox rendering differences compared to chrome (font)
// this test requires to use a dedicated reference screenshot, see https://github.com/process-analytics/bpmn-visualization-js/issues/2838
linux: 4.53 / 100, // 4.529968414713514%
macos: 0.5 / 100, // 0.49100385220669507%
windows: 2.1 / 100, // 2.096414153407533%
},
],
[
'labels.with.font.styles.ignored',
{
linux: 0.06 / 100, // 0.05351232277721607%
macos: 0.26 / 100, // 0.2513269855389466%
windows: 1.64 / 100, // 1.637503468193735%
},
],
]);
}

protected override getWebkitThresholds(): Map<string, ImageSnapshotThresholdConfig> {
return new Map<string, ImageSnapshotThresholdConfig>([
[
'labels.with.font.styles.not-ignored',
{
macos: 0.31 / 100, // 0.30621380597637415%
},
],
[
'labels.with.font.styles.ignored',
{
macos: 0.38 / 100, // 0.37988509633168904%
},
],
]);
}
}

function getConfigName(bpmnDiagramName: string, ignoredOption: boolean): string {
return bpmnDiagramName + '.' + (ignoredOption ? 'ignored' : 'not-ignored');
}

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 = new ImageSnapshotConfigurator(new ImageSnapshotThresholdsActivityLabelBounds(), 'bpmn-rendering-ignore-options');

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

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

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

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

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

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig(getConfigName(bpmnDiagramName, ignoreBpmnLabelStyles));
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>
Loading