From 16033a658cf77d0493c54f928f5f75f44ffcb55c Mon Sep 17 00:00:00 2001 From: Daniel Lazar Date: Wed, 7 Aug 2019 08:52:58 +0200 Subject: [PATCH 1/7] issue-277 few null checks to avoid crash when node went under bottom of screen --- .../react-diagrams-routing/src/engine/PathFinding.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-diagrams-routing/src/engine/PathFinding.ts b/packages/react-diagrams-routing/src/engine/PathFinding.ts index 48665f0..dab5f47 100644 --- a/packages/react-diagrams-routing/src/engine/PathFinding.ts +++ b/packages/react-diagrams-routing/src/engine/PathFinding.ts @@ -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 From 68c2c76d72b016c8a798cd09cf389ba4b2454c96 Mon Sep 17 00:00:00 2001 From: Daniel Lazar Date: Wed, 7 Aug 2019 15:34:10 +0200 Subject: [PATCH 2/7] Right angles routes added --- demos/demo-labview-routing/index.tsx | 80 ++++ src/labview/factories/LabViewLinkFactory.tsx | 36 ++ src/labview/widgets/LabViewLinkWidget.tsx | 398 +++++++++++++++++++ 3 files changed, 514 insertions(+) create mode 100644 demos/demo-labview-routing/index.tsx create mode 100644 src/labview/factories/LabViewLinkFactory.tsx create mode 100644 src/labview/widgets/LabViewLinkWidget.tsx diff --git a/demos/demo-labview-routing/index.tsx b/demos/demo-labview-routing/index.tsx new file mode 100644 index 0000000..3d8e5fe --- /dev/null +++ b/demos/demo-labview-routing/index.tsx @@ -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 ( + { + action("Serialized Graph")(JSON.stringify(model.serializeDiagram(), null, 2)); + }} + > + Serialize Graph + + } + > + + + ); +}; diff --git a/src/labview/factories/LabViewLinkFactory.tsx b/src/labview/factories/LabViewLinkFactory.tsx new file mode 100644 index 0000000..9d3e338 --- /dev/null +++ b/src/labview/factories/LabViewLinkFactory.tsx @@ -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 { + 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 ( + + ); + } +} diff --git a/src/labview/widgets/LabViewLinkWidget.tsx b/src/labview/widgets/LabViewLinkWidget.tsx new file mode 100644 index 0000000..fcff54b --- /dev/null +++ b/src/labview/widgets/LabViewLinkWidget.tsx @@ -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 { + 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 ( + + + { + 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")} + /> + + ); + } + + generateLabel(label: LabelModel) { + const canvas = this.props.diagramEngine.canvas as HTMLElement; + return ( + +
(this.refLabels[label.id] = ref)}> + {this.props.diagramEngine + .getFactoryForLabel(label) + .generateReactWidget(this.props.diagramEngine, label)} +
+
+ ); + } + + 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 ( + + {Bottom} + {Top} + + ); + } + + 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 ( + + {paths} + {_.map(this.props.link.labels, labelModel => { + return this.generateLabel(labelModel); + })} + + ); + } +} From 16f6b484b58541e8b15ea9b62cb01ed074c8973a Mon Sep 17 00:00:00 2001 From: Daniel Lazar Date: Wed, 7 Aug 2019 15:43:14 +0200 Subject: [PATCH 3/7] renamed due to restriction with labview --- .../index.tsx | 6 ++-- src/labview/factories/LabViewLinkFactory.tsx | 36 ------------------- .../factories/RightAngleLinkFactory.tsx | 36 +++++++++++++++++++ .../widgets/RightAngleLinkWidget.tsx} | 20 +++++------ 4 files changed, 49 insertions(+), 49 deletions(-) rename demos/{demo-labview-routing => demo-right-angles-routing}/index.tsx (94%) delete mode 100644 src/labview/factories/LabViewLinkFactory.tsx create mode 100644 src/routing/rightAngle/factories/RightAngleLinkFactory.tsx rename src/{labview/widgets/LabViewLinkWidget.tsx => routing/rightAngle/widgets/RightAngleLinkWidget.tsx} (94%) diff --git a/demos/demo-labview-routing/index.tsx b/demos/demo-right-angles-routing/index.tsx similarity index 94% rename from demos/demo-labview-routing/index.tsx rename to demos/demo-right-angles-routing/index.tsx index 3d8e5fe..f92b230 100644 --- a/demos/demo-labview-routing/index.tsx +++ b/demos/demo-right-angles-routing/index.tsx @@ -5,7 +5,7 @@ import { LinkModel, DefaultPortModel, DiagramWidget, - LabViewLinkFactory, DefaultLinkModel, + RightAngleLinkFactory, DefaultLinkModel, } from "storm-react-diagrams"; import * as React from "react"; import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget"; @@ -13,7 +13,7 @@ import { action } from "@storybook/addon-actions"; export class LabViewLinkModel extends DefaultLinkModel { constructor() { - super("labview"); + super("rightAngle"); } } @@ -27,7 +27,7 @@ export default () => { // setup the diagram engine const engine = new DiagramEngine(); engine.installDefaultFactories(); - engine.registerLinkFactory(new LabViewLinkFactory()); + engine.registerLinkFactory(new RightAngleLinkFactory()); // setup the diagram model const model = new DiagramModel(); diff --git a/src/labview/factories/LabViewLinkFactory.tsx b/src/labview/factories/LabViewLinkFactory.tsx deleted file mode 100644 index 9d3e338..0000000 --- a/src/labview/factories/LabViewLinkFactory.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 { - 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 ( - - ); - } -} diff --git a/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx b/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx new file mode 100644 index 0000000..9ff94e3 --- /dev/null +++ b/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { RightAngleLinkWidget } from "../widgets/RightAngleLinkWidget"; +import { DiagramEngine } from "../../../DiagramEngine"; +import { AbstractLinkFactory } from "../../../factories/AbstractLinkFactory"; +import { DefaultLinkModel } from "../../../defaults/models/DefaultLinkModel"; + +/** + * @author Dylan Vorster + */ +export class RightAngleLinkFactory extends AbstractLinkFactory { + constructor() { + super("rightAngle"); + } + + generateReactWidget(diagramEngine: DiagramEngine, link: DefaultLinkModel): JSX.Element { + return React.createElement(RightAngleLinkWidget, { + link: link, + diagramEngine: diagramEngine + }); + } + + getNewInstance(initialConfig?: any): DefaultLinkModel { + return new DefaultLinkModel(); + } + + generateLinkSegment(model: DefaultLinkModel, widget: RightAngleLinkWidget, selected: boolean, path: string) { + return ( + + ); + } +} diff --git a/src/labview/widgets/LabViewLinkWidget.tsx b/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx similarity index 94% rename from src/labview/widgets/LabViewLinkWidget.tsx rename to src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx index fcff54b..2e9d4d8 100644 --- a/src/labview/widgets/LabViewLinkWidget.tsx +++ b/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx @@ -1,13 +1,13 @@ 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 { DiagramEngine } from "../../../DiagramEngine"; +import { PointModel } from "../../../models/PointModel"; +import { Toolkit } from "../../../Toolkit"; +import { RightAngleLinkFactory } from "../factories/RightAngleLinkFactory"; +import { DefaultLinkModel } from "../../../defaults/models/DefaultLinkModel"; +import PathFinding from "../../PathFinding"; import * as _ from "lodash"; -import { LabelModel } from "../../models/LabelModel"; -import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget"; +import { LabelModel } from "../../../models/LabelModel"; +import { BaseWidget, BaseWidgetProps } from "../../../widgets/BaseWidget"; export interface LabViewLinkProps extends BaseWidgetProps { color?: string; @@ -23,7 +23,7 @@ export interface LabViewLinkState { canDrag: boolean, } -export class LabViewLinkWidget extends BaseWidget { +export class RightAngleLinkWidget extends BaseWidget { public static defaultProps: LabViewLinkProps = { color: "red", width: 3, @@ -146,7 +146,7 @@ export class LabViewLinkWidget extends BaseWidget Date: Wed, 7 Aug 2019 17:07:28 +0200 Subject: [PATCH 4/7] remove node5 --- demos/demo-right-angles-routing/index.tsx | 4 +--- .../rightAngle/widgets/RightAngleLinkWidget.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/demos/demo-right-angles-routing/index.tsx b/demos/demo-right-angles-routing/index.tsx index f92b230..4429607 100644 --- a/demos/demo-right-angles-routing/index.tsx +++ b/demos/demo-right-angles-routing/index.tsx @@ -46,15 +46,13 @@ export default () => { 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); + model.addAll(node1, node2, node3, node4, link1, link2); // load model into engine and render engine.setDiagramModel(model); diff --git a/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx b/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx index 2e9d4d8..c0d8d7c 100644 --- a/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx +++ b/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx @@ -9,7 +9,7 @@ import * as _ from "lodash"; import { LabelModel } from "../../../models/LabelModel"; import { BaseWidget, BaseWidgetProps } from "../../../widgets/BaseWidget"; -export interface LabViewLinkProps extends BaseWidgetProps { +export interface RightAngleLinkProps extends BaseWidgetProps { color?: string; width?: number; smooth?: boolean; @@ -18,13 +18,13 @@ export interface LabViewLinkProps extends BaseWidgetProps { pointAdded?: (point: PointModel, event: MouseEvent) => any; } -export interface LabViewLinkState { +export interface RightAngleLinkState { selected: boolean, canDrag: boolean, } -export class RightAngleLinkWidget extends BaseWidget { - public static defaultProps: LabViewLinkProps = { +export class RightAngleLinkWidget extends BaseWidget { + public static defaultProps: RightAngleLinkProps = { color: "red", width: 3, link: null, @@ -40,7 +40,7 @@ export class RightAngleLinkWidget extends BaseWidget Date: Mon, 12 Aug 2019 11:59:50 +0200 Subject: [PATCH 5/7] migrate to new six version --- demos/demo-right-angles-routing/index.tsx | 78 ---- .../demos/demo-right-angles-routing/index.tsx | 63 +++ packages/diagrams-demo-gallery/index.tsx | 4 +- packages/react-diagrams-routing/index.ts | 3 + .../src/link/RightAngleLinkFactory.tsx | 25 ++ .../src/link/RightAngleLinkModel.ts | 15 + .../src/link/RightAngleLinkWidget.tsx | 266 ++++++++++++ .../factories/RightAngleLinkFactory.tsx | 36 -- .../widgets/RightAngleLinkWidget.tsx | 398 ------------------ 9 files changed, 375 insertions(+), 513 deletions(-) delete mode 100644 demos/demo-right-angles-routing/index.tsx create mode 100644 packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx create mode 100644 packages/react-diagrams-routing/src/link/RightAngleLinkFactory.tsx create mode 100644 packages/react-diagrams-routing/src/link/RightAngleLinkModel.ts create mode 100644 packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx delete mode 100644 src/routing/rightAngle/factories/RightAngleLinkFactory.tsx delete mode 100644 src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx diff --git a/demos/demo-right-angles-routing/index.tsx b/demos/demo-right-angles-routing/index.tsx deleted file mode 100644 index 4429607..0000000 --- a/demos/demo-right-angles-routing/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - DiagramEngine, - DiagramModel, - DefaultNodeModel, - LinkModel, - DefaultPortModel, - DiagramWidget, - RightAngleLinkFactory, 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("rightAngle"); - } -} - -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 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 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); - - // 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.setDiagramModel(model); - - return ( - { - action("Serialized Graph")(JSON.stringify(model.serializeDiagram(), null, 2)); - }} - > - Serialize Graph - - } - > - - - ); -}; diff --git a/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx b/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx new file mode 100644 index 0000000..814f1f8 --- /dev/null +++ b/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx @@ -0,0 +1,63 @@ +import createEngine, { + DiagramModel, + DefaultNodeModel, + DefaultPortModel, + RightAngleLinkFactory, +} from '@projectstorm/react-diagrams'; +import * as React from 'react'; +import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; +import { action } from '@storybook/addon-actions'; +import { CanvasWidget } from '@projectstorm/react-canvas-core'; +import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; + +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 DefaultPortModel(false, "out-1", "Out")); + node1.setPosition(340, 350); + + const node2 = new DefaultNodeModel("Node B", "rgb(255,255,0)"); + const port2 = node2.addPort(new DefaultPortModel(false, "out-1", "Out")); + node2.setPosition(240, 80); + const node3 = new DefaultNodeModel("Node C", "rgb(192,255,255)"); + const port3 = node3.addPort(new DefaultPortModel(true, "in-1", "In")); + node3.setPosition(540, 180); + const node4 = new DefaultNodeModel("Node D", "rgb(192,0,255)"); + const port4 = node4.addPort(new DefaultPortModel(true, "in-1", "In")); + node4.setPosition(95, 185); + + const rightAngleLink = engine.getLinkFactories().getFactory(RightAngleLinkFactory.NAME); + + // linking things together + const link1 = port1.link(port4, rightAngleLink); + const link2 = port2.link(port3, rightAngleLink); + + // add all to the main model + model.addAll(node1, node2, node3, node4, link1, link2); + + // load model into engine and render + engine.setModel(model); + + return ( + { + action('Serialized Graph')(JSON.stringify(model.serialize(), null, 2)); + }}> + Serialize Graph + + }> + + + + + ); +}; diff --git a/packages/diagrams-demo-gallery/index.tsx b/packages/diagrams-demo-gallery/index.tsx index 7ee255a..f12ea92 100644 --- a/packages/diagrams-demo-gallery/index.tsx +++ b/packages/diagrams-demo-gallery/index.tsx @@ -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'; diff --git a/packages/react-diagrams-routing/index.ts b/packages/react-diagrams-routing/index.ts index fbab7be..eac701c 100644 --- a/packages/react-diagrams-routing/index.ts +++ b/packages/react-diagrams-routing/index.ts @@ -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'; diff --git a/packages/react-diagrams-routing/src/link/RightAngleLinkFactory.tsx b/packages/react-diagrams-routing/src/link/RightAngleLinkFactory.tsx new file mode 100644 index 0000000..f653c6c --- /dev/null +++ b/packages/react-diagrams-routing/src/link/RightAngleLinkFactory.tsx @@ -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 { + + static NAME = 'rightAngle'; + + constructor() { + super(RightAngleLinkFactory.NAME); + } + + generateModel(event): RightAngleLinkModel { + return new RightAngleLinkModel(); + } + + generateReactWidget(event): JSX.Element { + return + } +} diff --git a/packages/react-diagrams-routing/src/link/RightAngleLinkModel.ts b/packages/react-diagrams-routing/src/link/RightAngleLinkModel.ts new file mode 100644 index 0000000..f8decc2 --- /dev/null +++ b/packages/react-diagrams-routing/src/link/RightAngleLinkModel.ts @@ -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; + } +} diff --git a/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx b/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx new file mode 100644 index 0000000..2bc1868 --- /dev/null +++ b/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx @@ -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 { + public static defaultProps: RightAngleLinkProps = { + color: "red", + width: 3, + link: null, + smooth: false, + diagramEngine: null, + factory: null, + }; + + refPaths: React.RefObject[]; + + // 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(); + this.refPaths.push(ref); + return ( + { + 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 {paths}; + } +} diff --git a/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx b/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx deleted file mode 100644 index 9ff94e3..0000000 --- a/src/routing/rightAngle/factories/RightAngleLinkFactory.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { RightAngleLinkWidget } from "../widgets/RightAngleLinkWidget"; -import { DiagramEngine } from "../../../DiagramEngine"; -import { AbstractLinkFactory } from "../../../factories/AbstractLinkFactory"; -import { DefaultLinkModel } from "../../../defaults/models/DefaultLinkModel"; - -/** - * @author Dylan Vorster - */ -export class RightAngleLinkFactory extends AbstractLinkFactory { - constructor() { - super("rightAngle"); - } - - generateReactWidget(diagramEngine: DiagramEngine, link: DefaultLinkModel): JSX.Element { - return React.createElement(RightAngleLinkWidget, { - link: link, - diagramEngine: diagramEngine - }); - } - - getNewInstance(initialConfig?: any): DefaultLinkModel { - return new DefaultLinkModel(); - } - - generateLinkSegment(model: DefaultLinkModel, widget: RightAngleLinkWidget, selected: boolean, path: string) { - return ( - - ); - } -} diff --git a/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx b/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx deleted file mode 100644 index c0d8d7c..0000000 --- a/src/routing/rightAngle/widgets/RightAngleLinkWidget.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import * as React from "react"; -import { DiagramEngine } from "../../../DiagramEngine"; -import { PointModel } from "../../../models/PointModel"; -import { Toolkit } from "../../../Toolkit"; -import { RightAngleLinkFactory } from "../factories/RightAngleLinkFactory"; -import { DefaultLinkModel } from "../../../defaults/models/DefaultLinkModel"; -import PathFinding from "../../PathFinding"; -import * as _ from "lodash"; -import { LabelModel } from "../../../models/LabelModel"; -import { BaseWidget, BaseWidgetProps } from "../../../widgets/BaseWidget"; - -export interface RightAngleLinkProps extends BaseWidgetProps { - color?: string; - width?: number; - smooth?: boolean; - link: DefaultLinkModel; - diagramEngine: DiagramEngine; - pointAdded?: (point: PointModel, event: MouseEvent) => any; -} - -export interface RightAngleLinkState { - selected: boolean, - canDrag: boolean, -} - -export class RightAngleLinkWidget extends BaseWidget { - public static defaultProps: RightAngleLinkProps = { - 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: RightAngleLinkProps) { - 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 ( - - - { - 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")} - /> - - ); - } - - generateLabel(label: LabelModel) { - const canvas = this.props.diagramEngine.canvas as HTMLElement; - return ( - -
(this.refLabels[label.id] = ref)}> - {this.props.diagramEngine - .getFactoryForLabel(label) - .generateReactWidget(this.props.diagramEngine, label)} -
-
- ); - } - - 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 RightAngleLinkFactory).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 ( - - {Bottom} - {Top} - - ); - } - - 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 ( - - {paths} - {_.map(this.props.link.labels, labelModel => { - return this.generateLabel(labelModel); - })} - - ); - } -} From cff54426c9ac336bf26fbfd53e11ae622fef84a0 Mon Sep 17 00:00:00 2001 From: Daniel Lazar Date: Mon, 12 Aug 2019 12:57:04 +0200 Subject: [PATCH 6/7] update for new links ... port model needs RightAngleLinkModel --- .../demos/demo-right-angles-routing/index.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx b/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx index 814f1f8..022fffb 100644 --- a/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx +++ b/packages/diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx @@ -2,14 +2,21 @@ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, - RightAngleLinkFactory, + 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 { CanvasWidget } from '@projectstorm/react-canvas-core'; +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) { + return new RightAngleLinkModel(); + } +} + export default () => { // setup the diagram engine const engine = createEngine(); @@ -20,24 +27,22 @@ export default () => { // 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 DefaultPortModel(false, "out-1", "Out")); + 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 DefaultPortModel(false, "out-1", "Out")); + 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 DefaultPortModel(true, "in-1", "In")); + 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 DefaultPortModel(true, "in-1", "In")); + const port4 = node4.addPort(new RightAnglePortModel(true, "in-1", "In")); node4.setPosition(95, 185); - const rightAngleLink = engine.getLinkFactories().getFactory(RightAngleLinkFactory.NAME); - // linking things together - const link1 = port1.link(port4, rightAngleLink); - const link2 = port2.link(port3, rightAngleLink); + const link1 = port1.link(port4); + const link2 = port2.link(port3); // add all to the main model model.addAll(node1, node2, node3, node4, link1, link2); From 4cae28c8082aba4adca70c60b95169367474466c Mon Sep 17 00:00:00 2001 From: Daniel Lazar Date: Mon, 12 Aug 2019 19:45:43 +0200 Subject: [PATCH 7/7] of course there must be || to check positions if moving is too fast --- .../react-diagrams-routing/src/link/RightAngleLinkWidget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx b/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx index 2bc1868..984d720 100644 --- a/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx +++ b/packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx @@ -216,12 +216,12 @@ export class RightAngleLinkWidget extends React.Component