fix: consistently compute the arcSize for edges and vertices by tbouffard · Pull Request #884 · maxGraph/maxGraph · GitHub
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
6 changes: 4 additions & 2 deletions CHANGELOG.md
120 changes: 120 additions & 0 deletions packages/core/__tests__/view/shape/Shape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright 2025-present The maxGraph project Contributors

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 { beforeEach, describe, expect, test } from '@jest/globals';
import Shape from '../../../src/view/shape/Shape';

describe('getArcSize', () => {
let shape: Shape;

beforeEach(() => {
shape = new Shape();
});

describe('absoluteArcSize: true', () => {
test('height is smaller than width', () => {
shape.style = { absoluteArcSize: true, arcSize: 20 };
expect(shape.getArcSize(100, 50)).toBe(10);
});

test('width is smaller than height', () => {
shape.style = { absoluteArcSize: true, arcSize: 13 };
expect(shape.getArcSize(50, 100)).toBe(6.5);
});

test('width and height are equal', () => {
shape.style = { absoluteArcSize: true, arcSize: 30 };
expect(shape.getArcSize(50, 50)).toBe(15);
});

test('arcSize is zero', () => {
shape.style = { absoluteArcSize: true, arcSize: 0 };
expect(shape.getArcSize(100, 50)).toBe(0);
});

test('width and height are zero', () => {
shape.style = { absoluteArcSize: true, arcSize: 60 };
expect(shape.getArcSize(0, 0)).toBe(0);
});

test('arcSize is not set, large dimensions', () => {
shape.style = { absoluteArcSize: true };
expect(shape.getArcSize(170, 90)).toBe(10);
});

test('arcSize is not set, small dimensions', () => {
shape.style = { absoluteArcSize: true };
expect(shape.getArcSize(10, 17)).toBe(5);
});
});

describe.each([false, undefined])(
'absoluteArcSize: %s',
(absoluteArcSize?: boolean) => {
test('height is smaller than width', () => {
shape.style = { absoluteArcSize, arcSize: 40 };
expect(shape.getArcSize(400, 350)).toBe(140);
});

test('width is smaller than height', () => {
shape.style = { absoluteArcSize, arcSize: 30 };
expect(shape.getArcSize(40, 60)).toBe(12);
});

test('width and height are equal', () => {
shape.style = { absoluteArcSize, arcSize: 60 };
expect(shape.getArcSize(85, 85)).toBe(51);
});

test('arcSize not set, height is smaller than width', () => {
shape.style = { absoluteArcSize };
expect(shape.getArcSize(400, 350)).toBe(52.5);
});

test('arcSize not set, width is smaller than height', () => {
shape.style = { absoluteArcSize };
expect(shape.getArcSize(40, 60)).toBe(6);
});

test('arcSize not set, width and height are equal', () => {
shape.style = { absoluteArcSize };
expect(shape.getArcSize(85, 85)).toBe(12.75);
});
}
);

describe('style is not set', () => {
test('height is smaller than width', () => {
shape.style = null;
expect(shape.getArcSize(400, 350)).toBe(52.5);
});

test('width is smaller than height', () => {
shape.style = null;
expect(shape.getArcSize(40, 60)).toBe(6);
});

test('large dimensions', () => {
shape.style = null;
expect(shape.getArcSize(200, 468)).toBe(30);
});

test('width and height are equal', () => {
shape.style = null;
expect(shape.getArcSize(85, 85)).toBe(12.75);
});
});
});
38 changes: 38 additions & 0 deletions packages/core/__tests__/view/shape/edge/PolyLineShape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2025-present The maxGraph project Contributors

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 { afterEach, expect } from '@jest/globals';
import PolylineShape from '../../../../src/view/shape/edge/PolylineShape';
import AbstractCanvas2D from '../../../../src/view/canvas/AbstractCanvas2D';

afterEach(() => {
jest.clearAllMocks();
});

test('paintLine uses correct arc size', () => {
const polylineShape = new PolylineShape([], 'red');
const addPoints = jest.spyOn(polylineShape, 'addPoints');
// Mock only the methods used by PolylineShape.paintLine
const canvas2D = {
begin: jest.fn(),
stroke: jest.fn(),
} as unknown as AbstractCanvas2D;

polylineShape.paintLine(canvas2D, [], true);

const expectedUsedArcSize = 10; // no style set, so the value is derived from the default
expect(addPoints).toHaveBeenCalledWith(canvas2D, [], true, expectedUsedArcSize, false);
});
93 changes: 93 additions & 0 deletions packages/core/__tests__/view/shape/node/SwimlaneShape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2025-present The maxGraph project Contributors

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 { beforeEach, describe, expect, test } from '@jest/globals';
import SwimlaneShape from '../../../../src/view/shape/node/SwimlaneShape';

describe('getSwimlaneArcSize', () => {
let shape: SwimlaneShape;

beforeEach(() => {
// the values passed to the constructor are not used in this test
shape = new SwimlaneShape(null!, null!, null!);
});

describe('absoluteArcSize: true', () => {
const ignoredStartParameter: number = null!;

test('height is smaller than width', () => {
shape.style = { absoluteArcSize: true, arcSize: 20 };
expect(shape.getSwimlaneArcSize(100, 50, ignoredStartParameter)).toBe(10);
});

test('width is smaller than height', () => {
shape.style = { absoluteArcSize: true, arcSize: 13 };
expect(shape.getSwimlaneArcSize(50, 100, ignoredStartParameter)).toBe(6.5);
});

test('width and height are equal', () => {
shape.style = { absoluteArcSize: true, arcSize: 30 };
expect(shape.getSwimlaneArcSize(50, 50, ignoredStartParameter)).toBe(15);
});

test('arcSize is zero', () => {
shape.style = { absoluteArcSize: true, arcSize: 0 };
expect(shape.getSwimlaneArcSize(100, 50, ignoredStartParameter)).toBe(0);
});

test('width and height are zero', () => {
shape.style = { absoluteArcSize: true, arcSize: 60 };
expect(shape.getSwimlaneArcSize(0, 0, ignoredStartParameter)).toBe(0);
});

test('arcSize is not set, large dimensions', () => {
shape.style = { absoluteArcSize: true };
expect(shape.getSwimlaneArcSize(170, 90, ignoredStartParameter)).toBe(10);
});

test('arcSize is not set, small dimensions', () => {
shape.style = { absoluteArcSize: true };
expect(shape.getSwimlaneArcSize(10, 17, ignoredStartParameter)).toBe(5);
});
});

test.each([false, undefined])('absoluteArcSize: %s', (absoluteArcSize?: boolean) => {
shape.style = { absoluteArcSize, arcSize: 40 };
expect(shape.getSwimlaneArcSize(400, 350, 7)).toBe(8.4);
});

describe('style is not set', () => {
test('height is smaller than width', () => {
shape.style = null;
expect(shape.getSwimlaneArcSize(400, 350, 40)).toBe(18);
});

test('width is smaller than height', () => {
shape.style = null;
expect(shape.getSwimlaneArcSize(40, 60, 20)).toBe(9);
});

test('large dimensions', () => {
shape.style = null;
expect(shape.getSwimlaneArcSize(200, 468, 15)).toBe(6.75);
});

test('width and height are equal', () => {
shape.style = null;
expect(shape.getSwimlaneArcSize(85, 85, 19)).toBe(8.55);
});
});
});
19 changes: 10 additions & 9 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,21 @@ export type CellStateStyle = {
*/
anchorPointDirection?: boolean;
/**
* For **vertex**, this defines the rounding factor for a {@link rounded} vertex in percent.
* The possible values are between `0` and `100`.
* If this value is not specified, then `constants.RECTANGLE_ROUNDING_FACTOR * 100` is used.
*
* Shapes supporting `arcSize`:
* - Rectangle
* For **vertex**, this defines the absolute size of the {@link rounded} corners in pixels for the following shapes:
* - Hexagon
* - Rhombus
* - Swimlane
* - Triangle
* - `Rectangle` and `Swimlane`, if {@link absoluteArcSize} is `true`
*
* For **edge**, this defines the absolute size of the {@link rounded} corners in pixels.
* If this value is not specified, then {@link LINE_ARCSIZE} is used.
*
* See also {@link absoluteArcSize}.
* For the `Rectangle` and `Swimlane` shapes, if {@link absoluteArcSize} is not `true`, this defines the rounding factor for a {@link rounded} vertex in percent.
* The possible values are between `0` and `100`.
* If this value is not specified, then {@link RECTANGLE_ROUNDING_FACTOR}` * 100` is used.
*
*
* For **edge**, this defines the absolute size of the {@link rounded} corners in pixels.
* If this value is not specified, then {@link LINE_ARCSIZE} is used.
*/
arcSize?: number;
/**
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/util/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,14 +324,14 @@ export const DEFAULT_IMAGESIZE = 24;
export const ENTITY_SEGMENT = 30;

/**
* Defines the default rounding factor for the rounded vertices in percent between
* `0` and `1`. Values should be smaller than `0.5`.
* Defines the default rounding factor for the rounded vertices in percent between `0` and `1`.
* Values should be smaller than `0.5`.
* See {@link CellStateStyle.arcSize}.
*/
export const RECTANGLE_ROUNDING_FACTOR = 0.15;

/**
* Defines the default size in pixels of the arcs for the rounded edges.
* Defines the default size in pixels of the arcs for the rounded edges and vertices.
* See {@link CellStateStyle.arcSize}.
*/
export const LINE_ARCSIZE = 20;
Expand Down
32 changes: 17 additions & 15 deletions packages/core/src/view/shape/Shape.ts
Loading