|
@@ -4,8 +4,12 @@ import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
|
|
import { GraphNode } from './graphNode';
|
|
import { GraphNode } from './graphNode';
|
|
import * as dagre from 'dagre';
|
|
import * as dagre from 'dagre';
|
|
import { Nullable } from 'babylonjs/types';
|
|
import { Nullable } from 'babylonjs/types';
|
|
-import { NodeMaterialConnectionPoint } from 'babylonjs';
|
|
|
|
import { NodeLink } from './nodeLink';
|
|
import { NodeLink } from './nodeLink';
|
|
|
|
+import { NodePort } from './nodePort';
|
|
|
|
+import { NodeMaterialConnectionPoint, NodeMaterialConnectionPointDirection, NodeMaterialConnectionPointCompatibilityStates } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
|
|
|
|
+import { Vector2 } from 'babylonjs/Maths/math.vector';
|
|
|
|
+import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
|
|
|
|
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
|
|
|
|
|
|
require("./graphCanvas.scss");
|
|
require("./graphCanvas.scss");
|
|
|
|
|
|
@@ -21,10 +25,23 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
|
|
private _links: NodeLink[] = [];
|
|
private _links: NodeLink[] = [];
|
|
private _mouseStartPointX: Nullable<number> = null;
|
|
private _mouseStartPointX: Nullable<number> = null;
|
|
private _mouseStartPointY: Nullable<number> = null
|
|
private _mouseStartPointY: Nullable<number> = null
|
|
|
|
+ private _dropPointX = 0;
|
|
|
|
+ private _dropPointY = 0;
|
|
private _x = 0;
|
|
private _x = 0;
|
|
private _y = 0;
|
|
private _y = 0;
|
|
private _zoom = 1;
|
|
private _zoom = 1;
|
|
private _selectedNodes: GraphNode[] = [];
|
|
private _selectedNodes: GraphNode[] = [];
|
|
|
|
+ private _selectedLink: Nullable<NodeLink> = null;
|
|
|
|
+ private _candidateLink: Nullable<NodeLink> = null;
|
|
|
|
+ private _candidatePort: Nullable<NodePort> = null;
|
|
|
|
+
|
|
|
|
+ private _altKeyIsPressed = false;
|
|
|
|
+ private _ctrlKeyIsPressed = false;
|
|
|
|
+ private _oldY = -1;
|
|
|
|
+
|
|
|
|
+ public get globalState(){
|
|
|
|
+ return this.props.globalState;
|
|
|
|
+ }
|
|
|
|
|
|
public get nodes() {
|
|
public get nodes() {
|
|
return this._nodes;
|
|
return this._nodes;
|
|
@@ -65,20 +82,58 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
|
|
return this._selectedNodes;
|
|
return this._selectedNodes;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public get selectedLink() {
|
|
|
|
+ return this._selectedLink;
|
|
|
|
+ }
|
|
|
|
+
|
|
public get canvasContainer() {
|
|
public get canvasContainer() {
|
|
return this._graphCanvas;
|
|
return this._graphCanvas;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ public get svgCanvas() {
|
|
|
|
+ return this._svgCanvas;
|
|
|
|
+ }
|
|
|
|
+
|
|
constructor(props: IGraphCanvasComponentProps) {
|
|
constructor(props: IGraphCanvasComponentProps) {
|
|
super(props);
|
|
super(props);
|
|
|
|
|
|
- props.globalState.onSelectionChangedObservable.add(node => {
|
|
|
|
- if (!node) {
|
|
|
|
|
|
+ props.globalState.onSelectionChangedObservable.add(selection => {
|
|
|
|
+
|
|
|
|
+ if (!selection) {
|
|
this._selectedNodes = [];
|
|
this._selectedNodes = [];
|
|
|
|
+ this._selectedLink = null;
|
|
} else {
|
|
} else {
|
|
- this._selectedNodes = [node];
|
|
|
|
|
|
+ if (selection instanceof NodeLink) {
|
|
|
|
+ this._selectedLink = selection;
|
|
|
|
+ } else {
|
|
|
|
+ if (this._ctrlKeyIsPressed) {
|
|
|
|
+ this._selectedNodes.push(selection);
|
|
|
|
+ } else {
|
|
|
|
+ this._selectedNodes = [selection];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
+
|
|
|
|
+ props.globalState.onCandidatePortSelected.add(port => {
|
|
|
|
+ this._candidatePort = port;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ this.props.globalState.hostDocument!.addEventListener("keyup", () => this.onKeyUp(), false);
|
|
|
|
+ this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
|
|
|
|
+ this._altKeyIsPressed = evt.altKey;
|
|
|
|
+ this._ctrlKeyIsPressed = evt.ctrlKey;
|
|
|
|
+ }, false);
|
|
|
|
+ this.props.globalState.hostDocument!.defaultView!.addEventListener("blur", () => {
|
|
|
|
+ this._altKeyIsPressed = false;
|
|
|
|
+ this._ctrlKeyIsPressed = false;
|
|
|
|
+ }, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ onKeyUp() {
|
|
|
|
+ this._altKeyIsPressed = false;
|
|
|
|
+ this._ctrlKeyIsPressed = false;
|
|
|
|
+ this._oldY = -1;
|
|
}
|
|
}
|
|
|
|
|
|
findNodeFromBlock(block: NodeMaterialBlock) {
|
|
findNodeFromBlock(block: NodeMaterialBlock) {
|
|
@@ -202,6 +257,34 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
|
|
}
|
|
}
|
|
|
|
|
|
onMove(evt: React.PointerEvent) {
|
|
onMove(evt: React.PointerEvent) {
|
|
|
|
+ // Candidate link
|
|
|
|
+ if (this._candidateLink) {
|
|
|
|
+ const rootRect = this.canvasContainer.getBoundingClientRect();
|
|
|
|
+ this._candidatePort = null;
|
|
|
|
+ this.props.globalState.onCandidateLinkMoved.notifyObservers(new Vector2(evt.pageX, evt.pageY));
|
|
|
|
+ this._dropPointX = (evt.pageX - rootRect.left) / this.zoom;
|
|
|
|
+ this._dropPointY = (evt.pageY - rootRect.top) / this.zoom;
|
|
|
|
+
|
|
|
|
+ this._candidateLink.update(this._dropPointX, this._dropPointY, true);
|
|
|
|
+
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Zoom with mouse + alt
|
|
|
|
+ if (this._altKeyIsPressed && evt.buttons === 1) {
|
|
|
|
+ if (this._oldY < 0) {
|
|
|
|
+ this._oldY = evt.pageY;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let zoomDelta = (evt.pageY - this._oldY) / 10;
|
|
|
|
+ if (Math.abs(zoomDelta) > 5) {
|
|
|
|
+ this.zoom += zoomDelta / 100;
|
|
|
|
+ this._oldY = evt.pageY;
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Move canvas
|
|
this._rootContainer.style.cursor = "move";
|
|
this._rootContainer.style.cursor = "move";
|
|
|
|
|
|
if (this._mouseStartPointX === null || this._mouseStartPointY === null) {
|
|
if (this._mouseStartPointX === null || this._mouseStartPointY === null) {
|
|
@@ -214,17 +297,36 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
|
|
this._mouseStartPointY = evt.clientY;
|
|
this._mouseStartPointY = evt.clientY;
|
|
}
|
|
}
|
|
|
|
|
|
- onDown(evt: React.PointerEvent) {
|
|
|
|
|
|
+ onDown(evt: React.PointerEvent<HTMLElement>) {
|
|
|
|
+ this._rootContainer.setPointerCapture(evt.pointerId);
|
|
|
|
+
|
|
|
|
+ if (evt.nativeEvent.srcElement && (evt.nativeEvent.srcElement as HTMLElement).nodeName === "IMG") {
|
|
|
|
+ if (!this._candidateLink) {
|
|
|
|
+ let portElement = ((evt.nativeEvent.srcElement as HTMLElement).parentElement as any).port as NodePort;
|
|
|
|
+ this._candidateLink = new NodeLink(this, portElement, portElement.node);
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
|
|
this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
|
|
this._mouseStartPointX = evt.clientX;
|
|
this._mouseStartPointX = evt.clientX;
|
|
this._mouseStartPointY = evt.clientY;
|
|
this._mouseStartPointY = evt.clientY;
|
|
- this._rootContainer.setPointerCapture(evt.pointerId);
|
|
|
|
|
|
+
|
|
}
|
|
}
|
|
|
|
|
|
onUp(evt: React.PointerEvent) {
|
|
onUp(evt: React.PointerEvent) {
|
|
this._mouseStartPointX = null;
|
|
this._mouseStartPointX = null;
|
|
this._mouseStartPointY = null;
|
|
this._mouseStartPointY = null;
|
|
- this._rootContainer.releasePointerCapture(evt.pointerId);
|
|
|
|
|
|
+ this._rootContainer.releasePointerCapture(evt.pointerId);
|
|
|
|
+ this._oldY = -1;
|
|
|
|
+
|
|
|
|
+ if (this._candidateLink) {
|
|
|
|
+ this.processCandidatePort();
|
|
|
|
+ this.props.globalState.onCandidateLinkMoved.notifyObservers(null);
|
|
|
|
+ this._candidateLink.dispose();
|
|
|
|
+ this._candidateLink = null;
|
|
|
|
+ this._candidatePort = null;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
onWheel(evt: React.WheelEvent) {
|
|
onWheel(evt: React.WheelEvent) {
|
|
@@ -250,6 +352,96 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
|
|
this.x = 0;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.y = 0;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ processCandidatePort() {
|
|
|
|
+ let pointB = this._candidateLink!.portA.connectionPoint;
|
|
|
|
+ let nodeB = this._candidateLink!.portA.node;
|
|
|
|
+ let pointA: NodeMaterialConnectionPoint;
|
|
|
|
+ let nodeA: GraphNode;
|
|
|
|
+
|
|
|
|
+ if (this._candidatePort) {
|
|
|
|
+ pointA = this._candidatePort.connectionPoint;
|
|
|
|
+ nodeA = this._candidatePort.node;
|
|
|
|
+ } else {
|
|
|
|
+ if (pointB.direction === NodeMaterialConnectionPointDirection.Output) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // No destination so let's spin a new input block
|
|
|
|
+ let inputBlock = new InputBlock("", undefined, this._candidateLink!.portA.connectionPoint.type);
|
|
|
|
+ pointA = inputBlock.output;
|
|
|
|
+ nodeA = this.appendBlock(inputBlock);
|
|
|
|
+
|
|
|
|
+ nodeA.x = this._dropPointX - 200;
|
|
|
|
+ nodeA.y = this._dropPointY - 50;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointA.direction === NodeMaterialConnectionPointDirection.Input) {
|
|
|
|
+ let temp = pointB;
|
|
|
|
+ pointB = pointA;
|
|
|
|
+ pointA = temp;
|
|
|
|
+
|
|
|
|
+ let tempNode = nodeA;
|
|
|
|
+ nodeA = nodeB;
|
|
|
|
+ nodeB = tempNode;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointB.connectedPoint === pointA) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointB === pointA) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointB.direction === pointA.direction) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Check compatibility
|
|
|
|
+ let isFragmentOutput = pointB.ownerBlock.getClassName() === "FragmentOutputBlock";
|
|
|
|
+ let compatibilityState = pointA.checkCompatibilityState(pointB);
|
|
|
|
+ if (compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible) {
|
|
|
|
+ if (isFragmentOutput) {
|
|
|
|
+ let fragmentBlock = pointB.ownerBlock as FragmentOutputBlock;
|
|
|
|
+
|
|
|
|
+ if (pointB.name === "rgb" && fragmentBlock.rgba.isConnected) {
|
|
|
|
+ nodeB.getLinksForConnectionPoint(fragmentBlock.rgba)[0].dispose();
|
|
|
|
+ } else if (pointB.name === "rgba" && fragmentBlock.rgb.isConnected) {
|
|
|
|
+ nodeB.getLinksForConnectionPoint(fragmentBlock.rgb)[0].dispose();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ let message = "";
|
|
|
|
+
|
|
|
|
+ switch (compatibilityState) {
|
|
|
|
+ case NodeMaterialConnectionPointCompatibilityStates.TypeIncompatible:
|
|
|
|
+ message = "Cannot connect two different connection types";
|
|
|
|
+ break;
|
|
|
|
+ case NodeMaterialConnectionPointCompatibilityStates.TargetIncompatible:
|
|
|
|
+ message = "Source block can only work in fragment shader whereas destination block is currently aimed for the vertex shader";
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.props.globalState.onErrorMessageDialogRequiredObservable.notifyObservers(message);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pointB.isConnected) {
|
|
|
|
+ let links = nodeB.getLinksForConnectionPoint(pointB);
|
|
|
|
+
|
|
|
|
+ links.forEach(link => {
|
|
|
|
+ link.dispose();
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pointA.connectTo(pointB);
|
|
|
|
+ this.connectPorts(pointA, pointB);
|
|
|
|
+
|
|
|
|
+ nodeB.refresh();
|
|
|
|
+
|
|
|
|
+ this.props.globalState.onRebuildRequiredObservable.notifyObservers();
|
|
|
|
+ }
|
|
|
|
|
|
render() {
|
|
render() {
|
|
return (
|
|
return (
|