mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2026-03-13 09:50:09 +08:00
Right angles routes added
This commit is contained in:
80
demos/demo-labview-routing/index.tsx
Normal file
80
demos/demo-labview-routing/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
DiagramEngine,
|
||||
DiagramModel,
|
||||
DefaultNodeModel,
|
||||
LinkModel,
|
||||
DefaultPortModel,
|
||||
DiagramWidget,
|
||||
LabViewLinkFactory, DefaultLinkModel,
|
||||
} from "storm-react-diagrams";
|
||||
import * as React from "react";
|
||||
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
|
||||
export class LabViewLinkModel extends DefaultLinkModel {
|
||||
constructor() {
|
||||
super("labview");
|
||||
}
|
||||
}
|
||||
|
||||
export class LabViewPortModel extends DefaultPortModel {
|
||||
createLinkModel(): LabViewLinkModel | null {
|
||||
return new LabViewLinkModel();
|
||||
}
|
||||
}
|
||||
|
||||
export default () => {
|
||||
// setup the diagram engine
|
||||
const engine = new DiagramEngine();
|
||||
engine.installDefaultFactories();
|
||||
engine.registerLinkFactory(new LabViewLinkFactory());
|
||||
|
||||
// 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 LabViewPortModel(false, "out-1", "Out"));
|
||||
node1.setPosition(340, 350);
|
||||
|
||||
const node2 = new DefaultNodeModel("Node B", "rgb(255,255,0)");
|
||||
const port2 = node2.addPort(new LabViewPortModel(false, "out-1", "Out"));
|
||||
node2.setPosition(240, 80);
|
||||
const node3 = new DefaultNodeModel("Node C", "rgb(192,255,255)");
|
||||
const port3 = node3.addPort(new LabViewPortModel(true, "in-1", "In"));
|
||||
node3.setPosition(540, 180);
|
||||
const node4 = new DefaultNodeModel("Node D", "rgb(192,0,255)");
|
||||
const port4 = node4.addPort(new LabViewPortModel(true, "in-1", "In"));
|
||||
node4.setPosition(95, 185);
|
||||
const node5 = new DefaultNodeModel("Node E", "rgb(192,255,0)");
|
||||
node5.setPosition(250, 180);
|
||||
|
||||
// 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, node5, link1, link2);
|
||||
|
||||
// load model into engine and render
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
return (
|
||||
<DemoWorkspaceWidget
|
||||
buttons={
|
||||
<button
|
||||
onClick={() => {
|
||||
action("Serialized Graph")(JSON.stringify(model.serializeDiagram(), null, 2));
|
||||
}}
|
||||
>
|
||||
Serialize Graph
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DiagramWidget
|
||||
className="srd-demo-canvas"
|
||||
diagramEngine={engine}
|
||||
/>
|
||||
</DemoWorkspaceWidget>
|
||||
);
|
||||
};
|
||||
36
src/labview/factories/LabViewLinkFactory.tsx
Normal file
36
src/labview/factories/LabViewLinkFactory.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { LabViewLinkWidget } from "../widgets/LabViewLinkWidget";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { AbstractLinkFactory } from "../../factories/AbstractLinkFactory";
|
||||
import { DefaultLinkModel } from "../../defaults/models/DefaultLinkModel";
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class LabViewLinkFactory extends AbstractLinkFactory<DefaultLinkModel> {
|
||||
constructor() {
|
||||
super("labview");
|
||||
}
|
||||
|
||||
generateReactWidget(diagramEngine: DiagramEngine, link: DefaultLinkModel): JSX.Element {
|
||||
return React.createElement(LabViewLinkWidget, {
|
||||
link: link,
|
||||
diagramEngine: diagramEngine
|
||||
});
|
||||
}
|
||||
|
||||
getNewInstance(initialConfig?: any): DefaultLinkModel {
|
||||
return new DefaultLinkModel();
|
||||
}
|
||||
|
||||
generateLinkSegment(model: DefaultLinkModel, widget: LabViewLinkWidget, selected: boolean, path: string) {
|
||||
return (
|
||||
<path
|
||||
className={selected ? widget.bem("--path-selected") : ""}
|
||||
strokeWidth={model.width}
|
||||
stroke={model.color}
|
||||
d={path}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
398
src/labview/widgets/LabViewLinkWidget.tsx
Normal file
398
src/labview/widgets/LabViewLinkWidget.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import * as React from "react";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { PointModel } from "../../models/PointModel";
|
||||
import { Toolkit } from "../../Toolkit";
|
||||
import { LabViewLinkFactory } from "../factories/LabViewLinkFactory";
|
||||
import { DefaultLinkModel } from "../../defaults/models/DefaultLinkModel";
|
||||
import PathFinding from "../../routing/PathFinding";
|
||||
import * as _ from "lodash";
|
||||
import { LabelModel } from "../../models/LabelModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
|
||||
|
||||
export interface LabViewLinkProps extends BaseWidgetProps {
|
||||
color?: string;
|
||||
width?: number;
|
||||
smooth?: boolean;
|
||||
link: DefaultLinkModel;
|
||||
diagramEngine: DiagramEngine;
|
||||
pointAdded?: (point: PointModel, event: MouseEvent) => any;
|
||||
}
|
||||
|
||||
export interface LabViewLinkState {
|
||||
selected: boolean,
|
||||
canDrag: boolean,
|
||||
}
|
||||
|
||||
export class LabViewLinkWidget extends BaseWidget<LabViewLinkProps, LabViewLinkState> {
|
||||
public static defaultProps: LabViewLinkProps = {
|
||||
color: "red",
|
||||
width: 3,
|
||||
link: null,
|
||||
engine: null,
|
||||
smooth: false,
|
||||
diagramEngine: null
|
||||
};
|
||||
|
||||
// DOM references to the label and paths (if label is given), used to calculate dynamic positioning
|
||||
refLabels: { [id: string]: HTMLElement };
|
||||
refPaths: SVGPathElement[];
|
||||
dragging_index: number;
|
||||
|
||||
pathFinding: PathFinding; // only set when smart routing is active
|
||||
|
||||
constructor(props: LabViewLinkProps) {
|
||||
super("srd-default-link", props);
|
||||
|
||||
this.refLabels = {};
|
||||
this.refPaths = [];
|
||||
this.state = {
|
||||
selected: false,
|
||||
canDrag: false,
|
||||
};
|
||||
|
||||
if (props.diagramEngine.isSmartRoutingEnabled()) {
|
||||
this.pathFinding = new PathFinding(this.props.diagramEngine);
|
||||
}
|
||||
|
||||
this.dragging_index = 0;
|
||||
}
|
||||
|
||||
calculateAllLabelPosition() {
|
||||
_.forEach(this.props.link.labels, (label, index) => {
|
||||
this.calculateLabelPosition(label, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.link.labels.length > 0) {
|
||||
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.link.labels.length > 0) {
|
||||
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
addPointToLink = (event: MouseEvent, index: number): void => {
|
||||
if (
|
||||
!event.shiftKey &&
|
||||
!this.props.diagramEngine.isModelLocked(this.props.link) &&
|
||||
this.props.link.points.length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink()
|
||||
) {
|
||||
const point = new PointModel(this.props.link, this.props.diagramEngine.getRelativeMousePoint(event));
|
||||
point.setSelected(true);
|
||||
this.forceUpdate();
|
||||
this.props.link.addPoint(point, index);
|
||||
this.props.pointAdded(point, event);
|
||||
}
|
||||
};
|
||||
|
||||
generatePoint(pointIndex: number): JSX.Element {
|
||||
let x = this.props.link.points[pointIndex].x;
|
||||
let y = this.props.link.points[pointIndex].y;
|
||||
|
||||
return (
|
||||
<g key={"point-" + this.props.link.points[pointIndex].id}>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={5}
|
||||
className={
|
||||
"point " +
|
||||
this.bem("__point") +
|
||||
(this.props.link.points[pointIndex].isSelected() ? this.bem("--point-selected") : "")
|
||||
}
|
||||
/>
|
||||
<circle
|
||||
onMouseLeave={() => {
|
||||
this.setState({ selected: false });
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
this.setState({ selected: true });
|
||||
}}
|
||||
data-id={this.props.link.points[pointIndex].id}
|
||||
data-linkid={this.props.link.id}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={15}
|
||||
opacity={0}
|
||||
className={"point " + this.bem("__point")}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
generateLabel(label: LabelModel) {
|
||||
const canvas = this.props.diagramEngine.canvas as HTMLElement;
|
||||
return (
|
||||
<foreignObject
|
||||
key={label.id}
|
||||
className={this.bem("__label")}
|
||||
width={canvas.offsetWidth}
|
||||
height={canvas.offsetHeight}
|
||||
>
|
||||
<div ref={ref => (this.refLabels[label.id] = ref)}>
|
||||
{this.props.diagramEngine
|
||||
.getFactoryForLabel(label)
|
||||
.generateReactWidget(this.props.diagramEngine, label)}
|
||||
</div>
|
||||
</foreignObject>
|
||||
);
|
||||
}
|
||||
|
||||
generateLink(path: string, extraProps: any, id: string | number): JSX.Element {
|
||||
var props = this.props;
|
||||
|
||||
var Bottom = React.cloneElement(
|
||||
(props.diagramEngine.getFactoryForLink(this.props.link) as LabViewLinkFactory).generateLinkSegment(
|
||||
this.props.link,
|
||||
this,
|
||||
this.state.selected || this.props.link.isSelected(),
|
||||
path
|
||||
),
|
||||
{
|
||||
ref: ref => ref && this.refPaths.push(ref)
|
||||
}
|
||||
);
|
||||
|
||||
var Top = React.cloneElement(Bottom, {
|
||||
...extraProps,
|
||||
strokeLinecap: "round",
|
||||
onMouseLeave: () => {
|
||||
this.setState({ selected: false });
|
||||
},
|
||||
onMouseEnter: () => {
|
||||
this.setState({ selected: true });
|
||||
},
|
||||
ref: null,
|
||||
"data-linkid": this.props.link.getID(),
|
||||
strokeOpacity: this.state.selected ? 0.1 : 0,
|
||||
strokeWidth: 20,
|
||||
onContextMenu: () => {
|
||||
if (!this.props.diagramEngine.isModelLocked(this.props.link)) {
|
||||
event.preventDefault();
|
||||
this.props.link.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<g key={"link-" + id}>
|
||||
{Bottom}
|
||||
{Top}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
findPathAndRelativePositionToRenderLabel = (index: number): { path: any; position: number } => {
|
||||
// an array to hold all path lengths, making sure we hit the DOM only once to fetch this information
|
||||
const lengths = this.refPaths.map(path => path.getTotalLength());
|
||||
|
||||
// calculate the point where we want to display the label
|
||||
let labelPosition =
|
||||
lengths.reduce((previousValue, currentValue) => previousValue + currentValue, 0) *
|
||||
(index / (this.props.link.labels.length + 1));
|
||||
|
||||
// find the path where the label will be rendered and calculate the relative position
|
||||
let pathIndex = 0;
|
||||
while (pathIndex < this.refPaths.length) {
|
||||
if (labelPosition - lengths[pathIndex] < 0) {
|
||||
return {
|
||||
path: this.refPaths[pathIndex],
|
||||
position: labelPosition
|
||||
};
|
||||
}
|
||||
|
||||
// keep searching
|
||||
labelPosition -= lengths[pathIndex];
|
||||
pathIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
calculateLabelPosition = (label, index: number) => {
|
||||
if (!this.refLabels[label.id]) {
|
||||
// no label? nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
const { path, position } = this.findPathAndRelativePositionToRenderLabel(index);
|
||||
|
||||
const labelDimensions = {
|
||||
width: this.refLabels[label.id].offsetWidth,
|
||||
height: this.refLabels[label.id].offsetHeight
|
||||
};
|
||||
|
||||
const pathCentre = path.getPointAtLength(position);
|
||||
|
||||
const labelCoordinates = {
|
||||
x: pathCentre.x - labelDimensions.width / 2 + label.offsetX,
|
||||
y: pathCentre.y - labelDimensions.height / 2 + label.offsetY
|
||||
};
|
||||
this.refLabels[label.id].setAttribute(
|
||||
"style",
|
||||
`transform: translate(${labelCoordinates.x}px, ${labelCoordinates.y}px);`
|
||||
);
|
||||
};
|
||||
|
||||
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(this.props.link, { x: points[index].x, y: points[index].y });
|
||||
this.props.link.addPoint(point, index);
|
||||
this.dragging_index++;
|
||||
return;
|
||||
} else if (index === points.length - 2) {
|
||||
let point = new PointModel(this.props.link, { x: points[index + 1].x,
|
||||
y: points[index + 1].y, });
|
||||
this.props.link.addPoint(point, index + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge two points if it is not close to node port
|
||||
if (index - 2 > 0) {
|
||||
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 - 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) {
|
||||
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 + 1].remove();
|
||||
points[index + 1].remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If no condition above handled then just update path points position
|
||||
points[index][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
|
||||
points[index + 1][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate];
|
||||
}
|
||||
|
||||
draggingEvent(event: MouseEvent, index: number) {
|
||||
let points = this.props.link.points;
|
||||
|
||||
// get moving difference. Index + 1 will work because links indexes has
|
||||
// length = points.lenght - 1
|
||||
let dx = Math.abs(points[index].x - points[index + 1].x);
|
||||
let dy = Math.abs(points[index].y - points[index + 1].y);
|
||||
|
||||
// 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() {
|
||||
const { diagramEngine } = this.props;
|
||||
if (!diagramEngine.nodesRendered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//ensure id is present for all points on the path
|
||||
let points = this.props.link.points;
|
||||
let paths = [];
|
||||
|
||||
// Get points based on link orientation
|
||||
let pointLeft = points[0];
|
||||
let pointRight = points[points.length - 1];
|
||||
if (pointLeft.x > pointRight.x) {
|
||||
pointLeft = points[points.length - 1];
|
||||
pointRight = points[0];
|
||||
}
|
||||
let dy = Math.abs(points[0].y - points[points.length - 1].y);
|
||||
|
||||
// When new link add one middle point to get everywhere 90° angle
|
||||
if (this.props.link.targetPort === null && points.length === 2) {
|
||||
this.props.link.addPoint(new PointModel(this.props.link, { x: pointLeft.x,
|
||||
y: pointRight.y, }), 1);
|
||||
|
||||
}
|
||||
// When new link is moving and not connected to target port move with middle point
|
||||
else if (this.props.link.targetPort === null) {
|
||||
points[1].x = pointLeft.x;
|
||||
points[1].y = pointRight.y;
|
||||
}
|
||||
// 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].x - points[i - 1].x);
|
||||
let dy = Math.abs(points[i].y - points[i - 1].y);
|
||||
if (dx !== 0 && dy !== 0) {
|
||||
if (dx < dy) {
|
||||
if (i - 1 === 0) { points[i].x = points[i - 1].x; }
|
||||
else if (i === points.length - 1) { points[i - 1].x = points[i].x; }
|
||||
} else {
|
||||
if (i - 1 === 0) {points[i].y = points[i - 1].y;}
|
||||
else if (i === points.length - 1) { points[i - 1].y = points[i].y; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(this.props.link, { x: pointLeft.x,
|
||||
y: pointRight.y, }), 1);
|
||||
}
|
||||
|
||||
for (let j = 0; j < points.length - 1; j++) {
|
||||
paths.push(
|
||||
this.generateLink(
|
||||
Toolkit.generateLinePath(points[j], points[j + 1]),
|
||||
{
|
||||
"data-linkid": this.props.link.id,
|
||||
"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 {...this.getProps()}>
|
||||
{paths}
|
||||
{_.map(this.props.link.labels, labelModel => {
|
||||
return this.generateLabel(labelModel);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user