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',
|
||||
ORTHOGONAL = 'orthogonalEdgeStyle',
|
||||
SEGMENT = 'segmentEdgeStyle',
|
||||
MANHATTAN = 'manhattanEdgeStyle',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1459,7 +1459,7 @@ class EdgeStyle {
|
|||
if (
|
||||
currentIndex > 0 &&
|
||||
EdgeStyle.wayPoints1[currentIndex][currentOrientation] ===
|
||||
EdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]
|
||||
EdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]
|
||||
) {
|
||||
currentIndex--;
|
||||
} 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(
|
||||
dir: number[],
|
||||
quad: number,
|
||||
|
@ -1545,3 +2071,4 @@ class EdgeStyle {
|
|||
}
|
||||
|
||||
export default EdgeStyle;
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ StyleRegistry.putValue(EDGESTYLE.SIDETOSIDE, EdgeStyle.SideToSide);
|
|||
StyleRegistry.putValue(EDGESTYLE.TOPTOBOTTOM, EdgeStyle.TopToBottom);
|
||||
StyleRegistry.putValue(EDGESTYLE.ORTHOGONAL, EdgeStyle.OrthConnector);
|
||||
StyleRegistry.putValue(EDGESTYLE.SEGMENT, EdgeStyle.SegmentConnector);
|
||||
StyleRegistry.putValue(EDGESTYLE.MANHATTAN, EdgeStyle.ManhattanConnector);
|
||||
|
||||
StyleRegistry.putValue(PERIMETER.ELLIPSE, Perimeter.EllipsePerimeter);
|
||||
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