diff --git a/CHANGELOG.md b/CHANGELOG.md index 698300c21f..53f409acaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,13 @@ They had been made public by mistake, and had been considered internal since the So, remove it without replacement. - `Client.VERSION` moved to `constants.VERSION`. VERSION is supposed to be immutable as it represents the actual version of maxGraph. When it was stored in Client, it was a static property that could be modified. Moving it to the constants module ensures that it cannot be modified. +- `ManhattanConnector` is now configured with the global `ManhattanConnectorConfig` object. +The following properties that were previously on `EdgeStyle` have moved to `ManhattanConnectorConfig`: + - `MANHATTAN_END_DIRECTIONS` to `endDirections` + - `MANHATTAN_MAX_ALLOWED_DIRECTION_CHANGE` to `maxAllowedDirectionChange` + - `MANHATTAN_MAXIMUM_LOOPS` to `maxLoops` + - `MANHATTAN_START_DIRECTIONS` to `startDirections` + - `MANHATTAN_STEP` to `step` - `OrthConnector` is now configured with the global `OrthConnectorConfig` object. The following properties that were previously on `EdgeStyle` have moved to `OrthConnectorConfig`: - `orthBuffer` to `buffer` diff --git a/packages/core/__tests__/view/style/config.test.ts b/packages/core/__tests__/view/style/config.test.ts index 17558eb261..cc6fe15ac0 100644 --- a/packages/core/__tests__/view/style/config.test.ts +++ b/packages/core/__tests__/view/style/config.test.ts @@ -14,8 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { expect, test } from '@jest/globals'; -import { OrthConnectorConfig, resetOrthConnectorConfig } from '../../../src'; +import { describe, expect, test } from '@jest/globals'; +import { + ManhattanConnectorConfig, + OrthConnectorConfig, + resetManhattanConnectorConfig, + resetOrthConnectorConfig, +} from '../../../src'; +import { DIRECTION } from '../../../src/util/Constants'; test('resetOrthConnectorConfig', () => { // Keep track of original default values @@ -32,3 +38,34 @@ test('resetOrthConnectorConfig', () => { expect(OrthConnectorConfig.pointsFallback).toBeTruthy(); expect(OrthConnectorConfig).toStrictEqual(originalConfig); }); + +describe('resetManhattanConnectorConfig', () => { + test('simple values', () => { + // Keep track of original default values + const originalConfig = { ...ManhattanConnectorConfig }; + + // Change some values + ManhattanConnectorConfig.maxLoops = 100; + ManhattanConnectorConfig.step = 60; + + resetManhattanConnectorConfig(); + + // Ensure that the values have correctly been reset + expect(ManhattanConnectorConfig.maxLoops).toBe(2000); + expect(ManhattanConnectorConfig.step).toBe(12); + expect(ManhattanConnectorConfig).toStrictEqual(originalConfig); + }); + test('original direction arrays are kept untouched', () => { + const originalEndDirections = [...ManhattanConnectorConfig.endDirections]; + const originalStartDirections = [...ManhattanConnectorConfig.startDirections]; + + // Change some values + ManhattanConnectorConfig.endDirections = [DIRECTION.NORTH, DIRECTION.SOUTH]; + ManhattanConnectorConfig.startDirections.push(DIRECTION.NORTH, DIRECTION.SOUTH); + + resetManhattanConnectorConfig(); + + expect(ManhattanConnectorConfig.endDirections).toEqual(originalEndDirections); + expect(ManhattanConnectorConfig.startDirections).toEqual(originalStartDirections); + }); +}); diff --git a/packages/core/src/util/cloneUtils.ts b/packages/core/src/util/cloneUtils.ts index e05bea0469..d0ff5dc52b 100644 --- a/packages/core/src/util/cloneUtils.ts +++ b/packages/core/src/util/cloneUtils.ts @@ -60,7 +60,7 @@ export const clone = function _clone( /** * Shallow copies properties from the source object to the target object. * - * **WARNING**: This function performs only a **shallow** copy i.e. there is no deep copy of the properties that are objects. + * **WARNING**: This function performs only a **shallow** copy i.e. there is no deep copy of the properties that are objects, expect for arrays. * * @template T The type of the objects. * @@ -74,7 +74,13 @@ export const shallowCopy = (source: T, target: T): void => { for (const key in source) { // attempt to prevent prototype pollution if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; + const sourceValue = source[key]; + if (Array.isArray(sourceValue)) { + // @ts-ignore source and target are of the same type + target[key] = [...sourceValue]; + } else { + target[key] = sourceValue; + } } } }; diff --git a/packages/core/src/view/style/EdgeStyle.ts b/packages/core/src/view/style/EdgeStyle.ts index 30ab9ebc3e..7dd50b2151 100644 --- a/packages/core/src/view/style/EdgeStyle.ts +++ b/packages/core/src/view/style/EdgeStyle.ts @@ -32,7 +32,7 @@ import { import Rectangle from '../geometry/Rectangle'; import Geometry from '../geometry/Geometry'; import { scaleCellState, scalePointArray } from './edge/shared'; -import type { EdgeStyleFunction } from '../../types'; +import type { DirectionValue, EdgeStyleFunction } from '../../types'; import { ElbowConnector as ElbowConnectorFunction } from './edge/Elbow'; import { EntityRelation as EntityRelationFunction } from './edge/EntityRelation'; @@ -40,7 +40,7 @@ import { Loop as LoopFunction } from './edge/Loop'; import { SegmentConnector as SegmentConnectorFunction } from './edge/Segment'; import { SideToSide as SideToSideFunction } from './edge/SideToSide'; import { TopToBottom as TopToBottomFunction } from './edge/TopToBottom'; -import { OrthConnectorConfig } from './config'; +import { ManhattanConnectorConfig, OrthConnectorConfig } from './config'; /** * Provides various edge styles to be used as the values for `edgeStyle` in a cell style. @@ -788,39 +788,6 @@ class EdgeStyle { } }; - // Size of the step to find a route - static MANHATTAN_STEP = 12; - - // If number of route finding loops exceed the maximum, stops searching and returns - // fallback route - static MANHATTAN_MAXIMUM_LOOPS = 2000; - - // Possible starting directions from an element - static MANHATTAN_START_DIRECTIONS: DIRECTION[] = [ - DIRECTION.NORTH, - DIRECTION.EAST, - DIRECTION.SOUTH, - DIRECTION.WEST, - ]; - - // Possible ending directions to an element - static MANHATTAN_END_DIRECTIONS: DIRECTION[] = [ - DIRECTION.NORTH, - DIRECTION.EAST, - DIRECTION.SOUTH, - DIRECTION.WEST, - ]; - - // Limit for directions change when searching route - static MANHATTAN_MAX_ALLOWED_DIRECTION_CHANGE = 90; - - static MANHATTAN_PADDING_BOX = new Geometry( - -this.MANHATTAN_STEP, - -this.MANHATTAN_STEP, - this.MANHATTAN_STEP * 2, - this.MANHATTAN_STEP * 2 - ); - /** * ManhattanConnector code is based on code from https://github.com/mwangm/mxgraph-manhattan-connector * @@ -929,11 +896,11 @@ class EdgeStyle { return res; } - const mStep = EdgeStyle.MANHATTAN_STEP; + const mStep = ManhattanConnectorConfig.step; const config = { // Padding applied on the element bounding boxes - paddingBox: EdgeStyle.MANHATTAN_PADDING_BOX, + paddingBox: new Geometry(-mStep, -mStep, mStep * 2, mStep * 2), // An array of directions to find next points on the route directions: [ @@ -972,8 +939,7 @@ class EdgeStyle { // A penalty received for direction change penaltiesGenerator: (angle: number) => { - if (angle == 45 || angle == 90 || angle == 180) - return EdgeStyle.MANHATTAN_STEP / 2; + if (angle == 45 || angle == 90 || angle == 180) return mStep / 2; return 0; }, // If a function is provided, it's used to route the link while dragging an end @@ -1131,13 +1097,13 @@ class EdgeStyle { function getRectPoints( bbox: Rectangle, - directionList: DIRECTION[], + directions: DirectionValue[], opt: typeof config ): Point[] { - const step = EdgeStyle.MANHATTAN_STEP; + const step = ManhattanConnectorConfig.step; const center = getRectangleCenter(bbox); const res: Point[] = []; - for (const direction of directionList) { + for (const direction of directions) { const directionPoint = opt.directionMap[direction]; const x = (directionPoint.x * bbox.width) / 2; @@ -1189,10 +1155,10 @@ class EdgeStyle { const y = isSourceCell ? edgeState.style.exitY : edgeState.style.entryY; const onlyHorizontalDirections = isSourceCell - ? EdgeStyle.MANHATTAN_START_DIRECTIONS.every( + ? ManhattanConnectorConfig.startDirections.every( (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH ) - : EdgeStyle.MANHATTAN_END_DIRECTIONS.every( + : ManhattanConnectorConfig.endDirections.every( (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH ); @@ -1206,10 +1172,10 @@ class EdgeStyle { const x = isSourceCell ? edgeState.style.exitX : edgeState.style.entryX; const onlyVerticalDirections = isSourceCell - ? EdgeStyle.MANHATTAN_START_DIRECTIONS.every( + ? ManhattanConnectorConfig.startDirections.every( (d) => d != DIRECTION.WEST && d != DIRECTION.EAST ) - : EdgeStyle.MANHATTAN_END_DIRECTIONS.every( + : ManhattanConnectorConfig.endDirections.every( (d) => d != DIRECTION.WEST && d != DIRECTION.EAST ); if (x != undefined && onlyVerticalDirections) { @@ -1227,18 +1193,18 @@ class EdgeStyle { obstacleMap: ObstacleMap, opt: typeof config ) { - // Caculate start points and end points - const step = EdgeStyle.MANHATTAN_STEP; + // Calculate start points and end points + const step = ManhattanConnectorConfig.step; const startPoints = getRectPoints( start, - EdgeStyle.MANHATTAN_START_DIRECTIONS, + ManhattanConnectorConfig.startDirections, opt ).filter((p) => obstacleMap.isPointAccessible(p)); const startCenter = snapPointToGrid(getRectangleCenter(start), step); const endPoints = getRectPoints( end, - EdgeStyle.MANHATTAN_END_DIRECTIONS, + ManhattanConnectorConfig.endDirections, opt ).filter((p) => obstacleMap.isPointAccessible(p)); const endCenter = snapPointToGrid(getRectangleCenter(end), step); @@ -1255,7 +1221,7 @@ class EdgeStyle { openSet.add(key, estimateCost(p, endPoints)); costs[key] = 0; }); - let loopsRemain = EdgeStyle.MANHATTAN_MAXIMUM_LOOPS; + let loopsRemain = ManhattanConnectorConfig.maxLoops; const endPointsKeys = endPoints.map((p) => pointToString(p)); let currentDirectionAngle: number | undefined; let previousDirectionAngle: number | undefined; @@ -1297,7 +1263,7 @@ class EdgeStyle { ); if ( previousDirectionAngle && - directionChangedAngle > EdgeStyle.MANHATTAN_MAX_ALLOWED_DIRECTION_CHANGE + directionChangedAngle > ManhattanConnectorConfig.maxAllowedDirectionChange ) { continue; } diff --git a/packages/core/src/view/style/config.ts b/packages/core/src/view/style/config.ts index 662a1e203b..cf15b25ff8 100644 --- a/packages/core/src/view/style/config.ts +++ b/packages/core/src/view/style/config.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ENTITY_SEGMENT } from '../../util/Constants'; +import { DIRECTION, ENTITY_SEGMENT } from '../../util/Constants'; import { shallowCopy } from '../../util/cloneUtils'; +import type { DirectionValue } from '../../types'; /** * Configure the `Entity Relation connector` defaults for maxGraph. @@ -81,3 +82,61 @@ const originalOrthConnectorConfig = { ...OrthConnectorConfig }; export const resetOrthConnectorConfig = (): void => { shallowCopy(originalOrthConnectorConfig, OrthConnectorConfig); }; + +export type ManhattanConnectorConfigType = { + /** + * Limit for directions change when searching route. + * @default 90 + */ + maxAllowedDirectionChange: number; + /** + * If number of route finding loops exceed the maximum, stops searching and returns fallback route + */ + maxLoops: number; + /** + * Possible ending directions from an element. + * + * @default all directions + */ + endDirections: DirectionValue[]; + /** + * Possible starting directions from an element. + * + * @default all directions + */ + startDirections: DirectionValue[]; + /** + * Size of the step to find a route. + * + * @default 12 + */ + step: number; +}; + +/** + * Configure the {@link ManhattanConnector}. + * + * @experimental subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. + * @since 0.16.0 + * @category Configuration + */ +export const ManhattanConnectorConfig: ManhattanConnectorConfigType = { + maxAllowedDirectionChange: 90, + maxLoops: 2000, + endDirections: Object.values(DIRECTION), + startDirections: Object.values(DIRECTION), + step: 12, +}; + +const originalManhattanConnectorConfig = {} as typeof ManhattanConnectorConfig; +shallowCopy(ManhattanConnectorConfig, originalManhattanConnectorConfig); +/** + * Resets {@link ManhattanConnectorConfig} to default values. + * + * @experimental Subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. + * @since 0.16.0 + * @category Configuration + */ +export const resetManhattanConnectorConfig = (): void => { + shallowCopy(originalManhattanConnectorConfig, ManhattanConnectorConfig); +}; diff --git a/packages/html/.storybook/preview.ts b/packages/html/.storybook/preview.ts index 76f06f7aa6..f99007b848 100644 --- a/packages/html/.storybook/preview.ts +++ b/packages/html/.storybook/preview.ts @@ -5,6 +5,7 @@ import { resetEdgeHandlerConfig, resetEntityRelationConnectorConfig, resetHandleConfig, + resetManhattanConnectorConfig, resetOrthConnectorConfig, resetStyleDefaultsConfig, resetVertexHandlerConfig, @@ -23,6 +24,7 @@ const resetMaxGraphConfigs = (): void => { resetEdgeHandlerConfig(); resetEntityRelationConnectorConfig(); resetHandleConfig(); + resetManhattanConnectorConfig(); resetOrthConnectorConfig(); resetStyleDefaultsConfig(); resetVertexHandlerConfig(); diff --git a/packages/html/stories/Manhattan.stories.js b/packages/html/stories/Manhattan.stories.ts similarity index 60% rename from packages/html/stories/Manhattan.stories.js rename to packages/html/stories/Manhattan.stories.ts index 5c963661a2..8c2aa17c82 100644 --- a/packages/html/stories/Manhattan.stories.js +++ b/packages/html/stories/Manhattan.stories.ts @@ -15,7 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EdgeStyle, Graph, InternalEvent, SelectionHandler } from '@maxgraph/core'; +import { + EventObject, + Graph, + InternalEvent, + ManhattanConnectorConfig, + type SelectionHandler, +} from '@maxgraph/core'; import { globalTypes, globalValues } from './shared/args.js'; import { createGraphContainer } from './shared/configure.js'; @@ -29,33 +35,35 @@ export default { }, }; -const Template = ({ label, ...args }) => { +const Template = ({ label, ...args }: Record) => { const container = createGraphContainer(args); - // Enables guides - SelectionHandler.prototype.guidesEnabled = true; - // Allow end of edge to come only from west - EdgeStyle.MANHATTAN_END_DIRECTIONS = ['west']; + ManhattanConnectorConfig.endDirections = ['west']; // Creates the graph inside the given container const graph = new Graph(container); + // Enables guides + graph.getPlugin('SelectionHandler').guidesEnabled = true; + // Hack to rerender edge on any node move - graph.model.addListener(InternalEvent.CHANGE, (sender, evt) => { - const changesList = evt.getProperty('changes'); - const hasMoveEdits = - changesList && changesList.some((c) => c.constructor.name == 'GeometryChange'); + graph.model.addListener(InternalEvent.CHANGE, (_sender: unknown, evt: EventObject) => { + const changes = evt.getProperty('changes'); + const hasMoveEdits = changes?.some( + // checks for the existence of the geometry and previous properties which are characteristic of GeometryChange in the model + (c: any) => typeof c.geometry !== 'undefined' && typeof c.previous !== 'undefined' + ); // If detected GeometryChange event, call graph.view.refresh(), which will reroute edge if (hasMoveEdits) { - graph?.view?.refresh(); + graph.view.refresh(); } }); const parent = graph.getDefaultParent(); // Adds cells to the model in a single step graph.batchUpdate(() => { - var style = graph.getStylesheet().getDefaultEdgeStyle(); + const style = graph.getStylesheet().getDefaultEdgeStyle(); style.labelBackgroundColor = '#FFFFFF'; style.strokeWidth = 2; style.rounded = true; @@ -65,11 +73,11 @@ const Template = ({ label, ...args }) => { // After move of "obstacles" nodes, move "finish" node - edge route will be recalculated style.edgeStyle = 'manhattanEdgeStyle'; - var v1 = graph.insertVertex(parent, null, 'start', 50, 50, 140, 70); - var v2 = graph.insertVertex(parent, null, 'finish', 500, 450, 140, 72); - var v3 = graph.insertVertex(parent, null, 'obstacle', 450, 50, 140, 80); - var v4 = graph.insertVertex(parent, null, 'obstacle', 250, 350, 140, 80); - var e1 = graph.insertEdge(parent, null, 'route', v1, v2); + const v1 = graph.insertVertex(parent, null, 'start', 50, 50, 140, 70); + const v2 = graph.insertVertex(parent, null, 'finish', 500, 450, 140, 72); + graph.insertVertex(parent, null, 'obstacle 1', 450, 50, 140, 80); + graph.insertVertex(parent, null, 'obstacle 2', 250, 350, 140, 80); + graph.insertEdge(parent, null, 'route', v1, v2); }); return container; diff --git a/packages/website/docs/usage/global-configuration.md b/packages/website/docs/usage/global-configuration.md index 586b483877..f3742da45b 100644 --- a/packages/website/docs/usage/global-configuration.md +++ b/packages/website/docs/usage/global-configuration.md @@ -21,6 +21,7 @@ The following objects can be used to configure `maxGraph` globally: - `VertexHandlerConfig` (since 0.12.0): for `VertexHandler`. - For Connectors/EdgeStyles: - `EntityRelationConnectorConfig` (since 0.15.0): for `EntityRelation`. + - `ManhattanConnectorConfig` (since 0.16.0): for `ManhattanConnector`. - `OrthConnectorConfig` (since 0.16.0): for `OrthConnector`. Some functions are provided to reset the global configuration to the default values. For example: @@ -31,6 +32,7 @@ Some functions are provided to reset the global configuration to the default val - `resetVertexHandlerConfig` (since 0.14.0) - For Connectors/EdgeStyles: - `resetEntityRelationConnectorConfig` (since 0.15.0) + - `resetManhattanConnectorConfig` (since 0.16.0) - `resetOrthConnectorConfig` (since 0.16.0) :::note