mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2026-03-13 09:50:09 +08:00
Merge pull request #387 from DanieLazarLDAPPS/features/labview-style-of-routes
Features/labview style of routes
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}/>
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user