Просмотр исходного кода

Merge pull request #9609 from msDestiny14/msDestiny14/gui

Introducing GUI Editor into Master
David Catuhe 5 лет назад
Родитель
Сommit
9fc4ccba97
59 измененных файлов с 54199 добавлено и 2 удалено
  1. 1 0
      .gitignore
  2. 13 0
      .vscode/launch.json
  3. 1 0
      Playground/frame.html
  4. 1 0
      Playground/full.html
  5. 1 0
      Playground/index.html
  6. 1 0
      Playground/index.js
  7. 59 2
      Tools/Config/config.json
  8. 885 0
      dist/preview release/guiEditor/babylon.guiEditor.d.ts
  9. 30 0
      dist/preview release/guiEditor/babylon.guiEditor.js
  10. 46373 0
      dist/preview release/guiEditor/babylon.guiEditor.max.js
  11. 1 0
      dist/preview release/guiEditor/babylon.guiEditor.max.js.map
  12. 1878 0
      dist/preview release/guiEditor/babylon.guiEditor.module.d.ts
  13. 27 0
      dist/preview release/guiEditor/package.json
  14. 15 0
      dist/preview release/guiEditor/readme-es6.md
  15. 1 0
      dist/preview release/guiEditor/readme.md
  16. 4 0
      dist/preview release/what's new.md
  17. 1 0
      guiEditor/README-ES6.md
  18. 18 0
      guiEditor/README.md
  19. 3 0
      guiEditor/imgs/downArrow.svg
  20. 43 0
      guiEditor/public/index-local.html
  21. 42 0
      guiEditor/public/index.html
  22. 116 0
      guiEditor/public/index.js
  23. 211 0
      guiEditor/src/components/guiList/guiList.scss
  24. 89 0
      guiEditor/src/components/guiList/guiListComponent.tsx
  25. 20 0
      guiEditor/src/components/log/log.scss
  26. 63 0
      guiEditor/src/components/log/logComponent.tsx
  27. 798 0
      guiEditor/src/components/propertyTab/propertyTab.scss
  28. 228 0
      guiEditor/src/components/propertyTab/propertyTabComponent.tsx
  29. 198 0
      guiEditor/src/diagram/guiNode.ts
  30. 113 0
      guiEditor/src/diagram/properties/genericNodePropertyComponent.tsx
  31. 7 0
      guiEditor/src/diagram/properties/propertyComponentProps.ts
  32. 55 0
      guiEditor/src/diagram/properties/sliderGuiPropertyComponent.tsx
  33. 10 0
      guiEditor/src/diagram/propertyLedger.ts
  34. 541 0
      guiEditor/src/diagram/workbench.tsx
  35. 452 0
      guiEditor/src/diagram/workbenchCanvas.scss
  36. 42 0
      guiEditor/src/globalState.ts
  37. 80 0
      guiEditor/src/guiEditor.ts
  38. 54 0
      guiEditor/src/guiNodeTools.ts
  39. 1 0
      guiEditor/src/index.ts
  40. 9 0
      guiEditor/src/legacy/legacy.ts
  41. 344 0
      guiEditor/src/main.scss
  42. 26 0
      guiEditor/src/nodeLocationInfo.ts
  43. 17 0
      guiEditor/src/portal.tsx
  44. 8 0
      guiEditor/src/serializationTools.ts
  45. 95 0
      guiEditor/src/sharedComponents/checkBoxLineComponent.tsx
  46. 147 0
      guiEditor/src/sharedComponents/floatLineComponent.tsx
  47. 69 0
      guiEditor/src/sharedComponents/lineContainerComponent.tsx
  48. 52 0
      guiEditor/src/sharedComponents/lineWithFileButtonComponent.tsx
  49. 42 0
      guiEditor/src/sharedComponents/messageDialog.tsx
  50. 76 0
      guiEditor/src/sharedComponents/numericInputComponent.tsx
  51. 6 0
      guiEditor/src/sharedComponents/propertyChangedEvent.ts
  52. 126 0
      guiEditor/src/sharedComponents/sliderLineComponent.tsx
  53. 120 0
      guiEditor/src/sharedComponents/textInputLineComponent.tsx
  54. 406 0
      guiEditor/src/workbenchEditor.tsx
  55. 29 0
      guiEditor/tsconfig.json
  56. 46 0
      guiEditor/webpack.config.js
  57. 0 0
      nodeEditor/src/sharedComponents/popup.tsx
  58. 25 0
      sharedUiComponents/lines/draggableLineComponent.tsx
  59. 80 0
      sharedUiComponents/lines/popup.ts

+ 1 - 0
.gitignore

@@ -212,3 +212,4 @@ ktx2Decoder/dist/
 # Symlinks
 inspector/src/sharedUiComponents/**/*
 nodeEditor/src/sharedUiComponents/**/*
+guiEditor/src/sharedUiComponents/**/*

+ 13 - 0
.vscode/launch.json

@@ -40,6 +40,19 @@
             "runtimeArgs": [
                 "--enable-unsafe-es3-apis"
             ]
+        }, 
+        {
+            "name": "Launch GUI Editor (Chrome)",
+            "type": "chrome",
+            "request": "launch",
+            "url": "http://localhost:1338/guiEditor/public/index-local.html",
+            "webRoot": "${workspaceRoot}/",
+            "sourceMaps": true,
+            "preLaunchTask": "run",
+            "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
+            "runtimeArgs": [
+                "--enable-unsafe-es3-apis"
+            ]
         },     
         {
             "name": "Launch Viewer (Chrome)",

+ 1 - 0
Playground/frame.html

@@ -39,6 +39,7 @@
         <script src="https://preview.babylonjs.com/babylon.js"></script>
         <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
         <script src="https://preview.babylonjs.com/nodeEditor/babylon.nodeEditor.js"></script>
+        <script src="https://preview.babylonjs.com/guiEditor/babylon.guiEditor.js"></script>
         <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
         <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
         <script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>

+ 1 - 0
Playground/full.html

@@ -39,6 +39,7 @@
         <script src="https://preview.babylonjs.com/babylon.js"></script>
         <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
         <script src="https://preview.babylonjs.com/nodeEditor/babylon.nodeEditor.js"></script>
+        <script src="https://preview.babylonjs.com/guiEditor/babylon.guiEditor.js"></script>
         <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
         <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
         <script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>

+ 1 - 0
Playground/index.html

@@ -45,6 +45,7 @@
         <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
         <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
         <script src="https://preview.babylonjs.com/nodeEditor/babylon.nodeEditor.js"></script>
+        <script src="https://preview.babylonjs.com/guiEditor/babylon.guiEditor.js"></script>
 
          <!-- Extensions -->
          <script

+ 1 - 0
Playground/index.js

@@ -5,6 +5,7 @@ var Versions = {
         "https://preview.babylonjs.com/gui/babylon.gui.min.js",
         "https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js",
         "https://preview.babylonjs.com/nodeEditor/babylon.nodeEditor.js",
+        "https://preview.babylonjs.com/guiEditor/babylon.guiEditor.js",
         "https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js",
         "https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js",
         "https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js",

+ 59 - 2
Tools/Config/config.json

@@ -48,7 +48,8 @@
         "serializers",
         "gui",
         "inspector",
-        "nodeEditor"
+        "nodeEditor",
+        "guiEditor"
     ],
     "es6modules": [
         "core",
@@ -60,7 +61,8 @@
         "gui",
         "inspector",
         "viewer",
-        "nodeEditor"
+        "nodeEditor",
+        "guiEditor"
     ],
     "apps": [
         "playground",
@@ -651,6 +653,61 @@
             }
         }
     },
+    "guiEditor": {
+        "libraries": [
+            {
+                "output": "babylon.guiEditor.js",
+                "entry": "./legacy/legacy.ts"
+            }
+        ],
+        "build": {            
+            "ignoreInWorkerMode": true,
+            "ignoreInTestMode": true,
+            "mainFolder": "./guiEditor/",
+            "uncheckedLintImports": [
+                "react",
+                "react-dom",
+                "dagre",
+                "re-resizable",
+                "glTF",
+                "file-saver"
+            ],
+            "sharedUiComponents": "src/sharedUiComponents/",
+            "umd": {
+                "packageName": "babylonjs-gui-editor",
+                "webpackRoot": "GUIEDITOR",
+                "processDeclaration": {
+                    "filename": "babylon.guiEditor.module.d.ts",
+                    "moduleName": "GUIEDITOR",
+                    "importsToRemove": [],
+                    "classMap": {
+                        "babylonjs": "BABYLON",
+                        "react": "React",
+                        "@babylonjs/core": "BABYLON",
+                        "@fortawesome": false,
+                        "react-contextmenu": false
+                    }
+                }
+            },
+            "es6": {
+                "webpackBuild": true,
+                "buildDependencies": [
+                    "node_modules/re-resizable/lib/index.es5.js",
+                    "Tools/**/*"
+                ],
+                "packageName": "@babylonjs/gui-editor",
+                "readme": "dist/preview release/guiEditor/readme-es6.md",
+                "packagesFiles": [
+                    "babylon.guiEditor.max.js",
+                    "babylon.guiEditor.max.js.map",
+                    "babylon.guiEditor.module.d.ts",
+                    "readme.md"
+                ],
+                "typings": "babylon.guiEditor.module.d.ts",
+                "index": "babylon.guiEditor.max.js"
+            }
+        }
+    },
     "ktx2Decoder": {
         "tempFileName": "babylon.ktx2Decoder.js",
         "distFile": "/dist/preview release/babylon.ktx2Decoder.js",

+ 885 - 0
dist/preview release/guiEditor/babylon.guiEditor.d.ts

@@ -0,0 +1,885 @@
+/// <reference types="react" />
+declare module GUIEDITOR {
+    export class BlockTools {
+        static GetGuiFromString(data: string): Slider | Checkbox | ColorPicker | Ellipse | Rectangle | Line | TextBlock;
+    }
+}
+declare module GUIEDITOR {
+    interface ILogComponentProps {
+        globalState: GlobalState;
+    }
+    export class LogEntry {
+        message: string;
+        isError: boolean;
+        constructor(message: string, isError: boolean);
+    }
+    export class LogComponent extends React.Component<ILogComponentProps, {
+        logs: LogEntry[];
+    }> {
+        constructor(props: ILogComponentProps);
+        componentDidMount(): void;
+        componentDidUpdate(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IWorkbenchComponentProps {
+        globalState: GlobalState;
+    }
+    export type FramePortData = {};
+    export const isFramePortData: (variableToCheck: any) => variableToCheck is FramePortData;
+    export class WorkbenchComponent extends React.Component<IWorkbenchComponentProps> {
+        private readonly MinZoom;
+        private readonly MaxZoom;
+        private _hostCanvas;
+        private _graphCanvas;
+        private _selectionContainer;
+        private _frameContainer;
+        private _svgCanvas;
+        private _rootContainer;
+        private _guiNodes;
+        private _mouseStartPointX;
+        private _mouseStartPointY;
+        private _selectionStartX;
+        private _selectionStartY;
+        private _x;
+        private _y;
+        private _zoom;
+        private _selectedGuiNodes;
+        private _gridSize;
+        private _selectionBox;
+        private _frameCandidate;
+        private _altKeyIsPressed;
+        private _ctrlKeyIsPressed;
+        private _oldY;
+        _frameIsMoving: boolean;
+        _isLoading: boolean;
+        isOverGUINode: boolean;
+        get gridSize(): number;
+        set gridSize(value: number);
+        get globalState(): GlobalState;
+        get nodes(): GUINode[];
+        get zoom(): number;
+        set zoom(value: number);
+        get x(): number;
+        set x(value: number);
+        get y(): number;
+        set y(value: number);
+        get selectedGuiNodes(): GUINode[];
+        get canvasContainer(): HTMLDivElement;
+        get hostCanvas(): HTMLDivElement;
+        get svgCanvas(): HTMLElement;
+        get selectionContainer(): HTMLDivElement;
+        get frameContainer(): HTMLDivElement;
+        constructor(props: IWorkbenchComponentProps);
+        getGridPosition(position: number, useCeil?: boolean): number;
+        getGridPositionCeil(position: number): number;
+        updateTransform(): void;
+        onKeyUp(): void;
+        findNodeFromGuiElement(guiElement: Control): GUINode;
+        reset(): void;
+        appendBlock(guiElement: Control): GUINode;
+        distributeGraph(): void;
+        componentDidMount(): void;
+        onMove(evt: React.PointerEvent): void;
+        onDown(evt: React.PointerEvent<HTMLElement>): void;
+        onUp(evt: React.PointerEvent): void;
+        onWheel(evt: React.WheelEvent): void;
+        zoomToFit(): void;
+        createGUICanvas(): void;
+        updateGUIs(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IPropertyComponentProps {
+        globalState: GlobalState;
+        guiBlock: Control;
+    }
+}
+declare module GUIEDITOR {
+    interface ILineContainerComponentProps {
+        title: string;
+        children: any[] | any;
+        closed?: boolean;
+    }
+    export class LineContainerComponent extends React.Component<ILineContainerComponentProps, {
+        isExpanded: boolean;
+    }> {
+        constructor(props: ILineContainerComponentProps);
+        switchExpandedState(): void;
+        renderHeader(): JSX.Element;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class PropertyChangedEvent {
+        object: any;
+        property: string;
+        value: any;
+        initialValue: any;
+    }
+}
+declare module GUIEDITOR {
+    export interface ICheckBoxLineComponentProps {
+        label: string;
+        target?: any;
+        propertyName?: string;
+        isSelected?: () => boolean;
+        onSelect?: (value: boolean) => void;
+        onValueChanged?: () => void;
+        onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
+        disabled?: boolean;
+    }
+    export class CheckBoxLineComponent extends React.Component<ICheckBoxLineComponentProps, {
+        isSelected: boolean;
+        isDisabled?: boolean;
+    }> {
+        private static _UniqueIdSeed;
+        private _uniqueId;
+        private _localChange;
+        constructor(props: ICheckBoxLineComponentProps);
+        shouldComponentUpdate(nextProps: ICheckBoxLineComponentProps, nextState: {
+            isSelected: boolean;
+            isDisabled: boolean;
+        }): boolean;
+        onChange(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IFloatLineComponentProps {
+        label: string;
+        target: any;
+        propertyName: string;
+        onChange?: (newValue: number) => void;
+        isInteger?: boolean;
+        onPropertyChangedObservable?: BABYLON.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;
+        private _store;
+        private _regExp;
+        constructor(props: IFloatLineComponentProps);
+        shouldComponentUpdate(nextProps: IFloatLineComponentProps, nextState: {
+            value: string;
+        }): boolean;
+        raiseOnPropertyChanged(newValue: number, previousValue: number): void;
+        updateValue(valueString: string): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    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?: BABYLON.Observable<PropertyChangedEvent>;
+        decimalCount?: number;
+        globalState: GlobalState;
+    }
+    export class SliderLineComponent extends React.Component<ISliderLineComponentProps, {
+        value: number;
+    }> {
+        private _localChange;
+        constructor(props: ISliderLineComponentProps);
+        shouldComponentUpdate(nextProps: ISliderLineComponentProps, nextState: {
+            value: number;
+        }): boolean;
+        onChange(newValueString: any): void;
+        onInput(newValueString: any): void;
+        prepareDataToRead(value: number): number;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class GenericPropertyComponent extends React.Component<IPropertyComponentProps> {
+        constructor(props: IPropertyComponentProps);
+        render(): JSX.Element;
+    }
+    export class GeneralPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+        constructor(props: IPropertyComponentProps);
+        render(): JSX.Element;
+    }
+    export class GenericPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+        constructor(props: IPropertyComponentProps);
+        forceRebuild(notifiers?: {
+            "rebuild"?: boolean;
+            "update"?: boolean;
+        }): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface ITextLineComponentProps {
+        label: string;
+        value: string;
+        color?: string;
+        underline?: boolean;
+        onLink?: () => void;
+    }
+    export class TextLineComponent extends React.Component<ITextLineComponentProps> {
+        constructor(props: ITextLineComponentProps);
+        onLink(): void;
+        renderContent(): JSX.Element;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    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: number;
+        };
+        private _localChange;
+        constructor(props: INumericInputComponentProps);
+        shouldComponentUpdate(nextProps: INumericInputComponentProps, nextState: {
+            value: string;
+        }): boolean;
+        updateValue(evt: any): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class SliderPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+        constructor(props: IPropertyComponentProps);
+        private slider;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class PropertyGuiLedger {
+        static RegisteredControls: {
+            [key: string]: React.ComponentClass<IPropertyComponentProps>;
+        };
+    }
+}
+declare module GUIEDITOR {
+    export class GUINode {
+        guiNode: Control;
+        private _x;
+        private _y;
+        private _gridAlignedX;
+        private _gridAlignedY;
+        private _globalState;
+        private _onSelectionChangedObserver;
+        private _onSelectionBoxMovedObserver;
+        private _onUpdateRequiredObserver;
+        private _ownerCanvas;
+        private _isSelected;
+        private _isVisible;
+        private _enclosingFrameId;
+        get isVisible(): boolean;
+        set isVisible(value: boolean);
+        get gridAlignedX(): number;
+        get gridAlignedY(): number;
+        get x(): number;
+        set x(value: number);
+        get y(): number;
+        set y(value: number);
+        get width(): number;
+        get height(): number;
+        get id(): number;
+        get name(): string | undefined;
+        get isSelected(): boolean;
+        get enclosingFrameId(): number;
+        set enclosingFrameId(value: number);
+        set isSelected(value: boolean);
+        constructor(globalState: GlobalState, guiNode: Control);
+        cleanAccumulation(useCeil?: boolean): void;
+        clicked: boolean;
+        _onMove(evt: BABYLON.Vector2, startPos: BABYLON.Vector2): boolean;
+        renderProperties(): BABYLON.Nullable<JSX.Element>;
+        updateVisual(): void;
+        appendVisual(root: HTMLDivElement, owner: WorkbenchComponent): void;
+        dispose(): void;
+    }
+}
+declare module GUIEDITOR {
+    export class GlobalState {
+        guiTexture: AdvancedDynamicTexture;
+        hostElement: HTMLElement;
+        hostDocument: HTMLDocument;
+        hostWindow: Window;
+        onSelectionChangedObservable: BABYLON.Observable<BABYLON.Nullable<FramePortData | GUINode>>;
+        onRebuildRequiredObservable: BABYLON.Observable<void>;
+        onBuiltObservable: BABYLON.Observable<void>;
+        onResetRequiredObservable: BABYLON.Observable<void>;
+        onUpdateRequiredObservable: BABYLON.Observable<void>;
+        onZoomToFitRequiredObservable: BABYLON.Observable<void>;
+        onReOrganizedRequiredObservable: BABYLON.Observable<void>;
+        onLogRequiredObservable: BABYLON.Observable<LogEntry>;
+        onErrorMessageDialogRequiredObservable: BABYLON.Observable<string>;
+        onIsLoadingChanged: BABYLON.Observable<boolean>;
+        onPreviewCommandActivated: BABYLON.Observable<boolean>;
+        onLightUpdated: BABYLON.Observable<void>;
+        onPreviewBackgroundChanged: BABYLON.Observable<void>;
+        onBackFaceCullingChanged: BABYLON.Observable<void>;
+        onDepthPrePassChanged: BABYLON.Observable<void>;
+        onAnimationCommandActivated: BABYLON.Observable<void>;
+        onCandidateLinkMoved: BABYLON.Observable<BABYLON.Nullable<BABYLON.Vector2>>;
+        onSelectionBoxMoved: BABYLON.Observable<DOMRect | ClientRect>;
+        onImportFrameObservable: BABYLON.Observable<any>;
+        onGraphNodeRemovalObservable: BABYLON.Observable<GUINode>;
+        onGetNodeFromBlock: (block: BABYLON.NodeMaterialBlock) => GUINode;
+        onGridSizeChanged: BABYLON.Observable<void>;
+        onExposePortOnFrameObservable: BABYLON.Observable<GUINode>;
+        previewFile: File;
+        listOfCustomPreviewFiles: File[];
+        rotatePreview: boolean;
+        backgroundColor: BABYLON.Color4;
+        backFaceCulling: boolean;
+        depthPrePass: boolean;
+        blockKeyboardEvents: boolean;
+        hemisphericLight: boolean;
+        directionalLight0: boolean;
+        directionalLight1: boolean;
+        controlCamera: boolean;
+        workbench: WorkbenchComponent;
+        storeEditorData: (serializationObject: any, frame?: BABYLON.Nullable<null>) => void;
+        customSave?: {
+            label: string;
+            action: (data: string) => Promise<void>;
+        };
+        constructor();
+    }
+}
+declare module GUIEDITOR {
+    export interface IButtonLineComponentProps {
+        data: string;
+        tooltip: string;
+    }
+    export class DraggableLineComponent extends React.Component<IButtonLineComponentProps> {
+        constructor(props: IButtonLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IGuiListComponentProps {
+        globalState: GlobalState;
+    }
+    export class GuiListComponent extends React.Component<IGuiListComponentProps, {
+        filter: string;
+    }> {
+        private _onResetRequiredObserver;
+        private static _Tooltips;
+        constructor(props: IGuiListComponentProps);
+        componentWillUnmount(): void;
+        filterContent(filter: string): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IButtonLineComponentProps {
+        label: string;
+        onClick: () => void;
+    }
+    export class ButtonLineComponent extends React.Component<IButtonLineComponentProps> {
+        constructor(props: IButtonLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IFileButtonLineComponentProps {
+        label: string;
+        onClick: (file: File) => void;
+        accept: string;
+        uploadName?: string;
+    }
+    export class FileButtonLineComponent extends React.Component<IFileButtonLineComponentProps> {
+        private uploadRef;
+        constructor(props: IFileButtonLineComponentProps);
+        onChange(evt: any): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class SerializationTools {
+        static UpdateLocations(material: BABYLON.NodeMaterial, globalState: GlobalState): void;
+        static Serialize(material: BABYLON.NodeMaterial, globalState: GlobalState): string;
+        static Deserialize(serializationObject: any, globalState: GlobalState): void;
+        static AddFrameToMaterial(serializationObject: any, globalState: GlobalState, currentMaterial: BABYLON.NodeMaterial): void;
+    }
+}
+declare module GUIEDITOR {
+    interface IPropertyTabComponentProps {
+        globalState: GlobalState;
+    }
+    interface IPropertyTabComponentState {
+        currentNode: BABYLON.Nullable<GUINode>;
+    }
+    export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, IPropertyTabComponentState> {
+        private _onBuiltObserver;
+        constructor(props: IPropertyTabComponentProps);
+        componentDidMount(): void;
+        componentWillUnmount(): void;
+        processInputBlockUpdate(ib: BABYLON.InputBlock): void;
+        load(file: File): void;
+        loadFrame(file: File): void;
+        save(): void;
+        customSave(): void;
+        saveToSnippetServer(): void;
+        loadFromSnippet(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IPortalProps {
+        globalState: GlobalState;
+    }
+    export class Portal extends React.Component<IPortalProps> {
+        render(): React.ReactPortal;
+    }
+}
+declare module GUIEDITOR {
+    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;
+        };
+    }
+}
+declare module GUIEDITOR {
+    interface IMessageDialogComponentProps {
+        globalState: GlobalState;
+    }
+    export class MessageDialogComponent extends React.Component<IMessageDialogComponentProps, {
+        message: string;
+        isError: boolean;
+    }> {
+        constructor(props: IMessageDialogComponentProps);
+        render(): JSX.Element | null;
+    }
+}
+declare module GUIEDITOR {
+    interface IGraphEditorProps {
+        globalState: GlobalState;
+    }
+    interface IGraphEditorState {
+        showPreviewPopUp: boolean;
+    }
+    export class WorkbenchEditor extends React.Component<IGraphEditorProps, IGraphEditorState> {
+        private _workbenchCanvas;
+        private _startX;
+        private _moveInProgress;
+        private _leftWidth;
+        private _rightWidth;
+        private _blocks;
+        private _onWidgetKeyUpPointer;
+        private _popUpWindow;
+        /**
+         * Creates a node and recursivly creates its parent nodes from it's input
+         * @param block
+         */
+        createNodeFromObject(block: Control, recursion?: boolean): BABYLON.Nullable<GUINode>;
+        componentDidMount(): void;
+        componentWillUnmount(): void;
+        constructor(props: IGraphEditorProps);
+        pasteSelection(copiedNodes: GUINode[], currentX: number, currentY: number, selectNew?: boolean): GUINode[];
+        zoomToFit(): void;
+        buildMaterial(): void;
+        showWaitScreen(): void;
+        hideWaitScreen(): void;
+        reOrganize(editorData?: BABYLON.Nullable<IEditorData>, isImportingAFrame?: boolean): void;
+        onPointerDown(evt: React.PointerEvent<HTMLDivElement>): void;
+        onPointerUp(evt: React.PointerEvent<HTMLDivElement>): void;
+        resizeColumns(evt: React.PointerEvent<HTMLDivElement>, forLeft?: boolean): void;
+        buildColumnLayout(): string;
+        emitNewBlock(event: React.DragEvent<HTMLDivElement>): void;
+        handlePopUp: () => void;
+        handleClosingPopUp: () => void;
+        createPopupWindow: (title: string, windowVariableName: string, width?: number, height?: number) => Window | null;
+        copyStyles: (sourceDoc: HTMLDocument, targetDoc: HTMLDocument) => void;
+        fixPopUpStyles: (document: Document) => void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export class Popup {
+        static CreatePopup(title: string, windowVariableName: string, width?: number, height?: number): HTMLDivElement | null;
+        private static _CopyStyles;
+    }
+}
+declare module GUIEDITOR {
+    /**
+     * Interface used to specify creation options for the gui editor
+     */
+    export interface INodeEditorOptions {
+        hostElement?: HTMLElement;
+        customSave?: {
+            label: string;
+            action: (data: string) => Promise<void>;
+        };
+        customLoadObservable?: BABYLON.Observable<any>;
+    }
+    /**
+     * Class used to create a gui editor
+     */
+    export class GuiEditor {
+        private static _CurrentState;
+        /**
+         * Show the gui editor
+         * @param options defines the options to use to configure the gui editor
+         */
+        static Show(options: INodeEditorOptions): void;
+    }
+}
+declare module GUIEDITOR {
+    export class StringTools {
+        private static _SaveAs;
+        private static _Click;
+        /**
+         * Gets the base math type of node material block connection point.
+         * @param type Type to parse.
+         */
+        static GetBaseType(type: BABYLON.NodeMaterialBlockConnectionPointTypes): string;
+        /**
+         * Download a string into a file that will be saved locally by the browser
+         * @param content defines the string to download locally as a file
+         */
+        static DownloadAsFile(document: HTMLDocument, content: string, filename: string): void;
+    }
+}
+declare module GUIEDITOR {
+    interface IFloatPropertyTabComponentProps {
+        globalState: GlobalState;
+        inputBlock: BABYLON.InputBlock;
+    }
+    export class FloatPropertyTabComponent extends React.Component<IFloatPropertyTabComponentProps> {
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    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;
+        constructor(props: ILineWithFileButtonComponentProps);
+        onChange(evt: any): void;
+        switchExpandedState(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface ITextInputLineComponentProps {
+        label: string;
+        globalState: GlobalState;
+        target?: any;
+        propertyName?: string;
+        value?: string;
+        multilines?: boolean;
+        onChange?: (value: string) => void;
+        validator?: (value: string) => boolean;
+        onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
+    }
+    export class TextInputLineComponent extends React.Component<ITextInputLineComponentProps, {
+        value: string;
+    }> {
+        private _localChange;
+        constructor(props: ITextInputLineComponentProps);
+        shouldComponentUpdate(nextProps: ITextInputLineComponentProps, nextState: {
+            value: string;
+        }): boolean;
+        raiseOnPropertyChanged(newValue: string, previousValue: string): void;
+        updateValue(value: string, raisePropertyChanged: boolean): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IColorComponentEntryProps {
+        value: number;
+        label: string;
+        max?: number;
+        min?: number;
+        onChange: (value: number) => void;
+    }
+    export class ColorComponentEntry extends React.Component<IColorComponentEntryProps> {
+        constructor(props: IColorComponentEntryProps);
+        updateValue(valueString: string): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IHexColorProps {
+        value: string;
+        expectedLength: number;
+        onChange: (value: string) => void;
+    }
+    export class HexColor extends React.Component<IHexColorProps, {
+        hex: string;
+    }> {
+        constructor(props: IHexColorProps);
+        shouldComponentUpdate(nextProps: IHexColorProps, nextState: {
+            hex: string;
+        }): boolean;
+        updateHexValue(valueString: string): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    /**
+     * Interface used to specify creation options for color picker
+     */
+    export interface IColorPickerProps {
+        color: BABYLON.Color3 | BABYLON.Color4;
+        debugMode?: boolean;
+        onColorChanged?: (color: BABYLON.Color3 | BABYLON.Color4) => void;
+    }
+    /**
+     * Interface used to specify creation options for color picker
+     */
+    export interface IColorPickerState {
+        color: BABYLON.Color3;
+        alpha: number;
+    }
+    /**
+     * Class used to create a color picker
+     */
+    export class ColorPicker extends React.Component<IColorPickerProps, IColorPickerState> {
+        private _saturationRef;
+        private _hueRef;
+        private _isSaturationPointerDown;
+        private _isHuePointerDown;
+        constructor(props: IColorPickerProps);
+        onSaturationPointerDown(evt: React.PointerEvent<HTMLDivElement>): void;
+        onSaturationPointerUp(evt: React.PointerEvent<HTMLDivElement>): void;
+        onSaturationPointerMove(evt: React.PointerEvent<HTMLDivElement>): void;
+        onHuePointerDown(evt: React.PointerEvent<HTMLDivElement>): void;
+        onHuePointerUp(evt: React.PointerEvent<HTMLDivElement>): void;
+        onHuePointerMove(evt: React.PointerEvent<HTMLDivElement>): void;
+        private _evaluateSaturation;
+        private _evaluateHue;
+        componentDidUpdate(): void;
+        raiseOnColorChanged(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IBooleanLineComponentProps {
+        label: string;
+        value: boolean;
+    }
+    export class BooleanLineComponent extends React.Component<IBooleanLineComponentProps> {
+        constructor(props: IBooleanLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IButtonLineComponentProps {
+        label: string;
+        onClick: () => void;
+    }
+    export class ButtonLineComponent extends React.Component<IButtonLineComponentProps> {
+        constructor(props: IButtonLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IFileButtonLineComponentProps {
+        label: string;
+        onClick: (file: File) => void;
+        accept: string;
+    }
+    export class FileButtonLineComponent extends React.Component<IFileButtonLineComponentProps> {
+        private static _IDGenerator;
+        private _id;
+        private uploadInputRef;
+        constructor(props: IFileButtonLineComponentProps);
+        onChange(evt: any): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IFileMultipleButtonLineComponentProps {
+        label: string;
+        onClick: (event: any) => void;
+        accept: string;
+    }
+    export class FileMultipleButtonLineComponent extends React.Component<IFileMultipleButtonLineComponentProps> {
+        private static _IDGenerator;
+        private _id;
+        private uploadInputRef;
+        constructor(props: IFileMultipleButtonLineComponentProps);
+        onChange(evt: any): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    export interface IIconButtonLineComponentProps {
+        icon: string;
+        onClick: () => void;
+        tooltip: string;
+        active?: boolean;
+    }
+    export class IconButtonLineComponent extends React.Component<IIconButtonLineComponentProps> {
+        constructor(props: IIconButtonLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IIndentedTextLineComponentProps {
+        value?: string;
+        color?: string;
+        underline?: boolean;
+        onLink?: () => void;
+        url?: string;
+        additionalClass?: string;
+    }
+    export class IndentedTextLineComponent extends React.Component<IIndentedTextLineComponentProps> {
+        constructor(props: IIndentedTextLineComponentProps);
+        onLink(): void;
+        renderContent(): JSX.Element;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface ILinkButtonComponentProps {
+        label: string;
+        buttonLabel: string;
+        url?: string;
+        onClick: () => void;
+        onIconClick?: () => void;
+    }
+    export class LinkButtonComponent extends React.Component<ILinkButtonComponentProps> {
+        constructor(props: ILinkButtonComponentProps);
+        onLink(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IMessageLineComponentProps {
+        text: string;
+        color?: string;
+    }
+    export class MessageLineComponent extends React.Component<IMessageLineComponentProps> {
+        constructor(props: IMessageLineComponentProps);
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface INumericInputComponentProps {
+        label: string;
+        value: number;
+        step?: number;
+        onChange: (value: number) => void;
+        precision?: number;
+    }
+    export class NumericInputComponent extends React.Component<INumericInputComponentProps, {
+        value: string;
+    }> {
+        static defaultProps: {
+            step: number;
+        };
+        private _localChange;
+        constructor(props: INumericInputComponentProps);
+        shouldComponentUpdate(nextProps: INumericInputComponentProps, nextState: {
+            value: string;
+        }): boolean;
+        updateValue(evt: any): void;
+        onBlur(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IRadioButtonLineComponentProps {
+        onSelectionChangedObservable: BABYLON.Observable<RadioButtonLineComponent>;
+        label: string;
+        isSelected: () => boolean;
+        onSelect: () => void;
+    }
+    export class RadioButtonLineComponent extends React.Component<IRadioButtonLineComponentProps, {
+        isSelected: boolean;
+    }> {
+        private _onSelectionChangedObserver;
+        constructor(props: IRadioButtonLineComponentProps);
+        componentDidMount(): void;
+        componentWillUnmount(): void;
+        onChange(): void;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface ITextLineComponentProps {
+        label?: string;
+        value?: string;
+        color?: string;
+        underline?: boolean;
+        onLink?: () => void;
+        url?: string;
+        ignoreValue?: boolean;
+        additionalClass?: string;
+    }
+    export class TextLineComponent extends React.Component<ITextLineComponentProps> {
+        constructor(props: ITextLineComponentProps);
+        onLink(): void;
+        renderContent(): JSX.Element | null;
+        render(): JSX.Element;
+    }
+}
+declare module GUIEDITOR {
+    interface IValueLineComponentProps {
+        label: string;
+        value: number;
+        color?: string;
+        fractionDigits?: number;
+        units?: string;
+    }
+    export class ValueLineComponent extends React.Component<IValueLineComponentProps> {
+        constructor(props: IValueLineComponentProps);
+        render(): JSX.Element;
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 30 - 0
dist/preview release/guiEditor/babylon.guiEditor.js


Разница между файлами не показана из-за своего большого размера
+ 46373 - 0
dist/preview release/guiEditor/babylon.guiEditor.max.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
dist/preview release/guiEditor/babylon.guiEditor.max.js.map


Разница между файлами не показана из-за своего большого размера
+ 1878 - 0
dist/preview release/guiEditor/babylon.guiEditor.module.d.ts


+ 27 - 0
dist/preview release/guiEditor/package.json

@@ -0,0 +1,27 @@
+{
+    "author": {
+        "name": "David CATUHE"
+    },
+    "name": "babylonjs-gui-editor",
+    "description": "The Babylon.js GUI editor.",
+    "version": "5.0.0-alpha.3",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/BabylonJS/Babylon.js.git"
+    },
+    "license": "Apache-2.0",
+    "dependencies": {
+        "babylonjs": "5.0.0-alpha.3"
+    },
+    "files": [
+        "babylon.guiEditor.max.js.map",
+        "babylon.guiEditor.max.js",
+        "babylon.guiEditor.js",
+        "babylon.guiEditor.module.d.ts",
+		"readme.md",
+        "package.json"
+    ],
+    "engines": {
+        "node": "*"
+    }
+}

+ 15 - 0
dist/preview release/guiEditor/readme-es6.md

@@ -0,0 +1,15 @@
+Gui Editor es6
+
+# Babylon.js Gui Editor
+
+An extension to easily allow users to create and modify GUI for scenes.
+
+## Usage
+Currently avalible for local development by selecting "Launch GUI Editor (Chrome)"
+
+## Current Supported Features
+
+- Launch GUI editor in local dev mode.
+- Drag and drop GUI elements onto a canvas.
+- Select and move individual GUI elements.
+- Modify properties of selected GUI elements.

+ 1 - 0
dist/preview release/guiEditor/readme.md

@@ -0,0 +1 @@
+Gui Editor

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

@@ -50,6 +50,10 @@
 - Increased float precision to 4([msDestiny14](https://github.com/msDestiny14))
 - Added ability to make input node's properties visible in the properties of a custom frame ([msDestiny14](https://github.com/msDestiny14))
 
+### GUIEditor
+
+- Added GUI Editor project to master. ([msDestiny14](https://github.com/msDestiny14))
+
 ### GUI
 
 - Added a `FocusableButton` gui control to simplify creating menus with keyboard navigation ([Flux159](https://github.com/Flux159))

+ 1 - 0
guiEditor/README-ES6.md

@@ -0,0 +1 @@
+Gui Editor

+ 18 - 0
guiEditor/README.md

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

+ 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>Gui 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 Gui 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/guiEditor/babylon.guiEditor.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>

+ 116 - 0
guiEditor/public/index.js

@@ -0,0 +1,116 @@
+var snippetUrl = "https://snippet.babylonjs.com";
+var currentSnippetToken;
+var previousHash = "";
+
+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) {
+                            
+                            //TODO: Implement
+                            //var snippet = JSON.parse(JSON.parse(xmlHttp.responseText).jsonPayload);
+                            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.GuiEditor.Show({
+        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 gui layout. It may be too large (${(dataToSend.payload.length / 1024).toFixed(2)}`);
+                            }
+                        }
+                    }
+        
+                    xmlHttp.open("POST", snippetUrl + (currentSnippetToken ? "/" + currentSnippetToken : ""), true);
+                    xmlHttp.setRequestHeader("Content-Type", "application/json");
+        
+                    var dataToSend = {
+                        payload : JSON.stringify({
+                            guiLayout: 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);
+    var scene = new BABYLON.Scene(engine);
+
+    // Set to default
+    if (!location.hash) {
+        showEditor();
+    }
+}
+else {
+    alert('Babylon.js is not supported.')
+}
+
+checkHash();

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

@@ -0,0 +1,211 @@
+#guiList {
+    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);
+                        }
+                    }
+                }
+            }    
+        }
+    }
+}
+

+ 89 - 0
guiEditor/src/components/guiList/guiListComponent.tsx

@@ -0,0 +1,89 @@
+import * as React from "react";
+import { GlobalState } from "../../globalState";
+import { LineContainerComponent } from "../../sharedComponents/lineContainerComponent";
+import { DraggableLineComponent } from "../../sharedUiComponents/lines/draggableLineComponent";
+import { Observer } from "babylonjs/Misc/observable";
+import { Nullable } from "babylonjs/types";
+
+require("./guiList.scss");
+
+interface IGuiListComponentProps {
+    globalState: GlobalState;
+}
+
+export class GuiListComponent extends React.Component<IGuiListComponentProps, { filter: string }> {
+    private _onResetRequiredObserver: Nullable<Observer<void>>;
+
+    private static _Tooltips: { [key: string]: string } = {
+        Button: "A simple button",
+    };
+
+    constructor(props: IGuiListComponentProps) {
+        super(props);
+
+        this.state = { filter: "" };
+
+        this._onResetRequiredObserver = this.props.globalState.onResetRequiredObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onResetRequiredObservable.remove(this._onResetRequiredObserver);
+    }
+
+    filterContent(filter: string) {
+        this.setState({ filter: filter });
+    }
+
+    render() {
+        // Block types used to create the menu from
+        const allBlocks: any = {
+            Buttons: ["TextButton", "ImageButton"],
+            Controls: ["Slider", "Checkbox", "ColorPicker", "VisualKeyboard"],
+            Containers: ["DisplayGrid", "Grid", "StackPanel"],
+            Shapes: ["Ellipse", "Image", "Line", "Rectangle"],
+            Inputs: ["Text", "IntputText", "InputPassword"],
+        };
+
+        // 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) => {
+                    return <DraggableLineComponent key={block} data={block} tooltip={GuiListComponent._Tooltips[block] || ""} />;
+                });
+
+            if (blockList.length) {
+                blockMenu.push(
+                    <LineContainerComponent key={key + " blocks"} title={key.replace("__", ": ").replace("_", " ")} closed={false}>
+                        {blockList}
+                    </LineContainerComponent>
+                );
+            }
+        }
+
+        return (
+            <div id="guiList">
+                <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>
+        );
+    }
+}

+ 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;
+        }
+    }
+}

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

@@ -0,0 +1,63 @@
+
+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 newLogArray = this.state.logs.map(number => number);
+            newLogArray.push(log);
+            this.setState({ logs: newLogArray });
+        });
+    }
+
+    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>
+        );
+    }
+}

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

@@ -0,0 +1,798 @@
+#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;
+        }  
+    }
+
+    .numeric {
+        padding-left: $line-padding-left;
+        height: 30px;
+        display: grid;
+        grid-template-columns: 1fr 120px auto;
+
+        .numeric-label {
+            grid-column: 1;
+            display: flex;
+            align-items: center;
+        }
+
+        .number {                        
+            display: flex;
+            align-items: center;
+            grid-column: 2;
+            height: 10px;
+            .input {
+                width: calc(100% - 5px);
+                height: 10px;
+            }
+        }
+    }
+    
+    .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;
+            }  
+        }
+    }    
+
+}

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

@@ -0,0 +1,228 @@
+
+import * as React from "react";
+import { GlobalState } from '../../globalState';
+import { Nullable } from 'babylonjs/types';
+import { ButtonLineComponent } from '../../sharedUiComponents/lines/buttonLineComponent';
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { FileButtonLineComponent } from '../../sharedUiComponents/lines/fileButtonLineComponent';
+import { Tools } from 'babylonjs/Misc/tools';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { GUINode } from '../../diagram/guiNode';
+import { Observer } from 'babylonjs/Misc/observable';
+import { TextLineComponent } from "../../sharedUiComponents/lines/textLineComponent";
+import { SerializationTools } from "../../serializationTools";
+import { Engine } from "babylonjs/Engines/engine";
+
+require("./propertyTab.scss");
+
+interface IPropertyTabComponentProps {
+    globalState: GlobalState;
+}
+
+interface IPropertyTabComponentState {
+    currentNode: Nullable<GUINode>;
+ }
+
+export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, IPropertyTabComponentState> {
+    private _onBuiltObserver: Nullable<Observer<void>>;
+
+    constructor(props: IPropertyTabComponentProps) {
+        super(props);
+
+        this.state = { currentNode: null};
+    }
+
+    componentDidMount() {
+        this.props.globalState.onSelectionChangedObservable.add((selection) => {
+            if (selection instanceof GUINode) {
+                this.setState({ currentNode: selection});
+            } else {
+                this.setState({ currentNode: null });
+            }
+        });
+
+        this._onBuiltObserver = this.props.globalState.onBuiltObservable.add(() => {
+            this.forceUpdate();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onBuiltObservable.remove(this._onBuiltObserver);
+    }
+
+
+    load(file: File) {
+        Tools.ReadFile(file, (data) => {
+            let decoder = new TextDecoder("utf-8");
+            SerializationTools.Deserialize(JSON.parse(decoder.decode(data)), this.props.globalState);
+
+            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);
+        });*/
+    }
+
+
+    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">
+                            GUI EDITOR
+                        </div>
+                    </div>
+                    {this.state.currentNode?.renderProperties()}
+                </div>
+            );
+        }
+
+        return (
+            <div id="propertyTab">
+                <div id="header">
+                    <img id="logo" src="https://www.babylonjs.com/Assets/logo-babylonjs-social-twitter.png" />
+                    <div id="title">
+                        GUI EDITOR
+                    </div>
+                </div>
+                <div>
+                    <LineContainerComponent title="GENERAL">
+                        <TextLineComponent label="Version" value={Engine.Version}/>
+                        <TextLineComponent label="Help" value="doc.babylonjs.com" underline={true} onLink={() => window.open('https://doc.babylonjs.com', '_blank')}/>
+                        <ButtonLineComponent label="Reset to default" onClick={() => {
+                            this.props.globalState.onResetRequiredObservable.notifyObservers();
+                        }} />
+                    </LineContainerComponent>
+                    <LineContainerComponent title="OPTIONS">
+                        <CheckBoxLineComponent label="Show grid"
+                            isSelected={() => DataStorage.ReadBoolean("ShowGrid", true)}
+                            onSelect={(value: boolean) => {
+                                DataStorage.WriteBoolean("ShowGrid", value);
+                            }}
+                        />
+                    </LineContainerComponent>
+                    <LineContainerComponent title="FILE">
+                        <FileButtonLineComponent label="Load" onClick={(file) => this.load(file)} accept=".json" />
+                        <ButtonLineComponent label="Save" onClick={() => {
+                            this.save();
+                        }} />
+                        {
+                            this.props.globalState.customSave &&
+                            <ButtonLineComponent label={this.props.globalState.customSave!.label} onClick={() => {
+                                this.customSave();
+                            }} />
+                        }
+                    </LineContainerComponent>
+                    {
+                        !this.props.globalState.customSave &&
+                        <LineContainerComponent title="SNIPPET">
+                            <ButtonLineComponent label="Load from snippet server" onClick={() => this.loadFromSnippet()} />
+                            <ButtonLineComponent label="Save to snippet server" onClick={() => {
+                                this.saveToSnippetServer();
+                            }} />
+                        </LineContainerComponent>
+                    }
+                </div>
+            </div>
+        );
+    }
+}

+ 198 - 0
guiEditor/src/diagram/guiNode.ts

@@ -0,0 +1,198 @@
+import { GlobalState } from '../globalState';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+import { WorkbenchComponent, FramePortData } from './workbench';
+import { PropertyGuiLedger } from './propertyLedger';
+import * as React from 'react';
+import { GenericPropertyComponent } from './properties/genericNodePropertyComponent';
+import { Control } from 'babylonjs-gui/2D/controls/control';
+import { Vector2 } from 'babylonjs/Maths/math.vector';
+
+export class GUINode {
+    private _x = 0;
+    private _y = 0;
+    private _gridAlignedX = 0;
+    private _gridAlignedY = 0;    
+    private _globalState: GlobalState;
+    private _onSelectionChangedObserver: Nullable<Observer<Nullable<GUINode | FramePortData>>>;  
+    private _onSelectionBoxMovedObserver: Nullable<Observer<ClientRect | DOMRect>>;   
+    private _onUpdateRequiredObserver: Nullable<Observer<void>>;  
+    private _ownerCanvas: WorkbenchComponent; 
+    private _isSelected: boolean;
+    private _isVisible = true;
+    private _enclosingFrameId = -1;
+
+    public get isVisible() {
+        return this._isVisible;
+    }
+
+    public set isVisible(value: boolean) {
+        this._isVisible = value;
+    }
+
+    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);
+    }
+
+    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);
+    }
+
+    public get width() {
+        return this.guiControl.widthInPixels;
+    }
+
+    public get height() {
+        return this.guiControl.heightInPixels;
+    }
+
+    public get id() {
+        return this.guiControl.uniqueId;
+    }
+
+    public get name() {
+        return this.guiControl.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) {
+        this._isSelected = value;
+
+        if (value) {
+            this._globalState.onSelectionChangedObservable.notifyObservers(this);  
+        }
+    }
+
+    public constructor(globalState: GlobalState, public guiControl: Control) {
+        this._globalState = globalState;
+        this._ownerCanvas = this._globalState.workbench;
+        
+        guiControl.onPointerUpObservable.add(evt => {
+            this.clicked = false;
+            console.log("up");
+        });
+
+        guiControl.onPointerDownObservable.add( evt => {
+            this.clicked = true;
+            this.isSelected = true;
+            console.log("down");
+        }
+        );
+
+        guiControl.onPointerEnterObservable.add( evt => {
+            this._ownerCanvas.isOverGUINode = true;
+            console.log("in");
+        }
+        );
+
+        guiControl.onPointerOutObservable.add( evt => {
+            this._ownerCanvas.isOverGUINode = false;
+            console.log("out");
+        }
+        );
+
+        //TODO: Implement
+        this._onSelectionBoxMovedObserver = this._globalState.onSelectionBoxMoved.add(rect1 => {
+        });
+
+    }
+
+    public cleanAccumulation(useCeil = false) {
+        this.x = this._ownerCanvas.getGridPosition(this.x, useCeil);
+        this.y = this._ownerCanvas.getGridPosition(this.y, useCeil);
+    }
+
+    public clicked: boolean;
+    public _onMove(evt: Vector2, startPos: Vector2) {
+       
+        if(!this.clicked) return false;
+        console.log("moving");
+
+        //TODO: Implement move with zoom factor.
+        let newX = (evt.x - startPos.x) ;// / this._ownerCanvas.zoom;
+        let newY = (evt.y - startPos.y) ;// / this._ownerCanvas.zoom;
+
+        this.x += newX;
+        this.y += newY;  
+
+        return true;
+        //evt.stopPropagation();
+    }
+
+    public renderProperties(): Nullable<JSX.Element> {
+        let className = this.guiControl.getClassName();
+        let control = PropertyGuiLedger.RegisteredControls[className];
+        
+        if (!control) {
+            control = GenericPropertyComponent;
+        }
+
+        return React.createElement(control, {
+        globalState: this._globalState,
+        guiControl: this.guiControl
+        });
+    }
+
+    public updateVisual()
+    {
+        this.guiControl.leftInPixels = this.x;
+        this.guiControl.topInPixels = this.y;
+    }
+
+    public dispose() {
+        // notify frame observers that this node is being deleted
+        this._globalState.onGuiNodeRemovalObservable.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);
+        }
+
+        this.guiControl.dispose();   
+    }
+}

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

@@ -0,0 +1,113 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
+import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
+import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
+import { PropertyTypeForEdition, IPropertyDescriptionForEdition } from 'babylonjs/Materials/Node/nodeMaterialDecorator';
+
+export class GenericPropertyComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <>
+                <GeneralPropertyTabComponent globalState={this.props.globalState} guiControl={this.props.guiControl}/>
+                <GenericPropertyTabComponent globalState={this.props.globalState} guiControl={this.props.guiControl}/>
+            </>
+        );
+    }
+}
+
+export class GeneralPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <>
+                <LineContainerComponent title="GENERAL">
+                </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.guiControl,
+              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.guiControl} 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.guiControl} onChange={() => this.forceRebuild(options.notifiers)} />
+                        );
+                    } else {
+                        components.push(
+                            <SliderLineComponent label={displayName} target={this.props.guiControl} 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;
+                }
+            }
+        }
+
+        return (
+            <>
+            {
+                groups.map((group) =>
+                    <LineContainerComponent title={group}>
+                        {componentList[group]}
+                    </LineContainerComponent>
+                )
+            }
+            </>
+        );
+    }
+}

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

@@ -0,0 +1,7 @@
+import { Control } from "babylonjs-gui/2D/controls/control";
+import { GlobalState } from "../../globalState";
+
+export interface IPropertyComponentProps {
+    globalState: GlobalState;
+    guiControl: Control;
+}

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

@@ -0,0 +1,55 @@
+
+import * as React from "react";
+import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
+import { IPropertyComponentProps } from './propertyComponentProps';
+import { GeneralPropertyTabComponent } from './genericNodePropertyComponent';
+import { NumericInputComponent } from '../../sharedComponents/numericInputComponent';
+import { Slider } from "babylonjs-gui/2D/controls/sliders/slider";
+import { TextLineComponent } from "../../sharedUiComponents/lines/textLineComponent";
+
+
+export class SliderPropertyTabComponent extends React.Component<IPropertyComponentProps> {
+    constructor(props: IPropertyComponentProps) {
+        super(props);
+        this._slider = this.props.guiControl as Slider;
+    }
+
+    private _slider : Slider;
+
+    render() {
+        return (
+            <>                
+                <GeneralPropertyTabComponent globalState={this.props.globalState} guiControl={this.props.guiControl}/>
+                <LineContainerComponent title="PROPERTIES"> 
+                <NumericInputComponent globalState={this.props.globalState} label="Minimum Value" value={this._slider.minimum}
+                onChange={evt =>{
+                    this._slider.minimum = evt;
+                }}>
+                </NumericInputComponent>
+                <NumericInputComponent globalState={this.props.globalState} label="Maximum Value" value={this._slider.maximum} 
+                onChange={evt =>{
+                   this._slider.maximum = evt;
+                }}>
+
+                </NumericInputComponent>
+                <NumericInputComponent globalState={this.props.globalState} label="Value" value={this._slider.value} 
+                onChange={evt =>{
+                   this._slider.value = evt;
+                }}>
+                </NumericInputComponent>
+                <NumericInputComponent globalState={this.props.globalState} label="Height" value={this._slider.heightInPixels} 
+                onChange={evt =>{
+                   this._slider.height = evt;
+                }}>
+                </NumericInputComponent>
+                <NumericInputComponent globalState={this.props.globalState} label="Width" value={this._slider.widthInPixels} 
+                onChange={evt =>{
+                   this._slider.width = evt;
+                }}>
+                </NumericInputComponent>
+                <TextLineComponent label="Color" value={this._slider.background} />
+                </LineContainerComponent>            
+            </>
+        );
+    }
+}

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

@@ -0,0 +1,10 @@
+import { ComponentClass } from 'react';
+
+import { IPropertyComponentProps } from './properties/propertyComponentProps';
+import { SliderPropertyTabComponent } from './properties/sliderGuiPropertyComponent';
+
+export class PropertyGuiLedger {
+    public static RegisteredControls: {[key: string] : ComponentClass<IPropertyComponentProps>} = {};
+}
+
+PropertyGuiLedger.RegisteredControls["Slider"] = SliderPropertyTabComponent;

+ 541 - 0
guiEditor/src/diagram/workbench.tsx

@@ -0,0 +1,541 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+import { GUINode } from './guiNode';
+import * as dagre from 'dagre';
+import { Nullable } from 'babylonjs/types';
+
+import { DataStorage } from 'babylonjs/Misc/dataStorage';
+
+import {Control} from 'babylonjs-gui/2D/controls/control';
+import { AdvancedDynamicTexture } from "babylonjs-gui/2D/advancedDynamicTexture";
+import { Vector2, Vector3 } from "babylonjs/Maths/math.vector";
+import { Engine } from "babylonjs/Engines/engine";
+import { Scene } from "babylonjs/scene";
+import { Color4 } from "babylonjs/Maths/math.color";
+import { FreeCamera } from "babylonjs/Cameras/freeCamera";
+
+require("./workbenchCanvas.scss");
+
+export interface IWorkbenchComponentProps {
+    globalState: GlobalState
+}
+
+export type FramePortData = {
+}
+
+export const isFramePortData = (variableToCheck: any): variableToCheck is FramePortData => {
+    if (variableToCheck) {
+        return (variableToCheck as FramePortData) !== undefined;
+    }
+    else return false;
+}
+
+export class WorkbenchComponent extends React.Component<IWorkbenchComponentProps> {
+    private readonly MinZoom = 0.1;
+    private readonly MaxZoom = 4;
+
+    private _hostCanvas: HTMLDivElement;
+    private _gridCanvas: HTMLDivElement;
+    private _selectionContainer: HTMLDivElement;
+    private _frameContainer: HTMLDivElement;
+    private _svgCanvas: HTMLElement;
+    private _rootContainer: HTMLDivElement;
+    private _guiNodes: GUINode[] = [];
+    private _mouseStartPointX: Nullable<number> = null;
+    private _mouseStartPointY: Nullable<number> = null
+    private _selectionStartX = 0;
+    private _selectionStartY = 0;
+    private _x = 0;
+    private _y = 0;
+    private _zoom = 1;
+    private _selectedGuiNodes: GUINode[] = [];
+    private _gridSize = 20;
+    private _selectionBox: Nullable<HTMLDivElement> = null;    
+    private _frameCandidate: Nullable<HTMLDivElement> = null;
+
+    private _altKeyIsPressed = false;
+    private _ctrlKeyIsPressed = false;
+    private _oldY = -1;
+
+    public _frameIsMoving = false;
+    public _isLoading = false;
+    public isOverGUINode = 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._guiNodes;
+    }
+
+    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 selectedGuiNodes() {
+        return this._selectedGuiNodes;
+    }
+
+    public get canvasContainer() {
+        return this._gridCanvas;
+    }
+
+    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: IWorkbenchComponentProps) {
+        super(props);
+
+        props.globalState.onSelectionChangedObservable.add(selection => {  
+            console.log(selection);
+            
+            this.selectedGuiNodes.forEach(element => {
+                element.isSelected = false;
+            }); 
+            if (!selection) {
+                this._selectedGuiNodes = [];
+            } 
+            else {
+                if (selection instanceof GUINode){
+                    if (this._ctrlKeyIsPressed) {
+                        if (this._selectedGuiNodes.indexOf(selection) === -1) {
+                            this._selectedGuiNodes.push(selection);
+                        }
+                    } 
+                    else {              
+                        this._selectedGuiNodes = [selection];
+                    }
+                
+                } 
+            }
+        });
+
+
+        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) => {
+            editorData.x = this.x;
+            editorData.y = this.y;
+            editorData.zoom = this.zoom;
+        }
+        this.props.globalState.workbench = this;
+    }
+
+    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;
+    }
+
+    findNodeFromGuiElement(guiControl: Control) {
+       return this._guiNodes.filter(n => n.guiControl === guiControl)[0];
+    }
+
+    reset() {
+        for (var node of this._guiNodes) {
+            node.dispose();
+        }
+        this._guiNodes = [];
+        this._gridCanvas.innerHTML = "";
+        this._svgCanvas.innerHTML = "";
+    }
+
+    appendBlock(guiElement: Control) {
+        var newGuiNode = new GUINode(this.props.globalState, guiElement);
+        this._guiNodes.push(newGuiNode);
+        this.globalState.guiTexture.addControl(guiElement);  
+        return newGuiNode;
+    }
+
+    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._guiNodes.forEach(node => {
+
+
+            graph.setNode(node.id.toString(), {
+                id: node.id,
+                type: "node",
+                width: node.width,
+                height: node.height
+            });
+        });
+
+        // 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._guiNodes) {
+                    if (node.id === dagreNode.id) {
+                        node.x = dagreNode.x - dagreNode.width / 2;
+                        node.y = dagreNode.y - dagreNode.height / 2;
+                        node.cleanAccumulation();
+                        return;
+                    }
+                }
+                return;
+            }
+        });        
+    }
+
+    componentDidMount() {
+        this._hostCanvas = this.props.globalState.hostDocument.getElementById("workbench-canvas") as HTMLDivElement;
+        this._rootContainer = this.props.globalState.hostDocument.getElementById("workbench-container") as HTMLDivElement;
+        this._gridCanvas = this.props.globalState.hostDocument.getElementById("workbench-canvas-container") as HTMLDivElement;
+        this._svgCanvas = this.props.globalState.hostDocument.getElementById("workbench-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;
+        }
+
+        
+        // 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 and/or guiNodes
+        if (this._mouseStartPointX != null && this._mouseStartPointY != null) {
+
+            var x = this._mouseStartPointX;
+            var y = this._mouseStartPointY;
+            let selected = false;
+            this.selectedGuiNodes.forEach(element => {
+                selected = element._onMove(new Vector2(evt.clientX, evt.clientY), 
+                new Vector2( x, y)) || selected;
+            });
+
+            if(!selected) {
+                this._rootContainer.style.cursor = "move";
+                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);
+
+        //TODO: Inplement group selection
+        // 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;
+        }*/
+        console.log('workbench click');
+        if(!this.isOverGUINode) {
+            console.log('unclicked');
+            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._selectionBox) {
+           this._selectionBox.parentElement!.removeChild(this._selectionBox);
+           this._selectionBox = null;
+        }
+
+        if (this._frameCandidate) {            
+
+
+            this._frameCandidate.parentElement!.removeChild(this._frameCandidate);
+            this._frameCandidate = null;
+
+        }
+    }
+
+    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._guiNodes.forEach(node => {
+
+            if (node.x < minX) {
+                minX = node.x;
+            }
+            if (node.y < minY) {
+                minY = node.y;
+            }
+        });
+        // Restore to 0
+
+        this._guiNodes.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;
+    }
+
+
+    public createGUICanvas()
+    {
+        // Get the canvas element from the DOM.
+        const canvas = document.getElementById("workbench-canvas") as HTMLCanvasElement;
+
+        // Associate a Babylon Engine to it.
+        const engine = new Engine(canvas);
+        
+        // Create our first scene.
+        var scene = new Scene(engine);
+        scene.clearColor = new Color4(0.2, 0.2, 0.3, 0.1);
+
+        // This creates and positions a free camera (non-mesh)
+        var camera = new FreeCamera("camera1", new Vector3(0, 5, -10), scene);
+
+        // This targets the camera to scene origin
+        camera.setTarget(Vector3.Zero());
+        
+        // This attaches the camera to the canvas
+        //camera.attachControl(true);
+        
+        // GUI
+        this.globalState.guiTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI");
+        scene.getEngine().onCanvasPointerOutObservable.clear();
+        // Watch for browser/canvas resize events
+        window.addEventListener("resize", function () {
+        engine.resize();
+        });
+
+        engine.runRenderLoop(() => {this.updateGUIs(); scene.render()});
+    }
+    
+    updateGUIs()
+    {
+        this._guiNodes.forEach(element => {
+            element.updateVisual();
+            
+        });
+    }
+ 
+    render() {
+ 
+        return <canvas id="workbench-canvas" 
+        onWheel={evt => this.onWheel(evt)}
+        onPointerMove={evt => this.onMove(evt)}
+        onPointerDown={evt =>  this.onDown(evt)}   
+        onPointerUp={evt =>  this.onUp(evt)} 
+        >   
+        <div id="workbench-container">
+            <div id="workbench-canvas-container">  
+            </div>     
+            <div id="frame-container">                        
+            </div>
+            <svg id="workbench-svg-container">
+            </svg>                    
+            <div id="selection-container">                        
+            </div>
+        </div>
+        </canvas>
+    }
+}

+ 452 - 0
guiEditor/src/diagram/workbenchCanvas.scss

@@ -0,0 +1,452 @@
+#workbench-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;
+        }
+    }
+
+    #workbench-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;
+                }
+            }
+
+
+            .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;
+                    }
+                }
+            }
+        }
+
+        #workbench-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;
+                }
+            }
+        }
+
+        #workbench-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;
+                    }
+                }
+            }
+        }
+    }
+}

+ 42 - 0
guiEditor/src/globalState.ts

@@ -0,0 +1,42 @@
+import { Nullable } from "babylonjs/types";
+import { Observable } from "babylonjs/Misc/observable";
+import { LogEntry } from "./components/log/logComponent";
+import { DataStorage } from "babylonjs/Misc/dataStorage";
+import { Color4 } from "babylonjs/Maths/math.color";
+import { GUINode } from "./diagram/guiNode";
+import { WorkbenchComponent } from "./diagram/workbench";
+import { AdvancedDynamicTexture } from "babylonjs-gui/2D/advancedDynamicTexture";
+
+export class GlobalState {
+    guiTexture: AdvancedDynamicTexture;
+    hostElement: HTMLElement;
+    hostDocument: HTMLDocument;
+    hostWindow: Window;
+    onSelectionChangedObservable = new Observable<Nullable<GUINode>>();
+    onRebuildRequiredObservable = new Observable<void>();
+    onBuiltObservable = new Observable<void>();
+    onResetRequiredObservable = new Observable<void>();
+    onUpdateRequiredObservable = new Observable<void>();
+    onReOrganizedRequiredObservable = new Observable<void>();
+    onLogRequiredObservable = new Observable<LogEntry>();
+    onErrorMessageDialogRequiredObservable = new Observable<string>();
+    onIsLoadingChanged = new Observable<boolean>();
+    onSelectionBoxMoved = new Observable<ClientRect | DOMRect>();
+    onGuiNodeRemovalObservable = new Observable<GUINode>();
+    backgroundColor: Color4;
+    blockKeyboardEvents = false;
+    controlCamera: boolean;
+    workbench: WorkbenchComponent;
+    storeEditorData: (serializationObject: any) => void;
+
+    customSave?: { label: string; action: (data: string) => Promise<void> };
+
+    public constructor() {
+        this.controlCamera = DataStorage.ReadBoolean("ControlCamera", true);
+
+        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);
+    }
+}

+ 80 - 0
guiEditor/src/guiEditor.ts

@@ -0,0 +1,80 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import { GlobalState } from "./globalState";
+import { WorkbenchEditor } from "./workbenchEditor";
+import { Popup } from "./sharedUiComponents/lines/popup";
+import { SerializationTools } from "./serializationTools";
+import { Observable } from "babylonjs/Misc/observable";
+/**
+ * Interface used to specify creation options for the gui editor
+ */
+export interface IGUIEditorOptions {
+    hostElement?: HTMLElement;
+    customSave?: { label: string; action: (data: string) => Promise<void> };
+    customLoadObservable?: Observable<any>;
+}
+
+/**
+ * Class used to create a gui editor
+ */
+export class GUIEditor {
+    private static _CurrentState: GlobalState;
+
+    /**
+     * Show the gui editor
+     * @param options defines the options to use to configure the gui editor
+     */
+    public static Show(options: IGUIEditorOptions) {
+        if (this._CurrentState) {
+            var popupWindow = (Popup as any)["gui-editor"];
+            if (popupWindow) {
+                popupWindow.close();
+            }
+        }
+
+        let hostElement = options.hostElement;
+
+        if (!hostElement) {
+            hostElement = Popup.CreatePopup("BABYLON.JS GUI EDITOR", "gui-editor", 1000, 800)!;
+        }
+
+        let globalState = new GlobalState();
+        globalState.hostElement = hostElement;
+        globalState.hostDocument = hostElement.ownerDocument!;
+        globalState.customSave = options.customSave;
+        globalState.hostWindow = hostElement.ownerDocument!.defaultView!;
+
+        const graphEditor = React.createElement(WorkbenchEditor, {
+            globalState: globalState,
+        });
+
+        ReactDOM.render(graphEditor, hostElement);
+
+        // create the middle workbench canvas
+        if (!globalState.guiTexture) {
+            globalState.workbench.createGUICanvas();
+        }
+
+        if (options.customLoadObservable) {
+            options.customLoadObservable.add((data) => {
+                SerializationTools.Deserialize(data, globalState);
+                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)["gui-editor"];
+        if (popupWindow) {
+            window.onbeforeunload = () => {
+                var popupWindow = (Popup as any)["gui-editor"];
+                if (popupWindow) {
+                    popupWindow.close();
+                }
+            };
+        }
+        window.addEventListener("beforeunload", () => {});
+    }
+}

+ 54 - 0
guiEditor/src/guiNodeTools.ts

@@ -0,0 +1,54 @@
+import { Button } from "babylonjs-gui/2D/controls/button";
+import { Checkbox } from "babylonjs-gui/2D/controls/checkbox";
+import { ColorPicker } from "babylonjs-gui/2D/controls/colorpicker";
+import { Ellipse } from "babylonjs-gui/2D/controls/ellipse";
+import { Line } from "babylonjs-gui/2D/controls/line";
+import { Rectangle } from "babylonjs-gui/2D/controls/rectangle";
+import { Slider } from "babylonjs-gui/2D/controls/sliders/slider";
+import { TextBlock } from "babylonjs-gui/2D/controls/textBlock";
+
+export class GUINodeTools {
+    public static CreateControlFromString (data: string) {
+        //TODO: Add more elements and create default values for certain types.
+        let element;
+        switch (data) {
+            case "Slider":
+                element = new Slider("Slider");
+                break;
+            case "Checkbox":
+                element = new Checkbox("Checkbox");
+                break;
+            case "ColorPicker":
+                element = new ColorPicker("ColorPicker");
+                break;
+            case "Ellipse":
+                element = new Ellipse("Ellipse");
+                break;
+            case "Rectangle":
+                element = new Rectangle("Rectangle");
+                break;
+            case "Line":
+                element = new Line();
+                element.x1 = 10;
+                element.y1 = 10;
+                element.x2 = 100;
+                element.y2 = 100;
+                element.lineWidth = 5;
+                element.dash = [50, 10];
+                return element;
+            case "Text":
+                element = new TextBlock("Textblock");
+                element.text = "My Text";
+                return element;
+            default:
+                element = Button.CreateSimpleButton("Button", "Click Me");
+                break;
+        }
+
+        element.width = "150px";
+        element.height = "40px";
+        element.color = "#FFFFFFFF";
+        element.isPointerBlocker = true;
+        return element;
+    }
+}

+ 1 - 0
guiEditor/src/index.ts

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

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

@@ -0,0 +1,9 @@
+import { GUIEditor } 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.GuiEditor = GUIEditor;
+}
+
+export * from "../index";

+ 344 - 0
guiEditor/src/main.scss

@@ -0,0 +1,344 @@
+#gui-editor-workbench-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;
+    }
+}
+
+#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
+                
+            }
+        }
+    }
+}
+
+.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;
+                }
+            }
+        }
+    }
+}

+ 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
+        );
+    }
+}

+ 8 - 0
guiEditor/src/serializationTools.ts

@@ -0,0 +1,8 @@
+import { GlobalState } from './globalState';
+
+export class SerializationTools {
+
+    public static Deserialize(serializationObject: any, globalState: GlobalState) {
+        globalState.onIsLoadingChanged.notifyObservers(true);
+    }
+}

+ 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>
+        );
+    }
+}

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

@@ -0,0 +1,147 @@
+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;
+    private _digits: number;
+    constructor(props: IFloatLineComponentProps) {
+        super(props);
+        let currentValue = this.props.target[this.props.propertyName];
+        
+        this._digits == this.props.digits == undefined ? 2 : this.props.digits;
+    
+        this.state = { value: currentValue ? (this.props.isInteger ? currentValue.toFixed(0) : currentValue.toFixed(this._digits)) : "0" };
+        this._store = currentValue;
+
+        let rexp = "(.*\\.";
+        let numDigits = this._digits;
+        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._digits) : "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>
+        ); 
+    }
+}

+ 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>
+        );
+    }
+}

+ 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>
+        )
+    }
+}

+ 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>
+        );
+    }
+}

+ 406 - 0
guiEditor/src/workbenchEditor.tsx

@@ -0,0 +1,406 @@
+import * as React from "react";
+import { GlobalState } from "./globalState";
+import { GuiListComponent } from "./components/guiList/guiListComponent";
+import { PropertyTabComponent } from "./components/propertyTab/propertyTabComponent";
+import { Portal } from "./portal";
+import { LogComponent } from "./components/log/logComponent";
+import { DataStorage } from "babylonjs/Misc/dataStorage";
+import { Nullable } from "babylonjs/types";
+import { GUINodeTools } from "./guiNodeTools";
+import { IEditorData } from "./nodeLocationInfo";
+import { WorkbenchComponent } from "./diagram/workbench";
+import { GUINode } from "./diagram/guiNode";
+import { _TypeStore } from "babylonjs/Misc/typeStore";
+import { MessageDialogComponent } from "./sharedComponents/messageDialog";
+import { Control } from "babylonjs-gui/2D/controls/control";
+import { Container } from "babylonjs-gui/2D/controls/container";
+
+require("./main.scss");
+
+interface IGraphEditorProps {
+    globalState: GlobalState;
+}
+
+interface IGraphEditorState {
+    showPreviewPopUp: boolean;
+}
+
+export class WorkbenchEditor extends React.Component<IGraphEditorProps, IGraphEditorState> {
+    private _workbenchCanvas: WorkbenchComponent;
+
+    private _startX: number;
+    private _moveInProgress: boolean;
+
+    private _leftWidth = DataStorage.ReadNumber("LeftWidth", 200);
+    private _rightWidth = DataStorage.ReadNumber("RightWidth", 300);
+
+    private _blocks = new Array<Container | Control>();
+
+    private _onWidgetKeyUpPointer: any;
+
+    private _popUpWindow: Window;
+
+    /**
+     * Creates a node and recursivly creates its parent nodes from it's input
+     * @param block
+     */
+    public createNodeFromObject(block: Control, recursion = true) {
+        if (this._blocks.indexOf(block) !== -1) {
+            return this._workbenchCanvas.nodes.filter((n) => n.guiControl === block)[0];
+        }
+
+        this._blocks.push(block);
+
+        //TODO: Implement
+        const node = null;
+        return node;
+    }
+
+    componentDidMount() {
+        if (this.props.globalState.hostDocument) {
+            this._workbenchCanvas = this.refs["graphCanvas"] as WorkbenchComponent;
+        }
+
+        if (navigator.userAgent.indexOf("Mobile") !== -1) {
+            ((this.props.globalState.hostDocument || document).querySelector(".blocker") as HTMLElement).style.visibility = "visible";
+        }
+    }
+
+    componentWillUnmount() {
+        if (this.props.globalState.hostDocument) {
+            this.props.globalState.hostDocument!.removeEventListener("keyup", this._onWidgetKeyUpPointer, false);
+        }
+    }
+
+    constructor(props: IGraphEditorProps) {
+        super(props);
+
+        this.state = {
+            showPreviewPopUp: false,
+        };
+
+        this.props.globalState.hostDocument!.addEventListener(
+            "keydown",
+            (evt) => {
+                if ((evt.keyCode === 46 || evt.keyCode === 8) && !this.props.globalState.blockKeyboardEvents) {
+                    // Delete
+                    let selectedItems = this._workbenchCanvas.selectedGuiNodes;
+
+                    for (var selectedItem of selectedItems) {
+                        selectedItem.dispose();
+                    }
+
+                    this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+                    return;
+                }
+
+                if (!evt.ctrlKey || this.props.globalState.blockKeyboardEvents) {
+                    return;
+                }
+
+                if (evt.key === "c") {
+                    // Copy
+
+                    let selectedItems = this._workbenchCanvas.selectedGuiNodes;
+                    if (!selectedItems.length) {
+                        return;
+                    }
+
+                    let selectedItem = selectedItems[0] as GUINode;
+
+                    if (!selectedItem.guiControl) {
+                        return;
+                    }
+                } else if (evt.key === "v") {
+                    // Paste
+                    //TODO: Implement
+                }
+            },
+            false
+        );
+    }
+
+    pasteSelection(copiedNodes: GUINode[], currentX: number, currentY: number, selectNew = false) {
+        //let originalNode: Nullable<GUINode> = null;
+
+        let newNodes: GUINode[] = [];
+
+        // 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.guiControl;
+
+            if (!block) {
+                continue;
+            }
+        }
+
+        return newNodes;
+    }
+
+    zoomToFit() {
+        this._workbenchCanvas.zoomToFit();
+    }
+    
+    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._workbenchCanvas._isLoading = true; // Will help loading large graphes
+
+        setTimeout(() => {
+            if (!editorData || !editorData.locations) {
+                this._workbenchCanvas.distributeGraph();
+            } else {
+                // Locations
+                for (var location of editorData.locations) {
+                    for (var node of this._workbenchCanvas.nodes) {
+                        if (node.guiControl && node.guiControl.uniqueId === location.blockId) {
+                            node.x = location.x;
+                            node.y = location.y;
+                            node.cleanAccumulation();
+                            break;
+                        }
+                    }
+                }
+            }
+
+            this._workbenchCanvas._isLoading = false;
+            for (var node of this._workbenchCanvas.nodes) {
+            }
+            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("workbench-editor-workbench-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-gui-node") as string;
+
+        let guiElement = GUINodeTools.CreateControlFromString (data);
+
+        let newGuiNode = this._workbenchCanvas.appendBlock(guiElement);
+
+        //TODO: Get correct positioning
+
+        /*let x = event.clientX; // - event.currentTarget.offsetLeft - this._workbenchCanvas.x;
+        let y = event.clientY;   // - event.currentTarget.offsetTop - this._workbenchCanvas.y - 20; 
+
+        newGuiNode.x += (x - newGuiNode.x);
+        newGuiNode.y += y - newGuiNode.y;
+        //newGuiNode.cleanAccumulation();*/
+
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
+        this.props.globalState.onSelectionChangedObservable.notifyObservers(newGuiNode);
+
+        this.forceUpdate();
+    }
+
+    handlePopUp = () => {
+        this.setState({
+            showPreviewPopUp: true,
+        });
+        this.props.globalState.hostWindow.addEventListener("beforeunload", this.handleClosingPopUp);
+    };
+
+    handleClosingPopUp = () => {
+        this._popUpWindow.close();
+    };
+
+    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 = "gui-editor-workbench-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);
+            }
+        }
+    };
+
+    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="gui-editor-workbench-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 */}
+                    <GuiListComponent globalState={this.props.globalState} />
+
+                    <div id="leftGrab" onPointerDown={(evt) => this.onPointerDown(evt)} onPointerUp={(evt) => this.onPointerUp(evt)} onPointerMove={(evt) => this.resizeColumns(evt)}></div>
+
+                    {/* The gui workbench diagram */}
+                    <div
+                        className="diagram-container"
+                        onDrop={(event) => {
+                            this.emitNewBlock(event);
+                        }}
+                        onDragOver={(event) => {
+                            event.preventDefault();
+                        }}
+                    >
+                        <WorkbenchComponent 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} />
+                    </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>
+        );
+    }
+}

+ 29 - 0
guiEditor/tsconfig.json

@@ -0,0 +1,29 @@
+{
+    "extends": "../tsconfigRules",
+    "compilerOptions": {
+        "preserveSymlinks": false,
+        "jsx": "react",
+        "baseUrl": "./src/",
+        "rootDir": "./src/",
+        "paths": {
+            "babylonjs-gui/*": [
+                "../../dist/preview release/gui/babylon.gui.module.d.ts"
+            ],
+            "babylonjs-gltf2interface/*": [
+                "../../dist/preview release/glTF2Interface/babylon.glTF2Interface.d.ts"
+            ],
+            "babylonjs-loaders/*": [
+                "../../dist/preview release/loaders/babylonjs.loaders.module.d.ts"
+            ],
+            "babylonjs-serializers/*": [
+                "../../dist/preview release/serializers/babylonjs.serializers.module.d.ts"
+            ],
+            "babylonjs/*": [
+                "../../dist/preview release/babylon.module.d.ts"
+            ]
+        }
+    },
+    "exclude": [
+        "../../inspector"
+    ]
+}

+ 46 - 0
guiEditor/webpack.config.js

@@ -0,0 +1,46 @@
+const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const babylonWebpackConfig = require('../Tools/WebpackPlugins/babylonWebpackConfig');
+
+var config = babylonWebpackConfig({
+    module: "guiEditor",
+    resolve: {
+        symlinks: false,
+        extensions: [".js", '.ts', ".tsx"],
+    },
+    moduleRules: [
+        {
+            test: /\.scss$/,
+            use: [
+                // fallback to style-loader in development
+                process.env.NODE_ENV !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader,
+                "css-loader",
+                "sass-loader"
+            ]
+        },
+        {
+            test: /\.css$/,
+            use: ['style-loader', 'css-loader']
+        },
+    {
+        test: /\.svg$/,
+        use: [
+          {
+            loader: 'svg-url-loader',
+            options: {
+              limit: 10000,
+            },
+          },
+        ],
+      }],
+    plugins: [
+        new MiniCssExtractPlugin({
+            // Options similar to the same options in webpackOptions.output
+            // both options are optional
+            filename: "[name].css",
+            chunkFilename: "[id].css"
+        })
+    ]
+});
+
+module.exports = config;

nodeEditor/src/sharedComponents/popup.ts → nodeEditor/src/sharedComponents/popup.tsx


+ 25 - 0
sharedUiComponents/lines/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-gui-node", this.props.data);
+                }}>
+                {this.props.data.replace("Block", "")}
+            </div>
+        );
+    }
+}

+ 80 - 0
sharedUiComponents/lines/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)
+            }
+
+        }
+    }
+}