mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2025-08-26 07:51:10 +08:00
break it up further into canvas
This commit is contained in:
4
packages/react-diagrams-routing/.npmignore
Normal file
4
packages/react-diagrams-routing/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
3
packages/react-diagrams-routing/README.md
Normal file
3
packages/react-diagrams-routing/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Project STORM > React Diagrams > Dagre
|
||||
|
||||
This package adds dagre integration for laying out nodes and links
|
6
packages/react-diagrams-routing/index.ts
Normal file
6
packages/react-diagrams-routing/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './src/link/PathFindingLinkFactory';
|
||||
export * from './src/link/PathFindingLinkModel';
|
||||
export * from './src/link/PathFindingLinkWidget';
|
||||
|
||||
export * from './src/engine/PathFinding';
|
||||
export * from './src/dagre/DagreEngine';
|
8
packages/react-diagrams-routing/jest.config.js
Normal file
8
packages/react-diagrams-routing/jest.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
},
|
||||
roots: [path.join(__dirname, 'tests')],
|
||||
testMatch: ['**/*.test.{ts,tsx}']
|
||||
};
|
44
packages/react-diagrams-routing/package.json
Normal file
44
packages/react-diagrams-routing/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@projectstorm/react-diagrams-routing",
|
||||
"version": "6.0.0-alpha.4.2",
|
||||
"author": "dylanvorster",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "../../node_modules/.bin/webpack",
|
||||
"build:prod": "NODE_ENV=production ../../node_modules/.bin/webpack",
|
||||
"test": "../../node_modules/.bin/jest --no-cache"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"web",
|
||||
"diagram",
|
||||
"diagrams",
|
||||
"react",
|
||||
"typescript",
|
||||
"flowchart",
|
||||
"simple",
|
||||
"links",
|
||||
"nodes"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/@types/index",
|
||||
"dependencies": {
|
||||
"@projectstorm/react-diagrams-core": "^6.0.0-alpha.4.2",
|
||||
"@projectstorm/react-diagrams-defaults": "^6.0.0-alpha.4.2",
|
||||
"@projectstorm/geometry": "^6.0.0-alpha.4.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"dagre": "^0.8.4",
|
||||
"lodash": "4.*",
|
||||
"pathfinding": "^0.4.18",
|
||||
"paths-js": "^0.4.9",
|
||||
"react": "16.*"
|
||||
},
|
||||
"gitHead": "bb878657ba0c2f81764f32901fd96158a0f8352e"
|
||||
}
|
79
packages/react-diagrams-routing/src/dagre/DagreEngine.ts
Normal file
79
packages/react-diagrams-routing/src/dagre/DagreEngine.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { DiagramModel, PointModel } from '@projectstorm/react-diagrams-core';
|
||||
import * as dagre from 'dagre';
|
||||
import * as _ from 'lodash';
|
||||
import { GraphLabel } from 'dagre';
|
||||
import { Point } from '@projectstorm/geometry';
|
||||
|
||||
export interface DagreEngineOptions {
|
||||
graph?: GraphLabel;
|
||||
/**
|
||||
* Will also layout links
|
||||
*/
|
||||
includeLinks?: boolean;
|
||||
}
|
||||
|
||||
export class DagreEngine {
|
||||
options: DagreEngineOptions;
|
||||
|
||||
constructor(options: DagreEngineOptions = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
redistribute(model: DiagramModel) {
|
||||
// Create a new directed graph
|
||||
var g = new dagre.graphlib.Graph({
|
||||
multigraph: true
|
||||
});
|
||||
g.setGraph(this.options.graph || {});
|
||||
g.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
const processedlinks: { [id: string]: boolean } = {};
|
||||
|
||||
// set nodes
|
||||
_.forEach(model.getNodes(), node => {
|
||||
g.setNode(node.getID(), { width: node.width, height: node.height });
|
||||
});
|
||||
|
||||
_.forEach(model.getLinks(), link => {
|
||||
// set edges
|
||||
if (link.getSourcePort() && link.getTargetPort()) {
|
||||
processedlinks[link.getID()] = true;
|
||||
g.setEdge({
|
||||
v: link
|
||||
.getSourcePort()
|
||||
.getNode()
|
||||
.getID(),
|
||||
w: link
|
||||
.getTargetPort()
|
||||
.getNode()
|
||||
.getID(),
|
||||
name: link.getID()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// layout the graph
|
||||
dagre.layout(g);
|
||||
|
||||
g.nodes().forEach(v => {
|
||||
const node = g.node(v);
|
||||
model.getNode(v).setPosition(node.x, node.y);
|
||||
});
|
||||
|
||||
// also include links?
|
||||
if (this.options.includeLinks) {
|
||||
g.edges().forEach(e => {
|
||||
const edge = g.edge(e);
|
||||
const link = model.getLink(e.name);
|
||||
|
||||
const points = [link.getFirstPoint()];
|
||||
for (let i = 1; i < edge.points.length - 2; i++) {
|
||||
points.push(new PointModel({ link: link, position: new Point(edge.points[i].x, edge.points[i].y) }));
|
||||
}
|
||||
link.setPoints(points.concat(link.getLastPoint()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
124
packages/react-diagrams-routing/src/engine/PathFinding.ts
Normal file
124
packages/react-diagrams-routing/src/engine/PathFinding.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import * as PF from 'pathfinding';
|
||||
import { PathFindingLinkFactory } from '../link/PathFindingLinkFactory';
|
||||
import { PointModel } from '@projectstorm/react-diagrams-core';
|
||||
|
||||
/*
|
||||
it can be very expensive to calculate routes when every single pixel on the canvas
|
||||
is individually represented. Using the factor below, we combine values in order
|
||||
to achieve the best trade-off between accuracy and performance.
|
||||
*/
|
||||
|
||||
const pathFinderInstance = new PF.JumpPointFinder({
|
||||
heuristic: PF.Heuristic.manhattan,
|
||||
diagonalMovement: PF.DiagonalMovement.Never
|
||||
});
|
||||
|
||||
export default class PathFinding {
|
||||
instance: any;
|
||||
factory: PathFindingLinkFactory;
|
||||
|
||||
constructor(factory: PathFindingLinkFactory) {
|
||||
this.instance = pathFinderInstance;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taking as argument a fully unblocked walking matrix, this method
|
||||
* finds a direct path from point A to B.
|
||||
*/
|
||||
calculateDirectPath(from: PointModel, to: PointModel): number[][] {
|
||||
const matrix = this.factory.getCanvasMatrix();
|
||||
const grid = new PF.Grid(matrix);
|
||||
|
||||
return pathFinderInstance.findPath(
|
||||
this.factory.translateRoutingX(Math.floor(from.getX() / this.factory.ROUTING_SCALING_FACTOR)),
|
||||
this.factory.translateRoutingY(Math.floor(from.getY() / this.factory.ROUTING_SCALING_FACTOR)),
|
||||
this.factory.translateRoutingX(Math.floor(to.getX() / this.factory.ROUTING_SCALING_FACTOR)),
|
||||
this.factory.translateRoutingY(Math.floor(to.getY() / this.factory.ROUTING_SCALING_FACTOR)),
|
||||
grid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Using @link{#calculateDirectPath}'s result as input, we here
|
||||
* determine the first walkable point found in the matrix that includes
|
||||
* blocked paths.
|
||||
*/
|
||||
calculateLinkStartEndCoords(
|
||||
matrix: number[][],
|
||||
path: number[][]
|
||||
): {
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
pathToStart: number[][];
|
||||
pathToEnd: number[][];
|
||||
} {
|
||||
const startIndex = path.findIndex(point => matrix[point[1]][point[0]] === 0);
|
||||
const endIndex =
|
||||
path.length -
|
||||
1 -
|
||||
path
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex(point => matrix[point[1]][point[0]] === 0);
|
||||
|
||||
// are we trying to create a path exclusively through blocked areas?
|
||||
// if so, let's fallback to the linear routing
|
||||
if (startIndex === -1 || endIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pathToStart = path.slice(0, startIndex);
|
||||
const pathToEnd = path.slice(endIndex);
|
||||
|
||||
return {
|
||||
start: {
|
||||
x: path[startIndex][0],
|
||||
y: path[startIndex][1]
|
||||
},
|
||||
end: {
|
||||
x: path[endIndex][0],
|
||||
y: path[endIndex][1]
|
||||
},
|
||||
pathToStart,
|
||||
pathToEnd
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts everything together: merges the paths from/to the centre of the ports,
|
||||
* with the path calculated around other elements.
|
||||
*/
|
||||
calculateDynamicPath(
|
||||
routingMatrix: number[][],
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
pathToStart: number[][],
|
||||
pathToEnd: number[][]
|
||||
) {
|
||||
// generate the path based on the matrix with obstacles
|
||||
const grid = new PF.Grid(routingMatrix);
|
||||
const dynamicPath = pathFinderInstance.findPath(start.x, start.y, end.x, end.y, grid);
|
||||
|
||||
// aggregate everything to have the calculated path ready for rendering
|
||||
const pathCoords = pathToStart
|
||||
.concat(dynamicPath, pathToEnd)
|
||||
.map(coords => [
|
||||
this.factory.translateRoutingX(coords[0], true),
|
||||
this.factory.translateRoutingY(coords[1], true)
|
||||
]);
|
||||
return PF.Util.compressPath(pathCoords);
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
DiagramEngine,
|
||||
} from '@projectstorm/react-diagrams-core';
|
||||
import { PathFindingLinkModel } from './PathFindingLinkModel';
|
||||
import { PathFindingLinkWidget } from './PathFindingLinkWidget';
|
||||
import * as _ from 'lodash';
|
||||
import * as Path from 'paths-js/path';
|
||||
import { DefaultLinkFactory } from '@projectstorm/react-diagrams-defaults';
|
||||
import {AbstractFactory, FactoryBank, ListenerHandle} from "@projectstorm/react-canvas-core";
|
||||
|
||||
export class PathFindingLinkFactory extends DefaultLinkFactory<PathFindingLinkModel> {
|
||||
ROUTING_SCALING_FACTOR: number = 5;
|
||||
|
||||
// calculated only when smart routing is active
|
||||
canvasMatrix: number[][] = [];
|
||||
routingMatrix: number[][] = [];
|
||||
|
||||
// used when at least one element has negative coordinates
|
||||
hAdjustmentFactor: number = 0;
|
||||
vAdjustmentFactor: number = 0;
|
||||
|
||||
static NAME = 'pathfinding';
|
||||
listener: ListenerHandle;
|
||||
|
||||
constructor() {
|
||||
super(PathFindingLinkFactory.NAME);
|
||||
}
|
||||
|
||||
setDiagramEngine(engine: DiagramEngine): void {
|
||||
super.setDiagramEngine(engine);
|
||||
this.listener = engine.registerListener({
|
||||
canvasReady: () => {
|
||||
_.defer(() => {
|
||||
this.calculateRoutingMatrix();
|
||||
engine.repaintCanvas();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFactoryBank(bank: FactoryBank<AbstractFactory>): void {
|
||||
super.setFactoryBank(bank);
|
||||
if (!bank && this.listener) {
|
||||
this.listener.deregister();
|
||||
}
|
||||
}
|
||||
|
||||
generateReactWidget(event): JSX.Element {
|
||||
return <PathFindingLinkWidget diagramEngine={this.engine} link={event.model} factory={this} />;
|
||||
}
|
||||
|
||||
generateModel(event): PathFindingLinkModel {
|
||||
return new PathFindingLinkModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the canvas in the following format:
|
||||
*
|
||||
* +-----------------+
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* +-----------------+
|
||||
*
|
||||
* In which all walkable points are marked by zeros.
|
||||
* It uses @link{#ROUTING_SCALING_FACTOR} to reduce the matrix dimensions and improve performance.
|
||||
*/
|
||||
getCanvasMatrix(): number[][] {
|
||||
if (this.canvasMatrix.length === 0) {
|
||||
this.calculateCanvasMatrix();
|
||||
}
|
||||
|
||||
return this.canvasMatrix;
|
||||
}
|
||||
calculateCanvasMatrix() {
|
||||
const {
|
||||
width: canvasWidth,
|
||||
hAdjustmentFactor,
|
||||
height: canvasHeight,
|
||||
vAdjustmentFactor
|
||||
} = this.calculateMatrixDimensions();
|
||||
|
||||
this.hAdjustmentFactor = hAdjustmentFactor;
|
||||
this.vAdjustmentFactor = vAdjustmentFactor;
|
||||
|
||||
const matrixWidth = Math.ceil(canvasWidth / this.ROUTING_SCALING_FACTOR);
|
||||
const matrixHeight = Math.ceil(canvasHeight / this.ROUTING_SCALING_FACTOR);
|
||||
|
||||
this.canvasMatrix = _.range(0, matrixHeight).map(() => {
|
||||
return new Array(matrixWidth).fill(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the canvas in the following format:
|
||||
*
|
||||
* +-----------------+
|
||||
* | 0 0 1 1 0 0 0 0 |
|
||||
* | 0 0 1 1 0 0 1 1 |
|
||||
* | 0 0 0 0 0 0 1 1 |
|
||||
* | 1 1 0 0 0 0 0 0 |
|
||||
* | 1 1 0 0 0 0 0 0 |
|
||||
* +-----------------+
|
||||
*
|
||||
* In which all points blocked by a node (and its ports) are
|
||||
* marked as 1; points were there is nothing (ie, free) receive 0.
|
||||
*/
|
||||
getRoutingMatrix(): number[][] {
|
||||
if (this.routingMatrix.length === 0) {
|
||||
this.calculateRoutingMatrix();
|
||||
}
|
||||
|
||||
return this.routingMatrix;
|
||||
}
|
||||
calculateRoutingMatrix(): void {
|
||||
const matrix = _.cloneDeep(this.getCanvasMatrix());
|
||||
|
||||
// nodes need to be marked as blocked points
|
||||
this.markNodes(matrix);
|
||||
// same thing for ports
|
||||
this.markPorts(matrix);
|
||||
|
||||
this.routingMatrix = matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* The routing matrix does not have negative indexes, but elements could be negatively positioned.
|
||||
* We use the functions below to translate back and forth between these coordinates, relying on the
|
||||
* calculated values of hAdjustmentFactor and vAdjustmentFactor.
|
||||
*/
|
||||
translateRoutingX(x: number, reverse: boolean = false) {
|
||||
return x + this.hAdjustmentFactor * (reverse ? -1 : 1);
|
||||
}
|
||||
translateRoutingY(y: number, reverse: boolean = false) {
|
||||
return y + this.vAdjustmentFactor * (reverse ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Despite being a long method, we simply iterate over all three collections (nodes, ports and points)
|
||||
* to find the highest X and Y dimensions, so we can build the matrix large enough to contain all elements.
|
||||
*/
|
||||
calculateMatrixDimensions = (): {
|
||||
width: number;
|
||||
hAdjustmentFactor: number;
|
||||
height: number;
|
||||
vAdjustmentFactor: number;
|
||||
} => {
|
||||
const allNodesCoords = _.values(this.engine.getModel().getNodes()).map(item => ({
|
||||
x: item.getX(),
|
||||
width: item.width,
|
||||
y: item.getY(),
|
||||
height: item.height
|
||||
}));
|
||||
|
||||
const allLinks = _.values(this.engine.getModel().getLinks());
|
||||
const allPortsCoords = _.flatMap(allLinks.map(link => [link.getSourcePort(), link.getTargetPort()]))
|
||||
.filter(port => port !== null)
|
||||
.map(item => ({
|
||||
x: item.getX(),
|
||||
width: item.width,
|
||||
y: item.getY(),
|
||||
height: item.height
|
||||
}));
|
||||
const allPointsCoords = _.flatMap(allLinks.map(link => link.getPoints())).map(item => ({
|
||||
// points don't have width/height, so let's just use 0
|
||||
x: item.getX(),
|
||||
width: 0,
|
||||
y: item.getY(),
|
||||
height: 0
|
||||
}));
|
||||
|
||||
const canvas = this.engine.getCanvas() as HTMLDivElement;
|
||||
const minX =
|
||||
Math.floor(
|
||||
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.x).x, 0) /
|
||||
this.ROUTING_SCALING_FACTOR
|
||||
) * this.ROUTING_SCALING_FACTOR;
|
||||
const maxXElement = _.maxBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.x + item.width);
|
||||
const maxX = Math.max(maxXElement.x + maxXElement.width, canvas.offsetWidth);
|
||||
|
||||
const minY =
|
||||
Math.floor(
|
||||
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.y).y, 0) /
|
||||
this.ROUTING_SCALING_FACTOR
|
||||
) * this.ROUTING_SCALING_FACTOR;
|
||||
const maxYElement = _.maxBy(
|
||||
_.concat(allNodesCoords, allPortsCoords, allPointsCoords),
|
||||
item => item.y + item.height
|
||||
);
|
||||
const maxY = Math.max(maxYElement.y + maxYElement.height, canvas.offsetHeight);
|
||||
|
||||
return {
|
||||
width: Math.ceil(Math.abs(minX) + maxX),
|
||||
hAdjustmentFactor: Math.abs(minX) / this.ROUTING_SCALING_FACTOR + 1,
|
||||
height: Math.ceil(Math.abs(minY) + maxY),
|
||||
vAdjustmentFactor: Math.abs(minY) / this.ROUTING_SCALING_FACTOR + 1
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates (by reference) where nodes will be drawn on the matrix passed in.
|
||||
*/
|
||||
markNodes = (matrix: number[][]): void => {
|
||||
_.values(this.engine.getModel().getNodes()).forEach(node => {
|
||||
const startX = Math.floor(node.getX() / this.ROUTING_SCALING_FACTOR);
|
||||
const endX = Math.ceil((node.getX() + node.width) / this.ROUTING_SCALING_FACTOR);
|
||||
const startY = Math.floor(node.getY() / this.ROUTING_SCALING_FACTOR);
|
||||
const endY = Math.ceil((node.getY() + node.height) / this.ROUTING_SCALING_FACTOR);
|
||||
|
||||
for (let x = startX - 1; x <= endX + 1; x++) {
|
||||
for (let y = startY - 1; y < endY + 1; y++) {
|
||||
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates (by reference) where ports will be drawn on the matrix passed in.
|
||||
*/
|
||||
markPorts = (matrix: number[][]): void => {
|
||||
const allElements = _.flatMap(
|
||||
_.values(this.engine.getModel().getLinks()).map(link => [].concat(link.getSourcePort(), link.getTargetPort()))
|
||||
);
|
||||
allElements
|
||||
.filter(port => port !== null)
|
||||
.forEach(port => {
|
||||
const startX = Math.floor(port.x / this.ROUTING_SCALING_FACTOR);
|
||||
const endX = Math.ceil((port.x + port.width) / this.ROUTING_SCALING_FACTOR);
|
||||
const startY = Math.floor(port.y / this.ROUTING_SCALING_FACTOR);
|
||||
const endY = Math.ceil((port.y + port.height) / this.ROUTING_SCALING_FACTOR);
|
||||
|
||||
for (let x = startX - 1; x <= endX + 1; x++) {
|
||||
for (let y = startY - 1; y < endY + 1; y++) {
|
||||
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
markMatrixPoint = (matrix: number[][], x: number, y: number) => {
|
||||
if (matrix[y] !== undefined && matrix[y][x] !== undefined) {
|
||||
matrix[y][x] = 1;
|
||||
}
|
||||
};
|
||||
|
||||
generateDynamicPath(pathCoords: number[][]) {
|
||||
let path = Path();
|
||||
path = path.moveto(pathCoords[0][0] * this.ROUTING_SCALING_FACTOR, pathCoords[0][1] * this.ROUTING_SCALING_FACTOR);
|
||||
pathCoords.slice(1).forEach(coords => {
|
||||
path = path.lineto(coords[0] * this.ROUTING_SCALING_FACTOR, coords[1] * this.ROUTING_SCALING_FACTOR);
|
||||
});
|
||||
return path.print();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { PathFindingLinkFactory } from './PathFindingLinkFactory';
|
||||
import { DefaultLinkModel, DefaultLinkModelOptions } from '@projectstorm/react-diagrams-defaults';
|
||||
|
||||
export class PathFindingLinkModel extends DefaultLinkModel {
|
||||
constructor(options: DefaultLinkModelOptions = {}) {
|
||||
super({
|
||||
type: PathFindingLinkFactory.NAME,
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import { DiagramEngine } from '@projectstorm/react-diagrams-core';
|
||||
import PathFinding from '../engine/PathFinding';
|
||||
import { PathFindingLinkFactory } from './PathFindingLinkFactory';
|
||||
import { PathFindingLinkModel } from './PathFindingLinkModel';
|
||||
import { DefaultLinkSegmentWidget } from '@projectstorm/react-diagrams-defaults';
|
||||
|
||||
export interface PathFindingLinkWidgetProps {
|
||||
color?: string;
|
||||
width?: number;
|
||||
smooth?: boolean;
|
||||
link: PathFindingLinkModel;
|
||||
diagramEngine: DiagramEngine;
|
||||
factory: PathFindingLinkFactory;
|
||||
}
|
||||
|
||||
export interface PathFindingLinkWidgetState {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export class PathFindingLinkWidget extends React.Component<PathFindingLinkWidgetProps, PathFindingLinkWidgetState> {
|
||||
refPaths: React.RefObject<SVGPathElement>[];
|
||||
pathFinding: PathFinding;
|
||||
|
||||
constructor(props: PathFindingLinkWidgetProps) {
|
||||
super(props);
|
||||
this.refPaths = [];
|
||||
this.state = {
|
||||
selected: false
|
||||
};
|
||||
this.pathFinding = new PathFinding(this.props.factory);
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.props.link.setRenderedPaths(
|
||||
this.refPaths.map(ref => {
|
||||
return ref.current;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.link.setRenderedPaths(
|
||||
this.refPaths.map(ref => {
|
||||
return ref.current;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.link.setRenderedPaths([]);
|
||||
}
|
||||
|
||||
generateLink(path: string, id: string | number): JSX.Element {
|
||||
const ref = React.createRef<SVGPathElement>();
|
||||
this.refPaths.push(ref);
|
||||
return (
|
||||
<DefaultLinkSegmentWidget
|
||||
key={`link-${id}`}
|
||||
path={path}
|
||||
selected={this.state.selected}
|
||||
diagramEngine={this.props.diagramEngine}
|
||||
factory={this.props.diagramEngine.getFactoryForLink(this.props.link)}
|
||||
link={this.props.link}
|
||||
forwardRef={ref}
|
||||
onSelection={selected => {
|
||||
this.setState({ selected: selected });
|
||||
}}
|
||||
extras={{}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.refPaths = [];
|
||||
//ensure id is present for all points on the path
|
||||
var points = this.props.link.getPoints();
|
||||
var paths = [];
|
||||
|
||||
// first step: calculate a direct path between the points being linked
|
||||
const directPathCoords = this.pathFinding.calculateDirectPath(_.first(points), _.last(points));
|
||||
|
||||
const routingMatrix = this.props.factory.getRoutingMatrix();
|
||||
// now we need to extract, from the routing matrix, the very first walkable points
|
||||
// so they can be used as origin and destination of the link to be created
|
||||
const smartLink = this.pathFinding.calculateLinkStartEndCoords(routingMatrix, directPathCoords);
|
||||
if (smartLink) {
|
||||
const { start, end, pathToStart, pathToEnd } = smartLink;
|
||||
|
||||
// second step: calculate a path avoiding hitting other elements
|
||||
const simplifiedPath = this.pathFinding.calculateDynamicPath(routingMatrix, start, end, pathToStart, pathToEnd);
|
||||
|
||||
paths.push(
|
||||
//smooth: boolean, extraProps: any, id: string | number, firstPoint: PointModel, lastPoint: PointModel
|
||||
this.generateLink(this.props.factory.generateDynamicPath(simplifiedPath), '0')
|
||||
);
|
||||
}
|
||||
return <>{paths}</>;
|
||||
}
|
||||
}
|
41
packages/react-diagrams-routing/tests/PathFinding.test.tsx
Normal file
41
packages/react-diagrams-routing/tests/PathFinding.test.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import PathFinding from '../src/engine/PathFinding';
|
||||
|
||||
describe('calculating start and end points', () => {
|
||||
beforeEach(() => {
|
||||
this.pathFinding = new PathFinding(null);
|
||||
});
|
||||
|
||||
test('return correct object for valid walkable input', () => {
|
||||
const matrix = [
|
||||
[0, 0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 0, 1, 1],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[1, 1, 0, 0, 0, 0],
|
||||
[1, 1, 0, 0, 0, 0]
|
||||
];
|
||||
const path = [[0, 5], [1, 4], [2, 3], [3, 2], [4, 1], [5, 0]];
|
||||
|
||||
const result = this.pathFinding.calculateLinkStartEndCoords(matrix, path);
|
||||
|
||||
expect(result.start).toEqual({
|
||||
x: 2,
|
||||
y: 3
|
||||
});
|
||||
expect(result.end).toEqual({
|
||||
x: 3,
|
||||
y: 2
|
||||
});
|
||||
expect(result.pathToStart).toEqual([[0, 5], [1, 4]]);
|
||||
expect(result.pathToEnd).toEqual([[3, 2], [4, 1], [5, 0]]);
|
||||
});
|
||||
|
||||
test('undefined is returned when no walkable path exists', () => {
|
||||
const matrix = [[0, 0, 1, 1], [0, 0, 1, 1], [1, 1, 0, 0], [1, 1, 0, 0]];
|
||||
const path = [[0, 3], [1, 2], [2, 1], [3, 0]];
|
||||
|
||||
const result = this.pathFinding.calculateLinkStartEndCoords(matrix, path);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
13
packages/react-diagrams-routing/tsconfig.json
Normal file
13
packages/react-diagrams-routing/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/@types"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
}
|
8
packages/react-diagrams-routing/webpack.config.js
Normal file
8
packages/react-diagrams-routing/webpack.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
const config = require('../../webpack.shared')(__dirname);
|
||||
module.exports = {
|
||||
...config,
|
||||
output: {
|
||||
...config.output,
|
||||
library: 'projectstorm/react-diagrams-routing'
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user