From 8551814ad2766353f7d626949b154c1925a5eeb8 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Mon, 5 May 2025 19:28:30 +0200 Subject: [PATCH 1/8] feat!: improve tree-shaking of EdgeStyle Remove hard-coded EdgeStyle references throughout the codebase to enable proper tree-shaking. Previously, all built-in EdgeStyle implementations were always included in application bundles. Now, only registered EdgeStyle implementations are bundled, reducing application size by up to 17kB when no EdgeStyles are used. This is implemented by: - Creating a dedicated EdgeStyleRegistry to register EdgeStyle with metadata - Introducing PerimeterRegistry to manage Perimeter registrations separately - Removing StyleRegistry which previously handled both concerns Additional improvements: - Add dedicated unregister functions for each registry type, allowing independent management of EdgeStyle and Perimeter registrations - Create BaseRegistry to share common functionality between all style registries BREAKING CHANGES: `StyleRegistry` has been removed. Use `EdgeStyleRegistry` and `PerimeterRegistry` instead. The methods of the new registries are also named differently. --- CHANGELOG.md | 2 + .../core/__tests__/util/BaseRegistry.test.ts | 59 ++++++ packages/core/__tests__/view/Graph.test.ts | 171 ++++++++++++------ .../core/__tests__/view/GraphView.test.ts | 20 +- .../view/style/EdgeStyleRegistry.test.ts | 89 +++++++++ packages/core/src/index.ts | 4 +- .../serialization/codecs/GraphViewCodec.ts | 4 +- .../serialization/codecs/StylesheetCodec.ts | 4 +- .../core/src/serialization/codecs/utils.ts | 33 ++++ packages/core/src/types.ts | 43 ++++- packages/core/src/util/BaseRegistry.ts | 50 +++++ packages/core/src/view/AbstractGraph.ts | 54 +++--- packages/core/src/view/GraphView.ts | 9 +- packages/core/src/view/style/StyleRegistry.ts | 61 ------- .../src/view/style/edge/EdgeStyleRegistry.ts | 79 ++++++++ packages/core/src/view/style/edge/Elbow.ts | 6 +- .../src/view/style/edge/EntityRelation.ts | 6 +- packages/core/src/view/style/edge/Loop.ts | 6 +- .../core/src/view/style/edge/Manhattan.ts | 6 +- .../core/src/view/style/edge/Orthogonal.ts | 6 +- packages/core/src/view/style/edge/Segment.ts | 6 +- .../core/src/view/style/edge/SideToSide.ts | 6 +- .../core/src/view/style/edge/TopToBottom.ts | 6 +- .../view/style/marker/EdgeMarkerRegistry.ts | 6 +- .../view/style/perimeter/EllipsePerimeter.ts | 2 +- .../view/style/perimeter/HexagonPerimeter.ts | 2 +- .../view/style/perimeter/PerimeterRegistry.ts | 27 +++ .../style/perimeter/RectanglePerimeter.ts | 2 +- .../view/style/perimeter/RhombusPerimeter.ts | 2 +- .../view/style/perimeter/TrianglePerimeter.ts | 2 +- packages/core/src/view/style/register.ts | 70 +++++-- packages/html/stories/Wires.stories.js | 4 +- .../js-example-selected-features/src/index.js | 4 +- .../ts-example-selected-features/src/main.ts | 12 +- .../vite.config.js | 2 +- .../vite.config.js | 2 +- packages/ts-example/vite.config.js | 2 +- packages/website/docs/usage/edge-styles.md | 23 ++- .../docs/usage/global-configuration.md | 5 +- packages/website/docs/usage/perimeters.md | 17 +- 40 files changed, 691 insertions(+), 223 deletions(-) create mode 100644 packages/core/__tests__/util/BaseRegistry.test.ts create mode 100644 packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts create mode 100644 packages/core/src/serialization/codecs/utils.ts create mode 100644 packages/core/src/util/BaseRegistry.ts delete mode 100644 packages/core/src/view/style/StyleRegistry.ts create mode 100644 packages/core/src/view/style/edge/EdgeStyleRegistry.ts create mode 100644 packages/core/src/view/style/perimeter/PerimeterRegistry.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d4ac9c9a..c3fecc9f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ _**Note:** Yet to be released breaking changes appear here._ - `EdgeHandlerConfig` - `HandleConfig` - `VertexHandlerConfig` +- `StyleRegistry` has been removed. Use `EdgeStyleRegistry` and `PerimeterRegistry` instead. + The methods of the new registries are also named differently. ## 0.19.0 diff --git a/packages/core/__tests__/util/BaseRegistry.test.ts b/packages/core/__tests__/util/BaseRegistry.test.ts new file mode 100644 index 0000000000..56728f7365 --- /dev/null +++ b/packages/core/__tests__/util/BaseRegistry.test.ts @@ -0,0 +1,59 @@ +/* +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 { describe, expect, test } from '@jest/globals'; +import { BaseRegistry } from '../../src'; + +test('registration', () => { + const baseRegistry = new BaseRegistry(); + const object = { property: 'value' }; + baseRegistry.add('name', object); + + expect(baseRegistry.get('name')).toBe(object); + expect(baseRegistry.get('unknown')).toBeNull(); + expect(baseRegistry.get(null)).toBeNull(); + expect(baseRegistry.get(undefined)).toBeNull(); +}); + +test('clear', () => { + const baseRegistry = new BaseRegistry(); + baseRegistry.add('name', { property: 'value' }); + expect(baseRegistry.get('name')).toBeDefined(); + + baseRegistry.clear(); + + expect(baseRegistry.get('name')).toBeNull(); +}); + +describe('getName', () => { + test('no element registered', () => { + const baseRegistry = new BaseRegistry(); + expect(baseRegistry.getName({ prop1: 'value1' })).toBeNull(); + expect(baseRegistry.getName(null)).toBeNull(); + }); + + test('elements registered', () => { + const baseRegistry = new BaseRegistry(); + baseRegistry.add('element1', { prop: 'value1' }); + baseRegistry.add('element2', { prop: 'value2' }); + const targetObject = { prop: 'match' }; + baseRegistry.add('match', targetObject); + baseRegistry.add('element3', { prop: 'value3' }); + + expect(baseRegistry.getName(targetObject)).toBe('match'); + expect(baseRegistry.getName(null)).toBeNull(); + }); +}); diff --git a/packages/core/__tests__/view/Graph.test.ts b/packages/core/__tests__/view/Graph.test.ts index faa4576a0c..58f1beee2f 100644 --- a/packages/core/__tests__/view/Graph.test.ts +++ b/packages/core/__tests__/view/Graph.test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { describe, expect, test } from '@jest/globals'; +import { afterAll, beforeEach, describe, expect, test } from '@jest/globals'; import { AbstractGraph, BaseGraph, @@ -28,6 +28,8 @@ import { Point, Rectangle, RectangleShape, + registerDefaultEdgeStyles, + unregisterAllEdgeStyles, VertexHandler, } from '../../src'; @@ -53,33 +55,64 @@ describe('isOrthogonal', () => { expect(graph.isOrthogonal(cellState)).toBeFalsy(); }); + describe('Default builtin styles registered', () => { + beforeEach(() => { + unregisterAllEdgeStyles(); + registerDefaultEdgeStyles(); + }); + afterAll(() => { + unregisterAllEdgeStyles(); + }); + + test.each([ + ['EntityRelation', EdgeStyle.EntityRelation], + ['ElbowConnector', EdgeStyle.ElbowConnector], + ['ManhattanConnector', EdgeStyle.ManhattanConnector], + ['OrthogonalConnector', EdgeStyle.OrthConnector], + ['SegmentConnector', EdgeStyle.SegmentConnector], + ['SideToSide', EdgeStyle.SideToSide], + ['TopToBottom', EdgeStyle.TopToBottom], + ])('Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = new CellState(graph.view, null, { edgeStyle }); + expect(graph.isOrthogonal(cellState)).toBeTruthy(); + }); + + test.each([ + ['custom', customEdgeStyle], + ['Loop', EdgeStyle.Loop], + ['null', null], + ['undefined', undefined], // TODO probably already tested above + ])('Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = new CellState(graph.view, null, { edgeStyle }); + expect(graph.isOrthogonal(cellState)).toBeFalsy(); + }); + }); + test.each([ + ['custom', customEdgeStyle], ['EntityRelation', EdgeStyle.EntityRelation], ['ElbowConnector', EdgeStyle.ElbowConnector], + ['Loop', EdgeStyle.Loop], ['ManhattanConnector', EdgeStyle.ManhattanConnector], ['OrthogonalConnector', EdgeStyle.OrthConnector], ['SegmentConnector', EdgeStyle.SegmentConnector], ['SideToSide', EdgeStyle.SideToSide], ['TopToBottom', EdgeStyle.TopToBottom], - ])('Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { - const graph = new BaseGraph(); - const cellState = new CellState(graph.view, null, { edgeStyle }); - expect(graph.isOrthogonal(cellState)).toBeTruthy(); - }); - - test.each([ - ['custom', customEdgeStyle], - ['Loop', EdgeStyle.Loop], - ])('Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { - const graph = new BaseGraph(); - const cellState = new CellState(graph.view, null, { - edgeStyle: edgeStyle, - }); - expect(graph.isOrthogonal(cellState)).toBeFalsy(); - }); + ['null', null], + ['undefined', undefined], // TODO probably already tested above + ])( + 'Default builtin styles NOT registered - Style of the CellState, edgeStyle: %s', + (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = new CellState(graph.view, null, { edgeStyle }); + expect(graph.isOrthogonal(cellState)).toBeFalsy(); + } + ); }); -function createCellState(graph: AbstractGraph, isEdge: boolean): CellState { +const createCellState = (graph: AbstractGraph, isEdge: boolean): CellState => { const cell = new Cell(); cell.setEdge(isEdge); cell.setVertex(!isEdge); @@ -87,46 +120,76 @@ function createCellState(graph: AbstractGraph, isEdge: boolean): CellState { cellState.absolutePoints = [new Point(0, 0)]; cellState.shape = new RectangleShape(new Rectangle(), 'green', 'blue'); return cellState; -} +}; + +const createCellStateOfEdge = (graph: AbstractGraph): CellState => + createCellState(graph, true); describe('createEdgeHandler', () => { - test.each([ - ['ElbowConnector', EdgeStyle.ElbowConnector], - ['Loop', EdgeStyle.Loop], - ['SideToSide', EdgeStyle.SideToSide], - ['TopToBottom', EdgeStyle.TopToBottom], - ])('Expect ElbowEdgeHandler for edgeStyle: %s', (_name, edgeStyle) => { - const graph = new BaseGraph(); - const cellState = createCellState(graph, true); - expect(graph.createEdgeHandler(cellState, edgeStyle)).toBeInstanceOf( - ElbowEdgeHandler - ); - }); + describe('Default builtin styles registered', () => { + beforeEach(() => { + unregisterAllEdgeStyles(); + registerDefaultEdgeStyles(); + }); + afterAll(() => { + unregisterAllEdgeStyles(); + }); - test.each([ - ['ManhattanConnector', EdgeStyle.ManhattanConnector], - ['OrthogonalConnector', EdgeStyle.OrthConnector], - ['SegmentConnector', EdgeStyle.SegmentConnector], - ])('Expect EdgeSegmentHandler for edgeStyle: %s', (_name, edgeStyle) => { - const graph = new BaseGraph(); - const cellState = createCellState(graph, true); - expect(graph.createEdgeHandler(cellState, edgeStyle)).toBeInstanceOf( - EdgeSegmentHandler - ); + test.each([ + ['ElbowConnector', EdgeStyle.ElbowConnector], + ['Loop', EdgeStyle.Loop], + ['SideToSide', EdgeStyle.SideToSide], + ['TopToBottom', EdgeStyle.TopToBottom], + ])('Expect ElbowEdgeHandler for edgeStyle: %s', (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = createCellStateOfEdge(graph); + expect(graph.createEdgeHandler(cellState, edgeStyle)).toBeInstanceOf( + ElbowEdgeHandler + ); + }); + + test.each([ + ['ManhattanConnector', EdgeStyle.ManhattanConnector], + ['OrthogonalConnector', EdgeStyle.OrthConnector], + ['SegmentConnector', EdgeStyle.SegmentConnector], + ])('Expect EdgeSegmentHandler for edgeStyle: %s', (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = createCellStateOfEdge(graph); + expect(graph.createEdgeHandler(cellState, edgeStyle)).toBeInstanceOf( + EdgeSegmentHandler + ); + }); + + test.each([ + ['custom', customEdgeStyle], + ['EntityRelation', EdgeStyle.EntityRelation], + ['null', null], + ])('Expect EdgeHandler for edgeStyle: %s', (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = createCellStateOfEdge(graph); + expectExactInstanceOfEdgeHandler(graph.createEdgeHandler(cellState, edgeStyle)); + }); }); test.each([ ['custom', customEdgeStyle], ['EntityRelation', EdgeStyle.EntityRelation], + ['ElbowConnector', EdgeStyle.ElbowConnector], + ['Loop', EdgeStyle.Loop], + ['ManhattanConnector', EdgeStyle.ManhattanConnector], + ['OrthogonalConnector', EdgeStyle.OrthConnector], + ['SegmentConnector', EdgeStyle.SegmentConnector], + ['SideToSide', EdgeStyle.SideToSide], + ['TopToBottom', EdgeStyle.TopToBottom], ['null', null], - ])('Expect EdgeHandler for edgeStyle: %s', (_name, edgeStyle) => { - const graph = new BaseGraph(); - const cellState = createCellState(graph, true); - const edgeHandler = graph.createEdgeHandler(cellState, edgeStyle); - expect(edgeHandler).toBeInstanceOf(EdgeHandler); - expect(edgeHandler).not.toBeInstanceOf(EdgeSegmentHandler); - expect(edgeHandler).not.toBeInstanceOf(ElbowEdgeHandler); - }); + ])( + 'Default builtin styles NOT registered - Expect EdgeHandler for edgeStyle: %s', + (_name, edgeStyle) => { + const graph = new BaseGraph(); + const cellState = createCellStateOfEdge(graph); + expectExactInstanceOfEdgeHandler(graph.createEdgeHandler(cellState, edgeStyle)); + } + ); }); describe('createHandler', () => { @@ -138,7 +201,13 @@ describe('createHandler', () => { test('Expect EdgeHandler', () => { const graph = new BaseGraph(); - const cellState = createCellState(graph, true); - expect(graph.createHandler(cellState)).toBeInstanceOf(EdgeHandler); + const cellState = createCellStateOfEdge(graph); + expectExactInstanceOfEdgeHandler(graph.createHandler(cellState)); }); }); + +function expectExactInstanceOfEdgeHandler(handler: EdgeHandler): void { + expect(handler).toBeInstanceOf(EdgeHandler); + expect(handler).not.toBeInstanceOf(EdgeSegmentHandler); + expect(handler).not.toBeInstanceOf(ElbowEdgeHandler); +} diff --git a/packages/core/__tests__/view/GraphView.test.ts b/packages/core/__tests__/view/GraphView.test.ts index 0ab8421130..0ccc6d43fc 100644 --- a/packages/core/__tests__/view/GraphView.test.ts +++ b/packages/core/__tests__/view/GraphView.test.ts @@ -19,10 +19,12 @@ import { BaseGraph, CellState, EdgeStyle, + EdgeStyleRegistry, GraphView, Perimeter, - StyleRegistry, - unregisterAllEdgeStylesAndPerimeters, + PerimeterRegistry, + unregisterAllEdgeStyles, + unregisterAllPerimeters, } from '../../src'; describe('getEdgeStyle ', () => { @@ -55,10 +57,10 @@ describe('getEdgeStyle ', () => { describe('isLoopStyleEnabled returns false', () => { // Prevents side effects between tests beforeEach(() => { - unregisterAllEdgeStylesAndPerimeters(); + unregisterAllEdgeStyles(); }); afterAll(() => { - unregisterAllEdgeStylesAndPerimeters(); + unregisterAllEdgeStyles(); }); // use manual double instead of jest mock as this is a very simple use case, and we don't want to do any asserts on the fake method @@ -85,7 +87,7 @@ describe('getEdgeStyle ', () => { test('edgeStyle in CellStateStyle is a string, element matching in the registry', () => { const connector = EdgeStyle.OrthConnector; - StyleRegistry.putValue('customEdgeStyle', connector); + EdgeStyleRegistry.add('customEdgeStyle', connector); const graph = createGraph(); const cellState = new CellState(graph.view, null, { edgeStyle: 'customEdgeStyle' }); @@ -94,7 +96,7 @@ describe('getEdgeStyle ', () => { test('edgeStyle in CellStateStyle is a string, element matching in the registry BUT CellStateStyle.noEdgeStyle is true', () => { const connector = EdgeStyle.OrthConnector; - StyleRegistry.putValue('customEdgeStyle', connector); + EdgeStyleRegistry.add('customEdgeStyle', connector); const graph = createGraph(); const cellState = new CellState(graph.view, null, { @@ -117,10 +119,10 @@ describe('getEdgeStyle ', () => { describe('getPerimeterFunction', () => { // Prevents side effects between tests beforeEach(() => { - unregisterAllEdgeStylesAndPerimeters(); + unregisterAllPerimeters(); }); afterAll(() => { - unregisterAllEdgeStylesAndPerimeters(); + unregisterAllPerimeters(); }); test('no perimeter in CellStateStyle, no element matching in the registry', () => { @@ -137,7 +139,7 @@ describe('getPerimeterFunction', () => { test('perimeter in CellStateStyle is a string, element matching in the registry', () => { const perimeter = Perimeter.HexagonPerimeter; - StyleRegistry.putValue('customPerimeter', perimeter); + PerimeterRegistry.add('customPerimeter', perimeter); const graph = new BaseGraph(); const cellState = new CellState(graph.view, null, { perimeter: 'customPerimeter' }); diff --git a/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts b/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts new file mode 100644 index 0000000000..ff2765dcef --- /dev/null +++ b/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts @@ -0,0 +1,89 @@ +/* +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 { afterAll, beforeEach, describe, expect, test } from '@jest/globals'; +import { + EdgeStyle, + type EdgeStyleFunction, + EdgeStyleRegistry, + unregisterAllEdgeStyles, +} from '../../../src'; + +// WARNING the tests here involved global state, so they must not be run in parallel of other tests to prevent side effects +describe('registry', () => { + // Prevents side effects between tests + beforeEach(() => { + unregisterAllEdgeStyles(); + }); + afterAll(() => { + unregisterAllEdgeStyles(); + }); + + const customEdgeStyle: EdgeStyleFunction = () => { + // empty on purpose, we only need the reference to this function in the tests + }; + + test('verify registration', () => { + EdgeStyleRegistry.add('custom', customEdgeStyle, { + isOrthogonal: true, + handlerKind: 'customHandler', + }); + + expect(EdgeStyleRegistry.get('custom')).toBe(customEdgeStyle); + expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('customHandler'); + expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeTruthy(); + }); + + test('verify registration - no meta data', () => { + EdgeStyleRegistry.add('custom', customEdgeStyle); + + expect(EdgeStyleRegistry.get('custom')).toBe(customEdgeStyle); + expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('default'); + expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeFalsy(); + }); + + test('clear', () => { + EdgeStyleRegistry.add('custom', customEdgeStyle, { + isOrthogonal: false, + handlerKind: 'customHandler', + }); + expect(EdgeStyleRegistry.get('custom')).toBeDefined(); + + EdgeStyleRegistry.clear(); + + expect(EdgeStyleRegistry.get('custom')).toBeNull(); + // the edge style function is no longer registered, so returns default values + expect(EdgeStyleRegistry.getHandlerKind(customEdgeStyle)).toEqual('default'); + expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeFalsy(); + }); + + describe('getName', () => { + test('no element registered', () => { + expect(EdgeStyleRegistry.getName(customEdgeStyle)).toBeNull(); + expect(EdgeStyleRegistry.getName(null)).toBeNull(); + }); + + test('elements registered', () => { + EdgeStyleRegistry.add('element1', EdgeStyle.OrthConnector); + EdgeStyleRegistry.add('element2', customEdgeStyle); + EdgeStyleRegistry.add('match', EdgeStyle.SegmentConnector); + EdgeStyleRegistry.add('element3', EdgeStyle.ManhattanConnector); + + expect(EdgeStyleRegistry.getName(EdgeStyle.SegmentConnector)).toBe('match'); + expect(EdgeStyleRegistry.getName(null)).toBeNull(); + }); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c3be26be4c..52451cfa00 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -181,6 +181,7 @@ export * as styleUtils from './util/styleUtils'; */ export * as xmlUtils from './util/xmlUtils'; +export * from './util/BaseRegistry'; export * from './util/config'; export * from './util/logger'; @@ -201,8 +202,9 @@ export { default as Rectangle } from './view/geometry/Rectangle'; export * from './view/style/builtin-style-elements'; export * from './view/style/config'; export * from './view/style/register'; +export * from './view/style/edge/EdgeStyleRegistry'; export { default as MarkerShape } from './view/style/marker/EdgeMarkerRegistry'; -export { default as StyleRegistry } from './view/style/StyleRegistry'; +export { PerimeterRegistry } from './view/style/perimeter/PerimeterRegistry'; export { Stylesheet } from './view/style/Stylesheet'; export { default as DragSource } from './view/other/DragSource'; diff --git a/packages/core/src/serialization/codecs/GraphViewCodec.ts b/packages/core/src/serialization/codecs/GraphViewCodec.ts index fc6f3e3aa9..45dfafa066 100644 --- a/packages/core/src/serialization/codecs/GraphViewCodec.ts +++ b/packages/core/src/serialization/codecs/GraphViewCodec.ts @@ -15,9 +15,9 @@ limitations under the License. */ import ObjectCodec from '../ObjectCodec'; +import { getNameFromRegistries } from './utils'; import GraphView from '../../view/GraphView'; import Cell from '../../view/cell/Cell'; -import StyleRegistry from '../../view/style/StyleRegistry'; import Point from '../../view/geometry/Point'; /** @@ -110,7 +110,7 @@ export class GraphViewCodec extends ObjectCodec { // Tries to turn objects and functions into strings if (typeof value === 'function' && typeof value === 'object') { - value = StyleRegistry.getName(value); + value = getNameFromRegistries(value); } if ( diff --git a/packages/core/src/serialization/codecs/StylesheetCodec.ts b/packages/core/src/serialization/codecs/StylesheetCodec.ts index 2f038cfedd..891a00ce44 100644 --- a/packages/core/src/serialization/codecs/StylesheetCodec.ts +++ b/packages/core/src/serialization/codecs/StylesheetCodec.ts @@ -15,9 +15,9 @@ limitations under the License. */ import ObjectCodec from '../ObjectCodec'; +import { getNameFromRegistries } from './utils'; import { Stylesheet } from '../../view/style/Stylesheet'; import type Codec from '../Codec'; -import StyleRegistry from '../../view/style/StyleRegistry'; import { clone } from '../../util/cloneUtils'; import { GlobalConfig } from '../../util/config'; import { isNumeric } from '../../util/mathUtils'; @@ -85,7 +85,7 @@ export class StylesheetCodec extends ObjectCodec { const type = typeof value; if (type === 'function') { - value = StyleRegistry.getName(value); + value = getNameFromRegistries(value); } else if (type === 'object') { value = null; } diff --git a/packages/core/src/serialization/codecs/utils.ts b/packages/core/src/serialization/codecs/utils.ts new file mode 100644 index 0000000000..401f39cee7 --- /dev/null +++ b/packages/core/src/serialization/codecs/utils.ts @@ -0,0 +1,33 @@ +/* +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 { EdgeStyleRegistry } from '../../view/style/edge/EdgeStyleRegistry'; +import { PerimeterRegistry } from '../../view/style/perimeter/PerimeterRegistry'; + +const registries = [EdgeStyleRegistry, PerimeterRegistry]; + +/** + * @since 0.20.0 + */ +export const getNameFromRegistries = (value: any) => { + for (const registry of registries) { + const name = registry.getName(value); + if (name) { + return name; + } + } + return null; +}; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 121965b04b..5ac1f12113 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -185,10 +185,12 @@ export type CellStateStyle = { /** * This defines the style of the edge if the current cell is an Edge. * - * The possible values are all names of the shapes registered with {@link StyleRegistry.putValue}. + * The possible values are all names of the shapes registered with {@link EdgeStyleRegistry.add}. * This includes {@link EdgeStyleValue} values and custom names that have been registered. * * It is also possible to pass a {@link EdgeStyleFunction}. + * **IMPORTANT**: when using a {@link EdgeStyleFunction}, be sure that is correctly registered in the {@link EdgeStyleRegistry}. + * Otherwise, the function may not be correctly configured. See {@link EdgeStyleMetaData}. * * See {@link noEdgeStyle}. */ @@ -559,7 +561,7 @@ export type CellStateStyle = { * * For {@link PerimeterFunction} types, some possible values are the builtin functions defined in the `Perimeter` namespace. * - * Alternatively, use a string or a value from {@link PerimeterValue} to access perimeter styles registered in {@link StyleRegistry}. + * Alternatively, use a string or a value from {@link PerimeterValue} to access perimeter styles registered in {@link PerimeterRegistry}. * If {@link GraphView.allowEval} is set to `true`, you can pass the {@link PerimeterFunction} implementation directly as a string. * Remember that enabling this switch carries a possible security risk * @@ -1246,7 +1248,7 @@ export type PerimeterFunction = ( ) => Point | null; /** - * Names used to register the perimeter provided out-of-the-box by maxGraph with {@link StyleRegistry.putValue}. + * Names used to register the perimeter provided out-of-the-box by maxGraph with {@link PerimeterRegistry.add}. * * Can be used as a value for {@link CellStateStyle.perimeter}. * @@ -1280,7 +1282,7 @@ export type EdgeStyleFunction = ( ) => void; /** - * Names used to register the edge styles (a.k.a. connectors) provided out-of-the-box by maxGraph with {@link StyleRegistry.putValue}. + * Names used to register the edge styles (a.k.a. connectors) provided out-of-the-box by maxGraph with {@link EdgeStyleRegistry.add}. * * Can be used as a value for {@link CellStateStyle.edgeStyle}. * @@ -1484,3 +1486,36 @@ export type DialectValue = * @category Style */ export type ElbowValue = 'horizontal' | 'vertical'; + +/** + * Allowed values for {@link EdgeStyleMetaData.handlerKind}. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export type EdgeStyleHandlerKind = + | 'default' + | 'elbow' + | 'segment' + | (string & Record); // any other string value + +/** + * Metadata used to configure the edge style when adding it to {@link EdgeStyleRegistry}. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export type EdgeStyleMetaData = { + /** + * The kind of {@link EdgeHandler} to use for this edge style. + * This value is used to select the implementation of the edge handler to use to manage the underlying edge. + * @default 'default' + */ + handlerKind?: EdgeStyleHandlerKind; + /** + * Defines if the edge style is considered as orthogonal or not. + * @default false */ + isOrthogonal?: boolean; +}; diff --git a/packages/core/src/util/BaseRegistry.ts b/packages/core/src/util/BaseRegistry.ts new file mode 100644 index 0000000000..b942da9834 --- /dev/null +++ b/packages/core/src/util/BaseRegistry.ts @@ -0,0 +1,50 @@ +/* +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. +*/ + +/** + * Base implementation for all registries storing "style" configuration. + * @category Style + * @category Configuration + * @since 0.20.0 + */ +export class BaseRegistry { + protected values = new Map(); + + add(name: string, value: V): void { + this.values.set(name, value); + } + + get(name: string | null | undefined): V | null { + return this.values.get(name!) ?? null; + } + + getName(value: V | null): string | null { + for (const [name, style] of this.values.entries()) { + if (style === value) { + return name; + } + } + return null; + } + + /** + * **WARNING**: this method should not be called directly. Call the related global unregister function instead. + * @private + */ + clear(): void { + this.values.clear(); + } +} diff --git a/packages/core/src/view/AbstractGraph.ts b/packages/core/src/view/AbstractGraph.ts index d778ae54e3..d054ad53d2 100644 --- a/packages/core/src/view/AbstractGraph.ts +++ b/packages/core/src/view/AbstractGraph.ts @@ -40,6 +40,7 @@ import ValueChange from './undoable_changes/ValueChange'; import CellState from './cell/CellState'; import { isNode } from '../util/domUtils'; import { EdgeStyle } from './style/builtin-style-elements'; +import { EdgeStyleRegistry } from './style/edge/EdgeStyleRegistry'; import EdgeHandler from './handler/EdgeHandler'; import VertexHandler from './handler/VertexHandler'; import EdgeSegmentHandler from './handler/EdgeSegmentHandler'; @@ -338,8 +339,9 @@ export abstract class AbstractGraph extends EventSource { allowLoops = false; /** - * {@link EdgeStyle} to be used for loops. This is a fallback for loops if the - * {@link CellStateStyle.loopStyle} is `undefined`. + * {@link EdgeStyle} to be used for loops. + * + * This is a fallback for loops if the {@link CellStateStyle.loopStyle} is `undefined`. * @default {@link EdgeStyle.Loop} */ defaultLoopStyle = EdgeStyle.Loop; @@ -423,7 +425,7 @@ export abstract class AbstractGraph extends EventSource { * * @param state {@link CellState} to create the handler for. */ - createEdgeSegmentHandler(state: CellState) { + createEdgeSegmentHandler(state: CellState): EdgeSegmentHandler { return new EdgeSegmentHandler(state); } @@ -432,7 +434,7 @@ export abstract class AbstractGraph extends EventSource { * * @param state {@link CellState} to create the handler for. */ - createElbowEdgeHandler(state: CellState) { + createElbowEdgeHandler(state: CellState): ElbowEdgeHandler { return new ElbowEdgeHandler(state); } @@ -974,33 +976,25 @@ export abstract class AbstractGraph extends EventSource { /** * Hooks to create a new {@link EdgeHandler} for the given {@link CellState}. * + * This method relies on the registered elements in {@link EdgeStyleRegistry} to know which {@link EdgeHandler} to create. + * If the {@link EdgeStyle} is not registered, it will return a default {@link EdgeHandler}. + * * @param state {@link CellState} to create the handler for. * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state: CellState, edgeStyle: EdgeStyleFunction | null): EdgeHandler { - let result = null; - if ( - edgeStyle == EdgeStyle.ElbowConnector || - edgeStyle == EdgeStyle.Loop || - edgeStyle == EdgeStyle.SideToSide || - edgeStyle == EdgeStyle.TopToBottom - ) { - result = this.createElbowEdgeHandler(state); - } else if ( - edgeStyle == EdgeStyle.ManhattanConnector || - edgeStyle == EdgeStyle.OrthConnector || - edgeStyle == EdgeStyle.SegmentConnector - ) { - result = this.createEdgeSegmentHandler(state); - } else { - result = this.createEdgeHandlerInstance(state); + const handlerKind = EdgeStyleRegistry.getHandlerKind(edgeStyle!); // TODO accept nullish? + switch (handlerKind) { + case 'elbow': + return this.createElbowEdgeHandler(state); + case 'segment': + return this.createEdgeSegmentHandler(state); } - - return result; + return this.createEdgeHandlerInstance(state); } /***************************************************************************** - * Group: Drilldown + * Group: Drill down *****************************************************************************/ /** @@ -1179,6 +1173,9 @@ export abstract class AbstractGraph extends EventSource { /** * Returns `true` if perimeter points should be computed such that the resulting edge has only horizontal or vertical segments. * + * This method relies on the registered elements in {@link EdgeStyleRegistry} to know if the {@link CellStateStyle.edgeStyle} of the {@link CellState} is orthogonal. + * If the {@link EdgeStyle} is not registered, it is considered as NOT orthogonal. + * * @param edge {@link CellState} that represents the edge. */ isOrthogonal(edge: CellState): boolean { @@ -1189,16 +1186,7 @@ export abstract class AbstractGraph extends EventSource { // fallback when the orthogonal style is not defined const edgeStyle = this.view.getEdgeStyle(edge); - - return [ - EdgeStyle.EntityRelation, - EdgeStyle.ElbowConnector, - EdgeStyle.ManhattanConnector, - EdgeStyle.OrthConnector, - EdgeStyle.SegmentConnector, - EdgeStyle.SideToSide, - EdgeStyle.TopToBottom, - ].includes(edgeStyle!); + return EdgeStyleRegistry.isOrthogonal(edgeStyle!); } /***************************************************************************** diff --git a/packages/core/src/view/GraphView.ts b/packages/core/src/view/GraphView.ts index 62e732867f..6fece84f23 100644 --- a/packages/core/src/view/GraphView.ts +++ b/packages/core/src/view/GraphView.ts @@ -42,7 +42,8 @@ import type PopupMenuHandler from './plugins/PopupMenuHandler'; import { getClientX, getClientY, getSource, isConsumed } from '../util/EventUtils'; import { clone } from '../util/cloneUtils'; import type { AbstractGraph } from './AbstractGraph'; -import StyleRegistry from './style/StyleRegistry'; +import { EdgeStyleRegistry } from './style/edge/EdgeStyleRegistry'; +import { PerimeterRegistry } from './style/perimeter/PerimeterRegistry'; import type TooltipHandler from './plugins/TooltipHandler'; import type { EdgeStyleFunction, MouseEventListener } from '../types'; import { doEval } from '../internal/utils'; @@ -135,7 +136,7 @@ export class GraphView extends EventSource { /** * Specifies if string values in cell styles should be evaluated using {@link eval}. * - * This will only be used if the string values can't be mapped to objects using {@link StyleRegistry} when resolving {@link CellStateStyle.edgeStyle} and {@link CellStateStyle.perimeter}. + * This will only be used if the string values can't be mapped to objects using {@link EdgeStyleRegistry} or {@link PerimeterRegistry} when resolving {@link CellStateStyle.edgeStyle} and {@link CellStateStyle.perimeter} respectively. * * **WARNING**: Enabling this switch carries a possible security risk. * @@ -1325,7 +1326,7 @@ export class GraphView extends EventSource { // Converts string values to objects if (typeof edgeStyle === 'string') { - let tmp = StyleRegistry.getValue(edgeStyle); + let tmp = EdgeStyleRegistry.get(edgeStyle); if (!tmp && this.isAllowEval()) { tmp = doEval(edgeStyle); @@ -1593,7 +1594,7 @@ export class GraphView extends EventSource { // Converts string values to objects if (typeof perimeter === 'string') { - let tmp = StyleRegistry.getValue(perimeter); + let tmp = PerimeterRegistry.get(perimeter); if (tmp == null && this.isAllowEval()) { tmp = doEval(perimeter); } diff --git a/packages/core/src/view/style/StyleRegistry.ts b/packages/core/src/view/style/StyleRegistry.ts deleted file mode 100644 index af4a5709cb..0000000000 --- a/packages/core/src/view/style/StyleRegistry.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2021-present The maxGraph project Contributors -Copyright (c) 2006-2015, JGraph Ltd -Copyright (c) 2006-2015, Gaudenz Alder - -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 { EdgeStyleValue, PerimeterValue } from '../../types'; - -/** - * Singleton class that acts as a global converter from string to object values in a style. - * - * This is currently only used for handling perimeters and edge styles. - * - * @category Style - */ -class StyleRegistry { - /** - * Maps from strings to objects. - */ - static values = {}; - - /** - * Puts the given object into the registry under the given name. - */ - static putValue(name: PerimeterValue | EdgeStyleValue | string, obj: any): void { - StyleRegistry.values[name] = obj; - } - - /** - * Returns the value associated with the given name. - */ - static getValue(name: string): any { - return StyleRegistry.values[name]; - } - - /** - * Returns the name for the given value. - */ - static getName(value: any): string | null { - for (const key in StyleRegistry.values) { - if (StyleRegistry.values[key] === value) { - return key; - } - } - return null; - } -} - -export default StyleRegistry; diff --git a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts new file mode 100644 index 0000000000..ae590349fb --- /dev/null +++ b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts @@ -0,0 +1,79 @@ +/* +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 type { + EdgeStyleFunction, + EdgeStyleHandlerKind, + EdgeStyleMetaData, +} from '../../../types'; +import { isNullish } from '../../../internal/utils'; +import { BaseRegistry } from '../../../util/BaseRegistry'; + +/** + * Implementation of the {@link EdgeStyleRegistry}. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export class EdgeStyleRegistryImpl extends BaseRegistry { + private handlerMapping = new Map(); + private orthogonalStates = new Map(); + + add(name: string, edgeStyle: EdgeStyleFunction, metaData?: EdgeStyleMetaData): void { + super.add(name, edgeStyle); + metaData?.handlerKind && this.handlerMapping.set(edgeStyle, metaData.handlerKind); + !isNullish(metaData?.isOrthogonal) && + this.orthogonalStates.set(edgeStyle, metaData.isOrthogonal); + } + + /** + * Retrieves the orthogonal state of the specified `edgeStyle` as it was registered. + * + * If the `edgeStyle` is not registered or the orthogonal state was not set during registration, this method returns `false`. + */ + isOrthogonal(edgeStyle: EdgeStyleFunction): boolean { + return this.orthogonalStates.get(edgeStyle) ?? false; + } + + /** + * Retrieves the handler kind of the specified `edgeStyle` as it was registered. + * + * If the `edgeStyle` is not registered or the `handlerKind` was not set during registration, this method returns `'default'`. + */ + getHandlerKind(edgeStyle: EdgeStyleFunction): EdgeStyleHandlerKind { + return this.handlerMapping.get(edgeStyle) ?? 'default'; + } + + /** + * **WARNING**: this method should not be called directly. Call the {@link unregisterAllEdgeStyles} function instead. + * @private + */ + clear(): void { + super.clear(); + this.handlerMapping.clear(); + this.orthogonalStates.clear(); + } +} + +/** + * A registry that stores the {@link EdgeStyle}s and their configuration. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export const EdgeStyleRegistry = new EdgeStyleRegistryImpl(); diff --git a/packages/core/src/view/style/edge/Elbow.ts b/packages/core/src/view/style/edge/Elbow.ts index 61784a89e7..98000afe2c 100644 --- a/packages/core/src/view/style/edge/Elbow.ts +++ b/packages/core/src/view/style/edge/Elbow.ts @@ -27,7 +27,11 @@ import type { EdgeStyleFunction } from '../../../types'; * Uses either {@link SideToSide} or {@link TopToBottom} depending on the horizontal flag in the cell style. * {@link SideToSide} is used if horizontal is `true` or unspecified. * - * This EdgeStyle is registered under `elbowEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `elbowEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'elbow' + * - isOrthogonal: true */ export const ElbowConnector: EdgeStyleFunction = ( state: CellState, diff --git a/packages/core/src/view/style/edge/EntityRelation.ts b/packages/core/src/view/style/edge/EntityRelation.ts index 1b742e5310..fb9c960207 100644 --- a/packages/core/src/view/style/edge/EntityRelation.ts +++ b/packages/core/src/view/style/edge/EntityRelation.ts @@ -35,7 +35,11 @@ import { EntityRelationConnectorConfig } from '../config'; * * The first and the last point in the result array are then replaced with Point that take into account the terminal's perimeter and next point on the edge. * - * This EdgeStyle is registered under `entityRelationEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `entityRelationEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'default' or unset + * - isOrthogonal: true * * @param state {@link CellState} that represents the edge to be updated. * @param source {@link CellState} that represents the source terminal. diff --git a/packages/core/src/view/style/edge/Loop.ts b/packages/core/src/view/style/edge/Loop.ts index ba1f258a52..d333716dfb 100644 --- a/packages/core/src/view/style/edge/Loop.ts +++ b/packages/core/src/view/style/edge/Loop.ts @@ -25,7 +25,11 @@ import type { EdgeStyleFunction } from '../../../types'; /** * Implements a self-reference, aka. loop. * - * This EdgeStyle is registered under `loopEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `loopEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'elbow' + * - isOrthogonal: false */ export const Loop: EdgeStyleFunction = ( state: CellState, diff --git a/packages/core/src/view/style/edge/Manhattan.ts b/packages/core/src/view/style/edge/Manhattan.ts index 9653bca253..fbb60eceb8 100644 --- a/packages/core/src/view/style/edge/Manhattan.ts +++ b/packages/core/src/view/style/edge/Manhattan.ts @@ -28,7 +28,11 @@ import { SegmentConnector } from './Segment'; * * Implements router to find the shortest route that avoids cells using manhattan distance as metric. * - * This EdgeStyle is registered under `manhattanEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `manhattanEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'segment' + * - isOrthogonal: true */ export const ManhattanConnector: EdgeStyleFunction = ( state: CellState, diff --git a/packages/core/src/view/style/edge/Orthogonal.ts b/packages/core/src/view/style/edge/Orthogonal.ts index a08b1c71fb..004e2c5ab8 100644 --- a/packages/core/src/view/style/edge/Orthogonal.ts +++ b/packages/core/src/view/style/edge/Orthogonal.ts @@ -144,7 +144,11 @@ function getJettySize(state: CellState, isSource: boolean): number { /** * Implements a local orthogonal router between the given cells. * - * This EdgeStyle is registered under `orthogonalEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `orthogonalEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'segment' + * - isOrthogonal: true * * @param state {@link CellState} that represents the edge to be updated. * @param sourceScaled {@link CellState} that represents the source terminal. diff --git a/packages/core/src/view/style/edge/Segment.ts b/packages/core/src/view/style/edge/Segment.ts index f5e1f4aad7..6ec0fce8cb 100644 --- a/packages/core/src/view/style/edge/Segment.ts +++ b/packages/core/src/view/style/edge/Segment.ts @@ -27,7 +27,11 @@ import type { EdgeStyleFunction } from '../../../types'; * Implements an orthogonal edge style. * Use {@link EdgeSegmentHandler} as an interactive handler for this style. * - * This EdgeStyle is registered under `segmentEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `segmentEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'segment' + * - isOrthogonal: true * * @param state {@link CellState} that represents the edge to be updated. * @param sourceScaled {@link CellState} that represents the source terminal. diff --git a/packages/core/src/view/style/edge/SideToSide.ts b/packages/core/src/view/style/edge/SideToSide.ts index 9e946a5962..0b5e76f8d2 100644 --- a/packages/core/src/view/style/edge/SideToSide.ts +++ b/packages/core/src/view/style/edge/SideToSide.ts @@ -25,7 +25,11 @@ import type { EdgeStyleFunction } from '../../../types'; /** * Implements a horizontal elbow edge. * - * This EdgeStyle is registered under `sideToSideEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `sideToSideEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'elbow' + * - isOrthogonal: true */ export const SideToSide: EdgeStyleFunction = ( state: CellState, diff --git a/packages/core/src/view/style/edge/TopToBottom.ts b/packages/core/src/view/style/edge/TopToBottom.ts index be96d44ec1..7f5e6ff659 100644 --- a/packages/core/src/view/style/edge/TopToBottom.ts +++ b/packages/core/src/view/style/edge/TopToBottom.ts @@ -25,7 +25,11 @@ import type { EdgeStyleFunction } from '../../../types'; /** * Implements a vertical elbow edge. * - * This EdgeStyle is registered under `topToBottomEdgeStyle` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * This EdgeStyle is registered under `topToBottomEdgeStyle` in {@link EdgeStyleRegistry} when using {@link Graph} or calling {@link registerDefaultEdgeStyles}. + * + * **IMPORTANT**: When registering it manually in {@link EdgeStyleRegistry}, the following metadata must be used: + * - handlerKind: 'elbow' + * - isOrthogonal: true */ export const TopToBottom: EdgeStyleFunction = ( state: CellState, diff --git a/packages/core/src/view/style/marker/EdgeMarkerRegistry.ts b/packages/core/src/view/style/marker/EdgeMarkerRegistry.ts index 3658d30677..739094473a 100644 --- a/packages/core/src/view/style/marker/EdgeMarkerRegistry.ts +++ b/packages/core/src/view/style/marker/EdgeMarkerRegistry.ts @@ -50,7 +50,11 @@ class MarkerShape { } /** - * Returns a function to paint the given marker. + * Returns a {@link MarkerFunction} to paint the given marker. + * + * The type parameter is used to retrieve the correct {@link MarkerFactoryFunction} from the registry which is then used to create the {@link MarkerFunction}. + * + * If none is found, `null` is returned. */ static createMarker( canvas: AbstractCanvas2D, diff --git a/packages/core/src/view/style/perimeter/EllipsePerimeter.ts b/packages/core/src/view/style/perimeter/EllipsePerimeter.ts index 0ba55594d1..cc2496ec4e 100644 --- a/packages/core/src/view/style/perimeter/EllipsePerimeter.ts +++ b/packages/core/src/view/style/perimeter/EllipsePerimeter.ts @@ -22,7 +22,7 @@ import Point from '../../geometry/Point'; import type { PerimeterFunction } from '../../../types'; /** - * This perimeter is registered under `ellipsePerimeter` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. + * This perimeter is registered under `ellipsePerimeter` in {@link PerimeterRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. * * @category Perimeter */ diff --git a/packages/core/src/view/style/perimeter/HexagonPerimeter.ts b/packages/core/src/view/style/perimeter/HexagonPerimeter.ts index 59813a317a..672693b011 100644 --- a/packages/core/src/view/style/perimeter/HexagonPerimeter.ts +++ b/packages/core/src/view/style/perimeter/HexagonPerimeter.ts @@ -23,7 +23,7 @@ import type { PerimeterFunction } from '../../../types'; import { intersection } from '../../../util/mathUtils'; /** - * This perimeter is registered under `hexagonPerimeter` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. + * This perimeter is registered under `hexagonPerimeter` in {@link PerimeterRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. * * @category Perimeter */ diff --git a/packages/core/src/view/style/perimeter/PerimeterRegistry.ts b/packages/core/src/view/style/perimeter/PerimeterRegistry.ts new file mode 100644 index 0000000000..f3447daf2e --- /dev/null +++ b/packages/core/src/view/style/perimeter/PerimeterRegistry.ts @@ -0,0 +1,27 @@ +/* +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 { BaseRegistry } from '../../../util/BaseRegistry'; +import { PerimeterFunction } from '../../../types'; + +/** + * A registry that stores the {@link Perimeter}s. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export const PerimeterRegistry = new BaseRegistry(); diff --git a/packages/core/src/view/style/perimeter/RectanglePerimeter.ts b/packages/core/src/view/style/perimeter/RectanglePerimeter.ts index 09fff30ae0..ca197b2005 100644 --- a/packages/core/src/view/style/perimeter/RectanglePerimeter.ts +++ b/packages/core/src/view/style/perimeter/RectanglePerimeter.ts @@ -24,7 +24,7 @@ import type { PerimeterFunction } from '../../../types'; /** * Describes a rectangular perimeter for the given bounds. * - * This perimeter is registered under `rectanglePerimeter` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. + * This perimeter is registered under `rectanglePerimeter` in {@link PerimeterRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. * * @category Perimeter */ diff --git a/packages/core/src/view/style/perimeter/RhombusPerimeter.ts b/packages/core/src/view/style/perimeter/RhombusPerimeter.ts index b21f9fdfa3..fbda2b6c3f 100644 --- a/packages/core/src/view/style/perimeter/RhombusPerimeter.ts +++ b/packages/core/src/view/style/perimeter/RhombusPerimeter.ts @@ -23,7 +23,7 @@ import type { PerimeterFunction } from '../../../types'; import { intersection } from '../../../util/mathUtils'; /** - * This perimeter is registered under `rhombusPerimeter` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. + * This perimeter is registered under `rhombusPerimeter` in {@link PerimeterRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. * * @category Perimeter */ diff --git a/packages/core/src/view/style/perimeter/TrianglePerimeter.ts b/packages/core/src/view/style/perimeter/TrianglePerimeter.ts index 6b63391b46..4a0452b3e0 100644 --- a/packages/core/src/view/style/perimeter/TrianglePerimeter.ts +++ b/packages/core/src/view/style/perimeter/TrianglePerimeter.ts @@ -23,7 +23,7 @@ import type { PerimeterFunction } from '../../../types'; import { intersection } from '../../../util/mathUtils'; /** - * This perimeter is registered under `trianglePerimeter` in {@link StyleRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. + * This perimeter is registered under `trianglePerimeter` in {@link PerimeterRegistry} when using {@link Graph} or calling {@link registerDefaultPerimeters}. * * @category Perimeter */ diff --git a/packages/core/src/view/style/register.ts b/packages/core/src/view/style/register.ts index 214aa42200..c878935eee 100644 --- a/packages/core/src/view/style/register.ts +++ b/packages/core/src/view/style/register.ts @@ -15,11 +15,13 @@ limitations under the License. */ import { EdgeStyle, EdgeMarker, Perimeter } from './builtin-style-elements'; -import StyleRegistry from './StyleRegistry'; +import { EdgeStyleRegistry } from './edge/EdgeStyleRegistry'; import MarkerShape from './marker/EdgeMarkerRegistry'; +import { PerimeterRegistry } from './perimeter/PerimeterRegistry'; import type { ArrowValue, EdgeStyleFunction, + EdgeStyleMetaData, EdgeStyleValue, MarkerFactoryFunction, PerimeterFunction, @@ -29,7 +31,7 @@ import type { let isDefaultEdgeStylesRegistered = false; /** - * Register default builtin {@link EdgeStyle}s in {@link StyleRegistry}. + * Register default builtin {@link EdgeStyle}s in {@link EdgeStyleRegistry}. * * @category Configuration * @category Style @@ -37,18 +39,23 @@ let isDefaultEdgeStylesRegistered = false; */ export const registerDefaultEdgeStyles = (): void => { if (!isDefaultEdgeStylesRegistered) { - const edgeStylesToRegister: [EdgeStyleValue, EdgeStyleFunction][] = [ - ['elbowEdgeStyle', EdgeStyle.ElbowConnector], - ['entityRelationEdgeStyle', EdgeStyle.EntityRelation], - ['loopEdgeStyle', EdgeStyle.Loop], - ['manhattanEdgeStyle', EdgeStyle.ManhattanConnector], - ['orthogonalEdgeStyle', EdgeStyle.OrthConnector], - ['segmentEdgeStyle', EdgeStyle.SegmentConnector], - ['sideToSideEdgeStyle', EdgeStyle.SideToSide], - ['topToBottomEdgeStyle', EdgeStyle.TopToBottom], - ]; - for (const [name, edgeStyle] of edgeStylesToRegister) { - StyleRegistry.putValue(name, edgeStyle); + const edgeStylesToRegister: [EdgeStyleValue, EdgeStyleFunction, EdgeStyleMetaData][] = + [ + ['elbowEdgeStyle', EdgeStyle.ElbowConnector, { handlerKind: 'elbow' }], + ['entityRelationEdgeStyle', EdgeStyle.EntityRelation, {}], + ['loopEdgeStyle', EdgeStyle.Loop, { handlerKind: 'elbow', isOrthogonal: false }], + ['manhattanEdgeStyle', EdgeStyle.ManhattanConnector, { handlerKind: 'segment' }], + ['orthogonalEdgeStyle', EdgeStyle.OrthConnector, { handlerKind: 'segment' }], + ['segmentEdgeStyle', EdgeStyle.SegmentConnector, { handlerKind: 'segment' }], + ['sideToSideEdgeStyle', EdgeStyle.SideToSide, { handlerKind: 'elbow' }], + ['topToBottomEdgeStyle', EdgeStyle.TopToBottom, { handlerKind: 'elbow' }], + ]; + for (const [name, edgeStyle, metadata] of edgeStylesToRegister) { + EdgeStyleRegistry.add(name, edgeStyle, { + ...metadata, + // most edge styles registered here are orthogonal, so set to true by default to avoid to duplicate the configuration code + isOrthogonal: metadata.isOrthogonal ?? true, + }); } isDefaultEdgeStylesRegistered = true; @@ -58,7 +65,7 @@ export const registerDefaultEdgeStyles = (): void => { let isDefaultPerimetersRegistered = false; /** - * Register default builtin {@link Perimeter}s in {@link StyleRegistry}. + * Register default builtin {@link Perimeter}s in {@link PerimeterRegistry}. * * @category Configuration * @category Style @@ -74,7 +81,7 @@ export const registerDefaultPerimeters = (): void => { ['trianglePerimeter', Perimeter.TrianglePerimeter], ]; for (const [name, perimeter] of perimetersToRegister) { - StyleRegistry.putValue(name, perimeter); + PerimeterRegistry.add(name, perimeter); } isDefaultPerimetersRegistered = true; @@ -82,18 +89,41 @@ export const registerDefaultPerimeters = (): void => { }; /** - * Unregister all {@link EdgeStyle}s and {@link Perimeter}s from {@link StyleRegistry}. + * Unregister all {@link EdgeStyle}s and {@link Perimeter}s from their registries. * - * **NOTE**: in the future, this function will be replaced by dedicated functions to remove `Perimeter` and `EdgeStyle` individually. - * For more details, see [Issue #767](https://github.com/maxGraph/maxGraph/issues/767). + * @see unregisterAllEdgeStyles + * @see unregisterAllPerimeters * * @category Configuration * @category Style * @since 0.18.0 */ export const unregisterAllEdgeStylesAndPerimeters = (): void => { - StyleRegistry.values = {}; + unregisterAllEdgeStyles(); + unregisterAllPerimeters(); +}; + +/** + * Unregister all {@link EdgeStyle}s from {@link EdgeStyleRegistry}. + * + * @category Configuration + * @category Style + * @since 0.20.0 + */ +export const unregisterAllEdgeStyles = (): void => { + EdgeStyleRegistry.clear(); isDefaultEdgeStylesRegistered = false; +}; + +/** + * Unregister all {@link Perimeter}s from {@link PerimeterRegistry}. + * + * @category Configuration + * @category Style + * @since 0.20.0 + */ +export const unregisterAllPerimeters = (): void => { + PerimeterRegistry.clear(); isDefaultPerimetersRegistered = false; }; diff --git a/packages/html/stories/Wires.stories.js b/packages/html/stories/Wires.stories.js index c9ba86e94c..df8b8b14e5 100644 --- a/packages/html/stories/Wires.stories.js +++ b/packages/html/stories/Wires.stories.js @@ -67,7 +67,7 @@ import { DomHelpers, Rectangle, EdgeHandler, - StyleRegistry, + EdgeStyleRegistry, EdgeSegmentHandler, UndoManager, CellEditorHandler, @@ -698,7 +698,7 @@ const Template = ({ label, ...args }) => { } }; - StyleRegistry.putValue('wireEdgeStyle', WireConnector); + EdgeStyleRegistry.add('wireEdgeStyle', WireConnector); let graph = new MyCustomGraph(container, null, [ MyCustomCellEditorHandler, diff --git a/packages/js-example-selected-features/src/index.js b/packages/js-example-selected-features/src/index.js index fb1c227ff4..4fc18f9ad5 100644 --- a/packages/js-example-selected-features/src/index.js +++ b/packages/js-example-selected-features/src/index.js @@ -27,10 +27,10 @@ import { ModelXmlSerializer, PanningHandler, Perimeter, + PerimeterRegistry, RubberBandHandler, SelectionCellsHandler, SelectionHandler, - StyleRegistry, } from '@maxgraph/core'; /** @@ -39,7 +39,7 @@ import { class CustomGraph extends BaseGraph { registerDefaults() { // Register styles - StyleRegistry.putValue('rectanglePerimeter', Perimeter.RectanglePerimeter); // declared in the default vertex style, so must be registered to be used + PerimeterRegistry.add('rectanglePerimeter', Perimeter.RectanglePerimeter); // declared in the default vertex style, so must be registered to be used MarkerShape.addMarker('classic', EdgeMarker.createArrow(2)); } } diff --git a/packages/ts-example-selected-features/src/main.ts b/packages/ts-example-selected-features/src/main.ts index 52adbd3c46..64c02e93e3 100644 --- a/packages/ts-example-selected-features/src/main.ts +++ b/packages/ts-example-selected-features/src/main.ts @@ -23,16 +23,17 @@ import { constants, EdgeMarker, EdgeStyle, + EdgeStyleRegistry, EllipseShape, FitPlugin, InternalEvent, MarkerShape, PanningHandler, Perimeter, + PerimeterRegistry, RubberBandHandler, SelectionCellsHandler, SelectionHandler, - StyleRegistry, } from '@maxgraph/core'; /** @@ -45,9 +46,12 @@ class CustomGraph extends BaseGraph { CellRenderer.registerShape('ellipse', EllipseShape); // Register styles - StyleRegistry.putValue('ellipsePerimeter', Perimeter.EllipsePerimeter); - StyleRegistry.putValue('rectanglePerimeter', Perimeter.RectanglePerimeter); // declared in the default vertex style, so must be registered to be used - StyleRegistry.putValue('orthogonalEdgeStyle', EdgeStyle.OrthConnector); + PerimeterRegistry.add('ellipsePerimeter', Perimeter.EllipsePerimeter); + PerimeterRegistry.add('rectanglePerimeter', Perimeter.RectanglePerimeter); // declared in the default vertex style, so must be registered to be used + EdgeStyleRegistry.add('orthogonalEdgeStyle', EdgeStyle.OrthConnector, { + handlerKind: 'segment', + isOrthogonal: true, + }); const arrowFunction = EdgeMarker.createArrow(2); MarkerShape.addMarker('classic', arrowFunction); diff --git a/packages/ts-example-selected-features/vite.config.js b/packages/ts-example-selected-features/vite.config.js index f4bab01b25..2a2618826e 100644 --- a/packages/ts-example-selected-features/vite.config.js +++ b/packages/ts-example-selected-features/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 378, // @maxgraph/core + chunkSizeWarningLimit: 370, // @maxgraph/core }, }; }); diff --git a/packages/ts-example-without-defaults/vite.config.js b/packages/ts-example-without-defaults/vite.config.js index cffbd82595..9a0192cfa2 100644 --- a/packages/ts-example-without-defaults/vite.config.js +++ b/packages/ts-example-without-defaults/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 327, // @maxgraph/core + chunkSizeWarningLimit: 310, // @maxgraph/core }, }; }); diff --git a/packages/ts-example/vite.config.js b/packages/ts-example/vite.config.js index 9bac0c6678..ae63a91e86 100644 --- a/packages/ts-example/vite.config.js +++ b/packages/ts-example/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 435, // @maxgraph/core + chunkSizeWarningLimit: 436, // @maxgraph/core }, }; }); diff --git a/packages/website/docs/usage/edge-styles.md b/packages/website/docs/usage/edge-styles.md index 11e6601f34..a4025c1051 100644 --- a/packages/website/docs/usage/edge-styles.md +++ b/packages/website/docs/usage/edge-styles.md @@ -18,10 +18,15 @@ An `EdgeStyle` is configured within the style properties of the Cell that relate By default, an edge `EdgeStyle` is unset. :::note -All EdgeStyles provided by `maxGraph` are automatically registered in the `StyleRegistry` when a `Graph` instance is created. For more details, see the [Global Configuration](global-configuration.md#styles) documentation. +All EdgeStyles provided by `maxGraph` are automatically registered in the `EdgeStyleRegistry` when a `Graph` instance is created. For more details, see the [Global Configuration](global-configuration.md#styles) documentation. To check the list of registered EdgeStyles, refer to the `registerDefaultStyleElements` function. ::: +:::info +The `EdgeStyleRegistry` is a new registry introduced in version 0.20.0 to manage edge styles. + +Edge styles were previously managed by the `StyleRegistry`, which has then been removed. +::: ## How to Use a Specific EdgeStyle @@ -31,7 +36,7 @@ For more details about the usage of EdgeStyles, see the documentation of `CellSt `maxGraph` provides various edgeStyle functions under the `EdgeStyle` namespace to be used in the `style` property of an Edge as the value of `CellStateStyle.edgeStyle`. -The following example uses the built-in `ElbowConnector` (registered under the `elbowEdgeStyle` key in `StyleRegistry`): +The following example uses the built-in `ElbowConnector` which is registered by default under the `elbowEdgeStyle` key in `EdgeStyleRegistry`: ```javascript style.edgeStyle = 'elbowEdgeStyle'; @@ -43,7 +48,7 @@ The `CellStateStyle.edgeStyle` type guides you on how to set the EdgeStyle value ::: -It is possible to configure the default EdgeStyle for all edges in the `Graph`, for example to use `SegmentConnector` (registered by default under the `segmentEdgeStyle` key in the `StyleRegistry`), as follows: +It is possible to configure the default EdgeStyle for all edges in the `Graph`, for example to use `SegmentConnector` (registered by default under the `segmentEdgeStyle` key in the `EdgeStyleRegistry`), as follows: ```javascript const style = stylesheet.getDefaultEdgeStyle(); @@ -75,11 +80,19 @@ const MyStyle: EdgeStyleFunction = (state, source, target, points, result) => { }; ``` -The new edge style can then be registered in the `StyleRegistry` as follows: +The new edge style can then be registered in the `EdgeStyleRegistry` as follows: ```javascript -StyleRegistry.putValue('myEdgeStyle', MyStyle); +const edgeStyleMetadata = { + handlerKind: 'segment', + isOrthogonal: true, +}; +EdgeStyleRegistry.add('myEdgeStyle', MyStyle, edgeStyleMetadata); ``` +:::warning +When registering the `EdgeStyle`, be sure to register it in the `EdgeStyleRegistry` with correct `EdgeStyleMetaData`. +Some maxGraph features depend on the `EdgeStyleMetaData` to work correctly. +::: ### Using a Custom EdgeStyle diff --git a/packages/website/docs/usage/global-configuration.md b/packages/website/docs/usage/global-configuration.md index 7b20b0fb61..bc4188a9c3 100644 --- a/packages/website/docs/usage/global-configuration.md +++ b/packages/website/docs/usage/global-configuration.md @@ -52,9 +52,10 @@ See also discussions in [issue #192](https://github.com/maxGraph/maxGraph/issues `maxGraph` provides several global registries used to register style configurations. + - `EdgeStyleRegistry`: edge styles (since 0.20.0, previously managed by `StyleRegistry`) - `CellRenderer`: shapes - `MarkerShape`: edge markers (also known as `startArrow` and `endArrow` in `CellStateStyle`) - - `StyleRegistry`: edge styles and perimeters + - `PerimeterRegistry`: perimeters (since 0.20.0, previously managed by `StyleRegistry`) - `StencilShapeRegistry`: stencil shapes When instantiating a `Graph` object, the registries are filled with `maxGraph` default style configurations. There is no default stencil shapes registered by default. @@ -72,7 +73,9 @@ It is possible to unregister all elements from a style registry using the relate ```javascript unregisterAllEdgeMarkers(); +unregisterAllEdgeStyles(); // since 0.20.0 unregisterAllEdgeStylesAndPerimeters(); +unregisterAllPerimeters(); // since 0.20.0 unregisterAllShapes(); unregisterAllStencilShapes(); ``` diff --git a/packages/website/docs/usage/perimeters.md b/packages/website/docs/usage/perimeters.md index 7bfff4fa37..69b1f6ce51 100644 --- a/packages/website/docs/usage/perimeters.md +++ b/packages/website/docs/usage/perimeters.md @@ -20,10 +20,17 @@ When setting it up, ensure it aligns with the shape of the vertex. Otherwise, th By default, a vertex perimeter is a _rectangle_. :::note -All perimeters provided by `maxGraph` are automatically registered in the `StyleRegistry` when a `Graph` instance is created. For more details, see the [Global Configuration](global-configuration.md#styles) documentation. +All perimeters provided by `maxGraph` are automatically registered in the `PerimeterRegistry` when a `Graph` instance is created. For more details, see the [Global Configuration](global-configuration.md#styles) documentation. To check the list of registered perimeters, refer to the `registerDefaultStyleElements` function. ::: +:::info +The `PerimeterRegistry` is a new registry introduced in version 0.20.0 to manage edge styles. + +Edge styles were previously managed by the `StyleRegistry`, which has then been removed. +::: + + ### Disabling the Perimeter It is possible to configure a vertex to ignore the perimeter. To do so, set `style.perimeter` to `null` or `undefined`. @@ -101,8 +108,8 @@ For more details about the usage of perimeters, see the documentation of `CellSt style.perimeter = Perimeter.EllipsePerimeter; ``` -It is also possible to set the perimeter using a string under which the perimeter has been registered in `StyleRegistry`. -By default, `maxGraph` registers all perimeters functions under the `Perimeter` namespace in the `StyleRegistry`: +It is also possible to set the perimeter using a string under which the perimeter has been registered in `PerimeterRegistry`. +By default, `maxGraph` registers all perimeters functions under the `Perimeter` namespace in the `PerimeterRegistry`: ```javascript style.perimeter = 'rhombusPerimeter'; @@ -141,9 +148,9 @@ const CustomPerimeter: PerimeterFunction = ( } ``` -The new perimeter can then be registered in the `StyleRegistry` as follows if you are intended to use it as a string in `CellStateStyle.perimeter`: +The new perimeter can then be registered in the `PerimeterRegistry` as follows if you are intended to use it as a string in `CellStateStyle.perimeter`: ```javascript -StyleRegistry.putValue('customPerimeter', CustomPerimeter); +PerimeterRegistry.add('customPerimeter', CustomPerimeter); ``` ### Using a Custom Perimeter From 1bb0c278988191718f219b5bfca3c820fd468b16 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Mon, 5 May 2025 19:41:08 +0200 Subject: [PATCH 2/8] Registries: mark properties as read-only --- packages/core/src/util/BaseRegistry.ts | 2 +- packages/core/src/view/style/edge/EdgeStyleRegistry.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/util/BaseRegistry.ts b/packages/core/src/util/BaseRegistry.ts index b942da9834..98dbc6dce8 100644 --- a/packages/core/src/util/BaseRegistry.ts +++ b/packages/core/src/util/BaseRegistry.ts @@ -21,7 +21,7 @@ limitations under the License. * @since 0.20.0 */ export class BaseRegistry { - protected values = new Map(); + protected readonly values = new Map(); add(name: string, value: V): void { this.values.set(name, value); diff --git a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts index ae590349fb..b0a8d18d2d 100644 --- a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts +++ b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts @@ -30,8 +30,8 @@ import { BaseRegistry } from '../../../util/BaseRegistry'; * @category Configuration */ export class EdgeStyleRegistryImpl extends BaseRegistry { - private handlerMapping = new Map(); - private orthogonalStates = new Map(); + private readonly handlerMapping = new Map(); + private readonly orthogonalStates = new Map(); add(name: string, edgeStyle: EdgeStyleFunction, metaData?: EdgeStyleMetaData): void { super.add(name, edgeStyle); From e96be4a139ac74ef7dd3ed454c0730df02808315 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Mon, 5 May 2025 19:49:53 +0200 Subject: [PATCH 3/8] Allow passing nullish parameter when retrieving info from EdgeStyleRegistry --- .../core/__tests__/view/style/EdgeStyleRegistry.test.ts | 6 ++++++ packages/core/src/view/AbstractGraph.ts | 4 ++-- packages/core/src/view/style/edge/EdgeStyleRegistry.ts | 8 ++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts b/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts index ff2765dcef..a3fd5e8e59 100644 --- a/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts +++ b/packages/core/__tests__/view/style/EdgeStyleRegistry.test.ts @@ -47,6 +47,12 @@ describe('registry', () => { expect(EdgeStyleRegistry.isOrthogonal(customEdgeStyle)).toBeTruthy(); }); + test.each([null, undefined])('retrieve with nullish: %s', (value) => { + expect(EdgeStyleRegistry.get(value)).toBeNull(); + expect(EdgeStyleRegistry.getHandlerKind(value)).toEqual('default'); + expect(EdgeStyleRegistry.isOrthogonal(value)).toBeFalsy(); + }); + test('verify registration - no meta data', () => { EdgeStyleRegistry.add('custom', customEdgeStyle); diff --git a/packages/core/src/view/AbstractGraph.ts b/packages/core/src/view/AbstractGraph.ts index d054ad53d2..e5857b1470 100644 --- a/packages/core/src/view/AbstractGraph.ts +++ b/packages/core/src/view/AbstractGraph.ts @@ -983,7 +983,7 @@ export abstract class AbstractGraph extends EventSource { * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state: CellState, edgeStyle: EdgeStyleFunction | null): EdgeHandler { - const handlerKind = EdgeStyleRegistry.getHandlerKind(edgeStyle!); // TODO accept nullish? + const handlerKind = EdgeStyleRegistry.getHandlerKind(edgeStyle); switch (handlerKind) { case 'elbow': return this.createElbowEdgeHandler(state); @@ -1186,7 +1186,7 @@ export abstract class AbstractGraph extends EventSource { // fallback when the orthogonal style is not defined const edgeStyle = this.view.getEdgeStyle(edge); - return EdgeStyleRegistry.isOrthogonal(edgeStyle!); + return EdgeStyleRegistry.isOrthogonal(edgeStyle); } /***************************************************************************** diff --git a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts index b0a8d18d2d..dfd46c6128 100644 --- a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts +++ b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts @@ -45,8 +45,8 @@ export class EdgeStyleRegistryImpl extends BaseRegistry { * * If the `edgeStyle` is not registered or the orthogonal state was not set during registration, this method returns `false`. */ - isOrthogonal(edgeStyle: EdgeStyleFunction): boolean { - return this.orthogonalStates.get(edgeStyle) ?? false; + isOrthogonal(edgeStyle?: EdgeStyleFunction | null): boolean { + return this.orthogonalStates.get(edgeStyle!) ?? false; } /** @@ -54,8 +54,8 @@ export class EdgeStyleRegistryImpl extends BaseRegistry { * * If the `edgeStyle` is not registered or the `handlerKind` was not set during registration, this method returns `'default'`. */ - getHandlerKind(edgeStyle: EdgeStyleFunction): EdgeStyleHandlerKind { - return this.handlerMapping.get(edgeStyle) ?? 'default'; + getHandlerKind(edgeStyle?: EdgeStyleFunction | null): EdgeStyleHandlerKind { + return this.handlerMapping.get(edgeStyle!) ?? 'default'; } /** From d1056cab1773d25dd47ef4f3877765e8c8566349 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Mon, 5 May 2025 19:58:05 +0200 Subject: [PATCH 4/8] Codec: only try to turn function into string. No longer tries to do it with object in GraphViewCodec. The related registries only hold function. In addition, the logic was wrong. It should have been a OR instead of a AND when checking for 'function' and 'object'. --- packages/core/src/serialization/codecs/GraphViewCodec.ts | 4 ++-- packages/core/src/serialization/codecs/StylesheetCodec.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/serialization/codecs/GraphViewCodec.ts b/packages/core/src/serialization/codecs/GraphViewCodec.ts index 45dfafa066..57d1bc8242 100644 --- a/packages/core/src/serialization/codecs/GraphViewCodec.ts +++ b/packages/core/src/serialization/codecs/GraphViewCodec.ts @@ -108,8 +108,8 @@ export class GraphViewCodec extends ObjectCodec { // @ts-ignore let value = state.style[i]; - // Tries to turn objects and functions into strings - if (typeof value === 'function' && typeof value === 'object') { + // Tries to turn functions into strings + if (typeof value === 'function') { value = getNameFromRegistries(value); } diff --git a/packages/core/src/serialization/codecs/StylesheetCodec.ts b/packages/core/src/serialization/codecs/StylesheetCodec.ts index 891a00ce44..289f142631 100644 --- a/packages/core/src/serialization/codecs/StylesheetCodec.ts +++ b/packages/core/src/serialization/codecs/StylesheetCodec.ts @@ -84,6 +84,7 @@ export class StylesheetCodec extends ObjectCodec { getStringValue(key: string, value: any): string | null { const type = typeof value; + // Tries to turn functions into strings if (type === 'function') { value = getNameFromRegistries(value); } else if (type === 'object') { From 22796570df32a70dc66ce15bf27c78e043c1a8cb Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Mon, 5 May 2025 20:54:40 +0200 Subject: [PATCH 5/8] Graph.test.ts: remove TODO [skip ci] --- packages/core/__tests__/view/Graph.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/__tests__/view/Graph.test.ts b/packages/core/__tests__/view/Graph.test.ts index 58f1beee2f..7bfc4989f2 100644 --- a/packages/core/__tests__/view/Graph.test.ts +++ b/packages/core/__tests__/view/Graph.test.ts @@ -82,7 +82,7 @@ describe('isOrthogonal', () => { ['custom', customEdgeStyle], ['Loop', EdgeStyle.Loop], ['null', null], - ['undefined', undefined], // TODO probably already tested above + ['undefined', undefined], ])('Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { const graph = new BaseGraph(); const cellState = new CellState(graph.view, null, { edgeStyle }); @@ -101,7 +101,7 @@ describe('isOrthogonal', () => { ['SideToSide', EdgeStyle.SideToSide], ['TopToBottom', EdgeStyle.TopToBottom], ['null', null], - ['undefined', undefined], // TODO probably already tested above + ['undefined', undefined], ])( 'Default builtin styles NOT registered - Style of the CellState, edgeStyle: %s', (_name, edgeStyle) => { From 977f5e868097f9f34bd1bda8ab21a141a81568ec Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Tue, 6 May 2025 07:19:21 +0200 Subject: [PATCH 6/8] refactor: introduce interface and hide implementation details --- .../{util => internal}/BaseRegistry.test.ts | 2 +- packages/core/src/index.ts | 1 - .../src/{util => internal}/BaseRegistry.ts | 11 ++--- packages/core/src/types.ts | 45 +++++++++++++++++++ .../src/view/style/edge/EdgeStyleRegistry.ts | 30 ++++--------- .../view/style/perimeter/PerimeterRegistry.ts | 8 ++-- 6 files changed, 62 insertions(+), 35 deletions(-) rename packages/core/__tests__/{util => internal}/BaseRegistry.test.ts (96%) rename packages/core/src/{util => internal}/BaseRegistry.ts (84%) diff --git a/packages/core/__tests__/util/BaseRegistry.test.ts b/packages/core/__tests__/internal/BaseRegistry.test.ts similarity index 96% rename from packages/core/__tests__/util/BaseRegistry.test.ts rename to packages/core/__tests__/internal/BaseRegistry.test.ts index 56728f7365..1f2d716fe3 100644 --- a/packages/core/__tests__/util/BaseRegistry.test.ts +++ b/packages/core/__tests__/internal/BaseRegistry.test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { describe, expect, test } from '@jest/globals'; -import { BaseRegistry } from '../../src'; +import { BaseRegistry } from '../../src/internal/BaseRegistry'; test('registration', () => { const baseRegistry = new BaseRegistry(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 52451cfa00..3ad92bc6e2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -181,7 +181,6 @@ export * as styleUtils from './util/styleUtils'; */ export * as xmlUtils from './util/xmlUtils'; -export * from './util/BaseRegistry'; export * from './util/config'; export * from './util/logger'; diff --git a/packages/core/src/util/BaseRegistry.ts b/packages/core/src/internal/BaseRegistry.ts similarity index 84% rename from packages/core/src/util/BaseRegistry.ts rename to packages/core/src/internal/BaseRegistry.ts index 98dbc6dce8..c1908ba184 100644 --- a/packages/core/src/util/BaseRegistry.ts +++ b/packages/core/src/internal/BaseRegistry.ts @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { Registry } from '../types'; + /** * Base implementation for all registries storing "style" configuration. - * @category Style - * @category Configuration + * @private * @since 0.20.0 */ -export class BaseRegistry { +export class BaseRegistry implements Registry { protected readonly values = new Map(); add(name: string, value: V): void { @@ -40,10 +41,6 @@ export class BaseRegistry { return null; } - /** - * **WARNING**: this method should not be called directly. Call the related global unregister function instead. - * @private - */ clear(): void { this.values.clear(); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5ac1f12113..214343d518 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1487,6 +1487,26 @@ export type DialectValue = */ export type ElbowValue = 'horizontal' | 'vertical'; +/** + * The base definition of all registries storing "style" configuration. + * @category Style + * @category Configuration + * @since 0.20.0 + */ +export interface Registry { + add(name: string, value: V): void; + + get(name: string | null | undefined): V | null; + + getName(value: V | null): string | null; + + /** + * **WARNING**: this method should not be called directly. Call the related global unregister function instead. + * @private + */ + clear(): void; +} + /** * Allowed values for {@link EdgeStyleMetaData.handlerKind}. * @@ -1519,3 +1539,28 @@ export type EdgeStyleMetaData = { * @default false */ isOrthogonal?: boolean; }; + +/** + * The definition of a registry that stores the {@link EdgeStyleFunction}s and their configuration. + * + * @since 0.20.0 + * @category Style + * @category Configuration + */ +export interface EdgeStyleRegistryInterface extends Registry { + add(name: string, edgeStyle: EdgeStyleFunction, metaData?: EdgeStyleMetaData): void; + + /** + * Retrieves the orthogonal state of the specified `edgeStyle` as it was registered. + * + * If the `edgeStyle` is not registered or the orthogonal state was not set during registration, this method returns `false`. + */ + isOrthogonal(edgeStyle?: EdgeStyleFunction | null): boolean; + + /** + * Retrieves the handler kind of the specified `edgeStyle` as it was registered. + * + * If the `edgeStyle` is not registered or the `handlerKind` was not set during registration, this method returns `'default'`. + */ + getHandlerKind(edgeStyle?: EdgeStyleFunction | null): EdgeStyleHandlerKind; +} diff --git a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts index dfd46c6128..d4d7eab450 100644 --- a/packages/core/src/view/style/edge/EdgeStyleRegistry.ts +++ b/packages/core/src/view/style/edge/EdgeStyleRegistry.ts @@ -18,18 +18,18 @@ import type { EdgeStyleFunction, EdgeStyleHandlerKind, EdgeStyleMetaData, + EdgeStyleRegistryInterface, } from '../../../types'; import { isNullish } from '../../../internal/utils'; -import { BaseRegistry } from '../../../util/BaseRegistry'; +import { BaseRegistry } from '../../../internal/BaseRegistry'; /** - * Implementation of the {@link EdgeStyleRegistry}. - * - * @since 0.20.0 - * @category Style - * @category Configuration + * @private */ -export class EdgeStyleRegistryImpl extends BaseRegistry { +class EdgeStyleRegistryImpl + extends BaseRegistry + implements EdgeStyleRegistryInterface +{ private readonly handlerMapping = new Map(); private readonly orthogonalStates = new Map(); @@ -40,28 +40,14 @@ export class EdgeStyleRegistryImpl extends BaseRegistry { this.orthogonalStates.set(edgeStyle, metaData.isOrthogonal); } - /** - * Retrieves the orthogonal state of the specified `edgeStyle` as it was registered. - * - * If the `edgeStyle` is not registered or the orthogonal state was not set during registration, this method returns `false`. - */ isOrthogonal(edgeStyle?: EdgeStyleFunction | null): boolean { return this.orthogonalStates.get(edgeStyle!) ?? false; } - /** - * Retrieves the handler kind of the specified `edgeStyle` as it was registered. - * - * If the `edgeStyle` is not registered or the `handlerKind` was not set during registration, this method returns `'default'`. - */ getHandlerKind(edgeStyle?: EdgeStyleFunction | null): EdgeStyleHandlerKind { return this.handlerMapping.get(edgeStyle!) ?? 'default'; } - /** - * **WARNING**: this method should not be called directly. Call the {@link unregisterAllEdgeStyles} function instead. - * @private - */ clear(): void { super.clear(); this.handlerMapping.clear(); @@ -76,4 +62,4 @@ export class EdgeStyleRegistryImpl extends BaseRegistry { * @category Style * @category Configuration */ -export const EdgeStyleRegistry = new EdgeStyleRegistryImpl(); +export const EdgeStyleRegistry: EdgeStyleRegistryInterface = new EdgeStyleRegistryImpl(); diff --git a/packages/core/src/view/style/perimeter/PerimeterRegistry.ts b/packages/core/src/view/style/perimeter/PerimeterRegistry.ts index f3447daf2e..be22000989 100644 --- a/packages/core/src/view/style/perimeter/PerimeterRegistry.ts +++ b/packages/core/src/view/style/perimeter/PerimeterRegistry.ts @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { BaseRegistry } from '../../../util/BaseRegistry'; -import { PerimeterFunction } from '../../../types'; +import { BaseRegistry } from '../../../internal/BaseRegistry'; +import type { PerimeterFunction, Registry } from '../../../types'; /** - * A registry that stores the {@link Perimeter}s. + * A registry that stores the {@link PerimeterFunction}s. * * @since 0.20.0 * @category Style * @category Configuration */ -export const PerimeterRegistry = new BaseRegistry(); +export const PerimeterRegistry: Registry = new BaseRegistry(); From 74900b2d2c80130c3b8d271e9b0ac16210bc1280 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Tue, 6 May 2025 08:01:55 +0200 Subject: [PATCH 7/8] docs: explain implementation choice in BaseRegistry.getName [skip ci] --- packages/core/src/internal/BaseRegistry.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/internal/BaseRegistry.ts b/packages/core/src/internal/BaseRegistry.ts index c1908ba184..7df25b7f13 100644 --- a/packages/core/src/internal/BaseRegistry.ts +++ b/packages/core/src/internal/BaseRegistry.ts @@ -33,6 +33,10 @@ export class BaseRegistry implements Registry { } getName(value: V | null): string | null { + // Currently, the code performs a linear search through all entries. + // This implementation is straightforward and works well for small registries, but could become a performance bottleneck if the registry grows large. + // For the current use case of style registries, this is likely acceptable since the number of registered styles is typically small. + // If performance becomes an issue, consider maintaining a reverse lookup map. for (const [name, style] of this.values.entries()) { if (style === value) { return name; From 99a6a22416852a83b257d0102ec20d0704b7bc76 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Tue, 6 May 2025 08:03:53 +0200 Subject: [PATCH 8/8] BaseRegistry.test.ts: add a test for registry entry overrides --- .../core/__tests__/internal/BaseRegistry.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/core/__tests__/internal/BaseRegistry.test.ts b/packages/core/__tests__/internal/BaseRegistry.test.ts index 1f2d716fe3..aa71668153 100644 --- a/packages/core/__tests__/internal/BaseRegistry.test.ts +++ b/packages/core/__tests__/internal/BaseRegistry.test.ts @@ -28,6 +28,19 @@ test('registration', () => { expect(baseRegistry.get(undefined)).toBeNull(); }); +test('registration override', () => { + const baseRegistry = new BaseRegistry>(); + const object1 = { property: 'value1' }; + const object2 = { property: 'value2' }; + + baseRegistry.add('name', object1); + expect(baseRegistry.get('name')).toBe(object1); + + baseRegistry.add('name', object2); + expect(baseRegistry.get('name')).toBe(object2); + expect(baseRegistry.get('name')).not.toBe(object1); +}); + test('clear', () => { const baseRegistry = new BaseRegistry(); baseRegistry.add('name', { property: 'value' });