Ver código fonte

First UI integration

David Catuhe 6 anos atrás
pai
commit
694fe39430

+ 1 - 1
nodeEditor/src/components/diagram/defaultNodeModel.ts

@@ -51,7 +51,7 @@ export class DefaultNodeModel extends NodeModel {
                 var connectedNode;
                 var existingNodes = nodes.filter((n) => { return n.block === (connection as any)._connectedPoint._ownerBlock });
                 if (existingNodes.length == 0) {
-                    connectedNode = graphEditor.createNodeFromObject({ column: options.column + 1, nodeMaterialBlock: connection.connectedPoint._ownerBlock });
+                    connectedNode = graphEditor.createNodeFromObject({ nodeMaterialBlock: connection.connectedPoint._ownerBlock });
                 } else {
                     connectedNode = existingNodes[0];
                 }

+ 67 - 21
nodeEditor/src/components/diagram/diagram.scss

@@ -2,18 +2,45 @@
     width: 200px;
 }
 
+.srd-node--selected > * {
+    border: 4px solid  rgb(0, 192, 255) !important;
+    border-radius: 20px;
+}
+          
+.srd-port {
+    background: rgb(0, 192, 255);
+    border-radius: 10px;
+    border: black 4px solid;
+
+    &.connected {
+        background: #CAB422;
+    }
+}
+
 .diagramBlock {
     background: white;
     width: 100%;
     border: 4px solid black;
+    border-radius: 20px;
+    display: grid;
+    grid-template-rows: 30px auto;
+    grid-template-columns: 50% 50%;
 
     &.input {
-        background: green;
+        background: #40866E;
         color:white;
+
+        .value {
+            grid-row: 2;
+        }
+
+        .outputs {
+            transform: translateY(5px);
+        }
     }
 
     &.attribute {
-        background: orange;
+        background: #40866E;
     }
 
     &.output {
@@ -25,33 +52,51 @@
         }
     }
 
-    .header {
-        margin: 10px;
+    .header {            
+        grid-row: 1;
+        grid-column: 1 / span 2;
+        border: 4px solid black;
+        border-top-right-radius: 16px;
+        border-top-left-radius: 16px;
         font-size: 16px;
         text-align: center;
+        margin: -1px;
         
         white-space: nowrap;
         text-overflow: ellipsis;
         overflow: hidden;
+        background: black;
+        color: white;
     }
 
     .value {
+        grid-row: 3;
+        grid-column: 1 / span 2;
         height: 34px;
         text-align: center;
-        font-size: 20px;
+        font-size: 18px;
         font-weight: bold;
-        overflow: hidden;
-        margin: 0 5px;
+        margin: 0 10px;
 
-        .fullColor {
-            height: 100%;
+        .value-text {
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
         }
     }
 
+    .preview {
+        border-bottom-left-radius: 16px;
+        border-bottom-right-radius: 16px;
+        padding-top: 2px;
+    }
+
     .inputs {
+        grid-row: 2;
+        grid-column: 1;
         .input-port {
             display: grid;
-            grid-template-columns: 4px calc(100% - 4px);
+            grid-template-columns: 10px calc(100% - 10px);
             grid-template-rows: 100%;
 
             .input-port-plug {
@@ -60,25 +105,27 @@
                 display: grid;
                 align-content: center;
                 margin-left: -11px;
-                
-                .srd-port {
-                    background: grey;
-                }
+
             }
 
             .input-port-label {
                 margin-left: 10px;
                 grid-column: 2;
                 grid-row: 1;         
-                margin-bottom: 2px;       
+                margin-bottom: 2px;    
+                overflow: hidden;
+                white-space: nowrap;
+                text-overflow: ellipsis;   
             }
         }
     }
 
     .outputs {
+        grid-row: 2;
+        grid-column: 2;
         .output-port {
             display: grid;
-            grid-template-columns: calc(100% - 4px) 4px;
+            grid-template-columns: calc(100% - 10px) 10px;
             grid-template-rows: 100%;
 
             .output-port-plug {
@@ -86,10 +133,6 @@
                 grid-row: 1;
                 display: grid;
                 align-content: center;
-
-                .srd-port {
-                    background: grey;
-                }
             }
 
             .output-port-label {
@@ -97,7 +140,10 @@
                 margin-right: 10px;
                 grid-column: 1;
                 grid-row: 1;                        
-                margin-bottom: 2px;        
+                margin-bottom: 2px;   
+                overflow: hidden;
+                white-space: nowrap;
+                text-overflow: ellipsis;   
             }
         }
     }

+ 8 - 37
nodeEditor/src/components/diagram/generic/genericNodeWidget.tsx

@@ -1,9 +1,8 @@
 import * as React from "react";
-import { PortWidget } from "storm-react-diagrams";
-import { DefaultPortModel } from '../defaultPortModel';
 import { Nullable } from 'babylonjs/types';
 import { GlobalState } from '../../../globalState';
 import { GenericNodeModel } from './genericNodeModel';
+import { PortHelper } from '../portHelper';
 
 /**
  * GenericNodeWidgetProps
@@ -43,44 +42,16 @@ export class GenericNodeWidget extends React.Component<GenericNodeWidgetProps, G
     }
 
     render() {
+        // Header label
         var header = "";
-        var inputPorts = new Array<JSX.Element>()
-        var outputPorts = new Array<JSX.Element>()
-        if (this.props.node) {
-            // Header label
-            if (this.props.node.block) {
-                header = this.props.node.block.name;
-            }
-
-            // Input/Output ports
-            for (var key in this.props.node.ports) {
-                var port = this.props.node.ports[key] as DefaultPortModel;
-                if (port.position === "input") {
-                    inputPorts.push(
-                        <div key={key} className="input-port">
-                            <div className="input-port-plug">
-                                <PortWidget key={key} name={port.name} node={this.props.node} />
-                            </div>
-                            <div className="input-port-label">
-                                {port.name}
-                            </div>
-                        </div>
-                    )
-                } else {
-                    outputPorts.push(
-                        <div key={key} className="output-port">
-                            <div className="output-port-label">
-                                {port.name}
-                            </div>
-                            <div className="output-port-plug">
-                                <PortWidget key={key} name={port.name} node={this.props.node} />
-                            </div>
-                        </div>
-                    )
-                }
-            }
+        if (this.props.node && this.props.node.block) {
+            header = this.props.node.block.name;
         }
 
+        // Input/Output ports
+        var outputPorts = PortHelper.GenerateOutputPorts(this.props.node, false);
+        var inputPorts = PortHelper.GenerateInputPorts(this.props.node);
+
         return (
             <div className={"diagramBlock" + (outputPorts.length === 0 ? " output" : "")}>
                 <div className="header">

+ 19 - 38
nodeEditor/src/components/diagram/input/inputNodeWidget.tsx

@@ -1,13 +1,12 @@
 import * as React from "react";
-import { PortWidget } from "storm-react-diagrams";
 import { InputNodeModel } from './inputNodeModel';
 import { Nullable } from 'babylonjs/types';
 import { GlobalState } from '../../../globalState';
-import { DefaultPortModel } from '../defaultPortModel';
 import { NodeMaterialWellKnownValues } from 'babylonjs/Materials/Node/nodeMaterialWellKnownValues';
 import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPointTypes';
 import { Color3 } from 'babylonjs/Maths/math';
 import { StringTools } from '../../../stringTools';
+import { PortHelper } from '../portHelper';
 
 /**
  * GenericNodeWidgetProps
@@ -42,54 +41,22 @@ export class InputNodeWidget extends React.Component<InputNodeWidgetProps> {
     renderValue(value: string) {
         if (value) {
             return (
-                <div>
+                <div className="value-text">
                     {value}
                 </div>
             )
         }
 
-        let inputBlock = this.props.node!.inputBlock;
-        if (!inputBlock || !inputBlock.isUniform) {
-            return null;
-        }
-
-        switch (inputBlock.type) {
-            case NodeMaterialBlockConnectionPointTypes.Color3:
-            case NodeMaterialBlockConnectionPointTypes.Color3OrColor4:
-            case NodeMaterialBlockConnectionPointTypes.Color4: {
-                let color = inputBlock.value as Color3;
-                return (
-                    <div className="fullColor" style={{ background: color.toHexString() }}></div>
-                )
-            }
-        }
-
         return null;
     }
 
     render() {
-        var outputPorts = new Array<JSX.Element>()
-        let port: DefaultPortModel;
-        if (this.props.node) {
-            for (var key in this.props.node.ports) {
-                port = this.props.node.ports[key] as DefaultPortModel;
-
-                outputPorts.push(
-                    <div key={key} className="output-port">
-                        <div className="output-port-label">
-                        </div>
-                        <div className="output-port-plug">
-                            <PortWidget key={key} name={port.name} node={this.props.node} />
-                        </div>
-                    </div>
-                );
-                break;
-            }
-        }
+        var outputPorts = PortHelper.GenerateOutputPorts(this.props.node, true);
 
         let inputBlock = this.props.node!.inputBlock;
         let value = "";
         let name = StringTools.GetBaseType(inputBlock.output.type);
+        let color = "";
 
         if (inputBlock) {
             if (inputBlock.isAttribute) {
@@ -121,13 +88,27 @@ export class InputNodeWidget extends React.Component<InputNodeWidgetProps> {
                         value = "Fog color";
                         break;
                 }
+            } else {
+                if (!inputBlock || !inputBlock.isUniform) {
+                    return null;
+                }
+
+                switch (inputBlock.type) {
+                    case NodeMaterialBlockConnectionPointTypes.Color3:
+                    case NodeMaterialBlockConnectionPointTypes.Color3OrColor4:
+                    case NodeMaterialBlockConnectionPointTypes.Color4: {
+                        color = (inputBlock.value as Color3).toHexString();
+                    }
+                }
             }
         } else {
             name = "Not connected input";
         }
 
         return (
-            <div className={"diagramBlock input" + (inputBlock && inputBlock.isAttribute ? " attribute" : "")}>
+            <div className={"diagramBlock input" + (inputBlock && inputBlock.isAttribute ? " attribute" : "")} style={{
+                background: color
+            }}>
                 <div className="header">
                     {name}
                 </div>

+ 4 - 33
nodeEditor/src/components/diagram/light/lightNodeWidget.tsx

@@ -4,6 +4,7 @@ import { LightNodeModel } from './lightNodeModel';
 import { Nullable } from 'babylonjs/types';
 import { GlobalState } from '../../../globalState';
 import { DefaultPortModel } from '../defaultPortModel';
+import { PortHelper } from '../portHelper';
 
 /**
  * GenericNodeWidgetProps
@@ -36,39 +37,9 @@ export class LightNodeWidget extends React.Component<ILightNodeWidgetProps> {
     }
 
     render() {
-        var inputPorts = new Array<JSX.Element>();
-        var outputPorts = new Array<JSX.Element>();
-        if (this.props.node) {
-            // Input/Output ports
-            for (var key in this.props.node.ports) {
-                var port = this.props.node.ports[key] as DefaultPortModel;
-                if (port.position === "input") {
-                    if (port.name !== "light") {
-                        inputPorts.push(
-                            <div key={key} className="input-port">
-                                <div className="input-port-plug">
-                                    <PortWidget key={key} name={port.name} node={this.props.node} />
-                                </div>
-                                <div className="input-port-label">
-                                    {port.name}
-                                </div>
-                            </div>
-                        )
-                    }
-                } else {
-                    outputPorts.push(
-                        <div key={key} className="output-port">
-                            <div className="output-port-label">
-                                {port.name}
-                            </div>
-                            <div className="output-port-plug">
-                                <PortWidget key={key} name={port.name} node={this.props.node} />
-                            </div>
-                        </div>
-                    )
-                }
-            }
-        }
+        // Input/Output ports
+        var outputPorts = PortHelper.GenerateOutputPorts(this.props.node, false);
+        var inputPorts = PortHelper.GenerateInputPorts(this.props.node);
 
         return (
             <div className={"diagramBlock"}>

+ 61 - 0
nodeEditor/src/components/diagram/portHelper.tsx

@@ -0,0 +1,61 @@
+import * as React from "react";
+import { PortWidget } from "storm-react-diagrams";
+import { DefaultNodeModel } from './defaultNodeModel';
+import { DefaultPortModel } from './defaultPortModel';
+import { Nullable } from 'babylonjs/types';
+
+
+export class PortHelper {
+    public static GenerateOutputPorts(node: Nullable<DefaultNodeModel>, ignoreLabel: boolean) {
+        if (!node) {
+            return new Array<JSX.Element>();
+        }
+        let outputPorts = [];
+        for (var key in node.ports) {
+            let port = node.ports[key] as DefaultPortModel;
+            if (port.position === "output") {
+                outputPorts.push(
+                    <div key={key} className="output-port">
+                        {
+                            !ignoreLabel &&
+                            <div className="output-port-label">
+                                {port.name}
+                            </div>
+                        }
+                        <div className="output-port-plug">
+                            <PortWidget key={key} name={port.name} node={node} className={port.connection && port.connection.endpoints.length > 0 ? "connected" : ""} />
+                        </div>
+                    </div>
+                );
+            }
+        }
+
+        return outputPorts;
+    }
+
+    public static GenerateInputPorts(node: Nullable<DefaultNodeModel>, includeOnly?: string[]) {
+        if (!node) {
+            return new Array<JSX.Element>();
+        }
+        let inputPorts = [];
+        for (var key in node.ports) {
+            let port = node.ports[key] as DefaultPortModel;
+            if (port.position === "input") {
+                if (!includeOnly || includeOnly.indexOf(port.name) !== -1) {
+                    inputPorts.push(
+                        <div key={key} className="input-port">
+                            <div className="input-port-plug">
+                                <PortWidget key={key} name={port.name} node={node} className={port.connection && port.connection.connectedPoint ? "connected" : ""} />
+                            </div>
+                            <div className="input-port-label">
+                                {port.name}
+                            </div>
+                        </div>
+                    );
+                }
+            }
+        }
+
+        return inputPorts;
+    }
+}

+ 4 - 33
nodeEditor/src/components/diagram/texture/textureNodeWidget.tsx

@@ -1,10 +1,9 @@
 import * as React from "react";
-import { PortWidget } from "storm-react-diagrams";
 import { TextureNodeModel } from './textureNodeModel';
 import { TextureLineComponent } from "../../../sharedComponents/textureLineComponent"
 import { Nullable } from 'babylonjs/types';
 import { GlobalState } from '../../../globalState';
-import { DefaultPortModel } from '../defaultPortModel';
+import { PortHelper } from '../portHelper';
 
 /**
  * GenericNodeWidgetProps
@@ -37,37 +36,9 @@ export class TextureNodeWidget extends React.Component<ITextureNodeWidgetProps>
     }
 
     render() {
-        var inputPorts = new Array<JSX.Element>();
-        var outputPorts = new Array<JSX.Element>();
-        if (this.props.node) {
-            // Input/Output ports
-            for (var key in this.props.node.ports) {
-                var port = this.props.node.ports[key] as DefaultPortModel;
-                if (port.position === "output") {
-                    outputPorts.push(
-                        <div key={key} className="output-port">
-                            <div className="output-port-label">
-                                {port.name}
-                            </div>
-                            <div className="output-port-plug">
-                                <PortWidget key={key} name={port.name} node={this.props.node} />
-                            </div>
-                        </div>
-                    );
-                } else if (port.name === "uv") {
-                    inputPorts.push(
-                        <div key={key} className="input-port">
-                            <div className="input-port-plug">
-                                <PortWidget key={key} name={port.name} node={this.props.node} />
-                            </div>
-                            <div className="input-port-label">
-                                {port.name}
-                            </div>
-                        </div>
-                    )
-                }
-            }
-        }
+        // Input/Output ports
+        var outputPorts = PortHelper.GenerateOutputPorts(this.props.node, false);
+        var inputPorts = PortHelper.GenerateInputPorts(this.props.node, ["uv"]);
 
         return (
             <div className={"diagramBlock"}>

+ 18 - 0
nodeEditor/src/components/nodeList/nodeList.scss

@@ -22,6 +22,24 @@
         .underline {
             border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
         }
+
+        .draggableLine {
+            height: 30px;
+            display: grid;
+            align-items: center;
+            justify-items: stretch;
+            background: #222222;
+            border: 1px solid rgb(51, 122, 183) !important;
+            text-align: center;
+            margin: 4px 20px;
+            box-sizing: border-box;
+            border-radius: 10px;
+
+            &:hover {
+                background: rgb(51, 122, 183);
+                color: white;
+            }
+        }
     
         .buttonLine {
             height: 30px;

+ 7 - 34
nodeEditor/src/components/nodeList/nodeListComponent.tsx

@@ -2,57 +2,30 @@
 import * as React from "react";
 import { GlobalState } from '../../globalState';
 import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
-import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
-import { AlphaTestBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/alphaTestBlock';
-import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
-import { ImageProcessingBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/imageProcessingBlock';
-import { RGBAMergerBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/rgbaMergerBlock';
-import { RGBASplitterBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/rgbaSplitterBlock';
-import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/textureBlock';
-import { BonesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/bonesBlock';
-import { InstancesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/instancesBlock';
-import { MorphTargetsBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/morphTargetsBlock';
-import { VertexOutputBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/vertexOutputBlock';
-import { FogBlock } from 'babylonjs/Materials/Node/Blocks/Dual/fogBlock';
-import { AddBlock } from 'babylonjs/Materials/Node/Blocks/addBlock';
-import { ClampBlock } from 'babylonjs/Materials/Node/Blocks/clampBlock';
-import { MultiplyBlock } from 'babylonjs/Materials/Node/Blocks/multiplyBlock';
-import { Vector2TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector2TransformBlock';
-import { Vector3TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector3TransformBlock';
-import { Vector4TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector4TransformBlock';
-import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
-import { LightBlock } from 'babylonjs/Materials/Node/Blocks/Dual/lightBlock';
+import { DraggableLineComponent } from '../../sharedComponents/draggableLineComponent';
 
 require("./nodeList.scss");
 
 interface INodeListComponentProps {
     globalState: GlobalState;
-    onAddValueNode: (b: string) => void;
-    onAddNodeFromClass: (ObjectClass: typeof NodeMaterialBlock) => void;
 }
 
 export class NodeListComponent extends React.Component<INodeListComponentProps> {
     render() {
         // Block types used to create the menu from
         const allBlocks = {
-            Vertex: [BonesBlock, InstancesBlock, MorphTargetsBlock],
-            Fragment: [AlphaTestBlock, , ImageProcessingBlock, RGBAMergerBlock, RGBASplitterBlock, TextureBlock, LightBlock],
-            Outputs: [VertexOutputBlock, FragmentOutputBlock],
-            Dual: [FogBlock],
-            Math: [AddBlock, ClampBlock, MultiplyBlock, Vector2TransformBlock, Vector3TransformBlock, Vector4TransformBlock],
+            Vertex: ["BonesBlock", "InstancesBlock", "MorphTargetsBlock"],
+            Fragment: ["AlphaTestBlock", "ImageProcessingBlock", "RGBAMergerBlock", "RGBASplitterBlock", "TextureBlock", "LightBlock", "FogBlock"],
+            Outputs: ["VertexOutputBlock", "FragmentOutputBlock"],
+            Math: ["AddBlock", "ClampBlock", "MultiplyBlock", "Vector2TransformBlock", "Vector3TransformBlock", "Vector4TransformBlock"],
             Inputs: ["Float", "Vector2", "Vector3", "Vector4", "Color3", "Color4", "Matrix"],
         }
 
         // Create node menu
         var blockMenu = []
         for (var key in allBlocks) {
-            var blockList = (allBlocks as any)[key].map((b: any) => {
-                var label = typeof b === "string" ? b : b.prototype.getClassName().replace("Block", "")
-                var onClick = typeof b === "string" ? () => {
-                    this.props.onAddValueNode(b);
-                    this.props.globalState.onUpdateRequiredObservable.notifyObservers();
-                } : () => { this.props.onAddNodeFromClass(b) };
-                return <ButtonLineComponent key={label} label={label} onClick={onClick} />
+            var blockList = (allBlocks as any)[key].map((block: any, i: number) => {
+                return <DraggableLineComponent key={block} data={block} />
             })
             blockMenu.push(
                 <LineContainerComponent key={key + " blocks"} title={key + " blocks"} closed={false}>

+ 127 - 41
nodeEditor/src/graphEditor.tsx

@@ -2,7 +2,6 @@ import {
     DiagramEngine,
     DiagramModel,
     DiagramWidget,
-    MoveCanvasAction,
     LinkModel
 } from "storm-react-diagrams";
 
@@ -31,6 +30,23 @@ import { LightNodeFactory } from './components/diagram/light/lightNodeFactory';
 import { DataStorage } from './dataStorage';
 import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPointTypes';
 import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { Nullable } from 'babylonjs/types';
+import { BonesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/bonesBlock';
+import { InstancesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/instancesBlock';
+import { MorphTargetsBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/morphTargetsBlock';
+import { AlphaTestBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/alphaTestBlock';
+import { ImageProcessingBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/imageProcessingBlock';
+import { RGBAMergerBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/rgbaMergerBlock';
+import { RGBASplitterBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/rgbaSplitterBlock';
+import { FogBlock } from 'babylonjs/Materials/Node/Blocks/Dual/fogBlock';
+import { VertexOutputBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/vertexOutputBlock';
+import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
+import { AddBlock } from 'babylonjs/Materials/Node/Blocks/addBlock';
+import { ClampBlock } from 'babylonjs/Materials/Node/Blocks/clampBlock';
+import { MultiplyBlock } from 'babylonjs/Materials/Node/Blocks/multiplyBlock';
+import { Vector2TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector2TransformBlock';
+import { Vector3TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector3TransformBlock';
+import { Vector4TransformBlock } from 'babylonjs/Materials/Node/Blocks/vector4TransformBlock';
 
 require("storm-react-diagrams/dist/style.min.css");
 require("./main.scss");
@@ -55,7 +71,6 @@ interface IGraphEditorProps {
 }
 
 export class NodeCreationOptions {
-    column: number;
     nodeMaterialBlock: NodeMaterialBlock;
     type?: string;
     connection?: NodeMaterialConnectionPoint;
@@ -71,22 +86,10 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
     public _toAdd: LinkModel[] | null = [];
 
     /**
-     * Current row/column position used when adding new nodes
-     */
-    private _rowPos = new Array<number>();
-
-    /**
      * Creates a node and recursivly creates its parent nodes from it's input
      * @param nodeMaterialBlock 
      */
     public createNodeFromObject(options: NodeCreationOptions) {
-        // Update rows/columns
-        if (this._rowPos[options.column] == undefined) {
-            this._rowPos[options.column] = 0;
-        } else {
-            this._rowPos[options.column]++;
-        }
-
         // Create new node in the graph
         var newNode: DefaultNodeModel;
         var filterInputs = [];
@@ -110,7 +113,6 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         }
 
         this._nodes.push(newNode)
-        newNode.setPosition(1600 - (300 * options.column), 210 * this._rowPos[options.column])
         this._model.addAll(newNode);
 
         if (options.nodeMaterialBlock) {
@@ -154,7 +156,6 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         });
 
         this.props.globalState.onResetRequiredObservable.add(() => {
-            this._rowPos = [];
             this.build();
             if (this.props.globalState.nodeMaterial) {
                 this.buildMaterial();
@@ -258,6 +259,8 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                     if (targetBlock && targetBlock.isFinalMerger) {
                         this.props.globalState.nodeMaterial!.removeOutputNode(targetBlock);
                     }
+
+                    this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
                 }
             },
             linksUpdated: (e) => {
@@ -275,11 +278,12 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                             }
                         }
                     }
+                    this.forceUpdate();
+                    return;
                 }
 
                 e.link.addListener({
                     sourcePortChanged: () => {
-                        console.log("port change")
                     },
                     targetPortChanged: () => {
                         // Link is created with a target port
@@ -287,7 +291,18 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
 
                         if (link) {
                             if (link.output.connection && link.input.connection) {
-                                link.output.connection.connectTo(link.input.connection)
+                                // Disconnect previous connection
+                                for (var key in link.input.links) {
+                                    let other = link.input.links[key];
+
+                                    if (other.getSourcePort() !== link.output) {
+                                        other.remove();
+                                    }
+                                }
+
+                                link.output.connection.connectTo(link.input.connection);
+
+                                this.forceUpdate();
                             }
                             if (this.props.globalState.nodeMaterial) {
                                 this.buildMaterial();
@@ -302,10 +317,10 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         if (this.props.globalState.nodeMaterial) {
             var material: any = this.props.globalState.nodeMaterial;
             material._vertexOutputNodes.forEach((n: any) => {
-                this.createNodeFromObject({ column: 0, nodeMaterialBlock: n });
+                this.createNodeFromObject({ nodeMaterialBlock: n });
             })
             material._fragmentOutputNodes.forEach((n: any) => {
-                this.createNodeFromObject({ column: 0, nodeMaterialBlock: n });
+                this.createNodeFromObject({ nodeMaterialBlock: n });
             })
         }
 
@@ -320,7 +335,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
             this.forceUpdate();
 
             this.reOrganize();
-        }, 550);
+        }, 500);
     }
 
     reOrganize() {
@@ -338,23 +353,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         this.forceUpdate();
     }
 
-    addNodeFromClass(ObjectClass: typeof NodeMaterialBlock) {
-        var block = new ObjectClass(ObjectClass.prototype.getClassName())
-        var localNode = this.createNodeFromObject({ column: 0, nodeMaterialBlock: block })
-        var widget = (this.refs["test"] as DiagramWidget);
-
-        this.forceUpdate();
-
-        // This is needed to fix link offsets when created, (eg. create a fog block)
-        // Todo figure out how to correct this without this
-        setTimeout(() => {
-            widget.startFiringAction(new MoveCanvasAction(1, 0, this._model));
-        }, 500);
-
-        return localNode;
-    }
-
-    addValueNode(type: string, column = 0, connection?: NodeMaterialConnectionPoint) {
+    addValueNode(type: string) {
         let nodeType: NodeMaterialBlockConnectionPointTypes = NodeMaterialBlockConnectionPointTypes.Vector3;
         switch (type) {
             case "Float":
@@ -382,7 +381,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
 
         let newInputBlock = new InputBlock(type, undefined, nodeType);
         newInputBlock.setDefaultValue();
-        var localNode = this.createNodeFromObject({ column: column, type: type, connection: connection, nodeMaterialBlock: newInputBlock })
+        var localNode = this.createNodeFromObject({ type: type, nodeMaterialBlock: newInputBlock })
 
         return localNode;
     }
@@ -432,6 +431,84 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         return `${this._leftWidth}px 4px calc(100% - ${this._leftWidth + 8 + this._rightWidth}px) 4px ${this._rightWidth}px`;
     }
 
+    emitNewBlock(event: React.DragEvent<HTMLDivElement>) {
+        var data = event.dataTransfer.getData("babylonjs-material-node") as string;
+
+        if (data.indexOf("Block") === -1) {
+            this.addValueNode(data);
+        } else {
+            let block: Nullable<NodeMaterialBlock> = null;
+
+            switch (data) {
+                case "BonesBlock":
+                    block = new BonesBlock("Bones");
+                    break;
+                case "InstancesBlock":
+                    block = new InstancesBlock("Instances");
+                    break;
+                case "MorphTargetsBlock":
+                    block = new MorphTargetsBlock("MorphTargets");
+                    break;
+                case "AlphaTestBlock":
+                    block = new AlphaTestBlock("AlphaTest");
+                    break;
+                case "ImageProcessingBlock":
+                    block = new ImageProcessingBlock("ImageProcessing");
+                    break;
+                case "RGBAMergerBlock":
+                    block = new RGBAMergerBlock("RGBAMerger");
+                    break;
+                case "RGBASplitterBlock":
+                    block = new RGBASplitterBlock("RGBASplitter");
+                    break;
+                case "TextureBlock":
+                    block = new TextureBlock("Texture");
+                    break;
+                case "LightBlock":
+                    block = new LightBlock("Light");
+                    break;
+                case "FogBlock":
+                    block = new FogBlock("Fog");
+                    break;
+                case "VertexOutputBlock":
+                    block = new VertexOutputBlock("VertexOutput");
+                    break;
+                case "FragmentOutputBlock":
+                    block = new FragmentOutputBlock("FragmentOutput");
+                    break;
+                case "AddBlock":
+                    block = new AddBlock("Add");
+                    break;
+                case "ClampBlock":
+                    block = new ClampBlock("Clamp");
+                    break;
+                case "MultiplyBlock":
+                    block = new MultiplyBlock("Multiply");
+                    break;
+                case "Vector2TransformBlock":
+                    block = new Vector2TransformBlock("Vector2Transform");
+                    break;
+                case "Vector3TransformBlock":
+                    block = new Vector3TransformBlock("Vector3Transform");
+                    break;
+                case "Vector4TransformBlock":
+                    block = new Vector4TransformBlock("Vector4Transform");
+                    break;
+            }
+
+            if (block) {
+                let nodeModel = this.createNodeFromObject({ nodeMaterialBlock: block });
+                const zoomLevel = this._engine.diagramModel.getZoomLevel() / 100.0;
+
+                let x = (event.clientX - event.currentTarget.offsetLeft - this._engine.diagramModel.getOffsetX()) / zoomLevel;
+                let y = (event.clientY - event.currentTarget.offsetTop - this._engine.diagramModel.getOffsetY()) / zoomLevel;
+                nodeModel.setPosition(x, y);
+            }
+        };
+
+        this.forceUpdate();
+    }
+
     render() {
         return (
             <Portal globalState={this.props.globalState}>
@@ -441,7 +518,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                     }
                 }>
                     {/* Node creation menu */}
-                    <NodeListComponent globalState={this.props.globalState} onAddValueNode={b => this.addValueNode(b)} onAddNodeFromClass={b => this.addNodeFromClass(b)} />
+                    <NodeListComponent globalState={this.props.globalState} />
 
                     <div id="leftGrab"
                         onPointerDown={evt => this.onPointerDown(evt)}
@@ -450,7 +527,16 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                     ></div>
 
                     {/* The node graph diagram */}
-                    <DiagramWidget deleteKeys={[46]} ref={"test"} inverseZoom={true} className="diagram-container" diagramEngine={this._engine} maxNumberPointsPerLink={0} />
+                    <div className="diagram-container"
+                        onDrop={event => {
+                            this.emitNewBlock(event);
+                        }}
+                        onDragOver={event => {
+                            event.preventDefault();
+                        }}
+                    >
+                        <DiagramWidget className="diagram" deleteKeys={[46]} ref={"test"} inverseZoom={true} diagramEngine={this._engine} maxNumberPointsPerLink={0} />
+                    </div>
 
                     <div id="rightGrab"
                         onPointerDown={evt => this.onPointerDown(evt)}

+ 7 - 0
nodeEditor/src/main.scss

@@ -28,6 +28,13 @@
     grid-row: 1;
     grid-column: 3;
     background: #222222;
+    width: 100%;
+    height: 100%;
+
+    .diagram {
+        width: 100%;
+        height: 100%;
+    }
 }
 
 #propertyTab {

+ 23 - 0
nodeEditor/src/sharedComponents/draggableLineComponent.tsx

@@ -0,0 +1,23 @@
+import * as React from "react";
+
+export interface IButtonLineComponentProps {
+    data: string;
+}
+
+export class DraggableLineComponent extends React.Component<IButtonLineComponentProps> {
+    constructor(props: IButtonLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <div className="draggableLine"
+                draggable={true}
+                onDragStart={event => {
+                    event.dataTransfer.setData("babylonjs-material-node", this.props.data);
+                }}>
+                {this.props.data}
+            </div>
+        );
+    }
+}