Skip to content
This repository has been archived by the owner on Feb 19, 2024. It is now read-only.

Commit

Permalink
Add simple node linking (#2)
Browse files Browse the repository at this point in the history
* Add simple node linking

* Add simple node linking
  • Loading branch information
mavain committed Nov 1, 2020
1 parent aead9f7 commit 0e1cdc4
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 23 deletions.
15 changes: 14 additions & 1 deletion src/components/graph/Graph.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ body {
border: 4px solid #555555;
}

#graph>canvas {
#canvas {
position: fixed;
z-index: 400;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
pointer-events: none;
}

#backplane {
position: fixed;
width: 100%;
height: 100%;
}
95 changes: 93 additions & 2 deletions src/components/graph/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Guid } from "guid-typescript";
import { Node } from "../../nodes/Node";
import GraphNode from "./GraphNode";
import IPosition from "./IPosition";
import { BaseGraphNodeProperty } from "./GraphNodeProperties";
import GraphNodeOutput from "./GraphNodeOutput";

const GRID_SIZE: number = 10000; // in px
const GRID_SIZE_HALF: number = 5000; // in px
Expand Down Expand Up @@ -35,6 +37,13 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState

private _nodeTable: Map<Guid, Node>;
private _selectedGraphNode: GraphNode | null = null;
private _selectedGraphNodeIO: GraphNodeOutput<any> | BaseGraphNodeProperty<any> | null = null;

private _canvas: HTMLCanvasElement | null = null;
private _canvasContext: CanvasRenderingContext2D | null = null;

private _mouseX: number = 0;
private _mouseY: number = 0;

constructor(props: IGraphProperties) {
super(props);
Expand Down Expand Up @@ -71,6 +80,14 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState
return this._selectedGraphNode;
}

public set selectedGraphNodeIO(IO: GraphNodeOutput<any> | BaseGraphNodeProperty<any> | null) {
this._selectedGraphNodeIO = IO;
}

public get selectedGraphNodeIO(): GraphNodeOutput<any> | BaseGraphNodeProperty<any> | null {
return this._selectedGraphNodeIO;
}

public pageToGraphCoordinates(x: number, y: number): IPosition {
const screenWidth: number = window.innerWidth;
const screenHeight: number = window.innerHeight;
Expand Down Expand Up @@ -192,6 +209,18 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState
this.setTransform(0, 0, 1);
}

private backplaneMouseUp(): void {
if (!this._selectedGraphNodeIO) return;
this._selectedGraphNodeIO = null;
this.moveable = true;
}

private graphMouseLeave(event: React.MouseEvent): void {
if (!this._selectedGraphNodeIO) return;
this._selectedGraphNodeIO = null;
this.moveable = true;
}

private onWheel(event: WheelEvent): void {
event.preventDefault();

Expand Down Expand Up @@ -236,6 +265,9 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState
}

private onMouseMove(event: MouseEvent): void {
this._mouseX = this.getMouseX(event);
this._mouseY = this.getMouseY(event);

if (!this.mouseDown) return;

const x: number = this.getMouseX(event);
Expand All @@ -252,6 +284,64 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState
graph.addEventListener("mouseleave", this.onMouseUp.bind(this));
graph.addEventListener("mousemove", this.onMouseMove.bind(this));
}

this._canvas = document.getElementById("canvas") as HTMLCanvasElement;
if (this._canvas) {
this._canvasContext = this._canvas.getContext("2d");
window.requestAnimationFrame(this.renderNodesCanvas.bind(this));
}
}

private renderNodesCanvas(): void {
if (!this._canvas) return;
if (!this._canvasContext) return;

const canvas = this._canvas;
const context = this._canvasContext;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = "white";
context.lineWidth = 3 * this._currentZoom;

const bezierOffset = 150 * this._currentZoom;

if (this._selectedGraphNodeIO) {
const selector: HTMLElement | null = (this._selectedGraphNodeIO instanceof BaseGraphNodeProperty) ?
this._selectedGraphNodeIO.props.property.userSelector :
this._selectedGraphNodeIO.props.output.userSelector;
if (selector) {
const rect = selector.getBoundingClientRect();
const x = (rect.left + rect.right) / 2, y = (rect.top + rect.bottom) / 2;
context.beginPath();
context.moveTo(x, y);
context.lineTo(this._mouseX, this._mouseY);
context.stroke();
}
}

for (const node of this.state.nodes) {
for (const [_, property] of node.properties) {
if (property.linkedOutput && property.userSelector && property.linkedOutput.userSelector) {

const propertyRect = property.userSelector.getBoundingClientRect();
const outputRect = property.linkedOutput.userSelector.getBoundingClientRect();

const px = (propertyRect.left + propertyRect.right) / 2, py = (propertyRect.top + propertyRect.bottom) / 2;
const ox = (outputRect.left + outputRect.right) / 2, oy = (outputRect.top + outputRect.bottom) / 2;

context.beginPath();
context.moveTo(px, py);
context.bezierCurveTo(px - bezierOffset, py, ox + bezierOffset, oy, ox, oy);
context.stroke();

}
}
}

window.requestAnimationFrame(this.renderNodesCanvas.bind(this));
}

private renderNodes(): Array<JSX.Element> {
Expand All @@ -269,10 +359,11 @@ export default class Graph extends React.Component<IGraphProperties, IGraphState
<div id="graph" style={{
width: GRID_SIZE, height: GRID_SIZE,
left: -(GRID_SIZE_HALF - (window.innerWidth / 2)), top: -(GRID_SIZE_HALF - (window.innerHeight / 2))
}}>
}} onMouseLeave={this.graphMouseLeave.bind(this)}>
<div id="backplane" onMouseUp={this.backplaneMouseUp.bind(this)} />
{this.renderNodes()}
<canvas />
</div>
<canvas id="canvas" />
</div>);
}
}
2 changes: 2 additions & 0 deletions src/components/graph/GraphNode.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.graph-node {
position: absolute;
color: white;
box-shadow: 0px 0px 6px 0px #000000;
z-index: 500;
}

.graph-node>.header {
Expand Down
20 changes: 14 additions & 6 deletions src/components/graph/GraphNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export default class GraphNode extends React.Component<IGraphNodeProperties> {
private offsetY: number = 0;
private offsetZoom: number = 0;

private onContentMouseDown(event: React.MouseEvent): void {
this.props.graph.isMoveable = false;
}

private onContentMouseLeave(event: React.MouseEvent): void {
this.props.graph.isMoveable = true;
}

private onMouseDown(event: React.MouseEvent): void {
const x = this.props.node.x, y = this.props.node.y
const screenPos = this.props.graph.graphToPageCoordinates(x, y);
Expand Down Expand Up @@ -75,23 +83,23 @@ export default class GraphNode extends React.Component<IGraphNodeProperties> {
switch (property.typeName) {
case "Toggle":
elements.push(<GraphNodeToggleProperty id={elementId} key={elementId} node={this.props.node}
name={propertyName} property={property} />);
name={propertyName} property={property} graph={this.props.graph}/>);
break;
case "Number":
elements.push(<GraphNodeNumberProperty id={elementId} key={elementId} node={this.props.node}
name={propertyName} property={property} />);
name={propertyName} property={property} graph={this.props.graph}/>);
break;
case "Vector":
elements.push(<GraphNodeVectorProperty id={elementId} key={elementId} node={this.props.node}
name={propertyName} property={property} />);
name={propertyName} property={property} graph={this.props.graph}/>);
break
case "Angle":
elements.push(<GraphNodeAngleProperty id={elementId} key={elementId} node={this.props.node}
name={propertyName} property={property} />);
name={propertyName} property={property} graph={this.props.graph}/>);
break;
case "Color":
elements.push(<GraphNodeColorProperty id={elementId} key={elementId} node={this.props.node}
name={propertyName} property={property} />);
name={propertyName} property={property} graph={this.props.graph}/>);
break;
default:
break;
Expand All @@ -105,7 +113,7 @@ export default class GraphNode extends React.Component<IGraphNodeProperties> {
const elements: Array<JSX.Element> = [];
for (const [outputName, output] of this.props.node.outputs) {
const elementId: string = `${this.props.node.id}_${outputName}`;
elements.push(<GraphNodeOutput key={elementId} name={outputName} output={output} />);
elements.push(<GraphNodeOutput key={elementId} name={outputName} output={output} graph={this.props.graph}/>);
}

return elements;
Expand Down
7 changes: 7 additions & 0 deletions src/components/graph/GraphNodeOutput.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.graph-node-output {
text-align: right;
user-select: none;
}

.graph-node-output>.user-selection {
Expand All @@ -24,4 +25,10 @@
background-color: #eeeeee;
border: 2px solid #aaaaaa;
transform: scale(1.1);
}

.graph-node-output>.linked {
background-color: #eeeeee;
border: 2px solid #aaaaaa;
transform: scale(1.1);
}
37 changes: 35 additions & 2 deletions src/components/graph/GraphNodeOutput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import React from "react";
import { NodeOutput } from "../../nodes/Node";
import Graph from "./Graph";
import "./GraphNodeOutput.css";
import { BaseGraphNodeProperty } from "./GraphNodeProperties";

interface IGraphNodeOutputProperties<T> {
name: string;
output: NodeOutput<T>;
graph: Graph;
}

export default class GraphNodeOutput<T> extends React.Component<IGraphNodeOutputProperties<T>> {

private _userSelectionRef: React.RefObject<any> | null = null;

constructor(props: any) {
super(props);
this._userSelectionRef = React.createRef();
}

private onMouseDown(): void {
this.props.graph.selectedGraphNodeIO = this;
this.props.graph.isMoveable = false;
}

private onMouseUp(): void {
if (this.props.graph.selectedGraphNodeIO instanceof BaseGraphNodeProperty) {
this.props.graph.selectedGraphNodeIO.props.property.trySetLinkedOutput(this.props.output);
this.props.graph.selectedGraphNodeIO = null;
this.props.graph.isMoveable = true;
}
if (this.props.graph.selectedGraphNodeIO === this) {
this.props.graph.selectedGraphNodeIO = null;
this.props.graph.isMoveable = true;
}
}

public componentDidMount(): void {
if (!this._userSelectionRef) return;
this.props.output.userSelector = this._userSelectionRef.current;
}

public render(): JSX.Element {
return (<div className="graph-node-output">
return (<div className="graph-node-output" onMouseDown={this.onMouseDown.bind(this)} onMouseUp={this.onMouseUp.bind(this)}>
<span>{this.props.name}</span>
<div className="user-selection"/>
<div className="user-selection" ref={this._userSelectionRef}/>
</div>);
}
}
7 changes: 7 additions & 0 deletions src/components/graph/GraphNodeProperties.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
padding-bottom: 2px;
font-size: 12px;
vertical-align: middle;
user-select: none;
}

.graph-node-property-checkbox-label {
Expand Down Expand Up @@ -40,4 +41,10 @@
background-color: #eeeeee;
border: 2px solid #aaaaaa;
transform: scale(1.1);
}

.graph-node-property>.linked {
background-color: #eeeeee;
border: 2px solid #aaaaaa;
transform: scale(1.1);
}
Loading

0 comments on commit 0e1cdc4

Please sign in to comment.