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
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ _**Note:** Yet to be released breaking changes appear here._
**Breaking Changes**:
- The `AbstractGraph.fit` method moved to `FitPlugin`, as well as the `minFitScale` and `maxFitScale` properties.
The method now accepts a single parameter, mainly to minimize the need to pass many default values.
- The `AbstractGraph.getPlugin` method now explicitly returns `undefined` when a plugin is not found.
TypeScript users must update their code to handle the `undefined` case when calling this method.
- The `Dictionary` class has been removed. The maxGraph API use the `Map` class instead, which is a standard JavaScript feature.
If your code depends on the `Dictionary` class, you can use the `Map` class instead.
- The `arcSize` of rounded shapes was not always correctly computed in the past. The computation is now consistent everywhere in the code and matches the mxGraph behavior.
To have the same rendering as before for edges, you must multiply the `arcSize` by `2` in your styles.
- The `AbstractGraph.getPlugin` method now explicitly returns `undefined` when a plugin is not found.
TypeScript users must update their code to handle the `undefined` case when calling this method.
- The return types of some methods of EditorToolbar are now more precise.
- addPrototype(): HTMLImageElement instead of HTMLImageElement | HTMLButtonElement
- addCombo(): HTMLSelectElement instead of HTMLElement
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,7 @@ class Shape {
}

/**
* Initializes the shape by creaing the DOM node using <create>
* and adding it into the given container.
* Initializes the shape by adding it into the given container if the node of the shape doesn't already have a parent.
*
* @param container DOM node that will contain the shape.
*/
Expand All @@ -301,15 +300,14 @@ class Shape {
}

/**
* Returns true if HTML is allowed for this shape. This implementation always
* returns false.
* Returns true if HTML is allowed for this shape. This implementation always returns `false`.
*/
isHtmlAllowed() {
return false;
}

/**
* Returns 0, or 0.5 if <strokewidth> % 2 == 1.
* Returns 0, or 0.5 if {@link strokeWidth} % 2 == 1.
*/
getSvgScreenOffset(): number {
const sw =
Expand Down Expand Up @@ -759,18 +757,22 @@ class Shape {
}

/**
* Returns the arc size for the given dimension.
* Base arc size for the shape, taken from the style.
* @since 0.21.0
*/
getArcSize(w: number, h: number) {
let r = 0;
protected getBaseArcSize(): number {
return (this.style?.arcSize ?? LINE_ARCSIZE) / 2;
}

if (this.style?.absoluteArcSize ?? false) {
r = Math.min(w / 2, Math.min(h / 2, (this.style?.arcSize ?? LINE_ARCSIZE) / 2));
} else {
const f = (this.style?.arcSize ?? RECTANGLE_ROUNDING_FACTOR * 100) / 100;
r = Math.min(w * f, h * f);
/**
* Returns the arc size for the given dimension.
*/
getArcSize(w: number, h: number): number {
if (this.style?.absoluteArcSize) {
return Math.min(this.getBaseArcSize(), Math.min(h, w) / 2);
}
return r;
const roundingFactor = (this.style?.arcSize ?? RECTANGLE_ROUNDING_FACTOR * 100) / 100;
return Math.min(w, h) * roundingFactor;
}

/**
Expand Down Expand Up @@ -1011,7 +1013,7 @@ class Shape {
}

/**
* Hook for subclassers.
* Hook for subclassers. This implementation returns `false`.
*/
isRoundable(c: AbstractCanvas2D, x: number, y: number, w: number, h: number) {
return false;
Expand Down
Loading