David Catuhe 5 gadi atpakaļ
vecāks
revīzija
83217e369b

+ 5 - 0
dist/preview release/babylon.d.ts

@@ -53959,6 +53959,11 @@ declare module BABYLON {
         private _initializeBlock;
         private _resetDualBlocks;
         /**
+         * Remove a block from the current node material
+         * @param block defines the block to remove
+         */
+        removeBlock(block: NodeMaterialBlock): void;
+        /**
          * Build the material and generates the inner effect
          * @param verbose defines if the build should log activity
          */

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
dist/preview release/babylon.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 59 - 46
dist/preview release/babylon.max.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
dist/preview release/babylon.max.js.map


+ 10 - 0
dist/preview release/babylon.module.d.ts

@@ -56509,6 +56509,11 @@ declare module "babylonjs/Materials/Node/nodeMaterial" {
         private _initializeBlock;
         private _resetDualBlocks;
         /**
+         * Remove a block from the current node material
+         * @param block defines the block to remove
+         */
+        removeBlock(block: NodeMaterialBlock): void;
+        /**
          * Build the material and generates the inner effect
          * @param verbose defines if the build should log activity
          */
@@ -122672,6 +122677,11 @@ declare module BABYLON {
         private _initializeBlock;
         private _resetDualBlocks;
         /**
+         * Remove a block from the current node material
+         * @param block defines the block to remove
+         */
+        removeBlock(block: NodeMaterialBlock): void;
+        /**
          * Build the material and generates the inner effect
          * @param verbose defines if the build should log activity
          */

+ 167 - 149
nodeEditor/src/diagram/graphCanvas.scss

@@ -8,7 +8,7 @@
     user-select: none;
     overflow: hidden;
 
-    #graph-canvas-container {
+    #graph-container {
         position: absolute;
         width: 100%;
         height: 100%;
@@ -16,187 +16,205 @@
         top: 0;
         transform-origin: left top;
 
-        .visual {
-            width: 200px;
+        #graph-svg-container {
+            z-index: -10;
+            position:absolute;
+            width: 100%;
+            height: 100%;  
+            overflow: visible;  
+        }
+
+        #graph-canvas-container {
             position: absolute;
+            width: 100%;
+            height: 100%;
             left: 0;
             top: 0;
-            background: gray;
-            border: 4px solid black;
-            border-radius: 12px;
-            display: grid;
-            grid-template-rows: 30px auto;
-            grid-template-columns: 100%;
-            color: white;
-
-            &.selected {
-                border-color: white;
-            }
 
-            .header {
-                grid-row: 1;
-                grid-column: 1;
+            .visual {
+                width: 200px;
+                position: absolute;
+                left: 0;
+                top: 0;
+                background: gray;
                 border: 4px solid black;
-                border-top-right-radius: 7px;
-                border-top-left-radius: 7px;
-                font-size: 16px;
-                text-align: center;
-                margin-top: -1px;
-                white-space: nowrap;
-                text-overflow: ellipsis;
-                overflow: hidden;
-                background: black;
-                color: white;
-
-                &.constant {
-                    border-color: #4E5C74;
-                    background: #4E5C74;
-                }
-        
-                &.inspector {
-                    border-color: #396437;
-                    background: #396437;
-                }
-            }
-
-            .connections {
-                grid-row: 2;
-                grid-column: 1;
-
+                border-radius: 12px;
                 display: grid;
-                grid-template-columns: 50% 50%;
-
-                .port {
-                    border-radius: 20px;
-                    width: 20px;
-                    height: 20px;
-                }
+                grid-template-rows: 30px auto;
+                grid-template-columns: 100%;
+                color: white;
 
-                .portLine {
-                    height: 24px;
-                    display: grid;                
-                    grid-template-rows: 100%;
+                &.selected {
+                    border-color: white;
                 }
 
-                .label {                   
-                    align-items: center;
-                }
+                .header {
+                    grid-row: 1;
+                    grid-column: 1;
+                    border: 4px solid black;
+                    border-top-right-radius: 7px;
+                    border-top-left-radius: 7px;
+                    font-size: 16px;
+                    text-align: center;
+                    margin-top: -1px;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                    overflow: hidden;
+                    background: black;
+                    color: white;
 
-                .port {                              
-                    align-self: center;   
-                    .img {
-                        width: 100%;
-                    }            
+                    &.constant {
+                        border-color: #4E5C74;
+                        background: #4E5C74;
+                    }
+            
+                    &.inspector {
+                        border-color: #396437;
+                        background: #396437;
+                    }
                 }
 
-                .inputsContainer {                        
-                    grid-row: 1;
+                .connections {
+                    grid-row: 2;
                     grid-column: 1;
 
-                    .portLine {
-                        grid-template-columns: 12px calc(100% - 15px);
-
-                        .label {                    
-                            grid-row: 1;
-                            grid-column: 2;
+                    display: grid;
+                    grid-template-columns: 50% 50%;
+
+                    .port {
+                        border-radius: 20px;
+                        width: 20px;
+                        height: 20px;                                                    
+                        align-self: center;   
+                        
+                        .img {
+                            width: 100%;
+                        }     
+                        
+                        &:hover {
+                            filter: brightness(2);
                         }
+                    }
 
-                        .port {                              
-                            grid-row: 1;
-                            grid-column: 1;
-                            transform: translateX(-12px);     
-                        }
+                    .portLine {
+                        height: 24px;
+                        display: grid;                
+                        grid-template-rows: 100%;
                     }
-                }
 
-                .outputsContainer {                        
-                    grid-row: 1;
-                    grid-column: 2;
+                    .label {                   
+                        align-items: center;
+                    }
 
-                    .portLine {
-                        grid-template-columns: calc(100% - 10px) 12px;
+                    .inputsContainer {                        
+                        grid-row: 1;
+                        grid-column: 1;
+
+                        .portLine {
+                            grid-template-columns: 12px calc(100% - 15px);
 
-                        .label {                    
-                            grid-row: 1;
-                            grid-column: 1;
-                            text-align: right;
+                            .label {                    
+                                grid-row: 1;
+                                grid-column: 2;
+                            }
+
+                            .port {                              
+                                grid-row: 1;
+                                grid-column: 1;
+                                transform: translateX(-12px);     
+                            }
                         }
+                    }
 
-                        .port {                              
-                            grid-row: 1;
-                            grid-column: 2;
-                            transform: translateX(2px);                        
+                    .outputsContainer {                        
+                        grid-row: 1;
+                        grid-column: 2;
+
+                        .portLine {
+                            grid-template-columns: calc(100% - 10px) 12px;
+
+                            .label {                    
+                                grid-row: 1;
+                                grid-column: 1;
+                                text-align: right;
+                            }
+
+                            .port {                              
+                                grid-row: 1;
+                                grid-column: 2;
+                                transform: translateX(2px);                        
+                            }
+                        
                         }
-                    
                     }
                 }
-            }
 
-            .content {
-                min-height: 20px;
-                grid-row: 3;
-                grid-column: 1;
+                .content {
+                    min-height: 20px;
+                    grid-row: 3;
+                    grid-column: 1;
 
-                &.input-block {
-                    grid-row: 2;
-                    height: 34px;
-                    text-align: center;
-                    font-size: 18px;
-                    font-weight: bold;
-                    margin: 0 10px;
-                }
+                    &.input-block {
+                        grid-row: 2;
+                        height: 34px;
+                        text-align: center;
+                        font-size: 18px;
+                        font-weight: bold;
+                        margin: 0 10px;
+                    }
 
-                &.output-block {
-                    min-height: 0px;
-                    height: 5px;
-                }
+                    &.output-block {
+                        min-height: 0px;
+                        height: 5px;
+                    }
 
-                &.clamp-block {                    
-                    grid-row: 2;
-                    height: 34px;
-                    text-align: center;
-                    font-size: 18px;
-                    font-weight: bold;
-                    margin: 0 10px;
-                }
+                    &.clamp-block {                    
+                        grid-row: 2;
+                        height: 34px;
+                        text-align: center;
+                        font-size: 18px;
+                        font-weight: bold;
+                        margin: 0 10px;
+                    }
 
-                &.gradient-block {                    
-                    grid-row: 2;
-                    height: 34px;
-                }
+                    &.gradient-block {                    
+                        grid-row: 2;
+                        height: 34px;
+                    }
 
-                &.texture-block {                    
-                    grid-row: 3;
-                    height: 140px;
-                    width: 140px;
-                    margin-top: -115px;
-                    overflow: hidden;
-                    border-bottom-left-radius: 7px;
-                    border: black 4px solid;
-                    border-left: 0px;
-                    border-bottom: 0px;
-
-                    canvas {
-                        width: 100%;
-                        height: 100%;
+                    &.texture-block {                    
+                        grid-row: 3;
+                        height: 140px;
+                        width: 140px;
+                        margin-top: -115px;
+                        overflow: hidden;
+                        border-bottom-left-radius: 7px;
+                        border: black 4px solid;
+                        border-left: 0px;
+                        border-bottom: 0px;
+
+                        canvas {
+                            width: 100%;
+                            height: 100%;
+                        }
                     }
-                }
 
-                &.remap-block {                    
-                    height: 34px;
-                    text-align: center;
-                    font-size: 18px;
-                    font-weight: bold;
-                    margin: 0 10px;
-                }      
-                
-                &.trigonometry-block {                    
-                    grid-row: 2;
-                    height: 34px;
-                    text-align: center;
-                    font-size: 18px;
-                    font-weight: bold;
-                    margin: 0 10px;
+                    &.remap-block {                    
+                        height: 34px;
+                        text-align: center;
+                        font-size: 18px;
+                        font-weight: bold;
+                        margin: 0 10px;
+                    }      
+                    
+                    &.trigonometry-block {                    
+                        grid-row: 2;
+                        height: 34px;
+                        text-align: center;
+                        font-size: 18px;
+                        font-weight: bold;
+                        margin: 0 10px;
+                    }
                 }
             }
         }

+ 99 - 15
nodeEditor/src/diagram/graphCanvas.tsx

@@ -3,7 +3,9 @@ import { GlobalState } from '../globalState';
 import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
 import { GraphNode } from './graphNode';
 import * as dagre from 'dagre';
-import { Nullable } from 'babylonjs';
+import { Nullable } from 'babylonjs/types';
+import { NodeMaterialConnectionPoint } from 'babylonjs';
+import { NodeLink } from './nodeLink';
 
 require("./graphCanvas.scss");
 
@@ -12,25 +14,33 @@ export interface IGraphCanvasComponentProps {
 }
 
 export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentProps> {
-    private _rootCanvas: HTMLDivElement;
+    private _graphCanvas: HTMLDivElement;
+    private _svgCanvas: HTMLElement;
+    private _rootContainer: HTMLDivElement;
     private _nodes: GraphNode[] = [];
+    private _links: NodeLink[] = [];
     private _mouseStartPointX: Nullable<number> = null;
     private _mouseStartPointY: Nullable<number> = null
     private _x = 0;
     private _y = 0;
     private _zoom = 1;
+    private _selectedNodes: GraphNode[] = [];
 
     public get nodes() {
         return this._nodes;
     }
 
+    public get links() {
+        return this._links;
+    }
+
     public get zoom() {
         return this._zoom;
     }
 
     public set zoom(value: number) {
         this._zoom = value;
-        this._rootCanvas.style.transform = `scale(${value})`;
+        this._rootContainer.style.transform = `scale(${value})`;
     }    
 
     public get x() {
@@ -39,7 +49,7 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
 
     public set x(value: number) {
         this._x = value;
-        this._rootCanvas.style.left = `${value}px`;
+        this._rootContainer.style.left = `${value}px`;
     }
 
     public get y() {
@@ -48,22 +58,90 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
 
     public set y(value: number) {
         this._y = value;
-        this._rootCanvas.style.top = `${value}px`;
+        this._rootContainer.style.top = `${value}px`;
+    }
+
+    public get selectedNodes() {
+        return this._selectedNodes;
+    }
+
+    public get canvasContainer() {
+        return this._graphCanvas;
     }
 
     constructor(props: IGraphCanvasComponentProps) {
         super(props);
+
+        props.globalState.onSelectionChangedObservable.add(node => {
+            if (!node) {
+                this._selectedNodes = [];
+            } else {
+                this._selectedNodes = [node];
+            }
+        });
+    }
+
+    findNodeFromBlock(block: NodeMaterialBlock) {
+        return this.nodes.filter(n => n.block === block)[0];
     }
 
     reset() {
+        for (var node of this._nodes) {
+            node.dispose();
+        }
         this._nodes = [];
-        this._rootCanvas.innerHTML = "";
+        this._links = [];
+        this._graphCanvas.innerHTML = "";
+        this._svgCanvas.innerHTML = "";
+    }
+
+    connectPorts(pointA: NodeMaterialConnectionPoint, pointB: NodeMaterialConnectionPoint) {
+        var blockA = pointA.ownerBlock;
+        var blockB = pointB.ownerBlock;
+        var nodeA = this.findNodeFromBlock(blockA);
+        var nodeB = this.findNodeFromBlock(blockB);
+
+        if (!nodeA || !nodeB) {
+            return;
+        }
+
+        var portA = nodeA.getPortForConnectionPoint(pointA);
+        var portB = nodeB.getPortForConnectionPoint(pointB);
+
+        if (!portA || !portB) {
+            return;
+        }
+
+        for (var currentLink of this._links) {
+            if (currentLink.portA === portA && currentLink.portB === portB) {
+                return;
+            }
+            if (currentLink.portA === portB && currentLink.portB === portA) {
+                return;
+            }
+        }
+
+        const link = new NodeLink(this, portA, nodeA, portB, nodeB);
+        this._links.push(link);
+
+        nodeA.links.push(link);
+        nodeB.links.push(link);
+    }
+
+    removeLink(link: NodeLink) {
+        let index = this._links.indexOf(link);
+
+        if (index > -1) {
+            this._links.splice(index, 1);
+        }
+
+        link.dispose();
     }
 
     appendBlock(block: NodeMaterialBlock) {
         let newNode = new GraphNode(block, this.props.globalState);
 
-        newNode.appendVisual(this._rootCanvas, this);
+        newNode.appendVisual(this._graphCanvas, this);
 
         this._nodes.push(newNode);
 
@@ -118,11 +196,13 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     }
 
     componentDidMount() {
-        this._rootCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas-container") as HTMLDivElement;
+        this._rootContainer = this.props.globalState.hostDocument.getElementById("graph-container") as HTMLDivElement;
+        this._graphCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas-container") as HTMLDivElement;
+        this._svgCanvas = this.props.globalState.hostDocument.getElementById("graph-svg-container") as HTMLElement;
     }    
 
     onMove(evt: React.PointerEvent) {        
-        this._rootCanvas.style.cursor = "move";
+        this._rootContainer.style.cursor = "move";
 
         if (this._mouseStartPointX === null || this._mouseStartPointY === null) {
             return;
@@ -138,13 +218,13 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
         this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
         this._mouseStartPointX = evt.clientX;
         this._mouseStartPointY = evt.clientY;
-        this._rootCanvas.setPointerCapture(evt.pointerId);
+        this._rootContainer.setPointerCapture(evt.pointerId);
     }
 
     onUp(evt: React.PointerEvent) {
         this._mouseStartPointX = null;
         this._mouseStartPointY = null;
-        this._rootCanvas.releasePointerCapture(evt.pointerId);
+        this._rootContainer.releasePointerCapture(evt.pointerId);
     }
 
     onWheel(evt: React.WheelEvent) {
@@ -162,8 +242,8 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     }
 
     zoomToFit() {
-        const xFactor = this._rootCanvas.clientWidth / this._rootCanvas.scrollWidth;
-        const yFactor = this._rootCanvas.clientHeight / this._rootCanvas.scrollHeight;
+        const xFactor = this._rootContainer.clientWidth / this._rootContainer.scrollWidth;
+        const yFactor = this._rootContainer.clientHeight / this._rootContainer.scrollHeight;
         const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
         
         this.zoom = zoomFactor;
@@ -179,8 +259,12 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
                 onPointerDown={evt =>  this.onDown(evt)}   
                 onPointerUp={evt =>  this.onUp(evt)}   
             >    
-                <div id="graph-canvas-container">
-                </div>     
+                <div id="graph-container">
+                    <div id="graph-canvas-container">
+                    </div>     
+                    <svg id="graph-svg-container">
+                    </svg>
+                </div>
             </div>
         );
     }

+ 100 - 4
nodeEditor/src/diagram/graphNode.ts

@@ -11,6 +11,11 @@ import * as React from 'react';
 import { GenericPropertyTabComponent } from './properties/genericNodePropertyComponent';
 import { DisplayLedger } from './displayLedger';
 import { IDisplayManager } from './display/displayManager';
+import { NodeLink } from './nodeLink';
+
+export class ExtendedHTMLDivElement extends HTMLDivElement {
+    tag: NodeMaterialConnectionPoint;
+}
 
 export class GraphNode {
     private _visual: HTMLDivElement;
@@ -19,8 +24,9 @@ export class GraphNode {
     private _inputsContainer: HTMLDivElement;
     private _outputsContainer: HTMLDivElement;
     private _content: HTMLDivElement;
-    private _inputPorts: HTMLDivElement[] = [];
-    private _outputPorts: HTMLDivElement[] = [];
+    private _inputPorts: ExtendedHTMLDivElement[] = [];
+    private _outputPorts: ExtendedHTMLDivElement[] = [];
+    private _links: NodeLink[] = [];    
     private _x = 0;
     private _y = 0;
     private _mouseStartPointX: Nullable<number> = null;
@@ -31,14 +37,25 @@ export class GraphNode {
     private _ownerCanvas: GraphCanvasComponent; 
     private _isSelected: boolean;
     private _displayManager: Nullable<IDisplayManager> = null;
+    private _candidateLink: Nullable<NodeLink> = null;
+
+    public get links() {
+        return this._links;
+    }
 
     public get x() {
         return this._x;
     }
 
     public set x(value: number) {
+        if (this._x === value) {
+            return;
+        }
+        
         this._x = value;
         this._visual.style.left = `${value}px`;
+
+        this._refreshLinks();
     }
 
     public get y() {
@@ -46,8 +63,14 @@ export class GraphNode {
     }
 
     public set y(value: number) {
+        if (this._y === value) {
+            return;
+        }
+
         this._y = value;
         this._visual.style.top = `${value}px`;
+
+        this._refreshLinks();
     }
 
     public get width() {
@@ -100,6 +123,32 @@ export class GraphNode {
         });
     }
 
+    public getPortForConnectionPoint(point: NodeMaterialConnectionPoint) {
+        for (var port of this._inputPorts) {
+            let attachedPoint = (port as any).tag as NodeMaterialConnectionPoint;
+
+            if (attachedPoint === point) {
+                return port;
+            }
+        }
+
+        for (var port of this._outputPorts) {
+            let attachedPoint = (port as any).tag as NodeMaterialConnectionPoint;
+
+            if (attachedPoint === point) {
+                return port;
+            }
+        }
+
+        return null;
+    }
+
+    private _refreshLinks() {
+        for (var link of this._links) {
+            link.update();
+        }
+    }
+
     private _refresh() {
         if (this._displayManager) {
             this._header.innerHTML = this._displayManager.getHeaderText(this.block);
@@ -110,6 +159,34 @@ export class GraphNode {
         }
     }
 
+    private _onPortDown(evt: PointerEvent, port: ExtendedHTMLDivElement) {
+        if (!this._candidateLink) {
+            this._candidateLink = new NodeLink(this._ownerCanvas, port, this);
+        }        
+        port.setPointerCapture(evt.pointerId);
+        evt.stopPropagation();
+    }
+
+    private _onPortUp(evt: PointerEvent, port: ExtendedHTMLDivElement) {        
+        port.releasePointerCapture(evt.pointerId);
+        evt.stopPropagation();
+
+        if (this._candidateLink) {
+            this._candidateLink.dispose();
+            this._candidateLink = null;
+        }
+    }
+
+    private _onPortMove(evt: PointerEvent, port: ExtendedHTMLDivElement) {       
+        if (!this._candidateLink) {
+            return;
+        }
+
+        const rootRect = this._ownerCanvas.canvasContainer.getBoundingClientRect();
+
+        this._candidateLink.update((evt.pageX - rootRect.left) / this._ownerCanvas.zoom, (evt.pageY - rootRect.top) / this._ownerCanvas.zoom, true);
+    }
+
     private _appendConnection(connectionPoint: NodeMaterialConnectionPoint, root: HTMLDivElement, displayManager: Nullable<IDisplayManager>) {
         let portContainer = root.ownerDocument!.createElement("div");
         portContainer.classList.add("portLine");
@@ -122,7 +199,7 @@ export class GraphNode {
             portContainer.appendChild(portLabel);
         }
 
-        let port = root.ownerDocument!.createElement("div");
+        let port = root.ownerDocument!.createElement("div") as ExtendedHTMLDivElement;
         port.classList.add("port");     
         port.style.background = BlockTools.GetColorFromConnectionNodeType(connectionPoint.type);
         portContainer.appendChild(port);
@@ -150,7 +227,16 @@ export class GraphNode {
         }
         port.appendChild(portImg);
 
-        return portContainer;
+        port.tag = connectionPoint;
+
+        // Drag support
+        port.ondragstart= () => false;
+
+        port.addEventListener("pointerdown", evt => this._onPortDown(evt, port));
+        port.addEventListener("pointerup", evt => this._onPortUp(evt, port));
+        port.addEventListener("pointermove", evt => this._onPortMove(evt, port));
+
+        return port;
     }
 
     private _onDown(evt: PointerEvent) {
@@ -267,5 +353,15 @@ export class GraphNode {
         if (this._onUpdateRequiredObserver) {
             this._globalState.onUpdateRequiredObservable.remove(this._onUpdateRequiredObserver);
         }
+
+        if (this._visual.parentElement) {
+            this._visual.parentElement.removeChild(this._visual);
+        }
+
+        for (var link of this._links) {
+            link.dispose();           
+        }
+
+        this.block.dispose();
     }
 }

+ 80 - 0
nodeEditor/src/diagram/nodeLink.ts

@@ -0,0 +1,80 @@
+import { GraphCanvasComponent } from './graphCanvas';
+import { GraphNode } from './graphNode';
+
+export class NodeLink {   
+    private _graphCanvas: GraphCanvasComponent;
+    private _portA: HTMLDivElement;
+    private _portB?: HTMLDivElement;
+    private _nodeA: GraphNode;
+    private _nodeB?: GraphNode;
+    private _path: SVGPathElement;
+
+    public get portA() {
+        return this._portA;
+    }
+
+    public get portB() {
+        return this._portB;
+    }
+
+    public update(endX = 0, endY = 0, straight = false) {   
+        const rectA = this._portA.getBoundingClientRect();
+        const rootRect = this._graphCanvas.canvasContainer.getBoundingClientRect();
+        const zoom = this._graphCanvas.zoom;
+        const xOffset = rootRect.left;
+        const yOffset = rootRect.top;
+       
+        var startX = (rectA.left - xOffset + 0.5 * rectA.width) / zoom;
+        var startY = (rectA.top - yOffset + 0.5 * rectA.height) / zoom;  
+
+        if (this._portB) {
+            const rectB = this._portB.getBoundingClientRect();
+            endX = (rectB.left - xOffset + 0.5 * rectB.width) / zoom;
+            endY = (rectB.top - yOffset + 0.5 * rectB.height) / zoom;  
+        } else {
+
+        }
+    
+        if (straight) {
+            this._path.setAttribute("d",  `M${startX},${startY} L${endX},${endY}`);      
+            this._path.setAttribute("stroke-dasharray", "10, 10");
+            this._path.setAttribute("stroke-linecap", "round");
+        } else {
+            this._path.setAttribute("d",  `M${startX},${startY} C${startX + 80},${startY} ${endX - 80},${endY} ${endX},${endY}`);        
+        }
+        this._path.setAttribute("stroke", this._portA.style.backgroundColor!);
+    }
+
+    public constructor(graphCanvas: GraphCanvasComponent, portA: HTMLDivElement, nodeA: GraphNode, portB?: HTMLDivElement, nodeB?: GraphNode) {
+        this._portA = portA;
+        this._portB = portB;
+        this._nodeA = nodeA;
+        this._nodeB = nodeB;
+        this._graphCanvas = graphCanvas;
+
+        var document = portA.ownerDocument!;
+        var svg = document.getElementById("graph-svg-container")!;
+
+        // Create path
+        this._path = document.createElementNS('http://www.w3.org/2000/svg',"path"); 
+        this._path.setAttribute("fill", "none");
+        this._path.setAttribute("stroke-width", "4px");
+
+        svg.appendChild(this._path);
+
+        // Update
+        this.update();
+    }
+
+    public dispose() {
+        if (this._path.parentElement) {
+            this._path.parentElement.removeChild(this._path);
+        }
+
+        if (this._nodeB) {
+            this._nodeA.links.splice(this._nodeA.links.indexOf(this), 1);
+            this._nodeB.links.splice(this._nodeB.links.indexOf(this), 1);
+            this._graphCanvas.links.splice(this._graphCanvas.links.indexOf(this), 1);
+        }
+    }   
+}

+ 1 - 1
nodeEditor/src/diagram/properties/lightPropertyTabComponent.tsx

@@ -5,7 +5,7 @@ import { LineContainerComponent } from '../../sharedComponents/lineContainerComp
 import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
 import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
 import { IPropertyComponentProps } from './propertyComponentProps';
-import { LightBlock } from 'babylonjs';
+import { LightBlock } from 'babylonjs/Materials/Node/Blocks/Dual/lightBlock';
 
 export class LightPropertyTabComponent extends React.Component<IPropertyComponentProps> {
 

+ 141 - 257
nodeEditor/src/graphEditor.tsx

@@ -8,7 +8,6 @@ import * as React from "react";
 import { GlobalState } from './globalState';
 
 import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
-import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
 import { NodeListComponent } from './components/nodeList/nodeListComponent';
 import { PropertyTabComponent } from './components/propertyTab/propertyTabComponent';
 import { Portal } from './portal';
@@ -33,12 +32,6 @@ interface IGraphEditorProps {
     globalState: GlobalState;
 }
 
-export class NodeCreationOptions {
-    nodeMaterialBlock: NodeMaterialBlock;
-    type?: string;
-    connection?: NodeMaterialConnectionPoint;
-}
-
 export class GraphEditor extends React.Component<IGraphEditorProps> {
     private readonly NodeWidth = 100;
     private _graphCanvas: GraphCanvasComponent;
@@ -53,9 +46,9 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
     private _blocks = new Array<NodeMaterialBlock>();
 
     private _previewManager: PreviewManager;
-    // private _copiedNodes: GraphNode[] = [];
-    // private _mouseLocationX = 0;
-    // private _mouseLocationY = 0;
+    private _copiedNodes: GraphNode[] = [];
+    private _mouseLocationX = 0;
+    private _mouseLocationY = 0;
     private _onWidgetKeyUpPointer: any;
 
     private _altKeyIsPressed = false;
@@ -68,30 +61,50 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
      * Creates a node and recursivly creates its parent nodes from it's input
      * @param nodeMaterialBlock 
      */
-    public createNodeFromObject(options: NodeCreationOptions) {
-        if (this._blocks.indexOf(options.nodeMaterialBlock) !== -1) {        
-            return this._graphCanvas.nodes.filter(n => n.block === options.nodeMaterialBlock)[0];
+    public createNodeFromObject(block: NodeMaterialBlock) {
+        if (this._blocks.indexOf(block) !== -1) {        
+            return this._graphCanvas.nodes.filter(n => n.block === block)[0];
         }
 
-        this._blocks.push(options.nodeMaterialBlock);
+        this._blocks.push(block);
+
+        if (this.props.globalState.nodeMaterial!.attachedBlocks.indexOf(block) === -1) {
+            this.props.globalState.nodeMaterial!.attachedBlocks.push(block);
+        }
 
-        if (this.props.globalState.nodeMaterial!.attachedBlocks.indexOf(options.nodeMaterialBlock) === -1) {
-            this.props.globalState.nodeMaterial!.attachedBlocks.push(options.nodeMaterialBlock);
+        if (block.isFinalMerger) {
+            this.props.globalState.nodeMaterial!.addOutputNode(block);
         }
 
-        if (options.nodeMaterialBlock.isFinalMerger) {
-            this.props.globalState.nodeMaterial!.addOutputNode(options.nodeMaterialBlock);
+        // Connections
+        if (block.inputs.length) {
+            for (var input of block.inputs) {
+                if (input.isConnected) {
+                    this.createNodeFromObject(input.sourceBlock!);
+                }
+            }
         }
 
         // Graph
-        return this._graphCanvas.appendBlock(options.nodeMaterialBlock);
+        const node = this._graphCanvas.appendBlock(block);
+
+        // Links
+        if (block.inputs.length) {
+            for (var input of block.inputs) {
+                if (input.isConnected) {
+                    this._graphCanvas.connectPorts(input.connectedPoint!, input);
+                }
+            }
+        }
+
+        return node;
     }
     
     addValueNode(type: string) {
         let nodeType: NodeMaterialBlockConnectionPointTypes = BlockTools.GetConnectionNodeTypeFromString(type);
 
         let newInputBlock = new InputBlock(type, undefined, nodeType);
-        return this.createNodeFromObject({ type: type, nodeMaterialBlock: newInputBlock })
+        return this.createNodeFromObject(newInputBlock)
     }
 
     onWidgetKeyUp(evt: any) {        
@@ -195,72 +208,95 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         });
 
         this.props.globalState.onGetNodeFromBlock = (block) => {
-             return this._graphCanvas.nodes.filter(n => n.block === block)[0];
+             return this._graphCanvas.findNodeFromBlock(block);
         }
 
         this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
             this._altKeyIsPressed = evt.altKey;
 
+            if (evt.keyCode === 46) { // Delete                
+                let selectedItems = this._graphCanvas.selectedNodes;
+                if (!selectedItems.length) {
+                    return;
+                }
+
+                for (var selectedItem of selectedItems) {
+                    selectedItem.dispose();
+
+                    let targetBlock = selectedItem.block;
+                    this.props.globalState.nodeMaterial!.removeBlock(targetBlock);
+                    let blockIndex = this._blocks.indexOf(targetBlock);
+
+                    if (blockIndex > -1) {
+                        this._blocks.splice(blockIndex, 1);
+                    }
+                                  
+                }
+
+                this.props.globalState.onSelectionChangedObservable.notifyObservers(null);  
+                return;
+            }
+
             if (!evt.ctrlKey) {
                 return;
             }
 
-            // if (evt.key === "c") {
-            //     let selectedItems = this._engine.diagramModel.getSelectedItems();
-            //     if (!selectedItems.length) {
-            //         return;
-            //     }
+            if (evt.key === "c") { // Copy
+                let selectedItems = this._graphCanvas.selectedNodes;
+                if (!selectedItems.length) {
+                    return;
+                }
     
-            //     let selectedItem = selectedItems[0] as DefaultNodeModel;
+                let selectedItem = selectedItems[0] as GraphNode;
     
-            //     if (!selectedItem.block) {
-            //         return;
-            //     }
-
-            //     this._copiedNodes = selectedItems.map(i => (i as DefaultNodeModel)!);
-            // } else if (evt.key === "v") {
-            //     if (!this._copiedNodes.length) {
-            //         return;
-            //     }
-
-            //     const rootElement = this.props.globalState.hostDocument!.querySelector(".diagram-container") as HTMLDivElement;
-            //     const zoomLevel = this._engine.diagramModel.getZoomLevel() / 100.0;
-            //     let currentX = (this._mouseLocationX - rootElement.offsetLeft - this._engine.diagramModel.getOffsetX() - this.NodeWidth) / zoomLevel;
-            //     let currentY = (this._mouseLocationY - rootElement.offsetTop - this._engine.diagramModel.getOffsetY() - 20) / zoomLevel;
-            //     let originalNode: Nullable<DefaultNodeModel> = null;
-
-            //     for (var node of this._copiedNodes) {
-            //         let block = node.block;
-
-            //         if (!block) {
-            //             continue;
-            //         }
-
-            //         let clone = block.clone(this.props.globalState.nodeMaterial.getScene());
-
-            //         if (!clone) {
-            //             return;
-            //         }
+                if (!selectedItem.block) {
+                    return;
+                }
+
+                this._copiedNodes = selectedItems.slice(0);
+            } else if (evt.key === "v") { // Paste
+                if (!this._copiedNodes.length) {
+                    return;
+                }
+
+                const rootElement = this.props.globalState.hostDocument!.querySelector(".diagram-container") as HTMLDivElement;
+                const zoomLevel = this._graphCanvas.zoom;
+                let currentX = (this._mouseLocationX - rootElement.offsetLeft - this._graphCanvas.x - this.NodeWidth) / zoomLevel;
+                let currentY = (this._mouseLocationY - rootElement.offsetTop - this._graphCanvas.y - 20) / zoomLevel;
+                let originalNode: Nullable<GraphNode> = null;
+
+                for (var node of this._copiedNodes) {
+                    let block = node.block;
+
+                    if (!block) {
+                        continue;
+                    }
+
+                    let clone = block.clone(this.props.globalState.nodeMaterial.getScene());
+
+                    if (!clone) {
+                        return;
+                    }
                     
-            //         let newNode = this.createNodeFromObject({ nodeMaterialBlock: clone });
-
-            //         let x = 0;
-            //         let y = 0;
-            //         if (originalNode) {
-            //             x = currentX + node.x - originalNode.x;
-            //             y = currentY + node.y - originalNode.y;
-            //         } else {
-            //             originalNode = node;
-            //             x = currentX;
-            //             y = currentY;
-            //         }
-
-            //         newNode.x = x;
-            //         newNode.y = y;
-            //     }
-
-            //     this._engine.repaintCanvas();
-            // }
+                    let newNode = this.createNodeFromObject(clone);
+
+                    let x = 0;
+                    let y = 0;
+                    if (originalNode) {
+                        x = currentX + node.x - originalNode.x;
+                        y = currentY + node.y - originalNode.y;
+                    } else {
+                        originalNode = node;
+                        x = currentX;
+                        y = currentY;
+                    }
+
+                    newNode.x = x;
+                    newNode.y = y;
+                }
+
+                this._engine.repaintCanvas();
+            }
 
         }, false);
     }
@@ -308,165 +344,31 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         this._blocks = [];
         this._graphCanvas.reset();
 
-        // Listen to events
-        // this._model.addListener({
-        //     nodesUpdated: (e) => {                
-        //         if (!e.isCreated) {
-        //             // Block is deleted
-        //             let targetBlock = (e.node as GenericNodeModel).block;
-
-        //             if (targetBlock) {
-        //                 let attachedBlockIndex = this.props.globalState.nodeMaterial!.attachedBlocks.indexOf(targetBlock);
-        //                 if (attachedBlockIndex > -1) {
-        //                     this.props.globalState.nodeMaterial!.attachedBlocks.splice(attachedBlockIndex, 1);
-        //                 }
-
-        //                 if (targetBlock.isFinalMerger) {
-        //                     this.props.globalState.nodeMaterial!.removeOutputNode(targetBlock);
-        //                 }
-        //                 let blockIndex = this._blocks.indexOf(targetBlock);
-
-        //                 if (blockIndex > -1) {
-        //                     this._blocks.splice(blockIndex, 1);
-        //                 }
-        //             }                  
-
-        //             this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
-        //         } else {
-
-        //         }
-        //     },
-        //     linksUpdated: (e) => {
-        //         if (!e.isCreated) {
-        //             // Link is deleted
-        //             this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
-        //             let sourcePort = e.link.sourcePort as DefaultPortModel;
-
-        //             var link = DefaultPortModel.SortInputOutput(sourcePort, e.link.targetPort as DefaultPortModel);
-        //             if (link) {
-        //                 if (link.input.connection && link.output.connection) {
-        //                     if (link.input.connection.connectedPoint) {
-        //                         // Disconnect standard nodes
-        //                         link.output.connection.disconnectFrom(link.input.connection);
-        //                         link.input.syncWithNodeMaterialConnectionPoint(link.input.connection);
-        //                         link.output.syncWithNodeMaterialConnectionPoint(link.output.connection);
-                                
-        //                         this.props.globalState.onRebuildRequiredObservable.notifyObservers();
-        //                     }
-        //                 }
-        //             } else {
-        //                 if (!e.link.targetPort && e.link.sourcePort && (e.link.sourcePort as DefaultPortModel).position === "input" && !(e.link.sourcePort as DefaultPortModel).connection!.isConnected) {
-        //                     // Drag from input port, we are going to build an input for it                            
-        //                     let input = e.link.sourcePort as DefaultPortModel;
-
-        //                     if (input.connection!.type == NodeMaterialBlockConnectionPointTypes.AutoDetect) {
-        //                         return;
-        //                     }
-
-        //                     let nodeModel = this.addValueNode(BlockTools.GetStringFromConnectionNodeType(input.connection!.type));
-        //                     let link = nodeModel.ports.output.link(input);
-
-        //                     nodeModel.x = e.link.points[1].x - this.NodeWidth;
-        //                     nodeModel.y = e.link.points[1].y;
-
-        //                     setTimeout(() => {
-        //                         this._model.addLink(link);
-        //                         input.syncWithNodeMaterialConnectionPoint(input.connection!);
-        //                         nodeModel.ports.output.syncWithNodeMaterialConnectionPoint(nodeModel.ports.output.connection!);      
-                                
-        //                         let isFragmentOutput = (input.parent as DefaultNodeModel).block!.getClassName() === "FragmentOutputBlock";
-
-        //                         if (isFragmentOutput) {
-        //                             this.applyFragmentOutputConstraints(input);
-        //                         }
-
-        //                         this.forceUpdate();
-        //                     }, 1);
-                           
-        //                     nodeModel.ports.output.connection!.connectTo(input.connection!);
-        //                     this.props.globalState.onRebuildRequiredObservable.notifyObservers();
-        //                 }
-        //             }
-        //             this.forceUpdate();
-        //             return;
-        //         } else {
-        //             e.link.addListener({
-        //                 sourcePortChanged: () => {
-        //                 },
-        //                 targetPortChanged: (evt) => {
-        //                     // Link is created with a target port
-        //                     var link = DefaultPortModel.SortInputOutput(e.link.sourcePort as DefaultPortModel, e.link.targetPort as DefaultPortModel);
-    
-        //                     if (link) {
-        //                         if (link.output.connection && link.input.connection) {
-        //                             let currentBlock = link.input.connection.ownerBlock;
-        //                             let isFragmentOutput = currentBlock.getClassName() === "FragmentOutputBlock";
-    
-        //                             // Disconnect previous connection
-        //                             for (var key in link.input.links) {
-        //                                 let other = link.input.links[key];
-        //                                 let sourcePortConnection = (other.getSourcePort() as DefaultPortModel).connection;
-        //                                 let targetPortConnection = (other.getTargetPort() as DefaultPortModel).connection;
-    
-        //                                 if (
-        //                                     sourcePortConnection !== (link.output as DefaultPortModel).connection && 
-        //                                     targetPortConnection !== (link.output as DefaultPortModel).connection
-        //                                 ) {
-        //                                     other.remove();
-        //                                 }
-        //                             }
-    
-        //                             let compatibilityState = link.output.connection.checkCompatibilityState(link.input.connection);
-        //                             if (compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible) {
-        //                                 if (isFragmentOutput) {
-        //                                     this.applyFragmentOutputConstraints(link.input);
-        //                                 }
-        
-        //                                 link.output.connection.connectTo(link.input.connection);
-        //                             } else {
-        //                                 (evt.entity as AdvancedLinkModel).remove();
-
-        //                                 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);    
-        //                             }
-    
-        //                             this.forceUpdate();
-        //                         }
-        //                         if (this.props.globalState.nodeMaterial) {
-        //                             this.buildMaterial();
-        //                         }
-        //                     } else {
-        //                         e.link.remove();
-        //                     }
-        //                 }
-        //             });
-        //         }             
-        //     }
-        // });
-
         // Load graph of nodes from the material
         if (this.props.globalState.nodeMaterial) {
             var material = this.props.globalState.nodeMaterial;
             material._vertexOutputNodes.forEach((n: any) => {
-                this.createNodeFromObject({ nodeMaterialBlock: n });
+                this.createNodeFromObject(n);
             });
             material._fragmentOutputNodes.forEach((n: any) => {
-                this.createNodeFromObject({ nodeMaterialBlock: n });
+                this.createNodeFromObject(n);
+            });
+
+            material.attachedBlocks.forEach((n: any) => {
+                this.createNodeFromObject(n);
             });
 
+            // Links
             material.attachedBlocks.forEach((n: any) => {
-                this.createNodeFromObject({ nodeMaterialBlock: n });
+                if (n.inputs.length) {
+                    for (var input of n.inputs) {
+                        if (input.isConnected) {
+                            this._graphCanvas.connectPorts(input.connectedPoint!, input);
+                        }
+                    }
+                }
             });
+            
         }
 
         this.reOrganize(locations);
@@ -551,7 +453,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
 
             this._toAdd = [];
             block.autoConfigure(this.props.globalState.nodeMaterial);       
-            newNode = this.createNodeFromObject({ nodeMaterialBlock: block });
+            newNode = this.createNodeFromObject(block);
         };
 
         let x = (event.clientX - event.currentTarget.offsetLeft - this._graphCanvas.x - this.NodeWidth) / this._graphCanvas.zoom;
@@ -562,40 +464,22 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
 
         this.props.globalState.onSelectionChangedObservable.notifyObservers(newNode);
 
-        // if (nodeModel) {
-        //     const zoomLevel = this._engine.diagramModel.getZoomLevel() / 100.0;
+        let block = newNode.block;
 
-        //     let x = (event.clientX - event.currentTarget.offsetLeft - this._engine.diagramModel.getOffsetX() - this.NodeWidth) / zoomLevel;
-        //     let y = (event.clientY - event.currentTarget.offsetTop - this._engine.diagramModel.getOffsetY() - 20) / zoomLevel;
-        //     nodeModel.setPosition(x, y);
-        
-        //     let block = nodeModel!.block;
-
-        //     x -= this.NodeWidth + 150;
-
-        //     block!._inputs.forEach((connection) => {       
-        //         if (connection.connectedPoint) {
-        //             var existingNodes = this._nodes.filter((n) => { return n.block === (connection as any)._connectedPoint._ownerBlock });
-        //             let connectedNode = existingNodes[0];
-
-        //             if (connectedNode.x === 0 && connectedNode.y === 0) {
-        //                 connectedNode.setPosition(x, y);
-        //                 y += 80;
-        //             }
-        //         }
-        //     });
-            
-        //     this._engine.repaintCanvas();
+        x -= this.NodeWidth + 150;
 
-        //     setTimeout(() => {
-        //         this._model.addAll(...this._toAdd!);            
-        //         this._toAdd = null;  
-        //         this._model.clearSelection();
-        //         nodeModel!.setSelected(true);
+        block.inputs.forEach((connection) => {       
+            if (connection.connectedPoint) {
+                var existingNodes = this._graphCanvas.nodes.filter((n) => { return n.block === (connection as any).connectedPoint.ownerBlock });
+                let connectedNode = existingNodes[0];
 
-        //         this._engine.repaintCanvas();  
-        //     }, 150);
-        // }
+                if (connectedNode.x === 0 && connectedNode.y === 0) {
+                    connectedNode.x = x
+                    connectedNode.y = y;
+                    y += 80;
+                }
+            }
+        });
 
         this.forceUpdate();
     }
@@ -608,8 +492,8 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                         gridTemplateColumns: this.buildColumnLayout()
                     }}
                     onMouseMove={evt => {
-                        // this._mouseLocationX = evt.pageX;
-                        // this._mouseLocationY = evt.pageY;
+                        this._mouseLocationX = evt.pageX;
+                        this._mouseLocationY = evt.pageY;
                     }}
                     onMouseDown={(evt) => {
                         if ((evt.target as HTMLElement).nodeName === "INPUT") {

+ 15 - 0
src/Materials/Node/nodeMaterial.ts

@@ -522,6 +522,21 @@ export class NodeMaterial extends PushMaterial {
     }
 
     /**
+     * Remove a block from the current node material
+     * @param block defines the block to remove
+     */
+    public removeBlock(block: NodeMaterialBlock) {
+        let attachedBlockIndex = this.attachedBlocks.indexOf(block);
+        if (attachedBlockIndex > -1) {
+            this.attachedBlocks.splice(attachedBlockIndex, 1);
+        }
+
+        if (block.isFinalMerger) {
+            this.removeOutputNode(block);
+        }
+    }
+
+    /**
      * Build the material and generates the inner effect
      * @param verbose defines if the build should log activity
      */