Browse Source

Merge branch 'export-frames' of git://github.com/belfortk/Babylon.js

David Catuhe 5 years ago
parent
commit
31a24c731c

+ 1 - 0
dist/preview release/what's new.md

@@ -99,6 +99,7 @@
 - Fix freeze (infinite loop) when disposing a scene that loaded some specific gLTF files ([Popov72](https://github.com/Popov72)
 
 - Fix submesh recreation when it should not ([Popov72](https://github.com/Popov72)
+- Fix bug in NME where deleting a node from a frame would not remove its ports on the outside of a frame
 
 ## Breaking changes
 

+ 11 - 11
nodeEditor/src/components/propertyTab/propertyTabComponent.tsx

@@ -16,7 +16,7 @@ import { GraphFrame } from '../../diagram/graphFrame';
 import { TextLineComponent } from '../../sharedComponents/textLineComponent';
 import { Engine } from 'babylonjs/Engines/engine';
 import { FramePropertyTabComponent } from '../../diagram/properties/framePropertyComponent';
-import { NodePortPropertyTabComponent } from '../../diagram/properties/nodePortPropertyComponent';
+import { FrameNodePortPropertyTabComponent } from '../../diagram/properties/frameNodePortPropertyComponent';
 import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
 import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
 import { Color3LineComponent } from '../../sharedComponents/color3LineComponent';
@@ -26,7 +26,7 @@ import { Vector2LineComponent } from '../../sharedComponents/vector2LineComponen
 import { Vector3LineComponent } from '../../sharedComponents/vector3LineComponent';
 import { Vector4LineComponent } from '../../sharedComponents/vector4LineComponent';
 import { Observer } from 'babylonjs/Misc/observable';
-import { NodePort } from '../../diagram/nodePort';
+import { FrameNodePort } from '../../diagram/frameNodePort';
 import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
 require("./propertyTab.scss");
 
@@ -34,25 +34,25 @@ interface IPropertyTabComponentProps {
     globalState: GlobalState;
 }
 
-export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, { currentNode: Nullable<GraphNode>, currentFrame: Nullable<GraphFrame>, currentNodePort: Nullable<NodePort> }> {
+export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, { currentNode: Nullable<GraphNode>, currentFrame: Nullable<GraphFrame>, currentFrameNodePort: Nullable<FrameNodePort> }> {
     private _onBuiltObserver: Nullable<Observer<void>>;
 
     constructor(props: IPropertyTabComponentProps) {
         super(props)
 
-        this.state = { currentNode: null, currentFrame: null, currentNodePort: null };
+        this.state = { currentNode: null, currentFrame: null, currentFrameNodePort: null };
     }
 
     componentDidMount() {
         this.props.globalState.onSelectionChangedObservable.add(selection => {
             if (selection instanceof GraphNode) {
-                this.setState({ currentNode: selection, currentFrame: null, currentNodePort: null });
+                this.setState({ currentNode: selection, currentFrame: null, currentFrameNodePort: null });
             } else if (selection instanceof GraphFrame) {
-                this.setState({ currentNode: null, currentFrame: selection, currentNodePort: null });
-            } else if(selection instanceof NodePort) {
-                this.setState({ currentNode: null, currentFrame: null, currentNodePort: selection });
+                this.setState({ currentNode: null, currentFrame: selection, currentFrameNodePort: null });
+            } else if(selection instanceof FrameNodePort) {
+                this.setState({ currentNode: null, currentFrame: null, currentFrameNodePort: selection });
             } else {
-                this.setState({ currentNode: null, currentFrame: null, currentNodePort: null });
+                this.setState({ currentNode: null, currentFrame: null, currentFrameNodePort: null });
             }
         });
 
@@ -250,9 +250,9 @@ export class PropertyTabComponent extends React.Component<IPropertyTabComponentP
             );
         }
 
-        if (this.state.currentNodePort) {
+        if (this.state.currentFrameNodePort) {
             return (
-                <NodePortPropertyTabComponent globalState={this.props.globalState} nodePort={this.state.currentNodePort}/>
+                <FrameNodePortPropertyTabComponent globalState={this.props.globalState} frameNodePort={this.state.currentFrameNodePort}/>
             );
         }
 

+ 94 - 0
nodeEditor/src/diagram/frameNodePort.ts

@@ -0,0 +1,94 @@
+import { NodePort } from "./nodePort";
+import { Nullable, Observable, NodeMaterialConnectionPoint } from 'babylonjs';
+import { GraphNode } from './graphNode';
+import { FramePortPosition } from './graphFrame';
+import { GlobalState } from '../globalState';
+import { IDisplayManager } from './display/displayManager';
+
+export class FrameNodePort extends NodePort {
+    private _onFramePortMoveUpObservable = new Observable<FrameNodePort>();
+    private _onFramePortMoveDownObservable = new Observable<FrameNodePort>();
+    private _onFramePortPositionChangedObservable = new Observable<FramePortPosition>();
+    private _portLabel: Element;
+    private _isInput: boolean;
+    private _framePortPosition: FramePortPosition
+    private _framePortId: Nullable<number>;
+
+    public get onFramePortMoveUpObservable() {
+        return this._onFramePortMoveUpObservable;
+    }
+
+    public get onFramePortMoveDownObservable() {
+        return this._onFramePortMoveDownObservable;
+    }
+
+    public get onFramePortPositionChangedObservable() {
+        return this._onFramePortPositionChangedObservable;
+    }
+
+    public get isInput() {
+        return this._isInput;
+    }
+
+    public get portLabel() {
+        return this._portLabel.innerHTML;
+    }
+
+    public get framePortId() {
+        return this._framePortId;
+    }
+
+    public set portLabel(newLabel: string) {
+        this._portLabel.innerHTML = newLabel;
+    }
+
+    public get framePortPosition() {
+        return this._framePortPosition;
+    }
+
+    public set framePortPosition(position: FramePortPosition) {
+        this._framePortPosition = position;
+        this.onFramePortPositionChangedObservable.notifyObservers(position);
+    }
+
+    public constructor(portContainer: HTMLElement, public connectionPoint: NodeMaterialConnectionPoint, public node: GraphNode, globalState: GlobalState, isInput: boolean, framePortId: number) {
+        super(portContainer, connectionPoint,node, globalState);
+
+        this._portLabel = portContainer.children[0];
+        this._isInput = isInput;
+        this._framePortId = framePortId;
+
+        this._onSelectionChangedObserver = this._globalState.onSelectionChangedObservable.add((selection) => {
+            if (selection === this) {
+                this._img.classList.add("selected");
+            } else {
+                this._img.classList.remove("selected");
+            }
+        });
+
+        this.refresh();
+    }
+
+    public static CreateFrameNodePortElement(connectionPoint: NodeMaterialConnectionPoint, node: GraphNode, root: HTMLElement, 
+        displayManager: Nullable<IDisplayManager>, globalState: GlobalState, isInput: boolean, framePortId: number) {
+        let portContainer = root.ownerDocument!.createElement("div");
+        let block = connectionPoint.ownerBlock;
+
+        portContainer.classList.add("portLine");
+        if(framePortId !== null) {
+            portContainer.dataset.framePortId = `${framePortId}`;
+        }
+        root.appendChild(portContainer);
+
+        if (!displayManager || displayManager.shouldDisplayPortLabels(block)) {
+            let portLabel = root.ownerDocument!.createElement("div");
+            portLabel.classList.add("port-label");
+            portLabel.innerHTML = connectionPoint.name;        
+            portContainer.appendChild(portLabel);
+        }
+
+        return new FrameNodePort(portContainer, connectionPoint, node, globalState, isInput, framePortId);
+    }
+
+} 
+

+ 7 - 1
nodeEditor/src/diagram/graphCanvas.scss

@@ -30,7 +30,13 @@
         
         .img {
             width: 100%;
-        }     
+        }
+
+        img.selected {
+            margin: 3px 1px 0 4px;
+            box-shadow: 0 0 0 4px;
+            border-radius: 50%;
+        }
         
         &:hover, &.selected {
             filter: brightness(2);

+ 4 - 13
nodeEditor/src/diagram/graphCanvas.tsx

@@ -14,6 +14,7 @@ import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
 import { GraphFrame } from './graphFrame';
 import { IEditorData } from '../nodeLocationInfo';
+import { FrameNodePort } from './frameNodePort';
 
 require("./graphCanvas.scss");
 
@@ -47,7 +48,7 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     private _selectedLink: Nullable<NodeLink> = null;
     private _selectedPort: Nullable<NodePort> = null;
     private _candidateLink: Nullable<NodeLink> = null;
-    private _candidatePort: Nullable<NodePort> = null;
+    private _candidatePort: Nullable<NodePort | FrameNodePort> = null;
     private _gridSize = 20;
     private _selectionBox: Nullable<HTMLDivElement> = null;   
     private _selectedFrame: Nullable<GraphFrame> = null;   
@@ -195,20 +196,10 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
             }
         });
 
-        props.globalState.onCandidatePortSelected.add(port => {
+        props.globalState.onCandidatePortSelectedObservable.add(port => {
             this._candidatePort = port;
         });
 
-        props.globalState.onFramePortMoveUpObserver.add((nodePort: NodePort) => {
-            const frame = this._frames.find(frame => frame.id === nodePort.frameId);
-            frame?.moveFramePortUp(nodePort);
-        });
-
-        props.globalState.onFramePortMoveDownObserver.add((nodePort: NodePort) => {
-            const frame = this._frames.find(frame => frame.id === nodePort.frameId);
-            frame?.moveFramePortDown(nodePort);
-        })
-
         props.globalState.onGridSizeChanged.add(() => {
             this.gridSize = DataStorage.ReadNumber("GridSize", 20);
         });
@@ -608,7 +599,7 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
                 this.processCandidatePort();          
                 this.props.globalState.onCandidateLinkMoved.notifyObservers(null);
             } else { // is a click event on NodePort
-                if(this._candidateLink.portA.frameId !== null) { //only on Frame Ports
+                if(this._candidateLink.portA instanceof FrameNodePort) { //only on Frame Ports
                     this.props.globalState.onSelectionChangedObservable.notifyObservers(this._candidateLink.portA);
                 }
             }

+ 128 - 31
nodeEditor/src/diagram/graphFrame.ts

@@ -8,6 +8,7 @@ import { Color3 } from 'babylonjs/Maths/math.color';
 import { NodePort } from './nodePort';
 import { SerializationTools } from '../serializationTools';
 import { StringTools } from '../stringTools';
+import { FrameNodePort } from './frameNodePort';
 
 enum ResizingDirection {
         Right,
@@ -20,6 +21,10 @@ enum ResizingDirection {
         BottomLeft
 }
 
+export enum FramePortPosition {
+    Top, Middle, Bottom
+};
+
 export class GraphFrame {
     private readonly CollapsedWidth = 200;
     private static _FrameCounter = 0;
@@ -47,8 +52,10 @@ export class GraphFrame {
     private _mouseStartPointX: Nullable<number> = null;
     private _mouseStartPointY: Nullable<number> = null;
     private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame | NodePort>>>;
+    private _onGraphNodeRemovalObserver: Nullable<Observer<GraphNode>>; 
     private _isCollapsed = false;
-    private _ports: NodePort[] = []; // Ports on Outside of Frame
+    private _frameInPorts: FrameNodePort[] = [];
+    private _frameOutPorts: FrameNodePort[] = [];
     private _controlledPorts: NodePort[] = []; // Ports on Nodes that are shown on outside of frame
     private _id: number;
     private _comments: string;
@@ -73,11 +80,18 @@ export class GraphFrame {
     }
 
     private _createInputPort(port: NodePort, node: GraphNode) {
-        let localPort = NodePort.CreatePortElement(port.connectionPoint, node, this._inputPortContainer, null, this._ownerCanvas.globalState, true, this.id, GraphFrame._FramePortCounter++)
-        this._ports.push(localPort);
+        let localPort = FrameNodePort.CreateFrameNodePortElement(port.connectionPoint, node, this._inputPortContainer, null, this._ownerCanvas.globalState, true, GraphFrame._FramePortCounter++)
+        this._frameInPorts.push(localPort);
 
         port.delegatedPort = localPort;
         this._controlledPorts.push(port);
+        localPort.onFramePortMoveUpObservable.add((nodePort: FrameNodePort) => {
+                this.moveFramePortUp(nodePort);
+        });
+
+        localPort.onFramePortMoveDownObservable.add((nodePort: FrameNodePort) => {
+                this.moveFramePortDown(nodePort);
+        })
     }
 
     public set isCollapsed(value: boolean) {
@@ -102,14 +116,22 @@ export class GraphFrame {
 
                         for (var link of node.links) {
                             if (link.portA === port && this.nodes.indexOf(link.nodeB!) === -1) {
-                                let localPort: NodePort;
+                                let localPort: FrameNodePort;
 
                                 if (!portAdded) {
                                     portAdded = true;
-                                    localPort = NodePort.CreatePortElement(port.connectionPoint, link.nodeB!, this._outputPortContainer, null, this._ownerCanvas.globalState, false, this.id, GraphFrame._FramePortCounter++);
-                                    this._ports.push(localPort);
+                                    localPort = FrameNodePort.CreateFrameNodePortElement(port.connectionPoint, link.nodeB!, this._outputPortContainer, null, this._ownerCanvas.globalState, false, GraphFrame._FramePortCounter++);
+                                    this._frameOutPorts.push(localPort);
+
+                                    localPort.onFramePortMoveUpObservable.add((nodePort: FrameNodePort) => {
+                                            this.moveFramePortUp(nodePort);
+                                    });
+                            
+                                    localPort.onFramePortMoveDownObservable.add((nodePort: FrameNodePort) => {
+                                            this.moveFramePortDown(nodePort);
+                                    })
                                 } else {
-                                    localPort = this._ports.filter(p => p.connectionPoint === port.connectionPoint)[0];
+                                    localPort = this.ports.filter(p => p.connectionPoint === port.connectionPoint)[0];
                                 }
 
                                 port.delegatedPort = localPort;
@@ -118,10 +140,18 @@ export class GraphFrame {
                             }
                         }
                     } else {
-                        let localPort = NodePort.CreatePortElement(port.connectionPoint, node, this._outputPortContainer, null, this._ownerCanvas.globalState, false, this.id, GraphFrame._FramePortCounter++)
-                        this._ports.push(localPort);
+                        let localPort = FrameNodePort.CreateFrameNodePortElement(port.connectionPoint, node, this._outputPortContainer, null, this._ownerCanvas.globalState, false, GraphFrame._FramePortCounter++)
+                        this._frameOutPorts.push(localPort);
                         port.delegatedPort = localPort;
                         this._controlledPorts.push(port);
+
+                        localPort.onFramePortMoveUpObservable.add((nodePort: FrameNodePort) => {
+                                this.moveFramePortUp(nodePort);
+                        });
+                
+                        localPort.onFramePortMoveDownObservable.add((nodePort: FrameNodePort) => {
+                                this.moveFramePortDown(nodePort);
+                        });
                     }
                 }
 
@@ -138,12 +168,39 @@ export class GraphFrame {
                     }
                 }
             }
+
+            for(let i = 0; i < this._frameInPorts.length; i++) {
+                const port = this._frameInPorts[i];
+                if(i === 0){
+                    port.framePortPosition = FramePortPosition.Top;
+                } else if(i === this._frameInPorts.length -1){
+                    port.framePortPosition = FramePortPosition.Bottom;
+                } else {
+                    port.framePortPosition = FramePortPosition.Middle;
+                }
+            }
+
+            for(let i = 0; i < this._frameOutPorts.length; i++) {
+                const port = this._frameInPorts[i];
+                if(i === 0){
+                    port.framePortPosition = FramePortPosition.Top
+                } else if(i === this._frameInPorts.length -1){
+                    port.framePortPosition = FramePortPosition.Bottom
+                } else {
+                    port.framePortPosition = FramePortPosition.Middle
+                }
+            }
+
         } else {
             this.element.classList.remove("collapsed");
             this._outputPortContainer.innerHTML = "";
             this._inputPortContainer.innerHTML = "";
 
-            this._ports.forEach(p => {
+            this._frameInPorts.forEach(p => {
+                p.dispose();
+            });
+
+            this._frameOutPorts.forEach(p => {
                 p.dispose();
             });
 
@@ -152,7 +209,8 @@ export class GraphFrame {
                 port.refresh();
             })
 
-            this._ports = [];
+            this._frameInPorts = [];
+            this._frameOutPorts = [];
             this._controlledPorts = [];
 
             for (var node of this._nodes) {
@@ -182,7 +240,7 @@ export class GraphFrame {
     }
 
     public get ports(){
-        return this._ports;
+        return this._frameInPorts.concat(this._frameOutPorts);
     }
 
     public get name() {
@@ -425,7 +483,17 @@ export class GraphFrame {
             } else {
                 this.element.classList.remove("selected");
             }
-        }); 
+        });
+
+        this._onGraphNodeRemovalObserver = canvas.globalState.onGraphNodeRemovalObservable.add((node: GraphNode) => {
+            // remove node from this._nodes
+            const index = this._nodes.indexOf(node);
+            if (index === -1) {
+                return;
+            } else {
+                this._nodes.splice(index, 1);
+            }
+        });
 
         this._commentsElement = document.createElement('div');
         this._commentsElement.className = 'frame-comments';
@@ -442,7 +510,7 @@ export class GraphFrame {
 
     public refresh() {
         this._nodes = [];
-        this._ownerCanvas.globalState.onFrameCreated.notifyObservers(this);
+        this._ownerCanvas.globalState.onFrameCreatedObservable.notifyObservers(this);
     }
 
     public addNode(node: GraphNode) {
@@ -549,57 +617,82 @@ export class GraphFrame {
         evt.stopPropagation();
     }
 
-    public moveFramePortUp(nodePort: NodePort){
+    private moveFramePortUp(nodePort: FrameNodePort){
         if(nodePort.isInput) {
             if(this._inputPortContainer.children.length < 2) {
                 return;
             }
             const elementsArray = Array.from(this._inputPortContainer.childNodes);
-            this._moveFramePortUp(elementsArray, nodePort);
+            this._movePortUp(elementsArray, nodePort);
         } else {
             if(this._outputPortContainer.children.length < 2) {
                 return;
             }
             const elementsArray = Array.from(this._outputPortContainer.childNodes);
-            this._moveFramePortUp(elementsArray, nodePort);
+            this._movePortUp(elementsArray, nodePort);
         }
 
     }
 
-    private _moveFramePortUp(elementsArray: ChildNode[], nodePort: NodePort) {
-        const indexInContainer = (elementsArray as HTMLElement[]).findIndex(elem => elem.dataset.framePortId === `${nodePort.framePortId}`)
-        if(indexInContainer === 0){
+    private _movePortUp(elementsArray: ChildNode[], nodePort: FrameNodePort) {
+        // update UI
+        const indexInElementArray = (elementsArray as HTMLElement[]).findIndex(elem => elem.dataset.framePortId === `${nodePort.framePortId}`)
+        if(indexInElementArray === 0){
             return;
         }
-        const secondPort = elementsArray[indexInContainer];
-        const firstPort =  elementsArray[indexInContainer -1];
-        firstPort.parentElement?.insertBefore(secondPort, firstPort);
+        const secondPortElement = elementsArray[indexInElementArray];
+        const firstPortElement =  elementsArray[indexInElementArray -1];
+        firstPortElement.parentElement?.insertBefore(secondPortElement, firstPortElement);
+
+        // update Frame Port Container
+        const indexInContainer = this._frameInPorts.findIndex(framePort => framePort === nodePort);
+        [this._frameInPorts[indexInContainer -1], this._frameInPorts[indexInContainer]] = [this._frameInPorts[indexInContainer], this._frameInPorts[indexInContainer -1]]; // swap idicies
+        
+        // notify nodePort if it is now at Top (indexInElementArray === 1)
+        if (indexInElementArray === 1) {
+            nodePort.onFramePortPositionChangedObservable.notifyObservers(FramePortPosition.Top);
+        } else {
+            nodePort.onFramePortPositionChangedObservable.notifyObservers(FramePortPosition.Middle);
+        }
     }
     
-    public moveFramePortDown(nodePort: NodePort){
+    private moveFramePortDown(nodePort: FrameNodePort){
         if(nodePort.isInput) {
             if(this._inputPortContainer.children.length < 2) {
                 return;
             }
             const elementsArray = Array.from(this._inputPortContainer.childNodes);
-            this._moveFramePortDown(elementsArray, nodePort);
+            this._movePortDown(elementsArray, nodePort);
         } else {
             if(this._outputPortContainer.children.length < 2) {
                 return;
             }
             const elementsArray = Array.from(this._outputPortContainer.childNodes);
-            this._moveFramePortDown(elementsArray, nodePort);
+            this._movePortDown(elementsArray, nodePort);
         }
     }
 
-    private _moveFramePortDown(elementsArray: ChildNode[], nodePort: NodePort) {
-        const indexInContainer = (elementsArray as HTMLElement[]).findIndex(elem => elem.dataset.framePortId === `${nodePort.framePortId}`)
-        if(indexInContainer === elementsArray.length -1){
+    private _movePortDown(elementsArray: ChildNode[], nodePort: FrameNodePort) {
+        // update UI
+        const indexInElementArray = (elementsArray as HTMLElement[]).findIndex(elem => elem.dataset.framePortId === `${nodePort.framePortId}`)
+        if(indexInElementArray === elementsArray.length -1){
             return;
         }
-        const firstPort = elementsArray[indexInContainer];
-        const secondPort =  elementsArray[indexInContainer + 1];
+        const firstPort = elementsArray[indexInElementArray];
+        const secondPort =  elementsArray[indexInElementArray + 1];
         firstPort.parentElement?.insertBefore(secondPort, firstPort);
+
+        // update Frame Port Container
+        const indexInContainer = this._frameInPorts.findIndex(framePort => framePort === nodePort);
+        [this._frameInPorts[indexInContainer], this._frameInPorts[indexInContainer + 1]] = [this._frameInPorts[indexInContainer + 1], this._frameInPorts[indexInContainer]]; // swap idicies
+
+        // notify nodePort if it is now at bottom (indexInContainer === elementsArray.length-2)
+        if(indexInContainer === elementsArray.length-2) {
+            nodePort.onFramePortPositionChangedObservable.notifyObservers(FramePortPosition.Bottom);
+        } else {
+            nodePort.onFramePortPositionChangedObservable.notifyObservers(FramePortPosition.Middle);
+        }
+
     }
 
     private initResizing = (evt: PointerEvent) => {
@@ -1104,6 +1197,10 @@ export class GraphFrame {
             this._ownerCanvas.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
         }
 
+        if(this._onGraphNodeRemovalObserver) {
+            this._ownerCanvas.globalState.onGraphNodeRemovalObservable.remove(this._onGraphNodeRemovalObserver);
+        }
+
         this.element.parentElement!.removeChild(this.element);
 
         this._ownerCanvas.frames.splice(this._ownerCanvas.frames.indexOf(this), 1);

+ 8 - 5
nodeEditor/src/diagram/graphNode.ts

@@ -33,7 +33,7 @@ export class GraphNode {
     private _globalState: GlobalState;
     private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame | NodePort>>>;   
     private _onSelectionBoxMovedObserver: Nullable<Observer<ClientRect | DOMRect>>;  
-    private _onFrameCreatedObserver: Nullable<Observer<GraphFrame>>;  
+    private _onFrameCreatedObserver: Nullable<Observer<GraphFrame>>; 
     private _onUpdateRequiredObserver: Nullable<Observer<void>>;  
     private _ownerCanvas: GraphCanvasComponent; 
     private _isSelected: boolean;
@@ -183,7 +183,7 @@ export class GraphNode {
             this.isSelected = overlap;
         });
 
-        this._onFrameCreatedObserver = this._globalState.onFrameCreated.add(frame => {      
+        this._onFrameCreatedObserver = this._globalState.onFrameCreatedObservable.add(frame => {      
             if (this._ownerCanvas.frames.some(f => f.nodes.indexOf(this) !== -1)) {
                 return;
             }
@@ -407,17 +407,20 @@ export class GraphNode {
 
         // Connections
         for (var input of this.block.inputs) {
-            this._inputPorts.push(NodePort.CreatePortElement(input,  this, this._inputsContainer, this._displayManager, this._globalState, true, null, undefined));
+            this._inputPorts.push(NodePort.CreatePortElement(input,  this, this._inputsContainer, this._displayManager, this._globalState));
         }
 
         for (var output of this.block.outputs) {
-            this._outputPorts.push(NodePort.CreatePortElement(output,  this, this._outputsContainer, this._displayManager, this._globalState, false, null, undefined));
+            this._outputPorts.push(NodePort.CreatePortElement(output,  this, this._outputsContainer, this._displayManager, this._globalState));
         }
 
         this.refresh();
     }
 
     public dispose() {
+        // notify frame observers that this node is being deleted
+        this._globalState.onGraphNodeRemovalObservable.notifyObservers(this);
+
         if (this._onSelectionChangedObserver) {
             this._globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
         }
@@ -435,7 +438,7 @@ export class GraphNode {
         }
 
         if (this._onFrameCreatedObserver) {
-            this._globalState.onFrameCreated.remove(this._onFrameCreatedObserver);
+            this._globalState.onFrameCreatedObservable.remove(this._onFrameCreatedObserver);
         }
 
         for (var port of this._inputPorts) {

+ 3 - 2
nodeEditor/src/diagram/nodeLink.ts

@@ -4,11 +4,12 @@ import { NodePort } from './nodePort';
 import { Nullable } from 'babylonjs/types';
 import { Observer, Observable } from 'babylonjs/Misc/observable';
 import { GraphFrame } from './graphFrame';
+import { FrameNodePort } from './frameNodePort';
 
 export class NodeLink {
     private _graphCanvas: GraphCanvasComponent;
-    private _portA: NodePort;
-    private _portB?: NodePort;
+    private _portA: NodePort | FrameNodePort;
+    private _portB?: NodePort | FrameNodePort;
     private _nodeA: GraphNode;
     private _nodeB?: GraphNode;
     private _path: SVGPathElement;

+ 19 - 44
nodeEditor/src/diagram/nodePort.ts

@@ -7,18 +7,18 @@ import { Observer } from 'babylonjs/Misc/observable';
 import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { IDisplayManager } from './display/displayManager';
 import { GraphNode } from './graphNode';
+import { NodeLink } from './nodeLink';
+import { GraphFrame } from './graphFrame';
+import { FrameNodePort } from './frameNodePort';
 
 export class NodePort {
-    private _element: HTMLDivElement;
-    private _img: HTMLImageElement;
-    private _globalState: GlobalState;
-    private _onCandidateLinkMovedObserver: Nullable<Observer<Nullable<Vector2>>>;
-    private _portLabel: Element;
-    private _frameId: Nullable<number>
-    private _isInput: boolean;
-    private _framePortId: Nullable<number>;
-
-    public delegatedPort: Nullable<NodePort> = null;
+    protected _element: HTMLDivElement;
+    protected _img: HTMLImageElement;
+    protected _globalState: GlobalState;
+    protected _onCandidateLinkMovedObserver: Nullable<Observer<Nullable<Vector2>>>;
+    protected _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame | NodePort | FrameNodePort>>>;  
+    
+    public delegatedPort: Nullable<FrameNodePort> = null;
 
     public get element(): HTMLDivElement {
         if (this.delegatedPort) {
@@ -28,26 +28,6 @@ export class NodePort {
         return this._element;
     }
 
-    public get frameId() {
-        return this._frameId;
-    }
-
-    public get isInput() {
-        return this._isInput;
-    }
-
-    public get portLabel() {
-        return this._portLabel.innerHTML;
-    }
-
-    public get framePortId() {
-        return this._framePortId;
-    }
-
-    public set portLabel(newLabel: string) {
-        this._portLabel.innerHTML = newLabel;
-    }
-
     public refresh() {
         this._element.style.background = BlockTools.GetColorFromConnectionNodeType(this.connectionPoint.type);
         switch (this.connectionPoint.type) {
@@ -72,19 +52,12 @@ export class NodePort {
         }
     }
 
-    public constructor(portContainer: HTMLElement, public connectionPoint: NodeMaterialConnectionPoint, public node: GraphNode, globalState: GlobalState, isInput: boolean, frameId: Nullable<number>, framePortId: number | undefined) {
+    public constructor(portContainer: HTMLElement, public connectionPoint: NodeMaterialConnectionPoint, public node: GraphNode, globalState: GlobalState) {
         this._element = portContainer.ownerDocument!.createElement("div");
         this._element.classList.add("port");
         portContainer.appendChild(this._element);
         this._globalState = globalState;
 
-        this._portLabel = portContainer.children[0];
-        this._frameId = frameId
-        this._isInput = isInput;
-        if(framePortId !== undefined) {
-            this._framePortId = framePortId;
-        }
-
         this._img = portContainer.ownerDocument!.createElement("img");
         this._element.appendChild(this._img );
 
@@ -102,7 +75,7 @@ export class NodePort {
             }
 
             this._element.classList.add("selected"); 
-            this._globalState.onCandidatePortSelected.notifyObservers(this);
+            this._globalState.onCandidatePortSelectedObservable.notifyObservers(this);
         });
 
         this.refresh();
@@ -110,17 +83,19 @@ export class NodePort {
 
     public dispose() {
         this._globalState.onCandidateLinkMoved.remove(this._onCandidateLinkMovedObserver);
+
+        if (this._onSelectionChangedObserver) {
+            this._globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+        }
     }
 
     public static CreatePortElement(connectionPoint: NodeMaterialConnectionPoint, node: GraphNode, root: HTMLElement, 
-            displayManager: Nullable<IDisplayManager>, globalState: GlobalState, isInput: boolean,  frameId: Nullable<number> = null, framePortId: number | undefined) {
+            displayManager: Nullable<IDisplayManager>, globalState: GlobalState) {
         let portContainer = root.ownerDocument!.createElement("div");
         let block = connectionPoint.ownerBlock;
 
         portContainer.classList.add("portLine");
-        if(framePortId !== null) {
-            portContainer.dataset.framePortId = `${framePortId}`;
-        }
+
         root.appendChild(portContainer);
 
         if (!displayManager || displayManager.shouldDisplayPortLabels(block)) {
@@ -130,6 +105,6 @@ export class NodePort {
             portContainer.appendChild(portLabel);
         }
     
-        return new NodePort(portContainer, connectionPoint, node, globalState, isInput, frameId, framePortId);
+        return new NodePort(portContainer, connectionPoint, node, globalState);
     }
 }

+ 59 - 0
nodeEditor/src/diagram/properties/frameNodePortPropertyComponent.tsx

@@ -0,0 +1,59 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { GlobalState } from '../../globalState';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
+import { FramePortPosition } from '../graphFrame';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { FrameNodePort } from '../frameNodePort';
+
+export interface IFrameNodePortPropertyTabComponentProps {
+    globalState: GlobalState
+    frameNodePort: FrameNodePort;
+}
+
+export class FrameNodePortPropertyTabComponent extends React.Component<IFrameNodePortPropertyTabComponentProps, {framePortPosition: FramePortPosition}> {    
+    private _onFramePortPositionChangedObserver: Nullable<Observer<FramePortPosition>>;
+
+    constructor(props: IFrameNodePortPropertyTabComponentProps) {
+        super(props);
+        this.state = {
+            framePortPosition: this.props.frameNodePort.framePortPosition
+        };
+
+        this._onFramePortPositionChangedObserver = this.props.frameNodePort.onFramePortPositionChangedObservable.add((position) => {
+            this.setState({framePortPosition: position})
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.frameNodePort.onFramePortPositionChangedObservable.remove(this._onFramePortPositionChangedObserver)
+    }
+
+    render() {      
+        return (
+            <div id="propertyTab">
+            <div id="header">
+                <img id="logo" src="https://www.babylonjs.com/Assets/logo-babylonjs-social-twitter.png" />
+                <div id="title">
+                    NODE MATERIAL EDITOR
+                </div>
+            </div>
+            <div>
+                <LineContainerComponent title="GENERAL">
+                    <TextInputLineComponent globalState={this.props.globalState} label="Port Label" propertyName="portLabel" target={this.props.frameNodePort}/>
+                     {this.state.framePortPosition !== FramePortPosition.Top && <ButtonLineComponent label="Move Node Up" onClick={() => {
+                                this.props.frameNodePort.onFramePortMoveUpObservable.notifyObservers(this.props.frameNodePort);
+                            }} />}
+
+                    {this.state.framePortPosition !== FramePortPosition.Bottom && <ButtonLineComponent label="Move Node Down" onClick={() => {
+                                this.props.frameNodePort.onFramePortMoveDownObservable.notifyObservers(this.props.frameNodePort);
+                            }} />}
+                </LineContainerComponent>
+            </div>
+        </div>
+        );
+    }
+}

+ 0 - 44
nodeEditor/src/diagram/properties/nodePortPropertyComponent.tsx

@@ -1,44 +0,0 @@
-
-import * as React from "react";
-import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
-import { GlobalState } from '../../globalState';
-import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
-import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
-import { NodePort } from '../nodePort';
-
-export interface INodePortPropertyTabComponentProps {
-    globalState: GlobalState
-    nodePort: NodePort;
-}
-
-export class NodePortPropertyTabComponent extends React.Component<INodePortPropertyTabComponentProps> {
-
-    constructor(props: INodePortPropertyTabComponentProps) {
-        super(props)
-    }
-
-    render() {      
-        return (
-            <div id="propertyTab">
-            <div id="header">
-                <img id="logo" src="https://www.babylonjs.com/Assets/logo-babylonjs-social-twitter.png" />
-                <div id="title">
-                    NODE MATERIAL EDITOR
-                </div>
-            </div>
-            <div>
-                <LineContainerComponent title="GENERAL">
-                    <TextInputLineComponent globalState={this.props.globalState} label="Port Label" propertyName="portLabel" target={this.props.nodePort}/>
-                    <ButtonLineComponent label="Move Node Up" onClick={() => {
-                                this.props.globalState.onFramePortMoveUpObserver.notifyObservers(this.props.nodePort);
-                            }} />
-
-                    <ButtonLineComponent label="Move Node Down" onClick={() => {
-                                this.props.globalState.onFramePortMoveDownObserver.notifyObservers(this.props.nodePort);
-                            }} />
-                </LineContainerComponent>
-            </div>
-        </div>
-        );
-    }
-}

+ 4 - 4
nodeEditor/src/globalState.ts

@@ -11,6 +11,7 @@ import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { NodePort } from './diagram/nodePort';
 import { NodeLink } from './diagram/nodeLink';
 import { GraphFrame } from './diagram/graphFrame';
+import { FrameNodePort } from './diagram/frameNodePort';
 
 export class GlobalState {
     nodeMaterial: NodeMaterial;
@@ -35,10 +36,9 @@ export class GlobalState {
     onAnimationCommandActivated = new Observable<void>();
     onCandidateLinkMoved = new Observable<Nullable<Vector2>>();
     onSelectionBoxMoved = new Observable<ClientRect | DOMRect>();
-    onFrameCreated = new Observable<GraphFrame>();
-    onCandidatePortSelected = new Observable<Nullable<NodePort>>();
-    onFramePortMoveUpObserver = new Observable<NodePort>();
-    onFramePortMoveDownObserver = new Observable<NodePort>();
+    onFrameCreatedObservable = new Observable<GraphFrame>();
+    onCandidatePortSelectedObservable = new Observable<Nullable<NodePort | FrameNodePort>>();
+    onGraphNodeRemovalObservable = new Observable<GraphNode>();
     onGetNodeFromBlock: (block: NodeMaterialBlock) => GraphNode;
     onGridSizeChanged = new Observable<void>();
     previewMeshType: PreviewMeshType;