feat: add Manhattan connector EdgeStyle and demo (#232)
Add Manhattan connector edge style and demo for it in Storybook. Code is based on https://github.com/mwangm/mxgraph-manhattan-connector Ported code to Typescript, refactored it and placed to `EdgeStyle.ts`.development
parent
5bae81a1e3
commit
e3b61cdc21
|
@ -598,6 +598,7 @@ export const enum EDGESTYLE {
|
||||||
TOPTOBOTTOM = 'topToBottomEdgeStyle',
|
TOPTOBOTTOM = 'topToBottomEdgeStyle',
|
||||||
ORTHOGONAL = 'orthogonalEdgeStyle',
|
ORTHOGONAL = 'orthogonalEdgeStyle',
|
||||||
SEGMENT = 'segmentEdgeStyle',
|
SEGMENT = 'segmentEdgeStyle',
|
||||||
|
MANHATTAN = 'manhattanEdgeStyle',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1459,7 +1459,7 @@ class EdgeStyle {
|
||||||
if (
|
if (
|
||||||
currentIndex > 0 &&
|
currentIndex > 0 &&
|
||||||
EdgeStyle.wayPoints1[currentIndex][currentOrientation] ===
|
EdgeStyle.wayPoints1[currentIndex][currentOrientation] ===
|
||||||
EdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]
|
EdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]
|
||||||
) {
|
) {
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1513,6 +1513,532 @@ 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
|
||||||
|
*
|
||||||
|
* Implements router to find shortest route that avoids cells using
|
||||||
|
* manhattan distance as metric.
|
||||||
|
*/
|
||||||
|
static ManhattanConnector(
|
||||||
|
state: CellState,
|
||||||
|
source: CellState,
|
||||||
|
target: CellState,
|
||||||
|
points: Point[],
|
||||||
|
result: Point[]
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Adds all values from source geometry to target.
|
||||||
|
* Used to create padding box around cell geometry.
|
||||||
|
* @param target
|
||||||
|
* @param source
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function moveAndExpand(target: Rectangle, source: Rectangle): Rectangle {
|
||||||
|
target.x += source.x || 0;
|
||||||
|
target.y += source.y || 0;
|
||||||
|
target.width += source.width || 0;
|
||||||
|
target.height += source.height || 0;
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
function snapCoordinateToGrid(value: number, gridSize: number) {
|
||||||
|
return gridSize * Math.round(value / gridSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
function snapPointToGrid(p: Point, gx: number, gy?: number) {
|
||||||
|
p.x = snapCoordinateToGrid(p.x, gx);
|
||||||
|
p.y = snapCoordinateToGrid(p.y, gy || gx);
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPointInRectangle(rect: Rectangle, p: Point) {
|
||||||
|
return p.x >= rect.x && p.x <= rect.x + rect.width && p.y >= rect.y && p.y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRectangleCenter(rect: Rectangle): Point {
|
||||||
|
return new Point(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDifferencePoint(p1: Point, p2: Point): Point {
|
||||||
|
return new Point(p1.x - p2.x, p1.y - p2.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
function movePoint(p: Point, moveX?: number, moveY?: number): Point {
|
||||||
|
p.x += moveX || 0;
|
||||||
|
p.y += moveY || 0;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPointTheta(p1: Point, p2: Point) {
|
||||||
|
const p = p2.clone();
|
||||||
|
const y = -(p.y - p1.y);
|
||||||
|
const x = p.x - p1.x;
|
||||||
|
const PRECISION = 10;
|
||||||
|
const rad = (y.toFixed(PRECISION) == "0" && x.toFixed(PRECISION) == "0")
|
||||||
|
? 0
|
||||||
|
: Math.atan2(y, x);
|
||||||
|
return 180 * rad / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePoint(point: Point) {
|
||||||
|
return new Point(
|
||||||
|
point.x === 0 ? 0 : Math.abs(point.x) / point.x,
|
||||||
|
point.y === 0 ? 0 : Math.abs(point.y) / point.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManhattanDistance(p1: Point, p2: Point) {
|
||||||
|
return Math.abs(p2.x - p1.x) + Math.abs(p2.y - p1.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPointFromString(pointString: string) {
|
||||||
|
const xy = pointString.split(pointString.indexOf('@') === -1 ? ' ' : '@');
|
||||||
|
return new Point(parseInt(xy[0], 10), parseInt(xy[1], 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointToString(point: Point) {
|
||||||
|
return `${point.x}@${point.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellAbsoluteBounds(cellState: CellState) {
|
||||||
|
const graph = cellState.view.graph;
|
||||||
|
const cellBounds = graph.getCellBounds(cellState.cell, false, false)?.clone();
|
||||||
|
if (!cellBounds)
|
||||||
|
return undefined;
|
||||||
|
const view = graph.view;
|
||||||
|
const { scale, translate } = view;
|
||||||
|
const { x, y } = translate;
|
||||||
|
const round = (v: number) => Math.round(v * 10) / 10;
|
||||||
|
const res = new Rectangle(
|
||||||
|
round((cellBounds.x / scale) - x),
|
||||||
|
round((cellBounds.y / scale) - y),
|
||||||
|
round(cellBounds.width / scale),
|
||||||
|
round(cellBounds.height / scale),
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mStep = EdgeStyle.MANHATTAN_STEP;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
// Padding applied on the element bounding boxes
|
||||||
|
paddingBox: EdgeStyle.MANHATTAN_PADDING_BOX,
|
||||||
|
|
||||||
|
// An array of directions to find next points on the route
|
||||||
|
directions: [
|
||||||
|
{ offsetX: mStep, offsetY: 0, cost: mStep, angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(mStep, 0))) },
|
||||||
|
{ offsetX: 0, offsetY: mStep, cost: mStep, angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, mStep))) },
|
||||||
|
{ offsetX: -mStep, offsetY: 0, cost: mStep, angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(-mStep, 0))) },
|
||||||
|
{ offsetX: 0, offsetY: -mStep, cost: mStep, angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, -mStep))) }
|
||||||
|
],
|
||||||
|
|
||||||
|
directionMap: {
|
||||||
|
"east": { x: 1, y: 0 },
|
||||||
|
"south": { x: 0, y: 1 },
|
||||||
|
"west": { x: -1, y: 0 },
|
||||||
|
"north": { x: 0, y: -1 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// A penalty received for direction change
|
||||||
|
penaltiesGenerator: (angle: number) => {
|
||||||
|
if (angle == 45 || angle == 90 || angle == 180)
|
||||||
|
return EdgeStyle.MANHATTAN_STEP / 2;
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
// If a function is provided, it's used to route the link while dragging an end
|
||||||
|
// i.e. function(from, to, opts) { return []; }
|
||||||
|
draggingRoute: null,
|
||||||
|
|
||||||
|
previousDirAngle: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of obstacles
|
||||||
|
* Helper structure to identify whether a point lies in an obstacle.
|
||||||
|
*/
|
||||||
|
class ObstacleMap {
|
||||||
|
map: Map<string, Rectangle[]>;
|
||||||
|
options: typeof config;
|
||||||
|
// tells how to divide the paper when creating the elements map
|
||||||
|
mapGridSize: number;
|
||||||
|
constructor(opt: any) {
|
||||||
|
this.options = opt;
|
||||||
|
this.mapGridSize = 100;
|
||||||
|
this.map = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a map of all elements for quicker obstacle queries
|
||||||
|
// The svg is divided to cells, where each of them holds an information which
|
||||||
|
// elements belong to it. When we query whether a point is in an obstacle we don't need
|
||||||
|
// to go through all obstacles, we check only those in a particular cell.
|
||||||
|
build(source: CellState | null, target: CellState | null) {
|
||||||
|
const graph = source?.view.graph || target?.view.graph;
|
||||||
|
if (!graph)
|
||||||
|
return;
|
||||||
|
return Array.from(graph.getView().getCellStates())
|
||||||
|
.filter(s => s.cell && s.cell.isVertex() && !s.cell.isEdge())
|
||||||
|
.map(s => getCellAbsoluteBounds(s))
|
||||||
|
.map(bbox => bbox ? moveAndExpand(bbox, this.options.paddingBox) : null)
|
||||||
|
.forEach(bbox => {
|
||||||
|
if (!bbox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const origin = snapPointToGrid(new Point(bbox.x, bbox.y), this.mapGridSize);
|
||||||
|
const corner = snapPointToGrid(new Point(bbox.x + bbox.width, bbox.y + bbox.height), this.mapGridSize);
|
||||||
|
|
||||||
|
for (let x = origin.x; x <= corner.x; x += this.mapGridSize) {
|
||||||
|
for (let y = origin.y; y <= corner.y; y += this.mapGridSize) {
|
||||||
|
const gridKey = x + '@' + y;
|
||||||
|
|
||||||
|
const rectArr = this.map.get(gridKey) || [];
|
||||||
|
if (!this.map.has(gridKey))
|
||||||
|
this.map.set(gridKey, rectArr);
|
||||||
|
rectArr.push(bbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isPointAccessible(point: Point): boolean {
|
||||||
|
const mapKey = pointToString(snapPointToGrid(point.clone(), this.mapGridSize));
|
||||||
|
const obstacles = this.map.get(mapKey);
|
||||||
|
if (obstacles) {
|
||||||
|
return obstacles.every(obstacle => !isPointInRectangle(obstacle, point))
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortedSet {
|
||||||
|
items: string[];
|
||||||
|
|
||||||
|
hash: Map<string, { value: number; open: boolean }>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.items = [];
|
||||||
|
this.hash = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key: string, value: number) {
|
||||||
|
const hashItem = this.hash.get(key);
|
||||||
|
if (hashItem) {
|
||||||
|
hashItem.value = value;
|
||||||
|
this.items.splice(this.items.indexOf(key), 1);
|
||||||
|
} else {
|
||||||
|
this.hash.set(key, {
|
||||||
|
value,
|
||||||
|
open: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items.push(key);
|
||||||
|
this.items.sort((i1, i2) => {
|
||||||
|
const hashItem1 = this.hash.get(i1);
|
||||||
|
const hashItem2 = this.hash.get(i2);
|
||||||
|
if (!hashItem1 || !hashItem2)
|
||||||
|
return 0;
|
||||||
|
return hashItem1.value - hashItem2.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
remove(key: string) {
|
||||||
|
const hashItem = this.hash.get(key);
|
||||||
|
if (hashItem)
|
||||||
|
hashItem.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen(key: string) {
|
||||||
|
const hashItem = this.hash.get(key);
|
||||||
|
return hashItem && hashItem.open == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isClose(key: string) {
|
||||||
|
const hashItem = this.hash.get(key);
|
||||||
|
return hashItem && hashItem.open == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.items.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(): string | undefined {
|
||||||
|
const key = this.items.shift();
|
||||||
|
if (key)
|
||||||
|
this.remove(key);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconstructRoute(parents: { [key: string]: Point }, endPoint: Point, startCenter: Point, endCenter: Point) {
|
||||||
|
const route: Point[] = [];
|
||||||
|
let previousDirection = normalizePoint(getDifferencePoint(endCenter, endPoint));
|
||||||
|
let current = endPoint;
|
||||||
|
let parent;
|
||||||
|
|
||||||
|
while (parents[pointToString(current)]) {
|
||||||
|
parent = parents[pointToString(current)]
|
||||||
|
if (!parent)
|
||||||
|
continue;
|
||||||
|
const direction = normalizePoint(getDifferencePoint(current, parent));
|
||||||
|
|
||||||
|
// Add point in when direction change
|
||||||
|
if (!direction.equals(previousDirection)) {
|
||||||
|
route.unshift(current);
|
||||||
|
previousDirection = direction;
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDirection = normalizePoint(getDifferencePoint(current, startCenter));
|
||||||
|
if (!startDirection.equals(previousDirection)) {
|
||||||
|
route.unshift(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRectPoints(bbox: Rectangle, directionList: DIRECTION[], opt: typeof config): Point[] {
|
||||||
|
const step = EdgeStyle.MANHATTAN_STEP;
|
||||||
|
const center = getRectangleCenter(bbox);
|
||||||
|
const res: Point[] = [];
|
||||||
|
for (const direction of directionList) {
|
||||||
|
const directionPoint = opt.directionMap[direction];
|
||||||
|
|
||||||
|
const x = directionPoint.x * bbox.width / 2;
|
||||||
|
const y = directionPoint.y * bbox.height / 2;
|
||||||
|
|
||||||
|
const point = movePoint(center.clone(), x, y);
|
||||||
|
|
||||||
|
if (isPointInRectangle(bbox, point)) {
|
||||||
|
movePoint(point, directionPoint.x * step, directionPoint.y * step);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(snapPointToGrid(point, step));
|
||||||
|
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAngle(angle: number) {
|
||||||
|
return (angle % 360) + (angle < 0 ? 360 : 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDirectionAngle(start: Point, end: Point, directionLength: number) {
|
||||||
|
const q = 360 / directionLength;
|
||||||
|
return Math.floor(normalizeAngle(getPointTheta(start, end) + q / 2) / q) * q;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectionChange(angle1: number, angle2: number) {
|
||||||
|
const dirChange = Math.abs(angle1 - angle2);
|
||||||
|
return dirChange > 180 ? 360 - dirChange : dirChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateCost(from: Point, endPoints: Point[]) {
|
||||||
|
let min = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0, len = endPoints.length; i < len; i++) {
|
||||||
|
const cost = getManhattanDistance(from, endPoints[i]);
|
||||||
|
if (cost < min)
|
||||||
|
min = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignPointToCell(point: Point, edgeState: CellState, cellState: CellState, isSourceCell: boolean) {
|
||||||
|
const cellBounds = getCellAbsoluteBounds(cellState);
|
||||||
|
|
||||||
|
const y = isSourceCell
|
||||||
|
? edgeState.style.exitY
|
||||||
|
: edgeState.style.entryY;
|
||||||
|
const onlyHorizontalDirections = isSourceCell
|
||||||
|
? EdgeStyle.MANHATTAN_START_DIRECTIONS.every(d => d != DIRECTION.NORTH && d != DIRECTION.SOUTH)
|
||||||
|
: EdgeStyle.MANHATTAN_END_DIRECTIONS.every(d => d != DIRECTION.NORTH && d != DIRECTION.SOUTH)
|
||||||
|
|
||||||
|
if (y != undefined && onlyHorizontalDirections) {
|
||||||
|
const cellHeight = cellBounds?.height || 0;
|
||||||
|
point.y = cellBounds?.y != undefined
|
||||||
|
? cellBounds?.y + cellHeight * y
|
||||||
|
: point.y - cellHeight / 2 + cellHeight * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = isSourceCell
|
||||||
|
? edgeState.style.exitX
|
||||||
|
: edgeState.style.entryX;
|
||||||
|
const onlyVerticalDirections = isSourceCell
|
||||||
|
? EdgeStyle.MANHATTAN_START_DIRECTIONS.every(d => d != DIRECTION.WEST && d != DIRECTION.EAST)
|
||||||
|
: EdgeStyle.MANHATTAN_END_DIRECTIONS.every(d => d != DIRECTION.WEST && d != DIRECTION.EAST)
|
||||||
|
if (x != undefined && onlyVerticalDirections) {
|
||||||
|
const cellWidth = cellBounds?.width || 0;
|
||||||
|
point.x = cellBounds?.x != undefined
|
||||||
|
? cellBounds?.x + cellWidth * x
|
||||||
|
: point.x - cellWidth / 2 + cellWidth * (x || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRoute(start: Rectangle, end: Rectangle, obstacleMap: ObstacleMap, opt: typeof config) {
|
||||||
|
// Caculate start points and end points
|
||||||
|
const step = EdgeStyle.MANHATTAN_STEP;
|
||||||
|
const startPoints = getRectPoints(start, EdgeStyle.MANHATTAN_START_DIRECTIONS, opt)
|
||||||
|
.filter(p => obstacleMap.isPointAccessible(p));
|
||||||
|
|
||||||
|
const startCenter = snapPointToGrid(getRectangleCenter(start), step);
|
||||||
|
const endPoints = getRectPoints(end, EdgeStyle.MANHATTAN_END_DIRECTIONS, opt)
|
||||||
|
.filter(p => obstacleMap.isPointAccessible(p));
|
||||||
|
const endCenter = snapPointToGrid(getRectangleCenter(end), step);
|
||||||
|
if (startPoints.length > 0 && endPoints.length > 0) {
|
||||||
|
|
||||||
|
// The set of possible points to be evaluated, initially containing the start points.
|
||||||
|
const openSet = new SortedSet();
|
||||||
|
// Keeps predecessor of given element.
|
||||||
|
const parents: { [key: string]: Point } = {};
|
||||||
|
// Cost from start to a point along best known path.
|
||||||
|
const costs: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
startPoints.forEach(p => {
|
||||||
|
const key = pointToString(p);
|
||||||
|
openSet.add(key, estimateCost(p, endPoints));
|
||||||
|
costs[key] = 0;
|
||||||
|
});
|
||||||
|
let loopsRemain = EdgeStyle.MANHATTAN_MAXIMUM_LOOPS;
|
||||||
|
const endPointsKeys = endPoints.map(p => pointToString(p));
|
||||||
|
let currentDirectionAngle: number | undefined;
|
||||||
|
let previousDirectionAngle: number | undefined;
|
||||||
|
// Main route finding loop
|
||||||
|
while (!openSet.isEmpty() && loopsRemain > 0) {
|
||||||
|
const currentKey = openSet.pop();
|
||||||
|
if (currentKey == undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPoint = toPointFromString(currentKey);
|
||||||
|
const currentCost = costs[currentKey];
|
||||||
|
previousDirectionAngle = currentDirectionAngle;
|
||||||
|
currentDirectionAngle = parents[currentKey]
|
||||||
|
? getDirectionAngle(parents[currentKey], currentPoint, opt.directions.length)
|
||||||
|
: opt.previousDirAngle != 0 ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, opt.directions.length);
|
||||||
|
|
||||||
|
// if get the endpoint
|
||||||
|
if (endPointsKeys.indexOf(currentKey) >= 0) {
|
||||||
|
// stop route to enter the end point in opposite direction.
|
||||||
|
const directionChangedAngle = getDirectionChange(currentDirectionAngle, getDirectionAngle(currentPoint, endCenter, opt.directions.length));
|
||||||
|
if (currentPoint.equals(endCenter) || directionChangedAngle < 180) {
|
||||||
|
opt.previousDirAngle = currentDirectionAngle;
|
||||||
|
return reconstructRoute(parents, currentPoint, startCenter, endCenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go over all possible directions and find neighbors.
|
||||||
|
for (let i = 0; i < opt.directions.length; i++) {
|
||||||
|
const direction = opt.directions[i];
|
||||||
|
const directionChangedAngle = getDirectionChange(currentDirectionAngle, direction.angle);
|
||||||
|
if (previousDirectionAngle && directionChangedAngle > EdgeStyle.MANHATTAN_MAX_ALLOWED_DIRECTION_CHANGE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighborPoint = movePoint(currentPoint.clone(), direction.offsetX, direction.offsetY);
|
||||||
|
const neighborKey = pointToString(neighborPoint);
|
||||||
|
if (openSet.isClose(neighborKey) || !obstacleMap.isPointAccessible(neighborPoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const costFromStart = currentCost + direction.cost + opt.penaltiesGenerator(directionChangedAngle);
|
||||||
|
|
||||||
|
if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) {
|
||||||
|
// Neighbor point has not been processed yet or the cost of the path
|
||||||
|
// from start is lesser than previously calcluated.
|
||||||
|
parents[neighborKey] = currentPoint;
|
||||||
|
costs[neighborKey] = costFromStart;
|
||||||
|
openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loopsRemain--;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function router(
|
||||||
|
state: CellState,
|
||||||
|
source: CellState,
|
||||||
|
target: CellState,
|
||||||
|
points: Point[],
|
||||||
|
result: Point[],
|
||||||
|
opt: typeof config
|
||||||
|
) {
|
||||||
|
// If edge is dragged after calculation, points will be filled, so fallback to SegmentConnector
|
||||||
|
if ((points != null && points.length > 0) || source == null || target == null) {
|
||||||
|
EdgeStyle.SegmentConnector(state, source, target, points, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceBBox = getCellAbsoluteBounds(source);
|
||||||
|
sourceBBox = sourceBBox ? moveAndExpand(sourceBBox, opt.paddingBox) : undefined;
|
||||||
|
let targetBBox = getCellAbsoluteBounds(target);
|
||||||
|
targetBBox = targetBBox ? moveAndExpand(targetBBox, opt.paddingBox) : undefined;
|
||||||
|
const obstacleMap = new ObstacleMap(opt);
|
||||||
|
|
||||||
|
obstacleMap.build(source, target);
|
||||||
|
if (!sourceBBox || !targetBBox) {
|
||||||
|
// Fallback to OrthConnector
|
||||||
|
return EdgeStyle.OrthConnector(state, source, target, points, result);
|
||||||
|
}
|
||||||
|
const routePoints = findRoute(sourceBBox, targetBBox, obstacleMap, opt);
|
||||||
|
|
||||||
|
if (routePoints == null || routePoints.length == 0) {
|
||||||
|
// Fallback to OrthConnector
|
||||||
|
return EdgeStyle.OrthConnector(state, source, target, points, result);
|
||||||
|
}
|
||||||
|
if (state.style) {
|
||||||
|
if (state.visibleSourceState && routePoints.length > 0) {
|
||||||
|
// If there are at least one point, align it to source cell
|
||||||
|
alignPointToCell(routePoints[0], state, state.visibleSourceState, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.visibleTargetState && routePoints.length > 1) {
|
||||||
|
// If there are more than one point, align last point to target cell
|
||||||
|
alignPointToCell(routePoints[routePoints.length - 1], state, state.visibleTargetState, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaling and translating result points
|
||||||
|
const scale = state.view.scale;
|
||||||
|
routePoints.forEach(pt => result.push(new Point(
|
||||||
|
Math.round((pt.x + state.view.translate.x) * scale * 10) / 10,
|
||||||
|
Math.round((pt.y + state.view.translate.y) * scale * 10) / 10
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
router(state, source, target, points, result, config);
|
||||||
|
}
|
||||||
|
|
||||||
static getRoutePattern(
|
static getRoutePattern(
|
||||||
dir: number[],
|
dir: number[],
|
||||||
quad: number,
|
quad: number,
|
||||||
|
@ -1545,3 +2071,4 @@ class EdgeStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EdgeStyle;
|
export default EdgeStyle;
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ StyleRegistry.putValue(EDGESTYLE.SIDETOSIDE, EdgeStyle.SideToSide);
|
||||||
StyleRegistry.putValue(EDGESTYLE.TOPTOBOTTOM, EdgeStyle.TopToBottom);
|
StyleRegistry.putValue(EDGESTYLE.TOPTOBOTTOM, EdgeStyle.TopToBottom);
|
||||||
StyleRegistry.putValue(EDGESTYLE.ORTHOGONAL, EdgeStyle.OrthConnector);
|
StyleRegistry.putValue(EDGESTYLE.ORTHOGONAL, EdgeStyle.OrthConnector);
|
||||||
StyleRegistry.putValue(EDGESTYLE.SEGMENT, EdgeStyle.SegmentConnector);
|
StyleRegistry.putValue(EDGESTYLE.SEGMENT, EdgeStyle.SegmentConnector);
|
||||||
|
StyleRegistry.putValue(EDGESTYLE.MANHATTAN, EdgeStyle.ManhattanConnector);
|
||||||
|
|
||||||
StyleRegistry.putValue(PERIMETER.ELLIPSE, Perimeter.EllipsePerimeter);
|
StyleRegistry.putValue(PERIMETER.ELLIPSE, Perimeter.EllipsePerimeter);
|
||||||
StyleRegistry.putValue(PERIMETER.RECTANGLE, Perimeter.RectanglePerimeter);
|
StyleRegistry.putValue(PERIMETER.RECTANGLE, Perimeter.RectanglePerimeter);
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021-present The maxGraph project Contributors
|
||||||
|
Copyright (c) 2006-2020, JGraph Ltd
|
||||||
|
|
||||||
|
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 {
|
||||||
|
EdgeStyle,
|
||||||
|
Graph,
|
||||||
|
SelectionHandler,
|
||||||
|
InternalEvent
|
||||||
|
} from '@maxgraph/core';
|
||||||
|
|
||||||
|
import { globalTypes } from '../.storybook/preview';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Connections/Manhattan',
|
||||||
|
argTypes: {
|
||||||
|
...globalTypes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = ({ label, ...args }) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.position = 'relative';
|
||||||
|
container.style.overflow = 'hidden';
|
||||||
|
container.style.width = `${args.width}px`;
|
||||||
|
container.style.height = `${args.height}px`;
|
||||||
|
container.style.background = 'url(/images/grid.gif)';
|
||||||
|
container.style.cursor = 'default';
|
||||||
|
|
||||||
|
// Enables guides
|
||||||
|
SelectionHandler.prototype.guidesEnabled = true;
|
||||||
|
|
||||||
|
// Allow end of edge to come only from west
|
||||||
|
EdgeStyle.MANHATTAN_END_DIRECTIONS = ["west"];
|
||||||
|
|
||||||
|
// Creates the graph inside the given container
|
||||||
|
const graph = new Graph(container);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
// If detected GeometryChange event, call graph.view.refresh(), which will reroute edge
|
||||||
|
if (hasMoveEdits) {
|
||||||
|
graph?.view?.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const parent = graph.getDefaultParent();
|
||||||
|
|
||||||
|
// Adds cells to the model in a single step
|
||||||
|
graph.batchUpdate(() => {
|
||||||
|
var style = graph.getStylesheet().getDefaultEdgeStyle();
|
||||||
|
style.labelBackgroundColor = '#FFFFFF';
|
||||||
|
style.strokeWidth = 2;
|
||||||
|
style.rounded = true;
|
||||||
|
style.entryPerimeter = true;
|
||||||
|
style.entryY = .25;
|
||||||
|
style.entryX = 0;
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
Loading…
Reference in New Issue