Sfoglia il codice sorgente

creating folder for gui editor

Pamela Wolf 4 anni fa
parent
commit
ad7010b28f
100 ha cambiato i file con 13008 aggiunte e 0 eliminazioni
  1. 1 0
      guiEditor/README-ES6.md
  2. 18 0
      guiEditor/README.md
  3. 10 0
      guiEditor/imgs/add.svg
  4. 10 0
      guiEditor/imgs/copy.svg
  5. 10 0
      guiEditor/imgs/delete.svg
  6. 3 0
      guiEditor/imgs/downArrow.svg
  7. 43 0
      guiEditor/public/index-local.html
  8. 42 0
      guiEditor/public/index.html
  9. 151 0
      guiEditor/public/index.js
  10. 580 0
      guiEditor/src/blockTools.ts
  11. 20 0
      guiEditor/src/components/log/log.scss
  12. 64 0
      guiEditor/src/components/log/logComponent.tsx
  13. 211 0
      guiEditor/src/components/nodeList/nodeList.scss
  14. 326 0
      guiEditor/src/components/nodeList/nodeListComponent.tsx
  15. 114 0
      guiEditor/src/components/preview/previewAreaComponent.tsx
  16. 558 0
      guiEditor/src/components/preview/previewManager.ts
  17. 173 0
      guiEditor/src/components/preview/previewMeshControlComponent.tsx
  18. 19 0
      guiEditor/src/components/preview/previewType.ts
  19. 10 0
      guiEditor/src/components/preview/svgs/colorPicker.svg
  20. 1 0
      guiEditor/src/components/preview/svgs/depthPass.svg
  21. 1 0
      guiEditor/src/components/preview/svgs/directionalLeft.svg
  22. 1 0
      guiEditor/src/components/preview/svgs/directionalRight.svg
  23. 1 0
      guiEditor/src/components/preview/svgs/doubleSided.svg
  24. 10 0
      guiEditor/src/components/preview/svgs/omni.svg
  25. 10 0
      guiEditor/src/components/preview/svgs/pauseIcon.svg
  26. 10 0
      guiEditor/src/components/preview/svgs/playIcon.svg
  27. 10 0
      guiEditor/src/components/preview/svgs/popOut.svg
  28. 21 0
      guiEditor/src/components/propertyTab/properties/color3PropertyTabComponent.tsx
  29. 21 0
      guiEditor/src/components/propertyTab/properties/color4PropertyTabComponent.tsx
  30. 24 0
      guiEditor/src/components/propertyTab/properties/floatPropertyTabComponent.tsx
  31. 26 0
      guiEditor/src/components/propertyTab/properties/matrixPropertyTabComponent.tsx
  32. 21 0
      guiEditor/src/components/propertyTab/properties/vector2PropertyTabComponent.tsx
  33. 21 0
      guiEditor/src/components/propertyTab/properties/vector3PropertyTabComponent.tsx
  34. 21 0
      guiEditor/src/components/propertyTab/properties/vector4PropertyTabComponent.tsx
  35. 775 0
      guiEditor/src/components/propertyTab/propertyTab.scss
  36. 470 0
      guiEditor/src/components/propertyTab/propertyTabComponent.tsx
  37. 28 0
      guiEditor/src/diagram/display/clampDisplayManager.ts
  38. 24 0
      guiEditor/src/diagram/display/discardDisplayManager.ts
  39. 9 0
      guiEditor/src/diagram/display/displayManager.ts
  40. 29 0
      guiEditor/src/diagram/display/gradientDisplayManager.ts
  41. 143 0
      guiEditor/src/diagram/display/inputDisplayManager.ts
  42. 24 0
      guiEditor/src/diagram/display/outputDisplayManager.ts
  43. 49 0
      guiEditor/src/diagram/display/remapDisplayManager.ts
  44. 60 0
      guiEditor/src/diagram/display/textureDisplayManager.ts
  45. 28 0
      guiEditor/src/diagram/display/trigonometryDisplayManager.ts
  46. 27 0
      guiEditor/src/diagram/displayLedger.ts
  47. 87 0
      guiEditor/src/diagram/frameNodePort.ts
  48. 626 0
      guiEditor/src/diagram/graphCanvas.scss
  49. 945 0
      guiEditor/src/diagram/graphCanvas.tsx
  50. 1498 0
      guiEditor/src/diagram/graphFrame.ts
  51. 488 0
      guiEditor/src/diagram/graphNode.ts
  52. 156 0
      guiEditor/src/diagram/nodeLink.ts
  53. 197 0
      guiEditor/src/diagram/nodePort.ts
  54. 78 0
      guiEditor/src/diagram/properties/frameNodePortPropertyComponent.tsx
  55. 70 0
      guiEditor/src/diagram/properties/framePropertyComponent.tsx
  56. 145 0
      guiEditor/src/diagram/properties/genericNodePropertyComponent.tsx
  57. 151 0
      guiEditor/src/diagram/properties/gradientNodePropertyComponent.tsx
  58. 88 0
      guiEditor/src/diagram/properties/gradientStepComponent.tsx
  59. 323 0
      guiEditor/src/diagram/properties/inputNodePropertyComponent.tsx
  60. 32 0
      guiEditor/src/diagram/properties/lightInformationPropertyTabComponent.tsx
  61. 38 0
      guiEditor/src/diagram/properties/lightPropertyTabComponent.tsx
  62. 62 0
      guiEditor/src/diagram/properties/nodePortPropertyComponent.tsx
  63. 7 0
      guiEditor/src/diagram/properties/propertyComponentProps.ts
  64. 317 0
      guiEditor/src/diagram/properties/texturePropertyTabComponent.tsx
  65. 32 0
      guiEditor/src/diagram/properties/transformNodePropertyComponent.tsx
  66. 55 0
      guiEditor/src/diagram/properties/trigonometryNodePropertyComponent.tsx
  67. 26 0
      guiEditor/src/diagram/propertyLedger.ts
  68. 92 0
      guiEditor/src/globalState.ts
  69. 900 0
      guiEditor/src/graphEditor.tsx
  70. 1 0
      guiEditor/src/index.ts
  71. 9 0
      guiEditor/src/legacy/legacy.ts
  72. 387 0
      guiEditor/src/main.scss
  73. 94 0
      guiEditor/src/nodeEditor.ts
  74. 26 0
      guiEditor/src/nodeLocationInfo.ts
  75. 17 0
      guiEditor/src/portal.tsx
  76. 58 0
      guiEditor/src/serializationTools.ts
  77. 21 0
      guiEditor/src/sharedComponents/buttonLineComponent.tsx
  78. 95 0
      guiEditor/src/sharedComponents/checkBoxLineComponent.tsx
  79. 173 0
      guiEditor/src/sharedComponents/color3LineComponent.tsx
  80. 185 0
      guiEditor/src/sharedComponents/color4LineComponent.tsx
  81. 131 0
      guiEditor/src/sharedComponents/colorPickerComponent.tsx
  82. 10 0
      guiEditor/src/sharedComponents/copy.svg
  83. 25 0
      guiEditor/src/sharedComponents/draggableLineComponent.tsx
  84. 31 0
      guiEditor/src/sharedComponents/draggableLineWithButtonComponent.tsx
  85. 38 0
      guiEditor/src/sharedComponents/fileButtonLineComponent.tsx
  86. 145 0
      guiEditor/src/sharedComponents/floatLineComponent.tsx
  87. 69 0
      guiEditor/src/sharedComponents/lineContainerComponent.tsx
  88. 52 0
      guiEditor/src/sharedComponents/lineWithFileButtonComponent.tsx
  89. 171 0
      guiEditor/src/sharedComponents/matrixLineComponent.tsx
  90. 42 0
      guiEditor/src/sharedComponents/messageDialog.tsx
  91. 10 0
      guiEditor/src/sharedComponents/minus.svg
  92. 76 0
      guiEditor/src/sharedComponents/numericInputComponent.tsx
  93. 110 0
      guiEditor/src/sharedComponents/optionsLineComponent.tsx
  94. 10 0
      guiEditor/src/sharedComponents/plus.svg
  95. 80 0
      guiEditor/src/sharedComponents/popup.ts
  96. 6 0
      guiEditor/src/sharedComponents/propertyChangedEvent.ts
  97. 126 0
      guiEditor/src/sharedComponents/sliderLineComponent.tsx
  98. 120 0
      guiEditor/src/sharedComponents/textInputLineComponent.tsx
  99. 49 0
      guiEditor/src/sharedComponents/textLineComponent.tsx
  100. 0 0
      guiEditor/src/sharedComponents/textureLineComponent.tsx

+ 1 - 0
guiEditor/README-ES6.md

@@ -0,0 +1 @@
+Node Editor

+ 18 - 0
guiEditor/README.md

@@ -0,0 +1,18 @@
+# Babylon.js Node Editor
+
+An extension to easily create or update any NodeMaterial.
+
+## Usage
+### Online method
+Call the method `Show` of the `BABYLON.NodeEditor` class: 
+```
+BABYLON.NodeEditor.Show({hostElement: document.getElementById("host")});
+```
+This method will retrieve dynamically the library `babylon.nodeEditor.js`, download it and add
+it to the html page.
+
+### Offline method
+If you don't have access to internet, the node editor should be imported manually in your HTML page :
+```
+<script src="babylon.nodeEditor.js" />
+``` 

File diff suppressed because it is too large
+ 10 - 0
guiEditor/imgs/add.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/imgs/copy.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/imgs/delete.svg


+ 3 - 0
guiEditor/imgs/downArrow.svg

@@ -0,0 +1,3 @@
+<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="17" height="17">
+  <path d="M4.21967 8.46967C4.51256 8.17678 4.98744 8.17678 5.28033 8.46967L12 15.1893L18.7197 8.46967C19.0126 8.17678 19.4874 8.17678 19.7803 8.46967C20.0732 8.76256 20.0732 9.23744 19.7803 9.53033L12.5303 16.7803C12.2374 17.0732 11.7626 17.0732 11.4697 16.7803L4.21967 9.53033C3.92678 9.23744 3.92678 8.76256 4.21967 8.46967Z" fill="white" />
+</svg>

+ 43 - 0
guiEditor/public/index-local.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <title>Node Material Editor - Local Development</title>    
+    <meta name="viewport" content="width=device-width, user-scalable=no">
+    <link rel="shortcut icon" href="https://www.babylonjs.com/favicon.ico">
+
+    <script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
+    <script src="../../Tools/DevLoader/BabylonLoader.js"></script>
+    <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css"></link>
+
+    <style>
+        html,
+        body {
+            width: 100%;
+            height: 100%;
+            padding: 0;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        #host-element {
+            width: 100%;
+            height: 100%;            
+        }
+    </style>
+</head>
+
+<body>
+    <div id="host-element">
+    </div>
+    <script>
+        // Load the scripts + map file to allow vscode debug.
+        BABYLONDEVTOOLS.Loader
+            .require("index.js")
+            .load(() => {
+                BABYLONDEVTOOLS.Loader.debugShortcut(engine);
+            });
+    </script>
+</body>
+
+</html>

+ 42 - 0
guiEditor/public/index.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <title>Babylon.js Node Material Editor</title>
+
+    <meta name="viewport" content="width=device-width, user-scalable=no">
+    <link rel="shortcut icon" href="https://www.babylonjs.com/favicon.ico">
+
+    <script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
+    <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css"></link>
+    <script src="https://preview.babylonjs.com/babylon.js"></script>
+    <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
+    <script src="https://preview.babylonjs.com/nodeEditor/babylon.nodeEditor.js"></script>    
+    
+    <style>
+        html,
+        body {
+            width: 100%;
+            height: 100%;
+            padding: 0;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        #host-element {
+            width: 100%;
+            height: 100%;               
+            padding: 0;
+            margin: 0;
+            overflow: hidden;         
+        }
+    </style>
+</head>
+
+<body>    
+    <div id="host-element">
+    </div>
+    <script src="index.js"></script>
+</body>
+
+</html>

+ 151 - 0
guiEditor/public/index.js

@@ -0,0 +1,151 @@
+var snippetUrl = "https://snippet.babylonjs.com";
+var currentSnippetToken;
+var previousHash = "";
+var nodeMaterial;
+
+var customLoadObservable = new BABYLON.Observable();
+var editorDisplayed = false;
+
+var cleanHash = function () {
+    var splits = decodeURIComponent(location.hash.substr(1)).split("#");
+
+    if (splits.length > 2) {
+        splits.splice(2, splits.length - 2);
+    }
+
+    location.hash = splits.join("#");
+}
+
+var checkHash = function () {
+    if (location.hash) {
+        if (previousHash != location.hash) {
+            cleanHash();
+
+            previousHash = location.hash;
+
+            try {
+                var xmlHttp = new XMLHttpRequest();
+                xmlHttp.onreadystatechange = function () {
+                    if (xmlHttp.readyState == 4) {
+                        if (xmlHttp.status == 200) {
+                            var snippet = JSON.parse(JSON.parse(xmlHttp.responseText).jsonPayload);
+                            let serializationObject = JSON.parse(snippet.nodeMaterial);
+
+                            if (editorDisplayed) {
+                                customLoadObservable.notifyObservers(serializationObject);
+                            } else {
+                                nodeMaterial.loadFromSerialization(serializationObject);
+                                try {
+                                    nodeMaterial.build(true);
+                                } catch (err) {
+                                     // Swallow the error here
+                                }
+                                showEditor();
+                            }
+                        }
+                    }
+                }
+
+                var hash = location.hash.substr(1);
+                currentSnippetToken = hash.split("#")[0];
+                xmlHttp.open("GET", snippetUrl + "/" + hash.replace("#", "/"));
+                xmlHttp.send();
+            } catch (e) {
+
+            }
+        }
+    }
+
+    setTimeout(checkHash, 200);
+}
+
+var showEditor = function() {
+    editorDisplayed = true;
+    var hostElement = document.getElementById("host-element");
+
+    BABYLON.NodeEditor.Show({
+        nodeMaterial: nodeMaterial, 
+        hostElement: hostElement,
+        customLoadObservable: customLoadObservable,
+        customSave: {
+            label: "Save as unique URL",
+            action: (data) => {
+                return new Promise((resolve, reject) => {
+                    var xmlHttp = new XMLHttpRequest();
+                    xmlHttp.onreadystatechange = function () {
+                        if (xmlHttp.readyState == 4) {
+                            if (xmlHttp.status == 200) {
+                                var baseUrl = location.href.replace(location.hash, "").replace(location.search, "");
+                                var snippet = JSON.parse(xmlHttp.responseText);
+                                var newUrl = baseUrl + "#" + snippet.id;
+                                currentSnippetToken = snippet.id;
+                                if (snippet.version && snippet.version != "0") {
+                                    newUrl += "#" + snippet.version;
+                                }
+                                location.href = newUrl;
+                                resolve();
+                            }
+                            else {
+                                reject(`Unable to save your node material. It may be too large (${(dataToSend.payload.length / 1024).toFixed(2)} KB) because of embedded textures. Please reduce texture sizes or point to a specific url instead of embedding them and try again.`);
+                            }
+                        }
+                    }
+        
+                    xmlHttp.open("POST", snippetUrl + (currentSnippetToken ? "/" + currentSnippetToken : ""), true);
+                    xmlHttp.setRequestHeader("Content-Type", "application/json");
+        
+                    var dataToSend = {
+                        payload : JSON.stringify({
+                            nodeMaterial: data
+                        }),
+                        name: "",
+                        description: "",
+                        tags: ""
+                    };
+        
+                    xmlHttp.send(JSON.stringify(dataToSend));
+                });
+            }
+        }
+    });
+}
+
+// Let's start
+if (BABYLON.Engine.isSupported()) {
+    var canvas = document.createElement("canvas");
+    var engine = new BABYLON.Engine(canvas, false, {disableWebGL2Support: true});
+    var scene = new BABYLON.Scene(engine);    
+    var light0 = new BABYLON.HemisphericLight("light #0", new BABYLON.Vector3(0, 1, 0), scene);
+    var light1 = new BABYLON.HemisphericLight("light #1", new BABYLON.Vector3(0, 1, 0), scene);
+    var light2 = new BABYLON.HemisphericLight("light #2", new BABYLON.Vector3(0, 1, 0), scene);
+
+    nodeMaterial = new BABYLON.NodeMaterial("node");
+
+    // Set to default
+    if (!location.hash) {
+        const mode = BABYLON.DataStorage.ReadNumber("Mode", BABYLON.NodeMaterialModes.Material);
+        
+        switch (mode) {
+            case BABYLON.NodeMaterialModes.Material:
+                nodeMaterial.setToDefault();
+                break;
+            case BABYLON.NodeMaterialModes.PostProcess:
+                nodeMaterial.setToDefaultPostProcess();
+                break;
+            case BABYLON.NodeMaterialModes.Particle:
+                nodeMaterial.setToDefaultParticle();
+                break;                
+            case BABYLON.NodeMaterialModes.ProceduralTexture:
+                nodeMaterial.setToDefaultProceduralTexture();
+                break;
+        }
+        nodeMaterial.build(true);
+        showEditor();
+    }
+
+}
+else {
+    alert('Babylon.js is not supported.')
+}
+
+checkHash();

+ 580 - 0
guiEditor/src/blockTools.ts

@@ -0,0 +1,580 @@
+import { DiscardBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/discardBlock';
+import { BonesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/bonesBlock';
+import { InstancesBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/instancesBlock';
+import { MorphTargetsBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/morphTargetsBlock';
+import { ImageProcessingBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/imageProcessingBlock';
+import { ColorMergerBlock } from 'babylonjs/Materials/Node/Blocks/colorMergerBlock';
+import { VectorMergerBlock } from 'babylonjs/Materials/Node/Blocks/vectorMergerBlock';
+import { ColorSplitterBlock } from 'babylonjs/Materials/Node/Blocks/colorSplitterBlock';
+import { VectorSplitterBlock } from 'babylonjs/Materials/Node/Blocks/vectorSplitterBlock';
+import { RemapBlock } from 'babylonjs/Materials/Node/Blocks/remapBlock';
+import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/textureBlock';
+import { ReflectionTextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/reflectionTextureBlock';
+import { LightBlock } from 'babylonjs/Materials/Node/Blocks/Dual/lightBlock';
+import { FogBlock } from 'babylonjs/Materials/Node/Blocks/Dual/fogBlock';
+import { VertexOutputBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/vertexOutputBlock';
+import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
+import { NormalizeBlock } from 'babylonjs/Materials/Node/Blocks/normalizeBlock';
+import { AddBlock } from 'babylonjs/Materials/Node/Blocks/addBlock';
+import { ModBlock } from 'babylonjs/Materials/Node/Blocks/modBlock';
+import { ScaleBlock } from 'babylonjs/Materials/Node/Blocks/scaleBlock';
+import { TrigonometryBlock, TrigonometryBlockOperations } from 'babylonjs/Materials/Node/Blocks/trigonometryBlock';
+import { ClampBlock } from 'babylonjs/Materials/Node/Blocks/clampBlock';
+import { CrossBlock } from 'babylonjs/Materials/Node/Blocks/crossBlock';
+import { DotBlock } from 'babylonjs/Materials/Node/Blocks/dotBlock';
+import { MultiplyBlock } from 'babylonjs/Materials/Node/Blocks/multiplyBlock';
+import { TransformBlock } from 'babylonjs/Materials/Node/Blocks/transformBlock';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { FresnelBlock } from 'babylonjs/Materials/Node/Blocks/fresnelBlock';
+import { LerpBlock } from 'babylonjs/Materials/Node/Blocks/lerpBlock';
+import { NLerpBlock } from 'babylonjs/Materials/Node/Blocks/nLerpBlock';
+import { DivideBlock } from 'babylonjs/Materials/Node/Blocks/divideBlock';
+import { SubtractBlock } from 'babylonjs/Materials/Node/Blocks/subtractBlock';
+import { StepBlock } from 'babylonjs/Materials/Node/Blocks/stepBlock';
+import { SmoothStepBlock } from 'babylonjs/Materials/Node/Blocks/smoothStepBlock';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { NodeMaterialSystemValues } from 'babylonjs/Materials/Node/Enums/nodeMaterialSystemValues';
+import { AnimatedInputBlockTypes } from 'babylonjs/Materials/Node/Blocks/Input/animatedInputBlockTypes';
+import { OneMinusBlock } from 'babylonjs/Materials/Node/Blocks/oneMinusBlock';
+import { ViewDirectionBlock } from 'babylonjs/Materials/Node/Blocks/viewDirectionBlock';
+import { LightInformationBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/lightInformationBlock';
+import { MaxBlock } from 'babylonjs/Materials/Node/Blocks/maxBlock';
+import { MinBlock } from 'babylonjs/Materials/Node/Blocks/minBlock';
+import { PerturbNormalBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/perturbNormalBlock';
+import { LengthBlock } from 'babylonjs/Materials/Node/Blocks/lengthBlock';
+import { DistanceBlock } from 'babylonjs/Materials/Node/Blocks/distanceBlock';
+import { FrontFacingBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/frontFacingBlock';
+import { NegateBlock } from 'babylonjs/Materials/Node/Blocks/negateBlock';
+import { PowBlock } from 'babylonjs/Materials/Node/Blocks/powBlock';
+import { Scene } from 'babylonjs/scene';
+import { RandomNumberBlock } from 'babylonjs/Materials/Node/Blocks/randomNumberBlock';
+import { ReplaceColorBlock } from 'babylonjs/Materials/Node/Blocks/replaceColorBlock';
+import { PosterizeBlock } from 'babylonjs/Materials/Node/Blocks/posterizeBlock';
+import { ArcTan2Block } from 'babylonjs/Materials/Node/Blocks/arcTan2Block';
+import { ReciprocalBlock } from 'babylonjs/Materials/Node/Blocks/reciprocalBlock';
+import { GradientBlock } from 'babylonjs/Materials/Node/Blocks/gradientBlock';
+import { WaveBlock, WaveBlockKind } from 'babylonjs/Materials/Node/Blocks/waveBlock';
+import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
+import { WorleyNoise3DBlock } from 'babylonjs/Materials/Node/Blocks/worleyNoise3DBlock';
+import { SimplexPerlin3DBlock } from 'babylonjs/Materials/Node/Blocks/simplexPerlin3DBlock';
+import { NormalBlendBlock } from 'babylonjs/Materials/Node/Blocks/normalBlendBlock';
+import { Rotate2dBlock } from 'babylonjs/Materials/Node/Blocks/rotate2dBlock';
+import { DerivativeBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/derivativeBlock';
+import { RefractBlock } from 'babylonjs/Materials/Node/Blocks/refractBlock';
+import { ReflectBlock } from 'babylonjs/Materials/Node/Blocks/reflectBlock';
+import { DesaturateBlock } from 'babylonjs/Materials/Node/Blocks/desaturateBlock';
+import { PBRMetallicRoughnessBlock } from 'babylonjs/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock';
+import { SheenBlock } from 'babylonjs/Materials/Node/Blocks/PBR/sheenBlock';
+import { AmbientOcclusionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/ambientOcclusionBlock';
+import { ReflectivityBlock } from 'babylonjs/Materials/Node/Blocks/PBR/reflectivityBlock';
+import { AnisotropyBlock } from 'babylonjs/Materials/Node/Blocks/PBR/anisotropyBlock';
+import { ReflectionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/reflectionBlock';
+import { ClearCoatBlock } from 'babylonjs/Materials/Node/Blocks/PBR/clearCoatBlock';
+import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionBlock';
+import { SubSurfaceBlock } from 'babylonjs/Materials/Node/Blocks/PBR/subSurfaceBlock';
+import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
+import { ParticleRampGradientBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleRampGradientBlock';
+import { ParticleBlendMultiplyBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleBlendMultiplyBlock';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+import { FragCoordBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragCoordBlock';
+import { ScreenSizeBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/screenSizeBlock';
+
+export class BlockTools {
+    public static GetBlockFromString(data: string, scene: Scene, nodeMaterial: NodeMaterial) {
+        switch (data) {
+            case "DesaturateBlock":
+                return new DesaturateBlock("Desaturate");
+            case "RefractBlock":
+                return new RefractBlock("Refract");
+            case "ReflectBlock":
+                return new ReflectBlock("Reflect");
+            case "DerivativeBlock":
+                return new DerivativeBlock("Derivative");
+            case "Rotate2dBlock":
+                return new Rotate2dBlock("Rotate2d");
+            case "NormalBlendBlock":
+                return new NormalBlendBlock("NormalBlend");
+            case "WorleyNoise3DBlock":
+                return new WorleyNoise3DBlock("WorleyNoise3D");
+            case "SimplexPerlin3DBlock":
+                return new SimplexPerlin3DBlock("SimplexPerlin3D");
+            case "BonesBlock":
+                return new BonesBlock("Bones");
+            case "InstancesBlock":
+                return new InstancesBlock("Instances");
+            case "MorphTargetsBlock":
+                return new MorphTargetsBlock("MorphTargets");
+            case "DiscardBlock":
+                return new DiscardBlock("Discard");
+            case "ImageProcessingBlock":
+                return new ImageProcessingBlock("ImageProcessing");
+            case "ColorMergerBlock":
+                return new ColorMergerBlock("ColorMerger");
+            case "VectorMergerBlock":
+                return new VectorMergerBlock("VectorMerger");
+            case "ColorSplitterBlock":
+                return new ColorSplitterBlock("ColorSplitter");
+            case "VectorSplitterBlock":
+                return new VectorSplitterBlock("VectorSplitter");
+            case "TextureBlock":
+                return new TextureBlock("Texture", nodeMaterial.mode === NodeMaterialModes.Particle);
+            case "ReflectionTextureBlock":
+                return new ReflectionTextureBlock("Reflection texture");
+            case "LightBlock":
+                return new LightBlock("Lights");
+            case "FogBlock":
+                return new FogBlock("Fog");
+            case "VertexOutputBlock":
+                return new VertexOutputBlock("VertexOutput");
+            case "FragmentOutputBlock":
+                return new FragmentOutputBlock("FragmentOutput");
+            case "AddBlock":
+                return new AddBlock("Add");
+            case "ClampBlock":
+                return new ClampBlock("Clamp");
+            case "ScaleBlock":
+                return new ScaleBlock("Scale");
+            case "CrossBlock":
+                return new CrossBlock("Cross");
+            case "DotBlock":
+                return new DotBlock("Dot");
+            case "PowBlock":
+                return new PowBlock("Pow");
+            case "MultiplyBlock":
+                return new MultiplyBlock("Multiply");
+            case "TransformBlock":
+                return new TransformBlock("Transform");
+            case "TrigonometryBlock":
+                return new TrigonometryBlock("Trigonometry");
+            case "RemapBlock":
+                return new RemapBlock("Remap");
+            case "NormalizeBlock":
+                return new NormalizeBlock("Normalize");
+            case "FresnelBlock":
+                return new FresnelBlock("Fresnel");
+            case "LerpBlock":
+                return new LerpBlock("Lerp");
+            case "NLerpBlock":
+                return new NLerpBlock("NLerp");
+            case "DivideBlock":
+                return new DivideBlock("Divide");
+            case "SubtractBlock":
+                return new SubtractBlock("Subtract");
+            case "ModBlock":
+                return new ModBlock("Mod");
+            case "StepBlock":
+                return new StepBlock("Step");
+            case "SmoothStepBlock":
+                return new SmoothStepBlock("Smooth step");
+            case "OneMinusBlock":
+                return new OneMinusBlock("One minus");
+            case "ReciprocalBlock":
+                return new ReciprocalBlock("Reciprocal");
+            case "ViewDirectionBlock":
+                return new ViewDirectionBlock("View direction");
+            case "LightInformationBlock":
+                let lightInformationBlock = new LightInformationBlock("Light information");
+                lightInformationBlock.light = scene.lights.length ? scene.lights[0] : null;
+                return lightInformationBlock;
+            case "MaxBlock":
+                return new MaxBlock("Max");
+            case "MinBlock":
+                return new MinBlock("Min");
+            case "LengthBlock":
+                return new LengthBlock("Length");
+            case "DistanceBlock":
+                return new DistanceBlock("Distance");
+            case "NegateBlock":
+                return new NegateBlock("Negate");
+            case "PerturbNormalBlock":
+                return new PerturbNormalBlock("Perturb normal");
+            case "RandomNumberBlock":
+                return new RandomNumberBlock("Random number");
+            case "ReplaceColorBlock":
+                return new ReplaceColorBlock("Replace color");
+            case "PosterizeBlock":
+                return new PosterizeBlock("Posterize");
+            case "ArcTan2Block":
+                return new ArcTan2Block("ArcTan2");
+            case "GradientBlock":
+                return new GradientBlock("Gradient");
+            case "FrontFacingBlock":
+                return new FrontFacingBlock("Front facing");
+            case "CosBlock": {
+                let cosBlock = new TrigonometryBlock("Cos");
+                cosBlock.operation = TrigonometryBlockOperations.Cos;
+                return cosBlock;
+            }
+            case "SinBlock": {
+                let sinBlock = new TrigonometryBlock("Sin");
+                sinBlock.operation = TrigonometryBlockOperations.Sin;
+                return sinBlock;
+            }
+            case "AbsBlock": {
+                let absBlock = new TrigonometryBlock("Abs");
+                absBlock.operation = TrigonometryBlockOperations.Abs;
+                return absBlock;
+            }
+            case "SqrtBlock": {
+                let sqrtBlock = new TrigonometryBlock("Sqrt");
+                sqrtBlock.operation = TrigonometryBlockOperations.Sqrt;
+                return sqrtBlock;
+            }
+            case "ArcCosBlock": {
+                let acosBlock = new TrigonometryBlock("ArcCos");
+                acosBlock.operation = TrigonometryBlockOperations.ArcCos;
+                return acosBlock;
+            }
+            case "ArcSinBlock": {
+                let asinBlock = new TrigonometryBlock("ArcSin");
+                asinBlock.operation = TrigonometryBlockOperations.ArcSin;
+                return asinBlock;
+            }
+            case "TanBlock": {
+                let tanBlock = new TrigonometryBlock("Tan");
+                tanBlock.operation = TrigonometryBlockOperations.Tan;
+                return tanBlock;
+            }
+            case "ArcTanBlock": {
+                let atanBlock = new TrigonometryBlock("ArcTan");
+                atanBlock.operation = TrigonometryBlockOperations.ArcTan;
+                return atanBlock;
+            }
+            case "FractBlock": {
+                let fractBlock = new TrigonometryBlock("Fract");
+                fractBlock.operation = TrigonometryBlockOperations.Fract;
+                return fractBlock;
+            }
+            case "SignBlock": {
+                let signBlock = new TrigonometryBlock("Sign");
+                signBlock.operation = TrigonometryBlockOperations.Sign;
+                return signBlock;
+            }
+            case "LogBlock": {
+                let logBlock = new TrigonometryBlock("Log");
+                logBlock.operation = TrigonometryBlockOperations.Log;
+                return logBlock;
+            }
+            case "ExpBlock": {
+                let expBlock = new TrigonometryBlock("Exp");
+                expBlock.operation = TrigonometryBlockOperations.Exp;
+                return expBlock;
+            }
+            case "Exp2Block": {
+                let exp2Block = new TrigonometryBlock("Exp2");
+                exp2Block.operation = TrigonometryBlockOperations.Exp2;
+                return exp2Block;
+            }
+            case "DegreesToRadiansBlock": {
+                let degreesToRadiansBlock = new TrigonometryBlock("Degrees to radians");
+                degreesToRadiansBlock.operation = TrigonometryBlockOperations.Radians;
+                return degreesToRadiansBlock;
+            }
+            case "RadiansToDegreesBlock": {
+                let radiansToDegreesBlock = new TrigonometryBlock("Radians to degrees");
+                radiansToDegreesBlock.operation = TrigonometryBlockOperations.Degrees;
+                return radiansToDegreesBlock;
+            }
+            case "RoundBlock": {
+                let roundBlock = new TrigonometryBlock("Round");
+                roundBlock.operation = TrigonometryBlockOperations.Round;
+                return roundBlock;
+            }
+            case "CeilingBlock": {
+                let ceilingBlock = new TrigonometryBlock("Ceiling");
+                ceilingBlock.operation = TrigonometryBlockOperations.Ceiling;
+                return ceilingBlock;
+            }
+            case "FloorBlock": {
+                let floorBlock = new TrigonometryBlock("Floor");
+                floorBlock.operation = TrigonometryBlockOperations.Floor;
+                return floorBlock;
+            }
+            case "SawToothWaveBlock": {
+                let sawToothWaveBlock = new WaveBlock("SawTooth wave");
+                sawToothWaveBlock.kind = WaveBlockKind.SawTooth;
+                return sawToothWaveBlock;
+            }
+            case "SquareWaveBlock": {
+                let squareWaveBlock = new WaveBlock("Square wave");
+                squareWaveBlock.kind = WaveBlockKind.Square;
+                return squareWaveBlock;
+            }
+            case "TriangleWaveBlock": {
+                let triangleWaveBlock = new WaveBlock("Triangle wave");
+                triangleWaveBlock.kind = WaveBlockKind.Triangle;
+                return triangleWaveBlock;
+            }
+            case "WorldMatrixBlock": {
+                let worldMatrixBlock = new InputBlock("World");
+                worldMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.World);
+                return worldMatrixBlock;
+            }
+            case "WorldViewMatrixBlock": {
+                let worldViewMatrixBlock = new InputBlock("World x View");
+                worldViewMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.WorldView);
+                return worldViewMatrixBlock;
+            }
+            case "WorldViewProjectionMatrixBlock": {
+                let worldViewProjectionMatrixBlock = new InputBlock("World x View x Projection");
+                worldViewProjectionMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.WorldViewProjection);
+                return worldViewProjectionMatrixBlock;
+            }
+            case "ViewMatrixBlock": {
+                let viewMatrixBlock = new InputBlock("View");
+                viewMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.View);
+                return viewMatrixBlock;
+            }
+            case "ViewProjectionMatrixBlock": {
+                let viewProjectionMatrixBlock = new InputBlock("View x Projection");
+                viewProjectionMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.ViewProjection);
+                return viewProjectionMatrixBlock;
+            }
+            case "ProjectionMatrixBlock": {
+                let projectionMatrixBlock = new InputBlock("Projection");
+                projectionMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.Projection);
+                return projectionMatrixBlock;
+            }
+            case "CameraPositionBlock": {
+                let cameraPosition = new InputBlock("Camera position");
+                cameraPosition.setAsSystemValue(NodeMaterialSystemValues.CameraPosition);
+                return cameraPosition;
+            }
+            case "FogColorBlock": {
+                let FogColor = new InputBlock("Fog color");
+                FogColor.setAsSystemValue(NodeMaterialSystemValues.FogColor);
+                return FogColor;
+            }
+            case "PositionBlock": {
+                let meshPosition = new InputBlock("position");
+                meshPosition.setAsAttribute("position");
+                return meshPosition;
+            }
+            case "ScreenPositionBlock": {
+                let meshPosition = new InputBlock("position");
+                meshPosition.setAsAttribute("position2d");
+                return meshPosition;
+            }
+            case "UVBlock": {
+                let meshUV = new InputBlock("uv");
+                meshUV.setAsAttribute("uv");
+                return meshUV;
+            }
+            case "ColorBlock": {
+                let meshColor = new InputBlock("color");
+                meshColor.setAsAttribute("color");
+                return meshColor;
+            }
+            case "NormalBlock": {
+                let meshNormal = new InputBlock("normal");
+                meshNormal.setAsAttribute("normal");
+                return meshNormal;
+            }
+            case "TangentBlock": {
+                let meshTangent = new InputBlock("tangent");
+                meshTangent.setAsAttribute("tangent");
+                return meshTangent;
+            }
+            case "MatrixIndicesBlock": {
+                let meshMatrixIndices = new InputBlock("matricesIndices");
+                meshMatrixIndices.setAsAttribute("matricesIndices");
+                return meshMatrixIndices;
+            }
+            case "MatrixWeightsBlock": {
+                let meshMatrixWeights = new InputBlock("matricesWeights");
+                meshMatrixWeights.setAsAttribute("matricesWeights");
+                return meshMatrixWeights;
+            }
+            case "TimeBlock": {
+                let timeBlock = new InputBlock("Time", undefined, NodeMaterialBlockConnectionPointTypes.Float);
+                timeBlock.animationType = AnimatedInputBlockTypes.Time;
+                return timeBlock;
+            }
+            case "DeltaTimeBlock": {
+                let deltaTimeBlock = new InputBlock("Delta time");
+                deltaTimeBlock.setAsSystemValue(NodeMaterialSystemValues.DeltaTime);
+                return deltaTimeBlock;
+            }
+            case "WorldPositionBlock": {
+                let worldPositionBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isAttribute && b.name === "position");
+                if (!worldPositionBlock) {
+                    worldPositionBlock = new InputBlock("position");
+                    worldPositionBlock.setAsAttribute("position");
+                }
+
+                let worldMatrixBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isSystemValue && b.systemValue === NodeMaterialSystemValues.World);
+
+                if (!worldMatrixBlock) {
+                    worldMatrixBlock = new InputBlock("World");
+                    worldMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.World);
+                }
+
+                let transformBlock = new TransformBlock("World position");
+                worldPositionBlock.connectTo(transformBlock);
+                worldMatrixBlock.connectTo(transformBlock);
+
+                return transformBlock;
+            }
+            case "WorldNormalBlock": {
+                let worldNormalBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isAttribute && b.name === "normal");
+                if (!worldNormalBlock) {
+                    worldNormalBlock = new InputBlock("normal");
+                    worldNormalBlock.setAsAttribute("normal");
+                }
+
+                let worldMatrixBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isSystemValue && b.systemValue === NodeMaterialSystemValues.World);
+
+                if (!worldMatrixBlock) {
+                    worldMatrixBlock = new InputBlock("World");
+                    worldMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.World);
+                }
+
+                let transformBlock = new TransformBlock("World normal");
+                worldNormalBlock.connectTo(transformBlock);
+                worldMatrixBlock.connectTo(transformBlock);
+
+                return transformBlock;
+            }
+            case "WorldTangentBlock": {
+                let worldTangentBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isAttribute && b.name === "tangent");
+                if (!worldTangentBlock) {
+                    worldTangentBlock = new InputBlock("tangent");
+                    worldTangentBlock.setAsAttribute("tangent");
+                }
+
+                let worldMatrixBlock = nodeMaterial.getInputBlockByPredicate((b) => b.isSystemValue && b.systemValue === NodeMaterialSystemValues.World);
+
+                if (!worldMatrixBlock) {
+                    worldMatrixBlock = new InputBlock("World");
+                    worldMatrixBlock.setAsSystemValue(NodeMaterialSystemValues.World);
+                }
+
+                let transformBlock = new TransformBlock("World tangent");
+                worldTangentBlock.connectTo(transformBlock);
+                worldMatrixBlock.connectTo(transformBlock);
+
+                return transformBlock;
+            }
+            case "PBRMetallicRoughnessBlock":
+                return new PBRMetallicRoughnessBlock("PBRMetallicRoughness");
+            case "SheenBlock":
+                return new SheenBlock("Sheen");
+            case "AmbientOcclusionBlock":
+                return new AmbientOcclusionBlock("AmbientOcclusion");
+            case "ReflectivityBlock":
+                return new ReflectivityBlock("Reflectivity");
+            case "AnisotropyBlock":
+                return new AnisotropyBlock("Anisotropy");
+            case "ReflectionBlock":
+                return new ReflectionBlock("Reflection");
+            case "ClearCoatBlock":
+                return new ClearCoatBlock("ClearCoat");
+            case "RefractionBlock":
+                return new RefractionBlock("Refraction");
+            case "SubSurfaceBlock":
+                return new SubSurfaceBlock("SubSurface");
+            case "CurrentScreenBlock":
+                return new CurrentScreenBlock("CurrentScreen");
+            case "ParticleUVBlock": {
+                let uv = new InputBlock("uv");
+                uv.setAsAttribute("particle_uv");
+                return uv;
+            }
+            case "ParticleTextureBlock":
+                return new ParticleTextureBlock("ParticleTexture");
+            case "ParticleColorBlock": {
+                let color = new InputBlock("Color");
+                color.setAsAttribute("particle_color");
+                return color;
+            }
+            case "ParticleTextureMaskBlock": {
+                let u = new InputBlock("TextureMask");
+                u.setAsAttribute("particle_texturemask");
+                return u;
+            }
+            case "ParticlePositionWorldBlock": {
+                let pos = new InputBlock("PositionWorld");
+                pos.setAsAttribute("particle_positionw");
+                return pos;
+            }
+            case "ParticleRampGradientBlock":
+                return new ParticleRampGradientBlock("ParticleRampGradient");
+            case "ParticleBlendMultiplyBlock":
+                return new ParticleBlendMultiplyBlock("ParticleBlendMultiply");
+            case "FragCoordBlock":
+                return new FragCoordBlock("FragCoord");
+            case "ScreenSizeBlock":
+                return new ScreenSizeBlock("ScreenSize");
+        }
+
+        return null;
+    }
+
+    public static GetColorFromConnectionNodeType(type: NodeMaterialBlockConnectionPointTypes) {
+        let color = "#880000";
+        switch (type) {
+            case NodeMaterialBlockConnectionPointTypes.Float:
+                color = "#cb9e27";
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Vector2:
+                color = "#16bcb1";
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Vector3:
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+                color = "#b786cb";
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Vector4:
+            case NodeMaterialBlockConnectionPointTypes.Color4:
+                color = "#be5126";
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Matrix:
+                color = "#591990";
+                break;
+        }
+
+        return color;
+    }
+
+    public static GetConnectionNodeTypeFromString(type: string) {
+        switch (type) {
+            case "Float":
+                return NodeMaterialBlockConnectionPointTypes.Float;
+            case "Vector2":
+                return NodeMaterialBlockConnectionPointTypes.Vector2;
+            case "Vector3":
+                return NodeMaterialBlockConnectionPointTypes.Vector3;
+            case "Vector4":
+                return NodeMaterialBlockConnectionPointTypes.Vector4;
+            case "Matrix":
+                return NodeMaterialBlockConnectionPointTypes.Matrix;
+            case "Color3":
+                return NodeMaterialBlockConnectionPointTypes.Color3;
+            case "Color4":
+                return NodeMaterialBlockConnectionPointTypes.Color4;
+        }
+
+        return NodeMaterialBlockConnectionPointTypes.AutoDetect;
+    }
+
+    public static GetStringFromConnectionNodeType(type: NodeMaterialBlockConnectionPointTypes) {
+        switch (type){
+            case NodeMaterialBlockConnectionPointTypes.Float:
+                return "Float";
+            case NodeMaterialBlockConnectionPointTypes.Vector2:
+                return "Vector2";
+            case NodeMaterialBlockConnectionPointTypes.Vector3:
+                return "Vector3";
+            case NodeMaterialBlockConnectionPointTypes.Vector4:
+                return "Vector4";
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+                return "Color3";
+            case NodeMaterialBlockConnectionPointTypes.Color4:
+                return "Color4";
+            case NodeMaterialBlockConnectionPointTypes.Matrix:
+                return "Matrix";
+        }
+
+        return "";
+    }
+}

+ 20 - 0
guiEditor/src/components/log/log.scss

@@ -0,0 +1,20 @@
+#log-console {
+    background: #333333;
+    height: 120px;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 10px;
+    width: 100%; 
+    overflow: hidden;
+    overflow-y: auto;
+
+    .log {
+        color: white;
+        font-size: 14px;
+        font-family: 'Courier New', Courier, monospace;
+
+        &.error {
+            color:red;
+        }
+    }
+}

+ 64 - 0
guiEditor/src/components/log/logComponent.tsx

@@ -0,0 +1,64 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import * as ReactDOM from 'react-dom';
+
+require("./log.scss");
+
+interface ILogComponentProps {
+    globalState: GlobalState;
+}
+
+export class LogEntry {
+    constructor(public message: string, public isError: boolean) {
+
+    }
+}
+
+export class LogComponent extends React.Component<ILogComponentProps, { logs: LogEntry[] }> {
+
+    constructor(props: ILogComponentProps) {
+        super(props);
+
+        this.state = { logs: [] };
+    }
+
+    componentDidMount() {
+        this.props.globalState.onLogRequiredObservable.add(log => {
+            let currentLogs = this.state.logs;
+            currentLogs.push(log);
+
+            this.setState({ logs: currentLogs });
+        });
+    }
+
+    componentDidUpdate() {
+        const logConsole = ReactDOM.findDOMNode(this.refs["log-console"]) as HTMLElement;
+        if (!logConsole) {
+            return;
+        }
+
+        logConsole.scrollTop = logConsole.scrollHeight;
+    }
+
+    render() {
+        var today = new Date();
+        var h = today.getHours();
+        var m = today.getMinutes();
+        var s = today.getSeconds();
+
+        return (
+            <div id="log-console" ref={"log-console"} >
+                {
+                    this.state.logs.map((l, i) => {
+                        return (
+                            <div key={i} className={"log" + (l.isError ? " error" : "")}>
+                                {h + ":" + m + ":" + s+ ": " + l.message}
+                            </div>
+                        )
+                    })
+                }
+            </div>
+        );
+    }
+}

+ 211 - 0
guiEditor/src/components/nodeList/nodeList.scss

@@ -0,0 +1,211 @@
+#nodeList {
+    background: #333333;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+    display: grid;
+    width: 100%; 
+    overflow: hidden;
+
+    .panes {
+        overflow: hidden;
+        
+        .pane {
+            color: white;
+
+            overflow: hidden;
+            height: 100%;
+
+            -webkit-user-select: none; 
+            -moz-user-select: none;   
+            -ms-user-select: none;    
+            user-select: none;     
+
+            .filter {
+                display: flex;
+                align-items: stretch;
+        
+                input {
+                    width: 100%;
+                    margin: 10px 10px 5px 10px;
+                    display: block;
+                    border: none;
+                    padding: 0;
+                    border-bottom: solid 1px rgb(51, 122, 183);
+                    background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 96%, rgb(51, 122, 183) 4%);
+                    background-position: -1000px 0;
+                    background-size: 1000px 100%;
+                    background-repeat: no-repeat;  
+                    color:white;    
+                }
+        
+                input:focus  {
+                    box-shadow: none;
+                    outline: none;
+                    background-position: 0 0;
+                }
+
+                input::placeholder {
+                    color: gray;
+                }
+            }
+
+            .list-container {
+                overflow-x: hidden;
+                overflow-y: auto;
+                height: calc(100% - 32px);
+
+                .underline {
+                    border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
+                }
+
+                .draggableLine {
+                    height: 30px;
+                    display: grid;
+                    align-items: center;
+                    justify-items: stretch;
+                    background: #222222;
+                    cursor: grab;
+                    text-align: center;
+                    margin: 0;
+                    box-sizing: border-box;
+
+                    &:hover {
+                        background: rgb(51, 122, 183);
+                        color: white;
+                    }
+                }
+
+                .nonDraggableLine {
+                    height: 30px;
+                    display: grid;
+                    align-items: center;
+                    justify-items: stretch;
+                    background: #222222;
+                    text-align: center;
+                    margin: 0;
+                    box-sizing: border-box;
+                }
+
+                .withButton {
+                    height: 30px;
+                    position: relative;
+                    .icon {
+                        position: absolute;
+                        right: 4px;
+                        top: 5px;
+                        &:hover {
+                            cursor: pointer;
+                        }
+
+                        .img {
+                            height: 17px;
+                            width: 17px;
+                        }
+                    }
+
+                    .buttonLine {
+                        height: 30px;
+                        display: grid;
+                        align-items: center;
+                        justify-items: stretch;
+                        padding-bottom: 5px;
+                        position: absolute;
+                        right: 0px;
+                        top: 2px;
+                        input[type="file"] {
+                            display: none;
+                        }
+                
+                        .file-upload {            
+                            background: transparent;
+                            border: transparent;
+                            padding: 15px 200px;
+                            opacity: 0.9;
+                            cursor: pointer;
+                            text-align: center;
+                        }
+                
+                        .file-upload:hover {
+                            opacity: 1.0;
+                        }
+                
+                        .file-upload:active {
+                            transform: scale(0.98);
+                            transform-origin: 0.5 0.5;
+                        }
+                
+                        button {
+                            background: transparent;
+                            border: transparent;
+                            margin: 5px 10px 5px 10px;
+                            color:white;
+                            padding: 4px 5px;
+                            opacity: 0.9;
+                        }
+                
+                        button:hover {
+                            opacity: 0.0;
+                        }
+                
+                        button:active {
+                            background: transparent;
+                        }   
+                        
+                        button:focus {
+                            border: transparent;
+                            outline: 0px;
+                        }  
+                    }
+                    
+                }                
+
+                .paneContainer {
+                    margin-top: 3px;
+                    display:grid;
+                    grid-template-rows: 100%;
+                    grid-template-columns: 100%;
+
+                    .paneContainer-content {
+                        grid-row: 1;
+                        grid-column: 1;
+
+                        .header {
+                            display: grid;
+                            grid-template-columns: 1fr auto;
+                            background: #555555;    
+                            height: 30px;   
+                            padding-right: 5px;                        
+                            cursor: pointer;
+                            
+                            .title {                                
+                                border-left: 3px solid transparent;
+                                padding-left: 5px;
+                                grid-column: 1;
+                                display: flex;
+                                align-items: center;
+                            }
+
+                            .collapse {
+                                grid-column: 2;
+                                display: flex;
+                                align-items: center;  
+                                justify-items: center;
+                                transform-origin: center;
+
+                                &.closed {
+                                    transform: rotate(180deg);
+                                }
+                            }                        
+                        }
+
+                        .paneList > div:not(:last-child) {
+                            border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+                        }
+                    }
+                }
+            }    
+        }
+    }
+}
+

+ 326 - 0
guiEditor/src/components/nodeList/nodeListComponent.tsx

@@ -0,0 +1,326 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { DraggableLineComponent } from '../../sharedComponents/draggableLineComponent';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+import { Observer } from 'babylonjs/Misc/observable';
+import { Nullable } from 'babylonjs/types';
+import { DraggableLineWithButtonComponent } from '../../sharedComponents/draggableLineWithButtonComponent';
+import { LineWithFileButtonComponent } from '../../sharedComponents/lineWithFileButtonComponent';
+import { Tools } from 'babylonjs/Misc/tools';
+const addButton = require("../../../imgs/add.svg");
+const deleteButton = require('../../../imgs/delete.svg');
+
+require("./nodeList.scss");
+
+interface INodeListComponentProps {
+    globalState: GlobalState;
+}
+
+export class NodeListComponent extends React.Component<INodeListComponentProps, {filter: string}> {
+
+    private _onResetRequiredObserver: Nullable<Observer<void>>;
+
+    private static _Tooltips: {[key: string]: string} = {
+        "BonesBlock": "Provides a world matrix for each vertex, based on skeletal (bone/joint) animation",
+        "MorphTargetsBlock": "Provides the final positions, normals, tangents, and uvs based on morph targets in a mesh",
+        "AddBlock": "Adds the left and right inputs of the same type together",
+        "DistanceBlock": "Provides a distance vector based on the left and right input vectors",
+        "DivideBlock": "Divides the left input by the right input of the same type",
+        "LengthBlock": "Outputs the length of an input vector",
+        "MaxBlock": "Outputs the largest value between the left and right inputs of the same type",
+        "MinBlock": "Outputs the smallest value between the left and right inputs of the same type",
+        "MultiplyBlock": "Multiplies the left and right inputs of the same type together",
+        "NegateBlock": "Multiplies the input by -1",
+        "OneMinusBlock": "Subtracts each channel of the input value from 1 (1 - input)",
+        "RandomNumberBlock": "Provides a random number based on an input seed",
+        "ReciprocalBlock": "Quotient of 1 divided by the input",
+        "ScaleBlock": "Multiplies the input channels by a float factor",
+        "SubtractBlock": "Subtracts the right input from the left input of the same type",
+        "GradientBlock": "Returns the color in the gradient represented by the target value of the input",
+        "PosterizeBlock": "Reduces the number of values in each channel to the number in the corresponding channel of steps",
+        "ReplaceColorBlock": "Replaces a reference color in value with the color in replacement blended by distance",
+        "ColorMergerBlock": "Combines float input channels into a color",
+        "ColorSplitterBlock": "Separates color input channels into individual floats",
+        "VectorMergerBlock": "Combines up to four input floats into a vector",
+        "VectorSplitterBlock": "Separates vectors input channels into individual floats",
+        "Color3": "A color made up of red, green, and blue channel values",
+        "Color4": "A color made up of red, green, blue, and alpha channel values",
+        "DeltaTimeBlock": "A float representing the time that has passed since the last frame was rendered",
+        "Float": "A floating point number representing a value with a fractional component",
+        "TextureBlock": "A node for reading a linked or embedded texture file",
+        "TimeBlock": "A float value that represents the time that has passed since the scene was loaded",
+        "Vector2": "a vector composed of X and Y channels",
+        "Vector3": "a vector composed of X, Y, and Z channels",
+        "Vector4": "a vector composed of X, Y, Z, and W channels",
+        "LerpBlock": "Outputs a value that is a mix of the left and right inputs based on the target value",
+        "NLerpBlock": "Outputs a value that is a mix of the left and right inputs based on the target's normalized value",
+        "SmoothStepBlock": "Outputs a value based on a the input value's position on a curve between the two edge values",
+        "StepBlock": "Outputs 1 for any input value above the edge input, outputs 0 for any input value below the edge input",
+        "Matrix": "A 4x4 table of related values",
+        "ProjectionMatrixBlock": "A matrix to remap points in 3D space to 2D plane relative to the screen",
+        "ViewMatrixBlock": "A matrix to remap points in 3D space to 2D plane relative to the view of the scene camera",
+        "ViewProjectionMatrixBlock": "A matrix to remap points in 3D space to 2D view space before remapping to 2D screen space",
+        "WorldMatrixBlock": "A matrix to remap points in 3D local space to 3D world space",
+        "WorldViewProjectionMatrixBlock": "A matrix to remap points in 3D local space to 3D world space, then to 2D camera space, and ending in 2D screen space",
+        "ColorBlock": "Outputs the RGBA color of each vertex in the mesh",
+        "InstancesBlock": "Provides the world matrix for each instance to apply this material to all instances",
+        "MatrixIndicesBlock": "A Vector4 representing the vertex to bone skinning assignments",
+        "MatrixWeightsBlock": "A Vector4 representing the vertex to bone skinning weights",
+        "NormalBlock": "A Vector3 representing the normal of each vertex of the attached mesh",
+        "PositionBlock": "A Vector3 representing the position of each vertex of the attached mesh",
+        "TangentBlock": "A Vector3 representing the tangent of each vertex of the attached mesh",
+        "UVBlock": "A Vector2 representing the UV coordinates of each vertex of the attached mesh",
+        "WorldNormal": "A Vector4 representing the normal of each vertex of the attached mesh transformed into world space",
+        "WorldTangent": "A Vector4 representing the tangent of each vertex of the attached mesh transformed into world space",
+        "PerturbNormalBlock": "Creates high-frequency detail normal vectors based on a normal map, the world position, and world normal",
+        "NormalBlend": "Outputs the result of blending two normal maps together using a per-channel screen",
+        "WorldPosition": "A Vector4 representing the position of each vertex of the attached mesh transformed into world space",
+        "DiscardBlock": "A final node that will not output a pixel below the cutoff value",
+        "FragmentOutputBlock": "A mandatory final node for outputing the color of each pixel",
+        "VertexOutputBlock": "A mandatory final node for outputing the position of each vertex",
+        "ClampBlock": "Outputs values above the maximum or below minimum as maximum or minimum values respectively",
+        "NormalizeBlock": "Remaps the length of a vector or color to 1",
+        "RemapBlock": "Remaps input value between sourceMin and sourceMax to a new range between targetMin and targetMax",
+        "CeilingBlock": "Outputs fractional values as the next higher whole number",
+        "FloorBlock": "Outputs fractional values as the next lower whole number",
+        "RoundBlock": "Outputs fractional values rounded to the nearest whole number",
+        "ModBlock": "Outputs the value of one parameter modulo another",
+        "CameraPositionBlock": "Outputs a Vector3 position of the active scene camera",
+        "FogBlock": "Applies fog to the scene with an increasing opacity based on distance from the camera",
+        "FogColorBlock": "The system value for fog color pulled from the scene",
+        "ImageProcessingBlock": "Provides access to all of the Babylon image processing properties",
+        "LightBlock": "Outputs diffuse and specular contributions from one or more scene lights",
+        "LightInformationBlock": "Provides the direction, color and intensity of a selected light based on its world position",
+        "ReflectionTextureBlock": "Creates a reflection from the input texture",
+        "ViewDirectionBlock": "Outputs the direction vector of where the camera is aimed",
+        "AbsBlock": "Outputs the absolute value of the input value",
+        "ArcCosBlock": "Outputs the inverse of the cosine value based on the input value",
+        "ArcSinBlock": "Outputs the inverse of the sine value based on the input value",
+        "ArcTan2Block": "Outputs the inverse of the tangent value based on the input value",
+        "ArcTanBlock": "Outputs the inverse of the tangent value based on the input value",
+        "CosBlock": "Outputs the cosine value based on the input value",
+        "DegreesToRadiansBlock": "Converts the input degrees value to radians",
+        "Exp2Block": "Outputs the input value multiplied by itself 1 time. (Exponent of 2)",
+        "ExpBlock": "Outputs the input value multiplied by itself 9 time. (Exponent of 10)",
+        "FractBlock": "Outputs only the fractional value of a floating point number",
+        "LogBlock": "The logarithmic value based on the input value",
+        "PowBlock": "Outputs the input value multiplied by itself the number of times equal to the power input (Exponent of power)",
+        "RadiansToDegreesBlock": "Converts the input radians value to degrees",
+        "SawToothWaveBlock": "Outputs a sawtooth pattern value between -1 and 1 based on the input value",
+        "SignBlock": "returns 1 if the input is positive, 0 if input is equal to 0, or -1 if the input is negative",
+        "SinBlock": "Outputs the the sine value based on the input value",
+        "SqrtBlock": "Outputs the the square root of the input value",
+        "SquareWaveBlock": "Outputs a stepped pattern value between -1 and 1 based on the input value",
+        "TanBlock": "Outputs the the tangent value based on the input value",
+        "TriangleWaveBlock": "Outputs a sawtooth pattern value between 0 and 1 based on the input value",
+        "CrossBlock": "Outputs a vector that is perpendicular to two input vectors",
+        "DotBlock": "Outputs the cos of the angle between two vectors",
+        "FresnelBlock": "Outputs the grazing angle of the surface of the mesh, relative to a camera influenced by the bias and power inputs",
+        "TransformBlock": "Transforms a input vector based on the input matrix",
+        "DerivativeBlock": "FRAGMENT SHADER ONLY. Provides the rate of change for an input on a given axis (x,y).",
+        "DesaturateBlock": "Convert a color input into a grayscale representation.",
+        "WorldViewMatrixBlock": "A matrix to remap points in 3D local space to 3D world space, and ending in 2D camera space.",
+        "FrontFacingBlock": "Returns 1 if a mesh triangle faces the normal direction and 0 if it does not.",
+        "SimplexPerlin3DBlock": "Creates a type of gradient noise with few directional artifacts.",
+        "WorleyNoise3DBlock": "Creates a random pattern resembling cells.",
+        "ReflectBlock": "Outputs the direction of the input vector reflected across the surface normal.",
+        "RefractBlock": "Outputs a direction simulating a deflection of the input vector.",
+        "Rotate2dBlock": "Rotates UV coordinates around the W axis.",
+        "PBRMetallicRoughnessBlock": "PBR metallic/roughness material",
+        "SheenBlock": "PBR Sheen block",
+        "AmbientOcclusionBlock": "PBR Ambient occlusion block",
+        "ReflectivityBlock": "PBR Reflectivity block",
+        "AnisotropyBlock": "PBR Anisotropy block",
+        "ReflectionBlock": "PBR Reflection block",
+        "ClearCoatBlock": "PBR ClearCoat block",
+        "RefractionBlock": "PBR Refraction block",
+        "SubSurfaceBlock": "PBR SubSurface block",
+        "ScreenPositionBlock": "A Vector2 representing the position of each vertex of the screen quad (derived from UV set from the quad used to render)",
+        "CurrentScreenBlock": "The screen buffer used as input for the post process",
+        "ParticleUVBlock": "The particle uv texture coordinate",
+        "ParticleTextureBlock": "The particle texture",
+        "ParticleColorBlock": "The particle color",
+        "ParticleTextureMaskBlock": "The particle texture mask",
+        "ParticleRampGradientBlock": "The particle ramp gradient block",
+        "ParticleBlendMultiplyBlock": "The particle blend multiply block",
+        "ParticlePositionWorldBlock": "The world position of the particle",
+        "FragCoordBlock": "The gl_FragCoord predefined variable that contains the window relative coordinate (x, y, z, 1/w)",
+        "ScreenSizeBlock": "The size (in pixels) of the screen window",
+    };
+    
+    private _customFrameList: {[key: string]: string};
+
+    constructor(props: INodeListComponentProps) {
+        super(props);
+
+        this.state = { filter: "" };
+
+        let frameJson = localStorage.getItem("Custom-Frame-List");
+        if(frameJson) {
+            this._customFrameList = JSON.parse(frameJson);
+        }
+
+        this._onResetRequiredObserver = this.props.globalState.onResetRequiredObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onResetRequiredObservable.remove(this._onResetRequiredObserver);
+    }
+
+    filterContent(filter: string) {
+        this.setState({ filter: filter });
+    }
+
+    loadCustomFrame(file: File) {
+        Tools.ReadFile(file, async (data) => {
+            // get Frame Data from file
+            let decoder = new TextDecoder("utf-8");
+            const frameData = JSON.parse(decoder.decode(data));
+            let frameName = frameData.editorData.frames[0].name + "Custom";
+            let frameToolTip = frameData.editorData.frames[0].comments || "";
+
+            try {
+                localStorage.setItem(frameName, JSON.stringify(frameData));
+            } catch (error) {
+                this.props.globalState.onErrorMessageDialogRequiredObservable.notifyObservers("Error Saving Frame");
+                return;
+            }
+
+            let frameJson = localStorage.getItem("Custom-Frame-List");
+            let frameList:  {[key: string]: string} = {};
+            if(frameJson) {
+                frameList = JSON.parse(frameJson); 
+            }
+            frameList[frameName] = frameToolTip;
+            localStorage.setItem("Custom-Frame-List", JSON.stringify(frameList));
+                this._customFrameList = frameList;
+                this.forceUpdate();
+
+        }, undefined, true);
+    }
+
+    removeItem(value : string) : void {
+        let frameJson = localStorage.getItem("Custom-Frame-List");
+            if(frameJson) {
+                let frameList = JSON.parse(frameJson);
+                delete frameList[value];
+                localStorage.removeItem(value);
+                localStorage.setItem("Custom-Frame-List", JSON.stringify(frameList));
+                this._customFrameList = frameList;
+                this.forceUpdate();
+            }        
+    }
+
+    render() {
+
+        let customFrameNames: string[] = [];
+        for(let frame in this._customFrameList){
+            customFrameNames.push(frame);
+        }
+        
+        // Block types used to create the menu from
+        const allBlocks: any = {
+            Custom_Frames: customFrameNames,
+            Animation: ["BonesBlock", "MorphTargetsBlock"],
+            Color_Management: ["ReplaceColorBlock", "PosterizeBlock", "GradientBlock", "DesaturateBlock"],
+            Conversion_Blocks: ["ColorMergerBlock", "ColorSplitterBlock", "VectorMergerBlock", "VectorSplitterBlock"],
+            Inputs: ["Float", "Vector2", "Vector3", "Vector4", "Color3", "Color4", "TextureBlock", "ReflectionTextureBlock", "TimeBlock", "DeltaTimeBlock", "FragCoordBlock", "ScreenSizeBlock"],
+            Interpolation: ["LerpBlock", "StepBlock", "SmoothStepBlock", "NLerpBlock"],
+            Math__Standard: ["AddBlock", "DivideBlock", "MaxBlock", "MinBlock", "ModBlock", "MultiplyBlock", "NegateBlock", "OneMinusBlock", "ReciprocalBlock", "ScaleBlock", "SignBlock", "SqrtBlock", "SubtractBlock"],
+            Math__Scientific: ["AbsBlock", "ArcCosBlock", "ArcSinBlock", "ArcTanBlock", "ArcTan2Block", "CosBlock", "DegreesToRadiansBlock", "ExpBlock", "Exp2Block", "FractBlock", "LogBlock", "PowBlock", "RadiansToDegreesBlock", "SawToothWaveBlock", "SinBlock", "SquareWaveBlock", "TanBlock", "TriangleWaveBlock"],
+            Math__Vector: ["CrossBlock", "DerivativeBlock", "DistanceBlock", "DotBlock", "FresnelBlock", "LengthBlock", "ReflectBlock", "RefractBlock", "Rotate2dBlock", "TransformBlock", ],
+            Matrices: ["Matrix", "WorldMatrixBlock", "WorldViewMatrixBlock", "WorldViewProjectionMatrixBlock", "ViewMatrixBlock", "ViewProjectionMatrixBlock", "ProjectionMatrixBlock"],
+            Mesh: ["InstancesBlock", "PositionBlock", "UVBlock", "ColorBlock", "NormalBlock", "PerturbNormalBlock", "NormalBlendBlock" , "TangentBlock", "MatrixIndicesBlock", "MatrixWeightsBlock", "WorldPositionBlock", "WorldNormalBlock", "WorldTangentBlock", "FrontFacingBlock"],
+            Noises: ["RandomNumberBlock", "SimplexPerlin3DBlock", "WorleyNoise3DBlock"],
+            Output_Nodes: ["VertexOutputBlock", "FragmentOutputBlock", "DiscardBlock"],
+            Particle: ["ParticleBlendMultiplyBlock", "ParticleColorBlock", "ParticlePositionWorldBlock", "ParticleRampGradientBlock", "ParticleTextureBlock", "ParticleTextureMaskBlock", "ParticleUVBlock"],
+            PBR: ["PBRMetallicRoughnessBlock", "AmbientOcclusionBlock", "AnisotropyBlock", "ClearCoatBlock", "ReflectionBlock", "ReflectivityBlock", "RefractionBlock", "SheenBlock", "SubSurfaceBlock"],
+            PostProcess: ["ScreenPositionBlock", "CurrentScreenBlock"],
+            Procedural__Texture: ["ScreenPositionBlock"],
+            Range: ["ClampBlock", "RemapBlock", "NormalizeBlock"],
+            Round: ["RoundBlock", "CeilingBlock", "FloorBlock"],
+            Scene: ["FogBlock", "CameraPositionBlock", "FogColorBlock", "ImageProcessingBlock", "LightBlock", "LightInformationBlock", "ViewDirectionBlock"],
+        };
+
+        switch (this.props.globalState.mode) {
+            case NodeMaterialModes.Material:
+                delete allBlocks["PostProcess"];
+                delete allBlocks["Particle"];
+                delete allBlocks["Procedural__Texture"];
+                break;
+            case NodeMaterialModes.PostProcess:
+                delete allBlocks["Animation"];
+                delete allBlocks["Mesh"];
+                delete allBlocks["Particle"];
+                delete allBlocks["Procedural__Texture"];
+                break;
+            case NodeMaterialModes.ProceduralTexture:
+                delete allBlocks["Animation"];
+                delete allBlocks["Mesh"];  
+                delete allBlocks["Particle"];              
+                delete allBlocks["PostProcess"];
+                break;
+            case NodeMaterialModes.Particle:
+                delete allBlocks["Animation"];
+                delete allBlocks["Mesh"];
+                delete allBlocks["PostProcess"];            
+                delete allBlocks["Procedural__Texture"];
+                allBlocks.Output_Nodes.splice(allBlocks.Output_Nodes.indexOf("VertexOutputBlock"), 1);
+                break;
+        }
+
+        // Create node menu
+        var blockMenu = [];
+        for (var key in allBlocks) {
+            var blockList = (allBlocks as any)[key].filter((b: string) => !this.state.filter || b.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1)
+            .sort((a: string, b: string) => a.localeCompare(b))
+            .map((block: any, i: number) => {
+                if(key === "Custom_Frames") {
+                    return <DraggableLineWithButtonComponent key={block} data={block} tooltip={this._customFrameList[block] || ""} iconImage={deleteButton} iconTitle="Delete"
+                    onIconClick={ value => this.removeItem(value)}/>;
+                }
+                return <DraggableLineComponent key={block} data={block} tooltip={ NodeListComponent._Tooltips[block] || ""}/>;
+
+            });
+
+            if(key === "Custom_Frames") {
+                let line =  <LineWithFileButtonComponent key="add..."title={"Add Custom Frame"} closed={false}
+                label="Add..." uploadName={'custom-frame-upload'} iconImage={addButton} accept=".json" onIconClick={(file) => {
+                    this.loadCustomFrame(file);
+                }}/>;
+                blockList.push(line);
+            }         
+            if(blockList.length) {
+                blockMenu.push(
+                    <LineContainerComponent key={key + " blocks"} title={key.replace("__", ": ").replace("_", " ")} closed={false}>
+                        {blockList}
+                    </LineContainerComponent>
+                );
+           }
+        }
+
+        return (
+            <div id="nodeList">
+                <div className="panes">
+                    <div className="pane">
+                        <div className="filter">
+                            <input type="text" placeholder="Filter"
+                                onFocus={() => this.props.globalState.blockKeyboardEvents = true}
+                                onBlur={(evt) => {
+                                    this.props.globalState.blockKeyboardEvents = false;
+                                }}
+                                onChange={(evt) => this.filterContent(evt.target.value)} />
+                        </div>
+                        <div className="list-container">
+                            {blockMenu}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        );
+    }
+}

+ 114 - 0
guiEditor/src/components/preview/previewAreaComponent.tsx

@@ -0,0 +1,114 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { Observer } from 'babylonjs/Misc/observable';
+import { Nullable } from 'babylonjs/types';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+
+const doubleSided: string = require("./svgs/doubleSided.svg");
+const depthPass: string = require("./svgs/depthPass.svg");
+const omni: string = require("./svgs/omni.svg");
+const directionalRight: string = require("./svgs/directionalRight.svg");
+const directionalLeft: string = require("./svgs/directionalLeft.svg");
+
+interface IPreviewAreaComponentProps {
+    globalState: GlobalState;
+    width: number;
+}
+
+export class PreviewAreaComponent extends React.Component<IPreviewAreaComponentProps, {isLoading: boolean}> {
+    private _onIsLoadingChangedObserver: Nullable<Observer<boolean>>;
+    private _onResetRequiredObserver: Nullable<Observer<void>>;
+
+    constructor(props: IPreviewAreaComponentProps) {
+        super(props);
+        this.state = {isLoading: true};
+
+        this._onIsLoadingChangedObserver = this.props.globalState.onIsLoadingChanged.add((state) => this.setState({isLoading: state}));
+
+        this._onResetRequiredObserver = this.props.globalState.onResetRequiredObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onIsLoadingChanged.remove(this._onIsLoadingChangedObserver);
+        this.props.globalState.onResetRequiredObservable.remove(this._onResetRequiredObserver);
+    }
+
+    changeBackFaceCulling(value: boolean) {
+        this.props.globalState.backFaceCulling = value;
+        DataStorage.WriteBoolean("BackFaceCulling", value);
+        this.props.globalState.onBackFaceCullingChanged.notifyObservers();
+        this.forceUpdate();
+    }
+
+    changeDepthPrePass(value: boolean) {
+        this.props.globalState.depthPrePass = value;
+        DataStorage.WriteBoolean("DepthPrePass", value);
+        this.props.globalState.onDepthPrePassChanged.notifyObservers();
+        this.forceUpdate();
+    }
+
+    render() {
+        return (
+            <>
+                <div id="preview" style={{height: this.props.width + "px"}}>
+                    <canvas id="preview-canvas"/>
+                    {
+                        <div className={"waitPanel" + (this.state.isLoading ? "" : " hidden")}>
+                            Please wait, loading...
+                        </div>
+                    }
+                </div>
+                { this.props.globalState.mode === NodeMaterialModes.Material && <>
+                    <div id="preview-config-bar">
+                        <div
+                            title="Render without back face culling"
+                            onClick={() => this.changeBackFaceCulling(!this.props.globalState.backFaceCulling)} className={"button back-face" + (!this.props.globalState.backFaceCulling ? " selected" : "")}>
+                            <img src={doubleSided} alt=""/>
+                        </div>
+                        <div
+                            title="Render with depth pre-pass"
+                            onClick={() => this.changeDepthPrePass(!this.props.globalState.depthPrePass)} className={"button depth-pass" + (this.props.globalState.depthPrePass ? " selected" : "")}>
+                                <img src={depthPass} alt=""/>
+                        </div>
+                        <div
+                            title="Turn on/off hemispheric light"
+                            onClick={() => {
+                                this.props.globalState.hemisphericLight = !this.props.globalState.hemisphericLight;
+                                DataStorage.WriteBoolean("HemisphericLight", this.props.globalState.hemisphericLight);
+                                this.props.globalState.onLightUpdated.notifyObservers();
+                                this.forceUpdate();
+                            }} className={"button hemispheric-light" + (this.props.globalState.hemisphericLight ? " selected" : "")}>
+                            <img src={omni} alt=""/>
+                        </div>
+                        <div
+                            title="Turn on/off direction light #1"
+                            onClick={() => {
+                                this.props.globalState.directionalLight1 = !this.props.globalState.directionalLight1;
+                                DataStorage.WriteBoolean("DirectionalLight1", this.props.globalState.directionalLight1);
+                                this.props.globalState.onLightUpdated.notifyObservers();
+                                this.forceUpdate();
+                            }} className={"button direction-light-1" + (this.props.globalState.directionalLight1 ? " selected" : "")}>
+                            <img src={directionalRight} alt=""/>
+
+                        </div>
+                        <div
+                            title="Turn on/off direction light #0"
+                            onClick={() => {
+                                this.props.globalState.directionalLight0 = !this.props.globalState.directionalLight0;
+                                DataStorage.WriteBoolean("DirectionalLight0", this.props.globalState.directionalLight0);
+                                this.props.globalState.onLightUpdated.notifyObservers();
+                                this.forceUpdate();
+                            }} className={"button direction-light-0" + (this.props.globalState.directionalLight0 ? " selected" : "")}>
+                            <img src={directionalLeft} alt=""/>
+                        </div>
+                    </div>
+                </> }
+            </>
+        );
+
+    }
+}

+ 558 - 0
guiEditor/src/components/preview/previewManager.ts

@@ -0,0 +1,558 @@
+import { GlobalState } from "../../globalState";
+import { NodeMaterial } from "babylonjs/Materials/Node/nodeMaterial";
+import { Nullable } from "babylonjs/types";
+import { Observer } from "babylonjs/Misc/observable";
+import { Engine } from "babylonjs/Engines/engine";
+import { Scene } from "babylonjs/scene";
+import { Mesh } from "babylonjs/Meshes/mesh";
+import { Vector3 } from "babylonjs/Maths/math.vector";
+import { HemisphericLight } from "babylonjs/Lights/hemisphericLight";
+import { ArcRotateCamera } from "babylonjs/Cameras/arcRotateCamera";
+import { PreviewType } from "./previewType";
+import { Animation } from "babylonjs/Animations/animation";
+import { SceneLoader } from "babylonjs/Loading/sceneLoader";
+import { TransformNode } from "babylonjs/Meshes/transformNode";
+import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
+import { FramingBehavior } from "babylonjs/Behaviors/Cameras/framingBehavior";
+import { DirectionalLight } from "babylonjs/Lights/directionalLight";
+import { LogEntry } from "../log/logComponent";
+import { PointerEventTypes } from "babylonjs/Events/pointerEvents";
+import { Color3, Color4 } from "babylonjs/Maths/math.color";
+import { PostProcess } from "babylonjs/PostProcesses/postProcess";
+import { Constants } from "babylonjs/Engines/constants";
+import { CurrentScreenBlock } from "babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock";
+import { NodeMaterialModes } from "babylonjs/Materials/Node/Enums/nodeMaterialModes";
+import { ParticleSystem } from "babylonjs/Particles/particleSystem";
+import { IParticleSystem } from "babylonjs/Particles/IParticleSystem";
+import { ParticleHelper } from "babylonjs/Particles/particleHelper";
+import { Texture } from "babylonjs/Materials/Textures/texture";
+import { ParticleTextureBlock } from "babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock";
+import { FileTools } from "babylonjs/Misc/fileTools";
+import { ProceduralTexture } from "babylonjs/Materials/Textures/Procedurals/proceduralTexture";
+import { StandardMaterial } from "babylonjs/Materials/standardMaterial";
+import { Layer } from "babylonjs/Layers/layer";
+
+export class PreviewManager {
+    private _nodeMaterial: NodeMaterial;
+    private _onBuildObserver: Nullable<Observer<NodeMaterial>>;
+    private _onPreviewCommandActivatedObserver: Nullable<Observer<boolean>>;
+    private _onAnimationCommandActivatedObserver: Nullable<Observer<void>>;
+    private _onUpdateRequiredObserver: Nullable<Observer<void>>;
+    private _onPreviewBackgroundChangedObserver: Nullable<Observer<void>>;
+    private _onBackFaceCullingChangedObserver: Nullable<Observer<void>>;
+    private _onDepthPrePassChangedObserver: Nullable<Observer<void>>;
+    private _onLightUpdatedObserver: Nullable<Observer<void>>;
+    private _engine: Engine;
+    private _scene: Scene;
+    private _meshes: AbstractMesh[];
+    private _camera: ArcRotateCamera;
+    private _material: NodeMaterial | StandardMaterial;
+    private _globalState: GlobalState;
+    private _currentType: number;
+    private _lightParent: TransformNode;
+    private _postprocess: Nullable<PostProcess>;
+    private _proceduralTexture: Nullable<ProceduralTexture>;
+    private _particleSystem: Nullable<IParticleSystem>;
+    private _layer: Nullable<Layer>;
+
+    public constructor(targetCanvas: HTMLCanvasElement, globalState: GlobalState) {
+        this._nodeMaterial = globalState.nodeMaterial;
+        this._globalState = globalState;
+
+        this._onBuildObserver = this._nodeMaterial.onBuildObservable.add((nodeMaterial) => {
+            let serializationObject = nodeMaterial.serialize();
+            this._updatePreview(serializationObject);
+        });
+
+        this._onPreviewCommandActivatedObserver = globalState.onPreviewCommandActivated.add((forceRefresh: boolean) => {
+            if (forceRefresh) {
+                this._currentType = -1;
+            }
+            this._refreshPreviewMesh();
+        });
+
+        this._onLightUpdatedObserver = globalState.onLightUpdated.add(() => {
+            this._prepareLights();
+        });
+
+        this._onUpdateRequiredObserver = globalState.onUpdateRequiredObservable.add(() => {
+            let serializationObject = this._nodeMaterial.serialize();
+            this._updatePreview(serializationObject);
+        });
+
+        this._onPreviewBackgroundChangedObserver = globalState.onPreviewBackgroundChanged.add(() => {
+            this._scene.clearColor = this._globalState.backgroundColor;
+        });
+
+        this._onAnimationCommandActivatedObserver = globalState.onAnimationCommandActivated.add(() => {
+            this._handleAnimations();
+        });
+
+        this._onBackFaceCullingChangedObserver = globalState.onBackFaceCullingChanged.add(() => {
+            this._material.backFaceCulling = this._globalState.backFaceCulling;
+        });
+
+        this._onDepthPrePassChangedObserver = globalState.onDepthPrePassChanged.add(() => {
+            this._material.needDepthPrePass = this._globalState.depthPrePass;
+        });
+
+        this._engine = new Engine(targetCanvas, true);
+        this._scene = new Scene(this._engine);
+        this._scene.clearColor = this._globalState.backgroundColor;
+        this._camera = new ArcRotateCamera("Camera", 0, 0.8, 4, Vector3.Zero(), this._scene);
+
+        this._camera.lowerRadiusLimit = 3;
+        this._camera.upperRadiusLimit = 10;
+        this._camera.wheelPrecision = 20;
+        this._camera.minZ = 0.1;
+        this._camera.attachControl(false);
+
+        this._lightParent = new TransformNode("LightParent", this._scene);
+
+        this._refreshPreviewMesh();
+
+        this._engine.runRenderLoop(() => {
+            this._engine.resize();
+            this._scene.render();
+        });
+
+        let lastOffsetX: number | undefined = undefined;
+        const lightRotationSpeed = 0.01;
+
+        this._scene.onPointerObservable.add((evt) => {
+            if (this._globalState.controlCamera) {
+                return;
+            }
+
+            if (evt.type === PointerEventTypes.POINTERUP) {
+                lastOffsetX = undefined;
+                return;
+            }
+
+            if (evt.event.buttons !== 1) {
+                return;
+            }
+
+            if (lastOffsetX === undefined) {
+                lastOffsetX = evt.event.offsetX;
+            }
+
+            var rotateLighting = (lastOffsetX - evt.event.offsetX) * lightRotationSpeed;
+            this._lightParent.rotation.y += rotateLighting;
+            lastOffsetX = evt.event.offsetX;
+        });
+
+        // this._scene.registerBeforeRender(() => {
+        //     if (this._camera.alpha === cameraLastRotation) {
+        //         return;
+        //     }
+        //     if (!this._globalState.controlCamera) {
+        //         return;
+        //     }
+        //     var rotateLighting = (this._camera.alpha - cameraLastRotation) * lightRotationParallaxSpeed;
+        //     this._lightParent.rotate(Vector3.Up(), rotateLighting);
+        //     cameraLastRotation = this._camera.alpha;
+        // });
+    }
+
+    private _handleAnimations() {
+        this._scene.stopAllAnimations();
+
+        if (this._globalState.rotatePreview) {
+            for (var root of this._scene.rootNodes) {
+                let transformNode = root as TransformNode;
+
+                if (transformNode.getClassName() === "TransformNode" || transformNode.getClassName() === "Mesh" || transformNode.getClassName() === "GroundMesh") {
+                    if (transformNode.rotationQuaternion) {
+                        transformNode.rotation = transformNode.rotationQuaternion.toEulerAngles();
+                        transformNode.rotationQuaternion = null;
+                    }
+                    Animation.CreateAndStartAnimation("turnTable", root, "rotation.y", 60, 1200, transformNode.rotation.y, transformNode.rotation.y + 2 * Math.PI, 1);
+                }
+            }
+        }
+    }
+
+    private _prepareLights() {
+        // Remove current lights
+        let currentLights = this._scene.lights.slice(0);
+
+        for (var light of currentLights) {
+            light.dispose();
+        }
+
+        // Create new lights based on settings
+        if (this._globalState.hemisphericLight) {
+            new HemisphericLight("Hemispheric light", new Vector3(0, 1, 0), this._scene);
+        }
+
+        if (this._globalState.directionalLight0) {
+            let dir0 = new DirectionalLight("Directional light #0", new Vector3(0.841626576496605, -0.2193391004130599, -0.49351298337996535), this._scene);
+            dir0.intensity = 0.9;
+            dir0.diffuse = new Color3(0.9294117647058824, 0.9725490196078431, 0.996078431372549);
+            dir0.specular = new Color3(0.9294117647058824, 0.9725490196078431, 0.996078431372549);
+            dir0.parent = this._lightParent;
+        }
+
+        if (this._globalState.directionalLight1) {
+            let dir1 = new DirectionalLight("Directional light #1", new Vector3(-0.9519937437504213, -0.24389315636999764, -0.1849974057546125), this._scene);
+            dir1.intensity = 1.2;
+            dir1.specular = new Color3(0.9803921568627451, 0.9529411764705882, 0.7725490196078432);
+            dir1.diffuse = new Color3(0.9803921568627451, 0.9529411764705882, 0.7725490196078432);
+            dir1.parent = this._lightParent;
+        }
+    }
+
+    private _prepareScene() {
+        this._camera.useFramingBehavior = this._globalState.mode === NodeMaterialModes.Material;
+
+        switch (this._globalState.mode) {
+            case NodeMaterialModes.Material: {
+                this._prepareLights();
+
+                var framingBehavior = this._camera.getBehaviorByName("Framing") as FramingBehavior;
+
+                setTimeout(() => {
+                    // Let the behavior activate first
+                    framingBehavior.framingTime = 0;
+                    framingBehavior.elevationReturnTime = -1;
+
+                    if (this._scene.meshes.length) {
+                        var worldExtends = this._scene.getWorldExtends();
+                        this._camera.lowerRadiusLimit = null;
+                        this._camera.upperRadiusLimit = null;
+                        framingBehavior.zoomOnBoundingInfo(worldExtends.min, worldExtends.max);
+                    }
+
+                    this._camera.pinchPrecision = 200 / this._camera.radius;
+                    this._camera.upperRadiusLimit = 5 * this._camera.radius;
+                });
+
+                this._camera.wheelDeltaPercentage = 0.01;
+                this._camera.pinchDeltaPercentage = 0.01;
+
+                // Animations
+                this._handleAnimations();
+                break;
+            }
+            case NodeMaterialModes.PostProcess:
+            case NodeMaterialModes.ProceduralTexture: {
+                this._camera.radius = 4;
+                this._camera.upperRadiusLimit = 10;
+                break;
+            }
+            case NodeMaterialModes.Particle: {
+                this._camera.radius = this._globalState.previewType === PreviewType.Explosion ? 50 : this._globalState.previewType === PreviewType.DefaultParticleSystem ? 6 : 20;
+                this._camera.upperRadiusLimit = 5000;
+                break;
+            }
+        }
+
+        // Material
+        let serializationObject = this._nodeMaterial.serialize();
+        this._updatePreview(serializationObject);
+    }
+
+    private _refreshPreviewMesh() {
+        if (this._currentType !== this._globalState.previewType || this._currentType === PreviewType.Custom) {
+            this._currentType = this._globalState.previewType;
+            if (this._meshes && this._meshes.length) {
+                for (var mesh of this._meshes) {
+                    mesh.dispose();
+                }
+            }
+            this._meshes = [];
+
+            if (this._layer) {
+                this._layer.dispose();
+                this._layer = null;
+            }
+
+            let lights = this._scene.lights.slice(0);
+            for (var light of lights) {
+                light.dispose();
+            }
+
+            this._engine.releaseEffects();
+
+            if (this._particleSystem) {
+                this._particleSystem.onBeforeDrawParticlesObservable.clear();
+                this._particleSystem.onDisposeObservable.clear();
+                this._particleSystem.stop();
+                this._particleSystem.dispose();
+                this._particleSystem = null;
+            }
+
+            SceneLoader.ShowLoadingScreen = false;
+
+            this._globalState.onIsLoadingChanged.notifyObservers(true);
+
+            if (this._globalState.mode === NodeMaterialModes.Material) {
+                switch (this._globalState.previewType) {
+                    case PreviewType.Box:
+                        SceneLoader.AppendAsync("https://models.babylonjs.com/", "roundedCube.glb", this._scene).then(() => {
+                            this._meshes.push(...this._scene.meshes);
+                            this._prepareScene();
+                        });
+                        return;
+                    case PreviewType.Sphere:
+                        this._meshes.push(Mesh.CreateSphere("dummy-sphere", 32, 2, this._scene));
+                        break;
+                    case PreviewType.Torus:
+                        this._meshes.push(Mesh.CreateTorus("dummy-torus", 2, 0.5, 32, this._scene));
+                        break;
+                    case PreviewType.Cylinder:
+                        SceneLoader.AppendAsync("https://models.babylonjs.com/", "roundedCylinder.glb", this._scene).then(() => {
+                            this._meshes.push(...this._scene.meshes);
+                            this._prepareScene();
+                        });
+                        return;
+                    case PreviewType.Plane:
+                        let plane = Mesh.CreateGround("dummy-plane", 2, 2, 128, this._scene);
+                        plane.scaling.y = -1;
+                        plane.rotation.x = Math.PI;
+                        this._meshes.push(plane);
+                        break;
+                    case PreviewType.ShaderBall:
+                        SceneLoader.AppendAsync("https://models.babylonjs.com/", "shaderBall.glb", this._scene).then(() => {
+                            this._meshes.push(...this._scene.meshes);
+                            this._prepareScene();
+                        });
+                        return;
+                    case PreviewType.Custom:
+                        SceneLoader.AppendAsync("file:", this._globalState.previewFile, this._scene).then(() => {
+                            this._meshes.push(...this._scene.meshes);
+                            this._prepareScene();
+                        });
+                        return;
+                }
+            } else if (this._globalState.mode === NodeMaterialModes.ProceduralTexture) {
+                this._layer = new Layer("proceduralLayer", null, this._scene);
+            } else if (this._globalState.mode === NodeMaterialModes.Particle) {
+                switch (this._globalState.previewType) {
+                    case PreviewType.DefaultParticleSystem:
+                        this._particleSystem = ParticleHelper.CreateDefault(new Vector3(0, 0, 0), 500, this._scene);
+                        this._particleSystem.start();
+                        break;
+                    case PreviewType.Bubbles:
+                        this._particleSystem = new ParticleSystem("particles", 4000, this._scene);
+                        this._particleSystem.particleTexture = new Texture("https://assets.babylonjs.com/particles/textures/explosion/Flare.png", this._scene);
+                        this._particleSystem.minSize = 0.1;
+                        this._particleSystem.maxSize = 1.0;
+                        this._particleSystem.minLifeTime = 0.5;
+                        this._particleSystem.maxLifeTime = 5.0;
+                        this._particleSystem.minEmitPower = 0.5;
+                        this._particleSystem.maxEmitPower = 3.0;
+                        this._particleSystem.createBoxEmitter(new Vector3(-1, 1, -1), new Vector3(1, 1, 1), new Vector3(-0.1, -0.1, -0.1), new Vector3(0.1, 0.1, 0.1));
+                        this._particleSystem.emitRate = 100;
+                        this._particleSystem.blendMode = ParticleSystem.BLENDMODE_ONEONE;
+                        this._particleSystem.color1 = new Color4(1, 1, 0, 1);
+                        this._particleSystem.color2 = new Color4(1, 0.5, 0, 1);
+                        this._particleSystem.gravity = new Vector3(0, -1.0, 0);
+                        this._particleSystem.start();
+                        break;
+                    case PreviewType.Explosion:
+                        this._loadParticleSystem(this._globalState.previewType, 1);
+                        return;
+                    case PreviewType.Fire:
+                    case PreviewType.Rain:
+                    case PreviewType.Smoke:
+                        this._loadParticleSystem(this._globalState.previewType);
+                        return;
+                    case PreviewType.Custom:
+                        FileTools.ReadFile(
+                            this._globalState.previewFile,
+                            (json) => {
+                                this._particleSystem = ParticleSystem.Parse(JSON.parse(json), this._scene, "");
+                                this._particleSystem.start();
+                                this._prepareScene();
+                            },
+                            undefined,
+                            false,
+                            (error) => {
+                                console.log(error);
+                            }
+                        );
+                        return;
+                }
+            }
+
+            this._prepareScene();
+        }
+    }
+
+    private _loadParticleSystem(particleNumber: number, systemIndex = 0, prepareScene = true) {
+        let name = "";
+
+        switch (particleNumber) {
+            case PreviewType.Explosion:
+                name = "explosion";
+                break;
+            case PreviewType.Fire:
+                name = "fire";
+                break;
+            case PreviewType.Rain:
+                name = "rain";
+                break;
+            case PreviewType.Smoke:
+                name = "smoke";
+                break;
+        }
+
+        ParticleHelper.CreateAsync(name, this._scene).then((set) => {
+            for (let i = 0; i < set.systems.length; ++i) {
+                if (i == systemIndex) {
+                    this._particleSystem = set.systems[i];
+                    this._particleSystem.disposeOnStop = true;
+                    this._particleSystem.onDisposeObservable.add(() => {
+                        this._loadParticleSystem(particleNumber, systemIndex, false);
+                    });
+                    this._particleSystem.start();
+                } else {
+                    set.systems[i].dispose();
+                }
+            }
+            if (prepareScene) {
+                this._prepareScene();
+            } else {
+                let serializationObject = this._nodeMaterial.serialize();
+                this._updatePreview(serializationObject);
+            }
+        });
+    }
+
+    private _forceCompilationAsync(material: NodeMaterial, mesh: AbstractMesh): Promise<void> {
+        return material.forceCompilationAsync(mesh);
+    }
+
+    private _updatePreview(serializationObject: any) {
+        try {
+            let store = NodeMaterial.IgnoreTexturesAtLoadTime;
+            NodeMaterial.IgnoreTexturesAtLoadTime = false;
+            let tempMaterial = NodeMaterial.Parse(serializationObject, this._scene);
+            NodeMaterial.IgnoreTexturesAtLoadTime = store;
+
+            tempMaterial.backFaceCulling = this._globalState.backFaceCulling;
+            tempMaterial.needDepthPrePass = this._globalState.depthPrePass;
+
+            if (this._postprocess) {
+                this._postprocess.dispose(this._camera);
+                this._postprocess = null;
+            }
+
+            if (this._proceduralTexture) {
+                this._proceduralTexture.dispose();
+                this._proceduralTexture = null;
+            }
+
+            switch (this._globalState.mode) {
+                case NodeMaterialModes.PostProcess: {
+                    this._globalState.onIsLoadingChanged.notifyObservers(false);
+
+                    this._postprocess = tempMaterial.createPostProcess(this._camera, 1.0, Constants.TEXTURE_NEAREST_SAMPLINGMODE, this._engine);
+
+                    const currentScreen = tempMaterial.getBlockByPredicate((block) => block instanceof CurrentScreenBlock);
+                    if (currentScreen && this._postprocess) {
+                        this._postprocess.onApplyObservable.add((effect) => {
+                            effect.setTexture("textureSampler", (currentScreen as CurrentScreenBlock).texture);
+                        });
+                    }
+
+                    if (this._material) {
+                        this._material.dispose();
+                    }
+                    this._material = tempMaterial;
+                    break;
+                }
+                case NodeMaterialModes.ProceduralTexture: {
+                    this._globalState.onIsLoadingChanged.notifyObservers(false);
+
+                    this._proceduralTexture = tempMaterial.createProceduralTexture(512, this._scene);
+
+                    if (this._material) {
+                        this._material.dispose();
+                    }
+
+                    if (this._layer) {
+                        this._layer.texture = this._proceduralTexture;
+                    }
+
+                    break;
+                }
+
+                case NodeMaterialModes.Particle: {
+                    this._globalState.onIsLoadingChanged.notifyObservers(false);
+
+                    this._particleSystem!.onBeforeDrawParticlesObservable.clear();
+
+                    this._particleSystem!.onBeforeDrawParticlesObservable.add((effect) => {
+                        const textureBlock = tempMaterial.getBlockByPredicate((block) => block instanceof ParticleTextureBlock);
+                        if (textureBlock && (textureBlock as ParticleTextureBlock).texture && effect) {
+                            effect.setTexture("diffuseSampler", (textureBlock as ParticleTextureBlock).texture);
+                        }
+                    });
+                    tempMaterial.createEffectForParticles(this._particleSystem!);
+
+                    if (this._material) {
+                        this._material.dispose();
+                    }
+                    this._material = tempMaterial;
+                    break;
+                }
+
+                default: {
+                    if (this._meshes.length) {
+                        let tasks = this._meshes.map((m) => this._forceCompilationAsync(tempMaterial, m));
+
+                        Promise.all(tasks)
+                            .then(() => {
+                                for (var mesh of this._meshes) {
+                                    mesh.material = tempMaterial;
+                                }
+
+                                if (this._material) {
+                                    this._material.dispose();
+                                }
+
+                                this._material = tempMaterial;
+                                this._globalState.onIsLoadingChanged.notifyObservers(false);
+                            })
+                            .catch((reason) => {
+                                this._globalState.onLogRequiredObservable.notifyObservers(new LogEntry("Shader compilation error:\r\n" + reason, true));
+                                this._globalState.onIsLoadingChanged.notifyObservers(false);
+                            });
+                    } else {
+                        this._material = tempMaterial;
+                    }
+                    break;
+                }
+            }
+        } catch (err) {
+            // Ignore the error
+            this._globalState.onIsLoadingChanged.notifyObservers(false);
+        }
+    }
+
+    public dispose() {
+        this._nodeMaterial.onBuildObservable.remove(this._onBuildObserver);
+        this._globalState.onPreviewCommandActivated.remove(this._onPreviewCommandActivatedObserver);
+        this._globalState.onUpdateRequiredObservable.remove(this._onUpdateRequiredObserver);
+        this._globalState.onAnimationCommandActivated.remove(this._onAnimationCommandActivatedObserver);
+        this._globalState.onPreviewBackgroundChanged.remove(this._onPreviewBackgroundChangedObserver);
+        this._globalState.onBackFaceCullingChanged.remove(this._onBackFaceCullingChangedObserver);
+        this._globalState.onDepthPrePassChanged.remove(this._onDepthPrePassChangedObserver);
+        this._globalState.onLightUpdated.remove(this._onLightUpdatedObserver);
+
+        if (this._material) {
+            this._material.dispose();
+        }
+
+        this._camera.dispose();
+        for (var mesh of this._meshes) {
+            mesh.dispose();
+        }
+
+        this._scene.dispose();
+        this._engine.dispose();
+    }
+}

+ 173 - 0
guiEditor/src/components/preview/previewMeshControlComponent.tsx

@@ -0,0 +1,173 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { Color3, Color4 } from 'babylonjs/Maths/math.color';
+import { PreviewType } from './previewType';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { Observer } from 'babylonjs/Misc/observable';
+import { Nullable } from 'babylonjs/types';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+
+const popUpIcon: string = require("./svgs/popOut.svg");
+const colorPicker: string = require("./svgs/colorPicker.svg");
+const pauseIcon: string = require("./svgs/pauseIcon.svg");
+const playIcon: string = require("./svgs/playIcon.svg");
+
+interface IPreviewMeshControlComponent {
+    globalState: GlobalState;
+    togglePreviewAreaComponent: () => void;
+}
+
+export class PreviewMeshControlComponent extends React.Component<IPreviewMeshControlComponent> {
+    private colorInputRef: React.RefObject<HTMLInputElement>;
+    private filePickerRef: React.RefObject<HTMLInputElement>;
+    private _onResetRequiredObserver: Nullable<Observer<void>>;
+
+    constructor(props: IPreviewMeshControlComponent) {
+        super(props);
+        this.colorInputRef = React.createRef();
+        this.filePickerRef = React.createRef();
+
+        this._onResetRequiredObserver = this.props.globalState.onResetRequiredObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onResetRequiredObservable.remove(this._onResetRequiredObserver);
+    }
+
+    changeMeshType(newOne: PreviewType) {
+        if (this.props.globalState.previewType === newOne) {
+            return;
+        }
+
+        this.props.globalState.previewType = newOne;
+        this.props.globalState.onPreviewCommandActivated.notifyObservers(false);
+
+        DataStorage.WriteNumber("PreviewType", newOne);
+
+        this.forceUpdate();
+    }
+
+    useCustomMesh(evt: any) {
+        var files: File[] = evt.target.files;
+        if (files && files.length) {
+            let file = files[0];
+
+            this.props.globalState.previewFile = file;
+            this.props.globalState.previewType = PreviewType.Custom;
+            this.props.globalState.onPreviewCommandActivated.notifyObservers(false);
+            this.props.globalState.listOfCustomPreviewFiles = [file];
+            this.forceUpdate();
+        }
+        if (this.filePickerRef.current) {
+            this.filePickerRef.current.value = "";
+        }
+    }
+
+    onPopUp() {
+        this.props.togglePreviewAreaComponent();
+    }
+
+    changeAnimation() {
+        this.props.globalState.rotatePreview = !this.props.globalState.rotatePreview;
+        this.props.globalState.onAnimationCommandActivated.notifyObservers();
+        this.forceUpdate();
+    }
+
+    changeBackground(value: string) {
+        const newColor = Color3.FromHexString(value);
+
+        DataStorage.WriteNumber("BackgroundColorR", newColor.r);
+        DataStorage.WriteNumber("BackgroundColorG", newColor.g);
+        DataStorage.WriteNumber("BackgroundColorB", newColor.b);
+
+        const newBackgroundColor = Color4.FromColor3(newColor, 1.0);
+        this.props.globalState.backgroundColor = newBackgroundColor;
+        this.props.globalState.onPreviewBackgroundChanged.notifyObservers();
+    }
+
+    changeBackgroundClick() {
+        this.colorInputRef.current?.click();
+    }
+
+    render() {
+
+        var meshTypeOptions = [
+            { label: "Cube", value: PreviewType.Box },
+            { label: "Cylinder", value: PreviewType.Cylinder },
+            { label: "Plane", value: PreviewType.Plane },
+            { label: "Shader ball", value: PreviewType.ShaderBall },
+            { label: "Sphere", value: PreviewType.Sphere },
+            { label: "Load...", value: PreviewType.Custom + 1 }
+        ];
+
+        var particleTypeOptions = [
+            { label: "Default", value: PreviewType.DefaultParticleSystem },
+            { label: "Bubbles", value: PreviewType.Bubbles },
+            { label: "Explosion", value: PreviewType.Explosion },
+            { label: "Fire", value: PreviewType.Fire },
+            { label: "Rain", value: PreviewType.Rain },
+            { label: "Smoke", value: PreviewType.Smoke },
+            { label: "Load...", value: PreviewType.Custom + 1 }
+        ];
+
+        if (this.props.globalState.listOfCustomPreviewFiles.length > 0) {
+            meshTypeOptions.splice(0, 0, {
+                label: "Custom", value: PreviewType.Custom
+            });
+
+            particleTypeOptions.splice(0, 0, {
+                label: "Custom", value: PreviewType.Custom
+            });
+        }
+
+        var options = this.props.globalState.mode === NodeMaterialModes.Particle ? particleTypeOptions : meshTypeOptions;
+        var accept = this.props.globalState.mode === NodeMaterialModes.Particle ? ".json" : ".gltf, .glb, .babylon, .obj";
+
+        return (
+            <div id="preview-mesh-bar">
+                { (this.props.globalState.mode === NodeMaterialModes.Material || this.props.globalState.mode === NodeMaterialModes.Particle) && <>
+                    <OptionsLineComponent label="" options={options} target={this.props.globalState}
+                                propertyName="previewType"
+                                noDirectUpdate={true}
+                                onSelect={(value: any) => {
+                                    if (value !== PreviewType.Custom + 1) {
+                                        this.changeMeshType(value);
+                                    } else {
+                                        this.filePickerRef.current?.click();
+                                    }
+                                }} />
+                    <div style={{
+                        display: "none"
+                    }} title="Preview with a custom mesh" >
+                        <input ref={this.filePickerRef} id="file-picker" type="file" onChange={(evt) => this.useCustomMesh(evt)} accept={accept}/>
+                    </div>
+                </> }
+                { this.props.globalState.mode === NodeMaterialModes.Material && <>
+                    <div
+                        title="Turn-table animation"
+                        onClick={() => this.changeAnimation()} className="button" id="play-button">
+                        {this.props.globalState.rotatePreview ? <img src={pauseIcon} alt=""/> : <img src={playIcon} alt=""/>}
+                    </div>
+                    <div
+                    id="color-picker-button"
+                        title="Background color"
+                        className={"button align"}
+                        onClick={(_) => this.changeBackgroundClick()}
+                        >
+                        <img src={colorPicker} alt="" id="color-picker-image"/>
+                        <input ref={this.colorInputRef} id="color-picker" type="color" onChange={(evt) => this.changeBackground(evt.target.value)} />
+                    </div>
+                </> }
+                <div
+                    title="Open preview in new window" id="preview-new-window"
+                    onClick={() => this.onPopUp()} className="button">
+                    <img src={popUpIcon} alt=""/>
+                </div>
+            </div>
+        );
+    }
+}

+ 19 - 0
guiEditor/src/components/preview/previewType.ts

@@ -0,0 +1,19 @@
+export enum PreviewType {
+    // Meshes
+    Sphere,
+    Box,
+    Torus,
+    Cylinder,
+    Plane,
+    ShaderBall,
+
+    // Particle systems
+    DefaultParticleSystem,
+    Bubbles,
+    Smoke,
+    Rain,
+    Explosion,
+    Fire,
+
+    Custom,
+}

File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/components/preview/svgs/colorPicker.svg


File diff suppressed because it is too large
+ 1 - 0
guiEditor/src/components/preview/svgs/depthPass.svg


File diff suppressed because it is too large
+ 1 - 0
guiEditor/src/components/preview/svgs/directionalLeft.svg


File diff suppressed because it is too large
+ 1 - 0
guiEditor/src/components/preview/svgs/directionalRight.svg


File diff suppressed because it is too large
+ 1 - 0
guiEditor/src/components/preview/svgs/doubleSided.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/components/preview/svgs/omni.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/components/preview/svgs/pauseIcon.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/components/preview/svgs/playIcon.svg


File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/components/preview/svgs/popOut.svg


+ 21 - 0
guiEditor/src/components/propertyTab/properties/color3PropertyTabComponent.tsx

@@ -0,0 +1,21 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { Color3LineComponent } from '../../../sharedComponents/color3LineComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+
+interface IColor3PropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class Color3PropertyTabComponent extends React.Component<IColor3PropertyTabComponentProps> {
+
+    render() {
+        return (
+            <Color3LineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></Color3LineComponent>
+        );
+    }
+}

+ 21 - 0
guiEditor/src/components/propertyTab/properties/color4PropertyTabComponent.tsx

@@ -0,0 +1,21 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { Color4LineComponent } from '../../../sharedComponents/color4LineComponent';
+
+interface IColor4PropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class Color4PropertyTabComponent extends React.Component<IColor4PropertyTabComponentProps> {
+
+    render() {
+        return (
+            <Color4LineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></Color4LineComponent>
+        );
+    }
+}

+ 24 - 0
guiEditor/src/components/propertyTab/properties/floatPropertyTabComponent.tsx

@@ -0,0 +1,24 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { FloatLineComponent } from '../../../sharedComponents/floatLineComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+
+interface IFloatPropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class FloatPropertyTabComponent extends React.Component<IFloatPropertyTabComponentProps> {
+
+    render() {
+        return (
+            <FloatLineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                if (this.props.inputBlock.isConstant) {
+                    this.props.globalState.onRebuildRequiredObservable.notifyObservers();    
+                }
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></FloatLineComponent>
+        );
+    }
+}

+ 26 - 0
guiEditor/src/components/propertyTab/properties/matrixPropertyTabComponent.tsx

@@ -0,0 +1,26 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { MatrixLineComponent } from '../../../sharedComponents/matrixLineComponent';
+
+interface IMatrixPropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class MatrixPropertyTabComponent extends React.Component<IMatrixPropertyTabComponentProps> {
+
+    render() {
+        return (
+            <MatrixLineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}
+            mode={this.props.inputBlock.matrixMode}
+            onModeChange={mode => {
+                this.props.inputBlock.matrixMode = mode;
+            }}
+            ></MatrixLineComponent>
+        );
+    }
+}

+ 21 - 0
guiEditor/src/components/propertyTab/properties/vector2PropertyTabComponent.tsx

@@ -0,0 +1,21 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { Vector2LineComponent } from '../../../sharedComponents/vector2LineComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+
+interface IVector2PropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class Vector2PropertyTabComponent extends React.Component<IVector2PropertyTabComponentProps> {
+
+    render() {
+        return (
+            <Vector2LineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></Vector2LineComponent>
+        );
+    }
+}

+ 21 - 0
guiEditor/src/components/propertyTab/properties/vector3PropertyTabComponent.tsx

@@ -0,0 +1,21 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { Vector3LineComponent } from '../../../sharedComponents/vector3LineComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+
+interface IVector3PropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class Vector3PropertyTabComponent extends React.Component<IVector3PropertyTabComponentProps> {
+
+    render() {
+        return (
+            <Vector3LineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></Vector3LineComponent>
+        );
+    }
+}

+ 21 - 0
guiEditor/src/components/propertyTab/properties/vector4PropertyTabComponent.tsx

@@ -0,0 +1,21 @@
+
+import * as React from "react";
+import { GlobalState } from '../../../globalState';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { Vector4LineComponent } from '../../../sharedComponents/vector4LineComponent';
+
+interface IVector4PropertyTabComponentProps {
+    globalState: GlobalState;
+    inputBlock: InputBlock;
+}
+
+export class Vector4PropertyTabComponent extends React.Component<IVector4PropertyTabComponentProps> {
+
+    render() {
+        return (
+            <Vector4LineComponent globalState={this.props.globalState} label="Value" target={this.props.inputBlock} propertyName="value" onChange={() => {
+                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+            }}></Vector4LineComponent>
+        );
+    }
+}

+ 775 - 0
guiEditor/src/components/propertyTab/propertyTab.scss

@@ -0,0 +1,775 @@
+#propertyTab {
+    $line-padding-left: 5px;
+    color:white;
+    background: #333333;
+
+      #header {
+        height: 30px;
+        font-size: 16px;
+        color: white;
+        background: #222222;
+        grid-row: 1;
+        text-align: center;
+        display: grid;
+        grid-template-columns: 30px 1fr;        
+        -webkit-user-select: none; 
+        -moz-user-select: none;   
+        -ms-user-select: none;    
+        user-select: none;                
+
+        #logo {
+            position: relative;
+            grid-column: 1; 
+            width: 24px;
+            height: 24px;
+            left:0;
+            display: flex;
+            align-self: center;   
+            justify-self: center;
+        }        
+
+        #title {
+            grid-column: 2; 
+            display: grid;
+            align-items: center;   
+            text-align: center;
+        }
+    }
+
+    .range {
+        -webkit-appearance: none;
+        width: 120px;
+        height: 6px;
+        background: #d3d3d3;
+        border-radius: 5px;
+        outline: none;
+        opacity: 0.7;
+        -webkit-transition: .2s;
+        transition: opacity .2s;
+    }
+    
+    .range:hover {
+        opacity: 1;
+    }
+    
+    .range::-webkit-slider-thumb {
+        -webkit-appearance: none;
+        appearance: none;
+        width: 14px;
+        height: 14px;
+        border-radius: 50%;
+        background: rgb(51, 122, 183);
+        cursor: pointer;
+    }
+    
+    .range::-moz-range-thumb {
+        width: 14px;
+        height: 14px;
+        border-radius: 50%;
+        background: rgb(51, 122, 183);
+        cursor: pointer;
+    }
+
+    input[type="color"] {
+        -webkit-appearance: none;
+        border: 1px solid rgba(255, 255, 255, 0.5);
+        padding: 0;
+        width: 30px;
+        height: 20px;
+    }
+    input[type="color"]::-webkit-color-swatch-wrapper {
+        padding: 0;
+    }
+    input[type="color"]::-webkit-color-swatch {
+        border: none;
+    }
+
+    .sliderLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-rows: 100%;
+        grid-template-columns: 1fr 40px;
+
+        .label { 
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .slider {
+            grid-column: 3;
+            grid-row: 1;
+            margin-right: 5px;
+            width: 90%;
+            display: flex;
+            align-items: center;
+        }
+
+        .floatLine {
+            padding-left: $line-padding-left;
+    
+            .label {
+                grid-column: 1;
+                display: flex;
+                align-items: center;
+            }
+        
+            .short {
+                grid-column: 1; 
+                display: flex;
+                align-items: center;
+                
+                input {
+                    width: 27px;
+                }
+                
+                input::-webkit-outer-spin-button,
+                input::-webkit-inner-spin-button {
+                  -webkit-appearance: none;
+                  margin: 0;
+                }
+    
+                input[type=number] {
+                    -moz-appearance: textfield;
+                }
+            }
+        }  
+    }     
+
+    .textInputLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr 120px auto;
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .value {                        
+            display: flex;
+            align-items: center;
+            grid-column: 2;
+            
+            input {
+                width: calc(100% - 5px);
+            }
+        }
+    }
+    
+    .textInputArea {
+        padding-left: $line-padding-left;
+        height: 100%;
+        display: grid;
+        grid-template-columns: 1fr 120px;
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .value {                        
+            display: flex;
+            align-items: center;
+            grid-column: 2;
+            
+            textarea {
+                width: calc(150% - 5px);
+                margin-left: -50%;
+                height: 40px;
+            }
+        }
+    }
+    
+    .paneContainer {
+        margin-top: 3px;
+        display:grid;
+        grid-template-rows: 100%;
+        grid-template-columns: 100%;
+        
+        .paneList {
+            border-left: 3px solid transparent;
+        }
+
+        &:hover {  
+            .paneList {                      
+                border-left: 3px solid rgba(51, 122, 183, 0.8);
+            }
+
+            .paneContainer-content {
+                .header {
+                    .title {   
+                        border-left: 3px solid rgb(51, 122, 183);
+                    }
+                }
+            }
+        }
+        
+        .paneContainer-highlight-border {
+            grid-row: 1;
+            grid-column: 1;
+            opacity: 1;
+            border: 3px solid red;
+            transition: opacity 250ms;
+            pointer-events: none;
+            
+            &.transparent {
+                opacity: 0;
+            }
+        }
+
+        .paneContainer-content {
+            grid-row: 1;
+            grid-column: 1;
+
+            .header {
+                display: grid;
+                grid-template-columns: 1fr auto;
+                background: #555555;    
+                height: 30px;   
+                padding-right: 5px;                        
+                cursor: pointer;
+                
+                .title {                                
+                    border-left: 3px solid transparent;
+                    padding-left: 5px;
+                    grid-column: 1;
+                    display: flex;
+                    align-items: center;
+                }
+
+                .collapse {
+                    grid-column: 2;
+                    display: flex;
+                    align-items: center;  
+                    justify-items: center;
+                    transform-origin: center;
+
+                    &.closed {
+                        transform: rotate(180deg);
+                    }
+                }                        
+            }
+
+            .paneList > div:not(:last-child) {
+                border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
+            }
+
+            .fragment > div:not(:last-child)  {
+                border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
+            }
+        }
+    }
+
+    .color-picker {
+        height: calc(100% - 8px);
+        margin: 4px;
+        width: calc(100% - 8px);
+
+        .color-rect {
+            height: calc(100% - 4px);
+            border: 2px white solid;
+            cursor: pointer;
+            min-height: 18px;
+        }
+
+        .color-picker-cover {
+            position: fixed;
+            top: 0px;
+            right: 0px;
+            bottom: 0px;
+            left: 0px;
+        }
+
+        .color-picker-float {
+            z-index: 2;
+            position: absolute;  
+        }                
+    }
+
+    .gradient-step {
+        display: grid;
+        grid-template-rows: 100%;
+        grid-template-columns: 20px 30px 40px auto 20px 30px;
+        padding-top: 5px;
+        padding-left: 5px;
+        padding-bottom: 5px;
+
+        .step {
+            grid-row: 1;
+            grid-column: 1;
+        }
+            
+        .color {
+            grid-row: 1;
+            grid-column: 2;
+            cursor: pointer;
+        }
+
+        .step-value {       
+            margin-left: 5px;     
+            grid-row: 1;
+            grid-column: 3;
+            text-align: right;
+            margin-right: 5px;
+        }
+
+        .step-slider {            
+            grid-row: 1;
+            grid-column: 4;
+            display: grid;
+            justify-content: stretch;
+            align-content: center;
+            margin-right: -5px;
+            padding-left: 12px;
+
+            input {
+                width: 90%;
+            }
+        }
+
+        .gradient-copy {            
+            grid-row: 1;
+            grid-column: 5;
+            display: grid;
+            align-content: center;
+            justify-content: center;
+ 
+            .img {
+                height: 20px;
+                width: 20px;
+            }
+            .img:hover {
+                cursor: pointer;
+            }
+
+        }
+        .gradient-delete {            
+            grid-row: 1;
+            grid-column: 6;
+            display: grid;
+            align-content: center;
+            justify-content: center;
+            .img {
+                height: 20px;
+                width: 20px;
+            }
+            .img:hover {
+                cursor: pointer;
+            }
+
+        }
+
+    }
+
+    .floatLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr 120px;
+
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .value {
+            grid-column: 2;
+            
+            display: flex;
+            align-items: center;
+            
+            input {
+                width: 110px;
+            }
+        }
+
+        .short {
+            grid-column: 2;
+            
+            display: flex;
+            align-items: center;
+            
+            input {
+                width: 27px;
+            }
+            
+            input::-webkit-outer-spin-button,
+            input::-webkit-inner-spin-button {
+              -webkit-appearance: none;
+              margin: 0;
+            }
+
+            input[type=number] {
+                -moz-appearance: textfield;
+            }
+        }
+    }
+
+    .vector3Line {
+        padding-left:$line-padding-left;                    
+        display: grid;
+
+        .firstLine {
+            display: grid;
+            grid-template-columns: 1fr auto 20px;
+            height: 30px;
+
+            .label {
+                grid-column: 1;
+                display: flex;
+                align-items: center;
+            }
+
+            .vector {
+                grid-column: 2;
+                display: flex;
+                align-items: center;
+                text-align: right;
+                opacity: 0.8;
+            }
+
+            .expand {
+                grid-column: 3;
+                display: grid;
+                align-items: center;
+                justify-items: center;
+                cursor: pointer;
+            }
+        }
+
+        .secondLine {
+            display: grid;
+            padding-right: 5px;  
+            border-left: 1px solid rgb(51, 122, 183);
+
+            .no-right-margin {
+                margin-right: 0;
+            }
+
+            .numeric {
+                display: grid;
+                grid-template-columns: 1fr auto;
+            }
+
+            .numeric-label {
+                text-align: right;
+                grid-column: 1;
+                display: flex;
+                align-items: center;                            
+                justify-self: right;
+                margin-right: 10px;                          
+            }
+
+            .numeric-value {
+                width: 120px;
+                grid-column: 2;
+                display: flex;
+                align-items: center;  
+                border: 1px solid  rgb(51, 122, 183);
+            }                        
+        }
+    }
+
+    .buttonLine {
+        height: 30px;
+        display: grid;
+        align-items: center;
+        justify-items: stretch;
+        padding-bottom: 5px;
+
+        input[type="file"] {
+            display: none;
+        }
+
+        .file-upload {            
+            background: #222222;
+            border: 1px solid rgb(51, 122, 183);
+            margin: 5px 10px;
+            color:white;
+            padding: 4px 5px;
+            padding-top: 0px;
+            opacity: 0.9;
+            cursor: pointer;
+            text-align: center;
+        }
+
+        .file-upload:hover {
+            opacity: 1.0;
+        }
+
+        .file-upload:active {
+            transform: scale(0.98);
+            transform-origin: 0.5 0.5;
+        }
+
+        button {
+            background: #222222;
+            border: 1px solid rgb(51, 122, 183);
+            margin: 5px 10px 5px 10px;
+            color:white;
+            padding: 4px 5px;
+            opacity: 0.9;
+        }
+
+        button:hover {
+            opacity: 1.0;
+        }
+
+        button:active {
+            background: #282828;
+        }   
+        
+        button:focus {
+            border: 1px solid rgb(51, 122, 183);
+            outline: 0px;
+        }  
+    }
+
+    
+    .checkBoxLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr auto;
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .checkBox {
+            grid-column: 2;
+            
+            display: flex;
+            align-items: center;
+
+            .lbl {
+                position: relative;
+                display: block;
+                height: 14px;
+                width: 34px;
+                margin-right: 5px;
+                background: #898989;
+                border-radius: 100px;
+                cursor: pointer;
+                transition: all 0.3s ease;
+            }
+
+            .lbl:after {
+                position: absolute;
+                left: 3px;
+                top: 2px;
+                display: block;
+                width: 10px;
+                height: 10px;
+                border-radius: 100px;
+                background: #fff;
+                box-shadow: 0px 3px 3px rgba(0,0,0,0.05);
+                content: '';
+                transition: all 0.15s ease;
+            }
+
+            .lbl:active:after { 
+                transform: scale(1.15, 0.85); 
+            }
+
+            .cbx:checked ~ label { 
+                background: rgb(51, 122, 183);
+            }
+
+            .cbx:checked ~ label:after {
+                left: 20px;
+                background: rgb(22, 73, 117);
+            }
+
+            .cbx:checked ~ label.disabled { 
+                background: rgb(22, 73, 117);
+                cursor: pointer;
+            }
+
+            .cbx:checked ~ label.disabled:after {
+                left: 20px;
+                background: rgb(85, 85, 85);
+                cursor: pointer;
+            }
+
+            .cbx ~ label.disabled {
+                background: rgb(85, 85, 85);
+                cursor: pointer;
+            }
+
+            .hidden { 
+                display: none; 
+            }               
+        }                    
+    }  
+
+    .listLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr auto;
+
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .options {
+            grid-column: 2;
+            
+            display: flex;
+            align-items: center;   
+            margin-right: 5px;
+
+            select {
+                width: 115px;
+            }
+        }                    
+    }  
+                    
+    .color3Line {
+        padding-left: $line-padding-left;
+        display: grid;
+
+        .firstLine {
+            height: 30px;
+            display: grid;
+            grid-template-columns: 1fr auto 20px 20px;
+
+            .label {
+                grid-column: 1;
+                display: flex;
+                align-items: center;
+            }
+
+            .color3 {
+                grid-column: 2;                
+                width: 50px;
+                
+                display: flex;
+                align-items: center;            
+                
+                input {
+                    margin-right: 5px;
+                }
+            }
+
+            .copy {
+                grid-column: 3;
+                display: grid;
+                align-items: center;
+                justify-items: center;
+                cursor: pointer;
+                
+                img {
+                    height: 100%;
+                    width: 20px;
+                }
+            }
+
+            .expand {
+                grid-column: 4;
+                display: grid;
+                align-items: center;
+                justify-items: center;
+                cursor: pointer;
+
+                img {
+                    height: 100%;
+                    width: 20px;
+                }
+            }
+        }   
+
+        .secondLine {
+            display: grid;
+            padding-right: 5px;  
+            border-left: 1px solid rgb(51, 122, 183);
+
+            .numeric {
+                display: grid;
+                grid-template-columns: 1fr auto;
+            }
+
+            .numeric-label {
+                text-align: right;
+                grid-column: 1;
+                display: flex;
+                align-items: center;                            
+                justify-self: right;
+                margin-right: 10px;                          
+            }
+
+            .numeric-value {
+                width: 120px;
+                grid-column: 2;
+                display: flex;
+                align-items: center;  
+                border: 1px solid  rgb(51, 122, 183);
+            }                        
+        }                  
+    }     
+    
+    .textLine {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr auto;
+
+        .label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .link-value {
+            grid-column: 2;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
+            text-align: end;
+            opacity: 0.8;
+            margin:5px;
+            margin-top: 6px;
+            max-width: 140px;
+            text-decoration: underline;
+            cursor: pointer;
+        }
+
+        .value {
+            grid-column: 2;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
+            text-align: end;
+            opacity: 0.8;
+            margin:5px;
+            margin-top: 6px;
+            max-width: 200px;
+            -webkit-user-select: text; 
+            -moz-user-select: text;   
+            -ms-user-select: text;    
+            user-select: text;                
+
+            &.check {
+                color: green;
+            }
+
+            &.uncheck {
+                color: red;
+            }  
+        }
+    }    
+
+}

+ 470 - 0
guiEditor/src/components/propertyTab/propertyTabComponent.tsx

@@ -0,0 +1,470 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { Nullable } from 'babylonjs/types';
+import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { StringTools } from '../../stringTools';
+import { FileButtonLineComponent } from '../../sharedComponents/fileButtonLineComponent';
+import { Tools } from 'babylonjs/Misc/tools';
+import { SerializationTools } from '../../serializationTools';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { GraphNode } from '../../diagram/graphNode';
+import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
+import { GraphFrame } from '../../diagram/graphFrame';
+import { TextLineComponent } from '../../sharedComponents/textLineComponent';
+import { Engine } from 'babylonjs/Engines/engine';
+import { FramePropertyTabComponent } from '../../diagram/properties/framePropertyComponent';
+import { FrameNodePortPropertyTabComponent } from '../../diagram/properties/frameNodePortPropertyComponent';
+import { NodePortPropertyTabComponent } from '../../diagram/properties/nodePortPropertyComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { Color3LineComponent } from '../../sharedComponents/color3LineComponent';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+import { Color4LineComponent } from '../../sharedComponents/color4LineComponent';
+import { Vector2LineComponent } from '../../sharedComponents/vector2LineComponent';
+import { Vector3LineComponent } from '../../sharedComponents/vector3LineComponent';
+import { Vector4LineComponent } from '../../sharedComponents/vector4LineComponent';
+import { Observer } from 'babylonjs/Misc/observable';
+import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
+import { FrameNodePort } from '../../diagram/frameNodePort';
+import { NodePort } from '../../diagram/nodePort';
+import { isFramePortData } from '../../diagram/graphCanvas';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+import { PreviewType } from '../preview/previewType';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+require("./propertyTab.scss");
+
+interface IPropertyTabComponentProps {
+    globalState: GlobalState;
+}
+
+interface IPropertyTabComponentState {
+    currentNode: Nullable<GraphNode>;
+    currentFrame: Nullable<GraphFrame>;
+    currentFrameNodePort: Nullable<FrameNodePort>;
+    currentNodePort: Nullable<NodePort>;
+ }
+
+export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, IPropertyTabComponentState> {
+    private _onBuiltObserver: Nullable<Observer<void>>;
+    private _modeSelect: React.RefObject<OptionsLineComponent>;
+
+    constructor(props: IPropertyTabComponentProps) {
+        super(props);
+
+        this.state = { currentNode: null, currentFrame: null, currentFrameNodePort: null, currentNodePort: null };
+
+        this._modeSelect = React.createRef();
+    }
+
+    componentDidMount() {
+        this.props.globalState.onSelectionChangedObservable.add((selection) => {
+            if (selection instanceof GraphNode) {
+                this.setState({ currentNode: selection, currentFrame: null, currentFrameNodePort: null, currentNodePort: null });
+            } else if (selection instanceof GraphFrame) {
+                this.setState({ currentNode: null, currentFrame: selection, currentFrameNodePort: null, currentNodePort: null });
+            } else if (isFramePortData(selection)) {
+                this.setState({ currentNode: null, currentFrame: selection.frame, currentFrameNodePort: selection.port, currentNodePort: null });
+            } else if (selection instanceof NodePort) {
+                this.setState({ currentNode: null, currentFrame: null, currentFrameNodePort: null, currentNodePort: selection});
+            } else {
+                this.setState({ currentNode: null, currentFrame: null, currentFrameNodePort: null, currentNodePort: null });
+            }
+        });
+
+        this._onBuiltObserver = this.props.globalState.onBuiltObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onBuiltObservable.remove(this._onBuiltObserver);
+    }
+
+    processInputBlockUpdate(ib: InputBlock) {
+        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+
+        if (ib.isConstant) {
+            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+        }
+    }
+
+    renderInputBlock(block: InputBlock) {
+        switch (block.type) {
+            case NodeMaterialBlockConnectionPointTypes.Float:
+                    let cantDisplaySlider = (isNaN(block.min) || isNaN(block.max) || block.min === block.max);
+                    return (
+                        <div key={block.uniqueId} >
+                            {
+                                block.isBoolean &&
+                                <CheckBoxLineComponent key={block.uniqueId} label={block.name} target={block} propertyName="value"
+                                onValueChanged={() => {
+                                    this.processInputBlockUpdate(block);
+                                }}/>
+                            }
+                            {
+                                !block.isBoolean && cantDisplaySlider &&
+                                <FloatLineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block} propertyName="value"
+                                onChange={() => this.processInputBlockUpdate(block)}/>
+                            }
+                            {
+                                !block.isBoolean && !cantDisplaySlider &&
+                                <SliderLineComponent key={block.uniqueId} label={block.name} target={block} propertyName="value"
+                                step={(block.max - block.min) / 100.0} minimum={block.min} maximum={block.max} globalState={this.props.globalState}
+                                onChange={() => this.processInputBlockUpdate(block)}/>
+                            }
+                        </div>
+                    );
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+                return (
+                    <Color3LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
+                        propertyName="value"
+                        onChange={() => this.processInputBlockUpdate(block)}
+                    />
+                );
+            case NodeMaterialBlockConnectionPointTypes.Color4:
+                return (
+                    <Color4LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block} propertyName="value"
+                    onChange={() => this.processInputBlockUpdate(block)}/>
+                );
+            case NodeMaterialBlockConnectionPointTypes.Vector2:
+                return (
+                        <Vector2LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
+                        propertyName="value"
+                        onChange={() => this.processInputBlockUpdate(block)}/>
+                );
+            case NodeMaterialBlockConnectionPointTypes.Vector3:
+                return (
+                    <Vector3LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
+                    propertyName="value"
+                    onChange={() => this.processInputBlockUpdate(block)}/>
+                );
+            case NodeMaterialBlockConnectionPointTypes.Vector4:
+                return (
+                    <Vector4LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
+                    propertyName="value"
+                    onChange={() => this.processInputBlockUpdate(block)}/>
+                );
+            }
+        return null;
+    }
+
+    load(file: File) {
+        Tools.ReadFile(file, (data) => {
+            let decoder = new TextDecoder("utf-8");
+            SerializationTools.Deserialize(JSON.parse(decoder.decode(data)), this.props.globalState);
+
+            if (!this.changeMode(this.props.globalState.nodeMaterial!.mode, true, false)) {
+                this.props.globalState.onResetRequiredObservable.notifyObservers();
+            }
+            this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+        }, undefined, true);
+    }
+
+    loadFrame(file: File) {
+        Tools.ReadFile(file, (data) => {
+            // get Frame Data from file
+            let decoder = new TextDecoder("utf-8");
+            const frameData = JSON.parse(decoder.decode(data));
+            SerializationTools.AddFrameToMaterial(frameData, this.props.globalState, this.props.globalState.nodeMaterial);
+        }, undefined, true);
+    }
+
+    save() {
+        let json = SerializationTools.Serialize(this.props.globalState.nodeMaterial, this.props.globalState);
+        StringTools.DownloadAsFile(this.props.globalState.hostDocument, json, "nodeMaterial.json");
+    }
+
+    customSave() {
+        this.props.globalState.onLogRequiredObservable.notifyObservers({message: "Saving your material to Babylon.js snippet server...", isError: false});
+        this.props.globalState.customSave!.action(SerializationTools.Serialize(this.props.globalState.nodeMaterial, this.props.globalState)).then(() => {
+            this.props.globalState.onLogRequiredObservable.notifyObservers({message: "Material saved successfully", isError: false});
+        }).catch((err) => {
+            this.props.globalState.onLogRequiredObservable.notifyObservers({message: err, isError: true});
+        });
+    }
+
+    saveToSnippetServer() {
+        const material = this.props.globalState.nodeMaterial;
+        const xmlHttp = new XMLHttpRequest();
+
+        let json = SerializationTools.Serialize(material, this.props.globalState);
+
+        xmlHttp.onreadystatechange = () => {
+            if (xmlHttp.readyState == 4) {
+                if (xmlHttp.status == 200) {
+                    var snippet = JSON.parse(xmlHttp.responseText);
+                    const oldId = material.snippetId;
+                    material.snippetId = snippet.id;
+                    if (snippet.version && snippet.version != "0") {
+                        material.snippetId += "#" + snippet.version;
+                    }
+
+                    this.forceUpdate();
+                    if (navigator.clipboard) {
+                        navigator.clipboard.writeText(material.snippetId);
+                    }
+
+                    let windowAsAny = window as any;
+
+                    if (windowAsAny.Playground && oldId) {
+                        windowAsAny.Playground.onRequestCodeChangeObservable.notifyObservers({
+                            regex: new RegExp(oldId, "g"),
+                            replace: material.snippetId
+                        });
+                    }
+
+                    this.props.globalState.hostDocument.defaultView!.alert("NodeMaterial saved with ID: " + material.snippetId + " (please note that the id was also saved to your clipboard)");
+
+                }
+                else {
+                    this.props.globalState.hostDocument.defaultView!.alert(`Unable to save your node material. It may be too large (${(dataToSend.payload.length / 1024).toFixed(2)} KB) because of embedded textures. Please reduce texture sizes or point to a specific url instead of embedding them and try again.`);
+                }
+            }
+        };
+
+        xmlHttp.open("POST", NodeMaterial.SnippetUrl + (material.snippetId ? "/" + material.snippetId : ""), true);
+        xmlHttp.setRequestHeader("Content-Type", "application/json");
+
+        var dataToSend = {
+            payload : JSON.stringify({
+                nodeMaterial: json
+            }),
+            name: "",
+            description: "",
+            tags: ""
+        };
+
+        xmlHttp.send(JSON.stringify(dataToSend));
+    }
+
+    loadFromSnippet() {
+        const material = this.props.globalState.nodeMaterial;
+        const scene = material.getScene();
+
+        let snippedID = window.prompt("Please enter the snippet ID to use");
+
+        if (!snippedID) {
+            return;
+        }
+
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+
+        NodeMaterial.ParseFromSnippetAsync(snippedID, scene, "", material).then(() => {
+            material.build();
+            if (!this.changeMode(this.props.globalState.nodeMaterial!.mode, true, false)) {
+                this.props.globalState.onResetRequiredObservable.notifyObservers();
+            }
+        }).catch((err) => {
+            this.props.globalState.hostDocument.defaultView!.alert("Unable to load your node material: " + err);
+        });
+    }
+
+    changeMode(value: any, force = false, loadDefault = true): boolean {
+        if (this.props.globalState.mode === value) {
+            return false;
+        }
+
+        if (!force && !this.props.globalState.hostDocument.defaultView!.confirm('Are your sure? You will lose your current changes (if any) if they are not saved!')) {
+            this._modeSelect.current?.setValue(this.props.globalState.mode);
+            return false;
+        }
+
+        if (force) {
+            this._modeSelect.current?.setValue(value);
+        }
+
+        if (loadDefault) {
+            switch (value) {
+                case NodeMaterialModes.Material:
+                    this.props.globalState.nodeMaterial!.setToDefault();
+                    break;
+                case NodeMaterialModes.PostProcess:
+                    this.props.globalState.nodeMaterial!.setToDefaultPostProcess();
+                    break;
+                case NodeMaterialModes.Particle:
+                    this.props.globalState.nodeMaterial!.setToDefaultParticle();
+                    break;
+                case NodeMaterialModes.ProceduralTexture:
+                    this.props.globalState.nodeMaterial!.setToDefaultProceduralTexture();
+                    break;
+            }
+        }
+
+        switch (value) {
+            case NodeMaterialModes.Material:
+                this.props.globalState.previewType = PreviewType.Sphere;
+                break;
+            case NodeMaterialModes.Particle:
+                this.props.globalState.previewType = PreviewType.Bubbles;
+                break;
+        }
+
+        this.props.globalState.listOfCustomPreviewFiles = [];
+        (this.props.globalState.previewFile as any) = undefined;
+
+        DataStorage.WriteNumber("PreviewType", this.props.globalState.previewType);
+
+        this.props.globalState.mode = value as NodeMaterialModes;
+
+        this.props.globalState.onResetRequiredObservable.notifyObservers();
+
+        return true;
+    }
+
+    render() {
+        if (this.state.currentNode) {
+            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>
+                    {this.state.currentNode?.renderProperties() || this.state.currentNodePort?.node.renderProperties()}
+                </div>
+            );
+        }
+
+        if (this.state.currentFrameNodePort && this.state.currentFrame) {
+            return (
+                <FrameNodePortPropertyTabComponent globalState={this.props.globalState} frame={this.state.currentFrame} frameNodePort={this.state.currentFrameNodePort}/>
+            );
+        }
+
+        if (this.state.currentNodePort) {
+            return (
+                <NodePortPropertyTabComponent globalState={this.props.globalState} nodePort={this.state.currentNodePort}/>
+            );
+        }
+
+        if (this.state.currentFrame) {
+            return (
+                <FramePropertyTabComponent globalState={this.props.globalState} frame={this.state.currentFrame}/>
+            );
+        }
+
+        let gridSize = DataStorage.ReadNumber("GridSize", 20);
+
+        const modeList = [
+            { label: "Material", value: NodeMaterialModes.Material },
+            { label: "Post Process", value: NodeMaterialModes.PostProcess },
+            { label: "Particle", value: NodeMaterialModes.Particle },
+            { label: "Procedural", value: NodeMaterialModes.ProceduralTexture },
+        ];
+
+        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">
+                        <OptionsLineComponent ref={this._modeSelect} label="Mode" target={this} getSelection={(target) => this.props.globalState.mode} options={modeList} onSelect={(value) => this.changeMode(value)} />
+                        <TextLineComponent label="Version" value={Engine.Version}/>
+                        <TextLineComponent label="Help" value="doc.babylonjs.com" underline={true} onLink={() => window.open('https://doc.babylonjs.com/how_to/node_material', '_blank')}/>
+                        <TextInputLineComponent label="Comment" multilines={true} value={this.props.globalState.nodeMaterial!.comment} target={this.props.globalState.nodeMaterial} propertyName="comment" globalState={this.props.globalState}/>
+                        <ButtonLineComponent label="Reset to default" onClick={() => {
+                            switch (this.props.globalState.mode) {
+                                case NodeMaterialModes.Material:
+                                    this.props.globalState.nodeMaterial!.setToDefault();
+                                    break;
+                                case NodeMaterialModes.PostProcess:
+                                    this.props.globalState.nodeMaterial!.setToDefaultPostProcess();
+                                    break;
+                                case NodeMaterialModes.Particle:
+                                    this.props.globalState.nodeMaterial!.setToDefaultParticle();
+                                    break;
+                                case NodeMaterialModes.ProceduralTexture:
+                                    this.props.globalState.nodeMaterial!.setToDefaultProceduralTexture();
+                                    break;
+                            }
+                            this.props.globalState.onResetRequiredObservable.notifyObservers();
+                        }} />
+                    </LineContainerComponent>
+                    <LineContainerComponent title="UI">
+                        <ButtonLineComponent label="Zoom to fit" onClick={() => {
+                            this.props.globalState.onZoomToFitRequiredObservable.notifyObservers();
+                        }} />
+                        <ButtonLineComponent label="Reorganize" onClick={() => {
+                            this.props.globalState.onReOrganizedRequiredObservable.notifyObservers();
+                        }} />
+                    </LineContainerComponent>
+                    <LineContainerComponent title="OPTIONS">
+                        <CheckBoxLineComponent label="Embed textures when saving"
+                            isSelected={() => DataStorage.ReadBoolean("EmbedTextures", true)}
+                            onSelect={(value: boolean) => {
+                                DataStorage.WriteBoolean("EmbedTextures", value);
+                            }}
+                        />
+                        <SliderLineComponent label="Grid size" minimum={0} maximum={100} step={5}
+                            decimalCount={0} globalState={this.props.globalState}
+                            directValue={gridSize}
+                            onChange={(value) => {
+                                DataStorage.WriteNumber("GridSize", value);
+                                this.props.globalState.onGridSizeChanged.notifyObservers();
+                                this.forceUpdate();
+                            }}
+                        />
+                        <CheckBoxLineComponent label="Show grid"
+                            isSelected={() => DataStorage.ReadBoolean("ShowGrid", true)}
+                            onSelect={(value: boolean) => {
+                                DataStorage.WriteBoolean("ShowGrid", value);
+                                this.props.globalState.onGridSizeChanged.notifyObservers();
+                            }}
+                        />
+                    </LineContainerComponent>
+                    <LineContainerComponent title="FILE">
+                        <FileButtonLineComponent label="Load" onClick={(file) => this.load(file)} accept=".json" />
+                        <ButtonLineComponent label="Save" onClick={() => {
+                            this.save();
+                        }} />
+                        <ButtonLineComponent label="Generate code" onClick={() => {
+                            StringTools.DownloadAsFile(this.props.globalState.hostDocument, this.props.globalState.nodeMaterial!.generateCode(), "code.txt");
+                        }} />
+                        <ButtonLineComponent label="Export shaders" onClick={() => {
+                            StringTools.DownloadAsFile(this.props.globalState.hostDocument, this.props.globalState.nodeMaterial!.compiledShaders, "shaders.txt");
+                        }} />
+                        {
+                            this.props.globalState.customSave &&
+                            <ButtonLineComponent label={this.props.globalState.customSave!.label} onClick={() => {
+                                this.customSave();
+                            }} />
+                        }
+                        <FileButtonLineComponent label="Load Frame" uploadName={'frame-upload'} onClick={(file) => this.loadFrame(file)} accept=".json" />
+                    </LineContainerComponent>
+                    {
+                        !this.props.globalState.customSave &&
+                        <LineContainerComponent title="SNIPPET">
+                            {
+                                this.props.globalState.nodeMaterial!.snippetId &&
+                                <TextLineComponent label="Snippet ID" value={this.props.globalState.nodeMaterial!.snippetId} />
+                            }
+                            <ButtonLineComponent label="Load from snippet server" onClick={() => this.loadFromSnippet()} />
+                            <ButtonLineComponent label="Save to snippet server" onClick={() => {
+                                this.saveToSnippetServer();
+                            }} />
+                        </LineContainerComponent>
+                    }
+                    <LineContainerComponent title="INPUTS">
+                    {
+                        this.props.globalState.nodeMaterial.getInputBlocks().map((ib) => {
+                            if (!ib.isUniform || ib.isSystemValue || !ib.name) {
+                                return null;
+                            }
+                            return this.renderInputBlock(ib);
+                        })
+                    }
+                    </LineContainerComponent>
+                </div>
+            </div>
+        );
+    }
+}

+ 28 - 0
guiEditor/src/diagram/display/clampDisplayManager.ts

@@ -0,0 +1,28 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { ClampBlock } from 'babylonjs/Materials/Node/Blocks/clampBlock';
+
+export class ClampDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return false;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "#4086BB";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        const clampBlock = block as ClampBlock;
+
+        contentArea.classList.add("clamp-block");
+        contentArea.innerHTML = `[${clampBlock.minimum}, ${clampBlock.maximum}]`;
+    }
+}

+ 24 - 0
guiEditor/src/diagram/display/discardDisplayManager.ts

@@ -0,0 +1,24 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+
+export class DiscardDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return true;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "#540b0b";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        contentArea.classList.add("discard-block");
+    }
+}

+ 9 - 0
guiEditor/src/diagram/display/displayManager.ts

@@ -0,0 +1,9 @@
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+
+export interface IDisplayManager {
+    getHeaderClass(block: NodeMaterialBlock): string;
+    shouldDisplayPortLabels(block: NodeMaterialBlock): boolean;
+    updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void;
+    getBackgroundColor(block: NodeMaterialBlock): string;
+    getHeaderText(block: NodeMaterialBlock): string;
+}

+ 29 - 0
guiEditor/src/diagram/display/gradientDisplayManager.ts

@@ -0,0 +1,29 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { GradientBlock } from 'babylonjs/Materials/Node/Blocks/gradientBlock';
+
+export class GradientDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return false;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        let gradientBlock = block as GradientBlock;
+
+        let gradients = gradientBlock.colorSteps.map(c => `rgb(${c.color.r * 255}, ${c.color.g * 255}, ${c.color.b * 255}) ${c.step * 100}%`);
+
+        return gradients.length ? `linear-gradient(90deg, ${gradients.join(", ")})` : 'black';
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        contentArea.classList.add("gradient-block");
+    }
+}

+ 143 - 0
guiEditor/src/diagram/display/inputDisplayManager.ts

@@ -0,0 +1,143 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { NodeMaterialSystemValues } from 'babylonjs/Materials/Node/Enums/nodeMaterialSystemValues';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { AnimatedInputBlockTypes } from 'babylonjs/Materials/Node/Blocks/Input/animatedInputBlockTypes';
+import { Vector2, Vector3, Vector4 } from 'babylonjs/Maths/math.vector';
+import { Color3 } from 'babylonjs/Maths/math.color';
+import { BlockTools } from '../../blockTools';
+import { StringTools } from '../../stringTools';
+
+const inputNameToAttributeValue: { [name: string] : string } = {
+    "position2d" : "position",
+    "particle_uv" : "uv",
+    "particle_color" : "color",
+    "particle_texturemask": "textureMask",
+    "particle_positionw" : "positionW",
+};
+
+const inputNameToAttributeName: { [name: string] : string } = {
+    "position2d" : "screen",
+    "particle_uv" : "particle",
+    "particle_color" : "particle",
+    "particle_texturemask": "particle",
+    "particle_positionw": "particle",
+};
+
+export class InputDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        let inputBlock = block as InputBlock;
+
+        if (inputBlock.isConstant) {
+            return "constant";
+        }
+
+        if (inputBlock.visibleInInspector) {
+            return "inspector";
+        }
+
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return false;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        let inputBlock = block as InputBlock;
+        let name = `${inputBlock.name} (${StringTools.GetBaseType(inputBlock.output.type)})`;
+
+        if (inputBlock.isAttribute) {
+            name = StringTools.GetBaseType(inputBlock.output.type);
+        }
+
+        return name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        let color = "";
+        let inputBlock = block as InputBlock;
+
+        switch (inputBlock.type) {
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+            case NodeMaterialBlockConnectionPointTypes.Color4: {
+                if (inputBlock.value) {
+                    color = (inputBlock.value as Color3).toHexString();
+                    break;
+                }
+            }
+            default:
+                color = BlockTools.GetColorFromConnectionNodeType(inputBlock.type);
+                break;
+        }
+
+        return color;
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {
+        let value = "";
+        let inputBlock = block as InputBlock;
+
+        if (inputBlock.isAttribute) {
+            const attrVal = inputNameToAttributeValue[inputBlock.name] ?? inputBlock.name;
+            const attrName = inputNameToAttributeName[inputBlock.name] ?? 'mesh';
+            value = attrName + "." + attrVal;
+        } else if (inputBlock.isSystemValue) {
+            switch (inputBlock.systemValue) {
+                case NodeMaterialSystemValues.World:
+                    value = "World";
+                    break;
+                case NodeMaterialSystemValues.WorldView:
+                    value = "World x View";
+                    break;
+                case NodeMaterialSystemValues.WorldViewProjection:
+                    value = "World x View x Projection";
+                    break;
+                case NodeMaterialSystemValues.View:
+                    value = "View";
+                    break;
+                case NodeMaterialSystemValues.ViewProjection:
+                    value = "View x Projection";
+                    break;
+                case NodeMaterialSystemValues.Projection:
+                    value = "Projection";
+                    break;
+                case NodeMaterialSystemValues.CameraPosition:
+                    value = "Camera position";
+                    break;
+                case NodeMaterialSystemValues.FogColor:
+                    value = "Fog color";
+                    break;
+                case NodeMaterialSystemValues.DeltaTime:
+                    value = "Delta time";
+                    break;
+            }
+        } else {
+            switch (inputBlock.type) {
+                case NodeMaterialBlockConnectionPointTypes.Float:
+                    if (inputBlock.animationType !== AnimatedInputBlockTypes.None) {
+                        value = AnimatedInputBlockTypes[inputBlock.animationType];
+                    } else {
+                        value = inputBlock.value.toFixed(2);
+                    }
+                    break;
+                case NodeMaterialBlockConnectionPointTypes.Vector2:
+                    let vec2Value = inputBlock.value as Vector2;
+                    value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`;
+                    break;
+                case NodeMaterialBlockConnectionPointTypes.Vector3:
+                    let vec3Value = inputBlock.value as Vector3;
+                    value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`;
+                    break;
+                case NodeMaterialBlockConnectionPointTypes.Vector4:
+                    let vec4Value = inputBlock.value as Vector4;
+                    value = `(${vec4Value.x.toFixed(2)}, ${vec4Value.y.toFixed(2)}, ${vec4Value.z.toFixed(2)}, ${vec4Value.w.toFixed(2)})`;
+                    break;
+            }
+        }
+
+        contentArea.innerHTML = value;
+        contentArea.classList.add("input-block");
+    }
+}

+ 24 - 0
guiEditor/src/diagram/display/outputDisplayManager.ts

@@ -0,0 +1,24 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+
+export class OutputDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return true;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "rgb(106, 44, 131)";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        contentArea.classList.add("output-block");
+    }
+}

+ 49 - 0
guiEditor/src/diagram/display/remapDisplayManager.ts

@@ -0,0 +1,49 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { RemapBlock } from 'babylonjs/Materials/Node/Blocks/remapBlock';
+import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
+
+export class RemapDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return true;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "#4086BB";
+    }
+
+    private _extractInputValue(connectionPoint: NodeMaterialConnectionPoint) {
+        let connectedBlock = connectionPoint.connectedPoint!.ownerBlock;
+
+        if (connectedBlock.isInput) {
+            let inputBlock = connectedBlock as InputBlock;
+
+            if (inputBlock.isUniform && !inputBlock.isSystemValue) {
+                return inputBlock.value;
+            }
+        }
+
+        return "?";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        const remapBlock = block as RemapBlock;
+
+        let sourceRangeX = remapBlock.sourceMin.isConnected ? this._extractInputValue(remapBlock.sourceMin) : remapBlock.sourceRange.x;
+        let sourceRangeY = remapBlock.sourceMax.isConnected ? this._extractInputValue(remapBlock.sourceMax) : remapBlock.sourceRange.y;
+        let targetRangeX = remapBlock.targetMin.isConnected ? this._extractInputValue(remapBlock.targetMin) : remapBlock.targetRange.x;
+        let targetRangeY = remapBlock.targetMax.isConnected ? this._extractInputValue(remapBlock.targetMax) : remapBlock.targetRange.y;        
+
+        contentArea.classList.add("remap-block");
+        contentArea.innerHTML = `[${sourceRangeX}, ${sourceRangeY}] -> [${targetRangeX}, ${targetRangeY}]`;
+    }
+}

+ 60 - 0
guiEditor/src/diagram/display/textureDisplayManager.ts

@@ -0,0 +1,60 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/textureBlock';
+import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionBlock';
+import { ReflectionTextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/reflectionTextureBlock';
+import { TextureLineComponent } from '../../sharedComponents/textureLineComponent';
+import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
+
+export class TextureDisplayManager implements IDisplayManager {
+    private _previewCanvas: HTMLCanvasElement;
+    private _previewImage: HTMLImageElement;
+
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return true;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "#323232";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {
+        const textureBlock = block as TextureBlock | ReflectionTextureBlock | RefractionBlock | CurrentScreenBlock;
+
+        if (!this._previewCanvas) {
+            contentArea.classList.add("texture-block");
+            if (block instanceof TextureBlock || block instanceof CurrentScreenBlock || block instanceof ParticleTextureBlock) {
+                contentArea.classList.add("regular-texture-block");
+            }
+
+            this._previewCanvas = contentArea.ownerDocument!.createElement("canvas");
+            this._previewImage = contentArea.ownerDocument!.createElement("img");
+            contentArea.appendChild(this._previewImage);
+            this._previewImage.classList.add("empty");
+        }
+
+        if (textureBlock.texture) {
+            TextureLineComponent.UpdatePreview(this._previewCanvas, textureBlock.texture, 140, {
+                face: 0,
+                displayRed: true,
+                displayAlpha: true,
+                displayBlue: true,
+                displayGreen: true
+            }, () => {
+                this._previewImage.src = this._previewCanvas.toDataURL("image/png");
+                this._previewImage.classList.remove("empty");
+            });
+        } else {
+            this._previewImage.classList.add("empty");
+        }
+    }
+}

+ 28 - 0
guiEditor/src/diagram/display/trigonometryDisplayManager.ts

@@ -0,0 +1,28 @@
+import { IDisplayManager } from './displayManager';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { TrigonometryBlock, TrigonometryBlockOperations } from 'babylonjs/Materials/Node/Blocks/trigonometryBlock';
+
+export class TrigonometryDisplayManager implements IDisplayManager {
+    public getHeaderClass(block: NodeMaterialBlock) {
+        return "";
+    }
+
+    public shouldDisplayPortLabels(block: NodeMaterialBlock): boolean {
+        return false;
+    }
+
+    public getHeaderText(block: NodeMaterialBlock): string {
+        return block.name;
+    }
+
+    public getBackgroundColor(block: NodeMaterialBlock): string {
+        return "#405C86";
+    }
+
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
+        const trigonometryBlock = block as TrigonometryBlock;
+
+        contentArea.classList.add("trigonometry-block");
+        contentArea.innerHTML = TrigonometryBlockOperations[trigonometryBlock.operation];
+    }
+}

+ 27 - 0
guiEditor/src/diagram/displayLedger.ts

@@ -0,0 +1,27 @@
+import { InputDisplayManager } from './display/inputDisplayManager';
+import { OutputDisplayManager } from './display/outputDisplayManager';
+import { ClampDisplayManager } from './display/clampDisplayManager';
+import { GradientDisplayManager } from './display/gradientDisplayManager';
+import { RemapDisplayManager } from './display/remapDisplayManager';
+import { TrigonometryDisplayManager } from './display/trigonometryDisplayManager';
+import { TextureDisplayManager } from './display/textureDisplayManager';
+import { DiscardDisplayManager } from './display/discardDisplayManager';
+
+export class DisplayLedger {
+    public static RegisteredControls: {[key: string] : any} = {};
+}
+
+DisplayLedger.RegisteredControls["InputBlock"] = InputDisplayManager;
+DisplayLedger.RegisteredControls["VertexOutputBlock"] = OutputDisplayManager;
+DisplayLedger.RegisteredControls["FragmentOutputBlock"] = OutputDisplayManager;
+DisplayLedger.RegisteredControls["ClampBlock"] = ClampDisplayManager;
+DisplayLedger.RegisteredControls["GradientBlock"] = GradientDisplayManager;
+DisplayLedger.RegisteredControls["RemapBlock"] = RemapDisplayManager;
+DisplayLedger.RegisteredControls["TrigonometryBlock"] = TrigonometryDisplayManager;
+DisplayLedger.RegisteredControls["TextureBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["ReflectionTextureBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["ReflectionBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["RefractionBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["CurrentScreenBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["ParticleTextureBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["DiscardBlock"] = DiscardDisplayManager;

+ 87 - 0
guiEditor/src/diagram/frameNodePort.ts

@@ -0,0 +1,87 @@
+import { NodePort } from "./nodePort";
+import { GraphNode } from './graphNode';
+import { FramePortPosition } from './graphFrame';
+import { GlobalState } from '../globalState';
+import { IDisplayManager } from './display/displayManager';
+import { Observable } from 'babylonjs/Misc/observable';
+import { Nullable } from 'babylonjs/types';
+import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
+import { FramePortData, isFramePortData } from './graphCanvas';
+
+export class FrameNodePort extends NodePort {
+    private _parentFrameId: number;
+    private _isInput: boolean;
+    private _framePortPosition: FramePortPosition
+    private _framePortId: number;
+    private _onFramePortPositionChangedObservable = new Observable<FrameNodePort>();
+
+    public get parentFrameId () {
+        return this._parentFrameId;
+    }
+
+    public get onFramePortPositionChangedObservable() {
+        return this._onFramePortPositionChangedObservable;
+    }
+
+    public get isInput() {
+        return this._isInput;
+    }
+
+    public get framePortId() {
+        return this._framePortId;
+    }
+
+    public get framePortPosition() {
+        return this._framePortPosition;
+    }
+
+    public set framePortPosition(position: FramePortPosition) {
+        this._framePortPosition = position;
+        this.onFramePortPositionChangedObservable.notifyObservers(this);
+    }
+
+    public constructor(portContainer: HTMLElement, public connectionPoint: NodeMaterialConnectionPoint, public node: GraphNode, globalState: GlobalState, isInput: boolean, framePortId: number, parentFrameId: number) {
+        super(portContainer, connectionPoint,node, globalState);
+
+        this._parentFrameId = parentFrameId;
+        this._isInput = isInput;
+        this._framePortId = framePortId;
+
+        this._onSelectionChangedObserver = this._globalState.onSelectionChangedObservable.add((selection) => {
+            if (isFramePortData(selection) && (selection as FramePortData).port === 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, parentFrameId: 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");
+            let portName = connectionPoint.displayName || connectionPoint.name;
+            if (connectionPoint.ownerBlock.isInput) {
+                portName = node.name;
+            }
+            portLabel.innerHTML = portName;       
+            portContainer.appendChild(portLabel);
+        }
+
+        return new FrameNodePort(portContainer, connectionPoint, node, globalState, isInput, framePortId, parentFrameId);
+    }
+
+} 
+

+ 626 - 0
guiEditor/src/diagram/graphCanvas.scss

@@ -0,0 +1,626 @@
+#graph-canvas {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;            
+    font: 14px "acumin-pro";  
+    user-select: none;
+    overflow: hidden;
+    cursor: move;   
+    background-image:
+        linear-gradient(to right, #4F4E4F 1px, transparent 1px),
+        linear-gradient(to bottom, #4F4E4F 1px, transparent 1px);  
+
+    #selection-container {
+        pointer-events: none;
+        
+        .selection-box {
+            z-index: 10;
+            position: absolute;
+            background: rgba(72, 72, 196, 0.5);
+            border: blue solid 2px;
+        }
+    }
+
+    .port {
+        border-radius: 20px;
+        width: 20px;
+        height: 20px;                                                    
+        align-self: center;   
+        
+        .img {
+            width: 100%;
+        }
+
+        img.selected {
+            box-shadow: 0 0 0 2px;
+            border-radius: 50%;
+        }
+        
+        &: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%;
+        left: 0;
+        top: 0;
+        transform-origin: left top;
+        display: grid;
+        grid-template-rows: 100%;          
+        grid-template-columns: 100%;          
+
+        #frame-container {
+            overflow: visible;   
+            grid-row: 1;
+            grid-column: 1;
+            position: relative;
+            width: 100%;
+            height: 100%;             
+        }
+
+        .frame-box {
+            position: absolute;
+            background: rgba(72, 72, 72, 0.7);
+            display: grid;
+            grid-template-rows: 40px calc(100% - 40px);
+            grid-template-columns: 100%;            
+            box-sizing: border-box;
+
+            &.collapsed {
+                height: auto !important;
+                width: 200px !important;
+                z-index: 3;
+
+                .frame-box-header {
+                    font-size: 16px;
+                    grid-template-columns: calc(100% - 37px) 30px 7px;  
+                    
+                    .frame-box-header-collapse {
+                        margin-top: -2px;
+                    }
+                    
+                    .frame-box-header-close {
+                        display: none;
+                    }
+                }
+
+                .frame-comments.has-comments{
+                    .frame-comment-span{
+                        white-space: nowrap;
+                        text-overflow: ellipsis;
+                        overflow: hidden;
+                    }
+                }
+            }
+
+            .frame-box-border {                
+                grid-row: 1 / span 2;
+                grid-column: 1;
+                width: 100%;
+                height: 100%;
+                border: transparent solid 4px;
+                pointer-events: none;
+                box-sizing: border-box;
+            }
+
+            .frame-box-header {
+                grid-row: 1;
+                grid-column: 1;
+                background: rgba(72, 72, 72, 1);    
+                color: white;
+                font-size: 24px;
+                text-align: center;
+                display: grid;
+                grid-template-rows: 100%;  
+                grid-template-columns: calc(100% - 74px) 30px 7px 30px 7px;  
+                align-content: center;
+                overflow: hidden;
+
+                .frame-box-header-button {
+                    cursor: pointer;
+                    align-self: center;
+                    transform-origin: 50% 50%;
+                    transform: scale(1);
+                    stroke: transparent;
+                    fill: white;
+                    display: grid;               
+
+                    &.down {
+                        transform: scale(0.90);
+                    }
+                }
+
+                .frame-box-header-collapse {
+                    grid-column: 2;
+                    grid-row: 1;
+                }
+
+                .frame-box-header-close {
+                    grid-column: 4;
+                    grid-row: 1;
+                }
+
+                .frame-box-header-title {
+                    grid-column: 1;
+                    grid-row: 1;
+                    display: grid;
+                    height: 100%;
+                    width: 100%;
+                    align-self: stretch;
+                    align-items: center;
+                    margin-top: -2px;
+                }
+            }
+
+            .port-container {
+                margin-top: 6px;      
+                margin-bottom: 6px; 
+                margin-left: 4px;     
+                margin-right: 4px;     
+                color: white;
+                grid-row: 2;
+                grid-column: 1;
+                display: grid;
+                grid-template-rows: 100%;
+                grid-template-columns: 50% 50%; 
+                z-index: 2;
+            }
+
+            .frame-comments.has-comments{
+                display: grid;
+                grid-row: 2;
+                grid-column: 1;
+                padding: 0 10px;
+                font-style: italic;
+                word-wrap: break-word;
+            }
+
+            &.selected {
+                .frame-box-border {
+                  border-color: white;
+                }
+            }
+
+            .right-handle {
+                grid-area: 1 / 2 / 3 / 2;
+                width: 4px;
+                background-color: transparent;
+                cursor: ew-resize;
+
+                &::after{
+                    content: "";
+                    width: 8px;
+                    position: absolute;
+                    top: 0;
+                    bottom: 0;
+                    margin-left: -4px;
+                    cursor: ew-resize;
+                    
+                }
+
+                &.collapsed {
+                    cursor: pointer;
+                }
+            }
+
+            .top-right-corner-handle{
+                background-color: transparent;
+                height: 4px;
+                z-index: 21;
+                cursor: ne-resize;
+                width: 4px;
+                margin-left: -6px;
+
+                &::after {
+                    background-color: transparent;
+                    cursor: ne-resize;
+                    margin-left: unset;
+                    top: -4px;
+                    height: 10px;
+                    width: 10px;
+                }
+            }
+
+
+            .bottom-right-corner-handle{
+                background-color: transparent;
+                height: 0px;
+                z-index: 21;
+                cursor: nw-resize;
+                grid-area: 4 / 2 / 4 / 2;;
+                margin-left: -2px;
+
+
+                &::after {
+                    background-color: transparent;
+                    height: 10px;
+                    cursor: nw-resize;
+                    top: unset;
+                    bottom: -4px;
+                    width: 10px;               
+                }
+            }
+
+            .left-handle {
+                grid-area: 1 / 1 / 3 / 1;
+                width: 4px;
+                background-color: transparent;
+                cursor: ew-resize;
+
+                &::before{
+                    content: "";
+                    width: 8px;
+                    position: absolute;
+                    top: 0;
+                    bottom: 0;
+                    margin-left: -4px;
+
+                }
+            }
+
+            .top-left-corner-handle{
+                background-color: transparent;
+                height: 4px;
+                z-index: 21;
+                cursor: nw-resize;
+                width: 4px;
+                margin-left: -4px;
+
+                &::before {
+                    background-color: transparent;
+                    cursor: nw-resize;
+                    margin-left: unset;
+                    top: -4px;
+                    height: 10px;
+                    width: 10px;
+                }
+            }
+
+            .bottom-left-corner-handle{
+                background-color: transparent;
+                height: 0px;
+                z-index: 21;
+                cursor: sw-resize;
+                grid-area: 4 / 1 / 4 / 1;
+
+
+                &::before {
+                    background-color: transparent;
+                    height: 10px;
+                    cursor: sw-resize;
+                    top: unset;
+                    bottom: -4px;
+                    width: 10px;               
+                }
+            }
+
+            .top-handle {
+                grid-area: 1 / 1 / 1 / 1;
+                background-color: transparent;
+                height: 4px;
+                cursor: ns-resize;
+
+                &::before{
+                    content: "";
+                    width: 100%;
+                    position: absolute;
+                    top: -4px;
+                    bottom: 100%;
+                    right: 0;
+                    left: 0;
+                    margin-bottom: -8px;
+                    cursor: ns-resize;
+                    height: 8px;
+                }
+            }
+
+            .bottom-handle {
+                grid-area: 3 / 1 / 3 / 1;
+                background-color: transparent;
+                height: 4px;
+                cursor: ns-resize;
+
+                &::after {
+                    content: "";
+                    width: 100%;
+                    position: absolute;
+                    top: 100%;
+                    bottom: 0;
+                    right: 0;
+                    left: 0;
+                    margin-top: -8px;
+                    cursor: ns-resize;
+                    height: 12px;
+                }
+            }
+            
+            &.collapsed{
+                .top-handle, .top-right-corner-handle, .right-handle, .bottom-right-corner-handle, .bottom-handle, .bottom-left-corner-handle, .left-handle, .top-left-corner-handle {
+                    cursor: default;
+                }
+
+                .right-handle, .bottom-handle, .top-right-corner-handle, .bottom-right-corner-handle{
+                    &::after{
+                        cursor: default;
+                    }
+                }
+
+                .left-handle, .top-handle, .top-left-corner-handle, .bottom-left-corner-handle{
+                    &::before{
+                        cursor: default;
+                    }
+                }
+            }
+        }
+
+        #graph-svg-container {
+            grid-row: 1;
+            grid-column: 1;
+            position: relative;
+            width: 100%;
+            height: 100%;  
+            overflow: visible; 
+            pointer-events: none;
+            z-index: 2;
+            
+            .link {
+                stroke-width: 4px;    
+                &.selected {                    
+                    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;
+                }
+            }
+        }
+
+        #graph-canvas-container {
+            grid-row: 1;
+            grid-column: 1;
+            position: relative;
+            width: 100%;
+            height: 100%;                  
+
+            .visual {
+                z-index: 4;
+                width: 200px;
+                position: absolute;
+                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;
+
+                &.hidden {
+                    display: none;
+                }
+
+                .comments {
+                    position: absolute;
+                    top: -50px;
+                    width: 200px;
+                    height: 45px;
+                    overflow: hidden;                    
+                    font-style: italic;
+                    opacity: 0.8;
+                    display: grid;
+                    align-items: flex-end;
+                    pointer-events: none;
+                }
+
+                .selection-border {                    
+                    grid-row: 1 / span 3;
+                    grid-column: 1;
+                    margin: -4px;
+
+                    transition: border-color 100ms;
+
+                    border: 4px solid black;
+                    border-radius: 12px;
+                }
+
+                &.selected {
+                    .selection-border {  
+                        border-color: white;
+                    }
+                }
+
+                .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;
+                    margin-left: -1px;
+                    margin-right: -1px;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                    overflow: hidden;
+                    background: black;
+                    color: white;
+
+                    &.constant {
+                        border-color: #464348;
+                        background: #464348;
+                    }
+            
+                    &.inspector {
+                        border-color: #66491b;
+                        background: #66491b;
+                    }
+                }
+
+                .connections {
+                    grid-row: 2;
+                    grid-column: 1;
+
+                    display: grid;
+                    grid-template-columns: 50% 50%;                  
+                }
+
+                .content {
+                    min-height: 20px;
+                    grid-row: 3;
+                    grid-column: 1;
+
+                    &.input-block {
+                        grid-row: 2;
+                        min-height: 34px;
+                        text-align: center;
+                        font-size: 18px;
+                        font-weight: bold;
+                        margin: 0 10px 5px;
+                        display: grid;
+                        align-content: center;
+
+                        &.small-font {                            
+                            font-size: 17px;
+                        }
+                    }
+
+                    &.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;
+                    }
+
+                    &.gradient-block {                    
+                        grid-row: 2;
+                        height: 34px;
+                    }
+
+                    &.texture-block {                    
+                        grid-row: 3;
+                        height: 140px;
+                        width: 140px;
+                        overflow: hidden;
+                        border-bottom-left-radius: 7px;
+                        border: black 4px solid;
+                        border-left: 0px;
+                        border-bottom: 0px;
+
+                        img {
+                            width: 100%;
+                            height: 100%;
+                            pointer-events: none;
+
+                            &.empty {
+                                display: none;
+                            }
+                        }
+                    }
+
+                    &.regular-texture-block {  
+                        margin-top: -115px;                        
+                    }
+
+                    &.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;
+                    }
+                }
+            }
+        }
+    }
+}

+ 945 - 0
guiEditor/src/diagram/graphCanvas.tsx

@@ -0,0 +1,945 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { GraphNode } from './graphNode';
+import * as dagre from 'dagre';
+import { Nullable } from 'babylonjs/types';
+import { NodeLink } from './nodeLink';
+import { NodePort } from './nodePort';
+import { NodeMaterialConnectionPoint, NodeMaterialConnectionPointDirection, NodeMaterialConnectionPointCompatibilityStates } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
+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 'babylonjs/Misc/dataStorage';
+import { GraphFrame } from './graphFrame';
+import { IEditorData, IFrameData } from '../nodeLocationInfo';
+import { FrameNodePort } from './frameNodePort';
+
+require("./graphCanvas.scss");
+
+export interface IGraphCanvasComponentProps {
+    globalState: GlobalState
+}
+
+export type FramePortData = {
+    frame: GraphFrame,
+    port: FrameNodePort
+}
+
+export const isFramePortData = (variableToCheck: any): variableToCheck is FramePortData => {
+    if (variableToCheck) {
+        return (variableToCheck as FramePortData).port !== undefined;
+    }
+    else return false;
+}
+
+export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentProps> {
+    private readonly MinZoom = 0.1;
+    private readonly MaxZoom = 4;
+
+    private _hostCanvas: HTMLDivElement;
+    private _graphCanvas: HTMLDivElement;
+    private _selectionContainer: HTMLDivElement;
+    private _frameContainer: HTMLDivElement;
+    private _svgCanvas: HTMLElement;
+    private _rootContainer: HTMLDivElement;
+    private _nodes: GraphNode[] = [];
+    private _links: NodeLink[] = [];
+    private _mouseStartPointX: Nullable<number> = null;
+    private _mouseStartPointY: Nullable<number> = null
+    private _dropPointX = 0;
+    private _dropPointY = 0;
+    private _selectionStartX = 0;
+    private _selectionStartY = 0;
+    private _candidateLinkedHasMoved = false;
+    private _x = 0;
+    private _y = 0;
+    private _zoom = 1;
+    private _selectedNodes: GraphNode[] = [];
+    private _selectedLink: Nullable<NodeLink> = null;
+    private _selectedPort: Nullable<NodePort> = null;
+    private _candidateLink: Nullable<NodeLink> = null;
+    private _candidatePort: Nullable<NodePort | FrameNodePort> = null;
+    private _gridSize = 20;
+    private _selectionBox: Nullable<HTMLDivElement> = null;   
+    private _selectedFrame: Nullable<GraphFrame> = null;   
+    private _frameCandidate: Nullable<HTMLDivElement> = null;
+    private _frames: GraphFrame[] = [];
+
+    private _altKeyIsPressed = false;
+    private _ctrlKeyIsPressed = false;
+    private _oldY = -1;
+
+    public _frameIsMoving = false;
+    public _isLoading = false;
+
+    public get gridSize() {
+        return this._gridSize;
+    }
+
+    public set gridSize(value: number) {
+        this._gridSize = value;
+        
+        this.updateTransform();
+    }
+
+    public get globalState(){
+        return this.props.globalState;
+    }
+
+    public get nodes() {
+        return this._nodes;
+    }
+
+    public get links() {
+        return this._links;
+    }
+
+    public get frames() {
+        return this._frames;
+    }
+
+    public get zoom() {
+        return this._zoom;
+    }
+
+    public set zoom(value: number) {
+        if (this._zoom === value) {
+            return;
+        }
+
+        this._zoom = value;
+        
+        this.updateTransform();
+    }    
+
+    public get x() {
+        return this._x;
+    }
+
+    public set x(value: number) {
+        this._x = value;
+        
+        this.updateTransform();
+    }
+
+    public get y() {
+        return this._y;
+    }
+
+    public set y(value: number) {
+        this._y = value;
+        
+        this.updateTransform();
+    }
+
+    public get selectedNodes() {
+        return this._selectedNodes;
+    }
+
+    public get selectedLink() {
+        return this._selectedLink;
+    }
+    public get selectedFrame() {
+        return this._selectedFrame;
+    }
+
+    public get selectedPort() {
+        return this._selectedPort;
+    }
+
+    public get canvasContainer() {
+        return this._graphCanvas;
+    }
+
+    public get hostCanvas() {
+        return this._hostCanvas;
+    }
+
+    public get svgCanvas() {
+        return this._svgCanvas;
+    }
+
+    public get selectionContainer() {
+        return this._selectionContainer;
+    }
+
+    public get frameContainer() {
+        return this._frameContainer;
+    }
+    
+
+    constructor(props: IGraphCanvasComponentProps) {
+        super(props);
+
+        props.globalState.onSelectionChangedObservable.add(selection => {            
+            if (!selection) {
+                this._selectedNodes = [];
+                this._selectedLink = null;
+                this._selectedFrame = null;
+                this._selectedPort = null;
+            } else {
+                if (selection instanceof NodeLink) {
+                    this._selectedNodes = [];
+                    this._selectedFrame = null;
+                    this._selectedLink = selection;
+                    this._selectedPort = null;
+                } else if (selection instanceof GraphFrame) {
+                    this._selectedNodes = [];
+                    this._selectedFrame = selection;
+                    this._selectedLink = null;
+                    this._selectedPort = null;
+                } else if (selection instanceof GraphNode){
+                    if (this._ctrlKeyIsPressed) {
+                        if (this._selectedNodes.indexOf(selection) === -1) {
+                            this._selectedNodes.push(selection);
+                        }
+                    } else {                    
+                        this._selectedNodes = [selection];
+                    }
+                } else if(selection instanceof NodePort){
+                    this._selectedNodes = [];
+                    this._selectedFrame = null;
+                    this._selectedLink = null;
+                    this._selectedPort = selection;
+                } else {
+                    this._selectedNodes = [];
+                    this._selectedFrame = null;
+                    this._selectedLink = null;
+                    this._selectedPort = selection.port;
+                }
+            }
+        });
+
+        props.globalState.onCandidatePortSelectedObservable.add(port => {
+            this._candidatePort = port;
+        });
+
+        props.globalState.onGridSizeChanged.add(() => {
+            this.gridSize = DataStorage.ReadNumber("GridSize", 20);
+        });
+
+        this.props.globalState.hostDocument!.addEventListener("keyup", () => this.onKeyUp(), false);
+        this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
+            this._altKeyIsPressed = evt.altKey;            
+            this._ctrlKeyIsPressed = evt.ctrlKey;
+        }, false);
+        this.props.globalState.hostDocument!.defaultView!.addEventListener("blur", () => {
+            this._altKeyIsPressed = false;
+            this._ctrlKeyIsPressed = false;
+        }, false);     
+
+        // Store additional data to serialization object
+        this.props.globalState.storeEditorData = (editorData, graphFrame) => {
+            editorData.frames = [];
+            if (graphFrame) {
+                editorData.frames.push(graphFrame!.serialize());
+            } else {
+                editorData.x = this.x;
+                editorData.y = this.y;
+                editorData.zoom = this.zoom;
+                for (var frame of this._frames) {
+                    editorData.frames.push(frame.serialize());
+                }
+            }
+        }
+    }
+
+    public getGridPosition(position: number, useCeil = false) {
+        let gridSize = this.gridSize;
+		if (gridSize === 0) {
+			return position;
+        }
+        if (useCeil) {
+            return gridSize * Math.ceil(position / gridSize);    
+        }
+		return gridSize * Math.floor(position / gridSize);
+    }
+    
+    public getGridPositionCeil(position: number) {
+        let gridSize = this.gridSize;
+		if (gridSize === 0) {
+			return position;
+		}
+		return gridSize * Math.ceil(position / gridSize);
+	}
+
+    updateTransform() {
+        this._rootContainer.style.transform = `translate(${this._x}px, ${this._y}px) scale(${this._zoom})`;
+
+        if (DataStorage.ReadBoolean("ShowGrid", true)) {
+            this._hostCanvas.style.backgroundSize = `${this._gridSize * this._zoom}px ${this._gridSize * this._zoom}px`;
+            this._hostCanvas.style.backgroundPosition = `${this._x}px ${this._y}px`;
+        } else {
+            this._hostCanvas.style.backgroundSize = `0`;
+        }
+    }
+
+    onKeyUp() {        
+        this._altKeyIsPressed = false;
+        this._ctrlKeyIsPressed = false;
+        this._oldY = -1;
+    }
+
+    findNodeFromBlock(block: NodeMaterialBlock) {
+        return this.nodes.filter(n => n.block === block)[0];
+    }
+
+    reset() {
+        for (var node of this._nodes) {
+            node.dispose();
+        }
+        
+        const frames = this._frames.splice(0);
+        for (var frame of frames) {
+            frame.dispose();
+        }
+        this._nodes = [];
+        this._frames = [];
+        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._graphCanvas, this);
+
+        this._nodes.push(newNode);
+
+        return newNode;
+    }
+
+    distributeGraph() {
+        this.x = 0;
+        this.y = 0;
+        this.zoom = 1;
+
+        let graph = new dagre.graphlib.Graph();
+        graph.setGraph({});
+        graph.setDefaultEdgeLabel(() => ({}));
+        graph.graph().rankdir = "LR";
+
+        // Build dagre graph
+        this._nodes.forEach(node => {
+
+            if (this._frames.some(f => f.nodes.indexOf(node) !== -1)) {
+                return;
+            }
+
+            graph.setNode(node.id.toString(), {
+                id: node.id,
+                type: "node",
+                width: node.width,
+                height: node.height
+            });
+        });
+
+        this._frames.forEach(frame => {
+            graph.setNode(frame.id.toString(), {
+                id: frame.id,
+                type: "frame",
+                width: frame.element.clientWidth,
+                height: frame.element.clientHeight
+            });
+        })
+
+        this._nodes.forEach(node => {
+            node.block.outputs.forEach(output => {
+                if (!output.hasEndpoints) {
+                    return;
+                }
+
+                output.endpoints.forEach(endpoint => {
+                    let sourceFrames = this._frames.filter(f => f.nodes.indexOf(node) !== -1);
+                    let targetFrames = this._frames.filter(f => f.nodes.some(n => n.block === endpoint.ownerBlock));
+
+
+                    let sourceId = sourceFrames.length > 0 ? sourceFrames[0].id : node.id;
+                    let targetId = targetFrames.length > 0 ? targetFrames[0].id : endpoint.ownerBlock.uniqueId;
+
+                    graph.setEdge(sourceId.toString(), targetId.toString());
+                });
+            });
+        });
+
+        // Distribute
+        dagre.layout(graph);
+
+        // Update graph
+        let dagreNodes = graph.nodes().map(node => graph.node(node));
+        dagreNodes.forEach((dagreNode: any) => {
+            if (!dagreNode) {
+                return;
+            }
+            if (dagreNode.type === "node") {
+                for (var node of this._nodes) {
+                    if (node.id === dagreNode.id) {
+                        node.x = dagreNode.x - dagreNode.width / 2;
+                        node.y = dagreNode.y - dagreNode.height / 2;
+                        node.cleanAccumulation();
+                        return;
+                    }
+                }
+                return;
+            }
+
+            for (var frame of this._frames) {
+                if (frame.id === dagreNode.id) {                    
+                    this._frameIsMoving = true;
+                    frame.move(dagreNode.x - dagreNode.width / 2, dagreNode.y - dagreNode.height / 2, false);
+                    frame.cleanAccumulation();
+                    this._frameIsMoving = false;
+                    return;
+                }
+            }
+        });        
+    }
+
+    componentDidMount() {
+        this._hostCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas") 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;        
+        this._selectionContainer = this.props.globalState.hostDocument.getElementById("selection-container") as HTMLDivElement;   
+        this._frameContainer = this.props.globalState.hostDocument.getElementById("frame-container") as HTMLDivElement;        
+        
+        this.gridSize = DataStorage.ReadNumber("GridSize", 20);
+        this.updateTransform();
+    }    
+
+    onMove(evt: React.PointerEvent) {        
+        // Selection box
+        if (this._selectionBox) {
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+
+            const localX = evt.pageX - rootRect.left;
+            const localY = evt.pageY - rootRect.top;
+
+            if (localX > this._selectionStartX) {
+                this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
+                this._selectionBox.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
+            } else {
+                this._selectionBox.style.left = `${localX / this.zoom}px`;
+                this._selectionBox.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
+            }
+
+            if (localY > this._selectionStartY) {                
+                this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
+                this._selectionBox.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
+            } else {
+                this._selectionBox.style.top = `${localY / this.zoom}px`;
+                this._selectionBox.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
+            }
+            
+            this.props.globalState.onSelectionBoxMoved.notifyObservers(this._selectionBox.getBoundingClientRect());
+
+            return;
+        }
+
+        // Candidate frame box
+        if (this._frameCandidate) {
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+
+            const localX = evt.pageX - rootRect.left;
+            const localY = evt.pageY - rootRect.top;
+
+            if (localX > this._selectionStartX) {
+                this._frameCandidate.style.left = `${this._selectionStartX / this.zoom}px`;
+                this._frameCandidate.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
+            } else {
+                this._frameCandidate.style.left = `${localX / this.zoom}px`;
+                this._frameCandidate.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
+            }
+
+            if (localY > this._selectionStartY) {                
+                this._frameCandidate.style.top = `${this._selectionStartY / this.zoom}px`;
+                this._frameCandidate.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
+            } else {
+                this._frameCandidate.style.top = `${localY / this.zoom}px`;
+                this._frameCandidate.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
+            }
+
+            return;
+        }        
+
+        // Candidate link
+        if (this._candidateLink) {        
+            const rootRect = this.canvasContainer.getBoundingClientRect();       
+            this._candidatePort = null; 
+            this.props.globalState.onCandidateLinkMoved.notifyObservers(new Vector2(evt.pageX, evt.pageY));
+            this._dropPointX = (evt.pageX - rootRect.left) / this.zoom;
+            this._dropPointY = (evt.pageY - rootRect.top) / this.zoom;
+
+            this._candidateLink.update(this._dropPointX, this._dropPointY, true);
+            this._candidateLinkedHasMoved = true;
+            
+            return;
+        }          
+
+        // Zoom with mouse + alt
+        if (this._altKeyIsPressed && evt.buttons === 1) {
+            if (this._oldY < 0) {
+                this._oldY = evt.pageY;
+            }
+
+            let zoomDelta = (evt.pageY - this._oldY) / 10;
+            if (Math.abs(zoomDelta) > 5) {
+                const oldZoom = this.zoom;
+                this.zoom = Math.max(Math.min(this.MaxZoom, this.zoom + zoomDelta / 100), this.MinZoom);
+
+                const boundingRect = evt.currentTarget.getBoundingClientRect();
+                const clientWidth = boundingRect.width;
+                const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
+                const clientX = evt.clientX - boundingRect.left;
+        
+                const xFactor = (clientX - this.x) / oldZoom / clientWidth;
+        
+                this.x = this.x - widthDiff * xFactor;
+
+                this._oldY = evt.pageY;      
+            }
+            return;
+        }   
+
+        // Move canvas
+        this._rootContainer.style.cursor = "move";
+
+        if (this._mouseStartPointX === null || this._mouseStartPointY === null) {
+            return;
+        }
+        this.x += evt.clientX - this._mouseStartPointX;
+        this.y += evt.clientY - this._mouseStartPointY;
+
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;
+    }
+
+    onDown(evt: React.PointerEvent<HTMLElement>) {
+        this._rootContainer.setPointerCapture(evt.pointerId);
+
+        // Selection?
+        if (evt.currentTarget === this._hostCanvas && evt.ctrlKey) {
+            this._selectionBox = this.props.globalState.hostDocument.createElement("div");
+            this._selectionBox.classList.add("selection-box");
+            this._selectionContainer.appendChild(this._selectionBox);
+
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+            this._selectionStartX = (evt.pageX - rootRect.left);
+            this._selectionStartY = (evt.pageY - rootRect.top);
+            this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
+            this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
+            this._selectionBox.style.width = "0px";
+            this._selectionBox.style.height = "0px";
+            return;
+        }
+
+        // Frame?
+        if (evt.currentTarget === this._hostCanvas && evt.shiftKey) {
+            this._frameCandidate = this.props.globalState.hostDocument.createElement("div");
+            this._frameCandidate.classList.add("frame-box");
+            this._frameContainer.appendChild(this._frameCandidate);
+
+            const rootRect = this.canvasContainer.getBoundingClientRect();      
+            this._selectionStartX = (evt.pageX - rootRect.left);
+            this._selectionStartY = (evt.pageY - rootRect.top);
+            this._frameCandidate.style.left = `${this._selectionStartX / this.zoom}px`;
+            this._frameCandidate.style.top = `${this._selectionStartY / this.zoom}px`;
+            this._frameCandidate.style.width = "0px";
+            this._frameCandidate.style.height = "0px";
+            return;
+        }
+
+        // Port dragging
+        if (evt.nativeEvent.srcElement && (evt.nativeEvent.srcElement as HTMLElement).nodeName === "IMG") {
+            if (!this._candidateLink) {
+                let portElement = ((evt.nativeEvent.srcElement as HTMLElement).parentElement as any).port as NodePort;
+                this._candidateLink = new NodeLink(this, portElement, portElement.node);
+                this._candidateLinkedHasMoved = false;
+            }  
+            return;
+        }
+
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;        
+    }
+
+    onUp(evt: React.PointerEvent) {
+        this._mouseStartPointX = null;
+        this._mouseStartPointY = null;
+        this._rootContainer.releasePointerCapture(evt.pointerId);   
+        this._oldY = -1; 
+
+        if (this._candidateLink) {       
+            if (this._candidateLinkedHasMoved) {
+                this.processCandidatePort();          
+                this.props.globalState.onCandidateLinkMoved.notifyObservers(null);
+            } else { // is a click event on NodePort
+                if(this._candidateLink.portA instanceof FrameNodePort) { //only on Frame Node Ports
+                    const port = this._candidateLink.portA;
+                    const frame = this.frames.find((frame: GraphFrame) => frame.id === port.parentFrameId);
+                    if (frame) {
+                        const data: FramePortData = {
+                            frame,
+                            port
+                        }
+                        this.props.globalState.onSelectionChangedObservable.notifyObservers(data);
+                    }
+                } else if(this._candidateLink.portA instanceof NodePort){
+                    this.props.globalState.onSelectionChangedObservable.notifyObservers(this._candidateLink.portA );
+                }
+            }
+            this._candidateLink.dispose();
+            this._candidateLink = null;
+            this._candidatePort = null;
+        }
+
+        if (this._selectionBox) {
+           this._selectionBox.parentElement!.removeChild(this._selectionBox);
+           this._selectionBox = null;
+        }
+
+        if (this._frameCandidate) {            
+            let newFrame = new GraphFrame(this._frameCandidate, this);
+            this._frames.push(newFrame);
+
+            this._frameCandidate.parentElement!.removeChild(this._frameCandidate);
+            this._frameCandidate = null;
+
+            this.props.globalState.onSelectionChangedObservable.notifyObservers(newFrame);
+         }
+    }
+
+    onWheel(evt: React.WheelEvent) {
+        let delta = evt.deltaY < 0 ? 0.1 : -0.1;
+
+        let oldZoom = this.zoom;
+        this.zoom = Math.min(Math.max(this.MinZoom, this.zoom + delta * this.zoom), this.MaxZoom);
+
+        const boundingRect = evt.currentTarget.getBoundingClientRect();
+        const clientWidth = boundingRect.width;
+        const clientHeight = boundingRect.height;
+        const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
+        const heightDiff = clientHeight * this.zoom - clientHeight * oldZoom;
+        const clientX = evt.clientX - boundingRect.left;
+        const clientY = evt.clientY - boundingRect.top;
+
+        const xFactor = (clientX - this.x) / oldZoom / clientWidth;
+        const yFactor = (clientY - this.y) / oldZoom / clientHeight;
+
+        this.x = this.x - widthDiff * xFactor;
+        this.y = this.y - heightDiff * yFactor;
+
+        evt.stopPropagation();
+    }
+
+    zoomToFit() {
+        // Get negative offset
+        let minX = 0;
+        let minY = 0;
+        this._nodes.forEach(node => {
+            if (this._frames.some(f => f.nodes.indexOf(node) !== -1)) {
+                return;
+            }
+
+            if (node.x < minX) {
+                minX = node.x;
+            }
+            if (node.y < minY) {
+                minY = node.y;
+            }
+        });
+
+        this._frames.forEach(frame => {
+            if (frame.x < minX) {
+                minX = frame.x;
+            }
+            if (frame.y < minY) {
+                minY = frame.y;
+            }
+        });
+
+        // Restore to 0
+        this._frames.forEach(frame => {            
+            frame.x += -minX;
+            frame.y += -minY;
+            frame.cleanAccumulation();
+        });
+
+        this._nodes.forEach(node => {
+            node.x += -minX;
+            node.y += -minY;            
+            node.cleanAccumulation();
+        });
+
+        // Get correct zoom
+        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;
+        this.x = 0;
+        this.y = 0;
+    }
+
+    processCandidatePort() {
+        let pointB = this._candidateLink!.portA.connectionPoint;
+        let nodeB = this._candidateLink!.portA.node;
+        let pointA: NodeMaterialConnectionPoint;
+        let nodeA: GraphNode;
+
+        if (this._candidatePort) {
+            pointA = this._candidatePort.connectionPoint;
+            nodeA = this._candidatePort.node;
+        } else {
+            if (pointB.direction === NodeMaterialConnectionPointDirection.Output) {
+                return;
+            }
+
+            // No destination so let's spin a new input block
+            let pointName = "output", inputBlock;
+            let customInputBlock = this._candidateLink!.portA.connectionPoint.createCustomInputBlock();
+            if (!customInputBlock) {
+                inputBlock = new InputBlock(NodeMaterialBlockConnectionPointTypes[this._candidateLink!.portA.connectionPoint.type], undefined, this._candidateLink!.portA.connectionPoint.type);
+            } else {
+                [inputBlock, pointName] = customInputBlock;
+            }
+            this.props.globalState.nodeMaterial.attachedBlocks.push(inputBlock);
+            pointA = (inputBlock as any)[pointName];
+            nodeA = this.appendBlock(inputBlock);
+            
+            nodeA.x = this._dropPointX - 200;
+            nodeA.y = this._dropPointY - 50;    
+        }
+
+        if (pointA.direction === NodeMaterialConnectionPointDirection.Input) {
+            let temp = pointB;
+            pointB = pointA;
+            pointA = temp;
+
+            let tempNode = nodeA;
+            nodeA = nodeB;
+            nodeB = tempNode;
+        }
+
+        if (pointB.connectedPoint === pointA) {
+            return;
+        }
+
+        if (pointB === pointA) {
+            return;
+        }
+
+        if (pointB.direction === pointA.direction) {
+            return;
+        }
+
+        if (pointB.ownerBlock === pointA.ownerBlock) {
+            return;
+        }
+
+        // Check compatibility
+        let isFragmentOutput = pointB.ownerBlock.getClassName() === "FragmentOutputBlock";
+        let compatibilityState = pointA.checkCompatibilityState(pointB);
+        if ((pointA.needDualDirectionValidation || pointB.needDualDirectionValidation) && compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible && !(pointA instanceof InputBlock)) {
+            compatibilityState = pointB.checkCompatibilityState(pointA);
+        }
+        if (compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible) {
+            if (isFragmentOutput) {
+                let fragmentBlock = pointB.ownerBlock as FragmentOutputBlock;
+
+                if (pointB.name === "rgb" && fragmentBlock.rgba.isConnected) {
+                    nodeB.getLinksForConnectionPoint(fragmentBlock.rgba)[0].dispose();
+                } else if (pointB.name === "rgba" && fragmentBlock.rgb.isConnected) {
+                    nodeB.getLinksForConnectionPoint(fragmentBlock.rgb)[0].dispose();
+                }                     
+            }
+        } else {
+            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);             
+            return;
+        }
+
+        let linksToNotifyForDispose: Nullable<NodeLink[]> = null;
+
+        if (pointB.isConnected) {
+            let links = nodeB.getLinksForConnectionPoint(pointB);
+
+            linksToNotifyForDispose = links.slice();
+
+            links.forEach(link => {
+                link.dispose(false);
+            });
+        }
+
+        if (pointB.ownerBlock.inputsAreExclusive) { // Disconnect all inputs if block has exclusive inputs
+            pointB.ownerBlock.inputs.forEach(i => {
+                let links = nodeB.getLinksForConnectionPoint(i);
+
+                if (!linksToNotifyForDispose) {
+                    linksToNotifyForDispose = links.slice();
+                } else {
+                    linksToNotifyForDispose.push(...links.slice());
+                }
+
+                links.forEach(link => {
+                    link.dispose(false);
+                });
+            })
+        }
+
+        pointA.connectTo(pointB);
+        this.connectPorts(pointA, pointB);
+
+        if (pointB.innerType === NodeMaterialBlockConnectionPointTypes.AutoDetect) {
+            // need to potentially propagate the type of pointA to other ports of blocks connected to owner of pointB
+
+            const refreshNode = (node: GraphNode) => {
+                node.refresh();
+
+                const links = node.links;
+
+                // refresh first the nodes so that the right types are assigned to the auto-detect ports
+                links.forEach((link) => {
+                    const nodeA = link.nodeA, nodeB = link.nodeB;
+
+                    if (!visitedNodes.has(nodeA)) {
+                        visitedNodes.add(nodeA);
+                        refreshNode(nodeA);
+                    }
+
+                    if (nodeB && !visitedNodes.has(nodeB)) {
+                        visitedNodes.add(nodeB);
+                        refreshNode(nodeB);
+                    }
+                });
+
+                // then refresh the links to display the right color between ports
+                links.forEach((link) => {
+                    if (!visitedLinks.has(link)) {
+                        visitedLinks.add(link);
+                        link.update();
+                    }
+                });
+            };
+
+            const visitedNodes = new Set<GraphNode>([nodeA]);
+            const visitedLinks = new Set<NodeLink>([nodeB.links[nodeB.links.length - 1]]);
+
+            refreshNode(nodeB);
+        } else {
+            nodeB.refresh();
+        }
+
+        linksToNotifyForDispose?.forEach((link) => {
+            link.onDisposedObservable.notifyObservers(link);
+            link.onDisposedObservable.clear();
+        });
+
+        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+    }
+
+    processEditorData(editorData: IEditorData) {
+        const frames = this._frames.splice(0);
+        for (var frame of frames) {
+            frame.dispose();
+        }
+
+        this._frames = [];
+        this.x = editorData.x || 0;
+        this.y = editorData.y || 0;
+        this.zoom = editorData.zoom || 1;
+
+        // Frames
+        if (editorData.frames) {
+            for (var frameData of editorData.frames) {
+                var frame = GraphFrame.Parse(frameData, this, editorData.map);
+                this._frames.push(frame);
+            }  
+        }
+    }
+
+    addFrame(frameData: IFrameData) {
+            const frame = GraphFrame.Parse(frameData, this, this.props.globalState.nodeMaterial.editorData.map);
+            this._frames.push(frame);
+            this.globalState.onSelectionChangedObservable.notifyObservers(frame);
+    }
+ 
+    render() {
+        return (
+            <div id="graph-canvas" 
+                onWheel={evt => this.onWheel(evt)}
+                onPointerMove={evt => this.onMove(evt)}
+                onPointerDown={evt =>  this.onDown(evt)}   
+                onPointerUp={evt =>  this.onUp(evt)} 
+            >    
+                <div id="graph-container">
+                    <div id="graph-canvas-container">
+                    </div>     
+                    <div id="frame-container">                        
+                    </div>
+                    <svg id="graph-svg-container">
+                    </svg>                    
+                    <div id="selection-container">                        
+                    </div>
+                </div>
+            </div>
+        );
+    }
+}

File diff suppressed because it is too large
+ 1498 - 0
guiEditor/src/diagram/graphFrame.ts


+ 488 - 0
guiEditor/src/diagram/graphNode.ts

@@ -0,0 +1,488 @@
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { GlobalState } from '../globalState';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
+import { GraphCanvasComponent, FramePortData } from './graphCanvas';
+import { PropertyLedger } from './propertyLedger';
+import * as React from 'react';
+import { GenericPropertyComponent } from './properties/genericNodePropertyComponent';
+import { DisplayLedger } from './displayLedger';
+import { IDisplayManager } from './display/displayManager';
+import { NodeLink } from './nodeLink';
+import { NodePort } from './nodePort';
+import { GraphFrame } from './graphFrame';
+
+export class GraphNode {
+    private _visual: HTMLDivElement;
+    private _header: HTMLDivElement;
+    private _connections: HTMLDivElement;
+    private _inputsContainer: HTMLDivElement;
+    private _outputsContainer: HTMLDivElement;
+    private _content: HTMLDivElement;    
+    private _comments: HTMLDivElement;
+    private _inputPorts: NodePort[] = [];
+    private _outputPorts: NodePort[] = [];
+    private _links: NodeLink[] = [];    
+    private _x = 0;
+    private _y = 0;
+    private _gridAlignedX = 0;
+    private _gridAlignedY = 0;    
+    private _mouseStartPointX: Nullable<number> = null;
+    private _mouseStartPointY: Nullable<number> = null    
+    private _globalState: GlobalState;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphFrame | GraphNode | NodeLink | NodePort | FramePortData>>>;  
+    private _onSelectionBoxMovedObserver: Nullable<Observer<ClientRect | DOMRect>>;  
+    private _onFrameCreatedObserver: Nullable<Observer<GraphFrame>>; 
+    private _onUpdateRequiredObserver: Nullable<Observer<void>>;  
+    private _ownerCanvas: GraphCanvasComponent; 
+    private _isSelected: boolean;
+    private _displayManager: Nullable<IDisplayManager> = null;
+    private _isVisible = true;
+    private _enclosingFrameId = -1;
+
+    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");
+            this._upateNodePortNames();
+        }
+
+        for (var link of this._links) {
+            link.isVisible = value;
+        }
+
+        this._refreshLinks();
+    }
+
+    private _upateNodePortNames(){
+        for (var port of this._inputPorts.concat(this._outputPorts)) {
+            if(port.hasLabel()){
+                port.portName = port.connectionPoint.displayName || port.connectionPoint.name;
+            }
+        }
+    }
+
+    public get outputPorts() {
+        return this._outputPorts;
+    }
+
+    public get inputPorts() {
+        return this._inputPorts;
+    }
+
+    public get links() {
+        return this._links;
+    }
+
+    public get gridAlignedX() {
+        return this._gridAlignedX;
+    }
+
+    public get gridAlignedY() {
+        return this._gridAlignedY;
+    }
+
+    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._visual.style.left = `${this._gridAlignedX}px`;
+
+        this._refreshLinks();
+        this._refreshFrames();
+    }
+
+    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._visual.style.top = `${this._gridAlignedY}px`;
+
+        this._refreshLinks();
+        this._refreshFrames();
+    }
+
+    public get width() {
+        return this._visual.clientWidth;
+    }
+
+    public get height() {
+        return this._visual.clientHeight;
+    }
+
+    public get id() {
+        return this.block.uniqueId;
+    }
+
+    public get name() {
+        return this.block.name;
+    }
+
+    public get isSelected() {
+        return this._isSelected;
+    }
+
+    public get enclosingFrameId() {
+        return this._enclosingFrameId;
+    }
+
+    public set enclosingFrameId(value: number) {
+        this._enclosingFrameId = value;
+    }
+
+    public set isSelected(value: boolean) {
+        if (this._isSelected === value) {
+            return;            
+        }
+
+        this._isSelected = value;
+
+        if (!value) {
+            this._visual.classList.remove("selected");    
+            let indexInSelection = this._ownerCanvas.selectedNodes.indexOf(this);
+
+            if (indexInSelection > -1) {
+                this._ownerCanvas.selectedNodes.splice(indexInSelection, 1);
+            }
+        } else {
+            this._globalState.onSelectionChangedObservable.notifyObservers(this);  
+        }
+    }
+
+    public constructor(public block: NodeMaterialBlock, globalState: GlobalState) {
+        this._globalState = globalState;
+
+        this._onSelectionChangedObserver = this._globalState.onSelectionChangedObservable.add(node => {
+            if (node === this) {
+                this._visual.classList.add("selected");
+            } else {
+                setTimeout(() => {
+                    if (this._ownerCanvas.selectedNodes.indexOf(this) === -1) {
+                        this._visual.classList.remove("selected");
+                    }
+                })
+            }
+        });
+
+        this._onUpdateRequiredObserver = this._globalState.onUpdateRequiredObservable.add(() => {
+            this.refresh();
+        });
+
+        this._onSelectionBoxMovedObserver = this._globalState.onSelectionBoxMoved.add(rect1 => {
+            const rect2 = this._visual.getBoundingClientRect();
+            var overlap = !(rect1.right < rect2.left || 
+                rect1.left > rect2.right || 
+                rect1.bottom < rect2.top || 
+                rect1.top > rect2.bottom);
+
+            this.isSelected = overlap;
+        });
+
+        this._onFrameCreatedObserver = this._globalState.onFrameCreatedObservable.add(frame => {      
+            if (this._ownerCanvas.frames.some(f => f.nodes.indexOf(this) !== -1)) {
+                return;
+            }
+            
+            if (this.isOverlappingFrame(frame)) {
+                frame.nodes.push(this);
+            }
+        });
+    }
+
+    public isOverlappingFrame(frame: GraphFrame) {
+        const rect2 = this._visual.getBoundingClientRect();
+        const rect1 = frame.element.getBoundingClientRect();
+
+        // Add a tiny margin
+        rect1.width -= 5;
+        rect1.height -= 5;
+
+        const isOverlappingFrame = !(rect1.right < rect2.left || 
+            rect1.left > rect2.right || 
+            rect1.bottom < rect2.top || 
+            rect1.top > rect2.bottom);
+
+        if (isOverlappingFrame) {
+            this.enclosingFrameId = frame.id;
+        }
+        return isOverlappingFrame;
+    }
+
+    public getPortForConnectionPoint(point: NodeMaterialConnectionPoint) {
+        for (var port of this._inputPorts) {
+            let attachedPoint = port.connectionPoint;
+
+            if (attachedPoint === point) {
+                return port;
+            }
+        }
+
+        for (var port of this._outputPorts) {
+            let attachedPoint = port.connectionPoint;
+
+            if (attachedPoint === point) {
+                return port;
+            }
+        }
+
+        return null;
+    }
+
+    public getLinksForConnectionPoint(point: NodeMaterialConnectionPoint) {
+        return this._links.filter(link => link.portA.connectionPoint === point || link.portB!.connectionPoint === point);
+    }
+    
+    private _refreshFrames() {       
+        if (this._ownerCanvas._frameIsMoving || this._ownerCanvas._isLoading) {
+            return;
+        }
+        
+        // Frames
+        for (var frame of this._ownerCanvas.frames) {
+            frame.syncNode(this);
+        }
+    }
+
+    public _refreshLinks() {
+        if (this._ownerCanvas._isLoading) {
+            return;
+        }
+        for (var link of this._links) {
+            link.update();
+        }
+    }
+
+    public refresh() {
+        if (this._displayManager) {
+            this._header.innerHTML = this._displayManager.getHeaderText(this.block);
+            this._displayManager.updatePreviewContent(this.block, this._content);
+            this._visual.style.background = this._displayManager.getBackgroundColor(this.block);
+            let additionalClass = this._displayManager.getHeaderClass(this.block);
+            this._header.classList.value = "header";
+            if (additionalClass) {
+                this._header.classList.add(additionalClass);
+            }
+        } else {
+            this._header.innerHTML = this.block.name;
+        }
+
+        for (var port of this._inputPorts) {
+            port.refresh();
+        }
+
+        for (var port of this._outputPorts) {
+            port.refresh();
+        }
+
+        if(this.enclosingFrameId !== -1) {   
+            let index = this._ownerCanvas.frames.findIndex(frame => frame.id === this.enclosingFrameId);
+            if(index >= 0 && this._ownerCanvas.frames[index].isCollapsed) {
+                this._ownerCanvas.frames[index].redrawFramePorts();
+            }
+        }   
+        this._comments.innerHTML = this.block.comments || "";
+        this._comments.title = this.block.comments || "";
+
+    }
+
+    private _onDown(evt: PointerEvent) {
+        // Check if this is coming from the port
+        if (evt.srcElement && (evt.srcElement as HTMLElement).nodeName === "IMG") {
+            return;
+        }
+
+        const indexInSelection = this._ownerCanvas.selectedNodes.indexOf(this) ;
+        if (indexInSelection === -1) {
+            this._globalState.onSelectionChangedObservable.notifyObservers(this);
+        } else if (evt.ctrlKey) {
+            this.isSelected = false;
+        }
+
+        evt.stopPropagation();
+
+        for (var selectedNode of this._ownerCanvas.selectedNodes) {
+            selectedNode.cleanAccumulation();
+        }
+
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;        
+        
+        this._visual.setPointerCapture(evt.pointerId);
+    }
+
+    public cleanAccumulation(useCeil = false) {
+        this.x = this._ownerCanvas.getGridPosition(this.x, useCeil);
+        this.y = this._ownerCanvas.getGridPosition(this.y, useCeil);
+    }
+
+    private _onUp(evt: PointerEvent) {
+        evt.stopPropagation();
+
+        for (var selectedNode of this._ownerCanvas.selectedNodes) {
+            selectedNode.cleanAccumulation();
+        }
+        
+        this._mouseStartPointX = null;
+        this._mouseStartPointY = null;
+        this._visual.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._ownerCanvas.selectedNodes) {
+            selectedNode.x += newX;
+            selectedNode.y += newY;
+        }
+
+        this._mouseStartPointX = evt.clientX;
+        this._mouseStartPointY = evt.clientY;   
+
+        evt.stopPropagation();
+    }
+
+    public renderProperties(): Nullable<JSX.Element> {
+        let control = PropertyLedger.RegisteredControls[this.block.getClassName()];
+
+        if (!control) {
+            control = GenericPropertyComponent;
+        }
+
+        return React.createElement(control, {
+            globalState: this._globalState,
+            block: this.block
+        });
+    }
+
+    public appendVisual(root: HTMLDivElement, owner: GraphCanvasComponent) {
+        this._ownerCanvas = owner;
+
+        // Display manager
+        let displayManagerClass = DisplayLedger.RegisteredControls[this.block.getClassName()];
+        
+
+        if (displayManagerClass) {
+            this._displayManager = new displayManagerClass();
+        }
+
+        // DOM
+        this._visual = root.ownerDocument!.createElement("div");
+        this._visual.classList.add("visual");
+
+        this._visual.addEventListener("pointerdown", evt => this._onDown(evt));
+        this._visual.addEventListener("pointerup", evt => this._onUp(evt));
+        this._visual.addEventListener("pointermove", evt => this._onMove(evt));
+
+        this._header = root.ownerDocument!.createElement("div");
+        this._header.classList.add("header");
+
+        this._visual.appendChild(this._header);      
+
+        this._connections = root.ownerDocument!.createElement("div");
+        this._connections.classList.add("connections");
+        this._visual.appendChild(this._connections);        
+        
+        this._inputsContainer = root.ownerDocument!.createElement("div");
+        this._inputsContainer.classList.add("inputsContainer");
+        this._connections.appendChild(this._inputsContainer);      
+
+        this._outputsContainer = root.ownerDocument!.createElement("div");
+        this._outputsContainer.classList.add("outputsContainer");
+        this._connections.appendChild(this._outputsContainer);      
+
+        this._content = root.ownerDocument!.createElement("div");
+        this._content.classList.add("content");        
+        this._visual.appendChild(this._content);     
+
+        var selectionBorder = root.ownerDocument!.createElement("div");
+        selectionBorder.classList.add("selection-border");
+        this._visual.appendChild(selectionBorder);     
+
+        root.appendChild(this._visual);
+
+        // Comments
+        this._comments = root.ownerDocument!.createElement("div");
+        this._comments.classList.add("comments");
+            
+        this._visual.appendChild(this._comments);    
+
+        // Connections
+        for (var input of this.block.inputs) {
+            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));
+        }
+
+        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);
+        }
+
+        if (this._onUpdateRequiredObserver) {
+            this._globalState.onUpdateRequiredObservable.remove(this._onUpdateRequiredObserver);
+        }
+
+        if (this._onSelectionBoxMovedObserver) {
+            this._globalState.onSelectionBoxMoved.remove(this._onSelectionBoxMovedObserver);
+        }
+
+        if (this._visual.parentElement) {
+            this._visual.parentElement.removeChild(this._visual);
+        }
+
+        if (this._onFrameCreatedObserver) {
+            this._globalState.onFrameCreatedObservable.remove(this._onFrameCreatedObserver);
+        }
+
+        for (var port of this._inputPorts) {
+            port.dispose();
+        }
+
+        for (var port of this._outputPorts) {
+            port.dispose();
+        }
+
+        let links = this._links.slice(0);
+        for (var link of links) {
+            link.dispose();           
+        }
+
+        this.block.dispose();
+    }
+}

+ 156 - 0
guiEditor/src/diagram/nodeLink.ts

@@ -0,0 +1,156 @@
+import { GraphCanvasComponent, FramePortData } from './graphCanvas';
+import { GraphNode } from './graphNode';
+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 | FrameNodePort;
+    private _portB?: NodePort | FrameNodePort;
+    private _nodeA: GraphNode;
+    private _nodeB?: GraphNode;
+    private _path: SVGPathElement;
+    private _selectionPath: SVGPathElement;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphFrame | GraphNode | NodeLink | NodePort | FramePortData>>>;  
+    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;
+    }
+
+    public get portB() {
+        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();
+        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.element.getBoundingClientRect();
+            endX = (rectB.left - xOffset + 0.5 * rectB.width) / zoom;
+            endY = (rectB.top - yOffset + 0.5 * rectB.height) / zoom;
+        }
+
+        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 {
+            const deltaX = endX - startX;
+            const deltaY = endY - startY;
+            const tangentLength = Math.min(Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 0.5, 300);
+            this._path.setAttribute("d", `M${startX},${startY} C${startX + tangentLength},${startY} ${endX - tangentLength},${endY} ${endX},${endY}`);
+            this._selectionPath.setAttribute("d", `M${startX},${startY} C${startX + tangentLength},${startY} ${endX - tangentLength},${endY} ${endX},${endY}`);
+        }
+        this._path.setAttribute("stroke", this._portA.element.style.backgroundColor!);
+    }
+
+    public constructor(graphCanvas: GraphCanvasComponent, portA: NodePort, nodeA: GraphNode, portB?: NodePort, nodeB?: GraphNode) {
+        this._portA = portA;
+        this._portB = portB;
+        this._nodeA = nodeA;
+        this._nodeB = nodeB;
+        this._graphCanvas = graphCanvas;
+
+        var document = portA.element.ownerDocument!;
+        var svg = graphCanvas.svgCanvas;
+
+        // Create path
+        this._path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+        this._path.setAttribute("fill", "none");
+        this._path.classList.add("link");
+
+        svg.appendChild(this._path);
+
+        this._selectionPath = document.createElementNS('http://www.w3.org/2000/svg', "path");
+        this._selectionPath.setAttribute("fill", "none");
+        this._selectionPath.classList.add("selection-link");
+
+        svg.appendChild(this._selectionPath);
+
+        this._selectionPath.onmousedown = () => this.onClick();
+
+        if (this._portB) {
+            // Update
+            this.update();
+        }
+
+        this._onSelectionChangedObserver = this._graphCanvas.globalState.onSelectionChangedObservable.add((selection) => {
+            if (selection === this) {
+                this._path.classList.add("selected");
+                this._selectionPath.classList.add("selected");
+            } else {
+                this._path.classList.remove("selected");
+                this._selectionPath.classList.remove("selected");
+            }
+        });
+    }
+
+    onClick() {
+        this._graphCanvas.globalState.onSelectionChangedObservable.notifyObservers(this);
+    }
+
+    public dispose(notify = true) {
+        this._graphCanvas.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+
+        if (this._path.parentElement) {
+            this._path.parentElement.removeChild(this._path);
+        }
+
+        if (this._selectionPath.parentElement) {
+            this._selectionPath.parentElement.removeChild(this._selectionPath);
+        }
+
+        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);
+
+            this._portA.connectionPoint.disconnectFrom(this._portB!.connectionPoint);
+        }
+
+        if (notify) {
+            this.onDisposedObservable.notifyObservers(this);
+
+            this.onDisposedObservable.clear();
+        }
+    }
+}

File diff suppressed because it is too large
+ 197 - 0
guiEditor/src/diagram/nodePort.ts


+ 78 - 0
guiEditor/src/diagram/properties/frameNodePortPropertyComponent.tsx

@@ -0,0 +1,78 @@
+
+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, GraphFrame } from '../graphFrame';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { FrameNodePort } from '../frameNodePort';
+import { NodePort } from '../nodePort';
+import { GraphNode } from '../graphNode';
+import { NodeLink } from '../nodeLink';
+import { FramePortData, isFramePortData } from '../graphCanvas';
+
+export interface IFrameNodePortPropertyTabComponentProps {
+    globalState: GlobalState
+    frameNodePort: FrameNodePort;
+    frame: GraphFrame;
+}
+
+export class FrameNodePortPropertyTabComponent extends React.Component<IFrameNodePortPropertyTabComponentProps, { port: FrameNodePort }> {
+    private _onFramePortPositionChangedObserver: Nullable<Observer<FrameNodePort>>;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphFrame | NodePort | GraphNode | NodeLink | FramePortData>>>;
+
+    constructor(props: IFrameNodePortPropertyTabComponentProps) {
+        super(props);
+        this.state = {
+            port: this.props.frameNodePort
+        }
+
+        const _this = this;
+        this._onSelectionChangedObserver = this.props.globalState.onSelectionChangedObservable.add((selection) => {
+            if (isFramePortData(selection)) {
+                selection.port.onFramePortPositionChangedObservable.clear();
+                _this._onFramePortPositionChangedObserver = selection.port.onFramePortPositionChangedObservable.add((port: FrameNodePort) => {
+                    _this.setState({ port: port });
+                });
+
+                _this.setState({ port: selection.port });
+            }
+        });
+
+        this._onFramePortPositionChangedObserver = this.props.frameNodePort.onFramePortPositionChangedObservable.add((port: FrameNodePort) => {
+            _this.setState({ port: port });
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.frameNodePort.onFramePortPositionChangedObservable.remove(this._onFramePortPositionChangedObserver);
+        this.props.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+    }
+
+    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 Name" propertyName="portName" target={this.props.frameNodePort} />
+                        {this.props.frameNodePort.framePortPosition !== FramePortPosition.Top && <ButtonLineComponent label="Move Port Up" onClick={() => {
+                            this.props.frame.moveFramePortUp(this.props.frameNodePort);
+                        }} />}
+
+                        {this.props.frameNodePort.framePortPosition !== FramePortPosition.Bottom && <ButtonLineComponent label="Move Port Down" onClick={() => {
+                            this.props.frame.moveFramePortDown(this.props.frameNodePort);
+                        }} />}
+                    </LineContainerComponent>
+                </div>
+            </div>
+        );
+    }
+}

+ 70 - 0
guiEditor/src/diagram/properties/framePropertyComponent.tsx

@@ -0,0 +1,70 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { GraphFrame } from '../graphFrame';
+import { GlobalState } from '../../globalState';
+import { Color3LineComponent } from '../../sharedComponents/color3LineComponent';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+
+export interface IFramePropertyTabComponentProps {
+    globalState: GlobalState
+    frame: GraphFrame;
+}
+
+export class FramePropertyTabComponent extends React.Component<IFramePropertyTabComponentProps> {
+    private onFrameExpandStateChangedObserver: Nullable<Observer<GraphFrame>>;
+
+    constructor(props: IFramePropertyTabComponentProps) {
+        super(props)
+    }
+
+
+    componentDidMount() {
+        this.onFrameExpandStateChangedObserver = this.props.frame.onExpandStateChanged.add(() => this.forceUpdate());
+    }
+
+    componentWillUnmount() {
+        if (this.onFrameExpandStateChangedObserver) {
+            this.props.frame.onExpandStateChanged.remove(this.onFrameExpandStateChangedObserver);
+            this.onFrameExpandStateChangedObserver = null;
+        }
+    }    
+
+    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="Name" propertyName="name" target={this.props.frame} />
+                    <Color3LineComponent globalState={this.props.globalState} label="Color" target={this.props.frame} propertyName="color"></Color3LineComponent>
+                    <TextInputLineComponent globalState={this.props.globalState} label="Comments" propertyName="comments" target={this.props.frame}/>
+                    {
+                        !this.props.frame.isCollapsed &&
+                        <ButtonLineComponent label="Collapse" onClick={() => {
+                                this.props.frame!.isCollapsed = true;
+                            }} />
+                    }
+                    {
+                        this.props.frame.isCollapsed &&
+                        <ButtonLineComponent label="Expand" onClick={() => {
+                                this.props.frame!.isCollapsed = false;
+                            }} />
+                    }
+                    <ButtonLineComponent label="Export" onClick={() => {
+                                this.props.frame!.export();
+                            }} />
+                </LineContainerComponent>
+            </div>
+        </div>
+        );
+    }
+}

+ 145 - 0
guiEditor/src/diagram/properties/genericNodePropertyComponent.tsx

@@ -0,0 +1,145 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import { TextLineComponent } from '../../sharedComponents/textLineComponent';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
+import { Vector2LineComponent } from '../../sharedComponents/vector2LineComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { PropertyTypeForEdition, IPropertyDescriptionForEdition, IEditablePropertyListOption } from 'babylonjs/Materials/Node/nodeMaterialDecorator';
+
+export class GenericPropertyComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <>
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <GenericPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+            </>
+        );
+    }
+}
+
+export class GeneralPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <>
+                <LineContainerComponent title="GENERAL">
+                    {
+                        (!this.props.block.isInput || !(this.props.block as InputBlock).isAttribute) &&
+                        <TextInputLineComponent globalState={this.props.globalState} label="Name" propertyName="name" target={this.props.block}
+                            onChange={() => this.props.globalState.onUpdateRequiredObservable.notifyObservers()}
+                            validator={ newName => 
+                            {if(!this.props.block.validateBlockName(newName)){ 
+                                this.props.globalState.onErrorMessageDialogRequiredObservable.notifyObservers(`"${newName}" is a reserved name, please choose another`);
+                                return false;
+                            }
+                            return true;
+                            }} />
+                    }
+                    <TextLineComponent label="Type" value={this.props.block.getClassName()} />
+                    <TextInputLineComponent globalState={this.props.globalState} label="Comments" propertyName="comments" target={this.props.block}
+                            onChange={() => this.props.globalState.onUpdateRequiredObservable.notifyObservers()} />
+                </LineContainerComponent>
+            </>
+        );
+    }
+}
+
+export class GenericPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    forceRebuild(notifiers?: { "rebuild"?: boolean; "update"?: boolean; }) {
+        if (!notifiers || notifiers.update) {
+            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        }
+
+        if (!notifiers || notifiers.rebuild) {
+            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+        }
+    }
+
+    render() {
+        const block = this.props.block,
+              propStore: IPropertyDescriptionForEdition[] = (block as any)._propStore;
+
+        if (!propStore) {
+            return (
+                <>
+                </>
+            );
+        }
+
+        const componentList: { [groupName: string]: JSX.Element[]} = {},
+              groups: string[] = [];
+
+        for (const { propertyName, displayName, type, groupName, options } of propStore) {
+            let components = componentList[groupName];
+
+            if (!components) {
+                components = [];
+                componentList[groupName] = components;
+                groups.push(groupName);
+            }
+
+            switch (type) {
+                case PropertyTypeForEdition.Boolean: {
+                    components.push(
+                        <CheckBoxLineComponent label={displayName} target={this.props.block} propertyName={propertyName} onValueChanged={() => this.forceRebuild(options.notifiers)} />
+                    );
+                    break;
+                }
+                case PropertyTypeForEdition.Float: {
+                    let cantDisplaySlider = (isNaN(options.min as number) || isNaN(options.max as number) || options.min === options.max);
+                    if (cantDisplaySlider) {
+                        components.push(
+                            <FloatLineComponent globalState={this.props.globalState} label={displayName} propertyName={propertyName} target={this.props.block} onChange={() => this.forceRebuild(options.notifiers)} />
+                        );
+                    } else {
+                        components.push(
+                            <SliderLineComponent label={displayName} target={this.props.block} globalState={this.props.globalState} propertyName={propertyName} step={Math.abs((options.max as number) - (options.min as number)) / 100.0} minimum={Math.min(options.min as number, options.max as number)} maximum={options.max as number} onChange={() => this.forceRebuild(options.notifiers)}/>
+                        );
+                    }
+                    break;
+                }
+                case PropertyTypeForEdition.Vector2: {
+                    components.push(
+                        <Vector2LineComponent globalState={this.props.globalState} label={displayName} propertyName={propertyName} target={this.props.block} onChange={() => this.forceRebuild(options.notifiers)} />
+                    );
+                    break;
+                }
+                case PropertyTypeForEdition.List: {
+                    components.push(
+                        <OptionsLineComponent label={displayName} options={options.options as IEditablePropertyListOption[]} target={this.props.block} propertyName={propertyName} onSelect={() => this.forceRebuild(options.notifiers)} />
+                    );
+                    break;
+                }
+            }
+        }
+
+        return (
+            <>
+            {
+                groups.map((group) =>
+                    <LineContainerComponent title={group}>
+                        {componentList[group]}
+                    </LineContainerComponent>
+                )
+            }
+            </>
+        );
+    }
+}

+ 151 - 0
guiEditor/src/diagram/properties/gradientNodePropertyComponent.tsx

@@ -0,0 +1,151 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { GradientBlockColorStep, GradientBlock } from 'babylonjs/Materials/Node/Blocks/gradientBlock';
+import { GradientStepComponent } from './gradientStepComponent';
+import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
+import { Color3 } from 'babylonjs/Maths/math.color';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+
+export class GradientPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+
+    private onValueChangedObserver: Nullable<Observer<GradientBlock>>;
+
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    componentDidMount() {
+        let gradientBlock = this.props.block as GradientBlock;
+        this.onValueChangedObserver = gradientBlock.onValueChangedObservable.add(() => {
+            this.forceUpdate();
+            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        });
+    }
+
+    componentWillUnmount() {
+        let gradientBlock = this.props.block as GradientBlock;
+        if (this.onValueChangedObserver) {
+            gradientBlock.onValueChangedObservable.remove(this.onValueChangedObserver);
+            this.onValueChangedObserver = null;
+        }
+    }
+
+    forceRebuild() {
+        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+    }
+
+    deleteStep(step: GradientBlockColorStep) {
+        let gradientBlock = this.props.block as GradientBlock;
+
+        let index = gradientBlock.colorSteps.indexOf(step);
+
+        if (index > -1) {
+            gradientBlock.colorSteps.splice(index, 1);
+            gradientBlock.colorStepsUpdated();
+            this.forceRebuild();
+            this.forceUpdate();
+        }
+    }
+
+    copyStep(step: GradientBlockColorStep) {
+        let gradientBlock = this.props.block as GradientBlock;
+
+        let newStep = new GradientBlockColorStep(1.0, step.color);
+        gradientBlock.colorSteps.push(newStep);
+        gradientBlock.colorStepsUpdated();
+        this.forceRebuild();
+        this.forceUpdate();
+    }
+
+    addNewStep() {
+        let gradientBlock = this.props.block as GradientBlock;
+
+        let newStep = new GradientBlockColorStep(1.0, Color3.White());
+        gradientBlock.colorSteps.push(newStep);
+        gradientBlock.colorStepsUpdated();
+
+        this.forceRebuild();
+        this.forceUpdate();
+    }
+
+    checkForReOrder() {
+        let gradientBlock = this.props.block as GradientBlock;
+        gradientBlock.colorSteps.sort((a, b) => {
+            if (a.step === b.step) {
+                return 0;
+            }
+
+            if (a.step > b.step) {
+                return 1;
+            }
+
+            return -1;
+        });
+        gradientBlock.colorStepsUpdated();
+
+        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        this.forceUpdate();
+    }
+
+    render() {
+        let gradientBlock = this.props.block as GradientBlock;
+
+        var typeOptions = [
+            { label: "None", value: 0 },
+            { label: "Visible in the inspector", value: 1 },
+        ];
+
+        return (
+            <div>
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                <OptionsLineComponent label="Type" options={typeOptions} target={this.props.block}
+                            noDirectUpdate={true}
+                            getSelection={(block) => {
+                                if (block.visibleInInspector) {
+                                    return 1;
+                                }
+
+                                if (block.isConstant) {
+                                    return 2;
+                                }
+
+                                return 0;
+                            }}
+                            onSelect={(value: any) => {
+                                switch (value) {
+                                    case 0:
+                                        this.props.block.visibleInInspector = false;
+                                        break;
+                                    case 1:
+                                        this.props.block.visibleInInspector = true;
+                                        break;
+                                }
+                                this.forceUpdate();
+                                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                                this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                            }} />
+                </LineContainerComponent>
+                <LineContainerComponent title="STEPS">
+                    <ButtonLineComponent label="Add new step" onClick={() => this.addNewStep()} />
+                    {
+                        gradientBlock.colorSteps.map((c, i) => {
+                            return (
+                                <GradientStepComponent globalState={this.props.globalState}
+                                onCheckForReOrder={() => this.checkForReOrder()}
+                                onUpdateStep={() => this.forceRebuild()}
+                                key={"step-" + i} lineIndex={i} step={c} onCopy={() => this.copyStep(c)} onDelete={() => this.deleteStep(c)}/>
+                            );
+                        })
+                    }
+                </LineContainerComponent>
+            </div>
+        );
+    }
+}

+ 88 - 0
guiEditor/src/diagram/properties/gradientStepComponent.tsx

@@ -0,0 +1,88 @@
+import * as React from 'react';
+import { GlobalState } from '../../globalState';
+import { Color3 } from 'babylonjs/Maths/math.color';
+import { GradientBlockColorStep } from 'babylonjs/Materials/Node/Blocks/gradientBlock';
+import { ColorPickerLineComponent } from '../../sharedComponents/colorPickerComponent';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+
+const deleteButton = require('../../../imgs/delete.svg');
+const copyIcon: string = require('../../sharedComponents/copy.svg');
+
+interface IGradientStepComponentProps {
+    globalState: GlobalState;
+    step: GradientBlockColorStep;
+    lineIndex: number;
+    onDelete: () => void;
+    onUpdateStep: () => void;
+    onCheckForReOrder: () => void;
+    onCopy?: () => void;
+}
+
+export class GradientStepComponent extends React.Component<IGradientStepComponentProps, {gradient: number}> {
+
+    constructor(props: IGradientStepComponentProps) {
+        super(props);
+
+        this.state={gradient: props.step.step};
+    }
+
+    updateColor(color: string) {
+        this.props.step.color = Color3.FromHexString(color);
+
+        this.props.onUpdateStep();
+        this.forceUpdate();
+    }    
+    
+    updateStep(gradient: number) {
+        this.props.step.step = gradient;
+
+        this.setState({gradient: gradient});
+
+        this.props.onUpdateStep();
+    }
+
+    onPointerUp() {
+        this.props.onCheckForReOrder();
+    }
+
+    render() {
+        let step = this.props.step;
+        
+        return (
+            <div className="gradient-step">
+                <div className="step">
+                    {`#${this.props.lineIndex}`}
+                </div>
+                <div className="color">
+                    <ColorPickerLineComponent value={step.color} disableAlpha={true} globalState={this.props.globalState} 
+                            onColorChanged={color => {
+                                    this.updateColor(color);
+                            }} 
+                    />  
+                </div>
+                <div className="step-value">
+                    <FloatLineComponent globalState={this.props.globalState} smallUI={true} label="" target={step} propertyName="step"
+                    min={0} max={1}
+                    onEnter={ () => { 
+                            this.props.onUpdateStep();
+                            this.props.onCheckForReOrder();
+                            this.forceUpdate();
+                        }
+                    } 
+                    ></FloatLineComponent>
+                </div>
+                <div className="step-slider">
+                    <input className="range" type="range" step={0.01} min={0} max={1.0} value={step.step}
+                        onPointerUp={evt => this.onPointerUp()}
+                        onChange={evt => this.updateStep(parseFloat(evt.target.value))} />
+                </div>
+                <div className="gradient-copy" onClick={() => {if(this.props.onCopy) this.props.onCopy()}} title="Copy Step">
+                    <img className="img" src={copyIcon} />
+                </div>
+                <div className="gradient-delete" onClick={() => this.props.onDelete()} title={"Delete Step"}>
+                    <img className="img" src={deleteButton}/>
+                </div>
+            </div>
+        )
+    }
+}

+ 323 - 0
guiEditor/src/diagram/properties/inputNodePropertyComponent.tsx

@@ -0,0 +1,323 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+import { FloatPropertyTabComponent } from '../../components/propertyTab/properties/floatPropertyTabComponent';
+import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
+import { Vector2PropertyTabComponent } from '../../components/propertyTab/properties/vector2PropertyTabComponent';
+import { Color3PropertyTabComponent } from '../../components/propertyTab/properties/color3PropertyTabComponent';
+import { Vector3PropertyTabComponent } from '../../components/propertyTab/properties/vector3PropertyTabComponent';
+import { Vector4PropertyTabComponent } from '../../components/propertyTab/properties/vector4PropertyTabComponent';
+import { MatrixPropertyTabComponent } from '../../components/propertyTab/properties/matrixPropertyTabComponent';
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { NodeMaterialSystemValues } from 'babylonjs/Materials/Node/Enums/nodeMaterialSystemValues';
+import { AnimatedInputBlockTypes } from 'babylonjs/Materials/Node/Blocks/Input/animatedInputBlockTypes';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { Color4PropertyTabComponent } from '../../components/propertyTab/properties/color4PropertyTabComponent';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+
+export class InputPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    
+    private onValueChangedObserver: Nullable<Observer<InputBlock>>;
+
+    constructor(props: IPropertyComponentProps) {
+        super(props)
+    }
+
+    componentDidMount() {        
+        let inputBlock = this.props.block as InputBlock;
+        this.onValueChangedObserver = inputBlock.onValueChangedObservable.add(() => {
+            this.forceUpdate()
+            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        });
+    }
+
+    componentWillUnmount() {
+        
+        let inputBlock = this.props.block as InputBlock;
+        if (this.onValueChangedObserver) {
+            inputBlock.onValueChangedObservable.remove(this.onValueChangedObserver);
+            this.onValueChangedObserver = null;
+        }
+    }    
+
+    renderValue(globalState: GlobalState) {
+        let inputBlock = this.props.block as InputBlock;
+        switch (inputBlock.type) {
+            case NodeMaterialBlockConnectionPointTypes.Float: {
+                let cantDisplaySlider = (isNaN(inputBlock.min) || isNaN(inputBlock.max) || inputBlock.min === inputBlock.max);            
+                return (
+                    <>
+                        <CheckBoxLineComponent label="Is boolean" target={inputBlock} propertyName="isBoolean" />
+                        {
+                            inputBlock.isBoolean &&
+                            <CheckBoxLineComponent label="Value" isSelected={() => {
+                                return inputBlock.value === 1
+                            }} onSelect={(value) => {
+                                inputBlock.value = value ? 1 : 0;
+                                if (inputBlock.isConstant) {
+                                    this.props.globalState.onRebuildRequiredObservable.notifyObservers();    
+                                }
+                                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                            }}/>
+                        }
+                        {
+                            !inputBlock.isBoolean &&
+                            <FloatLineComponent globalState={this.props.globalState} label="Min" target={inputBlock} propertyName="min" onChange={() => this.forceUpdate()}></FloatLineComponent>
+                        }
+                        {
+                            !inputBlock.isBoolean &&
+                            <FloatLineComponent globalState={this.props.globalState} label="Max" target={inputBlock} propertyName="max" onChange={() => this.forceUpdate()}></FloatLineComponent>      
+                        }
+                        {
+                            !inputBlock.isBoolean && cantDisplaySlider &&
+                            <FloatPropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                        }        
+                        {
+                            !inputBlock.isBoolean && !cantDisplaySlider &&
+                            <SliderLineComponent label="Value" globalState={this.props.globalState} target={inputBlock} propertyName="value" step={Math.abs(inputBlock.max - inputBlock.min) / 100.0} minimum={Math.min(inputBlock.min, inputBlock.max)} maximum={inputBlock.max} onChange={() => {
+                                if (inputBlock.isConstant) {
+                                    this.props.globalState.onRebuildRequiredObservable.notifyObservers();    
+                                }
+                            }}/>
+                        }
+                    </>
+                );
+            }
+            case NodeMaterialBlockConnectionPointTypes.Vector2:
+                return (
+                    <Vector2PropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+                return (
+                    <Color3PropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );
+            case NodeMaterialBlockConnectionPointTypes.Color4:
+                return (
+                    <Color4PropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );
+            case NodeMaterialBlockConnectionPointTypes.Vector3:
+                return (
+                    <Vector3PropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );            
+            case NodeMaterialBlockConnectionPointTypes.Vector4:
+                return (
+                    <Vector4PropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );
+            case NodeMaterialBlockConnectionPointTypes.Matrix:
+                return (
+                    <MatrixPropertyTabComponent globalState={globalState} inputBlock={inputBlock} />
+                );                
+        }
+
+        return null;
+    }
+
+    setDefaultValue() {
+        let inputBlock = this.props.block as InputBlock;
+        inputBlock.setDefaultValue();
+    }
+
+    render() {
+        let inputBlock = this.props.block as InputBlock;
+
+        var systemValuesOptions: {label: string, value: NodeMaterialSystemValues}[] = [];
+        var attributeOptions: {label: string, value: string}[] = [];
+        var animationOptions: {label: string, value: AnimatedInputBlockTypes}[] = [];
+
+        switch(inputBlock.type) {      
+            case NodeMaterialBlockConnectionPointTypes.Float:
+                animationOptions = [
+                    { label: "None", value: AnimatedInputBlockTypes.None },
+                    { label: "Time", value: AnimatedInputBlockTypes.Time },
+                ];
+                systemValuesOptions = [ 
+                    { label: "Delta time", value: NodeMaterialSystemValues.DeltaTime }
+                ]
+                break;      
+            case NodeMaterialBlockConnectionPointTypes.Matrix:
+                systemValuesOptions = [
+                    { label: "World", value: NodeMaterialSystemValues.World },
+                    { label: "World x View", value: NodeMaterialSystemValues.WorldView },
+                    { label: "World x ViewxProjection", value: NodeMaterialSystemValues.WorldViewProjection },
+                    { label: "View", value: NodeMaterialSystemValues.View },
+                    { label: "View x Projection", value: NodeMaterialSystemValues.ViewProjection },
+                    { label: "Projection", value: NodeMaterialSystemValues.Projection }
+                ];
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Color3:
+                systemValuesOptions = [
+                    { label: "Fog color", value: NodeMaterialSystemValues.FogColor }
+                ];
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Color4:
+                attributeOptions = [
+                    { label: "color", value: "color" }
+                ];
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Vector2:
+                attributeOptions = [
+                    { label: "uv", value: "uv" },
+                    { label: "uv2", value: "uv2" },
+                ];
+                break;                
+            case NodeMaterialBlockConnectionPointTypes.Vector3:
+                systemValuesOptions = [
+                    { label: "Camera position", value: NodeMaterialSystemValues.CameraPosition }
+                ];
+                attributeOptions = [
+                    { label: "position", value: "position" },
+                    { label: "normal", value: "normal" },
+                    { label: "tangent", value: "tangent" },        
+                ];
+                break;
+            case NodeMaterialBlockConnectionPointTypes.Vector4:
+                    attributeOptions = [
+                        { label: "matricesIndices", value: "matricesIndices" },
+                        { label: "matricesWeights", value: "matricesWeights" }
+                    ];
+                    break;                
+        }
+
+        var modeOptions = [
+            { label: "User-defined", value: 0 }
+        ];
+
+        if (attributeOptions.length > 0) {
+            modeOptions.push({ label: "Mesh attribute", value: 1 });
+        }
+
+        if (systemValuesOptions.length > 0) {
+            modeOptions.push({ label: "System value", value: 2 });
+        }
+
+        var typeOptions = [
+            { label: "None", value: 0 },
+            { label: "Visible in the inspector", value: 1 },
+            { label: "Constant", value: 2 }
+        ];
+
+        return (
+            <div>
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                    {
+                        inputBlock.isUniform && !inputBlock.isSystemValue && inputBlock.animationType === AnimatedInputBlockTypes.None &&
+                        <OptionsLineComponent label="Type" options={typeOptions} target={inputBlock} 
+                            noDirectUpdate={true}
+                            getSelection={(block) => {
+                                if (block.visibleInInspector) {
+                                    return 1;
+                                }
+
+                                if (block.isConstant) {
+                                    return 2;
+                                }
+
+                                return 0;
+                            }}
+                            onSelect={(value: any) => {
+                                switch (value) {
+                                    case 0:
+                                        inputBlock.visibleInInspector = false;
+                                        inputBlock.isConstant = false;
+                                        break;
+                                    case 1:
+                                        inputBlock.visibleInInspector = true;
+                                        inputBlock.isConstant = false;
+                                        break;
+                                    case 2:
+                                        inputBlock.visibleInInspector = false;
+                                        inputBlock.isConstant = true;
+                                        break;
+                                }
+                                this.forceUpdate();
+                                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                                this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                            }} />                        
+                    }      
+                    {
+                        inputBlock.visibleInInspector &&
+                        <TextInputLineComponent globalState={this.props.globalState} label="Group" propertyName="groupInInspector" target={this.props.block} 
+                            onChange={() => {
+                                this.forceUpdate();
+                                this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                                this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                            }} />
+                    }     
+                    <OptionsLineComponent label="Mode" options={modeOptions} target={inputBlock} 
+                        noDirectUpdate={true}
+                        getSelection={(block) => {
+                            if (block.isAttribute) {
+                                return 1;
+                            }
+
+                            if (block.isSystemValue) {
+                                return 2;
+                            }
+
+                            return 0;
+                        }}
+                        onSelect={(value: any) => {
+                            switch (value) {
+                                case 0:
+                                    inputBlock.isUniform = true;
+                                    inputBlock.setAsSystemValue(null);
+                                    this.setDefaultValue();
+                                    break;
+                                case 1:
+                                    inputBlock.setAsAttribute(attributeOptions[0].value);
+                                    break;
+                                case 2:
+                                    inputBlock.setAsSystemValue(systemValuesOptions[0].value);
+                                    break;
+                            }
+                            this.forceUpdate();                            
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                        }} />
+                    {
+                        inputBlock.isAttribute &&
+                        <OptionsLineComponent label="Attribute" valuesAreStrings={true} options={attributeOptions} target={inputBlock} propertyName="name" onSelect={(value: any) => {
+                            inputBlock.setAsAttribute(value);
+                            this.forceUpdate();
+                            
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                        }} />
+                    }
+                    {
+                        inputBlock.isUniform && animationOptions.length > 0 &&
+                        <OptionsLineComponent label="Animation type" options={animationOptions} target={inputBlock} propertyName="animationType" onSelect={(value: any) => {
+                            this.forceUpdate();
+                            
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                        }} />
+                    }   
+                    {
+                        inputBlock.isUniform && !inputBlock.isSystemValue && inputBlock.animationType === AnimatedInputBlockTypes.None &&
+                        this.renderValue(this.props.globalState)
+                    }
+                    {
+                        inputBlock.isUniform && inputBlock.isSystemValue &&
+                        <OptionsLineComponent label="System value" options={systemValuesOptions} target={inputBlock} propertyName="systemValue" onSelect={(value: any) => {
+                            inputBlock.setAsSystemValue(value);
+                            this.forceUpdate();
+                            
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                            this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                        }} />
+                    }
+                </LineContainerComponent>
+            </div>
+        );
+    }
+}

+ 32 - 0
guiEditor/src/diagram/properties/lightInformationPropertyTabComponent.tsx

@@ -0,0 +1,32 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { LightInformationBlock } from 'babylonjs/Materials/Node/Blocks/Vertex/lightInformationBlock';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+
+export class LightInformationPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+
+    render() {
+        let scene = this.props.globalState.nodeMaterial!.getScene();
+        var lightOptions = scene.lights.map(l => {
+            return { label: l.name, value: l.name }
+        });
+        
+        let lightInformationBlock = this.props.block as LightInformationBlock;
+
+        return (
+            <div>               
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                    <OptionsLineComponent label="Light" noDirectUpdate={true} valuesAreStrings={true} options={lightOptions} target={lightInformationBlock} propertyName="name" onSelect={(name: any) => {
+                        lightInformationBlock.light = scene.getLightByName(name);
+                        this.forceUpdate();
+                        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                    }} />
+                </LineContainerComponent>
+            </div>
+        );
+    }
+}

+ 38 - 0
guiEditor/src/diagram/properties/lightPropertyTabComponent.tsx

@@ -0,0 +1,38 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { LightBlock } from 'babylonjs/Materials/Node/Blocks/Dual/lightBlock';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+
+export class LightPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+
+    render() {
+        let scene = this.props.globalState.nodeMaterial!.getScene();
+        var lightOptions = scene.lights.map(l => {
+            return { label: l.name, value: l.name }
+        });
+
+        lightOptions.splice(0, 0, { label: "All", value: "" })
+
+        let lightBlock = this.props.block as LightBlock;
+
+        return (
+            <div>                
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                    <OptionsLineComponent label="Light" defaultIfNull={0} noDirectUpdate={true} valuesAreStrings={true} options={lightOptions} target={lightBlock} propertyName="name" onSelect={(name: any) => {
+                        if (name === "") {
+                            lightBlock.light = null;
+                        } else {
+                            lightBlock.light = scene.getLightByName(name);
+                        }
+                        this.forceUpdate();
+                        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                    }} />
+                </LineContainerComponent>
+            </div>
+        );
+    }
+}

+ 62 - 0
guiEditor/src/diagram/properties/nodePortPropertyComponent.tsx

@@ -0,0 +1,62 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { GlobalState } from '../../globalState';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import {  GraphFrame } from '../graphFrame';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { NodePort } from '../nodePort';
+import { GraphNode } from '../graphNode';
+import { NodeLink } from '../nodeLink';
+import { FramePortData } from '../graphCanvas';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { TextLineComponent } from '../../sharedComponents/textLineComponent';
+
+export interface IFrameNodePortPropertyTabComponentProps {
+    globalState: GlobalState
+    nodePort: NodePort;
+}
+
+export class NodePortPropertyTabComponent extends React.Component<IFrameNodePortPropertyTabComponentProps> {
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphFrame | NodePort | GraphNode | NodeLink | FramePortData>>>;
+
+    constructor(props: IFrameNodePortPropertyTabComponentProps) {
+        super(props);
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+    }
+
+    toggleExposeOnFrame(value: boolean){
+        this.props.nodePort.exposedOnFrame = value;
+        this.props.globalState.onExposePortOnFrameObservable.notifyObservers(this.props.nodePort.node);
+    }
+
+    render() {
+
+        let info =  this.props.nodePort.hasLabel() ?
+            <>
+            {this.props.nodePort.hasLabel() && <TextInputLineComponent globalState={this.props.globalState} label="Port Label" propertyName="portName" target={this.props.nodePort} />}
+            {this.props.nodePort.node.enclosingFrameId !== -1 && <CheckBoxLineComponent label= "Expose Port on Frame" target={this.props.nodePort} isSelected={() => this.props.nodePort.exposedOnFrame} onSelect={(value: boolean) => this.toggleExposeOnFrame(value)}  propertyName="exposedOnFrame" disabled={this.props.nodePort.disabled} />}
+            </> :
+            <TextLineComponent label="This node is a constant input node and cannot be exposed to the frame." value=" " ></TextLineComponent>
+
+        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">
+                   {info}
+                </LineContainerComponent>
+                </div>
+            </div>
+        );
+    }
+}

+ 7 - 0
guiEditor/src/diagram/properties/propertyComponentProps.ts

@@ -0,0 +1,7 @@
+import { GlobalState } from "../../globalState";
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+
+export interface IPropertyComponentProps {
+    globalState: GlobalState;
+    block: NodeMaterialBlock;
+}

+ 317 - 0
guiEditor/src/diagram/properties/texturePropertyTabComponent.tsx

@@ -0,0 +1,317 @@
+
+import * as React from "react";
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+import { FileButtonLineComponent } from '../../sharedComponents/fileButtonLineComponent';
+import { Tools } from 'babylonjs/Misc/tools';
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { TextInputLineComponent } from '../../sharedComponents/textInputLineComponent';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { Texture } from 'babylonjs/Materials/Textures/texture';
+import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
+import { CubeTexture } from 'babylonjs/Materials/Textures/cubeTexture';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { ReflectionTextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/reflectionTextureBlock';
+import { ReflectionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/reflectionBlock';
+import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionBlock';
+import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/textureBlock';
+import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
+import { GeneralPropertyTabComponent, GenericPropertyTabComponent } from './genericNodePropertyComponent';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+
+type ReflectionTexture = ReflectionTextureBlock | ReflectionBlock | RefractionBlock;
+
+type AnyTexture = TextureBlock | ReflectionTexture | CurrentScreenBlock | ParticleTextureBlock;
+
+export class TexturePropertyTabComponent extends React.Component<IPropertyComponentProps, {isEmbedded: boolean, loadAsCubeTexture: boolean}> {
+
+    get textureBlock(): AnyTexture {
+        return this.props.block as AnyTexture;
+    }
+
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+
+        let texture = this.textureBlock.texture as BaseTexture;
+
+        this.state = {isEmbedded: !texture || texture.name.substring(0, 4) === "data", loadAsCubeTexture: texture && texture.isCube};
+    }
+
+    UNSAFE_componentWillUpdate(nextProps: IPropertyComponentProps, nextState: {isEmbedded: boolean, loadAsCubeTexture: boolean}) {
+        if (nextProps.block !== this.props.block) {
+            let texture = (nextProps.block as AnyTexture).texture as BaseTexture;
+
+            nextState.isEmbedded = !texture || texture.name.substring(0, 4) === "data";
+            nextState.loadAsCubeTexture = texture && texture.isCube;
+        }
+    }
+
+    private _generateRandomForCache() {
+        return 'xxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, (c) => {
+            var r = Math.random() * 10 | 0;
+            return r.toString();
+        });
+    }
+
+
+    updateAfterTextureLoad() {
+        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+        this.forceUpdate();
+    }
+
+    removeTexture() {
+        let texture = this.textureBlock.texture as BaseTexture;
+
+        if (texture) {
+            texture.dispose();
+            (texture as any) = null;
+            this.textureBlock.texture = null;
+        }
+
+        this.updateAfterTextureLoad();
+    }
+
+    _prepareTexture() {
+        let texture = this.textureBlock.texture as BaseTexture;
+
+        if (texture && texture.isCube !== this.state.loadAsCubeTexture) {
+            texture.dispose();
+            (texture as any) = null;
+        }
+
+        if (!texture) {
+            if (!this.state.loadAsCubeTexture) {
+                this.textureBlock.texture = new Texture(null, this.props.globalState.nodeMaterial.getScene(), false, 
+                    this.textureBlock instanceof ReflectionTextureBlock || this.textureBlock instanceof ReflectionBlock || this.textureBlock instanceof RefractionBlock || this.props.globalState.mode === NodeMaterialModes.PostProcess);
+                texture = this.textureBlock.texture;
+                texture.coordinatesMode = Texture.EQUIRECTANGULAR_MODE;
+            } else {
+                this.textureBlock.texture = new CubeTexture("", this.props.globalState.nodeMaterial.getScene());
+                texture = this.textureBlock.texture;
+                texture.coordinatesMode = Texture.CUBIC_MODE;
+            }
+        }  
+    }
+
+	/**
+	 * Replaces the texture of the node
+	 * @param file the file of the texture to use
+	 */
+    replaceTexture(file: File) {
+        this._prepareTexture();
+
+        let texture = this.textureBlock.texture as BaseTexture;
+        Tools.ReadFile(file, (data) => {
+            var blob = new Blob([data], { type: "octet/stream" });
+
+            var reader = new FileReader();
+            reader.readAsDataURL(blob); 
+            reader.onloadend = () => {
+                let base64data = reader.result as string;                
+
+                let extension: string | undefined = undefined;
+                if (file.name.toLowerCase().indexOf(".dds") > 0) {
+                    extension = ".dds";
+                } else if (file.name.toLowerCase().indexOf(".env") > 0) {
+                    extension = ".env";
+                }
+
+                (texture as Texture).updateURL(base64data, extension, () => this.updateAfterTextureLoad());
+            }
+        }, undefined, true);
+    }
+
+    replaceTextureWithUrl(url: string) {
+        this._prepareTexture();
+
+        let texture = this.textureBlock.texture as BaseTexture;       
+        if (texture.isCube || this.textureBlock instanceof ReflectionTextureBlock || this.textureBlock instanceof ReflectionBlock || this.textureBlock instanceof RefractionBlock) {
+            let extension: string | undefined = undefined;
+            if (url.toLowerCase().indexOf(".dds") > 0) {
+                extension = ".dds";
+            } else if (url.toLowerCase().indexOf(".env") > 0) {
+                extension = ".env";
+            }
+
+            (texture as Texture).updateURL(url, extension, () => this.updateAfterTextureLoad());
+        } else {
+            (texture as Texture).updateURL(url, null, () => this.updateAfterTextureLoad());
+        }
+    }
+
+    render() {
+        let url = "";
+
+        let texture = this.textureBlock.texture as BaseTexture;
+        if (texture && texture.name && texture.name.substring(0, 4) !== "data") {
+            url = texture.name;
+        }
+
+        url = url.replace(/\?nocache=\d+/, "");
+
+        let isInReflectionMode = this.textureBlock instanceof ReflectionTextureBlock || this.textureBlock instanceof ReflectionBlock || this.textureBlock instanceof RefractionBlock;
+        let isFrozenTexture = this.textureBlock instanceof CurrentScreenBlock || this.textureBlock instanceof ParticleTextureBlock;
+
+        var reflectionModeOptions: {label: string, value: number}[] = [
+            {
+                label: "Cubic", value: Texture.CUBIC_MODE
+            },
+            {                
+                label: "Equirectangular", value: Texture.EQUIRECTANGULAR_MODE
+            },
+            {
+                label: "Explicit", value: Texture.EXPLICIT_MODE
+            },
+            {
+                label: "Fixed equirectangular", value: Texture.FIXED_EQUIRECTANGULAR_MODE
+            },
+            {
+                label: "Fixed mirrored equirectangular", value: Texture.FIXED_EQUIRECTANGULAR_MIRRORED_MODE
+            },
+            {
+                label: "Planar", value: Texture.PLANAR_MODE
+            },              
+            {
+                label: "Projection", value: Texture.PROJECTION_MODE
+            },         
+            {
+                label: "Skybox", value: Texture.SKYBOX_MODE
+            },         
+            {
+                label: "Spherical", value: Texture.SPHERICAL_MODE
+            },
+        ];
+        
+        return (
+            <div>                
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                    <CheckBoxLineComponent label="Auto select UV" propertyName="autoSelectUV" target={this.props.block} onValueChanged={() => {                        
+                        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                    }}/> 
+                    {
+                        texture && !isInReflectionMode &&
+                        <CheckBoxLineComponent label="Convert to gamma space" propertyName="convertToGammaSpace" target={this.props.block} onValueChanged={() => {                        
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}/>
+                    }
+                    {
+                        texture && !isInReflectionMode &&
+                        <CheckBoxLineComponent label="Convert to linear space" propertyName="convertToLinearSpace" target={this.props.block} onValueChanged={() => {                        
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}/>
+                    }
+                    {
+                        texture && isInReflectionMode &&
+                        <OptionsLineComponent label="Reflection mode" options={reflectionModeOptions} target={texture} propertyName="coordinatesMode" onSelect={(value: any) => {
+                            texture.coordinatesMode = value;
+                            this.forceUpdate();
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }} />
+                    }                    
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <CheckBoxLineComponent label="Clamp U" isSelected={() => texture.wrapU === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => {
+                            texture.wrapU = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE;
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }} />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <CheckBoxLineComponent label="Clamp V" isSelected={() => texture.wrapV === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => {
+                            texture.wrapV = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE;
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }} />
+                    }        
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <FloatLineComponent globalState={this.props.globalState} label="Offset U" target={texture} propertyName="uOffset" 
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}
+                        />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <FloatLineComponent globalState={this.props.globalState} label="Offset V" target={texture} propertyName="vOffset"
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}
+                        />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <FloatLineComponent globalState={this.props.globalState} label="Scale U" target={texture} propertyName="uScale"
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }} />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <FloatLineComponent globalState={this.props.globalState} label="Scale V" target={texture} propertyName="vScale"
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }} />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <SliderLineComponent label="Rotation U" target={texture} globalState={this.props.globalState} propertyName="uAng" minimum={0} maximum={Math.PI * 2} useEuler={true} step={0.1}
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}
+                        />
+                    }
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <SliderLineComponent label="Rotation V" target={texture} globalState={this.props.globalState} propertyName="vAng" minimum={0} maximum={Math.PI * 2} useEuler={true} step={0.1}
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}
+                        />
+                    }                    
+                    {
+                        texture && !isInReflectionMode && !isFrozenTexture &&
+                        <SliderLineComponent label="Rotation W" target={texture} globalState={this.props.globalState} propertyName="wAng" minimum={0} maximum={Math.PI * 2} useEuler={true} step={0.1}
+                        onChange={() => {
+                            this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        }}
+                        />
+                    }
+                </LineContainerComponent>
+                <LineContainerComponent title="SOURCE">
+                    <CheckBoxLineComponent label="Embed static texture" isSelected={() => this.state.isEmbedded} onSelect={value => {
+                        this.setState({isEmbedded: value});
+                        this.textureBlock.texture = null;
+                        this.updateAfterTextureLoad();
+                    }}/>
+                    {
+                        isInReflectionMode &&
+                        <CheckBoxLineComponent label="Load as cube texture" isSelected={() => this.state.loadAsCubeTexture} 
+                            onSelect={value => this.setState({loadAsCubeTexture: value})}/> 
+                    }
+                    {
+                        this.state.isEmbedded &&
+                        <FileButtonLineComponent label="Upload" onClick={(file) => this.replaceTexture(file)} accept=".jpg, .png, .tga, .dds, .env" />
+                    }
+                    {
+                        !this.state.isEmbedded &&
+                        <TextInputLineComponent label="Link" globalState={this.props.globalState} value={url} onChange={newUrl => this.replaceTextureWithUrl(newUrl)}/>
+                    }
+                    {
+                        !this.state.isEmbedded && url &&
+                        <ButtonLineComponent label="Refresh" onClick={() => this.replaceTextureWithUrl(url + "?nocache=" + this._generateRandomForCache())}/>
+                    }
+                    {
+                        texture &&
+                        <ButtonLineComponent label="Remove" onClick={() => this.removeTexture()}/>
+                    }
+                </LineContainerComponent>
+                <GenericPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+            </div>
+        );
+    }
+}

+ 32 - 0
guiEditor/src/diagram/properties/transformNodePropertyComponent.tsx

@@ -0,0 +1,32 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { TransformBlock } from 'babylonjs/Materials/Node/Blocks/transformBlock';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+
+export class TransformPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props)
+    }
+
+    render() {
+        return (
+            <>                
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">
+                    <CheckBoxLineComponent label="Transform as direction" onSelect={value => {
+                        let transformBlock = this.props.block as TransformBlock;
+                        if (value) {
+                            transformBlock.complementW = 0;
+                        } else {
+                            transformBlock.complementW = 1;
+                        }
+                        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                    }} isSelected={() => (this.props.block as TransformBlock).complementW === 0} />
+                </LineContainerComponent>            
+            </>
+        );
+    }
+}

+ 55 - 0
guiEditor/src/diagram/properties/trigonometryNodePropertyComponent.tsx

@@ -0,0 +1,55 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
+import { TrigonometryBlockOperations, TrigonometryBlock } from 'babylonjs/Materials/Node/Blocks/trigonometryBlock';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+
+export class TrigonometryPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+
+    constructor(props: IPropertyComponentProps) {
+        super(props)
+    }
+
+    render() {
+        let trigonometryBlock = this.props.block as TrigonometryBlock;
+        
+        var operationOptions: {label: string, value: TrigonometryBlockOperations}[] = [
+            {label: "Cos", value: TrigonometryBlockOperations.Cos},
+            {label: "Sin", value: TrigonometryBlockOperations.Sin},
+            {label: "Abs", value: TrigonometryBlockOperations.Abs},
+            {label: "Exp", value: TrigonometryBlockOperations.Exp},
+            {label: "Exp2", value: TrigonometryBlockOperations.Exp2},
+            {label: "Round", value: TrigonometryBlockOperations.Round},
+            {label: "Ceiling", value: TrigonometryBlockOperations.Ceiling},
+            {label: "Floor", value: TrigonometryBlockOperations.Floor},
+            {label: "ArcCos", value: TrigonometryBlockOperations.ArcCos},
+            {label: "ArcSin", value: TrigonometryBlockOperations.ArcSin},
+            {label: "ArcTan", value: TrigonometryBlockOperations.ArcTan},
+            {label: "Tan", value: TrigonometryBlockOperations.Tan},
+            {label: "Log", value: TrigonometryBlockOperations.Log},
+            {label: "Fract", value: TrigonometryBlockOperations.Fract},
+            {label: "Sign", value: TrigonometryBlockOperations.Sign},
+            {label: "Radians to degrees", value: TrigonometryBlockOperations.Degrees},
+            {label: "Degrees to radians", value: TrigonometryBlockOperations.Radians}
+        ];
+
+        operationOptions.sort((a, b) => {
+            return a.label.localeCompare(b.label);
+        })
+        
+        return (
+            <div>                
+                <GeneralPropertyTabComponent globalState={this.props.globalState} block={this.props.block}/>
+                <LineContainerComponent title="PROPERTIES">  
+                    <OptionsLineComponent label="Operation" options={operationOptions} target={trigonometryBlock} propertyName="operation" onSelect={(value: any) => {
+                        this.props.globalState.onUpdateRequiredObservable.notifyObservers();
+                        this.props.globalState.onRebuildRequiredObservable.notifyObservers();
+                        this.forceUpdate();
+                    }} />                  
+                </LineContainerComponent>
+            </div>
+        );
+    }
+}

+ 26 - 0
guiEditor/src/diagram/propertyLedger.ts

@@ -0,0 +1,26 @@
+import { ComponentClass } from 'react';
+import { InputPropertyTabComponent } from './properties/inputNodePropertyComponent';
+import { IPropertyComponentProps } from './properties/propertyComponentProps';
+import { TransformPropertyTabComponent } from './properties/transformNodePropertyComponent';
+import { GradientPropertyTabComponent } from './properties/gradientNodePropertyComponent';
+import { LightPropertyTabComponent } from './properties/lightPropertyTabComponent';
+import { LightInformationPropertyTabComponent } from './properties/lightInformationPropertyTabComponent';
+import { TexturePropertyTabComponent } from './properties/texturePropertyTabComponent';
+import { TrigonometryPropertyTabComponent } from './properties/trigonometryNodePropertyComponent';
+
+export class PropertyLedger {
+    public static RegisteredControls: {[key: string] : ComponentClass<IPropertyComponentProps>} = {};
+}
+
+PropertyLedger.RegisteredControls["TransformBlock"] = TransformPropertyTabComponent;
+PropertyLedger.RegisteredControls["InputBlock"] = InputPropertyTabComponent;
+PropertyLedger.RegisteredControls["GradientBlock"] = GradientPropertyTabComponent;
+PropertyLedger.RegisteredControls["LightBlock"] = LightPropertyTabComponent;
+PropertyLedger.RegisteredControls["LightInformationBlock"] = LightInformationPropertyTabComponent;
+PropertyLedger.RegisteredControls["TextureBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["ReflectionTextureBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["ReflectionBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["RefractionBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["CurrentScreenBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["ParticleTextureBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["TrigonometryBlock"] = TrigonometryPropertyTabComponent;

+ 92 - 0
guiEditor/src/globalState.ts

@@ -0,0 +1,92 @@
+import { NodeMaterial } from "babylonjs/Materials/Node/nodeMaterial";
+import { Nullable } from "babylonjs/types";
+import { Observable } from 'babylonjs/Misc/observable';
+import { LogEntry } from './components/log/logComponent';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { PreviewType } from './components/preview/previewType';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { Color4 } from 'babylonjs/Maths/math.color';
+import { GraphNode } from './diagram/graphNode';
+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';
+import { FramePortData } from './diagram/graphCanvas';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+
+export class GlobalState {
+    nodeMaterial: NodeMaterial;
+    hostElement: HTMLElement;
+    hostDocument: HTMLDocument;
+    hostWindow: Window;
+    onSelectionChangedObservable = new Observable<Nullable<GraphNode | NodeLink | GraphFrame | NodePort | FramePortData>>();
+    onRebuildRequiredObservable = new Observable<void>();
+    onBuiltObservable = new Observable<void>();
+    onResetRequiredObservable = new Observable<void>();
+    onUpdateRequiredObservable = new Observable<void>();
+    onZoomToFitRequiredObservable = new Observable<void>();
+    onReOrganizedRequiredObservable = new Observable<void>();
+    onLogRequiredObservable = new Observable<LogEntry>();
+    onErrorMessageDialogRequiredObservable = new Observable<string>();
+    onIsLoadingChanged = new Observable<boolean>();
+    onPreviewCommandActivated = new Observable<boolean>();
+    onLightUpdated = new Observable<void>();
+    onPreviewBackgroundChanged = new Observable<void>();
+    onBackFaceCullingChanged = new Observable<void>();
+    onDepthPrePassChanged = new Observable<void>();
+    onAnimationCommandActivated = new Observable<void>();
+    onCandidateLinkMoved = new Observable<Nullable<Vector2>>();
+    onSelectionBoxMoved = new Observable<ClientRect | DOMRect>();
+    onFrameCreatedObservable = new Observable<GraphFrame>();
+    onCandidatePortSelectedObservable = new Observable<Nullable<NodePort | FrameNodePort>>();
+    onImportFrameObservable = new Observable<any>();
+    onGraphNodeRemovalObservable = new Observable<GraphNode>();
+    onGetNodeFromBlock: (block: NodeMaterialBlock) => GraphNode;
+    onGridSizeChanged = new Observable<void>();
+    onExposePortOnFrameObservable = new Observable<GraphNode>();
+    previewType: PreviewType;
+    previewFile: File;
+    listOfCustomPreviewFiles: File[] = [];
+    rotatePreview: boolean;
+    backgroundColor: Color4;
+    backFaceCulling: boolean;
+    depthPrePass: boolean;
+    blockKeyboardEvents = false;
+    hemisphericLight: boolean;
+    directionalLight0: boolean;
+    directionalLight1: boolean;
+    controlCamera: boolean;
+    storeEditorData: (serializationObject: any, frame?: Nullable<GraphFrame>) => void;
+    _mode: NodeMaterialModes;
+
+    /** Gets the mode */
+    public get mode(): NodeMaterialModes {
+        return this._mode;
+    }
+
+    /** Sets the mode */
+    public set mode(m: NodeMaterialModes) {
+        DataStorage.WriteNumber("Mode", m);
+        this._mode = m;
+        this.onPreviewCommandActivated.notifyObservers(true);
+    }
+
+    customSave?: {label: string, action: (data: string) => Promise<void>};
+
+    public constructor() {
+        this.previewType = DataStorage.ReadNumber("PreviewType", PreviewType.Box);
+        this.backFaceCulling = DataStorage.ReadBoolean("BackFaceCulling", true);
+        this.depthPrePass = DataStorage.ReadBoolean("DepthPrePass", false);
+        this.hemisphericLight = DataStorage.ReadBoolean("HemisphericLight", true);
+        this.directionalLight0 = DataStorage.ReadBoolean("DirectionalLight0", false);
+        this.directionalLight1 = DataStorage.ReadBoolean("DirectionalLight1", false);
+        this.controlCamera = DataStorage.ReadBoolean("ControlCamera", true);
+        this._mode = DataStorage.ReadNumber("Mode", NodeMaterialModes.Material);
+
+        let r = DataStorage.ReadNumber("BackgroundColorR", 0.12549019607843137);
+        let g = DataStorage.ReadNumber("BackgroundColorG", 0.09803921568627451);
+        let b = DataStorage.ReadNumber("BackgroundColorB", 0.25098039215686274);
+        this.backgroundColor = new Color4(r, g, b, 1.0);
+    }
+}

+ 900 - 0
guiEditor/src/graphEditor.tsx

@@ -0,0 +1,900 @@
+import * as React from "react";
+import { GlobalState } from './globalState';
+
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { NodeListComponent } from './components/nodeList/nodeListComponent';
+import { PropertyTabComponent } from './components/propertyTab/propertyTabComponent';
+import { Portal } from './portal';
+import { LogComponent, LogEntry } from './components/log/logComponent';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
+import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
+import { Nullable } from 'babylonjs/types';
+import { MessageDialogComponent } from './sharedComponents/messageDialog';
+import { BlockTools } from './blockTools';
+import { PreviewManager } from './components/preview/previewManager';
+import { IEditorData } from './nodeLocationInfo';
+import { PreviewMeshControlComponent } from './components/preview/previewMeshControlComponent';
+import { PreviewAreaComponent } from './components/preview/previewAreaComponent';
+import { SerializationTools } from './serializationTools';
+import { GraphCanvasComponent } from './diagram/graphCanvas';
+import { GraphNode } from './diagram/graphNode';
+import { GraphFrame } from './diagram/graphFrame';
+import * as ReactDOM from 'react-dom';
+import { IInspectorOptions } from "babylonjs/Debug/debugLayer";
+import { _TypeStore } from 'babylonjs/Misc/typeStore';
+
+
+require("./main.scss");
+
+interface IGraphEditorProps {
+    globalState: GlobalState;
+}
+
+interface IGraphEditorState {
+    showPreviewPopUp: boolean;
+};
+
+interface IInternalPreviewAreaOptions extends IInspectorOptions {
+    popup: boolean;
+    original: boolean;
+    explorerWidth?: string;
+    inspectorWidth?: string;
+    embedHostWidth?: string;
+}
+
+export class GraphEditor extends React.Component<IGraphEditorProps, IGraphEditorState> {
+    private readonly NodeWidth = 100;
+    private _graphCanvas: GraphCanvasComponent;
+
+    private _startX: number;
+    private _moveInProgress: boolean;
+
+    private _leftWidth = DataStorage.ReadNumber("LeftWidth", 200);
+    private _rightWidth = DataStorage.ReadNumber("RightWidth", 300);
+
+    private _blocks = new Array<NodeMaterialBlock>();
+
+    private _previewManager: PreviewManager;
+    private _copiedNodes: GraphNode[] = [];
+    private _copiedFrame: Nullable<GraphFrame> = null;
+    private _mouseLocationX = 0;
+    private _mouseLocationY = 0;
+    private _onWidgetKeyUpPointer: any;
+
+    private _previewHost: Nullable<HTMLElement>;
+    private _popUpWindow: Window;
+
+    /**
+     * Creates a node and recursivly creates its parent nodes from it's input
+     * @param nodeMaterialBlock 
+     */
+    public createNodeFromObject(block: NodeMaterialBlock, recursion = true) {
+        if (this._blocks.indexOf(block) !== -1) {        
+            return this._graphCanvas.nodes.filter(n => n.block === block)[0];
+        }
+
+        this._blocks.push(block);
+
+        if (this.props.globalState.nodeMaterial!.attachedBlocks.indexOf(block) === -1) {
+            this.props.globalState.nodeMaterial!.attachedBlocks.push(block);
+        }
+
+        if (block.isFinalMerger) {
+            this.props.globalState.nodeMaterial!.addOutputNode(block);
+        }
+
+        // Connections
+        if (block.inputs.length) {
+            for (var input of block.inputs) {
+                if (input.isConnected && recursion) {
+                    this.createNodeFromObject(input.sourceBlock!);
+                }
+            }
+        }
+
+        // Graph
+        const node = this._graphCanvas.appendBlock(block);
+
+        // Links
+        if (block.inputs.length && recursion) {
+            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(newInputBlock);
+    }
+
+    componentDidMount() {
+        if (this.props.globalState.hostDocument) {
+            this._graphCanvas = (this.refs["graphCanvas"] as GraphCanvasComponent);
+            this._previewManager = new PreviewManager(this.props.globalState.hostDocument.getElementById("preview-canvas") as HTMLCanvasElement, this.props.globalState);
+        }
+
+        if (navigator.userAgent.indexOf("Mobile") !== -1) {
+            ((this.props.globalState.hostDocument || document).querySelector(".blocker") as HTMLElement).style.visibility = "visible";
+        }
+
+        this.build();
+    }
+
+    componentWillUnmount() {
+        if (this.props.globalState.hostDocument) {
+            this.props.globalState.hostDocument!.removeEventListener("keyup", this._onWidgetKeyUpPointer, false);
+        }
+
+        if (this._previewManager) {
+            this._previewManager.dispose();
+        }
+    }
+
+    constructor(props: IGraphEditorProps) {
+        super(props);
+
+        this.state = {
+            showPreviewPopUp: false
+        };
+
+        this.props.globalState.onRebuildRequiredObservable.add(() => {
+            if (this.props.globalState.nodeMaterial) {
+                this.buildMaterial();
+            }
+        });
+
+        this.props.globalState.onResetRequiredObservable.add(() => {
+            this.build();
+            if (this.props.globalState.nodeMaterial) {
+                this.buildMaterial();
+            }
+        });
+
+        this.props.globalState.onImportFrameObservable.add((source: any) => {
+            const frameData = source.editorData.frames[0];
+
+            // create new graph nodes for only blocks from frame (last blocks added)
+            this.props.globalState.nodeMaterial.attachedBlocks.slice(-(frameData.blocks.length)).forEach((block: NodeMaterialBlock) => {
+                this.createNodeFromObject(block);
+            });
+            this._graphCanvas.addFrame(frameData);
+            this.reOrganize(this.props.globalState.nodeMaterial.editorData, true);
+        })
+
+        this.props.globalState.onZoomToFitRequiredObservable.add(() => {
+            this.zoomToFit();
+        });
+
+        this.props.globalState.onReOrganizedRequiredObservable.add(() => {
+            this.reOrganize();
+        });
+
+        this.props.globalState.onGetNodeFromBlock = (block) => {
+             return this._graphCanvas.findNodeFromBlock(block);
+        }
+
+        this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
+            if ((evt.keyCode === 46 || evt.keyCode === 8) && !this.props.globalState.blockKeyboardEvents) { // Delete                
+                let selectedItems = this._graphCanvas.selectedNodes;
+
+                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);
+                    }                                  
+                }
+
+                if (this._graphCanvas.selectedLink) {
+                    this._graphCanvas.selectedLink.dispose();
+                }
+
+                if (this._graphCanvas.selectedFrame) {
+                    var frame = this._graphCanvas.selectedFrame;
+                    
+                    if(frame.isCollapsed) {
+                        while(frame.nodes.length > 0) {
+                            let targetBlock = frame.nodes[0].block;
+                            this.props.globalState.nodeMaterial!.removeBlock(targetBlock);
+                            let blockIndex = this._blocks.indexOf(targetBlock);
+        
+                            if (blockIndex > -1) {
+                                this._blocks.splice(blockIndex, 1);
+                            }
+                            frame.nodes[0].dispose();            
+                        }
+                        frame.isCollapsed = false;
+                    }
+                    else {
+                        frame.nodes.forEach(node => {
+                            node.enclosingFrameId = -1;
+                        });
+                    }
+                    this._graphCanvas.selectedFrame.dispose();
+                }
+
+                this.props.globalState.onSelectionChangedObservable.notifyObservers(null);  
+                this.props.globalState.onRebuildRequiredObservable.notifyObservers();  
+                return;
+            }
+
+            if (!evt.ctrlKey || this.props.globalState.blockKeyboardEvents) {
+                return;
+            }
+
+            if (evt.key === "c") { // Copy
+                this._copiedNodes = [];
+                this._copiedFrame = null;
+
+                if (this._graphCanvas.selectedFrame) {
+                    this._copiedFrame = this._graphCanvas.selectedFrame;
+                    return;
+                }
+
+                let selectedItems = this._graphCanvas.selectedNodes;
+                if (!selectedItems.length) {
+                    return;
+                }
+    
+                let selectedItem = selectedItems[0] as GraphNode;
+    
+                if (!selectedItem.block) {
+                    return;
+                }
+
+                this._copiedNodes = selectedItems.slice(0);
+            } else if (evt.key === "v") { // Paste
+                const rootElement = this.props.globalState.hostDocument!.querySelector(".diagram-container") as HTMLDivElement;
+                const zoomLevel = this._graphCanvas.zoom;
+                let currentY = (this._mouseLocationY - rootElement.offsetTop - this._graphCanvas.y - 20) / zoomLevel;
+
+                if (this._copiedFrame) {                    
+                    // New frame
+                    let newFrame = new GraphFrame(null, this._graphCanvas, true);
+                    this._graphCanvas.frames.push(newFrame);
+
+                    newFrame.width = this._copiedFrame.width;
+                    newFrame.height = this._copiedFrame.height;newFrame.width / 2
+                    newFrame.name = this._copiedFrame.name;
+                    newFrame.color = this._copiedFrame.color;
+
+                    let currentX = (this._mouseLocationX - rootElement.offsetLeft - this._graphCanvas.x) / zoomLevel;
+                    newFrame.x = currentX - newFrame.width / 2;
+                    newFrame.y = currentY;
+
+                    // Paste nodes
+                    if (this._copiedFrame.nodes.length) {
+                        currentX = newFrame.x + this._copiedFrame.nodes[0].x - this._copiedFrame.x;
+                        currentY = newFrame.y + this._copiedFrame.nodes[0].y - this._copiedFrame.y;
+                        
+                        this._graphCanvas._frameIsMoving = true;
+                        let newNodes = this.pasteSelection(this._copiedFrame.nodes, currentX, currentY);       
+                        if (newNodes) {           
+                            for (var node of newNodes) {
+                                newFrame.syncNode(node);
+                            }
+                        }
+                        this._graphCanvas._frameIsMoving = false;
+                    }
+
+                    if (this._copiedFrame.isCollapsed) {
+                        newFrame.isCollapsed = true;
+                    }
+
+                    // Select
+                    this.props.globalState.onSelectionChangedObservable.notifyObservers(newFrame);
+                    return;
+                }
+
+                if (!this._copiedNodes.length) {
+                    return;
+                }
+
+                let currentX = (this._mouseLocationX - rootElement.offsetLeft - this._graphCanvas.x - this.NodeWidth) / zoomLevel;
+                this.pasteSelection(this._copiedNodes, currentX, currentY, true);
+            }
+
+        }, false);
+    }
+
+    reconnectNewNodes(nodeIndex: number, newNodes:GraphNode[], sourceNodes:GraphNode[], done: boolean[]) {
+        if (done[nodeIndex]) {
+            return;
+        }
+
+        const currentNode = newNodes[nodeIndex];
+        const block = currentNode.block;
+        const sourceNode = sourceNodes[nodeIndex];
+
+        for (var inputIndex = 0; inputIndex < sourceNode.block.inputs.length; inputIndex++) {
+            let sourceInput = sourceNode.block.inputs[inputIndex];
+            const currentInput = block.inputs[inputIndex];
+            if (!sourceInput.isConnected) {
+                continue;
+            }
+            const sourceBlock = sourceInput.connectedPoint!.ownerBlock;
+            const activeNodes = sourceNodes.filter(s => s.block === sourceBlock);
+
+            if (activeNodes.length > 0) {
+                const activeNode = activeNodes[0];
+                let indexInList = sourceNodes.indexOf(activeNode);
+
+                // First make sure to connect the other one
+                this.reconnectNewNodes(indexInList, newNodes, sourceNodes, done);
+
+                // Then reconnect
+                const outputIndex = sourceBlock.outputs.indexOf(sourceInput.connectedPoint!);
+                const newOutput = newNodes[indexInList].block.outputs[outputIndex];
+
+                newOutput.connectTo(currentInput);
+            } else {
+                // Connect with outside blocks
+                sourceInput._connectedPoint!.connectTo(currentInput);
+            }
+
+            this._graphCanvas.connectPorts(currentInput.connectedPoint!, currentInput);
+        }
+
+        currentNode.refresh();
+
+        done[nodeIndex] = true;
+    }
+
+    pasteSelection(copiedNodes: GraphNode[], currentX: number, currentY: number, selectNew = false) {
+
+        let originalNode: Nullable<GraphNode> = null;
+
+        let newNodes:GraphNode[] = [];
+
+        // Copy to prevent recursive side effects while creating nodes.
+        copiedNodes = copiedNodes.slice();
+
+        // Cancel selection        
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+
+        // Create new nodes
+        for (var node of copiedNodes) {
+            let block = node.block;
+
+            if (!block) {
+                continue;
+            }
+
+            let clone = block.clone(this.props.globalState.nodeMaterial.getScene());
+
+            if (!clone) {
+                return;
+            }
+
+            let newNode = this.createNodeFromObject(clone, false);
+
+            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;
+            newNode.cleanAccumulation();
+
+            newNodes.push(newNode);
+
+            if (selectNew) {
+                this.props.globalState.onSelectionChangedObservable.notifyObservers(newNode);
+            }
+        }
+
+        // Relink
+        let done = new Array<boolean>(newNodes.length);
+        for (var index = 0; index < newNodes.length; index++) {
+            this.reconnectNewNodes(index, newNodes, copiedNodes, done);
+        }
+
+        return newNodes;
+    }
+
+    zoomToFit() {
+        this._graphCanvas.zoomToFit();
+    }
+
+    buildMaterial() {
+        if (!this.props.globalState.nodeMaterial) {
+            return;
+        }
+
+        try {
+            this.props.globalState.nodeMaterial.options.emitComments = true;
+            this.props.globalState.nodeMaterial.build(true);
+            this.props.globalState.onLogRequiredObservable.notifyObservers(new LogEntry("Node material build successful", false));
+        }
+        catch (err) {
+            this.props.globalState.onLogRequiredObservable.notifyObservers(new LogEntry(err, true));
+        }
+
+        SerializationTools.UpdateLocations(this.props.globalState.nodeMaterial, this.props.globalState);
+
+        this.props.globalState.onBuiltObservable.notifyObservers();
+    }
+
+    build() {        
+        let editorData = this.props.globalState.nodeMaterial.editorData;        
+        this._graphCanvas._isLoading = true; // Will help loading large graphes
+
+        if (editorData instanceof Array) {
+            editorData = {
+                locations: editorData
+            }
+        }
+
+        // setup the diagram model
+        this._blocks = [];
+        this._graphCanvas.reset();
+
+        // Load graph of nodes from the material
+        if (this.props.globalState.nodeMaterial) {
+            this.loadGraph()
+        }
+
+        this.reOrganize(editorData);
+    }
+
+    loadGraph() {
+        var material = this.props.globalState.nodeMaterial;
+        material._vertexOutputNodes.forEach((n: any) => {
+            this.createNodeFromObject(n, true);
+        });
+        material._fragmentOutputNodes.forEach((n: any) => {
+            this.createNodeFromObject(n, true);
+        });
+
+        material.attachedBlocks.forEach((n: any) => {
+            this.createNodeFromObject(n, true);
+        });
+
+        // Links
+        material.attachedBlocks.forEach((n: any) => {
+            if (n.inputs.length) {
+                for (var input of n.inputs) {
+                    if (input.isConnected) {
+                        this._graphCanvas.connectPorts(input.connectedPoint!, input);
+                    }
+                }
+            }
+        });           
+    }
+
+    showWaitScreen() {
+        this.props.globalState.hostDocument.querySelector(".wait-screen")?.classList.remove("hidden");
+    }
+
+    hideWaitScreen() {
+        this.props.globalState.hostDocument.querySelector(".wait-screen")?.classList.add("hidden");
+    }
+
+    reOrganize(editorData: Nullable<IEditorData> = null, isImportingAFrame = false) {
+        this.showWaitScreen();
+        this._graphCanvas._isLoading = true; // Will help loading large graphes
+
+        setTimeout(() => {
+            if (!editorData || !editorData.locations) {
+                this._graphCanvas.distributeGraph();
+            } else {
+                // Locations
+                for (var location of editorData.locations) {
+                    for (var node of this._graphCanvas.nodes) {
+                        if (node.block && node.block.uniqueId === location.blockId) {
+                            node.x = location.x;
+                            node.y = location.y;
+                            node.cleanAccumulation();
+                            break;
+                        }
+                    }
+                }
+                
+                if (!isImportingAFrame){
+                    this._graphCanvas.processEditorData(editorData);
+                }
+            }
+
+            this._graphCanvas._isLoading = false;
+            for (var node of this._graphCanvas.nodes) {
+                node._refreshLinks();
+            }
+            this.hideWaitScreen();
+        });
+    }
+
+    onPointerDown(evt: React.PointerEvent<HTMLDivElement>) {
+        this._startX = evt.clientX;
+        this._moveInProgress = true;
+        evt.currentTarget.setPointerCapture(evt.pointerId);
+    }
+
+    onPointerUp(evt: React.PointerEvent<HTMLDivElement>) {
+        this._moveInProgress = false;
+        evt.currentTarget.releasePointerCapture(evt.pointerId);
+    }
+
+    resizeColumns(evt: React.PointerEvent<HTMLDivElement>, forLeft = true) {
+        if (!this._moveInProgress) {
+            return;
+        }
+
+        const deltaX = evt.clientX - this._startX;
+        const rootElement = evt.currentTarget.ownerDocument!.getElementById("node-editor-graph-root") as HTMLDivElement;
+
+        if (forLeft) {
+            this._leftWidth += deltaX;
+            this._leftWidth = Math.max(150, Math.min(400, this._leftWidth));
+            DataStorage.WriteNumber("LeftWidth", this._leftWidth);
+        } else {
+            this._rightWidth -= deltaX;
+            this._rightWidth = Math.max(250, Math.min(500, this._rightWidth));
+            DataStorage.WriteNumber("RightWidth", this._rightWidth);
+            rootElement.ownerDocument!.getElementById("preview")!.style.height = this._rightWidth + "px";
+        }
+
+        rootElement.style.gridTemplateColumns = this.buildColumnLayout();
+
+        this._startX = evt.clientX;
+    }
+
+    buildColumnLayout() {
+        return `${this._leftWidth}px 4px calc(100% - ${this._leftWidth + 8 + this._rightWidth}px) 4px ${this._rightWidth}px`;
+    }
+
+    emitNewBlock(event: React.DragEvent<HTMLDivElement>) {
+        var data = event.dataTransfer.getData("babylonjs-material-node") as string;
+        let newNode: GraphNode;
+        
+        if(data.indexOf("Custom") > -1) {
+            let storageData = localStorage.getItem(data);
+            if(storageData) {   
+                let frameData = JSON.parse(storageData);
+
+                //edit position before loading.
+                let newX = (event.clientX - event.currentTarget.offsetLeft - this._graphCanvas.x - this.NodeWidth) / this._graphCanvas.zoom;
+                let newY = (event.clientY - event.currentTarget.offsetTop - this._graphCanvas.y - 20) / this._graphCanvas.zoom;;
+                let oldX = frameData.editorData.frames[0].x;
+                let oldY = frameData.editorData.frames[0].y;
+                frameData.editorData.frames[0].x = newX;
+                frameData.editorData.frames[0].y = newY;
+                for (var location of frameData.editorData.locations) {
+                    location.x +=  newX - oldX;
+                    location.y +=  newY - oldY;       
+                }
+
+                SerializationTools.AddFrameToMaterial(frameData, this.props.globalState, this.props.globalState.nodeMaterial); 
+                this._graphCanvas.frames[this._graphCanvas.frames.length -1].cleanAccumulation();
+                this.forceUpdate();
+                return;
+            }
+        }
+
+        if (data.indexOf("Block") === -1) {
+            newNode = this.addValueNode(data);
+        } 
+        else {
+            let block = BlockTools.GetBlockFromString(data, this.props.globalState.nodeMaterial.getScene(), this.props.globalState.nodeMaterial)!;   
+            
+            if (block.isUnique) {
+                const className = block.getClassName();
+                for (var other of this._blocks) {
+                    if (other !== block && other.getClassName() === className) {
+                        this.props.globalState.onErrorMessageDialogRequiredObservable.notifyObservers(`You can only have one ${className} per graph`);                                
+                        return;
+                    }
+                }
+            } 
+
+            block.autoConfigure(this.props.globalState.nodeMaterial);       
+            newNode = this.createNodeFromObject(block);
+        };
+
+        let x = event.clientX - event.currentTarget.offsetLeft - this._graphCanvas.x - this.NodeWidth;
+        let y = event.clientY - event.currentTarget.offsetTop - this._graphCanvas.y - 20;
+        
+        newNode.x = x / this._graphCanvas.zoom;
+        newNode.y = y / this._graphCanvas.zoom;
+        newNode.cleanAccumulation();
+
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(newNode);
+
+        let block = newNode.block;
+
+        x -= this.NodeWidth + 150;
+
+        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];
+
+                if (connectedNode.x === 0 && connectedNode.y === 0) {
+                    connectedNode.x = x / this._graphCanvas.zoom; 
+                    connectedNode.y = y / this._graphCanvas.zoom;
+                    connectedNode.cleanAccumulation();
+                    y += 80;
+                }
+            }
+        });
+
+        this.forceUpdate();
+    }
+
+    handlePopUp = () => {
+        this.setState({
+            showPreviewPopUp : true
+        });
+        this.createPopUp();
+        this.props.globalState.hostWindow.addEventListener('beforeunload', this.handleClosingPopUp);
+    }
+
+    handleClosingPopUp = () => {
+        this._previewManager.dispose();
+        this._popUpWindow.close();
+        this.setState({
+            showPreviewPopUp: false
+        }, () => this.initiatePreviewArea()
+        );
+    }
+
+    initiatePreviewArea = (canvas: HTMLCanvasElement = this.props.globalState.hostDocument.getElementById("preview-canvas") as HTMLCanvasElement) => {
+        this._previewManager =  new PreviewManager(canvas, this.props.globalState);
+    }
+
+    createPopUp = () => {
+        const userOptions = {
+            original: true,
+            popup: true,
+            overlay: false,
+            embedMode: false,
+            enableClose: true,
+            handleResize: true,
+            enablePopup: true,
+
+        };
+        const options = {
+            embedHostWidth: "100%",
+            ...userOptions
+        };
+        const popUpWindow = this.createPopupWindow("PREVIEW AREA", "_PreviewHostWindow");
+        if (popUpWindow) {
+            popUpWindow.addEventListener('beforeunload',  this.handleClosingPopUp);
+            const parentControl = popUpWindow.document.getElementById('node-editor-graph-root');
+            this.createPreviewMeshControlHost(options, parentControl);
+            this.createPreviewHost(options, parentControl);
+            if (parentControl) {
+                this.fixPopUpStyles(parentControl.ownerDocument!);
+                this.initiatePreviewArea(parentControl.ownerDocument!.getElementById("preview-canvas") as HTMLCanvasElement);
+            }
+        }
+    }
+
+    createPopupWindow = (title: string, windowVariableName: string, width = 500, height = 500): Window | null => {
+        const windowCreationOptionsList = {
+            width: width,
+            height: height,
+            top: (this.props.globalState.hostWindow.innerHeight - width) / 2 + window.screenY,
+            left: (this.props.globalState.hostWindow.innerWidth - height) / 2 + window.screenX
+        };
+
+        var windowCreationOptions = Object.keys(windowCreationOptionsList)
+            .map(
+                (key) => key + '=' + (windowCreationOptionsList as any)[key]
+            )
+            .join(',');
+
+        const popupWindow = this.props.globalState.hostWindow.open("", title, windowCreationOptions);
+        if (!popupWindow) {
+            return null;
+        }
+
+        const parentDocument = popupWindow.document;
+
+        parentDocument.title = title;
+        parentDocument.body.style.width = "100%";
+        parentDocument.body.style.height = "100%";
+        parentDocument.body.style.margin = "0";
+        parentDocument.body.style.padding = "0";
+
+        let parentControl = parentDocument.createElement("div");
+        parentControl.style.width = "100%";
+        parentControl.style.height = "100%";
+        parentControl.style.margin = "0";
+        parentControl.style.padding = "0";
+        parentControl.style.display = "grid";
+        parentControl.style.gridTemplateRows = "40px auto";
+        parentControl.id = 'node-editor-graph-root';
+        parentControl.className = 'right-panel';
+
+        popupWindow.document.body.appendChild(parentControl);
+
+        this.copyStyles(this.props.globalState.hostWindow.document, parentDocument);
+
+        (this as any)[windowVariableName] = popupWindow;
+
+        this._popUpWindow = popupWindow;
+
+        return popupWindow;
+    }
+
+    copyStyles = (sourceDoc: HTMLDocument, targetDoc: HTMLDocument) => {
+        const styleContainer = [];
+        for (var index = 0; index < sourceDoc.styleSheets.length; index++) {
+            var styleSheet: any = sourceDoc.styleSheets[index];
+            try {
+                if (styleSheet.href) { // for <link> elements loading CSS from a URL
+                    const newLinkEl = sourceDoc.createElement('link');
+
+                    newLinkEl.rel = 'stylesheet';
+                    newLinkEl.href = styleSheet.href;
+                    targetDoc.head!.appendChild(newLinkEl);
+                    styleContainer.push(newLinkEl);
+                }
+                else if (styleSheet.cssRules) { // for <style> elements
+                    const newStyleEl = sourceDoc.createElement('style');
+
+                    for (var cssRule of styleSheet.cssRules) {
+                        newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
+                    }
+
+                    targetDoc.head!.appendChild(newStyleEl);
+                    styleContainer.push(newStyleEl);
+                } 
+            } catch (e) {
+                console.log(e);
+            }
+        }
+    }
+
+    createPreviewMeshControlHost = (options: IInternalPreviewAreaOptions, parentControl: Nullable<HTMLElement>) => {
+        // Prepare the preview control host
+        if (parentControl) {
+
+            const host = parentControl.ownerDocument!.createElement("div");
+
+            host.id = "PreviewMeshControl-host";
+            host.style.width = options.embedHostWidth || "auto";
+
+            parentControl.appendChild(host);
+            const PreviewMeshControlComponentHost = React.createElement(PreviewMeshControlComponent, {
+                globalState: this.props.globalState,
+                togglePreviewAreaComponent: this.handlePopUp
+            });
+            ReactDOM.render(PreviewMeshControlComponentHost, host);
+        }
+    }
+
+    createPreviewHost = (options: IInternalPreviewAreaOptions, parentControl: Nullable<HTMLElement>) => {
+        // Prepare the preview host
+        if (parentControl) {
+            const host = parentControl.ownerDocument!.createElement("div");
+
+            host.id = "PreviewAreaComponent-host";
+            host.style.width = options.embedHostWidth || "auto";
+            host.style.display = "grid";
+            host.style.gridRow = '2';
+            host.style.gridTemplateRows = "auto 40px";
+
+            parentControl.appendChild(host);
+
+            this._previewHost = host;
+
+            if (!options.overlay) {
+                this._previewHost.style.position = "relative";
+            }
+        }
+
+        if (this._previewHost) {
+            const PreviewAreaComponentHost = React.createElement(PreviewAreaComponent, {
+                globalState: this.props.globalState,
+                width: 200
+            });
+            ReactDOM.render(PreviewAreaComponentHost, this._previewHost);
+        }
+    }
+
+    fixPopUpStyles = (document: Document) => {
+        const previewContainer = document.getElementById("preview");
+        if (previewContainer) {
+            previewContainer.style.height = "auto";
+            previewContainer.style.gridRow = "1";
+        }
+        const previewConfigBar = document.getElementById("preview-config-bar");
+        if (previewConfigBar) {
+            previewConfigBar.style.gridRow = "2";
+        }
+        const newWindowButton = document.getElementById('preview-new-window');
+        if (newWindowButton) {
+            newWindowButton.style.display = 'none';
+        }
+        const previewMeshBar = document.getElementById('preview-mesh-bar');
+        if (previewMeshBar) {
+            previewMeshBar.style.gridTemplateColumns = "auto 1fr 40px 40px";
+        }
+    }
+
+    render() {
+        return (
+            <Portal globalState={this.props.globalState}>
+                <div id="node-editor-graph-root" style={
+                    {
+                        gridTemplateColumns: this.buildColumnLayout()
+                    }}
+                    onMouseMove={evt => {                
+                        this._mouseLocationX = evt.pageX;
+                        this._mouseLocationY = evt.pageY;
+                    }}
+                    onMouseDown={(evt) => {
+                        if ((evt.target as HTMLElement).nodeName === "INPUT") {
+                            return;
+                        }
+                        this.props.globalState.blockKeyboardEvents = false;
+                    }}
+                    >
+                    {/* Node creation menu */}
+                    <NodeListComponent globalState={this.props.globalState} />
+
+                    <div id="leftGrab"
+                        onPointerDown={evt => this.onPointerDown(evt)}
+                        onPointerUp={evt => this.onPointerUp(evt)}
+                        onPointerMove={evt => this.resizeColumns(evt)}
+                    ></div>
+
+                    {/* The node graph diagram */}
+                    <div className="diagram-container"
+                        onDrop={event => {
+                            this.emitNewBlock(event);
+                        }}
+                        onDragOver={event => {
+                            event.preventDefault();
+                        }}
+                    >                        
+                        <GraphCanvasComponent ref={"graphCanvas"} globalState={this.props.globalState}/>
+                    </div>
+
+                    <div id="rightGrab"
+                        onPointerDown={evt => this.onPointerDown(evt)}
+                        onPointerUp={evt => this.onPointerUp(evt)}
+                        onPointerMove={evt => this.resizeColumns(evt, false)}
+                    ></div>
+
+                    {/* Property tab */}
+                    <div className="right-panel">
+                        <PropertyTabComponent globalState={this.props.globalState} />
+                        {!this.state.showPreviewPopUp ? <PreviewMeshControlComponent globalState={this.props.globalState} togglePreviewAreaComponent={this.handlePopUp} /> : null }
+                        {!this.state.showPreviewPopUp ? <PreviewAreaComponent globalState={this.props.globalState} width={this._rightWidth} /> : null}
+                    </div>
+
+                    <LogComponent globalState={this.props.globalState} />
+                </div>                
+                <MessageDialogComponent globalState={this.props.globalState} />
+                <div className="blocker">
+                    Node Material Editor runs only on desktop
+                </div>
+                <div className="wait-screen hidden">
+                    Processing...please wait
+                </div>
+            </Portal>
+        );
+    }
+}

+ 1 - 0
guiEditor/src/index.ts

@@ -0,0 +1 @@
+export * from "./nodeEditor";

+ 9 - 0
guiEditor/src/legacy/legacy.ts

@@ -0,0 +1,9 @@
+import { NodeEditor } from "../index";
+
+var globalObject = (typeof global !== 'undefined') ? global : ((typeof window !== 'undefined') ? window : undefined);
+if (typeof globalObject !== "undefined") {
+    (<any>globalObject).BABYLON = (<any>globalObject).BABYLON || {};
+    (<any>globalObject).BABYLON.NodeEditor = NodeEditor;
+}
+
+export * from "../index";

+ 387 - 0
guiEditor/src/main.scss

@@ -0,0 +1,387 @@
+#node-editor-graph-root {
+    display: grid;
+    grid-template-rows: calc(100% - 120px) 120px;
+    height: 100%;
+    width: 100%;
+    background: #464646;
+    font: 14px "acumin-pro";   
+}
+
+.wait-screen {
+    display: grid;
+    justify-content: center;
+    align-content: center;
+    height: 100%;
+    width: 100%;
+    background: #464646;
+    opacity: 0.95;
+    color:white;
+    font: 24px "acumin-pro";  
+    position: absolute;
+    top: 0;
+    left: 0; 
+
+    &.hidden {
+        visibility: hidden;
+    }
+}
+
+#nodeList {
+    grid-row: 1 / span 2;
+    grid-column: 1;
+}
+
+#leftGrab {
+    grid-row: 1 / span 2;
+    grid-column: 2;
+    cursor: ew-resize;
+}
+
+#rightGrab {
+    grid-row: 1 / span 2;
+    grid-column: 4;
+    cursor: ew-resize;
+}
+
+.diagram-container {
+    grid-row: 1;
+    grid-column: 3;
+    background: #5f5b60;
+    width: 100%;
+    height: 100%;
+
+    .diagram {
+        display: none;
+        width: 100%;
+        height: 100%;
+    }
+}
+
+.right-panel {
+    grid-row: 1 / span 2;
+    grid-column: 5;
+    display: grid;
+    grid-template-rows: 1fr 40px auto 40px;
+    grid-template-columns: 100%;
+    height: 100%;
+    overflow-y: auto;
+
+    #propertyTab {
+        grid-row: 1;
+        grid-column: 1;
+    }        
+    
+    .button {
+        display: grid;
+        justify-content: center;
+        align-content: center;
+        height: auto;
+        width: calc(100% / 7);
+        cursor: pointer;
+
+        &:hover {
+            background: rgb(51, 122, 183);
+            color: white;
+            opacity: 0.8;
+        }
+
+        &.selected {
+            background: rgb(51, 122, 183);
+            color: white;
+        }
+        
+        &.align {
+            justify-content: stretch;
+            text-align: center;
+        }
+    }    
+
+    #preview-mesh-bar {
+        grid-row: 2;
+        grid-column: 1;
+        display: grid;
+        grid-template-columns: auto 1fr 40px 40px 40px;
+        align-items: center;
+        font-size: 18px;
+        background-color: #555555;
+
+        #file-picker {
+            display: none;
+        }
+
+        .listLine {
+            grid-column: 1;
+            height: 40px;
+            display: grid;
+            grid-template-columns: 0px 1fr;  
+    
+            .label {
+                grid-column: 1;
+                display: flex;
+                align-items: center;
+                font-size: 14px;
+            }
+    
+            .options {
+                grid-column: 2;
+                
+                display: flex;
+                align-items: center;   
+                margin-left: 5px;
+    
+                select {
+                    width: 115px;
+                }
+            } 
+        }
+
+        .button{
+            color: #ffffff;
+            width: 40px;
+            height: 40px;
+            transform-origin: 50% 50%;
+            
+            &:active {
+                transform: scale(0.90);
+            }
+
+            &:hover {
+                background: #3f3461;
+            }
+            
+            &.selected {
+                background: #9379e6;
+            } 
+
+            img{
+                height: 40px;
+                width: 100%;
+            }
+        }
+
+
+        #play-button {
+            grid-column: 3;
+        }
+
+        #color-picker-button {
+            grid-column: 4;
+            display: grid;
+            grid-template-columns: 100%;
+            grid-template-rows: 100%;
+
+            img {
+                height: 40px;
+                width: 30px;  
+            }
+            #color-picker-image {                
+                padding-left: 5px;
+                padding-bottom: 38px;
+            }
+
+            #color-picker {
+                transform: scale(0);
+                grid-column: 1;
+                grid-row: 1;
+            }
+
+            #color-picker-label {
+                width: 100%;
+                background: transparent;
+                cursor: pointer;            
+            }
+        }
+
+        #preview-new-window {
+            grid-column: 5;
+        }
+
+        select {
+            background-color: #a3a3a3;
+            color: #333333;
+        }
+    }
+
+    #preview-config-bar {
+        grid-row: 4;
+        grid-column: 1;
+        display: grid;
+        grid-template-columns: 40px 40px 40px 1fr 40px 40px;
+        color: white;
+        align-items: center;
+        font-size: 18px;    
+
+        .button {
+            width: 40px;
+            grid-row: 1;
+            height: 40px;
+            transform-origin: 50% 50%;
+
+            &:hover {
+                background: #3f3461;
+            }
+
+            &.selected {
+                background: #9379e6;
+            } 
+            
+
+            &:active {
+                transform: scale(0.90);
+            }
+
+            img{
+                height: auto;
+                width: 100%;
+            }
+
+            &.back-face {
+                grid-column: 6
+            }
+
+            &.depth-pass {
+                grid-column: 5 / 6
+            }
+
+            &.hemispheric-light{
+                grid-column: 3 / 4
+            }
+            &.direction-light-1{
+                grid-column: 2 / 3
+
+            }
+            &.direction-light-0{
+                grid-column: 1 / 2
+                
+            }
+        }
+    }
+    
+    #preview {
+        border-top: 1px solid rgb(85, 85, 85);
+        grid-row: 3;
+        grid-column: 1;
+        width: 100%;
+        display: grid;
+        outline: 0 !important;
+        user-select: none;
+
+        #preview-canvas {
+            width: 100%;
+            height: 100%;
+            outline: 0 !important;
+            grid-row: 1;
+            grid-column: 1;            
+        }
+
+        .waitPanel {
+            width: 100%;
+            height: 100%;
+            grid-row: 1;
+            grid-column: 1;  
+            color: white;
+            font-size: 18px;
+            align-content: center;
+            justify-content: center;
+            background: rgba(20, 20, 20, 0.95);    
+            z-index: 10;      
+            display: grid;
+            transition: opacity 250ms;
+
+            &.hidden {
+                opacity: 0;
+                pointer-events: none;
+            }
+        }
+    }
+}
+
+.blocker {
+    visibility: hidden;
+    position: absolute;
+    width: calc(100% - 40px);
+    height: 100%;
+    top: 0;
+    left: 0;
+
+    background: rgba(20, 20, 20, 0.95);    
+    font-family: "acumin-pro";
+    color: white;
+    font-size: 24px;
+
+    display: grid;
+    align-content: center;
+    justify-content: center;
+
+    user-select: none;
+
+    padding: 20px;
+    text-align: center;
+}
+
+#log-console {
+    grid-row: 2;
+    grid-column: 3;
+}
+
+.dialog-container {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    background: rgba(0.1, 0.1, 0.1, 0.6);
+    display: grid;
+    font-family: "acumin-pro";
+    top:0;
+
+    .dialog {
+        align-self: center;
+        justify-self: center;
+        min-height: 140px;
+        max-width: 400px;
+        border-radius: 10px;
+        background: white;
+
+        display: grid;
+        grid-template-columns: 100%;
+        grid-template-rows: calc(100% - 50px) 50px;
+
+        .dialog-message {
+            grid-row: 1;
+            grid-column: 1;
+            margin-top: 20px;
+            padding: 10px;
+            font-size: 18px;
+            color: black;
+        }
+
+        .dialog-buttons {
+            grid-row: 2;
+            grid-column: 1;
+            display: grid;
+            grid-template-rows: 100%;
+            grid-template-columns: 100%;
+            color: white;
+
+            .dialog-button-ok {
+                cursor: pointer;
+                justify-self: center;
+                background:green;
+                min-width: 80px;
+                justify-content: center;
+                display: grid;
+                align-content: center;
+                align-self: center;
+                height: 35px;      
+                border-radius: 10px;
+
+                &:hover {
+                    opacity: 0.8;
+                }
+
+                &.error {
+                    background: red;
+                }
+            }
+        }
+    }
+}

+ 94 - 0
guiEditor/src/nodeEditor.ts

@@ -0,0 +1,94 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import { GlobalState } from './globalState';
+import { GraphEditor } from './graphEditor';
+import { NodeMaterial } from "babylonjs/Materials/Node/nodeMaterial"
+import { Popup } from "../src/sharedComponents/popup"
+import { SerializationTools } from './serializationTools';
+import { Observable } from 'babylonjs/Misc/observable';
+import { PreviewType } from './components/preview/previewType';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+/**
+ * Interface used to specify creation options for the node editor
+ */
+export interface INodeEditorOptions {
+    nodeMaterial: NodeMaterial,
+    hostElement?: HTMLElement,
+    customSave?: {label: string, action: (data: string) => Promise<void>};
+    customLoadObservable?: Observable<any>
+}
+
+/**
+ * Class used to create a node editor
+ */
+export class NodeEditor {
+    private static _CurrentState: GlobalState;
+
+    /**
+     * Show the node editor
+     * @param options defines the options to use to configure the node editor
+     */
+    public static Show(options: INodeEditorOptions) {
+        if (this._CurrentState) {
+            var popupWindow = (Popup as any)["node-editor"];
+            if (popupWindow) {
+                popupWindow.close();
+            }
+        }
+
+        let hostElement = options.hostElement;
+        
+        if (!hostElement) {
+            hostElement = Popup.CreatePopup("BABYLON.JS NODE EDITOR", "node-editor", 1000, 800)!;
+        }
+        
+        let globalState = new GlobalState();
+        globalState.nodeMaterial = options.nodeMaterial;
+        globalState.mode = options.nodeMaterial.mode;
+        globalState.hostElement = hostElement;
+        globalState.hostDocument = hostElement.ownerDocument!;
+        globalState.customSave = options.customSave;
+        globalState.hostWindow =  hostElement.ownerDocument!.defaultView!;
+
+        const graphEditor = React.createElement(GraphEditor, {
+            globalState: globalState
+        });
+
+        ReactDOM.render(graphEditor, hostElement);
+
+        if (options.customLoadObservable) {
+            options.customLoadObservable.add(data => {
+                SerializationTools.Deserialize(data, globalState);
+                globalState.mode = options.nodeMaterial.mode;
+                globalState.onResetRequiredObservable.notifyObservers();
+                globalState.onBuiltObservable.notifyObservers();
+            })
+        }
+
+        this._CurrentState = globalState;
+
+        // Close the popup window when the page is refreshed or scene is disposed
+        var popupWindow = (Popup as any)["node-editor"];
+        if (globalState.nodeMaterial && popupWindow) {
+            globalState.nodeMaterial.getScene().onDisposeObservable.addOnce(() => {
+                if (popupWindow) {
+                    popupWindow.close();
+                }
+            })
+            window.onbeforeunload = () => {
+                var popupWindow = (Popup as any)["node-editor"];
+                if (popupWindow) {
+                    popupWindow.close();
+                }
+
+            };
+        }
+        window.addEventListener('beforeunload', () => {
+            if(DataStorage.ReadNumber("PreviewType", PreviewType.Box) === PreviewType.Custom){
+                DataStorage.WriteNumber("PreviewType", globalState.mode === NodeMaterialModes.Material ? PreviewType.Box : PreviewType.Bubbles);
+            }
+        });
+    }
+}
+

+ 26 - 0
guiEditor/src/nodeLocationInfo.ts

@@ -0,0 +1,26 @@
+export interface INodeLocationInfo {
+    blockId: number;
+    x: number;
+    y: number;
+}
+
+export interface IFrameData {
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+    color: number[];
+    name: string;
+    isCollapsed: boolean;
+    blocks: number[];
+    comments: string;
+}
+
+export interface IEditorData {
+    locations: INodeLocationInfo[];
+    x: number;
+    y: number;
+    zoom: number;
+    frames?: IFrameData[];
+    map?: {[key: number]: number};
+}

+ 17 - 0
guiEditor/src/portal.tsx

@@ -0,0 +1,17 @@
+
+import * as React from "react";
+import { GlobalState } from './globalState';
+import * as ReactDOM from 'react-dom';
+
+interface IPortalProps {
+    globalState: GlobalState;
+}
+
+export class Portal extends React.Component<IPortalProps> {
+    render() {
+        return ReactDOM.createPortal(
+            this.props.children,
+            this.props.globalState.hostElement
+        );
+    }
+}

+ 58 - 0
guiEditor/src/serializationTools.ts

@@ -0,0 +1,58 @@
+import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
+import { GlobalState } from './globalState';
+import { Texture } from 'babylonjs/Materials/Textures/texture';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { Nullable } from 'babylonjs/types';
+import { GraphFrame } from './diagram/graphFrame';
+
+export class SerializationTools {
+
+    public static UpdateLocations(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>) {
+        material.editorData = {
+            locations: []
+        };
+
+        // Store node locations
+        const blocks: NodeMaterialBlock[] = frame ? frame.nodes.map(n => n.block) : material.attachedBlocks;
+
+        for (var block of blocks) {
+            let node = globalState.onGetNodeFromBlock(block);
+
+            material.editorData.locations.push({
+                blockId: block.uniqueId,
+                x: node ? node.x : 0,
+                y: node ? node.y : 0
+            });
+        }
+
+        globalState.storeEditorData(material.editorData, frame);
+    }
+
+    public static Serialize(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>) {
+        let bufferSerializationState = Texture.SerializeBuffers;
+        Texture.SerializeBuffers = DataStorage.ReadBoolean("EmbedTextures", true);
+
+        this.UpdateLocations(material, globalState, frame);
+
+        const selectedBlocks = frame ? frame.nodes.map(n => n.block) : undefined;
+
+        let serializationObject = material.serialize(selectedBlocks);
+
+        Texture.SerializeBuffers = bufferSerializationState;
+
+        return JSON.stringify(serializationObject, undefined, 2);
+    }
+
+    public static Deserialize(serializationObject: any, globalState: GlobalState) {
+        globalState.onIsLoadingChanged.notifyObservers(true);
+        globalState.nodeMaterial!.loadFromSerialization(serializationObject, "");
+    }
+
+    public static AddFrameToMaterial(serializationObject: any, globalState: GlobalState, currentMaterial: NodeMaterial) {
+        globalState.onIsLoadingChanged.notifyObservers(true);
+        this.UpdateLocations(currentMaterial, globalState);
+        globalState.nodeMaterial!.loadFromSerialization(serializationObject, "", true);
+        globalState.onImportFrameObservable.notifyObservers(serializationObject);
+    }
+}

+ 21 - 0
guiEditor/src/sharedComponents/buttonLineComponent.tsx

@@ -0,0 +1,21 @@
+import * as React from "react";
+
+export interface IButtonLineComponentProps {
+    label: string;
+    onClick: () => void;
+}
+
+export class ButtonLineComponent extends React.Component<IButtonLineComponentProps> {
+    constructor(props: IButtonLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+
+        return (
+            <div className="buttonLine">
+                <button onClick={() => this.props.onClick()}>{this.props.label}</button>
+            </div>
+        );
+    }
+}

+ 95 - 0
guiEditor/src/sharedComponents/checkBoxLineComponent.tsx

@@ -0,0 +1,95 @@
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+
+export interface ICheckBoxLineComponentProps {
+    label: string;
+    target?: any;
+    propertyName?: string;
+    isSelected?: () => boolean;
+    onSelect?: (value: boolean) => void;
+    onValueChanged?: () => void;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    disabled?: boolean;
+}
+
+export class CheckBoxLineComponent extends React.Component<ICheckBoxLineComponentProps, { isSelected: boolean, isDisabled?: boolean }> {
+    private static _UniqueIdSeed = 0;
+    private _uniqueId: number;
+    private _localChange = false;
+    constructor(props: ICheckBoxLineComponentProps) {
+        super(props);
+
+        this._uniqueId = CheckBoxLineComponent._UniqueIdSeed++;
+
+        if (this.props.isSelected) {
+            this.state = { isSelected: this.props.isSelected() };
+        } else {
+            this.state = { isSelected: this.props.target[this.props.propertyName!] == true };
+        }
+
+        if (this.props.disabled) {
+            this.state = { ...this.state, isDisabled: this.props.disabled };
+        }
+    }
+
+    shouldComponentUpdate(nextProps: ICheckBoxLineComponentProps, nextState: { isSelected: boolean, isDisabled: boolean }) {
+        var currentState: boolean;
+
+        if (nextProps.isSelected) {
+            currentState = nextProps.isSelected!();
+        } else {
+            currentState = nextProps.target[nextProps.propertyName!] == true;
+        }
+
+        if (currentState !== nextState.isSelected || this._localChange) {
+            nextState.isSelected = currentState;
+            this._localChange = false;
+            return true;
+        }
+
+        if(nextProps.disabled !== !!nextState.isDisabled){
+            return true;
+        }
+        
+        return nextProps.label !== this.props.label || nextProps.target !== this.props.target;
+    }
+
+    onChange() {
+        this._localChange = true;
+        if (this.props.onSelect) {
+            this.props.onSelect(!this.state.isSelected);
+        } else {
+            if (this.props.onPropertyChangedObservable) {
+                this.props.onPropertyChangedObservable.notifyObservers({
+                    object: this.props.target,
+                    property: this.props.propertyName!,
+                    value: !this.state.isSelected,
+                    initialValue: this.state.isSelected
+                });
+            }
+
+            this.props.target[this.props.propertyName!] = !this.state.isSelected;
+        }
+
+        if (this.props.onValueChanged) {
+            this.props.onValueChanged();
+        }
+
+        this.setState({ isSelected: !this.state.isSelected });
+    }
+
+    render() {
+        return (
+            <div className="checkBoxLine">
+                <div className="label">
+                    {this.props.label}
+                </div>
+                <div className="checkBox">
+                    <input type="checkbox" id={"checkbox" + this._uniqueId} className="cbx hidden" checked={this.state.isSelected} onChange={() => this.onChange()} disabled={!!this.props.disabled}/>
+                    <label htmlFor={"checkbox" + this._uniqueId} className={`lbl${!!this.props.disabled ? ' disabled' : ''}`}></label>
+                </div>
+            </div>
+        );
+    }
+}

+ 173 - 0
guiEditor/src/sharedComponents/color3LineComponent.tsx

@@ -0,0 +1,173 @@
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { Color3, Color4 } from "babylonjs/Maths/math.color";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+import { NumericInputComponent } from "./numericInputComponent";
+import { GlobalState } from '../globalState';
+import { ColorPickerLineComponent } from './colorPickerComponent';
+
+const copyIcon: string = require("./copy.svg");
+const plusIcon: string = require("./plus.svg");
+const minusIcon: string = require("./minus.svg");
+
+export interface IColor3LineComponentProps {
+    label: string;
+    target: any;
+    propertyName: string;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    onChange?: () => void;
+    globalState: GlobalState;
+}
+
+export class Color3LineComponent extends React.Component<IColor3LineComponentProps, { isExpanded: boolean, color: Color3 }> {
+    private _localChange = false;
+    constructor(props: IColor3LineComponentProps) {
+        super(props);
+
+        this.state = { isExpanded: false, color: this.props.target[this.props.propertyName].clone() };
+    }
+
+    shouldComponentUpdate(nextProps: IColor3LineComponentProps, nextState: { color: Color3 }) {
+        const currentState = nextProps.target[nextProps.propertyName];
+
+        if (!currentState.equals(nextState.color) || this._localChange) {
+            nextState.color = currentState.clone();
+            this._localChange = false;
+            return true;
+        }
+        return false;
+    }
+
+    onChange(newValue: string) {
+        this._localChange = true;
+        const newColor = Color3.FromHexString(newValue);
+
+        if (this.props.onPropertyChangedObservable) {
+            this.props.onPropertyChangedObservable.notifyObservers({
+                object: this.props.target,
+                property: this.props.propertyName,
+                value: newColor,
+                initialValue: this.state.color
+            });
+        }
+
+        if (this.props.target[this.props.propertyName].getClassName() === "Color4") {
+            this.props.target[this.props.propertyName] = new Color4(newColor.r, newColor.g, newColor.b, 1.0);
+        } else {
+            this.props.target[this.props.propertyName] = newColor;
+        }
+
+        this.setState({ color: newColor });
+
+        if (this.props.onChange) {
+            this.props.onChange();
+        }
+    }
+
+    switchExpandState() {
+        this._localChange = true;
+        this.setState({ isExpanded: !this.state.isExpanded });
+    }
+
+    raiseOnPropertyChanged(previousValue: Color3) {
+        if (this.props.onChange) {
+            this.props.onChange();
+        }
+
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName,
+            value: this.state.color,
+            initialValue: previousValue
+        });
+    }
+
+    updateStateR(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].x = value;
+        this.state.color.r = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateStateG(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].g = value;
+        this.state.color.g = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateStateB(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].b = value;
+        this.state.color.b = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    copyToClipboard() {
+        var element = document.createElement('div');
+        element.textContent = this.state.color.toHexString();
+        document.body.appendChild(element);
+
+        if (window.getSelection) {
+            var range = document.createRange();
+            range.selectNode(element);
+            window.getSelection()!.removeAllRanges();
+            window.getSelection()!.addRange(range);
+        }
+
+        document.execCommand('copy');
+        element.remove();
+    }
+
+    render() {
+
+        const expandedIcon = this.state.isExpanded ? minusIcon : plusIcon;
+
+        return (
+            <div className="color3Line">
+                <div className="firstLine">
+                    <div className="label">
+                        {this.props.label}
+                    </div>
+                    <div className="color3">
+                        <ColorPickerLineComponent value={this.state.color} disableAlpha={true} globalState={this.props.globalState} onColorChanged={color => {
+                                this.onChange(color);
+                            }} />  
+                    </div>
+                    <div className="copy hoverIcon" onClick={() => this.copyToClipboard()} title="Copy to clipboard">
+                        <img src={copyIcon} alt=""/>
+                    </div>
+                    <div className="expand hoverIcon" onClick={() => this.switchExpandState()} title="Expand">
+                        <img src={expandedIcon} alt=""/>
+                    </div>
+                </div>
+                {
+                    this.state.isExpanded &&
+                    <div className="secondLine">
+                        <NumericInputComponent globalState={this.props.globalState} label="r" value={this.state.color.r} onChange={(value) => this.updateStateR(value)} />
+                        <NumericInputComponent globalState={this.props.globalState} label="g" value={this.state.color.g} onChange={(value) => this.updateStateG(value)} />
+                        <NumericInputComponent globalState={this.props.globalState} label="b" value={this.state.color.b} onChange={(value) => this.updateStateB(value)} />
+                    </div>
+                }
+            </div>
+        );
+    }
+}

+ 185 - 0
guiEditor/src/sharedComponents/color4LineComponent.tsx

@@ -0,0 +1,185 @@
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { Color4 } from "babylonjs/Maths/math.color";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+import { NumericInputComponent } from "./numericInputComponent";
+import { GlobalState } from '../globalState';
+import { ColorPickerLineComponent } from './colorPickerComponent';
+
+const copyIcon: string = require("./copy.svg");
+const plusIcon: string = require("./plus.svg");
+const minusIcon: string = require("./minus.svg");
+
+export interface IColor4LineComponentProps {
+    label: string;
+    target: any;
+    propertyName: string;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    onChange?: () => void;
+    globalState: GlobalState;
+}
+
+export class Color4LineComponent extends React.Component<IColor4LineComponentProps, { isExpanded: boolean, color: Color4 }> {
+    private _localChange = false;
+    constructor(props: IColor4LineComponentProps) {
+        super(props);
+
+        let value = this.props.target[this.props.propertyName];
+        let currentColor = value.getClassName() === "Color4" ? value.clone() : new Color4(value.r, value.g, value.b, 1.0);
+        this.state = { isExpanded: false, color: currentColor };
+    }
+
+    shouldComponentUpdate(nextProps: IColor4LineComponentProps, nextState: { color: Color4 }) {
+        const currentState = nextProps.target[nextProps.propertyName];
+        let currentColor = currentState.getClassName() === "Color4" ? currentState : new Color4(currentState.r, currentState.g, currentState.b, 1.0);  
+
+        if (!currentColor.equals(nextState.color) || this._localChange) {
+            nextState.color = currentColor.clone();
+            this._localChange = false;
+            return true;
+        }
+        return false;
+    }
+
+    onChange(newValue: string) {
+        this._localChange = true;
+        const newColor = Color4.FromHexString(newValue);
+
+        if (this.props.onPropertyChangedObservable) {
+            this.props.onPropertyChangedObservable.notifyObservers({
+                object: this.props.target,
+                property: this.props.propertyName,
+                value: newColor,
+                initialValue: this.state.color
+            });
+        }
+
+        this.props.target[this.props.propertyName] = newColor;
+
+        this.setState({ color: this.props.target[this.props.propertyName] });
+
+        if (this.props.onChange) {
+            this.props.onChange();
+        }
+    }
+
+    switchExpandState() {
+        this._localChange = true;
+        this.setState({ isExpanded: !this.state.isExpanded });
+    }
+
+    raiseOnPropertyChanged(previousValue: Color4) {
+        if (this.props.onChange) {
+            this.props.onChange();
+        }
+
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName,
+            value: this.state.color,
+            initialValue: previousValue
+        });
+    }
+
+    updateStateR(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].x = value;
+        this.state.color.r = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateStateG(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].g = value;
+        this.state.color.g = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateStateB(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].b = value;
+        this.state.color.b = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateStateA(value: number) {
+        this._localChange = true;
+
+        const store = this.state.color.clone();
+        this.props.target[this.props.propertyName].a = value;
+        this.state.color.a = value;
+        this.props.target[this.props.propertyName] = this.state.color;
+        this.setState({ color: this.state.color });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    copyToClipboard() {
+        var element = document.createElement('div');
+        element.textContent = this.state.color.toHexString();
+        document.body.appendChild(element);
+
+        if (window.getSelection) {
+            var range = document.createRange();
+            range.selectNode(element);
+            window.getSelection()!.removeAllRanges();
+            window.getSelection()!.addRange(range);
+        }
+
+        document.execCommand('copy');
+        element.remove();
+    }
+
+    render() {
+
+        const expandedIcon = this.state.isExpanded ? minusIcon : plusIcon;
+
+        return (
+            <div className="color3Line">
+                <div className="firstLine">
+                    <div className="label">
+                        {this.props.label}
+                    </div>
+                    <div className="color3">
+                        <ColorPickerLineComponent globalState={this.props.globalState} value={this.state.color} onColorChanged={color => {
+                                this.onChange(color);
+                            }} />  
+                    </div>
+                    <div className="copy hoverIcon" onClick={() => this.copyToClipboard()} title="Copy to clipboard">
+                        <img src={copyIcon} alt=""/>
+                    </div>
+                    <div className="expand hoverIcon" onClick={() => this.switchExpandState()} title="Expand">
+                        <img src={expandedIcon} alt=""/>
+                    </div>
+                </div>
+                {
+                    this.state.isExpanded &&
+                    <div className="secondLine">
+                        <NumericInputComponent globalState={this.props.globalState} label="r" value={this.state.color.r} onChange={(value) => this.updateStateR(value)} />
+                        <NumericInputComponent globalState={this.props.globalState} label="g" value={this.state.color.g} onChange={(value) => this.updateStateG(value)} />
+                        <NumericInputComponent globalState={this.props.globalState} label="b" value={this.state.color.b} onChange={(value) => this.updateStateB(value)} />
+                        <NumericInputComponent globalState={this.props.globalState} label="a" value={this.state.color.a} onChange={(value) => this.updateStateA(value)} />
+                    </div>
+                }
+            </div>
+        );
+    }
+}

+ 131 - 0
guiEditor/src/sharedComponents/colorPickerComponent.tsx

@@ -0,0 +1,131 @@
+import * as React from "react";
+import { Color4, Color3 } from 'babylonjs/Maths/math.color';
+import { SketchPicker } from 'react-color';
+import { GlobalState } from '../globalState';
+
+export interface IColorPickerComponentProps {
+    value: Color4 | Color3;
+    onColorChanged: (newOne: string) => void;
+    globalState: GlobalState;
+    disableAlpha?: boolean;
+}
+
+interface IColorPickerComponentState {
+    pickerEnabled: boolean;
+    color: {
+        r: number,
+        g: number,
+        b: number,
+        a?: number
+    },
+    hex: string
+}
+
+export class ColorPickerLineComponent extends React.Component<IColorPickerComponentProps, IColorPickerComponentState> {
+    private _floatRef: React.RefObject<HTMLDivElement>
+    private _floatHostRef: React.RefObject<HTMLDivElement>
+
+    constructor(props: IColorPickerComponentProps) {
+        super(props);
+
+        this.state = {pickerEnabled: false, color: {
+            r: this.props.value.r * 255,
+            g: this.props.value.g * 255,
+            b: this.props.value.b * 255,
+            a: this.props.value instanceof Color4 ? this.props.value.a * 100 : 100,
+        }, hex: this.props.value.toHexString()};
+        
+        this._floatRef = React.createRef();
+        this._floatHostRef = React.createRef();
+    }
+
+    syncPositions() {
+        const div = this._floatRef.current as HTMLDivElement;
+        const host = this._floatHostRef.current as HTMLDivElement;
+
+        if (!div || !host) {
+            return;
+        }
+
+        let top = host.getBoundingClientRect().top;
+        let height = div.getBoundingClientRect().height;
+
+        if (top + height + 10 > window.innerHeight) {
+            top = window.innerHeight - height - 10;
+        }
+
+        div.style.top = top + "px";
+        div.style.left = host.getBoundingClientRect().left - div.getBoundingClientRect().width + "px";
+    }
+
+    shouldComponentUpdate(nextProps: IColorPickerComponentProps, nextState: IColorPickerComponentState) {
+        let result = nextProps.value.toHexString() !== this.props.value.toHexString() 
+            || nextProps.disableAlpha !== this.props.disableAlpha 
+            || nextState.hex !== this.state.hex
+            || nextState.pickerEnabled !== this.state.pickerEnabled;
+        
+        if(result) {
+            nextState.color =  {
+                r: nextProps.value.r * 255,
+                g: nextProps.value.g * 255,
+                b: nextProps.value.b * 255,
+                a: nextProps.value instanceof Color4 ? nextProps.value.a : 1,
+            };
+            nextState.hex = nextProps.value.toHexString();
+        }
+        return result;   
+    }
+
+    componentDidUpdate() {
+        this.syncPositions();
+    }
+
+    componentDidMount() {
+        this.syncPositions();
+    }
+
+    setPickerState(enabled: boolean) {
+        this.setState({ pickerEnabled: enabled });
+        this.props.globalState.blockKeyboardEvents = enabled;
+    }
+
+    render() {
+        var color = this.state.color;
+
+        this.props.globalState.blockKeyboardEvents = this.state.pickerEnabled;
+
+        return (
+            <div className="color-picker">
+                <div className="color-rect"  ref={this._floatHostRef} 
+                    style={{background: this.state.hex}} 
+                    onClick={() => this.setPickerState(true)}>
+
+                </div>
+                {
+                    this.state.pickerEnabled &&
+                    <>
+                        <div className="color-picker-cover" onClick={() => this.setPickerState(false)}></div>
+                        <div className="color-picker-float" ref={this._floatRef}>
+                            <SketchPicker color={color} 
+                                disableAlpha={this.props.disableAlpha}
+                                onChange={(color) => {
+                                    let hex: string;
+
+                                    if (this.props.disableAlpha) {
+                                        let newColor3 = Color3.FromInts(color.rgb.r, color.rgb.g, color.rgb.b);
+                                        hex = newColor3.toHexString();    
+                                    } else {
+                                        let newColor4 = Color4.FromInts(color.rgb.r, color.rgb.g, color.rgb.b, 255 * (color.rgb.a || 0));
+                                        hex = newColor4.toHexString();   
+                                    }
+                                    this.setState({hex: hex, color: color.rgb});
+                                    this.props.onColorChanged(hex);
+                                }}
+                            />
+                        </div>
+                    </>
+                }                
+            </div>
+        );
+    }
+}

File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/sharedComponents/copy.svg


+ 25 - 0
guiEditor/src/sharedComponents/draggableLineComponent.tsx

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

+ 31 - 0
guiEditor/src/sharedComponents/draggableLineWithButtonComponent.tsx

@@ -0,0 +1,31 @@
+import * as React from "react";
+
+export interface IDraggableLineWithButtonComponent {
+    data: string;
+    tooltip: string;
+    iconImage: any;
+    onIconClick: (value: string) => void;
+    iconTitle: string;
+}
+
+export class DraggableLineWithButtonComponent extends React.Component<IDraggableLineWithButtonComponent> {
+    constructor(props: IDraggableLineWithButtonComponent) {
+        super(props);
+    }
+
+    render() { 
+        return (
+            <div className="draggableLine withButton" 
+                title={this.props.tooltip}
+                draggable={true}
+                onDragStart={event => {
+                    event.dataTransfer.setData("babylonjs-material-node", this.props.data);
+                }}>
+                {this.props.data.substr(0, this.props.data.length - 6)}
+                <div className="icon" onClick={() => { this.props.onIconClick(this.props.data); }} title={this.props.iconTitle}>
+                    <img className="img" title={this.props.iconTitle} src={this.props.iconImage}/>
+                </div>
+            </div>
+        );
+    }
+}

+ 38 - 0
guiEditor/src/sharedComponents/fileButtonLineComponent.tsx

@@ -0,0 +1,38 @@
+import * as React from "react";
+
+interface IFileButtonLineComponentProps {
+    label: string;
+    onClick: (file: File) => void;
+    accept: string;
+    uploadName?: string;
+}
+
+export class FileButtonLineComponent extends React.Component<IFileButtonLineComponentProps> {
+    private uploadRef: React.RefObject<HTMLInputElement>;
+
+    constructor(props: IFileButtonLineComponentProps) {
+        super(props);
+
+        this.uploadRef = React.createRef();
+    }
+
+    onChange(evt: any) {
+        var files: File[] = evt.target.files;
+        if (files && files.length) {
+            this.props.onClick(files[0]);
+        }
+
+        evt.target.value = "";
+    }
+
+    render() {
+        return (
+            <div className="buttonLine">
+                <label htmlFor={this.props.uploadName ? this.props.uploadName : "file-upload"} className="file-upload">
+                    {this.props.label}
+                </label>
+                <input ref={this.uploadRef} id={this.props.uploadName ? this.props.uploadName : "file-upload"} type="file" accept={this.props.accept} onChange={evt => this.onChange(evt)} />
+            </div>
+        );
+    }
+}

+ 145 - 0
guiEditor/src/sharedComponents/floatLineComponent.tsx

@@ -0,0 +1,145 @@
+import * as React from "react";
+
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+import { GlobalState } from '../globalState';
+
+interface IFloatLineComponentProps {
+    label: string;
+    target: any;
+    propertyName: string;
+    onChange?: (newValue: number) => void;
+    isInteger?: boolean;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    additionalClass?: string;
+    step?: string;
+    digits?: number;
+    globalState: GlobalState;
+    min?: number
+    max?: number
+    smallUI?: boolean;
+    onEnter?: (newValue:number) => void;
+}
+
+export class FloatLineComponent extends React.Component<IFloatLineComponentProps, { value: string }> {
+    private _localChange = false;
+    private _store: number;
+    private _regExp: RegExp;
+
+    constructor(props: IFloatLineComponentProps) {
+        super(props);
+        let currentValue = this.props.target[this.props.propertyName];
+        this.state = { value: currentValue ? (this.props.isInteger ? currentValue.toFixed(0) : currentValue.toFixed(this.props.digits || 2)) : "0" };
+        this._store = currentValue;
+
+        let rexp = "(.*\\.";
+        let numDigits = this.props.digits || 2;
+        while (numDigits--) {
+            rexp += ".";
+        }
+        rexp += ").+";
+
+        this._regExp = new RegExp(rexp);
+    }
+
+    shouldComponentUpdate(nextProps: IFloatLineComponentProps, nextState: { value: string }) {
+        if (this._localChange) {
+            this._localChange = false;
+            return true;
+        }
+
+        const newValue = nextProps.target[nextProps.propertyName];
+        const newValueString = newValue ? this.props.isInteger ? newValue.toFixed(0) : newValue.toFixed(this.props.digits || 2) : "0";
+
+        if (newValueString !== nextState.value) {
+            nextState.value = newValueString;
+            return true;
+        }
+        return false;
+    }
+
+    raiseOnPropertyChanged(newValue: number, previousValue: number) {
+        if (this.props.onChange) {
+            this.props.onChange(newValue);
+        }
+
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName,
+            value: newValue,
+            initialValue: previousValue
+        });
+    }
+
+    updateValue(valueString: string) {
+        if (/[^0-9\.\-]/g.test(valueString)) {
+            return;
+        }
+
+        valueString = valueString.replace(this._regExp, "$1");
+
+        let valueAsNumber: number;
+
+        if (this.props.isInteger) {
+            valueAsNumber = parseInt(valueString);
+        } else {
+            valueAsNumber = parseFloat(valueString);
+        }
+
+        this._localChange = true;
+        this.setState({ value: valueString});
+
+        if (isNaN(valueAsNumber)) {
+            return;
+        }
+        if(this.props.max != undefined && (valueAsNumber > this.props.max)) {
+            valueAsNumber = this.props.max;
+        }
+        if(this.props.min != undefined && (valueAsNumber < this.props.min)) {
+            valueAsNumber = this.props.min;
+        }
+
+        this.props.target[this.props.propertyName] = valueAsNumber;
+        this.raiseOnPropertyChanged(valueAsNumber, this._store);
+
+        this._store = valueAsNumber;
+    }
+
+    render() {
+        let className = this.props.smallUI ? "short": "value";
+
+        return (
+            <div>
+                {
+                    <div className={this.props.additionalClass ? this.props.additionalClass + " floatLine" : "floatLine"}>
+                        <div className="label">
+                            {this.props.label}
+                        </div>
+                        <div className={className}>
+                            <input type="number" step={this.props.step || "0.01"} className="numeric-input"
+                            onBlur={(evt) => {
+                                this.props.globalState.blockKeyboardEvents = false;
+                                if(this.props.onEnter) {
+                                    this.props.onEnter(this._store);
+                                }
+                            }}
+                            onKeyDown={evt => {
+                                if (evt.keyCode !== 13) {
+                                    return;
+                                }
+                                if(this.props.onEnter) {
+                                    this.props.onEnter(this._store);
+                                }
+                            }}
+                            onFocus={() => this.props.globalState.blockKeyboardEvents = true}
+                            value={this.state.value} onChange={(evt) => this.updateValue(evt.target.value)} />
+                        </div>
+                    </div>
+                }
+            </div>
+        );
+    }
+}

+ 69 - 0
guiEditor/src/sharedComponents/lineContainerComponent.tsx

@@ -0,0 +1,69 @@
+import * as React from "react";
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+const downArrow = require("../../imgs/downArrow.svg");
+
+interface ILineContainerComponentProps {
+    title: string;
+    children: any[] | any;
+    closed?: boolean;
+}
+
+export class LineContainerComponent extends React.Component<ILineContainerComponentProps, { isExpanded: boolean }> {
+    constructor(props: ILineContainerComponentProps) {
+        super(props);
+
+        let initialState = DataStorage.ReadBoolean(this.props.title, !this.props.closed);
+
+        this.state = { isExpanded: initialState };
+    }
+
+    switchExpandedState(): void {
+        const newState = !this.state.isExpanded;
+
+        DataStorage.WriteBoolean(this.props.title, newState);
+
+        this.setState({ isExpanded: newState });
+    }
+
+    renderHeader() {
+        const className = this.state.isExpanded ? "collapse" : "collapse closed";
+
+        return (
+            <div className="header" onClick={() => this.switchExpandedState()}>
+                <div className="title">
+                    {this.props.title}
+                </div>
+                <div className={className}>
+                    <img className="img" title={this.props.title} src={downArrow}/>
+                </div>
+            </div>
+        );
+    }
+
+    render() {
+        if (!this.state.isExpanded) {
+            return (
+                <div className="paneContainer">
+                    <div className="paneContainer-content">
+                        {
+                            this.renderHeader()
+                        }
+                    </div>
+                </div>
+            );
+        }
+
+        return (
+            <div className="paneContainer">
+                <div className="paneContainer-content">
+                    {
+                        this.renderHeader()
+                    }
+                    <div className="paneList">
+                        {this.props.children}
+                    </div >
+                </div>
+            </div>
+        );
+    }
+}

+ 52 - 0
guiEditor/src/sharedComponents/lineWithFileButtonComponent.tsx

@@ -0,0 +1,52 @@
+import * as React from "react";
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+
+interface ILineWithFileButtonComponentProps {
+    title: string;
+    closed?: boolean;
+    label: string;
+    iconImage: any;
+    onIconClick: (file: File) => void;
+    accept: string;
+    uploadName?: string;
+}
+
+export class LineWithFileButtonComponent extends React.Component<ILineWithFileButtonComponentProps, { isExpanded: boolean }> {
+    private uploadRef: React.RefObject<HTMLInputElement>
+    constructor(props: ILineWithFileButtonComponentProps) {
+        super(props);
+
+        let initialState = DataStorage.ReadBoolean(this.props.title, !this.props.closed);
+        this.state = { isExpanded: initialState };
+        this.uploadRef = React.createRef();
+    }
+
+    onChange(evt: any) {
+        var files: File[] = evt.target.files;
+        if (files && files.length) {
+            this.props.onIconClick(files[0]);
+        }
+        evt.target.value = "";
+    }
+
+    switchExpandedState(): void {
+        const newState = !this.state.isExpanded;
+        DataStorage.WriteBoolean(this.props.title, newState);
+        this.setState({ isExpanded: newState });
+    }
+
+    render() {
+        return (
+            <div className="nonDraggableLine withButton">
+                {this.props.label}
+                <div className="icon" title={this.props.title}>
+                <img className="img" src={this.props.iconImage}/>
+                </div>
+                <div className="buttonLine" title={this.props.title}>
+                    <label htmlFor={this.props.uploadName ? this.props.uploadName : "file-upload"} className="file-upload"/>   
+                    <input ref={this.uploadRef} id={this.props.uploadName ? this.props.uploadName : "file-upload"} type="file" accept={this.props.accept} onChange={evt => this.onChange(evt)} />
+                </div>
+            </div>
+        ); 
+    }
+}

+ 171 - 0
guiEditor/src/sharedComponents/matrixLineComponent.tsx

@@ -0,0 +1,171 @@
+import * as React from "react";
+import { Vector3, Matrix, Vector4, Quaternion } from "babylonjs/Maths/math.vector";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+import { Vector4LineComponent } from './vector4LineComponent';
+import { OptionsLineComponent } from './optionsLineComponent';
+import { SliderLineComponent } from './sliderLineComponent';
+import { GlobalState } from '../globalState';
+
+interface IMatrixLineComponentProps {
+    label: string;
+    target: any;
+    propertyName: string;
+    step?: number;
+    onChange?: (newValue: Matrix) => void;
+    onModeChange?: (mode: number) => void;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    mode?: number;
+    globalState: GlobalState;
+}
+
+export class MatrixLineComponent extends React.Component<IMatrixLineComponentProps, { value: Matrix, mode: number, angle: number}> {
+   private _localChange = false;
+
+    constructor(props: IMatrixLineComponentProps) {
+        super(props);
+
+        let matrix: Matrix = this.props.target[this.props.propertyName].clone();
+
+        let angle = 0;
+
+        if (this.props.mode) {
+            let quat = new Quaternion();
+            matrix.decompose(undefined, quat);
+
+            let euler = quat.toEulerAngles();
+
+            switch (this.props.mode) {
+                case 1:
+                    angle = euler.x;
+                    break;
+                case 2:
+                    angle = euler.y;
+                    break;
+                case 3:
+                    angle = euler.z;
+                    break;
+            }
+        }
+
+        this.state = { value:matrix, mode: this.props.mode || 0, angle: angle };
+    }
+
+    shouldComponentUpdate(nextProps: IMatrixLineComponentProps, nextState: { value: Matrix, mode: number, angle: number }) {
+        const nextPropsValue = nextProps.target[nextProps.propertyName];
+
+        if (!nextPropsValue.equals(nextState.value) || this._localChange) {
+            nextState.value = nextPropsValue.clone();
+            this._localChange = false;
+            return true;
+        }
+        return nextState.mode !== this.state.mode || nextState.angle !== this.state.angle;
+    }
+
+    raiseOnPropertyChanged(previousValue: Vector3) {
+        if (this.props.onChange) {
+            this.props.onChange(this.state.value);
+        }
+
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName,
+            value: this.state.value,
+            initialValue: previousValue
+        });
+    }
+
+    updateMatrix() {
+        const store = this.props.target[this.props.propertyName].clone();
+        this.props.target[this.props.propertyName] = this.state.value;
+
+        this.setState({ value: store });
+
+        this.raiseOnPropertyChanged(store);
+    }
+
+    updateRow(value: Vector4, row: number) {
+        this._localChange = true;
+
+        this.state.value.setRow(row, value);
+        this.updateMatrix();
+    }
+
+    updateBasedOnMode(value: number) {
+
+        switch (this.state.mode) {
+            case 1: {
+                Matrix.RotationXToRef(this.state.angle, this.state.value);
+                break;
+            }
+            case 2: {
+                Matrix.RotationYToRef(this.state.angle, this.state.value);
+                break;
+            }
+            case 3: {
+                Matrix.RotationZToRef(this.state.angle, this.state.value);
+                break;
+            }
+        }
+        this.updateMatrix();
+
+        this.setState({angle: value});
+    }
+
+    render() {
+        var modeOptions = [
+            { label: "User-defined", value: 0 },
+            { label: "Rotation over X axis", value: 1 },
+            { label: "Rotation over Y axis", value: 2 },
+            { label: "Rotation over Z axis", value: 3 },
+        ];
+
+        return (
+            <div className="vector3Line">
+                <div className="firstLine">
+                    <div className="label">
+                        {this.props.label}
+                    </div>
+                </div>
+                <div className="secondLine">
+                    <OptionsLineComponent label="Mode"
+                        className="no-right-margin"
+                        options={modeOptions} target={this} 
+                        noDirectUpdate={true}
+                        getSelection={() => {
+                            return this.state.mode;
+                        }}
+                        onSelect={(value: any) => {
+                            this.props.target[this.props.propertyName] = Matrix.Identity();
+                            Matrix.IdentityToRef(this.state.value);
+                            this.setState({mode: value, angle: 0});
+                            
+                            this.updateMatrix();
+
+                            if (this.props.onModeChange) {
+                                this.props.onModeChange(value);
+                            }
+                        }} />                
+                    </div>
+                {
+                    this.state.mode === 0 &&
+                    <div className="secondLine">
+                        <Vector4LineComponent globalState={this.props.globalState} label="Row #0" value={this.state.value.getRow(0)!} onChange={value => this.updateRow(value, 0)}/>
+                        <Vector4LineComponent globalState={this.props.globalState} label="Row #1" value={this.state.value.getRow(1)!} onChange={value => this.updateRow(value, 1)}/>
+                        <Vector4LineComponent globalState={this.props.globalState} label="Row #2" value={this.state.value.getRow(2)!} onChange={value => this.updateRow(value, 2)}/>
+                        <Vector4LineComponent globalState={this.props.globalState} label="Row #3" value={this.state.value.getRow(3)!} onChange={value => this.updateRow(value, 3)}/>
+                    </div>
+                }
+                {
+                    this.state.mode !== 0 &&
+                    <div className="secondLine">
+                        <SliderLineComponent label="Angle" minimum={0} maximum={2 * Math.PI} useEuler={true} step={0.1} globalState={this.props.globalState} directValue={this.state.angle} onChange={value => this.updateBasedOnMode(value)}/>
+                    </div>
+                }
+            </div>
+        );
+    }
+}

+ 42 - 0
guiEditor/src/sharedComponents/messageDialog.tsx

@@ -0,0 +1,42 @@
+import * as React from "react";
+
+import { GlobalState } from '../globalState';
+
+interface IMessageDialogComponentProps {
+    globalState: GlobalState
+}
+
+export class MessageDialogComponent extends React.Component<IMessageDialogComponentProps, { message: string, isError: boolean }> {
+    constructor(props: IMessageDialogComponentProps) {
+        super(props);
+
+        this.state = {message: "", isError: false};
+
+        this.props.globalState.onErrorMessageDialogRequiredObservable.add((message: string) => {
+            this.setState({message: message, isError: true});
+        });
+    }
+
+    render() {
+        if (!this.state.message) {
+            return null;
+        }
+
+        return (
+            <div className="dialog-container">
+                <div className="dialog">
+                    <div className="dialog-message">
+                        {
+                            this.state.message
+                        }
+                    </div>
+                    <div className="dialog-buttons">
+                        <div className={"dialog-button-ok" + (this.state.isError ? " error" : "")} onClick={() => this.setState({message: ""})}>
+                            OK
+                        </div>
+                    </div>
+                </div>
+            </div>
+        );
+    }
+}

+ 10 - 0
guiEditor/src/sharedComponents/minus.svg

@@ -0,0 +1,10 @@
+<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24">
+  <!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
+  <title>ic_fluent_remove_24_regular</title>
+  <desc>Created with Sketch.</desc>
+  <g id="🔍-Product-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="ic_fluent_remove_24_regular" fill="white" fill-rule="nonzero">
+      <path d="M3.7547787,12.4995322 L20.2466903,12.4995322 C20.6609039,12.4995322 20.9966903,12.1637458 20.9966903,11.7495322 C20.9966903,11.3353187 20.6609039,10.9995322 20.2466903,10.9995322 L3.7547787,10.9995322 C3.34056514,10.9995322 3.0047787,11.3353187 3.0047787,11.7495322 C3.0047787,12.1637458 3.34056514,12.4995322 3.7547787,12.4995322 Z" id="🎨-Color"></path>
+    </g>
+  </g>
+</svg>

+ 76 - 0
guiEditor/src/sharedComponents/numericInputComponent.tsx

@@ -0,0 +1,76 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+
+interface INumericInputComponentProps {
+    label: string;
+    value: number;
+    step?: number;
+    onChange: (value: number) => void;
+    globalState: GlobalState;
+}
+
+export class NumericInputComponent extends React.Component<INumericInputComponentProps, { value: string }> {
+
+    static defaultProps = {
+        step: 1,
+    };
+
+    private _localChange = false;
+    constructor(props: INumericInputComponentProps) {
+        super(props);
+
+        this.state = { value: this.props.value.toFixed(3) }
+    }
+
+    shouldComponentUpdate(nextProps: INumericInputComponentProps, nextState: { value: string }) {
+        if (this._localChange) {
+            this._localChange = false;
+            return true;
+        }
+
+        if (nextProps.value.toString() !== nextState.value) {
+            nextState.value = nextProps.value.toFixed(3);
+            return true;
+        }
+        return false;
+    }
+
+    updateValue(evt: any) {
+        let value = evt.target.value;
+
+        if (/[^0-9\.\-]/g.test(value)) {
+            return;
+        }
+
+        let valueAsNumber = parseFloat(value);
+
+        this._localChange = true;
+        this.setState({ value: value });
+
+        if (isNaN(valueAsNumber)) {
+            return;
+        }
+
+        this.props.onChange(valueAsNumber);
+    }
+
+
+    render() {
+        return (
+            <div className="numeric">
+                {
+                    this.props.label &&
+                    <div className="numeric-label">
+                        {`${this.props.label}: `}
+                    </div>
+                }
+                <input type="number" 
+                    onFocus={() => this.props.globalState.blockKeyboardEvents = true}
+                    onBlur={evt => {
+                        this.props.globalState.blockKeyboardEvents = false;
+                    }}
+                    step={this.props.step} className="numeric-input" value={this.state.value} onChange={evt => this.updateValue(evt)} />
+            </div>
+        )
+    }
+}

+ 110 - 0
guiEditor/src/sharedComponents/optionsLineComponent.tsx

@@ -0,0 +1,110 @@
+import * as React from "react";
+
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+
+class ListLineOption {
+    public label: string;
+    public value: number | string;
+}
+
+interface IOptionsLineComponentProps {
+    label: string,
+    target: any,
+    className?: string,
+    propertyName?: string,
+    options: ListLineOption[],
+    noDirectUpdate?: boolean,
+    onSelect?: (value: number | string) => void,
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>,
+    valuesAreStrings?: boolean
+    defaultIfNull?: number,
+    getSelection?: (target: any) => number;
+}
+
+export class OptionsLineComponent extends React.Component<IOptionsLineComponentProps, { value: number | string }> {
+    private _localChange = false;
+
+    private _getValue(props: IOptionsLineComponentProps) {
+        if (props.getSelection) {
+            return props.getSelection(props.target);
+        }
+        return (props.target && props.propertyName) ? props.target[props.propertyName] : props.options[props.defaultIfNull || 0];
+    }
+
+    constructor(props: IOptionsLineComponentProps) {
+        super(props);
+
+        this.state = { value: this._getValue(props) };
+    }
+
+    setValue(value: string | number) {
+        this.setState({ value: value });
+    }
+
+    shouldComponentUpdate(nextProps: IOptionsLineComponentProps, nextState: { value: number }) {
+        if (this._localChange) {
+            this._localChange = false;
+            return true;
+        }
+
+        const newValue = this._getValue(nextProps);
+        if (newValue != null && newValue !== nextState.value) {
+            nextState.value = newValue;
+            return true;
+        }
+        return false;
+    }
+
+    raiseOnPropertyChanged(newValue: number | string, previousValue: number | string) {
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName!,
+            value: newValue,
+            initialValue: previousValue
+        });
+    }
+
+    updateValue(valueString: string) {
+        const value = this.props.valuesAreStrings ? valueString : parseInt(valueString);
+        this._localChange = true;
+
+        const store = this.state.value;
+        if (!this.props.noDirectUpdate) {
+            this.props.target[this.props.propertyName!] = value;
+        }
+        this.setState({ value: value });
+
+        this.raiseOnPropertyChanged(value, store);
+
+        if (this.props.onSelect) {
+            this.props.onSelect(value);
+        }
+    }
+
+    render() {
+        return (
+            <div className="listLine">
+                <div className="label">
+                    {this.props.label}
+
+                </div>
+                <div className={"options" + (this.props.className ? " " + this.props.className : "")}>
+                    <select onChange={evt => this.updateValue(evt.target.value)} value={this.state.value ?? ""}>
+                        {
+                            this.props.options.map(option => {
+                                return (
+                                    <option key={option.label} value={option.value}>{option.label}</option>
+                                )
+                            })
+                        }
+                    </select>
+                </div>
+            </div>
+        );
+    }
+}

File diff suppressed because it is too large
+ 10 - 0
guiEditor/src/sharedComponents/plus.svg


+ 80 - 0
guiEditor/src/sharedComponents/popup.ts

@@ -0,0 +1,80 @@
+export class Popup {
+    public static CreatePopup(title: string, windowVariableName: string, width = 300, height = 800) {
+        const windowCreationOptionsList = {
+            width: width,
+            height: height,
+            top: (window.innerHeight - width) / 2 + window.screenY,
+            left: (window.innerWidth - height) / 2 + window.screenX
+        };
+
+        var windowCreationOptions = Object.keys(windowCreationOptionsList)
+            .map(
+                (key) => key + '=' + (windowCreationOptionsList as any)[key]
+            )
+            .join(',');
+
+        const popupWindow = window.open("", title, windowCreationOptions);
+        if (!popupWindow) {
+            return null;
+        }
+
+        const parentDocument = popupWindow.document;
+
+        // Font
+        const newLinkEl = parentDocument.createElement('link');
+
+        newLinkEl.rel = 'stylesheet';
+        newLinkEl.href = "https://use.typekit.net/cta4xsb.css";
+        parentDocument.head!.appendChild(newLinkEl);
+
+
+        parentDocument.title = title;
+        parentDocument.body.style.width = "100%";
+        parentDocument.body.style.height = "100%";
+        parentDocument.body.style.margin = "0";
+        parentDocument.body.style.padding = "0";
+
+        let parentControl = parentDocument.createElement("div");
+        parentControl.style.width = "100%";
+        parentControl.style.height = "100%";
+        parentControl.style.margin = "0";
+        parentControl.style.padding = "0";
+
+        popupWindow.document.body.appendChild(parentControl);
+        this._CopyStyles(window.document, parentDocument);
+        setTimeout(() => { // need this for late bindings
+            this._CopyStyles(window.document, parentDocument);
+        }, 0);
+
+        (this as any)[windowVariableName] = popupWindow;
+
+        return parentControl;
+    }
+
+    private static _CopyStyles(sourceDoc: HTMLDocument, targetDoc: HTMLDocument) {
+        for (var index = 0; index < sourceDoc.styleSheets.length; index++) {
+            var styleSheet: any = sourceDoc.styleSheets[index];
+            try {
+                if (styleSheet.cssRules) { // for <style> elements
+                    const newStyleEl = sourceDoc.createElement('style');
+
+                    for (var cssRule of styleSheet.cssRules) {
+                        // write the text of each rule into the body of the style element
+                        newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
+                    }
+
+                    targetDoc.head!.appendChild(newStyleEl);
+                } else if (styleSheet.href) { // for <link> elements loading CSS from a URL
+                    const newLinkEl = sourceDoc.createElement('link');
+
+                    newLinkEl.rel = 'stylesheet';
+                    newLinkEl.href = styleSheet.href;
+                    targetDoc.head!.appendChild(newLinkEl);
+                }
+            } catch (e) {
+                console.log(e)
+            }
+
+        }
+    }
+}

+ 6 - 0
guiEditor/src/sharedComponents/propertyChangedEvent.ts

@@ -0,0 +1,6 @@
+export class PropertyChangedEvent {
+    public object: any;
+    public property: string;
+    public value: any;
+    public initialValue: any;
+}

+ 126 - 0
guiEditor/src/sharedComponents/sliderLineComponent.tsx

@@ -0,0 +1,126 @@
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { Tools } from 'babylonjs/Misc/tools';
+import { PropertyChangedEvent } from './propertyChangedEvent';
+import { FloatLineComponent } from './floatLineComponent';
+import { GlobalState } from '../globalState';
+
+interface ISliderLineComponentProps {
+    label: string;
+    target?: any;
+    propertyName?: string;
+    minimum: number;
+    maximum: number;
+    step: number;
+    directValue?: number;
+    useEuler?: boolean;
+    onChange?: (value: number) => void;
+    onInput?: (value: number) => void;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    decimalCount?: number;
+    globalState: GlobalState;
+}
+
+export class SliderLineComponent extends React.Component<ISliderLineComponentProps, { value: number }> {
+    private _localChange = false;
+    constructor(props: ISliderLineComponentProps) {
+        super(props);
+
+        if (this.props.directValue !== undefined) {
+            this.state = {
+                value: this.props.directValue
+            }
+        } else {
+            let value = this.props.target![this.props.propertyName!];
+
+            if (value === undefined) {
+                value = this.props.maximum;
+            }
+            this.state = { value: value };
+        }
+    }
+
+    shouldComponentUpdate(nextProps: ISliderLineComponentProps, nextState: { value: number }) {
+        if (nextProps.directValue !== undefined) {
+            nextState.value = nextProps.directValue;
+            return true;
+        }
+
+        let currentState = nextProps.target![nextProps.propertyName!];
+        if (currentState === undefined) {
+            currentState = nextProps.maximum;
+        }
+
+        if (currentState !== nextState.value || nextProps.minimum !== this.props.minimum || nextProps.maximum !== this.props.maximum || this._localChange) {
+            nextState.value = Math.min(Math.max(currentState, nextProps.minimum), nextProps.maximum);
+            this._localChange = false;
+            return true;
+        }
+        return false;
+    }
+
+    onChange(newValueString: any) {
+        this._localChange = true;
+        let newValue = parseFloat(newValueString);
+
+        if (this.props.useEuler) {
+            newValue = Tools.ToRadians(newValue);
+        }
+
+        if (this.props.target) {
+            if (this.props.onPropertyChangedObservable) {
+                this.props.onPropertyChangedObservable.notifyObservers({
+                    object: this.props.target,
+                    property: this.props.propertyName!,
+                    value: newValue,
+                    initialValue: this.state.value
+                });
+            }
+
+            this.props.target[this.props.propertyName!] = newValue;
+        }
+
+        if (this.props.onChange) {
+            this.props.onChange(newValue);
+        }
+
+        this.setState({ value: newValue });
+    }
+
+    onInput(newValueString: any) {
+        const newValue = parseFloat(newValueString);
+        if (this.props.onInput) {
+            this.props.onInput(newValue);
+        }
+    }
+
+    prepareDataToRead(value: number) {
+        if (this.props.useEuler) {
+            return Tools.ToDegrees(value);
+        }
+
+        return value;
+    }
+
+    render() {
+
+        return (
+            <div className="sliderLine">
+                <div className="label">
+                    {this.props.label}
+                </div>
+                <FloatLineComponent globalState={this.props.globalState} smallUI={true} label="" target={this.state} propertyName="value" min={this.prepareDataToRead(this.props.minimum)} max={this.prepareDataToRead(this.props.maximum)}
+                    onEnter={ () => { 
+                        this.onChange(this.state.value)
+                    }
+                } > 
+                </FloatLineComponent>
+                <div className="slider">
+                    <input className="range" type="range" step={this.props.step} min={this.prepareDataToRead(this.props.minimum)} max={this.prepareDataToRead(this.props.maximum)} value={this.prepareDataToRead(this.state.value)}
+                        onInput={evt => this.onInput((evt.target as HTMLInputElement).value)}
+                        onChange={evt => this.onChange(evt.target.value)} />
+                </div>
+            </div>
+        );
+    }
+}

+ 120 - 0
guiEditor/src/sharedComponents/textInputLineComponent.tsx

@@ -0,0 +1,120 @@
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "./propertyChangedEvent";
+import { GlobalState } from '../globalState';
+
+interface ITextInputLineComponentProps {
+    label: string;
+    globalState: GlobalState;
+    target?: any;
+    propertyName?: string;
+    value?: string;
+    multilines?: boolean;
+    onChange?: (value: string) => void;
+    validator?: (value: string) => boolean;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+}
+
+export class TextInputLineComponent extends React.Component<ITextInputLineComponentProps, { value: string }> {
+    private _localChange = false;
+
+    constructor(props: ITextInputLineComponentProps) {
+        super(props);
+
+        this.state = { value: this.props.value !== undefined ? this.props.value : this.props.target[this.props.propertyName!] || "" };
+    }
+
+    shouldComponentUpdate(nextProps: ITextInputLineComponentProps, nextState: { value: string }) {
+        if (this._localChange) {
+            this._localChange = false;
+            return true;
+        }
+
+        const newValue = nextProps.value !== undefined ? nextProps.value : nextProps.target[nextProps.propertyName!];
+        if (newValue !== nextState.value) {
+            nextState.value = newValue || "";
+            return true;
+        }
+        return false;
+    }
+
+    raiseOnPropertyChanged(newValue: string, previousValue: string) {
+        if (this.props.onChange) {
+            this.props.onChange(newValue);
+            return;
+        }
+
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.target,
+            property: this.props.propertyName!,
+            value: newValue,
+            initialValue: previousValue
+        });
+    }
+
+    updateValue(value: string, raisePropertyChanged: boolean) {
+
+        this._localChange = true;
+        const store = this.props.value !== undefined ? this.props.value : this.props.target[this.props.propertyName!];
+
+        if (this.props.validator && raisePropertyChanged) {
+            if (this.props.validator(value) == false) {
+                value = store;
+            }
+        }
+
+        this.setState({ value: value });
+
+        if (raisePropertyChanged) {
+            this.raiseOnPropertyChanged(value, store);
+        }
+
+        if (this.props.propertyName) {
+            this.props.target[this.props.propertyName] = value;
+        }
+    }
+
+    render() {
+        return (
+            <div className={this.props.multilines ? "textInputArea" : "textInputLine"}>
+                <div className="label">
+                    {this.props.label}
+                </div>
+                <div className="value">
+                    {this.props.multilines && <>
+                        <textarea value={this.state.value}
+                            onFocus={() => this.props.globalState.blockKeyboardEvents = true}
+                            onChange={(evt) => this.updateValue(evt.target.value, false)}
+                            onKeyDown={(evt) => {
+                                if (evt.keyCode !== 13) {
+                                    return;
+                                }
+                                this.updateValue(this.state.value, true);
+                            }} onBlur={(evt) => {
+                                this.updateValue(evt.target.value, true);
+                                this.props.globalState.blockKeyboardEvents = false;
+                            }}/>
+                    </>}
+                    {!this.props.multilines && <>
+                        <input value={this.state.value}
+                            onFocus={() => this.props.globalState.blockKeyboardEvents = true}
+                            onChange={(evt) => this.updateValue(evt.target.value, false)}
+                            onKeyDown={(evt) => {
+                                if (evt.keyCode !== 13) {
+                                    return;
+                                }
+                                this.updateValue(this.state.value, true);
+                            }} onBlur={(evt) => {
+                                this.updateValue(evt.target.value, true);
+                                this.props.globalState.blockKeyboardEvents = false;
+                            }}/>
+                        </>}
+                </div>
+            </div>
+        );
+    }
+}

+ 49 - 0
guiEditor/src/sharedComponents/textLineComponent.tsx

@@ -0,0 +1,49 @@
+import * as React from "react";
+
+interface ITextLineComponentProps {
+    label: string;
+    value: string;
+    color?: string;
+    underline?: boolean;
+    onLink?: () => void;
+}
+
+export class TextLineComponent extends React.Component<ITextLineComponentProps> {
+    constructor(props: ITextLineComponentProps) {
+        super(props);
+    }
+
+    onLink() {
+        if (!this.props.onLink) {
+            return;
+        }
+
+        this.props.onLink();
+    }
+
+    renderContent() {
+        if (this.props.onLink) {
+            return (
+                <div className="link-value" title={this.props.value} onClick={() => this.onLink()}>
+                    {this.props.value || "no name"}
+                </div>
+            )
+        }
+        return (
+            <div className="value" title={this.props.value} style={{ color: this.props.color ? this.props.color : "" }}>
+                {this.props.value || "no name"}
+            </div>
+        )
+    }
+
+    render() {
+        return (
+            <div className={this.props.underline ? "textLine underline" : "textLine"}>
+                <div className="label">
+                    {this.props.label}
+                </div>
+                {this.renderContent()}
+            </div>
+        );
+    }
+}

+ 0 - 0
guiEditor/src/sharedComponents/textureLineComponent.tsx


Some files were not shown because too many files changed in this diff