Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ describe('import mxGraph model', () => {
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" vertex="1" parent="1" value="Vertex with style" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;dashed=1;">
</mxCell>
<mxCell id="2" vertex="1" parent="1" value="Vertex with style" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;dashed=1;portConstraint=northsouth"/>
<mxCell id="3" edge="1" parent="1" value="Edge with style" style="sourcePortConstraint=northsouth;targetPortConstraint=eastwest;strokeColor=#FF0000;strokeWidth=2"/>
</root>
</mxGraphModel>`;

Expand All @@ -84,18 +84,30 @@ describe('import mxGraph model', () => {
const modelChecker = new ModelChecker(model);

modelChecker.checkRootCells();
modelChecker.checkCellsCount(3);
modelChecker.checkCellsCount(4);
modelChecker.expectIsVertex(model.getCell('2'), 'Vertex with style', {
style: {
// @ts-ignore FIX should be true
dashed: 1,
fillColor: '#E6E6E6',
html: 1,
// @ts-ignore mxGraph compatibility, getPortConstraints is able to support such a string even if it doesn't match the function signature
portConstraint: 'northsouth',
// @ts-ignore FIX should be false
rounded: 0,
whiteSpace: 'wrap',
},
});
modelChecker.expectIsEdge(model.getCell('3'), 'Edge with style', {
style: {
// @ts-ignore mxGraph compatibility, getPortConstraints is able to support such a string even if it doesn't match the function signature
sourcePortConstraint: 'northsouth',
strokeColor: '#FF0000',
strokeWidth: 2,
// @ts-ignore mxGraph compatibility, getPortConstraints is able to support such a string even if it doesn't match the function signature
targetPortConstraint: 'eastwest',
},
});
});
});

Expand Down
262 changes: 261 additions & 1 deletion packages/core/__tests__/util/mathUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,267 @@ limitations under the License.
*/

import { describe, expect, test } from '@jest/globals';
import { isNumeric } from '../../src/util/mathUtils';
import { getPortConstraints, isNumeric } from '../../src/util/mathUtils';
import { DIRECTION, DIRECTION_MASK } from '../../src/util/Constants';
import CellState from '../../src/view/cell/CellState';
import { DirectionValue } from '../../src';

describe('getPortConstraints', () => {
const defaultMask = DIRECTION_MASK.NONE;

test('returns defaultValue when portConstraint is null', () => {
const terminal = new CellState();
terminal.style = {};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NONE
);
expect(getPortConstraints(terminal, edge, true, DIRECTION_MASK.ALL)).toBe(
DIRECTION_MASK.ALL
);
});

test('uses terminal.style.portConstraint over edge style constraints', () => {
const terminal = new CellState();
terminal.style = { portConstraint: DIRECTION.NORTH };
const edge = new CellState();
edge.style = {
sourcePortConstraint: DIRECTION.SOUTH,
targetPortConstraint: DIRECTION.EAST,
};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH
);
});

test('falls back to edge.style.sourcePortConstraint when terminal has no constraint and source is true', () => {
const terminal = new CellState();
terminal.style = {};
const edge = new CellState();
edge.style = { sourcePortConstraint: 'north' };

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH
);
});

test('falls back to edge.style.targetPortConstraint when terminal has no constraint and source is false', () => {
const terminal = new CellState();
terminal.style = {};
const edge = new CellState();
edge.style = { targetPortConstraint: DIRECTION.SOUTH };

expect(getPortConstraints(terminal, edge, false, defaultMask)).toBe(
DIRECTION_MASK.SOUTH
);
});

test.each([
[DIRECTION.NORTH, DIRECTION_MASK.NORTH],
[DIRECTION.SOUTH, DIRECTION_MASK.SOUTH],
[DIRECTION.EAST, DIRECTION_MASK.EAST],
[DIRECTION.WEST, DIRECTION_MASK.WEST],
])('handles single direction %s', (direction: DirectionValue, expectedMask: number) => {
const terminal = new CellState();
terminal.style = { portConstraint: direction };
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(expectedMask);
});

test('handles array of port constraints (north and south)', () => {
const terminal = new CellState();
terminal.style = { portConstraint: [DIRECTION.NORTH, 'south'] };
const edge = new CellState();
edge.style = {};

// This should return a mask with both NORTH and SOUTH bits set
expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH | DIRECTION_MASK.SOUTH
);
});

test('handles array of port constraints in edge.style.sourcePortConstraint', () => {
const terminal = new CellState();
terminal.style = {}; // No port constraint on terminal
const edge = new CellState();
edge.style = {
sourcePortConstraint: ['north', DIRECTION.SOUTH],
};

// When terminal has no constraint, should use the edge's sourcePortConstraint array
expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH | DIRECTION_MASK.SOUTH
);
});

test('handles array of port constraints in edge.style.targetPortConstraint', () => {
const terminal = new CellState();
terminal.style = {}; // No port constraint on terminal
const edge = new CellState();
edge.style = {
targetPortConstraint: [DIRECTION.EAST, 'west'],
};

// When terminal has no constraint, should use the edge's targetPortConstraint array
expect(getPortConstraints(terminal, edge, false, defaultMask)).toBe(
DIRECTION_MASK.EAST | DIRECTION_MASK.WEST
);
});

test('terminal portConstraint array takes precedence over edge port constraints', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: [DIRECTION.NORTH, DIRECTION.WEST],
};
const edge = new CellState();
edge.style = {
sourcePortConstraint: DIRECTION.SOUTH,
targetPortConstraint: DIRECTION.EAST,
};

// Terminal's portConstraint should take precedence over edge's sourcePortConstraint
expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH | DIRECTION_MASK.WEST
);
});

test('handles rotated constraints when portConstraintRotation is true', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: DIRECTION.NORTH,
portConstraintRotation: true,
rotation: 90,
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.EAST
);
});

test('handles constraints with rotation defaulting to 0', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: DIRECTION.NORTH,
portConstraintRotation: true,
// rotation not set, should default to 0
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH
);
});

test('ignores rotation when portConstraintRotation is false', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: DIRECTION.NORTH,
portConstraintRotation: false,
rotation: 90,
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH
);
});

test('handles 180 degree rotation', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: DIRECTION.NORTH,
portConstraintRotation: true,
rotation: 180,
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.SOUTH
);
});

test('handles negative rotation (-90 degrees)', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: 'north',
portConstraintRotation: true,
rotation: -90,
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.WEST
);
});

test('handles extreme negative rotation (-135 degrees)', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: 'north',
portConstraintRotation: true,
rotation: -135,
};
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.SOUTH
);
});

test('handles very extreme negative rotation (-180 degrees)', () => {
const terminal = new CellState();
terminal.style = {
portConstraint: 'north',
portConstraintRotation: true,
rotation: -180,
};
const edge = new CellState();
edge.style = {};

// With rotation <= -135, quad = 2, so a NORTH constraint becomes SOUTH
expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.SOUTH
);
});

describe('legacy mxGraph, to support backward compatibility when loading mxGraph XML models', () => {
test('handles combined directions NORTH and SOUTH', () => {
const terminal = new CellState();
// @ts-ignore mxGraph set 'northsouth' as a string
terminal.style = { portConstraint: DIRECTION.NORTH + DIRECTION.SOUTH };
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.NORTH | DIRECTION_MASK.SOUTH
);
});

test('handles combined directions EAST and WEST', () => {
const terminal = new CellState();
// @ts-ignore mxGraph set 'eastwest' as a string
terminal.style = { portConstraint: DIRECTION.EAST + DIRECTION.WEST };
const edge = new CellState();
edge.style = {};

expect(getPortConstraints(terminal, edge, true, defaultMask)).toBe(
DIRECTION_MASK.EAST | DIRECTION_MASK.WEST
);
});
});
});

describe('isNumeric', () => {
test.each([null, undefined])('nullish value: %s', (value: null | undefined) => {
Expand Down
28 changes: 21 additions & 7 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,17 @@ export type CellStateStyle = {
*/
pointerEvents?: boolean;
/**
* Defines the direction(s) in which edges are allowed to connect to cells.
* For vertices only. Defines the direction(s) in which edges are allowed to connect to cells.
*
* If not set, use the {@link sourcePortConstraint} or the {@link targetPortConstraint} property of the related edges.
*/
portConstraint?: DIRECTION;
portConstraint?: StylePortConstraint;
/**
* Define if the directions of the port constraints are rotated with the vertex rotation.
* For vertices only. Defines if the directions of the port constraints are rotated with the vertex rotation.
* - `false` makes the port constraints remain absolute, relative to the graph.
* - `true` makes the constraints rotate with the vertex.
*
* @see portConstraint
* @default false
*/
portConstraintRotation?: boolean;
Expand Down Expand Up @@ -695,9 +699,11 @@ export type CellStateStyle = {
*/
sourcePort?: string;
/**
* Defines the direction(s) in which edges are allowed to connect to sources.
* For edges only. Defines the direction(s) in which edges are allowed to connect to sources.
*
* Used as fallback if no {@link portConstraint} is defined on the source vertex of the edge.
*/
sourcePortConstraint?: DIRECTION;
sourcePortConstraint?: StylePortConstraint;
/**
* The value represents the spacing, in pixels, added to each side of a label in a vertex.
*
Expand Down Expand Up @@ -825,9 +831,11 @@ export type CellStateStyle = {
*/
targetPort?: string;
/**
* Defines the direction(s) in which edges are allowed to connect to sources.
* For edges only. Defines the direction(s) in which edges are allowed to connect to sources.
*
* Used as fallback if no {@link portConstraint} is defined on the target vertex of the edge.
*/
targetPortConstraint?: DIRECTION;
targetPortConstraint?: StylePortConstraint;
/**
* @default {@link DEFAULT_TEXT_DIRECTION}
*/
Expand Down Expand Up @@ -928,6 +936,12 @@ export type ArrowValue =
*/
export type StyleArrowValue = ArrowValue | (string & {});

/**
* @category Style
* @since 0.17.0
*/
export type StylePortConstraint = DirectionValue | DirectionValue[];

/**
* Names used to register the shapes provided out-of-the-box by maxGraph with {@link CellRenderer.registerShape}.
* @category Style
Expand Down
Loading