Pārlūkot izejas kodu

New graph node block step 1

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

+ 1 - 1
inspector/src/components/sceneExplorer/entities/cameraTreeItemComponent.tsx

@@ -69,7 +69,7 @@ export class CameraTreeItemComponent extends React.Component<ICameraTreeItemComp
                 <TreeItemLabelComponent label={this.props.camera.name} onClick={() => this.props.onClick()} icon={faCamera} color="green" />
                 {
                     (!scene.activeCameras || scene.activeCameras.length === 0) &&
-                    <div className="activeCamera icon" onClick={() => this.setActive()} title="Set as main camera">
+                    <div className="activeCamera icon" onClick={() => this.setActive()} title="Set as main camera and attach to controls">
                         {isActiveElement}
                     </div>
                 }

+ 4 - 2
nodeEditor/src/diagram/display/inputDisplayManager.ts

@@ -46,8 +46,10 @@ export class InputDisplayManager implements IDisplayManager {
         switch (inputBlock.type) {                    
             case NodeMaterialBlockConnectionPointTypes.Color3:
             case NodeMaterialBlockConnectionPointTypes.Color4: {
-                color = (inputBlock.value as Color3).toHexString();
-                break;
+                if (inputBlock.value) {
+                    color = (inputBlock.value as Color3).toHexString();
+                    break;
+                }
             }
             default:
                 color = BlockTools.GetColorFromConnectionNodeType(inputBlock.type);

+ 14 - 0
nodeEditor/src/diagram/graphCanvas.scss

@@ -22,6 +22,20 @@
         }
     }
 
+    #group-container {
+        overflow: visible;
+        
+        .group-box {
+            position: absolute;
+            background: rgba(72, 72, 72, 0.5);
+            border: rgba(255, 255, 255, 0.432) solid 2px;
+
+            &.selected {
+                border-color: white;
+            }
+        }
+    }
+
     #graph-container {
         width: 100%;
         height: 100%;

+ 85 - 5
nodeEditor/src/diagram/graphCanvas.tsx

@@ -11,6 +11,7 @@ import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
 import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
 import { DataStorage } from '../dataStorage';
+import { GraphNodeGroup } from './graphNodeGroup';
 
 require("./graphCanvas.scss");
 
@@ -25,6 +26,7 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     private _hostCanvas: HTMLDivElement;
     private _graphCanvas: HTMLDivElement;
     private _selectionContainer: HTMLDivElement;
+    private _groupContainer: HTMLDivElement;
     private _svgCanvas: HTMLElement;
     private _rootContainer: HTMLDivElement;
     private _nodes: GraphNode[] = [];
@@ -43,7 +45,11 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     private _candidateLink: Nullable<NodeLink> = null;
     private _candidatePort: Nullable<NodePort> = null;
     private _gridSize = 20;
-    private _selectionBox: Nullable<HTMLDivElement> = null;  
+    private _selectionBox: Nullable<HTMLDivElement> = null;   
+    private _selectedGroup: Nullable<GraphNodeGroup> = null;   
+    private _groupCandidateBox: Nullable<HTMLDivElement> = null;  
+
+    private _groups: GraphNodeGroup[] = [];
 
     private _altKeyIsPressed = false;
     private _ctrlKeyIsPressed = false;
@@ -112,6 +118,9 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
     public get selectedLink() {
         return this._selectedLink;
     }
+    public get selectedGroup() {
+        return this._selectedGroup;
+    }
 
     public get canvasContainer() {
         return this._graphCanvas;
@@ -121,18 +130,28 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
         return this._svgCanvas;
     }
 
+    public get selectionContainer() {
+        return this._selectionContainer;
+    }
+
+    public get groupContainer() {
+        return this._groupContainer;
+    }
+
     constructor(props: IGraphCanvasComponentProps) {
         super(props);
 
-        props.globalState.onSelectionChangedObservable.add(selection => {
-            
+        props.globalState.onSelectionChangedObservable.add(selection => {            
             if (!selection) {
                 this._selectedNodes = [];
                 this._selectedLink = null;
+                this._selectedGroup = null;
             } else {
                 if (selection instanceof NodeLink) {
                     this._selectedLink = selection;
-                } else {
+                } else if (selection instanceof GraphNodeGroup) {
+                    this._selectedGroup = selection;
+                } else{
                     if (this._ctrlKeyIsPressed) {
                         if (this._selectedNodes.indexOf(selection) === -1) {
                             this._selectedNodes.push(selection);
@@ -161,6 +180,13 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
             this._altKeyIsPressed = false;
             this._ctrlKeyIsPressed = false;
         }, false);     
+
+        // Store additional data to serialization object
+        this.props.globalState.storeEditorData = (editorData) => {
+            editorData.zoom = this.zoom;
+            editorData.x = this.x;
+            editorData.y = this.y;
+        }
     }
 
     public getGridPosition(position: number) {
@@ -308,7 +334,8 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
         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;        
-        this._selectionContainer = this.props.globalState.hostDocument.getElementById("selection-container") as HTMLDivElement;        
+        this._selectionContainer = this.props.globalState.hostDocument.getElementById("selection-container") as HTMLDivElement;   
+        this._groupContainer = this.props.globalState.hostDocument.getElementById("group-container") as HTMLDivElement;        
         
         this.gridSize = DataStorage.ReadNumber("GridSize", 20);
         this.updateTransform();
@@ -343,6 +370,32 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
             return;
         }
 
+        // Candidate group box
+        if (this._groupCandidateBox) {
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+
+            const localX = evt.pageX - rootRect.left;
+            const localY = evt.pageY - rootRect.top;
+
+            if (localX > this._selectionStartX) {
+                this._groupCandidateBox.style.left = `${this._selectionStartX / this.zoom}px`;
+                this._groupCandidateBox.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
+            } else {
+                this._groupCandidateBox.style.left = `${localX / this.zoom}px`;
+                this._groupCandidateBox.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
+            }
+
+            if (localY > this._selectionStartY) {                
+                this._groupCandidateBox.style.top = `${this._selectionStartY / this.zoom}px`;
+                this._groupCandidateBox.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
+            } else {
+                this._groupCandidateBox.style.top = `${localY / this.zoom}px`;
+                this._groupCandidateBox.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
+            }
+
+            return;
+        }        
+
         // Candidate link
         if (this._candidateLink) {        
             const rootRect = this.canvasContainer.getBoundingClientRect();       
@@ -410,6 +463,23 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
             this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
             this._selectionBox.style.width = "0px";
             this._selectionBox.style.height = "0px";
+            return;
+        }
+
+        // Group?
+        if (evt.currentTarget === this._hostCanvas && evt.shiftKey) {
+            this._groupCandidateBox = this.props.globalState.hostDocument.createElement("div");
+            this._groupCandidateBox.classList.add("group-box");
+            this._groupContainer.appendChild(this._groupCandidateBox);
+
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+            this._selectionStartX = (evt.pageX - rootRect.left);
+            this._selectionStartY = (evt.pageY - rootRect.top);
+            this._groupCandidateBox.style.left = `${this._selectionStartX / this.zoom}px`;
+            this._groupCandidateBox.style.top = `${this._selectionStartY / this.zoom}px`;
+            this._groupCandidateBox.style.width = "0px";
+            this._groupCandidateBox.style.height = "0px";
+            return;
         }
 
         // Port dragging
@@ -444,6 +514,14 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
            this._selectionBox.parentElement!.removeChild(this._selectionBox);
            this._selectionBox = null;
         }
+
+        if (this._groupCandidateBox) {            
+            let newGroup = new GraphNodeGroup(this._groupCandidateBox, this);
+            this._groups.push(newGroup);
+
+            this._groupCandidateBox.parentElement!.removeChild(this._groupCandidateBox);
+            this._groupCandidateBox = null;
+         }
     }
 
     onWheel(evt: React.WheelEvent) {
@@ -580,6 +658,8 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
                 <div id="graph-container">
                     <div id="graph-canvas-container">
                     </div>     
+                    <div id="group-container">                        
+                    </div>
                     <svg id="graph-svg-container">
                     </svg>
                     <div id="selection-container">                        

+ 26 - 7
nodeEditor/src/diagram/graphNode.ts

@@ -11,6 +11,7 @@ import { DisplayLedger } from './displayLedger';
 import { IDisplayManager } from './display/displayManager';
 import { NodeLink } from './nodeLink';
 import { NodePort } from './nodePort';
+import { GraphNodeGroup } from './graphNodeGroup';
 
 export class GraphNode {
     private _visual: HTMLDivElement;
@@ -30,8 +31,9 @@ export class GraphNode {
     private _mouseStartPointX: Nullable<number> = null;
     private _mouseStartPointY: Nullable<number> = null    
     private _globalState: GlobalState;
-    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink>>>;   
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphNodeGroup>>>;   
     private _onSelectionBoxMovedObserver: Nullable<Observer<ClientRect | DOMRect>>;  
+    private _onGroupAboutToMoveObserver: Nullable<Observer<GraphNodeGroup>>;  
     private _onUpdateRequiredObserver: Nullable<Observer<void>>;  
     private _ownerCanvas: GraphCanvasComponent; 
     private _isSelected: boolean;
@@ -149,6 +151,19 @@ export class GraphNode {
 
             this.isSelected = overlap;
         });
+
+        this._onGroupAboutToMoveObserver = this._globalState.onGroupAboutToMove.add(group => {
+            const rect2 = this._visual.getBoundingClientRect();
+            const rect1 = group.element.getBoundingClientRect();
+            var overlap = !(rect1.right < rect2.left || 
+                rect1.left > rect2.right || 
+                rect1.bottom < rect2.top || 
+                rect1.top > rect2.bottom);
+            
+            if (overlap) {
+                group.nodes.push(this);
+            }
+        });
     }
 
     public getPortForConnectionPoint(point: NodeMaterialConnectionPoint) {
@@ -198,8 +213,8 @@ export class GraphNode {
             port.refresh();
         }
 
-        this._comments.innerHTML = this.block.comments;
-        this._comments.title = this.block.comments;
+        this._comments.innerHTML = this.block.comments || "";
+        this._comments.title = this.block.comments || "";
     }
 
     private _appendConnection(connectionPoint: NodeMaterialConnectionPoint, root: HTMLDivElement, displayManager: Nullable<IDisplayManager>) {
@@ -260,12 +275,12 @@ export class GraphNode {
             return;
         }
 
-        let newX = evt.clientX - this._mouseStartPointX;
-        let newY = evt.clientY - this._mouseStartPointY;
+        let newX = (evt.clientX - this._mouseStartPointX) / this._ownerCanvas.zoom;
+        let newY = (evt.clientY - this._mouseStartPointY) / this._ownerCanvas.zoom;
 
         for (var selectedNode of this._ownerCanvas.selectedNodes) {
-            selectedNode.x += newX / this._ownerCanvas.zoom;
-            selectedNode.y += newY / this._ownerCanvas.zoom;
+            selectedNode.x += newX;
+            selectedNode.y += newY;
         }
 
         this._mouseStartPointX = evt.clientX;
@@ -375,6 +390,10 @@ export class GraphNode {
             this._visual.parentElement.removeChild(this._visual);
         }
 
+        if (this._onGroupAboutToMoveObserver) {
+            this._globalState.onGroupAboutToMove.remove(this._onGroupAboutToMoveObserver);
+        }
+
         for (var port of this._inputPorts) {
             port.dispose();
         }

+ 163 - 0
nodeEditor/src/diagram/graphNodeGroup.ts

@@ -0,0 +1,163 @@
+import { GraphNode } from './graphNode';
+import { GraphCanvasComponent } from './graphCanvas';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { NodeLink } from './nodeLink';
+
+export class GraphNodeGroup {
+    private _name: string;
+    private _x = 0;
+    private _y = 0;
+    private _gridAlignedX = 0;
+    private _gridAlignedY = 0;    
+    public width: number;
+    public height: number;
+    public element: HTMLDivElement;    
+    private _nodes: GraphNode[] = [];
+    private _ownerCanvas: GraphCanvasComponent;
+    private _mouseStartPointX: Nullable<number> = null;
+    private _mouseStartPointY: Nullable<number> = null;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphNodeGroup>>>;   
+
+    public get nodes() {
+        return this._nodes;
+    }
+
+    public get gridAlignedX() {
+        return this._gridAlignedX;
+    }
+
+    public get gridAlignedY() {
+        return this._gridAlignedY;
+    }
+
+    public get name() {
+        return this._name;
+    }
+
+    public set name(value: string) {
+        this._name = value;
+        this.element.innerHTML = value;
+    }
+
+    public get x() {
+        return this._x;
+    }
+
+    public set x(value: number) {
+        if (this._x === value) {
+            return;
+        }
+        this._x = value;
+        
+        this._gridAlignedX = this._ownerCanvas.getGridPosition(value);
+        this.element.style.left = `${this._gridAlignedX}px`;
+    }
+
+    public get y() {
+        return this._y;
+    }
+
+    public set y(value: number) {
+        if (this._y === value) {
+            return;
+        }
+
+        this._y = value;
+
+        this._gridAlignedY = this._ownerCanvas.getGridPosition(value);
+        this.element.style.top = `${this._gridAlignedY}px`;
+    }    
+
+    public constructor(candidate: HTMLDivElement, canvas: GraphCanvasComponent) {
+        this._ownerCanvas = canvas;
+        const root = canvas.groupContainer;
+        this.element = root.ownerDocument!.createElement("div");        
+        this.element.classList.add("group-box");
+        root.appendChild(this.element);
+
+        this.x = parseFloat(candidate.style.left!.replace("px", ""));
+        this.y = parseFloat(candidate.style.top!.replace("px", ""));
+        this.width = parseFloat(candidate.style.width!.replace("px", ""));
+        this.height = parseFloat(candidate.style.height!.replace("px", ""));
+
+        this.cleanAccumulation();        
+
+        this.element.style.width = `${this.width / canvas.zoom}px`;
+        this.element.style.height = `${this.height / canvas.zoom}px`;
+        
+        this.element.addEventListener("pointerdown", evt => this._onDown(evt));
+        this.element.addEventListener("pointerup", evt => this._onUp(evt));
+        this.element.addEventListener("pointermove", evt => this._onMove(evt));
+
+        this._onSelectionChangedObserver = canvas.globalState.onSelectionChangedObservable.add(node => {
+            if (node === this) {
+                this.element.classList.add("selected");
+            } else {
+                this.element.classList.remove("selected");
+            }
+        });        
+    }
+
+    public cleanAccumulation() {
+        this.x = this.gridAlignedX;
+        this.y = this.gridAlignedY;
+    }
+
+    private _onDown(evt: PointerEvent) {
+        evt.stopPropagation();
+
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;        
+        
+        this.element.setPointerCapture(evt.pointerId);
+        this._ownerCanvas.globalState.onSelectionChangedObservable.notifyObservers(this);
+
+        // Get nodes
+        this._nodes = [];
+        this._ownerCanvas.globalState.onGroupAboutToMove.notifyObservers(this);
+    }    
+
+    private _onUp(evt: PointerEvent) {
+        evt.stopPropagation();
+
+        for (var selectedNode of this._nodes) {
+            selectedNode.cleanAccumulation();
+        }
+
+        this.cleanAccumulation();
+        this._mouseStartPointX = null;
+        this._mouseStartPointY = null;
+        this.element.releasePointerCapture(evt.pointerId);
+    }
+
+    private _onMove(evt: PointerEvent) {
+        if (this._mouseStartPointX === null || this._mouseStartPointY === null || evt.ctrlKey) {
+            return;
+        }
+
+        let newX = (evt.clientX - this._mouseStartPointX) / this._ownerCanvas.zoom;
+        let newY = (evt.clientY - this._mouseStartPointY) / this._ownerCanvas.zoom;
+
+        for (var selectedNode of this._nodes) {
+            selectedNode.x += newX;
+            selectedNode.y += newY;
+        }
+
+        this.x += newX;
+        this.y += newY;
+
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;   
+
+        evt.stopPropagation();
+    }
+
+    public dispose() {
+        if (this._onSelectionChangedObserver) {
+            this._ownerCanvas.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+        }
+
+        this.element.parentElement!.removeChild(this.element);
+    }
+}

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

@@ -3,6 +3,7 @@ import { GraphNode } from './graphNode';
 import { NodePort } from './nodePort';
 import { Nullable } from 'babylonjs/types';
 import { Observer } from 'babylonjs/Misc/observable';
+import { GraphNodeGroup } from './graphNodeGroup';
 
 export class NodeLink {   
     private _graphCanvas: GraphCanvasComponent;
@@ -12,7 +13,7 @@ export class NodeLink {
     private _nodeB?: GraphNode;
     private _path: SVGPathElement;
     private _selectionPath: SVGPathElement;
-    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink>>>;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphNodeGroup>>>;
 
     public get portA() {
         return this._portA;

+ 4 - 2
nodeEditor/src/globalState.ts

@@ -10,12 +10,13 @@ import { GraphNode } from './diagram/graphNode';
 import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { NodePort } from './diagram/nodePort';
 import { NodeLink } from './diagram/nodeLink';
+import { GraphNodeGroup } from './diagram/graphNodeGroup';
 
 export class GlobalState {
     nodeMaterial: NodeMaterial;
     hostElement: HTMLElement;
     hostDocument: HTMLDocument;
-    onSelectionChangedObservable = new Observable<Nullable<GraphNode | NodeLink>>();
+    onSelectionChangedObservable = new Observable<Nullable<GraphNode | NodeLink | GraphNodeGroup>>();
     onRebuildRequiredObservable = new Observable<void>();
     onResetRequiredObservable = new Observable<void>();
     onUpdateRequiredObservable = new Observable<void>();
@@ -31,7 +32,8 @@ export class GlobalState {
     onDepthPrePassChanged = new Observable<void>();
     onAnimationCommandActivated = new Observable<void>();
     onCandidateLinkMoved = new Observable<Nullable<Vector2>>();   
-    onSelectionBoxMoved = new Observable<ClientRect | DOMRect>();   
+    onSelectionBoxMoved = new Observable<ClientRect | DOMRect>();       
+    onGroupAboutToMove = new Observable<GraphNodeGroup>();   
     onCandidatePortSelected = new Observable<Nullable<NodePort>>();
     onGetNodeFromBlock: (block: NodeMaterialBlock) => GraphNode;
     onGridSizeChanged = new Observable<void>();

+ 4 - 6
nodeEditor/src/graphEditor.tsx

@@ -165,6 +165,10 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                     this._graphCanvas.selectedLink.dispose();
                 }
 
+                if (this._graphCanvas.selectedGroup) {
+                    this._graphCanvas.selectedGroup.dispose();
+                }
+
                 this.props.globalState.onSelectionChangedObservable.notifyObservers(null);  
                 this.props.globalState.onRebuildRequiredObservable.notifyObservers();  
                 return;
@@ -231,12 +235,6 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
             }
 
         }, false);
-
-        this.props.globalState.storeEditorData = (editorData) => {
-            editorData.zoom = this._graphCanvas.zoom;
-            editorData.x = this._graphCanvas.x;
-            editorData.y = this._graphCanvas.y;
-        }
     }
 
     zoomToFit() {

+ 2 - 19
nodeEditor/src/serializationTools.ts

@@ -28,26 +28,9 @@ export class SerializationTools {
         let bufferSerializationState = Texture.SerializeBuffers;
         Texture.SerializeBuffers = DataStorage.ReadBoolean("EmbedTextures", true);
 
-        let serializationObject = material.serialize();
+        this.UpdateLocations(material, globalState);
 
-        // Store node locations
-        for (var block of material.attachedBlocks) {
-            let node = globalState.onGetNodeFromBlock(block);
-
-            if (!serializationObject.editorData) {
-                serializationObject.editorData = {
-                    locations: []
-                };
-            }
-
-            serializationObject.editorData.locations.push({
-                blockId: block.uniqueId,
-                x: node ? node.x : 0,
-                y: node ? node.y : 0
-            });
-        }
-
-        globalState.storeEditorData(serializationObject.editorData);
+        let serializationObject = material.serialize();
 
         Texture.SerializeBuffers = bufferSerializationState;
 

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

@@ -1135,6 +1135,7 @@ export class NodeMaterial extends PushMaterial {
         serializationObject.customType = "BABYLON.NodeMaterial";
 
         serializationObject.outputNodes = [];
+        serializationObject.editorData = this.editorData;
 
         let blocks: NodeMaterialBlock[] = [];