David `Deltakosh` Catuhe 5 năm trước cách đây
mục cha
commit
cd45c7334a

+ 4 - 7
Playground/public/index-local.html

@@ -8,7 +8,8 @@
         <link rel="shortcut icon" href="https://www.babylonjs.com/favicon.ico">
         <meta name="description" content="Babylon.js playground is a live editor for Babylon.js WebGL 3D scenes">
         <meta name="keywords" content="Babylon.js,WebGL,3D">
-
+        
+        <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css">
         <link rel="stylesheet"
             href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css" />
 
@@ -37,18 +38,14 @@
 
         <style>
             html,
-            body {
+            body,
+            #host-element {
                 width: 100%;
                 height: 100%;
                 padding: 0;
                 margin: 0;
                 overflow: hidden;
             }
-    
-            #host-element {
-                width: 100%;
-                height: 100%;
-            }
         </style>        
     </head>
 

+ 71 - 0
Playground/src/components/footerComponent..tsx

@@ -0,0 +1,71 @@
+import * as React from "react";
+import { GlobalState, EditionMode } from '../globalState';
+import DocumentationIcon from "../imgs/documentation.svg";
+import ForumIcon from "../imgs/forum.svg";
+import SearchIcon from "../imgs/search.svg";
+import CodeOnlyIcon from "../imgs/codeOnly.svg";
+import RenderingOnlyIcon from "../imgs/renderingOnly.svg";
+
+require("../scss/footer.scss");
+
+interface IFooterComponentProps {
+    globalState: GlobalState;
+}
+
+export class FooterComponent extends React.Component<IFooterComponentProps> {    
+    private _fpsRef: React.RefObject<HTMLDivElement>;
+    
+    public constructor(props: IFooterComponentProps) {
+        super(props);
+        this._fpsRef = React.createRef();
+    }
+    
+    componentDidMount() {       
+        this.props.globalState.fpsElement = this._fpsRef.current!; 
+    }
+
+    switchMobileDefaultMode() {
+        if (this.props.globalState.mobileDefaultMode === EditionMode.CodeOnly) {
+            this.props.globalState.mobileDefaultMode = EditionMode.RenderingOnly;
+        } else {
+            this.props.globalState.mobileDefaultMode = EditionMode.CodeOnly;
+        }
+
+        this.props.globalState.onMobileDefaultModeChangedObservable.notifyObservers();
+    }
+
+    public render() {
+        return (
+            <div id="footer">   
+                {
+                    window.innerWidth < this.props.globalState.MobileSizeTrigger &&
+                    <div className="modeBar">
+                        <div className='link'>
+                            {
+                                this.props.globalState.mobileDefaultMode === EditionMode.CodeOnly &&
+                                <a onClick={() => this.switchMobileDefaultMode()} title="Switch to rendering"><RenderingOnlyIcon/></a>
+                            }
+                            {
+                                this.props.globalState.mobileDefaultMode === EditionMode.RenderingOnly &&
+                                <a onClick={() => this.switchMobileDefaultMode()} title="Switch to code"><CodeOnlyIcon/></a>
+                            }
+                        </div>
+                    </div>
+                }
+                <div id="statusBar"></div>
+                <div className="links">
+                    <div className='link'>
+                        <a target='_new' href="https://forum.babylonjs.com/" title="Forum"><ForumIcon/></a>
+                    </div>
+                    <div className='link'>
+                        <a target='_new' href="https://doc.babylonjs.com" title="Documentation"><DocumentationIcon/></a>
+                    </div>
+                    <div className='link'>
+                        <a target='_new' href="https://doc.babylonjs.com/playground" title="Search"><SearchIcon /></a>
+                    </div>
+                </div>  
+                <div className="fps" ref={this._fpsRef}></div>          
+            </div>   
+        )
+    }
+}

+ 9 - 278
Playground/src/components/monacoComponent.tsx

@@ -1,296 +1,27 @@
 import * as React from "react";
-import 'monaco-editor/esm/vs/editor/editor.api';
-
-declare type IStandaloneCodeEditor = import('monaco-editor/esm/vs/editor/editor.api').editor.IStandaloneCodeEditor;
-declare type IEditorConstructionOptions = import('monaco-editor/esm/vs/editor/editor.api').editor.IEditorConstructionOptions;
-
-declare var monaco: any;
+import { MonacoManager } from '../tools/monacoManager';
+import { GlobalState } from '../globalState';
 
 require("../scss/monaco.scss");
 
 interface IMonacoComponentProps {
-    language: "JS" | "TS";
     className: string;
     refObject: React.RefObject<HTMLDivElement>;
+    globalState: GlobalState;
 }
 
 export class MonacoComponent extends React.Component<IMonacoComponentProps> {
-    private _editor: IStandaloneCodeEditor;
-    private _definitionWorker: Worker;
-    private _deprecatedCandidates: string[];
-   // private _templates: string[];
+    private _monacoManager: MonacoManager;
     
     public constructor(props: IMonacoComponentProps) {
         super(props);
-    }
-
-    async setupMonaco() {        
-        // let response = await fetch("https://preview.babylonjs.com/babylon.d.ts");
-        // if (!response.ok) {
-        //     return;
-        // }
-
-        // let libContent = await response.text();
-
-        // response = await fetch("https://preview.babylonjs.com/gui/babylon.gui.d.ts");
-        // if (!response.ok) {
-        //     return;
-        // }
-
-        // libContent += await response.text();
-
-        //   this.setupDefinitionWorker(libContent);
-
-            // Load code templates
-        //   response = await fetch("/templates.json");
-        // if (response.ok) {
-        //      this._templates = await response.json();
-            //}
-
-        // Setup the Monaco compilation pipeline, so we can reuse it directly for our scrpting needs
-        //this.setupMonacoCompilationPipeline(libContent);
-
-            // This is used for a vscode-like color preview for ColorX types
-            //this.setupMonacoColorProvider();
-
-        let hostElement = this.props.refObject.current!;  
-        var editorOptions: IEditorConstructionOptions = {
-            value: "",
-            language: this.props.language === "JS" ? "javascript" : "typescript",
-            lineNumbers: "on",
-            roundedSelection: true,
-            automaticLayout: true,
-            scrollBeyondLastLine: false,
-            readOnly: false,
-            theme: "vs-dark",
-            contextmenu: false,
-            folding: true,
-            showFoldingControls: "always",
-            renderIndentGuides: true,
-            minimap: {
-                enabled: true
-            }
-        };      
 
-        this._editor = monaco.editor.create(
-            hostElement,
-            editorOptions as any
-        );        
-        
-        this._editor.setValue(`var createScene = function () {
-
-            // This creates a basic Babylon Scene object (non-mesh)
-            var scene = new BABYLON.Scene(engine);
-        
-            // This creates and positions a free camera (non-mesh)
-            var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
-        
-            // This targets the camera to scene origin
-            camera.setTarget(BABYLON.Vector3.Zero());
-        
-            // This attaches the camera to the canvas
-            camera.attachControl(canvas, true);
-        
-            // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
-            var light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
-        
-            // Default intensity is 1. Let's dim the light a small amount
-            light.intensity = 0.7;
-        
-            // Our built-in 'sphere' shape.
-            var sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 2, segments: 32}, scene);
-        
-            // Move the sphere upward 1/2 its height
-            sphere.position.y = 1;
-        
-            // Our built-in 'ground' shape.
-            var ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 6, height: 6}, scene);
-        
-            return scene;
-        
-        };`);
-        const model = this._editor.getModel();
-        monaco.editor.setModelLanguage(model, "javascript");
-        console.log(model)
-
-    }
-
-      // Provide an adornment for BABYLON.ColorX types: color preview
-    setupMonacoColorProvider() {
-        monaco.languages.registerColorProvider(this.props.language == "JS" ? "javascript" : "typescript", {
-            provideColorPresentations: (model: any, colorInfo: any) => {
-                const color = colorInfo.color;
-
-                const precision = 100.0;
-                const converter = (n: number) => Math.round(n * precision) / precision;
-
-                let label;
-                if (color.alpha === undefined || color.alpha === 1.0) {
-                    label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)})`;
-                } else {
-                    label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)}, ${converter(color.alpha)})`;
-                }
-
-                return [{
-                    label: label
-                }];
-            },
-
-            provideDocumentColors: (model: any) => {
-                const digitGroup = "\\s*(\\d*(?:\\.\\d+)?)\\s*";
-                // we add \n{0} to workaround a Monaco bug, when setting regex options on their side
-                const regex = `BABYLON\\.Color(?:3|4)\\s*\\(${digitGroup},${digitGroup},${digitGroup}(?:,${digitGroup})?\\)\\n{0}`;
-                const matches = model.findMatches(regex, false, true, true, null, true);
-
-                const converter = (g: string) => g === undefined ? undefined : Number(g);
-
-                return matches.map((match: any) => ({
-                    color: {
-                        red: converter(match.matches![1])!,
-                        green: converter(match.matches![2])!,
-                        blue: converter(match.matches![3])!,
-                        alpha: converter(match.matches![4])!
-                    },
-                    range: {
-                        startLineNumber: match.range.startLineNumber,
-                        startColumn: match.range.startColumn + match.matches![0].indexOf("("),
-                        endLineNumber: match.range.startLineNumber,
-                        endColumn: match.range.endColumn
-                    }
-                }));
-            }
-        });
+        this._monacoManager = new MonacoManager(this.props.globalState);
     }
-
-    // Setup both JS and TS compilation pipelines to work with our scripts. 
-    setupMonacoCompilationPipeline(libContent: string) {
-        var typescript = monaco.languages.typescript;
-
-        if (this.props.language === "JS") {
-            typescript.javascriptDefaults.setCompilerOptions({
-                noLib: false,
-                allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
-            });
-
-            typescript.javascriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
-        } else {
-            typescript.typescriptDefaults.setCompilerOptions({
-                module: typescript.ModuleKind.AMD,
-                target: typescript.ScriptTarget.ESNext,
-                noLib: false,
-                strict: false,
-                alwaysStrict: false,
-                strictFunctionTypes: false,
-                suppressExcessPropertyErrors: false,
-                suppressImplicitAnyIndexErrors: true,
-                noResolve: true,
-                suppressOutputPathCheck: true,
-
-                allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
-            });
-            typescript.typescriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
-        }
-    }
-
-    setupDefinitionWorker(libContent: string) {
-        this._definitionWorker = new Worker('workers/definitionWorker.js');
-        this._definitionWorker.addEventListener('message', ({
-            data
-        }) => {
-            this._deprecatedCandidates = data.result;
-            this.analyzeCode();
-        });
-        this._definitionWorker.postMessage({
-            code: libContent
-        });
-    }
-
-    // This will make sure that all members marked with a deprecated jsdoc attribute will be marked as such in Monaco UI
-    // We use a prefiltered list of deprecated candidates, because the effective call to getCompletionEntryDetails is slow.
-    // @see setupDefinitionWorker
-    async analyzeCode() {
-        // if the definition worker is very fast, this can be called out of context. @see setupDefinitionWorker
-        if (!this._editor)
-            return;
-
-        const model = this._editor.getModel();
-        if (!model)
-            return;
-
-        const uri = model.uri;
-
-        let worker = null;
-        if (this.props.language == "JS")
-            worker = await monaco.languages.typescript.getJavaScriptWorker();
-        else
-            worker = await monaco.languages.typescript.getTypeScriptWorker();
-
-        const languageService = await worker(uri);
-        const source = '[deprecated members]';
-
-        monaco.editor.setModelMarkers(model, source, []);
-        const markers: {
-            startLineNumber: number,
-            endLineNumber: number,
-            startColumn: number,
-            endColumn: number,
-            message: string,
-            severity: number,
-            source: string,
-        }[] = [];
-
-        for (const candidate of this._deprecatedCandidates) {
-            const matches = model.findMatches(candidate, false, false, true, null, false);
-            for (const match of matches) {
-                const position = {
-                    lineNumber: match.range.startLineNumber,
-                    column: match.range.startColumn
-                };
-                const wordInfo = model.getWordAtPosition(position);
-                const offset = model.getOffsetAt(position);
-
-                if (!wordInfo) {
-                    continue;
-                }
-
-                // continue if we already found an issue here
-                if (markers.find(m => m.startLineNumber == position.lineNumber && m.startColumn == position.column))
-                    continue;
-
-                // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls
-                // @see setupDefinitionWorker
-                const details = await languageService.getCompletionEntryDetails(uri.toString(), offset, wordInfo.word);
-                if (this.isDeprecatedEntry(details)) {
-                    const deprecatedInfo = details.tags.find(this.isDeprecatedTag);
-                    markers.push({
-                        startLineNumber: match.range.startLineNumber,
-                        endLineNumber: match.range.endLineNumber,
-                        startColumn: wordInfo.startColumn,
-                        endColumn: wordInfo.endColumn,
-                        message: deprecatedInfo.text,
-                        severity: monaco.MarkerSeverity.Warning,
-                        source: source,
-                    });
-                }
-            }
-        }
-
-        monaco.editor.setModelMarkers(model, source, markers);
-    }
-
-    isDeprecatedEntry(details: any) {
-        return details &&
-            details.tags &&
-            details.tags.find(this.isDeprecatedTag);
-    }
-
-    isDeprecatedTag(tag: any) {
-        return tag &&
-            tag.name == "deprecated";
-    }
-
-    componentDidMount() {
-       this.setupMonaco();
+    
+    componentDidMount() {        
+        let hostElement = this.props.refObject.current!;  
+        this._monacoManager.setupMonacoAsync(hostElement);
     }
 
     public render() {

+ 184 - 1
Playground/src/components/rendererComponent.tsx

@@ -1,19 +1,202 @@
 import * as React from "react";
+import { GlobalState } from '../globalState';
+import {Engine} from "babylonjs/Engines/engine"
+import { Nullable } from 'babylonjs/types';
+import { Scene } from 'babylonjs/scene';
+import { Utilities } from '../tools/utilities';
 
 require("../scss/rendering.scss");
 
 interface IRenderingComponentProps {
+    globalState: GlobalState;
 }
 
 export class RenderingComponent extends React.Component<IRenderingComponentProps> {
+    private _engine: Nullable<Engine>;
+    private _scene: Nullable<Scene>;
+    private _canvasRef: React.RefObject<HTMLCanvasElement>;
+
+    public constructor(props: IRenderingComponentProps) {
+        super(props);
+
+        this._canvasRef = React.createRef();
+
+        // Create the global handleException
+        (window as any).handleException = (e: Error) => {
+            console.error(e);
+        }
+
+        this.props.globalState.onRunRequiredObservable.add(() => {
+            this.compileAndRun();
+        })
+    }
 
     componentDidMount() {
+        this.compileAndRun();
+    }
+
+    compileAndRun() {
+        if (this._engine) {
+            try {
+                this._engine.dispose();
+            } 
+            catch (ex) {
+                // just ignore
+            }
+            this._engine = null;
+        }   
+
+        let globalObject = window as any;
+        let canvas = this._canvasRef.current!;
+        globalObject.canvas = canvas;
         
+        globalObject.createDefaultEngine = function () {
+            return new Engine(canvas, true, {
+                preserveDrawingBuffer: true,
+                stencil: true
+            });
+        }
+
+       // let zipVariables = "var engine = null;\r\nvar scene = null;\r\nvar sceneToRender = null;\r\n";
+       // let defaultEngineZip = "var createDefaultEngine = function() { return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true }); }";
+        let code = this.props.globalState.currentCode;        
+        let createEngineFunction = "createDefaultEngine";
+        let createSceneFunction = "";
+        let checkCamera = true;
+        let checkSceneCount = true;
+
+        if (code.indexOf("createEngine") !== -1) {
+            createEngineFunction = "createEngine";
+        }
+
+        // Check for different typos
+        if (code.indexOf("delayCreateScene") !== -1) { // delayCreateScene
+            createSceneFunction = "delayCreateScene";
+            checkCamera = false;
+        } else if (code.indexOf("createScene") !== -1) { // createScene
+            createSceneFunction = "createScene";
+        } else if (code.indexOf("CreateScene") !== -1) { // CreateScene
+            createSceneFunction = "CreateScene";
+        } else if (code.indexOf("createscene") !== -1) { // createscene
+            createSceneFunction = "createscene";
+        }
+
+        if (!createSceneFunction) {
+            this._engine = globalObject.createDefaultEngine() as Engine;
+            this._scene = new Scene(this._engine);
+
+            globalObject.engine = this._engine;
+            globalObject.scene = this._scene;
+
+            let runScript:any = null;
+            Utilities.FastEval("runScript = function(scene, canvas) {" + code + "}");
+            runScript(this._scene, canvas);            
+
+            //parent.zipTool.ZipCode = zipVariables + defaultEngineZip + "var engine = createDefaultEngine();" + ";\r\nvar scene = new BABYLON.Scene(engine);\r\n\r\n" + code;
+        } else {
+            code += `
+var engine;
+try {
+engine = ${createEngineFunction}();
+} catch(e) {
+console.log("the available createEngine function failed. Creating the default engine instead");
+engine = createDefaultEngine();
+}`;
+            code += "\r\nif (!engine) throw 'engine should not be null.';";
+
+            if (this.props.globalState.language === "JS") {
+                code += "\r\n" + "scene = " + createSceneFunction + "();";
+            } else {
+                var startCar = code.search('var ' + createSceneFunction);
+                code = code.substr(0, startCar) + code.substr(startCar + 4);
+                code += "\n" + "scene = " + createSceneFunction + "();";
+            }
+
+            // Execute the code
+            Utilities.FastEval(code);
+
+            this._engine = globalObject.engine;
+
+            if (!this._engine) {
+                this.props.globalState.onErrorObservable.notifyObservers("createEngine function must return an engine.");
+                return;
+            }
+
+            if (!globalObject.scene) {
+                this.props.globalState.onErrorObservable.notifyObservers(createSceneFunction + " function must return a scene.");
+                return;
+            }
+
+            let sceneToRenderCode = 'sceneToRender = scene';
+
+            // if scene returns a promise avoid checks
+            if (globalObject.scene.then) {
+                checkCamera = false;
+                checkSceneCount = false;
+                sceneToRenderCode = 'scene.then(returnedScene => { sceneToRender = returnedScene; });\r\n';
+            } 
+
+            // let createEngineZip = (createEngineFunction === "createEngine") ?
+            //     zipVariables :
+            //     zipVariables + defaultEngineZip;
+
+            // parent.zipTool.zipCode =
+            //     createEngineZip + ";\r\n" +
+            //     code + ";\r\n" +
+            //     sceneToRenderCode;
+        }
+
+        if (globalObject.scene.then) {
+            globalObject.scene.then((s : Scene) => {
+                this._scene = s;
+                globalObject.scene = this._scene;
+            });
+        } else {
+            this._scene = globalObject.scene as Scene;
+        }
+
+        this._engine.runRenderLoop(() => {
+            if (!this._scene || !this._engine) {
+                return;
+            }
+
+            if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
+                this._engine.resize();
+            }
+
+            if (this._scene.activeCamera || this._scene.activeCameras.length > 0) {
+                this._scene.render();
+            }
+
+            // Update FPS if camera is not a webxr camera
+            if(!(this._scene.activeCamera && 
+                this._scene.activeCamera.getClassName && 
+                this._scene.activeCamera.getClassName() === 'WebXRCamera')) {
+                this.props.globalState.fpsElement.innerHTML = this._engine.getFps().toFixed() + " fps";
+            }
+        });
+
+        if (checkSceneCount && this._engine.scenes.length === 0) {
+            this.props.globalState.onErrorObservable.notifyObservers("You must at least create a scene.");
+            return;
+        }
+
+        if (checkCamera && this._engine.scenes[0].activeCamera == null) {
+            this.props.globalState.onErrorObservable.notifyObservers("You must at least create a camera.");
+            return;
+        } else if (globalObject.scene.then) {
+            globalObject.scene.then(function () {
+            });
+        } else {
+            this._engine.scenes[0].executeWhenReady(function () {
+            });
+        }
+
     }
 
     public render() {
         return (
-            <canvas id="renderingCanvas"></canvas>
+            <canvas id="renderingCanvas" ref={this._canvasRef}></canvas>
         )
     }
 }

+ 17 - 0
Playground/src/globalState.ts

@@ -1,2 +1,19 @@
+import { Observable } from 'babylonjs/Misc/observable';
+
+export enum EditionMode {
+    Desktop,
+    CodeOnly,
+    RenderingOnly
+}
+
 export class GlobalState {
+    public readonly MobileSizeTrigger = 1024;
+    public currentCode: string;
+    public language: "JS" | "TS" = "JS";
+    public fpsElement: HTMLDivElement;
+    public mobileDefaultMode = EditionMode.RenderingOnly;
+
+    public onRunRequiredObservable = new Observable<void>();
+    public onErrorObservable = new Observable<string>();    
+    public onMobileDefaultModeChangedObservable = new Observable<void>();
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
Playground/src/imgs/codeOnly.svg


+ 3 - 0
Playground/src/imgs/documentation.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32">
+  <path d="M1920 512v1408H768v-256H512v-256H256V0h731l256 256h421v256h256zm-896-128h165l-165-165v165zm256 896V512H896V128H384v1152h896zm256 256V384h-128v1024H640v128h896zm257-896h-129v1024H896v128h897V640z" />
+</svg>

+ 3 - 0
Playground/src/imgs/forum.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32">
+  <path d="M0 128h2048v1408H731l-475 475v-475H0V128zm1920 1280V256H128v1152h256v293l293-293h1243zM1792 384v896H256V384h1536zM512 1152V896H384v256h128zm0-384V512H384v256h128zm1152 384V896H640v256h1024zm0-384V512H640v256h1024z" />
+</svg>

+ 1 - 0
Playground/src/imgs/renderingOnly.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55 55"><defs><style>.cls-1{fill:none;}</style></defs><title>canvas3Dbutton</title><g id="Icon_buttons" data-name="Icon buttons"><rect class="cls-1" width="55" height="55"/><path d="M27.71,17.25l8.94,4.47V32.88l-8.94,4.48-8.94-4.48V21.72ZM27,35.47V26.7l-6.87-3.45V32ZM21,22.15l6.71,3.35,6.71-3.35L27.71,18.8ZM35.27,32V23.25L28.4,26.7v8.77Z"/></g></svg>

+ 3 - 0
Playground/src/imgs/search.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32">
+  <path d="M1344 0q97 0 187 25t168 71 142 110 111 143 71 168 25 187q0 97-25 187t-71 168-110 142-143 111-168 71-187 25q-125 0-239-42t-211-121l-785 784q-19 19-45 19t-45-19-19-45q0-26 19-45l784-785q-79-96-121-210t-42-240q0-97 25-187t71-168 110-142T989 96t168-71 187-25zm0 1280q119 0 224-45t183-124 123-183 46-224q0-119-45-224t-124-183-183-123-224-46q-119 0-224 45T937 297 814 480t-46 224q0 119 45 224t124 183 183 123 224 46z" />
+</svg>

+ 55 - 13
Playground/src/playground.tsx

@@ -2,7 +2,8 @@ import * as React from "react";
 import * as ReactDOM from "react-dom";
 import { MonacoComponent } from './components/monacoComponent';
 import { RenderingComponent } from './components/rendererComponent';
-//import { GlobalState } from './globalState';
+import { GlobalState, EditionMode } from './globalState';
+import { FooterComponent } from './components/footerComponent.';
 
 require("./scss/main.scss");
 const Split = require('split.js').default;
@@ -10,30 +11,70 @@ const Split = require('split.js').default;
 interface IPlaygroundProps {
 }
 
-export class Playground extends React.Component<IPlaygroundProps, {errorMessage: string}> {    
+export class Playground extends React.Component<IPlaygroundProps, {errorMessage: string, mode: EditionMode}> {    
     private splitRef: React.RefObject<HTMLDivElement>;
     private monacoRef: React.RefObject<HTMLDivElement>;
     private renderingRef: React.RefObject<HTMLDivElement>;
 
-    //private _globalState: GlobalState;
+    private _globalState: GlobalState;
+    private _splitInstance: any;
     
     public constructor(props: IPlaygroundProps) {
-        super(props);
-       // this._globalState = new GlobalState();
+       super(props);
+       this._globalState = new GlobalState();
 
        this.splitRef = React.createRef();
        this.monacoRef = React.createRef();
        this.renderingRef = React.createRef();
 
-       this.state = {errorMessage: ""};
+       this.state = {errorMessage: "", mode: window.innerWidth < this._globalState.MobileSizeTrigger ? this._globalState.mobileDefaultMode : EditionMode.Desktop};
+
+       window.addEventListener("resize", () => {
+           this.setState({mode: window.innerWidth < this._globalState.MobileSizeTrigger ? this._globalState.mobileDefaultMode : EditionMode.Desktop});
+       });
+
+       this._globalState.onMobileDefaultModeChangedObservable.add(() => {
+           this.setState({mode: this._globalState.mobileDefaultMode});
+       })
     }
 
     componentDidMount() {
-        Split([this.monacoRef.current, this.renderingRef.current], {
-            direction: "horizontal",
-            minSize: [200, 200],
-            gutterSize: 4
-        });
+        this.checkSize();
+    }
+
+    componentDidUpdate() {
+        this.checkSize();
+    }
+
+    checkSize() {
+        switch(this.state.mode) {
+            case EditionMode.CodeOnly:                
+                this._splitInstance?.destroy();
+                this._splitInstance = null;
+                this.renderingRef.current!.classList.add("hidden");
+                this.monacoRef.current!.classList.remove("hidden");
+                this.monacoRef.current!.style.width = "100%";
+                break;
+            case EditionMode.RenderingOnly:
+                this._splitInstance?.destroy();
+                this._splitInstance = null;
+                this.monacoRef.current!.classList.add("hidden");
+                this.renderingRef.current!.classList.remove("hidden");
+                this.renderingRef.current!.style.width = "100%";
+                break;
+            case EditionMode.Desktop:
+                if (this._splitInstance) {
+                    return;
+                }
+                this.renderingRef.current!.classList.remove("hidden");
+                this.monacoRef.current!.classList.remove("hidden");
+                this._splitInstance = Split([this.monacoRef.current, this.renderingRef.current], {
+                    direction: "horizontal",
+                    minSize: [200, 200],
+                    gutterSize: 4
+                });
+                break;
+        }
     }
 
     public render() {
@@ -41,11 +82,12 @@ export class Playground extends React.Component<IPlaygroundProps, {errorMessage:
         return (
             <div id="root">  
                 <div ref={this.splitRef} id="split">
-                    <MonacoComponent language="JS" className="split-part" refObject={this.monacoRef}/>    
+                    <MonacoComponent globalState={this._globalState} className="split-part" refObject={this.monacoRef}/>    
                     <div ref={this.renderingRef} className="split-part">
-                        <RenderingComponent />
+                        <RenderingComponent globalState={this._globalState}/>
                     </div>
                 </div>
+                <FooterComponent globalState={this._globalState}/>
             </div>   
         )
     }

+ 84 - 0
Playground/src/scss/footer.scss

@@ -0,0 +1,84 @@
+#footer {
+    width: 100%;
+    height: 100%;
+    grid-column: 1;
+    grid-row: 3;
+    padding: 0;
+    margin: 0;
+    line-height: 100%;
+    display: grid;
+    grid-template-columns: 35px 1fr auto 80px;
+    grid-template-rows: 100%;
+
+    .modeBar {        
+        grid-row: 1;
+        grid-column: 1;
+        cursor: pointer;
+
+        .link {
+            svg {
+                height: 35px;
+                width: 35px;
+            }
+        }
+    }
+   
+    #statusBar {
+        grid-row: 1;
+        grid-column: 2;
+        color: #E74C3C;
+        font-size: 14px;
+        padding-left: 20px;
+        &.languageTS {
+            color: #3f3461;
+
+            .links .link a:hover {
+                background-color: white;
+                color: #bb464b;
+            }
+        }
+    }
+            
+    .links {
+        height: 100%;
+        grid-row: 1;
+        grid-column: 3;      
+    }
+
+    .fps {        
+        grid-row: 1;
+        grid-column: 4;
+        display: grid;
+        align-content: center;
+        justify-content: center;
+        color:white;
+        font-size: 16px;
+    }
+
+    .link {
+        display: inline-block;
+        width: 55px;
+        height: 100%;
+        
+        svg {
+            fill: white;
+            height: 16px;
+            width: 16px;
+        }
+
+        &:hover {
+            background-color: white;
+            svg {
+                fill: #3f3461;
+            }
+        }
+
+        a {
+            height: 100%;
+            width: 100%;
+            display: grid;
+            align-content: center;
+            justify-content: center;
+        }
+    }
+}

+ 11 - 1
Playground/src/scss/main.scss

@@ -1,4 +1,4 @@
-html, body, #root {
+#root {
     width: 100%;
     height: 100%;
     padding: 0;
@@ -6,6 +6,9 @@ html, body, #root {
     overflow: hidden;
     font-family: "acumin-pro-condensed";
     font-weight: normal;    
+    display: grid;
+    grid-template-columns: 100%;
+    grid-template-rows: auto 1fr 35px;
 }
 
 #split {
@@ -14,6 +17,9 @@ html, body, #root {
     padding: 0;
     margin: 0;
     display: flex;
+    grid-row: 2;
+    grid-column: 1;
+    overflow: hidden;
 
     .split-part {
         width: 100%;
@@ -28,3 +34,7 @@ html, body, #root {
         cursor: col-resize;
     }
 }
+
+.hidden {
+    display: none;
+}

+ 3 - 1
Playground/src/scss/rendering.scss

@@ -3,5 +3,7 @@
     height: 100%;
     padding: 0;
     margin: 0;
-    overflow: unset;
+    overflow: unset;    
+    border:none !important;
+    outline:none !important;
 }

+ 282 - 0
Playground/src/tools/monacoManager.ts

@@ -0,0 +1,282 @@
+import 'monaco-editor/esm/vs/editor/editor.api';
+import { GlobalState } from '../globalState';
+
+declare type IStandaloneCodeEditor = import('monaco-editor/esm/vs/editor/editor.api').editor.IStandaloneCodeEditor;
+declare type IEditorConstructionOptions = import('monaco-editor/esm/vs/editor/editor.api').editor.IEditorConstructionOptions;
+
+declare var monaco: any;
+
+export class MonacoManager {
+    private _editor: IStandaloneCodeEditor;
+    private _definitionWorker: Worker;
+    private _deprecatedCandidates: string[];
+    // private _templates: string[];
+
+    public constructor(public globalState: GlobalState) {
+
+    }
+    
+    public async setupMonacoAsync(hostElement: HTMLDivElement) {        
+        // let response = await fetch("https://preview.babylonjs.com/babylon.d.ts");
+        // if (!response.ok) {
+        //     return;
+        // }
+
+        // let libContent = await response.text();
+
+        // response = await fetch("https://preview.babylonjs.com/gui/babylon.gui.d.ts");
+        // if (!response.ok) {
+        //     return;
+        // }
+
+        // libContent += await response.text();
+
+        //   this.setupDefinitionWorker(libContent);
+
+            // Load code templates
+        //   response = await fetch("/templates.json");
+        // if (response.ok) {
+        //      this._templates = await response.json();
+            //}
+
+        // Setup the Monaco compilation pipeline, so we can reuse it directly for our scrpting needs
+        //this.setupMonacoCompilationPipeline(libContent);
+
+            // This is used for a vscode-like color preview for ColorX types
+            //this.setupMonacoColorProvider();
+
+        var editorOptions: IEditorConstructionOptions = {
+            value: "",
+            language: this.globalState.language === "JS" ? "javascript" : "typescript",
+            lineNumbers: "on",
+            roundedSelection: true,
+            automaticLayout: true,
+            scrollBeyondLastLine: false,
+            readOnly: false,
+            theme: "vs-dark",
+            contextmenu: false,
+            folding: true,
+            showFoldingControls: "always",
+            renderIndentGuides: true,
+            minimap: {
+                enabled: true
+            }
+        };      
+
+        this._editor = monaco.editor.create(
+            hostElement,
+            editorOptions as any
+        );     
+        
+        this._editor.onDidChangeModelContent(() => {
+            this.globalState.currentCode = this._editor.getValue();
+        });
+        
+        this._editor.setValue(`var createScene = function () {
+
+            // This creates a basic Babylon Scene object (non-mesh)
+            var scene = new BABYLON.Scene(engine);
+        
+            // This creates and positions a free camera (non-mesh)
+            var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
+        
+            // This targets the camera to scene origin
+            camera.setTarget(BABYLON.Vector3.Zero());
+        
+            // This attaches the camera to the canvas
+            camera.attachControl(canvas, true);
+        
+            // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
+            var light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
+        
+            // Default intensity is 1. Let's dim the light a small amount
+            light.intensity = 0.7;
+        
+            // Our built-in 'sphere' shape.
+            var sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 2, segments: 32}, scene);
+        
+            // Move the sphere upward 1/2 its height
+            sphere.position.y = 1;
+        
+            // Our built-in 'ground' shape.
+            var ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 6, height: 6}, scene);
+        
+            return scene;
+        
+        };`);
+    }
+
+    // Provide an adornment for BABYLON.ColorX types: color preview
+    private _setupMonacoColorProvider() {
+        monaco.languages.registerColorProvider(this.globalState.language == "JS" ? "javascript" : "typescript", {
+            provideColorPresentations: (model: any, colorInfo: any) => {
+                const color = colorInfo.color;
+
+                const precision = 100.0;
+                const converter = (n: number) => Math.round(n * precision) / precision;
+
+                let label;
+                if (color.alpha === undefined || color.alpha === 1.0) {
+                    label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)})`;
+                } else {
+                    label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)}, ${converter(color.alpha)})`;
+                }
+
+                return [{
+                    label: label
+                }];
+            },
+
+            provideDocumentColors: (model: any) => {
+                const digitGroup = "\\s*(\\d*(?:\\.\\d+)?)\\s*";
+                // we add \n{0} to workaround a Monaco bug, when setting regex options on their side
+                const regex = `BABYLON\\.Color(?:3|4)\\s*\\(${digitGroup},${digitGroup},${digitGroup}(?:,${digitGroup})?\\)\\n{0}`;
+                const matches = model.findMatches(regex, false, true, true, null, true);
+
+                const converter = (g: string) => g === undefined ? undefined : Number(g);
+
+                return matches.map((match: any) => ({
+                    color: {
+                        red: converter(match.matches![1])!,
+                        green: converter(match.matches![2])!,
+                        blue: converter(match.matches![3])!,
+                        alpha: converter(match.matches![4])!
+                    },
+                    range: {
+                        startLineNumber: match.range.startLineNumber,
+                        startColumn: match.range.startColumn + match.matches![0].indexOf("("),
+                        endLineNumber: match.range.startLineNumber,
+                        endColumn: match.range.endColumn
+                    }
+                }));
+            }
+        });
+    }
+
+    // Setup both JS and TS compilation pipelines to work with our scripts. 
+    private _setupMonacoCompilationPipeline(libContent: string) {
+        var typescript = monaco.languages.typescript;
+
+        if (this.globalState.language === "JS") {
+            typescript.javascriptDefaults.setCompilerOptions({
+                noLib: false,
+                allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
+            });
+
+            typescript.javascriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
+        } else {
+            typescript.typescriptDefaults.setCompilerOptions({
+                module: typescript.ModuleKind.AMD,
+                target: typescript.ScriptTarget.ESNext,
+                noLib: false,
+                strict: false,
+                alwaysStrict: false,
+                strictFunctionTypes: false,
+                suppressExcessPropertyErrors: false,
+                suppressImplicitAnyIndexErrors: true,
+                noResolve: true,
+                suppressOutputPathCheck: true,
+
+                allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
+            });
+            typescript.typescriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
+        }
+    }
+
+    private _setupDefinitionWorker(libContent: string) {
+        this._definitionWorker = new Worker('workers/definitionWorker.js');
+        this._definitionWorker.addEventListener('message', ({
+            data
+        }) => {
+            this._deprecatedCandidates = data.result;
+            this._analyzeCodeAsync();
+        });
+        this._definitionWorker.postMessage({
+            code: libContent
+        });
+    }
+
+    // This will make sure that all members marked with a deprecated jsdoc attribute will be marked as such in Monaco UI
+    // We use a prefiltered list of deprecated candidates, because the effective call to getCompletionEntryDetails is slow.
+    // @see setupDefinitionWorker
+    private async _analyzeCodeAsync() {
+        // if the definition worker is very fast, this can be called out of context. @see setupDefinitionWorker
+        if (!this._editor)
+            return;
+
+        const model = this._editor.getModel();
+        if (!model)
+            return;
+
+        const uri = model.uri;
+
+        let worker = null;
+        if (this.globalState.language === "JS")
+            worker = await monaco.languages.typescript.getJavaScriptWorker();
+        else
+            worker = await monaco.languages.typescript.getTypeScriptWorker();
+
+        const languageService = await worker(uri);
+        const source = '[deprecated members]';
+
+        monaco.editor.setModelMarkers(model, source, []);
+        const markers: {
+            startLineNumber: number,
+            endLineNumber: number,
+            startColumn: number,
+            endColumn: number,
+            message: string,
+            severity: number,
+            source: string,
+        }[] = [];
+
+        for (const candidate of this._deprecatedCandidates) {
+            const matches = model.findMatches(candidate, false, false, true, null, false);
+            for (const match of matches) {
+                const position = {
+                    lineNumber: match.range.startLineNumber,
+                    column: match.range.startColumn
+                };
+                const wordInfo = model.getWordAtPosition(position);
+                const offset = model.getOffsetAt(position);
+
+                if (!wordInfo) {
+                    continue;
+                }
+
+                // continue if we already found an issue here
+                if (markers.find(m => m.startLineNumber == position.lineNumber && m.startColumn == position.column))
+                    continue;
+
+                // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls
+                // @see setupDefinitionWorker
+                const details = await languageService.getCompletionEntryDetails(uri.toString(), offset, wordInfo.word);
+                if (this.isDeprecatedEntry(details)) {
+                    const deprecatedInfo = details.tags.find(this.isDeprecatedTag);
+                    markers.push({
+                        startLineNumber: match.range.startLineNumber,
+                        endLineNumber: match.range.endLineNumber,
+                        startColumn: wordInfo.startColumn,
+                        endColumn: wordInfo.endColumn,
+                        message: deprecatedInfo.text,
+                        severity: monaco.MarkerSeverity.Warning,
+                        source: source,
+                    });
+                }
+            }
+        }
+
+        monaco.editor.setModelMarkers(model, source, markers);
+    }
+
+    isDeprecatedEntry(details: any) {
+        return details &&
+            details.tags &&
+            details.tags.find(this.isDeprecatedTag);
+    }
+
+    isDeprecatedTag(tag: any) {
+        return tag &&
+            tag.name == "deprecated";
+    }
+}

+ 14 - 0
Playground/src/tools/utilities.ts

@@ -0,0 +1,14 @@
+export class Utilities {
+    public static FastEval(code: string) {
+        var head = document.getElementsByTagName('head')[0];
+        var script = document.createElement('script');
+        script.setAttribute('type', 'text/javascript');
+    
+        script.innerHTML = `try {${code};}
+        catch(e) {
+            handleException(e);
+        }`;
+    
+        head.appendChild(script);
+    }
+}

+ 1 - 8
Playground/webpack.config.js

@@ -24,14 +24,7 @@ var config = babylonWebpackConfig({
         },
         {
             test: /\.svg$/,
-            use: [
-              {
-                loader: 'svg-url-loader',
-                options: {
-                  limit: 10000,
-                },
-              },
-            ],
+            use: ['@svgr/webpack']
         }, {
             test: /\.ttf$/,
             use: ['file-loader']

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
         "@fortawesome/free-regular-svg-icons": "~5.13.0",
         "@fortawesome/free-solid-svg-icons": "~5.13.0",
         "@fortawesome/react-fontawesome": "~0.1.10",
+        "@svgr/webpack": "^5.4.0",
         "@types/chai": "^4.2.11",
         "@types/dagre": "^0.7.44",
         "@types/mocha": "^7.0.2",