Merge pull request #387 from DanieLazarLDAPPS/features/labview-style-of-routes

Features/labview style of routes
This commit is contained in:
Dylan Vorster
2019-08-13 00:07:26 +02:00
committed by GitHub
7 changed files with 390 additions and 3 deletions

View File

@@ -0,0 +1,68 @@
import createEngine, {
DiagramModel,
DefaultNodeModel,
DefaultPortModel,
RightAngleLinkFactory, LinkModel, RightAngleLinkModel,
} from '@projectstorm/react-diagrams';
import * as React from 'react';
import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget';
import { action } from '@storybook/addon-actions';
import {AbstractModelFactory, CanvasWidget} from '@projectstorm/react-canvas-core';
import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget';
// When new link is created by clicking on port the RightAngleLinkModel needs to be returned.
export class RightAnglePortModel extends DefaultPortModel {
createLinkModel(factory?: AbstractModelFactory<LinkModel>) {
return new RightAngleLinkModel();
}
}
export default () => {
// setup the diagram engine
const engine = createEngine();
engine.getLinkFactories().registerFactory(new RightAngleLinkFactory());
// setup the diagram model
const model = new DiagramModel();
// create four nodes in a way that straight links wouldn't work
const node1 = new DefaultNodeModel("Node A", "rgb(0,192,255)");
const port1 = node1.addPort(new RightAnglePortModel(false, "out-1", "Out"));
node1.setPosition(340, 350);
const node2 = new DefaultNodeModel("Node B", "rgb(255,255,0)");
const port2 = node2.addPort(new RightAnglePortModel(false, "out-1", "Out"));
node2.setPosition(240, 80);
const node3 = new DefaultNodeModel("Node C", "rgb(192,255,255)");
const port3 = node3.addPort(new RightAnglePortModel(true, "in-1", "In"));
node3.setPosition(540, 180);
const node4 = new DefaultNodeModel("Node D", "rgb(192,0,255)");
const port4 = node4.addPort(new RightAnglePortModel(true, "in-1", "In"));
node4.setPosition(95, 185);
// linking things together
const link1 = port1.link(port4);
const link2 = port2.link(port3);
// add all to the main model
model.addAll(node1, node2, node3, node4, link1, link2);
// load model into engine and render
engine.setModel(model);
return (
<DemoWorkspaceWidget
buttons={
<DemoButton
onClick={() => {
action('Serialized Graph')(JSON.stringify(model.serialize(), null, 2));
}}>
Serialize Graph
</DemoButton>
}>
<DemoCanvasWidget>
<CanvasWidget engine={engine} />
</DemoCanvasWidget>
</DemoWorkspaceWidget>
);
};

View File

@@ -50,13 +50,15 @@ import demo_adv_ser_des from './demos/demo-serializing';
import demo_adv_prog from './demos/demo-mutate-graph';
import demo_adv_dnd from './demos/demo-drag-and-drop';
import demo_smart_routing from './demos/demo-smart-routing';
import demo_right_angles_routing from './demos/demo-right-angles-routing';
storiesOf('Advanced Techniques', module)
.add('Clone Selected', demo_adv_clone_selected)
.add('Serializing and de-serializing', demo_adv_ser_des)
.add('Programatically modifying graph', demo_adv_prog)
.add('Drag and drop', demo_adv_dnd)
.add('Smart routing', demo_smart_routing);
.add('Smart routing', demo_smart_routing)
.add('Right angles routing', demo_right_angles_routing);
import demo_cust_nodes from './demos/demo-custom-node1';
import demo_cust_links from './demos/demo-custom-link1';

View File

@@ -1,6 +1,9 @@
export * from './src/link/PathFindingLinkFactory';
export * from './src/link/PathFindingLinkModel';
export * from './src/link/PathFindingLinkWidget';
export * from './src/link/RightAngleLinkWidget';
export * from './src/link/RightAngleLinkFactory';
export * from './src/link/RightAngleLinkModel';
export * from './src/engine/PathFinding';
export * from './src/dagre/DagreEngine';

View File

@@ -59,14 +59,22 @@ export default class PathFinding {
pathToStart: number[][];
pathToEnd: number[][];
} {
const startIndex = path.findIndex(point => matrix[point[1]][point[0]] === 0);
const startIndex = path.findIndex(point => {
if (matrix[point[1]])
return matrix[point[1]][point[0]] === 0;
else return false;
});
const endIndex =
path.length -
1 -
path
.slice()
.reverse()
.findIndex(point => matrix[point[1]][point[0]] === 0);
.findIndex(point => {
if (matrix[point[1]])
return matrix[point[1]][point[0]] === 0;
else return false;
});
// are we trying to create a path exclusively through blocked areas?
// if so, let's fallback to the linear routing

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { RightAngleLinkWidget } from "./RightAngleLinkWidget";
import { DiagramEngine } from '@projectstorm/react-diagrams-core';
import { DefaultLinkFactory, DefaultLinkModel } from '@projectstorm/react-diagrams-defaults';
import {RightAngleLinkModel} from "./RightAngleLinkModel";
/**
* @author Daniel Lazar
*/
export class RightAngleLinkFactory extends DefaultLinkFactory<RightAngleLinkModel> {
static NAME = 'rightAngle';
constructor() {
super(RightAngleLinkFactory.NAME);
}
generateModel(event): RightAngleLinkModel {
return new RightAngleLinkModel();
}
generateReactWidget(event): JSX.Element {
return <RightAngleLinkWidget diagramEngine={this.engine} link={event.model} factory={this}/>
}
}

View File

@@ -0,0 +1,15 @@
import { DefaultLinkModel, DefaultLinkModelOptions } from '@projectstorm/react-diagrams-defaults';
import {RightAngleLinkFactory} from "./RightAngleLinkFactory";
export class RightAngleLinkModel extends DefaultLinkModel {
constructor(options: DefaultLinkModelOptions = {}) {
super({
type: RightAngleLinkFactory.NAME,
...options
});
}
performanceTune() {
return false;
}
}

View File

@@ -0,0 +1,266 @@
import * as React from "react";
import {DiagramEngine, LinkWidget, PointModel} from '@projectstorm/react-diagrams-core';
import { RightAngleLinkFactory } from "./RightAngleLinkFactory";
import {DefaultLinkModel, DefaultLinkSegmentWidget} from "@projectstorm/react-diagrams-defaults";
import {Point} from "@projectstorm/geometry";
import {MouseEvent} from "react";
import {RightAngleLinkModel} from "./RightAngleLinkModel";
export interface RightAngleLinkProps {
color?: string;
width?: number;
smooth?: boolean;
link: RightAngleLinkModel;
diagramEngine: DiagramEngine;
factory: RightAngleLinkFactory;
}
export interface RightAngleLinkState {
selected: boolean,
canDrag: boolean,
}
export class RightAngleLinkWidget extends React.Component<RightAngleLinkProps, RightAngleLinkState> {
public static defaultProps: RightAngleLinkProps = {
color: "red",
width: 3,
link: null,
smooth: false,
diagramEngine: null,
factory: null,
};
refPaths: React.RefObject<SVGPathElement>[];
// DOM references to the label and paths (if label is given), used to calculate dynamic positioning
refLabels: { [id: string]: HTMLElement };
dragging_index: number;
constructor(props: RightAngleLinkProps) {
super(props);
this.refPaths = [];
this.state = {
selected: false,
canDrag: false,
};
this.dragging_index = 0;
}
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, extraProps: any, 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={extraProps}
/>
);
}
calculatePositions(points: PointModel[], event: MouseEvent, index: number, coordinate: string) {
// If path is first or last add another point to keep node port on its position
if (index === 0) {
let point = new PointModel({
link: this.props.link,
position: new Point(points[index].getX(), points[index].getY())
},);
this.props.link.addPoint(point, index);
this.dragging_index++;
return;
} else if (index === points.length - 2) {
let point = new PointModel({
link: this.props.link,
position: new Point(points[index + 1].getX(), points[index + 1].getY())
},);
this.props.link.addPoint(point, index + 1);
return;
}
// Merge two points if it is not close to node port and close to each other
if (index - 2 > 0) {
let _points = {
[index - 2]: points[index - 2].getPosition(),
[index + 1]: points[index + 1].getPosition(),
[index - 1]: points[index - 1].getPosition(),
};
if (Math.abs(_points[index - 1][coordinate] - _points[index + 1][coordinate]) < 5) {
_points[index - 2][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
_points[index + 1][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
points[index - 2].setPosition(_points[index - 2]);
points[index + 1].setPosition(_points[index + 1]);
points[index - 1].remove();
points[index - 1].remove();
this.dragging_index--;
this.dragging_index--;
return;
}
}
// Merge two points if it is not close to node port
if (index + 2 < points.length - 2) {
let _points = {
[index + 3]: points[index + 3].getPosition(),
[index + 2]: points[index + 2].getPosition(),
[index + 1]: points[index + 1].getPosition(),
[index]: points[index].getPosition(),
};
if (Math.abs(_points[index + 1][coordinate] - _points[index + 2][coordinate]) < 5) {
_points[index][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
_points[index + 3][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
points[index].setPosition(_points[index]);
points[index + 3].setPosition(_points[index + 3]);
points[index + 1].remove();
points[index + 1].remove();
return;
}
}
// If no condition above handled then just update path points position
let _points = {
[index]: points[index].getPosition(),
[index + 1]: points[index + 1].getPosition(),
};
_points[index][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
_points[index + 1][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
points[index].setPosition(_points[index]);
points[index + 1].setPosition(_points[index + 1]);
}
draggingEvent(event: MouseEvent, index: number) {
let points = this.props.link.getPoints();
// get moving difference. Index + 1 will work because links indexes has
// length = points.lenght - 1
let dx = Math.abs(points[index].getX() - points[index + 1].getX());
let dy = Math.abs(points[index].getY() - points[index + 1].getY());
// moving with y direction
if (dx === 0) {
this.calculatePositions(points, event, index, 'x');
} else if (dy === 0) {
this.calculatePositions(points, event, index, 'y');
}
}
handleMove = function (event: MouseEvent) {
this.draggingEvent(event, this.dragging_index);
}.bind(this);
handleUp = function (event: MouseEvent) {
// Unregister handlers to avoid multiple event handlers for other links
this.setState({ canDrag: false, selected: false });
window.removeEventListener('mousemove', this.handleMove);
window.removeEventListener('mouseup', this.handleUp);
}.bind(this);
render() {
//ensure id is present for all points on the path
let points = this.props.link.getPoints();
let paths = [];
// Get points based on link orientation
let pointLeft = points[0];
let pointRight = points[points.length - 1];
if (pointLeft.getX() > pointRight.getX()) {
pointLeft = points[points.length - 1];
pointRight = points[0];
}
let dy = Math.abs(points[0].getY() - points[points.length - 1].getY());
// When new link add one middle point to get everywhere 90° angle
if (this.props.link.getTargetPort() === null && points.length === 2) {
this.props.link.addPoint(new PointModel({
link: this.props.link,
position: new Point(pointLeft.getX(), pointRight.getY())
}), 1);
}
// When new link is moving and not connected to target port move with middle point
else if (this.props.link.getTargetPort() === null) {
points[1].setPosition(pointLeft.getX(), pointRight.getY());
}
// Render was called but link is not moved but user.
// Node is moved and in this case fix coordinates to get 90° angle.
// For loop just for first and last path
else if (!this.state.canDrag && points.length > 2) {
for (let i = 1; i < points.length; i+= points.length - 2) {
let dx = Math.abs(points[i].getX() - points[i - 1].getX());
let dy = Math.abs(points[i].getY() - points[i - 1].getY());
if (dx !== 0 || dy !== 0) {
if (dx < dy) {
if (i - 1 === 0) { points[i].setPosition(points[i - 1].getX(), points[i].getY()); }
else if (i === points.length - 1) { points[i - 1].setPosition(points[i].getX(), points[i - 1].getY());}
} else {
if (i - 1 === 0) { points[i].setPosition(points[i].getX(), points[i - 1].getY());}
else if (i === points.length - 1) { points[i - 1].setPosition(points[i - 1].getX(), points[i].getY());}
}
}
}
}
// If there is existing link which has two points add one
// NOTE: It doesn't matter if check is for dy or dx
if (points.length === 2 && dy !== 0 && !this.state.canDrag) {
this.props.link.addPoint(new PointModel({
link: this.props.link,
position: new Point(pointLeft.getX(), pointRight.getY())
}), 1);
}
for (let j = 0; j < points.length - 1; j++) {
paths.push(
this.generateLink(
LinkWidget.generateLinePath(points[j], points[j + 1]),
{
'data-linkid': this.props.link.getID(),
'data-point': j,
onMouseDown: (event: MouseEvent) => {
if (event.button === 0) {
this.setState({ canDrag: true });
this.dragging_index = j;
// Register mouse move event to track mouse position
// On mouse up these events are unregistered check "this.handleUp"
window.addEventListener('mousemove', this.handleMove);
window.addEventListener('mouseup', this.handleUp);
}
}
},
j
)
);
}
this.refPaths = [];
return <g data-default-link-test={this.props.link.getOptions().testName}>{paths}</g>;
}
}