瀏覽代碼

Frame collapse

David Catuhe 5 年之前
父節點
當前提交
d239a96541

+ 14 - 0
nodeEditor/src/components/propertyTab/propertyTabComponent.tsx

@@ -93,6 +93,20 @@ export class PropertyTabComponent extends React.Component<IPropertyTabComponentP
                         <LineContainerComponent title="GENERAL">
                             <TextInputLineComponent globalState={this.props.globalState} label="Name" propertyName="name" target={this.state.currentFrame} />
                             <Color3LineComponent label="Color" target={this.state.currentFrame} propertyName="color"></Color3LineComponent>
+                            {
+                                !this.state.currentFrame.isCollapsed &&
+                                <ButtonLineComponent label="Collapse" onClick={() => {
+                                        this.state.currentFrame!.isCollapsed = true;
+                                        this.forceUpdate();
+                                    }} />
+                            }
+                            {
+                                this.state.currentFrame.isCollapsed &&
+                                <ButtonLineComponent label="Expand" onClick={() => {
+                                        this.state.currentFrame!.isCollapsed = false;
+                                        this.forceUpdate();
+                                    }} />
+                            }
                         </LineContainerComponent>
                     </div>
                 </div>

+ 106 - 72
nodeEditor/src/diagram/graphCanvas.scss

@@ -22,6 +22,72 @@
         }
     }
 
+    .port {
+        border-radius: 20px;
+        width: 20px;
+        height: 20px;                                                    
+        align-self: center;   
+        
+        .img {
+            width: 100%;
+        }     
+        
+        &:hover, &.selected {
+            filter: brightness(2);
+        }
+    }
+
+    .portLine {
+        height: 24px;
+        display: grid;                
+        grid-template-rows: 100%;
+    }
+
+    .port-label {                   
+        align-items: center;
+    }
+
+    .inputsContainer {                        
+        grid-row: 1;
+        grid-column: 1;
+
+        .portLine {
+            grid-template-columns: 12px calc(100% - 15px);
+
+            .port-label {                    
+                grid-row: 1;
+                grid-column: 2;
+            }
+
+            .port {                              
+                grid-row: 1;
+                grid-column: 1;
+                transform: translateX(-12px);     
+            }
+        }
+    }
+
+    .outputsContainer {                  
+        grid-row: 1;
+        grid-column: 2;
+
+        .portLine {
+            grid-template-columns: calc(100% - 10px) 12px;
+
+            .port-label {                    
+                grid-row: 1;
+                grid-column: 1;
+                text-align: right;
+            }
+
+            .port {                              
+                grid-row: 1;
+                grid-column: 2;
+                transform: translateX(2px);                        
+            }        
+        }
+    }
+
     #graph-container {
         width: 100%;
         height: 100%;
@@ -50,6 +116,17 @@
             border: transparent solid 4px;
             box-sizing: border-box;
 
+            &.collapsed {
+                height: auto !important;
+                width: 200px !important;
+                border-radius: 12px;
+
+                .frame-box-header {
+                    font-size: 16px;
+                    border-radius: 12px 12px 0 0;
+                }
+            }
+
             .frame-box-header {
                 grid-row: 1;
                 grid-column: 1;
@@ -65,9 +142,20 @@
                 border-bottom: 0;
             }
 
+            .port-container {
+                margin-top: 10px;      
+                margin-bottom: 10px;      
+                color: white;
+                grid-row: 2;
+                grid-column: 1;
+                display: grid;
+                grid-template-rows: 100%;
+                grid-template-columns: 50% 50%; 
+                z-index: 2;
+            }
+
             &.selected {
                 border-color: white;
-                z-index: 1;
 
                 .frame-box-header {
                     border-color: white !important;
@@ -91,19 +179,28 @@
                     stroke: white !important;
                     stroke-dasharray: 10, 2;
                 }       
+
+                &.hidden {
+                    display: none;
+                }
             }
 
             .selection-link {
                 pointer-events: all;
                 stroke-width: 16px;
                 opacity: 0;
+                transition: opacity 75ms;
+                stroke: transparent;                        
+                cursor: pointer;
+
+                &.hidden {
+                    display: none;
+                }
+
                 &:hover, &.selected {
                     stroke: white !important;
                     opacity: 0.4;
                 }
-                transition: opacity 75ms;
-                stroke: transparent;                        
-                cursor: pointer;
             }
         }
 
@@ -128,6 +225,10 @@
                 grid-template-columns: 100%;
                 color: white;
 
+                &.hidden {
+                    display: none;
+                }
+
                 .comments {
                     position: absolute;
                     top: -50px;
@@ -191,74 +292,7 @@
                     grid-column: 1;
 
                     display: grid;
-                    grid-template-columns: 50% 50%;
-
-                    .port {
-                        border-radius: 20px;
-                        width: 20px;
-                        height: 20px;                                                    
-                        align-self: center;   
-                        
-                        .img {
-                            width: 100%;
-                        }     
-                        
-                        &:hover, &.selected {
-                            filter: brightness(2);
-                        }
-                    }
-
-                    .portLine {
-                        height: 24px;
-                        display: grid;                
-                        grid-template-rows: 100%;
-                    }
-
-                    .label {                   
-                        align-items: center;
-                    }
-
-                    .inputsContainer {                        
-                        grid-row: 1;
-                        grid-column: 1;
-
-                        .portLine {
-                            grid-template-columns: 12px calc(100% - 15px);
-
-                            .label {                    
-                                grid-row: 1;
-                                grid-column: 2;
-                            }
-
-                            .port {                              
-                                grid-row: 1;
-                                grid-column: 1;
-                                transform: translateX(-12px);     
-                            }
-                        }
-                    }
-
-                    .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);                        
-                            }
-                        
-                        }
-                    }
+                    grid-template-columns: 50% 50%;                  
                 }
 
                 .content {

+ 139 - 13
nodeEditor/src/diagram/graphFrame.ts

@@ -5,6 +5,7 @@ import { Observer } from 'babylonjs/Misc/observable';
 import { NodeLink } from './nodeLink';
 import { IFrameData } from '../nodeLocationInfo';
 import { Color3 } from 'babylonjs/Maths/math.color';
+import { NodePort } from './nodePort';
 
 export class GraphFrame {
     private _name: string;
@@ -17,11 +18,96 @@ export class GraphFrame {
     private _height: number;
     public element: HTMLDivElement;   
     private _headerElement: HTMLDivElement;    
+    private _portContainer: HTMLDivElement;    
+    private _outputPortContainer: HTMLDivElement;    
+    private _inputPortContainer: HTMLDivElement;    
     private _nodes: GraphNode[] = [];
     private _ownerCanvas: GraphCanvasComponent;
     private _mouseStartPointX: Nullable<number> = null;
     private _mouseStartPointY: Nullable<number> = null;
     private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame>>>;   
+    private _isCollapsed = false;
+    private _ports: NodePort[] = [];
+    private _controlledPorts: NodePort[] = [];
+
+    public get isCollapsed() {
+        return this._isCollapsed;
+    }
+
+    public set isCollapsed(value: boolean) {
+        if (this._isCollapsed === value) {
+            return;
+        }
+
+        this._isCollapsed = value;
+
+        // Need to delegate the outside ports to the frame
+        if (value) {
+            this.element.classList.add("collapsed");
+
+            for (var node of this._nodes) {
+                node.isVisible = false;
+                for (var port of node.outputPorts) { // Output
+                    if (port.connectionPoint.hasEndpoints) {
+                        let portAdded = false;
+
+                        for (var link of node.links) {
+                            if (link.portA === port && this.nodes.indexOf(link.nodeB!) === -1) {
+                                let localPort: NodePort;
+
+                                if (!portAdded) {
+                                    portAdded = true;
+                                    localPort = NodePort.CreatePortElement(port.connectionPoint, link.nodeB!, this._outputPortContainer, null, this._ownerCanvas.globalState)
+                                    this._ports.push(localPort);
+                                } else {
+                                    localPort = this._ports.filter(p => p.connectionPoint === port.connectionPoint)[0];
+                                }
+
+                                port.delegatedPort = localPort;
+                                this._controlledPorts.push(port);
+                                link.isVisible = true;
+                            }
+                        }
+                    }
+                }
+
+                for (var port of node.inputPorts) { // Input
+                    if (port.connectionPoint.isConnected) {
+                        for (var link of node.links) {
+                            if (link.portB === port && this.nodes.indexOf(link.nodeA) === -1) {
+                                let localPort = NodePort.CreatePortElement(port.connectionPoint, link.nodeA, this._inputPortContainer, null, this._ownerCanvas.globalState)
+                                this._ports.push(localPort);
+
+                                port.delegatedPort = localPort;
+                                this._controlledPorts.push(port);
+                                link.isVisible = true;
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            this.element.classList.remove("collapsed");
+            this._outputPortContainer.innerHTML = "";
+            this._inputPortContainer.innerHTML = "";
+
+            this._ports.forEach(p => {
+                p.dispose();
+            });
+
+            this._controlledPorts.forEach(port => {
+                port.delegatedPort = null;
+                port.refresh();
+            })
+
+            this._ports = [];
+            this._controlledPorts = [];
+
+            for (var node of this._nodes) {
+                node.isVisible = true;
+            }
+        }
+    }
 
     public get nodes() {
         return this._nodes;
@@ -106,7 +192,7 @@ export class GraphFrame {
         this.element.style.height = `${gridAlignedBottom - this._gridAlignedY}px`;
     }
 
-    public constructor(candidate: Nullable<HTMLDivElement>, canvas: GraphCanvasComponent) {
+    public constructor(candidate: Nullable<HTMLDivElement>, canvas: GraphCanvasComponent, doNotCaptureNodes = false) {
         this._ownerCanvas = canvas;
         const root = canvas.frameContainer;
         this.element = root.ownerDocument!.createElement("div");        
@@ -115,8 +201,23 @@ export class GraphFrame {
 
         this._headerElement = root.ownerDocument!.createElement("div");  
         this._headerElement.classList.add("frame-box-header");
+        this._headerElement.addEventListener("dblclick", () => {
+            this.isCollapsed = !this.isCollapsed;
+        });
         this.element.appendChild(this._headerElement);
 
+        this._portContainer = root.ownerDocument!.createElement("div");  
+        this._portContainer.classList.add("port-container");
+        this.element.appendChild(this._portContainer);
+
+        this._outputPortContainer = root.ownerDocument!.createElement("div");  
+        this._outputPortContainer.classList.add("outputsContainer");
+        this._portContainer.appendChild(this._outputPortContainer);
+
+        this._inputPortContainer = root.ownerDocument!.createElement("div");  
+        this._inputPortContainer.classList.add("inputsContainer");
+        this._portContainer.appendChild(this._inputPortContainer);
+
         this.name = "Frame";
         this.color = Color3.FromInts(72, 72, 72);
 
@@ -142,23 +243,41 @@ export class GraphFrame {
         });  
                 
         // Get nodes
+        if (!doNotCaptureNodes) {
+            this.refresh();
+        }
+    }
+
+    public refresh() {
         this._nodes = [];
         this._ownerCanvas.globalState.onFrameCreated.notifyObservers(this);
     }
 
+    public addNode(node: GraphNode) {
+        let index = this.nodes.indexOf(node);
+
+        if (index === -1) {
+            this.nodes.push(node);
+        }
+    }
+
+    public removeNode(node: GraphNode) {
+        let index = this.nodes.indexOf(node);
+
+        if (index > -1) {
+            this.nodes.splice(index, 1);
+        }
+    }
+
     public syncNode(node: GraphNode) {
-        if (node.isOverlappingFrame(this)) {
-            let index = this.nodes.indexOf(node);
+        if (this.isCollapsed) {
+            return;
+        }
 
-            if (index === -1) {
-                this.nodes.push(node);
-            }
+        if (node.isOverlappingFrame(this)) {
+            this.addNode(node);
         } else {
-            let index = this.nodes.indexOf(node);
-
-            if (index > -1) {
-                this.nodes.splice(index, 1);
-            }
+            this.removeNode(node);
         }
     }
 
@@ -217,6 +336,8 @@ export class GraphFrame {
     }
 
     public dispose() {
+        this.isCollapsed = false;
+
         if (this._onSelectionChangedObserver) {
             this._ownerCanvas.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
         }
@@ -234,12 +355,13 @@ export class GraphFrame {
             width: this._width,
             height: this._height,
             color: this._color.asArray(),
-            name: this.name
+            name: this.name,
+            isCollapsed: this.isCollapsed
         }
     }
 
     public static Parse(serializationData: IFrameData, canvas: GraphCanvasComponent) {
-        let newFrame = new GraphFrame(null, canvas);
+        let newFrame = new GraphFrame(null, canvas, true);
 
         newFrame.x = serializationData.x;
         newFrame.y = serializationData.y;
@@ -248,6 +370,10 @@ export class GraphFrame {
         newFrame.name = serializationData.name;
         newFrame.color = Color3.FromArray(serializationData.color);
 
+        newFrame.refresh();
+
+        newFrame.isCollapsed = !!serializationData.isCollapsed;
+
         return newFrame;
     }
 }

+ 31 - 17
nodeEditor/src/diagram/graphNode.ts

@@ -38,6 +38,35 @@ export class GraphNode {
     private _ownerCanvas: GraphCanvasComponent; 
     private _isSelected: boolean;
     private _displayManager: Nullable<IDisplayManager> = null;
+    private _isVisible = true;
+
+    public get isVisible() {
+        return this._isVisible;
+    }
+
+    public set isVisible(value: boolean) {
+        this._isVisible = value;
+
+        if (!value) {
+            this._visual.classList.add("hidden");
+        } else {
+            this._visual.classList.remove("hidden");
+        }
+
+        for (var link of this._links) {
+            link.isVisible = value;
+        }
+
+        this._refreshLinks();
+    }
+
+    public get outputPorts() {
+        return this._outputPorts;
+    }
+
+    public get inputPorts() {
+        return this._inputPorts;
+    }
 
     public get links() {
         return this._links;
@@ -237,21 +266,6 @@ export class GraphNode {
 
     }
 
-    private _appendConnection(connectionPoint: NodeMaterialConnectionPoint, root: HTMLDivElement, displayManager: Nullable<IDisplayManager>) {
-        let portContainer = root.ownerDocument!.createElement("div");
-        portContainer.classList.add("portLine");
-        root.appendChild(portContainer);
-
-        if (!displayManager || displayManager.shouldDisplayPortLabels(this.block)) {
-            let portLabel = root.ownerDocument!.createElement("div");
-            portLabel.classList.add("label");
-            portLabel.innerHTML = connectionPoint.name;        
-            portContainer.appendChild(portLabel);
-        }
-    
-        return new NodePort(portContainer, connectionPoint, this, this._globalState);
-    }
-
     private _onDown(evt: PointerEvent) {
         // Check if this is coming from the port
         if (evt.srcElement && (evt.srcElement as HTMLElement).nodeName === "IMG") {
@@ -376,11 +390,11 @@ export class GraphNode {
 
         // Connections
         for (var input of this.block.inputs) {
-            this._inputPorts.push(this._appendConnection(input, this._inputsContainer, this._displayManager));
+            this._inputPorts.push(NodePort.CreatePortElement(input,  this, this._inputsContainer, this._displayManager, this._globalState));
         }
 
         for (var output of this.block.outputs) {
-            this._outputPorts.push(this._appendConnection(output, this._outputsContainer, this._displayManager));
+            this._outputPorts.push(NodePort.CreatePortElement(output,  this, this._outputsContainer, this._displayManager, this._globalState));
         }
 
         this.refresh();

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

@@ -2,18 +2,39 @@ import { GraphCanvasComponent } from './graphCanvas';
 import { GraphNode } from './graphNode';
 import { NodePort } from './nodePort';
 import { Nullable } from 'babylonjs/types';
-import { Observer } from 'babylonjs/Misc/observable';
+import { Observer, Observable } from 'babylonjs/Misc/observable';
 import { GraphFrame } from './graphFrame';
 
 export class NodeLink {   
     private _graphCanvas: GraphCanvasComponent;
     private _portA: NodePort;
-    private _portB?: NodePort;
+    private _portB?: NodePort;    
     private _nodeA: GraphNode;
     private _nodeB?: GraphNode;
     private _path: SVGPathElement;
     private _selectionPath: SVGPathElement;
-    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame>>>;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame>>>;    
+    private _isVisible = true;
+
+    public onDisposedObservable = new Observable<NodeLink>();
+
+    public get isVisible() {
+        return this._isVisible;
+    }
+
+    public set isVisible(value: boolean) {
+        this._isVisible = value;
+
+        if (!value) {
+            this._path.classList.add("hidden");
+            this._selectionPath.classList.add("hidden");
+        } else {
+            this._path.classList.remove("hidden");
+            this._selectionPath.classList.remove("hidden");
+        }
+
+        this.update();
+    }
 
     public get portA() {
         return this._portA;
@@ -23,6 +44,14 @@ export class NodeLink {
         return this._portB;
     }
 
+    public get nodeA() {
+        return this._nodeA;
+    }
+
+    public get nodeB() {
+        return this._nodeB;
+    }
+
     public update(endX = 0, endY = 0, straight = false) {   
         const rectA = this._portA.element.getBoundingClientRect();
         const rootRect = this._graphCanvas.canvasContainer.getBoundingClientRect();
@@ -109,9 +138,12 @@ export class NodeLink {
         if (this._nodeB) {
             this._nodeA.links.splice(this._nodeA.links.indexOf(this), 1);
             this._nodeB.links.splice(this._nodeB.links.indexOf(this), 1);
+            this._nodeB.links.splice(this._nodeB.links.indexOf(this), 1);
             this._graphCanvas.links.splice(this._graphCanvas.links.indexOf(this), 1);
 
             this._portA.connectionPoint.disconnectFrom(this._portB!.connectionPoint);
         }
+
+        this.onDisposedObservable.notifyObservers(this);
     }   
 }

+ 27 - 2
nodeEditor/src/diagram/nodePort.ts

@@ -1,11 +1,12 @@
 import { BlockTools } from '../blockTools';
-import { GraphNode } from './graphNode';
 import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
 import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
 import { GlobalState } from '../globalState';
 import { Nullable } from 'babylonjs/types';
 import { Observer } from 'babylonjs/Misc/observable';
 import { Vector2 } from 'babylonjs/Maths/math.vector';
+import { IDisplayManager } from './display/displayManager';
+import { GraphNode } from './graphNode';
 
 
 export class NodePort {
@@ -14,7 +15,13 @@ export class NodePort {
     private _globalState: GlobalState;
     private _onCandidateLinkMovedObserver: Nullable<Observer<Nullable<Vector2>>>;
 
-    public get element() {
+    public delegatedPort: Nullable<NodePort> = null;
+
+    public get element(): HTMLDivElement {
+        if (this.delegatedPort) {
+            return this.delegatedPort.element;
+        }
+
         return this._element;
     }
 
@@ -74,4 +81,22 @@ export class NodePort {
     public dispose() {
         this._globalState.onCandidateLinkMoved.remove(this._onCandidateLinkMovedObserver);
     }
+
+    public static CreatePortElement(connectionPoint: NodeMaterialConnectionPoint, node: GraphNode, root: HTMLElement, 
+            displayManager: Nullable<IDisplayManager>, globalState: GlobalState) {
+        let portContainer = root.ownerDocument!.createElement("div");
+        let block = connectionPoint.ownerBlock;
+
+        portContainer.classList.add("portLine");
+        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 NodePort(portContainer, connectionPoint, node, globalState);
+    }
 }

+ 4 - 3
nodeEditor/src/graphEditor.tsx

@@ -174,7 +174,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                 return;
             }
 
-            if (!evt.ctrlKey) {
+            if (!evt.ctrlKey || this.props.globalState.blockKeyboardEvents) {
                 return;
             }
 
@@ -303,8 +303,6 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         if (!editorData || !editorData.locations) {
             this._graphCanvas.distributeGraph();
         } else {
-            this._graphCanvas.processEditorData(editorData);
-
             // Locations
             for (var location of editorData.locations) {
                 for (var node of this._graphCanvas.nodes) {
@@ -316,6 +314,8 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
                     }
                 }
             }
+
+            this._graphCanvas.processEditorData(editorData);
         }
     }
 
@@ -388,6 +388,7 @@ export class GraphEditor extends React.Component<IGraphEditorProps> {
         newNode.y = y / this._graphCanvas.zoom;
         newNode.cleanAccumulation();
 
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
         this.props.globalState.onSelectionChangedObservable.notifyObservers(newNode);
 
         let block = newNode.block;

+ 2 - 1
nodeEditor/src/nodeLocationInfo.ts

@@ -10,7 +10,8 @@ export interface IFrameData {
     width: number;
     height: number;
     color: number[];
-    name: string
+    name: string,
+    isCollapsed: boolean
 }
 
 export interface IEditorData {