Browse Source

Merge pull request #8402 from BabylonJS/master

Nightly
mergify[bot] 5 years ago
parent
commit
8e4c3dcf93
97 changed files with 36504 additions and 2431 deletions
  1. 2 2
      .vscode/launch.json
  2. 52 2
      Tools/Config/config.json
  3. 9 5
      dist/preview release/babylon.d.ts
  4. 1 1
      dist/preview release/babylon.js
  5. 23 9
      dist/preview release/babylon.max.js
  6. 1 1
      dist/preview release/babylon.max.js.map
  7. 19 10
      dist/preview release/babylon.module.d.ts
  8. 9 5
      dist/preview release/documentation.d.ts
  9. 3 3
      dist/preview release/inspector/babylon.inspector.bundle.js
  10. 599 310
      dist/preview release/inspector/babylon.inspector.bundle.max.js
  11. 1 1
      dist/preview release/inspector/babylon.inspector.bundle.max.js.map
  12. 47 23
      dist/preview release/inspector/babylon.inspector.d.ts
  13. 111 63
      dist/preview release/inspector/babylon.inspector.module.d.ts
  14. 10 12
      dist/preview release/loaders/babylon.glTF2FileLoader.js
  15. 1 1
      dist/preview release/loaders/babylon.glTF2FileLoader.js.map
  16. 1 1
      dist/preview release/loaders/babylon.glTF2FileLoader.min.js
  17. 10 12
      dist/preview release/loaders/babylon.glTFFileLoader.js
  18. 1 1
      dist/preview release/loaders/babylon.glTFFileLoader.js.map
  19. 1 1
      dist/preview release/loaders/babylon.glTFFileLoader.min.js
  20. 4 4
      dist/preview release/loaders/babylon.objFileLoader.js
  21. 1 1
      dist/preview release/loaders/babylon.objFileLoader.js.map
  22. 1 1
      dist/preview release/loaders/babylon.objFileLoader.min.js
  23. 14 16
      dist/preview release/loaders/babylonjs.loaders.js
  24. 1 1
      dist/preview release/loaders/babylonjs.loaders.js.map
  25. 2 2
      dist/preview release/loaders/babylonjs.loaders.min.js
  26. 2 2
      dist/preview release/materialsLibrary/babylon.customMaterial.js
  27. 1 1
      dist/preview release/materialsLibrary/babylon.customMaterial.js.map
  28. 1 1
      dist/preview release/materialsLibrary/babylon.customMaterial.min.js
  29. 2 2
      dist/preview release/materialsLibrary/babylonjs.materials.js
  30. 1 1
      dist/preview release/materialsLibrary/babylonjs.materials.js.map
  31. 1 1
      dist/preview release/materialsLibrary/babylonjs.materials.min.js
  32. 1 1
      dist/preview release/packagesSizeBaseLine.json
  33. 135 0
      dist/preview release/sandbox/babylon.sandbox.d.ts
  34. 45 0
      dist/preview release/sandbox/babylon.sandbox.js
  35. 31107 0
      dist/preview release/sandbox/babylon.sandbox.max.js
  36. 1 0
      dist/preview release/sandbox/babylon.sandbox.max.js.map
  37. 298 0
      dist/preview release/sandbox/babylon.sandbox.module.d.ts
  38. 27 0
      dist/preview release/sandbox/package.json
  39. 3 0
      dist/preview release/sandbox/readme-es6.md
  40. 16 0
      dist/preview release/sandbox/readme.md
  41. 19 10
      dist/preview release/viewer/babylon.module.d.ts
  42. 10 10
      dist/preview release/viewer/babylon.viewer.js
  43. 2 2
      dist/preview release/viewer/babylon.viewer.max.js
  44. 3 1
      dist/preview release/what's new.md
  45. 21 15
      inspector/src/components/actionTabs/lines/iconButtonLineComponent.tsx
  46. 346 253
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx
  47. 1380 1033
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx
  48. 300 209
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx
  49. 132 55
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss
  50. 29 28
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx
  51. 85 47
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar.tsx
  52. 72 19
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx
  53. 290 184
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx
  54. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/cameras/commonCameraPropertyGridComponent.tsx
  55. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/meshes/meshPropertyGridComponent.tsx
  56. 2 0
      inspector/src/components/globalState.ts
  57. 4 4
      loaders/src/OBJ/objFileLoader.ts
  58. 12 16
      loaders/src/glTF/2.0/Extensions/KHR_materials_sheen.ts
  59. 1 1
      loaders/src/glTF/2.0/glTFLoader.ts
  60. 1 1
      materialsLibrary/src/custom/customMaterial.ts
  61. 1 1
      materialsLibrary/src/custom/pbrCustomMaterial.ts
  62. 3 0
      sandbox/README-ES6.md
  63. 16 0
      sandbox/README.md
  64. 0 23
      sandbox/index.css
  65. 45 0
      sandbox/public/index-local.html
  66. 54 0
      sandbox/public/index.html
  67. 3 0
      sandbox/public/index.js
  68. 70 0
      sandbox/src/components/dropUpButton.tsx
  69. 68 0
      sandbox/src/components/footer.tsx
  70. 25 0
      sandbox/src/components/footerButton.tsx
  71. 30 0
      sandbox/src/components/footerFileButton.tsx
  72. 271 0
      sandbox/src/components/renderingZone.tsx
  73. 29 0
      sandbox/src/globalState.ts
  74. 1 0
      sandbox/src/img/babylon-identity.svg
  75. 1 0
      sandbox/src/img/icon-edit.svg
  76. 1 0
      sandbox/src/img/icon-ibl.svg
  77. 1 0
      sandbox/src/img/icon-open.svg
  78. 1 0
      sandbox/src/img/logo-fullscreen.svg
  79. 1 0
      sandbox/src/index.ts
  80. 9 0
      sandbox/src/legacy/legacy.ts
  81. 150 0
      sandbox/src/sandbox.tsx
  82. 133 0
      sandbox/src/scss/footer.scss
  83. 98 0
      sandbox/src/scss/main.scss
  84. 26 0
      sandbox/src/scss/renderingZone.scss
  85. 57 0
      sandbox/src/tools/environmentTools.ts
  86. 9 0
      sandbox/src/tools/localStorageHelper.ts
  87. 28 0
      sandbox/tsconfig.json
  88. 46 0
      sandbox/webpack.config.js
  89. 2 2
      src/Engines/thinEngine.ts
  90. 1 1
      src/Materials/PBR/pbrSheenConfiguration.ts
  91. 1 1
      src/Materials/Textures/Loaders/basisTextureLoader.ts
  92. 3 2
      src/Materials/Textures/Loaders/ktxTextureLoader.ts
  93. 2 1
      src/Materials/Textures/internalTextureLoader.ts
  94. 2 1
      src/Misc/fileTools.ts
  95. 24 11
      src/Misc/filesInput.ts
  96. 6 2
      src/PostProcesses/RenderPipeline/Pipelines/defaultRenderingPipeline.ts
  97. 10 1
      src/Shaders/ShadersInclude/pbrBlockSheen.fx

+ 2 - 2
.vscode/launch.json

@@ -45,7 +45,7 @@
             "name": "Launch sandbox (Chrome)",
             "type": "chrome",
             "request": "launch",
-            "url": "http://localhost:1338/sandbox/index-local.html",
+            "url": "http://localhost:1338/sandbox/public/index-local.html",
             "webRoot": "${workspaceRoot}/",
             "sourceMaps": true,
             "preLaunchTask": "run",
@@ -59,7 +59,7 @@
             "type": "edge",
             "version": "dev",
             "request": "launch",
-            "url": "http://localhost:1338/sandbox/index-local.html",
+            "url": "http://localhost:1338/sandbox/public/index-local.html",
             "webRoot": "${workspaceRoot}/",
             "sourceMaps": true,
             "preLaunchTask": "run",

+ 52 - 2
Tools/Config/config.json

@@ -47,7 +47,8 @@
         "serializers",
         "gui",
         "inspector",
-        "nodeEditor"
+        "nodeEditor",
+        "sandbox"
     ],
     "es6modules": [
         "core",
@@ -59,7 +60,8 @@
         "gui",
         "inspector",
         "viewer",
-        "nodeEditor"
+        "nodeEditor",
+        "sandbox"
     ],
     "lintModules": [
         "core",
@@ -636,6 +638,54 @@
             }
         }
     },
+    "sandbox": {
+        "libraries": [
+            {
+                "output": "babylon.sandbox.js",
+                "entry": "./legacy/legacy.ts"
+            }
+        ],
+        "build": {            
+            "ignoreInWorkerMode": true,
+            "ignoreInTestMode": true,
+            "mainFolder": "./sandbox/",
+            "uncheckedLintImports": [
+                "react",
+                "react-dom"
+            ],
+            "umd": {
+                "packageName": "babylonjs-sandbox",
+                "webpackRoot": "SANDBOX",
+                "processDeclaration": {
+                    "filename": "babylon.sandbox.module.d.ts",
+                    "moduleName": "SANDBOX",
+                    "importsToRemove": [],
+                    "classMap": {
+                        "babylonjs": "BABYLON",
+                        "react": "React",
+                        "@babylonjs/core": "BABYLON",
+                        "@fortawesome": false
+                    }
+                }
+            },
+            "es6": {
+                "webpackBuild": true,
+                "buildDependencies": [
+                    "Tools/**/*"
+                ],
+                "packageName": "@babylonjs/sandbox",
+                "readme": "dist/preview release/sandbox/readme-es6.md",
+                "packagesFiles": [
+                    "babylon.sandbox.max.js",
+                    "babylon.sandbox.max.js.map",
+                    "babylon.sandbox.module.d.ts",
+                    "readme.md"
+                ],
+                "typings": "babylon.sandbox.module.d.ts",
+                "index": "babylon.sandbox.max.js"
+            }
+        }
+    },
     "viewer": {
         "libraries": [
             {

File diff suppressed because it is too large
+ 9 - 5
dist/preview release/babylon.d.ts


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/babylon.js


File diff suppressed because it is too large
+ 23 - 9
dist/preview release/babylon.max.js


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/babylon.max.js.map


File diff suppressed because it is too large
+ 19 - 10
dist/preview release/babylon.module.d.ts


File diff suppressed because it is too large
+ 9 - 5
dist/preview release/documentation.d.ts


File diff suppressed because it is too large
+ 3 - 3
dist/preview release/inspector/babylon.inspector.bundle.js


File diff suppressed because it is too large
+ 599 - 310
dist/preview release/inspector/babylon.inspector.bundle.max.js


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/inspector/babylon.inspector.bundle.max.js.map


+ 47 - 23
dist/preview release/inspector/babylon.inspector.d.ts

@@ -647,7 +647,10 @@ declare module INSPECTOR {
     }> {
         readonly _frames: object[];
         private _scrollable;
+        private _scrollbarHandle;
         private _direction;
+        private _scrolling;
+        private _shiftX;
         constructor(props: ITimelineProps);
         playBackwards(event: React.MouseEvent<HTMLDivElement>): void;
         play(event: React.MouseEvent<HTMLDivElement>): void;
@@ -664,6 +667,12 @@ declare module INSPECTOR {
         isFrameBeingUsed(frame: number, direction: number): number | false;
         dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
         dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+        scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
         render(): JSX.Element;
     }
 }
@@ -671,9 +680,19 @@ declare module INSPECTOR {
     interface IPlayheadProps {
         frame: number;
         offset: number;
+        onCurrentFrameChange: (frame: number) => void;
     }
     export class Playhead extends React.Component<IPlayheadProps> {
+        private _direction;
+        private _active;
         constructor(props: IPlayheadProps);
+        dragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        dragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        drag(e: React.TouchEvent<HTMLDivElement>): void;
+        drag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        dragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        dragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        calculateMove(): string;
         render(): JSX.Element;
     }
 }
@@ -883,6 +902,7 @@ declare module INSPECTOR {
         playheadPos: number;
         isPlaying: boolean;
         selectedPathData: ICurveData[] | undefined;
+        selectedCoordinate: number;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -890,37 +910,38 @@ declare module INSPECTOR {
         private _svgKeyframes;
         private _isPlaying;
         private _graphCanvas;
-        private _selectedCurve;
         private _svgCanvas;
         private _isTargetedAnimation;
+        private _onBeforeRenderObserver;
+        private _mainAnimatable;
         constructor(props: IAnimationCurveEditorComponentProps);
         componentDidMount(): void;
         /**
-        * Notifications
-        * To add notification we set the state and clear to make the notification bar hide.
-        */
+         * Notifications
+         * To add notification we set the state and clear to make the notification bar hide.
+         */
         clearNotification(): void;
         /**
-        * Zoom and Scroll
-        * This section handles zoom and scroll
-        * of the graph area.
-        */
+         * Zoom and Scroll
+         * This section handles zoom and scroll
+         * of the graph area.
+         */
         zoom(e: React.WheelEvent<HTMLDivElement>): void;
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
         /**
-        * Keyframe Manipulation
-        * This section handles events from SvgDraggableArea.
-        */
+         * Keyframe Manipulation
+         * This section handles events from SvgDraggableArea.
+         */
         selectKeyframe(id: string): void;
         selectedControlPoint(type: string, id: string): void;
         updateValuePerCoordinate(dataType: number, value: number | BABYLON.Vector2 | BABYLON.Vector3 | BABYLON.Color3 | BABYLON.Color4 | BABYLON.Size | BABYLON.Quaternion, newValue: number, coordinate?: number): number | BABYLON.Vector3 | BABYLON.Quaternion | BABYLON.Color3 | BABYLON.Color4 | BABYLON.Vector2 | BABYLON.Size;
         renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string): void;
         /**
-        * Actions
-        * This section handles events from GraphActionsBar.
-        */
+         * Actions
+         * This section handles events from GraphActionsBar.
+         */
         handleFrameChange(event: React.ChangeEvent<HTMLInputElement>): void;
         handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void;
         setFlatTangent(): void;
@@ -931,9 +952,9 @@ declare module INSPECTOR {
         removeKeyframeClick(): void;
         addKeyFrame(event: React.MouseEvent<SVGSVGElement>): void;
         /**
-        * Curve Rendering Functions
-        * This section handles how to render curves.
-        */
+         * Curve Rendering Functions
+         * This section handles how to render curves.
+         */
         linearInterpolation(keyframes: BABYLON.IAnimationKey[], data: string, middle: number): string;
         setKeyframePointLinear(point: BABYLON.Vector2, index: number): void;
         flatTangents(keyframes: BABYLON.IAnimationKey[], dataType: number): BABYLON.IAnimationKey[];
@@ -958,20 +979,23 @@ declare module INSPECTOR {
         setKeyframePoint(controlPoints: BABYLON.Vector2[], index: number, keyframesCount: number): void;
         interpolateControlPoints(p0: BABYLON.Vector2, p1: BABYLON.Vector2, u: number, p2: BABYLON.Vector2, v: number, p3: BABYLON.Vector2): BABYLON.Vector2[] | undefined;
         /**
-        * Core functions
-        * This section handles main Curve Editor Functions.
-        */
+         * Core functions
+         * This section handles main Curve Editor Functions.
+         */
         selectAnimation(animation: BABYLON.Animation, coordinate?: SelectedCoordinate): void;
         isAnimationPlaying(): boolean;
         playStopAnimation(): boolean;
         analizeAnimationForLerp(animation: BABYLON.Animation | null): boolean;
         /**
-        * Timeline
-        * This section controls the timeline.
-        */
+         * Timeline
+         * This section controls the timeline.
+         */
         changeCurrentFrame(frame: number): void;
         updateFrameInKeyFrame(frame: number, index: number): void;
         playPause(direction: number): void;
+        moveFrameTo(e: React.MouseEvent<SVGRectElement, MouseEvent>): void;
+        registerObs(): void;
+        componentWillUnmount(): void;
         render(): JSX.Element;
     }
 }

+ 111 - 63
dist/preview release/inspector/babylon.inspector.module.d.ts

@@ -672,7 +672,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/lines/iconButtonLineComponent" {
-    import * as React from "react";
+    import * as React from 'react';
     export interface IIconButtonLineComponentProps {
         icon: string;
         onClick: () => void;
@@ -711,7 +711,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/timeline" {
-    import * as React from "react";
+    import * as React from 'react';
     import { IAnimationKey } from 'babylonjs/Animations/animationKey';
     interface ITimelineProps {
         keyframes: IAnimationKey[] | null;
@@ -728,7 +728,10 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }> {
         readonly _frames: object[];
         private _scrollable;
+        private _scrollbarHandle;
         private _direction;
+        private _scrolling;
+        private _shiftX;
         constructor(props: ITimelineProps);
         playBackwards(event: React.MouseEvent<HTMLDivElement>): void;
         play(event: React.MouseEvent<HTMLDivElement>): void;
@@ -745,17 +748,33 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         isFrameBeingUsed(frame: number, direction: number): number | false;
         dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
         dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+        scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
         render(): JSX.Element;
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/playhead" {
-    import * as React from "react";
+    import * as React from 'react';
     interface IPlayheadProps {
         frame: number;
         offset: number;
+        onCurrentFrameChange: (frame: number) => void;
     }
     export class Playhead extends React.Component<IPlayheadProps> {
+        private _direction;
+        private _active;
         constructor(props: IPlayheadProps);
+        dragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        dragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        drag(e: React.TouchEvent<HTMLDivElement>): void;
+        drag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        dragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        dragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        calculateMove(): string;
         render(): JSX.Element;
     }
 }
@@ -772,7 +791,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar" {
-    import * as React from "react";
+    import * as React from 'react';
     interface IGraphActionsBarProps {
         addKeyframe: () => void;
         removeKeyframe: () => void;
@@ -795,8 +814,8 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/addAnimation" {
-    import * as React from "react";
-    import { Observable } from "babylonjs/Misc/observable";
+    import * as React from 'react';
+    import { Observable } from 'babylonjs/Misc/observable';
     import { PropertyChangedEvent } from "babylonjs-inspector/components/propertyChangedEvent";
     import { Animation } from 'babylonjs/Animations/animation';
     import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
@@ -922,13 +941,13 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/editorControls" {
-    import * as React from "react";
-    import { Observable } from "babylonjs/Misc/observable";
+    import * as React from 'react';
+    import { Observable } from 'babylonjs/Misc/observable';
     import { PropertyChangedEvent } from "babylonjs-inspector/components/propertyChangedEvent";
-    import { Animation } from "babylonjs/Animations/animation";
+    import { Animation } from 'babylonjs/Animations/animation';
     import { SelectedCoordinate } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/animationListTree";
-    import { IAnimatable } from "babylonjs/Animations/animatable.interface";
-    import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+    import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+    import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
     import { LockObject } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/lockObject";
     interface IEditorControlsProps {
         isTargetedAnimation: boolean;
@@ -958,7 +977,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent" {
-    import * as React from "react";
+    import * as React from 'react';
     import { Animation } from 'babylonjs/Animations/animation';
     import { Vector2, Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
     import { Color3, Color4 } from 'babylonjs/Maths/math.color';
@@ -966,9 +985,9 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     import { EasingFunction } from 'babylonjs/Animations/easing';
     import { IAnimationKey } from 'babylonjs/Animations/animationKey';
     import { IKeyframeSvgPoint } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/keyframeSvgPoint";
-    import { Scene } from "babylonjs/scene";
+    import { Scene } from 'babylonjs/scene';
     import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-    import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+    import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
     import { SelectedCoordinate } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/animationListTree";
     import { LockObject } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/lockObject";
     interface IAnimationCurveEditorComponentProps {
@@ -1008,6 +1027,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         playheadPos: number;
         isPlaying: boolean;
         selectedPathData: ICurveData[] | undefined;
+        selectedCoordinate: number;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -1015,37 +1035,38 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         private _svgKeyframes;
         private _isPlaying;
         private _graphCanvas;
-        private _selectedCurve;
         private _svgCanvas;
         private _isTargetedAnimation;
+        private _onBeforeRenderObserver;
+        private _mainAnimatable;
         constructor(props: IAnimationCurveEditorComponentProps);
         componentDidMount(): void;
         /**
-        * Notifications
-        * To add notification we set the state and clear to make the notification bar hide.
-        */
+         * Notifications
+         * To add notification we set the state and clear to make the notification bar hide.
+         */
         clearNotification(): void;
         /**
-        * Zoom and Scroll
-        * This section handles zoom and scroll
-        * of the graph area.
-        */
+         * Zoom and Scroll
+         * This section handles zoom and scroll
+         * of the graph area.
+         */
         zoom(e: React.WheelEvent<HTMLDivElement>): void;
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
         /**
-        * Keyframe Manipulation
-        * This section handles events from SvgDraggableArea.
-        */
+         * Keyframe Manipulation
+         * This section handles events from SvgDraggableArea.
+         */
         selectKeyframe(id: string): void;
         selectedControlPoint(type: string, id: string): void;
         updateValuePerCoordinate(dataType: number, value: number | Vector2 | Vector3 | Color3 | Color4 | Size | Quaternion, newValue: number, coordinate?: number): number | Vector3 | Quaternion | Color3 | Color4 | Vector2 | Size;
         renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string): void;
         /**
-        * Actions
-        * This section handles events from GraphActionsBar.
-        */
+         * Actions
+         * This section handles events from GraphActionsBar.
+         */
         handleFrameChange(event: React.ChangeEvent<HTMLInputElement>): void;
         handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void;
         setFlatTangent(): void;
@@ -1056,9 +1077,9 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         removeKeyframeClick(): void;
         addKeyFrame(event: React.MouseEvent<SVGSVGElement>): void;
         /**
-        * Curve Rendering Functions
-        * This section handles how to render curves.
-        */
+         * Curve Rendering Functions
+         * This section handles how to render curves.
+         */
         linearInterpolation(keyframes: IAnimationKey[], data: string, middle: number): string;
         setKeyframePointLinear(point: Vector2, index: number): void;
         flatTangents(keyframes: IAnimationKey[], dataType: number): IAnimationKey[];
@@ -1083,20 +1104,23 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         setKeyframePoint(controlPoints: Vector2[], index: number, keyframesCount: number): void;
         interpolateControlPoints(p0: Vector2, p1: Vector2, u: number, p2: Vector2, v: number, p3: Vector2): Vector2[] | undefined;
         /**
-        * Core functions
-        * This section handles main Curve Editor Functions.
-        */
+         * Core functions
+         * This section handles main Curve Editor Functions.
+         */
         selectAnimation(animation: Animation, coordinate?: SelectedCoordinate): void;
         isAnimationPlaying(): boolean;
         playStopAnimation(): boolean;
         analizeAnimationForLerp(animation: Animation | null): boolean;
         /**
-        * Timeline
-        * This section controls the timeline.
-        */
+         * Timeline
+         * This section controls the timeline.
+         */
         changeCurrentFrame(frame: number): void;
         updateFrameInKeyFrame(frame: number, index: number): void;
         playPause(direction: number): void;
+        moveFrameTo(e: React.MouseEvent<SVGRectElement, MouseEvent>): void;
+        registerObs(): void;
+        componentWillUnmount(): void;
         render(): JSX.Element;
     }
 }
@@ -1126,9 +1150,9 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
     }
 }
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent" {
-    import * as React from "react";
-    import { Observable } from "babylonjs/Misc/observable";
-    import { Scene } from "babylonjs/scene";
+    import * as React from 'react';
+    import { Observable } from 'babylonjs/Misc/observable';
+    import { Scene } from 'babylonjs/scene';
     import { PropertyChangedEvent } from "babylonjs-inspector/components/propertyChangedEvent";
     import { LockObject } from "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/lockObject";
     import { GlobalState } from "babylonjs-inspector/components/globalState";
@@ -4166,7 +4190,10 @@ declare module INSPECTOR {
     }> {
         readonly _frames: object[];
         private _scrollable;
+        private _scrollbarHandle;
         private _direction;
+        private _scrolling;
+        private _shiftX;
         constructor(props: ITimelineProps);
         playBackwards(event: React.MouseEvent<HTMLDivElement>): void;
         play(event: React.MouseEvent<HTMLDivElement>): void;
@@ -4183,6 +4210,12 @@ declare module INSPECTOR {
         isFrameBeingUsed(frame: number, direction: number): number | false;
         dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
         dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+        scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
         render(): JSX.Element;
     }
 }
@@ -4190,9 +4223,19 @@ declare module INSPECTOR {
     interface IPlayheadProps {
         frame: number;
         offset: number;
+        onCurrentFrameChange: (frame: number) => void;
     }
     export class Playhead extends React.Component<IPlayheadProps> {
+        private _direction;
+        private _active;
         constructor(props: IPlayheadProps);
+        dragStart(e: React.TouchEvent<HTMLDivElement>): void;
+        dragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        drag(e: React.TouchEvent<HTMLDivElement>): void;
+        drag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        dragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+        dragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+        calculateMove(): string;
         render(): JSX.Element;
     }
 }
@@ -4402,6 +4445,7 @@ declare module INSPECTOR {
         playheadPos: number;
         isPlaying: boolean;
         selectedPathData: ICurveData[] | undefined;
+        selectedCoordinate: number;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -4409,37 +4453,38 @@ declare module INSPECTOR {
         private _svgKeyframes;
         private _isPlaying;
         private _graphCanvas;
-        private _selectedCurve;
         private _svgCanvas;
         private _isTargetedAnimation;
+        private _onBeforeRenderObserver;
+        private _mainAnimatable;
         constructor(props: IAnimationCurveEditorComponentProps);
         componentDidMount(): void;
         /**
-        * Notifications
-        * To add notification we set the state and clear to make the notification bar hide.
-        */
+         * Notifications
+         * To add notification we set the state and clear to make the notification bar hide.
+         */
         clearNotification(): void;
         /**
-        * Zoom and Scroll
-        * This section handles zoom and scroll
-        * of the graph area.
-        */
+         * Zoom and Scroll
+         * This section handles zoom and scroll
+         * of the graph area.
+         */
         zoom(e: React.WheelEvent<HTMLDivElement>): void;
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
         /**
-        * Keyframe Manipulation
-        * This section handles events from SvgDraggableArea.
-        */
+         * Keyframe Manipulation
+         * This section handles events from SvgDraggableArea.
+         */
         selectKeyframe(id: string): void;
         selectedControlPoint(type: string, id: string): void;
         updateValuePerCoordinate(dataType: number, value: number | BABYLON.Vector2 | BABYLON.Vector3 | BABYLON.Color3 | BABYLON.Color4 | BABYLON.Size | BABYLON.Quaternion, newValue: number, coordinate?: number): number | BABYLON.Vector3 | BABYLON.Quaternion | BABYLON.Color3 | BABYLON.Color4 | BABYLON.Vector2 | BABYLON.Size;
         renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string): void;
         /**
-        * Actions
-        * This section handles events from GraphActionsBar.
-        */
+         * Actions
+         * This section handles events from GraphActionsBar.
+         */
         handleFrameChange(event: React.ChangeEvent<HTMLInputElement>): void;
         handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void;
         setFlatTangent(): void;
@@ -4450,9 +4495,9 @@ declare module INSPECTOR {
         removeKeyframeClick(): void;
         addKeyFrame(event: React.MouseEvent<SVGSVGElement>): void;
         /**
-        * Curve Rendering Functions
-        * This section handles how to render curves.
-        */
+         * Curve Rendering Functions
+         * This section handles how to render curves.
+         */
         linearInterpolation(keyframes: BABYLON.IAnimationKey[], data: string, middle: number): string;
         setKeyframePointLinear(point: BABYLON.Vector2, index: number): void;
         flatTangents(keyframes: BABYLON.IAnimationKey[], dataType: number): BABYLON.IAnimationKey[];
@@ -4477,20 +4522,23 @@ declare module INSPECTOR {
         setKeyframePoint(controlPoints: BABYLON.Vector2[], index: number, keyframesCount: number): void;
         interpolateControlPoints(p0: BABYLON.Vector2, p1: BABYLON.Vector2, u: number, p2: BABYLON.Vector2, v: number, p3: BABYLON.Vector2): BABYLON.Vector2[] | undefined;
         /**
-        * Core functions
-        * This section handles main Curve Editor Functions.
-        */
+         * Core functions
+         * This section handles main Curve Editor Functions.
+         */
         selectAnimation(animation: BABYLON.Animation, coordinate?: SelectedCoordinate): void;
         isAnimationPlaying(): boolean;
         playStopAnimation(): boolean;
         analizeAnimationForLerp(animation: BABYLON.Animation | null): boolean;
         /**
-        * Timeline
-        * This section controls the timeline.
-        */
+         * Timeline
+         * This section controls the timeline.
+         */
         changeCurrentFrame(frame: number): void;
         updateFrameInKeyFrame(frame: number, index: number): void;
         playPause(direction: number): void;
+        moveFrameTo(e: React.MouseEvent<SVGRectElement, MouseEvent>): void;
+        registerObs(): void;
+        componentWillUnmount(): void;
         render(): JSX.Element;
     }
 }

+ 10 - 12
dist/preview release/loaders/babylon.glTF2FileLoader.js

@@ -931,23 +931,21 @@ var KHR_materials_sheen = /** @class */ (function () {
         }
         var promises = new Array();
         babylonMaterial.sheen.isEnabled = true;
-        if (properties.intensityFactor != undefined) {
-            babylonMaterial.sheen.intensity = properties.intensityFactor;
+        babylonMaterial.sheen.intensity = 1;
+        if (properties.sheenColorFactor != undefined) {
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.sheenColorFactor);
         }
         else {
-            babylonMaterial.sheen.intensity = 0;
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].Black();
         }
-        if (properties.colorFactor != undefined) {
-            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.colorFactor);
-        }
-        if (properties.colorIntensityTexture) {
-            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.colorIntensityTexture, function (texture) {
-                texture.name = babylonMaterial.name + " (Sheen Intensity)";
+        if (properties.sheenTexture) {
+            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.sheenTexture, function (texture) {
+                texture.name = babylonMaterial.name + " (Sheen Color)";
                 babylonMaterial.sheen.texture = texture;
             }));
         }
-        if (properties.roughnessFactor !== undefined) {
-            babylonMaterial.sheen.roughness = properties.roughnessFactor;
+        if (properties.sheenRoughnessFactor !== undefined) {
+            babylonMaterial.sheen.roughness = properties.sheenRoughnessFactor;
         }
         else {
             babylonMaterial.sheen.roughness = 0;
@@ -2667,7 +2665,7 @@ var GLTFLoader = /** @class */ (function () {
                 });
             });
             return resultPromise;
-        }, function (error) {
+        }).catch(function (error) {
             if (!_this._disposed) {
                 _this._parent.onErrorObservable.notifyObservers(error);
                 _this._parent.onErrorObservable.clear();

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.glTF2FileLoader.js.map


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.glTF2FileLoader.min.js


+ 10 - 12
dist/preview release/loaders/babylon.glTFFileLoader.js

@@ -3511,23 +3511,21 @@ var KHR_materials_sheen = /** @class */ (function () {
         }
         var promises = new Array();
         babylonMaterial.sheen.isEnabled = true;
-        if (properties.intensityFactor != undefined) {
-            babylonMaterial.sheen.intensity = properties.intensityFactor;
+        babylonMaterial.sheen.intensity = 1;
+        if (properties.sheenColorFactor != undefined) {
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.sheenColorFactor);
         }
         else {
-            babylonMaterial.sheen.intensity = 0;
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].Black();
         }
-        if (properties.colorFactor != undefined) {
-            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.colorFactor);
-        }
-        if (properties.colorIntensityTexture) {
-            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.colorIntensityTexture, function (texture) {
-                texture.name = babylonMaterial.name + " (Sheen Intensity)";
+        if (properties.sheenTexture) {
+            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.sheenTexture, function (texture) {
+                texture.name = babylonMaterial.name + " (Sheen Color)";
                 babylonMaterial.sheen.texture = texture;
             }));
         }
-        if (properties.roughnessFactor !== undefined) {
-            babylonMaterial.sheen.roughness = properties.roughnessFactor;
+        if (properties.sheenRoughnessFactor !== undefined) {
+            babylonMaterial.sheen.roughness = properties.sheenRoughnessFactor;
         }
         else {
             babylonMaterial.sheen.roughness = 0;
@@ -5247,7 +5245,7 @@ var GLTFLoader = /** @class */ (function () {
                 });
             });
             return resultPromise;
-        }, function (error) {
+        }).catch(function (error) {
             if (!_this._disposed) {
                 _this._parent.onErrorObservable.notifyObservers(error);
                 _this._parent.onErrorObservable.clear();

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.glTFFileLoader.js.map


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.glTFFileLoader.min.js


+ 4 - 4
dist/preview release/loaders/babylon.objFileLoader.js

@@ -738,10 +738,10 @@ var OBJFileLoader = /** @class */ (function () {
                 unwrappedPositionsForBabylon.push(wrappedPositionForBabylon[l].x, wrappedPositionForBabylon[l].y, wrappedPositionForBabylon[l].z);
                 unwrappedNormalsForBabylon.push(wrappedNormalsForBabylon[l].x, wrappedNormalsForBabylon[l].y, wrappedNormalsForBabylon[l].z);
                 unwrappedUVForBabylon.push(wrappedUvsForBabylon[l].x, wrappedUvsForBabylon[l].y); //z is an optional value not supported by BABYLON
-            }
-            if (_this._meshLoadOptions.ImportVertexColors === true) {
-                //Push the r, g, b, a values of each element in the unwrapped array
-                unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                if (_this._meshLoadOptions.ImportVertexColors === true) {
+                    //Push the r, g, b, a values of each element in the unwrapped array
+                    unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                }
             }
             // Reset arrays for the next new meshes
             wrappedPositionForBabylon = [];

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.objFileLoader.js.map


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylon.objFileLoader.min.js


+ 14 - 16
dist/preview release/loaders/babylonjs.loaders.js

@@ -967,10 +967,10 @@ var OBJFileLoader = /** @class */ (function () {
                 unwrappedPositionsForBabylon.push(wrappedPositionForBabylon[l].x, wrappedPositionForBabylon[l].y, wrappedPositionForBabylon[l].z);
                 unwrappedNormalsForBabylon.push(wrappedNormalsForBabylon[l].x, wrappedNormalsForBabylon[l].y, wrappedNormalsForBabylon[l].z);
                 unwrappedUVForBabylon.push(wrappedUvsForBabylon[l].x, wrappedUvsForBabylon[l].y); //z is an optional value not supported by BABYLON
-            }
-            if (_this._meshLoadOptions.ImportVertexColors === true) {
-                //Push the r, g, b, a values of each element in the unwrapped array
-                unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                if (_this._meshLoadOptions.ImportVertexColors === true) {
+                    //Push the r, g, b, a values of each element in the unwrapped array
+                    unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                }
             }
             // Reset arrays for the next new meshes
             wrappedPositionForBabylon = [];
@@ -4891,23 +4891,21 @@ var KHR_materials_sheen = /** @class */ (function () {
         }
         var promises = new Array();
         babylonMaterial.sheen.isEnabled = true;
-        if (properties.intensityFactor != undefined) {
-            babylonMaterial.sheen.intensity = properties.intensityFactor;
+        babylonMaterial.sheen.intensity = 1;
+        if (properties.sheenColorFactor != undefined) {
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.sheenColorFactor);
         }
         else {
-            babylonMaterial.sheen.intensity = 0;
-        }
-        if (properties.colorFactor != undefined) {
-            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].FromArray(properties.colorFactor);
+            babylonMaterial.sheen.color = babylonjs_Materials_PBR_pbrMaterial__WEBPACK_IMPORTED_MODULE_0__["Color3"].Black();
         }
-        if (properties.colorIntensityTexture) {
-            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.colorIntensityTexture, function (texture) {
-                texture.name = babylonMaterial.name + " (Sheen Intensity)";
+        if (properties.sheenTexture) {
+            promises.push(this._loader.loadTextureInfoAsync(context + "/sheenTexture", properties.sheenTexture, function (texture) {
+                texture.name = babylonMaterial.name + " (Sheen Color)";
                 babylonMaterial.sheen.texture = texture;
             }));
         }
-        if (properties.roughnessFactor !== undefined) {
-            babylonMaterial.sheen.roughness = properties.roughnessFactor;
+        if (properties.sheenRoughnessFactor !== undefined) {
+            babylonMaterial.sheen.roughness = properties.sheenRoughnessFactor;
         }
         else {
             babylonMaterial.sheen.roughness = 0;
@@ -6627,7 +6625,7 @@ var GLTFLoader = /** @class */ (function () {
                 });
             });
             return resultPromise;
-        }, function (error) {
+        }).catch(function (error) {
             if (!_this._disposed) {
                 _this._parent.onErrorObservable.notifyObservers(error);
                 _this._parent.onErrorObservable.clear();

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/loaders/babylonjs.loaders.js.map


File diff suppressed because it is too large
+ 2 - 2
dist/preview release/loaders/babylonjs.loaders.min.js


+ 2 - 2
dist/preview release/materialsLibrary/babylon.customMaterial.js

@@ -501,7 +501,7 @@ var CustomMaterial = /** @class */ (function (_super) {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 this._newSamplerInstances[kind + "-" + name] = param;
             }
             else {
@@ -758,7 +758,7 @@ var PBRCustomMaterial = /** @class */ (function (_super) {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 this._newSamplerInstances[kind + "-" + name] = param;
             }
             else {

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/materialsLibrary/babylon.customMaterial.js.map


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/materialsLibrary/babylon.customMaterial.min.js


+ 2 - 2
dist/preview release/materialsLibrary/babylonjs.materials.js

@@ -889,7 +889,7 @@ var CustomMaterial = /** @class */ (function (_super) {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 this._newSamplerInstances[kind + "-" + name] = param;
             }
             else {
@@ -1146,7 +1146,7 @@ var PBRCustomMaterial = /** @class */ (function (_super) {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 this._newSamplerInstances[kind + "-" + name] = param;
             }
             else {

File diff suppressed because it is too large
+ 1 - 1
dist/preview release/materialsLibrary/babylonjs.materials.js.map


File diff suppressed because it is too large
+ 1 - 1
dist/preview release/materialsLibrary/babylonjs.materials.min.js


+ 1 - 1
dist/preview release/packagesSizeBaseLine.json

@@ -1 +1 @@
-{"thinEngineOnly":116131,"engineOnly":152567,"sceneOnly":514714,"minGridMaterial":651577,"minStandardMaterial":800106}
+{"thinEngineOnly":116133,"engineOnly":152569,"sceneOnly":514772,"minGridMaterial":651635,"minStandardMaterial":800164}

+ 135 - 0
dist/preview release/sandbox/babylon.sandbox.d.ts

@@ -0,0 +1,135 @@
+/// <reference types="react" />
+declare module SANDBOX {
+    export class GlobalState {
+        currentScene: BABYLON.Scene;
+        onSceneLoaded: BABYLON.Observable<{
+            scene: BABYLON.Scene;
+            filename: string;
+        }>;
+        onError: BABYLON.Observable<{
+            scene?: BABYLON.Scene | undefined;
+            message?: string | undefined;
+        }>;
+        onEnvironmentChanged: BABYLON.Observable<string>;
+        onRequestClickInterceptor: BABYLON.Observable<void>;
+        onClickInterceptorClicked: BABYLON.Observable<void>;
+        filesInput: BABYLON.FilesInput;
+        isDebugLayerEnabled: boolean;
+        showDebugLayer(): void;
+        hideDebugLayer(): void;
+    }
+}
+declare module SANDBOX {
+    export class LocalStorageHelper {
+        static ReadLocalStorageValue(key: string, defaultValue: number): number;
+    }
+}
+declare module SANDBOX {
+    export class EnvironmentTools {
+        static SkyboxPath: string;
+        static Skyboxes: string[];
+        static SkyboxesNames: string[];
+        static LoadSkyboxPathTexture(scene: BABYLON.Scene): BABYLON.HDRCubeTexture | BABYLON.CubeTexture;
+        static HookWithEnvironmentChange(globalState: GlobalState): void;
+    }
+}
+declare module SANDBOX {
+    interface IRenderingZoneProps {
+        globalState: GlobalState;
+        assetUrl?: string;
+        cameraPosition?: BABYLON.Vector3;
+        expanded: boolean;
+    }
+    export class RenderingZone extends React.Component<IRenderingZoneProps> {
+        private _currentPluginName;
+        private _engine;
+        private _scene;
+        private _canvas;
+        constructor(props: IRenderingZoneProps);
+        initEngine(): void;
+        prepareCamera(): void;
+        handleErrors(): void;
+        prepareLighting(): void;
+        onSceneLoaded(filename: string): void;
+        loadAssetFromUrl(): void;
+        loadAsset(): void;
+        componentDidMount(): void;
+        shouldComponentUpdate(nextProps: IRenderingZoneProps): boolean;
+        render(): JSX.Element;
+    }
+}
+declare module SANDBOX {
+    interface IFooterButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        onClick: () => void;
+        icon: any;
+        label: string;
+    }
+    export class FooterButton extends React.Component<IFooterButtonProps> {
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IDropUpButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        options: string[];
+        onOptionPicked: (option: string) => void;
+    }
+    export class DropUpButton extends React.Component<IDropUpButtonProps, {
+        isOpen: boolean;
+    }> {
+        private _onClickInterceptorClickedObserver;
+        constructor(props: IDropUpButtonProps);
+        componentWillUnmount(): void;
+        switchDropUp(): void;
+        clickOption(option: string): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IFooterFileButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        onFilesPicked: (evt: Event, files: FileList | null) => void;
+    }
+    export class FooterFileButton extends React.Component<IFooterFileButtonProps> {
+        onFilePicked(evt: React.ChangeEvent<HTMLInputElement>): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IFooterProps {
+        globalState: GlobalState;
+    }
+    export class Footer extends React.Component<IFooterProps> {
+        constructor(props: IFooterProps);
+        showInspector(): void;
+        render(): JSX.Element;
+    }
+}
+declare module SANDBOX {
+    interface ISandboxProps {
+    }
+    export class Sandbox extends React.Component<ISandboxProps, {
+        isFooterVisible: boolean;
+        errorMessage: string;
+    }> {
+        private _globalState;
+        private _assetUrl?;
+        private _cameraPosition?;
+        private _logoRef;
+        private _dropTextRef;
+        private _clickInterceptorRef;
+        constructor(props: ISandboxProps);
+        checkUrl(): void;
+        componentDidUpdate(): void;
+        render(): JSX.Element;
+        static Show(hostElement: HTMLElement): void;
+    }
+}

File diff suppressed because it is too large
+ 45 - 0
dist/preview release/sandbox/babylon.sandbox.js


File diff suppressed because it is too large
+ 31107 - 0
dist/preview release/sandbox/babylon.sandbox.max.js


File diff suppressed because it is too large
+ 1 - 0
dist/preview release/sandbox/babylon.sandbox.max.js.map


+ 298 - 0
dist/preview release/sandbox/babylon.sandbox.module.d.ts

@@ -0,0 +1,298 @@
+/// <reference types="react" />
+declare module "babylonjs-sandbox/globalState" {
+    import { Observable } from 'babylonjs/Misc/observable';
+    import { Scene } from 'babylonjs/scene';
+    import { FilesInput } from 'babylonjs/Misc/filesInput';
+    export class GlobalState {
+        currentScene: Scene;
+        onSceneLoaded: Observable<{
+            scene: Scene;
+            filename: string;
+        }>;
+        onError: Observable<{
+            scene?: Scene | undefined;
+            message?: string | undefined;
+        }>;
+        onEnvironmentChanged: Observable<string>;
+        onRequestClickInterceptor: Observable<void>;
+        onClickInterceptorClicked: Observable<void>;
+        filesInput: FilesInput;
+        isDebugLayerEnabled: boolean;
+        showDebugLayer(): void;
+        hideDebugLayer(): void;
+    }
+}
+declare module "babylonjs-sandbox/tools/localStorageHelper" {
+    export class LocalStorageHelper {
+        static ReadLocalStorageValue(key: string, defaultValue: number): number;
+    }
+}
+declare module "babylonjs-sandbox/tools/environmentTools" {
+    import { HDRCubeTexture } from 'babylonjs/Materials/Textures/hdrCubeTexture';
+    import { CubeTexture } from 'babylonjs/Materials/Textures/cubeTexture';
+    import { Scene } from 'babylonjs/scene';
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    export class EnvironmentTools {
+        static SkyboxPath: string;
+        static Skyboxes: string[];
+        static SkyboxesNames: string[];
+        static LoadSkyboxPathTexture(scene: Scene): HDRCubeTexture | CubeTexture;
+        static HookWithEnvironmentChange(globalState: GlobalState): void;
+    }
+}
+declare module "babylonjs-sandbox/components/renderingZone" {
+    import * as React from "react";
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    import { Vector3 } from 'babylonjs/Maths/math.vector';
+    interface IRenderingZoneProps {
+        globalState: GlobalState;
+        assetUrl?: string;
+        cameraPosition?: Vector3;
+        expanded: boolean;
+    }
+    export class RenderingZone extends React.Component<IRenderingZoneProps> {
+        private _currentPluginName;
+        private _engine;
+        private _scene;
+        private _canvas;
+        constructor(props: IRenderingZoneProps);
+        initEngine(): void;
+        prepareCamera(): void;
+        handleErrors(): void;
+        prepareLighting(): void;
+        onSceneLoaded(filename: string): void;
+        loadAssetFromUrl(): void;
+        loadAsset(): void;
+        componentDidMount(): void;
+        shouldComponentUpdate(nextProps: IRenderingZoneProps): boolean;
+        render(): JSX.Element;
+    }
+}
+declare module "babylonjs-sandbox/components/footerButton" {
+    import * as React from "react";
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    interface IFooterButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        onClick: () => void;
+        icon: any;
+        label: string;
+    }
+    export class FooterButton extends React.Component<IFooterButtonProps> {
+        render(): JSX.Element | null;
+    }
+}
+declare module "babylonjs-sandbox/components/dropUpButton" {
+    import * as React from "react";
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    interface IDropUpButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        options: string[];
+        onOptionPicked: (option: string) => void;
+    }
+    export class DropUpButton extends React.Component<IDropUpButtonProps, {
+        isOpen: boolean;
+    }> {
+        private _onClickInterceptorClickedObserver;
+        constructor(props: IDropUpButtonProps);
+        componentWillUnmount(): void;
+        switchDropUp(): void;
+        clickOption(option: string): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module "babylonjs-sandbox/components/footerFileButton" {
+    import * as React from "react";
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    interface IFooterFileButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        onFilesPicked: (evt: Event, files: FileList | null) => void;
+    }
+    export class FooterFileButton extends React.Component<IFooterFileButtonProps> {
+        onFilePicked(evt: React.ChangeEvent<HTMLInputElement>): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module "babylonjs-sandbox/components/footer" {
+    import * as React from "react";
+    import { GlobalState } from "babylonjs-sandbox/globalState";
+    interface IFooterProps {
+        globalState: GlobalState;
+    }
+    export class Footer extends React.Component<IFooterProps> {
+        constructor(props: IFooterProps);
+        showInspector(): void;
+        render(): JSX.Element;
+    }
+}
+declare module "babylonjs-sandbox/sandbox" {
+    import * as React from "react";
+    interface ISandboxProps {
+    }
+    export class Sandbox extends React.Component<ISandboxProps, {
+        isFooterVisible: boolean;
+        errorMessage: string;
+    }> {
+        private _globalState;
+        private _assetUrl?;
+        private _cameraPosition?;
+        private _logoRef;
+        private _dropTextRef;
+        private _clickInterceptorRef;
+        constructor(props: ISandboxProps);
+        checkUrl(): void;
+        componentDidUpdate(): void;
+        render(): JSX.Element;
+        static Show(hostElement: HTMLElement): void;
+    }
+}
+declare module "babylonjs-sandbox/index" {
+    export * from "babylonjs-sandbox/sandbox";
+}
+declare module "babylonjs-sandbox/legacy/legacy" {
+    export * from "babylonjs-sandbox/index";
+}
+declare module "babylonjs-sandbox" {
+    export * from "babylonjs-sandbox/legacy/legacy";
+}
+/// <reference types="react" />
+declare module SANDBOX {
+    export class GlobalState {
+        currentScene: BABYLON.Scene;
+        onSceneLoaded: BABYLON.Observable<{
+            scene: BABYLON.Scene;
+            filename: string;
+        }>;
+        onError: BABYLON.Observable<{
+            scene?: BABYLON.Scene | undefined;
+            message?: string | undefined;
+        }>;
+        onEnvironmentChanged: BABYLON.Observable<string>;
+        onRequestClickInterceptor: BABYLON.Observable<void>;
+        onClickInterceptorClicked: BABYLON.Observable<void>;
+        filesInput: BABYLON.FilesInput;
+        isDebugLayerEnabled: boolean;
+        showDebugLayer(): void;
+        hideDebugLayer(): void;
+    }
+}
+declare module SANDBOX {
+    export class LocalStorageHelper {
+        static ReadLocalStorageValue(key: string, defaultValue: number): number;
+    }
+}
+declare module SANDBOX {
+    export class EnvironmentTools {
+        static SkyboxPath: string;
+        static Skyboxes: string[];
+        static SkyboxesNames: string[];
+        static LoadSkyboxPathTexture(scene: BABYLON.Scene): BABYLON.HDRCubeTexture | BABYLON.CubeTexture;
+        static HookWithEnvironmentChange(globalState: GlobalState): void;
+    }
+}
+declare module SANDBOX {
+    interface IRenderingZoneProps {
+        globalState: GlobalState;
+        assetUrl?: string;
+        cameraPosition?: BABYLON.Vector3;
+        expanded: boolean;
+    }
+    export class RenderingZone extends React.Component<IRenderingZoneProps> {
+        private _currentPluginName;
+        private _engine;
+        private _scene;
+        private _canvas;
+        constructor(props: IRenderingZoneProps);
+        initEngine(): void;
+        prepareCamera(): void;
+        handleErrors(): void;
+        prepareLighting(): void;
+        onSceneLoaded(filename: string): void;
+        loadAssetFromUrl(): void;
+        loadAsset(): void;
+        componentDidMount(): void;
+        shouldComponentUpdate(nextProps: IRenderingZoneProps): boolean;
+        render(): JSX.Element;
+    }
+}
+declare module SANDBOX {
+    interface IFooterButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        onClick: () => void;
+        icon: any;
+        label: string;
+    }
+    export class FooterButton extends React.Component<IFooterButtonProps> {
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IDropUpButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        options: string[];
+        onOptionPicked: (option: string) => void;
+    }
+    export class DropUpButton extends React.Component<IDropUpButtonProps, {
+        isOpen: boolean;
+    }> {
+        private _onClickInterceptorClickedObserver;
+        constructor(props: IDropUpButtonProps);
+        componentWillUnmount(): void;
+        switchDropUp(): void;
+        clickOption(option: string): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IFooterFileButtonProps {
+        globalState: GlobalState;
+        enabled: boolean;
+        icon: any;
+        label: string;
+        onFilesPicked: (evt: Event, files: FileList | null) => void;
+    }
+    export class FooterFileButton extends React.Component<IFooterFileButtonProps> {
+        onFilePicked(evt: React.ChangeEvent<HTMLInputElement>): void;
+        render(): JSX.Element | null;
+    }
+}
+declare module SANDBOX {
+    interface IFooterProps {
+        globalState: GlobalState;
+    }
+    export class Footer extends React.Component<IFooterProps> {
+        constructor(props: IFooterProps);
+        showInspector(): void;
+        render(): JSX.Element;
+    }
+}
+declare module SANDBOX {
+    interface ISandboxProps {
+    }
+    export class Sandbox extends React.Component<ISandboxProps, {
+        isFooterVisible: boolean;
+        errorMessage: string;
+    }> {
+        private _globalState;
+        private _assetUrl?;
+        private _cameraPosition?;
+        private _logoRef;
+        private _dropTextRef;
+        private _clickInterceptorRef;
+        constructor(props: ISandboxProps);
+        checkUrl(): void;
+        componentDidUpdate(): void;
+        render(): JSX.Element;
+        static Show(hostElement: HTMLElement): void;
+    }
+}

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

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

+ 3 - 0
dist/preview release/sandbox/readme-es6.md

@@ -0,0 +1,3 @@
+# Babylon.js Sandbox
+
+An extension to easily create a full page viewer (ala sandbox.babylonjs.com)

+ 16 - 0
dist/preview release/sandbox/readme.md

@@ -0,0 +1,16 @@
+# Babylon.js Sandbox
+
+An extension to easily create a full page viewer (ala sandbox.babylonjs.com)
+
+## Usage
+### Online method
+Call the method `Show` of the `BABYLON.Sandbox` class: 
+```
+BABYLON.Sandbox.Show({hostElement: document.getElementById("host")});
+```
+
+### Offline method
+If you don't have access to internet, the node editor should be imported manually in your HTML page :
+```
+<script src="babylon.sandbox.js" />
+``` 

File diff suppressed because it is too large
+ 19 - 10
dist/preview release/viewer/babylon.module.d.ts


File diff suppressed because it is too large
+ 10 - 10
dist/preview release/viewer/babylon.viewer.js


File diff suppressed because it is too large
+ 2 - 2
dist/preview release/viewer/babylon.viewer.max.js


+ 3 - 1
dist/preview release/what's new.md

@@ -8,7 +8,7 @@
 - Added HDR texture filtering tools to the sandbox ([Sebavan](https://github.com/sebavan/))
 - Reflection probes can now be used to give accurate shading with PBR ([CraigFeldpsar](https://github.com/craigfeldspar) and ([Sebavan](https://github.com/sebavan/)))
 - Added editing of PBR materials, Post processes and Particle fragment shaders in the node material editor ([Popov72](https://github.com/Popov72))
-- Added Curve editor to manage entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
+- Added Curve editor to manage selected entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
 - Added support in `ShadowGenerator` for fast fake soft transparent shadows ([Popov72](https://github.com/Popov72))
 - Added support for **thin instances** for faster mesh instances. [Doc](https://doc.babylonjs.com/how_to/how_to_use_thininstances) ([Popov72](https://github.com/Popov72))
 
@@ -231,6 +231,8 @@
 - Fixed `DracoCompression` to not load empty data into attributes ([bghgary](https://github.com/bghgary))
 - Fixed `Mesh.subdivide` where one face could be lost depending on the number of subdivision ([Popov72](https://github.com/Popov72))
 - Fixed `AssetContainer.instantiateModelsToScene` with cloneMaterials=true and MultiMaterials to properly set the cloned submaterials ([ghempton](https://github.com/ghempton))
+- Fixed wrong display when setting `DefaultRenderingPipeline.imageProcessingEnabled` to `false` ([Popov72](https://github.com/Popov72))
+- Fix crash when loading a .obj file with vertex colors ([Popov72](https://github.com/Popov72))
 
 ## Breaking changes
 

+ 21 - 15
inspector/src/components/actionTabs/lines/iconButtonLineComponent.tsx

@@ -1,21 +1,27 @@
-import * as React from "react";
+import * as React from 'react';
 
 export interface IIconButtonLineComponentProps {
-    icon: string;
-    onClick: () => void;
-    tooltip: string;
-    active?: boolean;
+  icon: string;
+  onClick: () => void;
+  tooltip: string;
+  active?: boolean;
 }
 
-export class IconButtonLineComponent extends React.Component<IIconButtonLineComponentProps> {
-    constructor(props: IIconButtonLineComponentProps) {
-        super(props);
-    }
+export class IconButtonLineComponent extends React.Component<
+  IIconButtonLineComponentProps
+> {
+  constructor(props: IIconButtonLineComponentProps) {
+    super(props);
+  }
 
-    render() {
-
-        return (
-            <div style={{backgroundColor: this.props.active ? '#111111' : 'transparent'}} title={this.props.tooltip} className={`icon ${this.props.icon}`} onClick={() => this.props.onClick()} />
-        );
-    }
+  render() {
+    return (
+      <div
+        style={{ backgroundColor: this.props.active ? '#111111' : '' }}
+        title={this.props.tooltip}
+        className={`icon ${this.props.icon}`}
+        onClick={() => this.props.onClick()}
+      />
+    );
+  }
 }

+ 346 - 253
inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx

@@ -1,8 +1,7 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { Observable } from "babylonjs/Misc/observable";
-import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Observable } from 'babylonjs/Misc/observable';
+import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
 import { Animation } from 'babylonjs/Animations/animation';
 import { Vector2, Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
 import { Size } from 'babylonjs/Maths/math.size';
@@ -10,277 +9,371 @@ import { Color3, Color4 } from 'babylonjs/Maths/math.color';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 
 interface IAddAnimationProps {
-   isOpen: boolean;
-   close: () => void;
-   entity: IAnimatable;
-   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-   setNotificationMessage: (message: string) => void;
-   changed: () => void;
+  isOpen: boolean;
+  close: () => void;
+  entity: IAnimatable;
+  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+  setNotificationMessage: (message: string) => void;
+  changed: () => void;
 }
 
-export class AddAnimation extends React.Component<IAddAnimationProps, {animationName: string, animationTargetProperty: string, animationType:string, loopMode: number, animationTargetPath:string}>{ 
-    constructor(props: IAddAnimationProps) {
-        super(props);
-        this.state = { animationName: "", animationTargetPath: "", animationType: "Float", loopMode: Animation.ANIMATIONLOOPMODE_CYCLE, animationTargetProperty: ""}
-    }
-
-    getAnimationTypeofChange(selected: string) {
-        let dataType = 0;
-        switch (selected) {
-            case "Float":
-                dataType = Animation.ANIMATIONTYPE_FLOAT;
-                break;
-            case "Quaternion":
-                dataType = Animation.ANIMATIONTYPE_QUATERNION;
-                break;
-            case "Vector3":
-                dataType = Animation.ANIMATIONTYPE_VECTOR3;
-                break;
-            case "Vector2":
-                dataType = Animation.ANIMATIONTYPE_VECTOR2;
-                break;
-            case "Size":
-                dataType = Animation.ANIMATIONTYPE_SIZE;
-                break;
-            case "Color3":
-                dataType = Animation.ANIMATIONTYPE_COLOR3;
-                break;
-            case "Color4":
-                dataType = Animation.ANIMATIONTYPE_COLOR4;
-                break;
-        }
-
-        return dataType;
+export class AddAnimation extends React.Component<
+  IAddAnimationProps,
+  {
+    animationName: string;
+    animationTargetProperty: string;
+    animationType: string;
+    loopMode: number;
+    animationTargetPath: string;
+  }
+> {
+  constructor(props: IAddAnimationProps) {
+    super(props);
+    this.state = {
+      animationName: '',
+      animationTargetPath: '',
+      animationType: 'Float',
+      loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
+      animationTargetProperty: '',
+    };
+  }
 
+  getAnimationTypeofChange(selected: string) {
+    let dataType = 0;
+    switch (selected) {
+      case 'Float':
+        dataType = Animation.ANIMATIONTYPE_FLOAT;
+        break;
+      case 'Quaternion':
+        dataType = Animation.ANIMATIONTYPE_QUATERNION;
+        break;
+      case 'Vector3':
+        dataType = Animation.ANIMATIONTYPE_VECTOR3;
+        break;
+      case 'Vector2':
+        dataType = Animation.ANIMATIONTYPE_VECTOR2;
+        break;
+      case 'Size':
+        dataType = Animation.ANIMATIONTYPE_SIZE;
+        break;
+      case 'Color3':
+        dataType = Animation.ANIMATIONTYPE_COLOR3;
+        break;
+      case 'Color4':
+        dataType = Animation.ANIMATIONTYPE_COLOR4;
+        break;
     }
 
-    addAnimation() {
-        if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
-
-            let matchTypeTargetProperty = this.state.animationTargetProperty.split('.');
-            let animationDataType = this.getAnimationTypeofChange(this.state.animationType);
-            let matched = false;
-
-            if (matchTypeTargetProperty.length === 1) {
-                let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
+    return dataType;
+  }
 
-                if (match) {
-                    switch (match.constructor.name) {
-                        case "Vector2":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR2 ? matched = true : matched = false;
-                            break;
-                        case "Vector3":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR3 ? matched = true : matched = false;
-                            break;
-                        case "Quaternion":
-                            animationDataType === Animation.ANIMATIONTYPE_QUATERNION ? matched = true : matched = false;
-                            break;
-                        case "Color3":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR3 ? matched = true : matched = false;
-                            break;
-                        case "Color4":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR4 ? matched = true : matched = false;
-                            break;
-                        case "Size":
-                            animationDataType === Animation.ANIMATIONTYPE_SIZE ? matched = true : matched = false;
-                            break;
-                        default: console.log("not recognized");
-                            break;
-                    }
-                } else {
-                   this.props.setNotificationMessage(`The selected entity doesn't have a ${matchTypeTargetProperty[0]} property`)
-                }
-            } else if (matchTypeTargetProperty.length > 1) {
-                let match = (this.props.entity as any)[matchTypeTargetProperty[0]][matchTypeTargetProperty[1]];
-                if (typeof match === "number") {
-                    animationDataType === Animation.ANIMATIONTYPE_FLOAT ? matched = true : matched = false;
-                }
-            }
+  addAnimation() {
+    if (
+      this.state.animationName != '' &&
+      this.state.animationTargetProperty != ''
+    ) {
+      let matchTypeTargetProperty = this.state.animationTargetProperty.split(
+        '.'
+      );
+      let animationDataType = this.getAnimationTypeofChange(
+        this.state.animationType
+      );
+      let matched = false;
 
-            if (matched) {
+      if (matchTypeTargetProperty.length === 1) {
+        let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
 
-                let startValue;
-                let endValue;
-                let outTangent;
-                let inTangent;
-                // Default start and end values for new animations
-                switch (animationDataType) {
-                    case Animation.ANIMATIONTYPE_FLOAT:
-                        startValue = 1;
-                        endValue = 1;
-                        outTangent = 0;
-                        inTangent = 0;
-                        break;
-                    case Animation.ANIMATIONTYPE_VECTOR2:
-                        startValue = new Vector2(1, 1);
-                        endValue = new Vector2(1, 1);
-                        outTangent = Vector2.Zero();
-                        inTangent = Vector2.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_VECTOR3:
-                        startValue = new Vector3(1, 1, 1);
-                        endValue = new Vector3(1, 1, 1);
-                        outTangent = Vector3.Zero();
-                        inTangent = Vector3.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_QUATERNION:
-                        startValue = new Quaternion(1, 1, 1, 1);
-                        endValue = new Quaternion(1, 1, 1, 1);
-                        outTangent = Quaternion.Zero();
-                        inTangent = Quaternion.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_COLOR3:
-                        startValue = new Color3(1, 1, 1);
-                        endValue = new Color3(1, 1, 1);
-                        outTangent = new Color3(0, 0, 0);
-                        inTangent = new Color3(0, 0, 0);
-                        break;
-                    case Animation.ANIMATIONTYPE_COLOR4:
-                        startValue = new Color4(1, 1, 1, 1);
-                        endValue = new Color4(1, 1, 1, 1);
-                        outTangent = new Color4(0, 0, 0, 0);
-                        inTangent = new Color4(0, 0, 0, 0);
-                        break;
-                    case Animation.ANIMATIONTYPE_SIZE:
-                        startValue = new Size(1, 1);
-                        endValue = new Size(1, 1);
-                        outTangent = Size.Zero();
-                        inTangent = Size.Zero();
-                        break;
-                    default: console.log("not recognized");
-                        break;
-                }
+        if (match) {
+          switch (match.constructor.name) {
+            case 'Vector2':
+              animationDataType === Animation.ANIMATIONTYPE_VECTOR2
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Vector3':
+              animationDataType === Animation.ANIMATIONTYPE_VECTOR3
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Quaternion':
+              animationDataType === Animation.ANIMATIONTYPE_QUATERNION
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Color3':
+              animationDataType === Animation.ANIMATIONTYPE_COLOR3
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Color4':
+              animationDataType === Animation.ANIMATIONTYPE_COLOR4
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Size':
+              animationDataType === Animation.ANIMATIONTYPE_SIZE
+                ? (matched = true)
+                : (matched = false);
+              break;
+            default:
+              console.log('not recognized');
+              break;
+          }
+        } else {
+          this.props.setNotificationMessage(
+            `The selected entity doesn't have a ${matchTypeTargetProperty[0]} property`
+          );
+        }
+      } else if (matchTypeTargetProperty.length > 1) {
+        let matchProp = (this.props.entity as any)[matchTypeTargetProperty[0]];
+        if (matchProp) {
+          let match = matchProp[matchTypeTargetProperty[1]];
+          if (typeof match === 'number') {
+            animationDataType === Animation.ANIMATIONTYPE_FLOAT
+              ? (matched = true)
+              : (matched = false);
+          }
+        }
+      }
 
-                let alreadyAnimatedProperty = (this.props.entity as IAnimatable).animations?.find(anim =>
-                    anim.targetProperty === this.state.animationTargetProperty
-                    , this);
+      if (matched) {
+        let startValue;
+        let endValue;
+        let outTangent;
+        let inTangent;
+        // Default start and end values for new animations
+        switch (animationDataType) {
+          case Animation.ANIMATIONTYPE_FLOAT:
+            startValue = 1;
+            endValue = 1;
+            outTangent = 0;
+            inTangent = 0;
+            break;
+          case Animation.ANIMATIONTYPE_VECTOR2:
+            startValue = new Vector2(1, 1);
+            endValue = new Vector2(1, 1);
+            outTangent = Vector2.Zero();
+            inTangent = Vector2.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_VECTOR3:
+            startValue = new Vector3(1, 1, 1);
+            endValue = new Vector3(1, 1, 1);
+            outTangent = Vector3.Zero();
+            inTangent = Vector3.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_QUATERNION:
+            startValue = new Quaternion(1, 1, 1, 1);
+            endValue = new Quaternion(1, 1, 1, 1);
+            outTangent = Quaternion.Zero();
+            inTangent = Quaternion.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_COLOR3:
+            startValue = new Color3(1, 1, 1);
+            endValue = new Color3(1, 1, 1);
+            outTangent = new Color3(0, 0, 0);
+            inTangent = new Color3(0, 0, 0);
+            break;
+          case Animation.ANIMATIONTYPE_COLOR4:
+            startValue = new Color4(1, 1, 1, 1);
+            endValue = new Color4(1, 1, 1, 1);
+            outTangent = new Color4(0, 0, 0, 0);
+            inTangent = new Color4(0, 0, 0, 0);
+            break;
+          case Animation.ANIMATIONTYPE_SIZE:
+            startValue = new Size(1, 1);
+            endValue = new Size(1, 1);
+            outTangent = Size.Zero();
+            inTangent = Size.Zero();
+            break;
+          default:
+            console.log('not recognized');
+            break;
+        }
 
-                let alreadyAnimationName = (this.props.entity as IAnimatable).animations?.find(anim =>
-                    anim.name === this.state.animationName
-                    , this);
+        let alreadyAnimatedProperty = (this.props
+          .entity as IAnimatable).animations?.find(
+          (anim) => anim.targetProperty === this.state.animationTargetProperty,
+          this
+        );
 
-                if (alreadyAnimatedProperty) {
-                    this.props.setNotificationMessage(`The property "${this.state.animationTargetProperty}" already has an animation`);
-                } else if (alreadyAnimationName) {
-                    this.props.setNotificationMessage(`There is already an animation with the name: "${this.state.animationName}"`);
-                } else {
+        let alreadyAnimationName = (this.props
+          .entity as IAnimatable).animations?.find(
+          (anim) => anim.name === this.state.animationName,
+          this
+        );
 
-                    let animation = new Animation(this.state.animationName, this.state.animationTargetProperty, 30, animationDataType);
+        if (alreadyAnimatedProperty) {
+          this.props.setNotificationMessage(
+            `The property "${this.state.animationTargetProperty}" already has an animation`
+          );
+        } else if (alreadyAnimationName) {
+          this.props.setNotificationMessage(
+            `There is already an animation with the name: "${this.state.animationName}"`
+          );
+        } else {
+          let animation = new Animation(
+            this.state.animationName,
+            this.state.animationTargetProperty,
+            30,
+            animationDataType
+          );
 
-                    // Start with two keyframes
-                    var keys = [];
-                    keys.push({
-                        frame: 0,
-                        value: startValue,
-                        outTangent: outTangent
-                    });
+          // Start with two keyframes
+          var keys = [];
+          keys.push({
+            frame: 0,
+            value: startValue,
+            outTangent: outTangent,
+          });
 
-                    keys.push({
-                        inTangent: inTangent,
-                        frame: 100,
-                        value: endValue
-                    });
+          keys.push({
+            inTangent: inTangent,
+            frame: 100,
+            value: endValue,
+          });
 
-                    animation.setKeys(keys);
+          animation.setKeys(keys);
 
-                    if (this.props.entity.animations){
-                        const store = this.props.entity.animations;
-                        const updatedCollection = [...this.props.entity.animations, animation]
-                        this.raiseOnPropertyChanged(updatedCollection, store);
-                        this.props.entity.animations = updatedCollection;
-                        this.props.changed();
-                        this.props.close();
-                        //Cleaning form fields
-                        this.setState({ animationName: "", animationTargetPath: "", animationType: "Float", loopMode: Animation.ANIMATIONLOOPMODE_CYCLE, animationTargetProperty: ""});
-                    }   
-                }
-            } else {
-                this.props.setNotificationMessage(`The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type`);
-            }
-        } else {
-            this.props.setNotificationMessage(`You need to provide a name and target property.`);
+          if (this.props.entity.animations) {
+            const store = this.props.entity.animations;
+            const updatedCollection = [
+              ...this.props.entity.animations,
+              animation,
+            ];
+            this.raiseOnPropertyChanged(updatedCollection, store);
+            this.props.entity.animations = updatedCollection;
+            this.props.changed();
+            this.props.close();
+            //Cleaning form fields
+            this.setState({
+              animationName: '',
+              animationTargetPath: '',
+              animationType: 'Float',
+              loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
+              animationTargetProperty: '',
+            });
+          }
         }
+      } else {
+        this.props.setNotificationMessage(
+          `The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type`
+        );
+      }
+    } else {
+      this.props.setNotificationMessage(
+        `You need to provide a name and target property.`
+      );
     }
+  }
 
-    raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
-        if (!this.props.onPropertyChangedObservable) {
-            return;
-        }
-
-        this.props.onPropertyChangedObservable.notifyObservers({
-            object: this.props.entity,
-            property: 'animations',
-            value: newValue,
-            initialValue: previousValue
-        });
+  raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
+    if (!this.props.onPropertyChangedObservable) {
+      return;
     }
 
-    handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationName: event.target.value.trim() });
-    }
-    
-    handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationTargetPath: event.target.value.trim() });
-    }
+    this.props.onPropertyChangedObservable.notifyObservers({
+      object: this.props.entity,
+      property: 'animations',
+      value: newValue,
+      initialValue: previousValue,
+    });
+  }
 
-    handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
-        event.preventDefault();
-        this.setState({ animationType: event.target.value });
-    }
+  handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationName: event.target.value.trim() });
+  }
 
-    handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationTargetProperty: event.target.value });
-    }
+  handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationTargetPath: event.target.value.trim() });
+  }
 
-    handleLoopModeChange(event: React.ChangeEvent<HTMLSelectElement>) {
-        event.preventDefault();
-        this.setState({ loopMode: parseInt(event.target.value) });
-    }
-     
-    render() { 
-       return (
-        <div className="new-animation" style={{ display: this.props.isOpen ? "block" : "none" }}>
-            <div className="sub-content">
-            <div className="label-input">
-                <label>Target Path</label>
-                <input type="text" value={this.state.animationTargetPath} onChange={(e) => this.handlePathChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Display Name</label>
-                <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Property</label>
-                <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Type</label>
-                <select onChange={(e) => this.handleTypeChange(e)} value={this.state.animationType}>
-                    <option value="Float">Float</option>
-                    <option value="Vector3">Vector3</option>
-                    <option value="Vector2">Vector2</option>
-                    <option value="Quaternion">Quaternion</option>
-                    <option value="Color3">Color3</option>
-                    <option value="Color4">Color4</option>
-                    <option value="Size">Size</option>
-                </select>
-            </div>
-            <div className="label-input">
-                <label>Loop Mode</label>
-                <select onChange={(e) => this.handleLoopModeChange(e)} value={this.state.loopMode}>
-                    <option value={Animation.ANIMATIONLOOPMODE_CYCLE}>Cycle</option>
-                    <option value={Animation.ANIMATIONLOOPMODE_RELATIVE}>Relative</option>
-                    <option value={Animation.ANIMATIONLOOPMODE_CONSTANT}>Constant</option>
-                </select>
-            </div>
-           <div className="confirm-buttons">
-            <ButtonLineComponent label={"Create"} onClick={() => this.addAnimation()} />
-            </div>
-            </div>
+  handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    event.preventDefault();
+    this.setState({ animationType: event.target.value });
+  }
+
+  handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationTargetProperty: event.target.value });
+  }
+
+  handleLoopModeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    event.preventDefault();
+    this.setState({ loopMode: parseInt(event.target.value) });
+  }
+
+  render() {
+    return (
+      <div
+        className='new-animation'
+        style={{ display: this.props.isOpen ? 'block' : 'none' }}
+      >
+        <div className='sub-content'>
+          <div className='label-input'>
+            <label>Target Path</label>
+            <input
+              type='text'
+              value={this.state.animationTargetPath}
+              onChange={(e) => this.handlePathChange(e)}
+              disabled
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Display Name</label>
+            <input
+              type='text'
+              value={this.state.animationName}
+              onChange={(e) => this.handleNameChange(e)}
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Property</label>
+            <input
+              type='text'
+              value={this.state.animationTargetProperty}
+              onChange={(e) => this.handlePropertyChange(e)}
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Type</label>
+            <select
+              onChange={(e) => this.handleTypeChange(e)}
+              value={this.state.animationType}
+            >
+              <option value='Float'>Float</option>
+              <option value='Vector3'>Vector3</option>
+              <option value='Vector2'>Vector2</option>
+              <option value='Quaternion'>Quaternion</option>
+              <option value='Color3'>Color3</option>
+              <option value='Color4'>Color4</option>
+              <option value='Size'>Size</option>
+            </select>
+          </div>
+          <div className='label-input'>
+            <label>Loop Mode</label>
+            <select
+              onChange={(e) => this.handleLoopModeChange(e)}
+              value={this.state.loopMode}
+            >
+              <option value={Animation.ANIMATIONLOOPMODE_CYCLE}>Cycle</option>
+              <option value={Animation.ANIMATIONLOOPMODE_RELATIVE}>
+                Relative
+              </option>
+              <option value={Animation.ANIMATIONLOOPMODE_CONSTANT}>
+                Constant
+              </option>
+            </select>
+          </div>
+          <div className='confirm-buttons'>
+            <ButtonLineComponent
+              label={'Create'}
+              onClick={() => this.addAnimation()}
+            />
+          </div>
         </div>
-        )
-    }
-} 
+      </div>
+    );
+  }
+}

File diff suppressed because it is too large
+ 1380 - 1033
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx


+ 300 - 209
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx

@@ -1,13 +1,13 @@
-import * as React from "react";
+import * as React from 'react';
 
-import { Observable, Observer } from "babylonjs/Misc/observable";
-import { Scene } from "babylonjs/scene";
+import { Observable, Observer } from 'babylonjs/Misc/observable';
+import { Scene } from 'babylonjs/scene';
 
-import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
-import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
-import { LineContainerComponent } from "../../../lineContainerComponent";
-import { SliderLineComponent } from "../../../lines/sliderLineComponent";
-import { LockObject } from "../lockObject";
+import { PropertyChangedEvent } from '../../../../propertyChangedEvent';
+import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { LineContainerComponent } from '../../../lineContainerComponent';
+import { SliderLineComponent } from '../../../lines/sliderLineComponent';
+import { LockObject } from '../lockObject';
 import { GlobalState } from '../../../../globalState';
 import { Animation } from 'babylonjs/Animations/animation';
 import { Animatable } from 'babylonjs/Animations/animatable';
@@ -22,238 +22,329 @@ import { AnimationCurveEditorComponent } from '../animations/animationCurveEdito
 import { PopupComponent } from '../animations/popupComponent';
 
 interface IAnimationGridComponentProps {
-    globalState: GlobalState;
-    animatable: IAnimatable,
-    scene: Scene,
-    lockObject: LockObject,
-    onPropertyChangedObservable?: Observable<PropertyChangedEvent>
+  globalState: GlobalState;
+  animatable: IAnimatable;
+  scene: Scene;
+  lockObject: LockObject;
+  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
 }
 
-export class AnimationGridComponent extends React.Component<IAnimationGridComponentProps, { currentFrame: number }> {
-    private _animations: Nullable<Animation[]> = null;
-    private _ranges: AnimationRange[];
-    private _mainAnimatable: Nullable<Animatable>;
-    private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
-    private _isPlaying = false;
-    private timelineRef: React.RefObject<SliderLineComponent>;
-    private _isCurveEditorOpen = false;
-    private _animationControl = {
-        from: 0,
-        to: 0,
-        loop: false
-    }
-
-    constructor(props: IAnimationGridComponentProps) {
-        super(props);
-
-        this.state = { currentFrame: 0 };
-
-        const animatableAsAny = this.props.animatable as any;
+export class AnimationGridComponent extends React.Component<
+  IAnimationGridComponentProps,
+  { currentFrame: number }
+> {
+  private _animations: Nullable<Animation[]> = null;
+  private _ranges: AnimationRange[];
+  private _mainAnimatable: Nullable<Animatable>;
+  private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
+  private _isPlaying = false;
+  private timelineRef: React.RefObject<SliderLineComponent>;
+  private _isCurveEditorOpen = false;
+  private _animationControl = {
+    from: 0,
+    to: 0,
+    loop: false,
+  };
 
-        this._ranges = animatableAsAny.getAnimationRanges ? animatableAsAny.getAnimationRanges() : [];
-        if (animatableAsAny.getAnimatables) {
-            const animatables = animatableAsAny.getAnimatables();
-            this._animations = new Array<Animation>();
+  constructor(props: IAnimationGridComponentProps) {
+    super(props);
 
-            animatables.forEach((animatable: IAnimatable) => {
-                if (animatable.animations) {
-                    this._animations!.push(...animatable.animations);
-                }
-            });
+    this.state = { currentFrame: 0 };
 
-            if (animatableAsAny.animations) {
-                this._animations!.push(...animatableAsAny.animations);
-            }
+    const animatableAsAny = this.props.animatable as any;
 
-            // Extract from and to
-            if (this._animations && this._animations.length) {
-                this._animations.forEach(animation => {
-                    let keys = animation.getKeys();
+    this._ranges = animatableAsAny.getAnimationRanges
+      ? animatableAsAny.getAnimationRanges()
+      : [];
+    if (animatableAsAny.getAnimatables) {
+      const animatables = animatableAsAny.getAnimatables();
+      this._animations = new Array<Animation>();
 
-                    if (keys && keys.length > 0) {
-                        if (keys[0].frame < this._animationControl.from) {
-                            this._animationControl.from = keys[0].frame;
-                        }
-                        const lastKeyIndex = keys.length - 1;
-                        if (keys[lastKeyIndex].frame > this._animationControl.to) {
-                            this._animationControl.to = keys[lastKeyIndex].frame;
-                        }
-                    }
-                });
-            }
+      animatables.forEach((animatable: IAnimatable) => {
+        if (animatable.animations) {
+          this._animations!.push(...animatable.animations);
         }
+      });
 
-        this.timelineRef = React.createRef();
-    }
+      if (animatableAsAny.animations) {
+        this._animations!.push(...animatableAsAny.animations);
+      }
 
-    playOrPause() {
-        const animatable = this.props.animatable;
-        this._isPlaying = this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
+      // Extract from and to
+      if (this._animations && this._animations.length) {
+        this._animations.forEach((animation) => {
+          let keys = animation.getKeys();
 
-        if (this._isPlaying) {
-            this.props.scene.stopAnimation(this.props.animatable);
-            this._mainAnimatable = null;
-        } else {
-            this._mainAnimatable = this.props.scene.beginAnimation(this.props.animatable, this._animationControl.from, this._animationControl.to, this._animationControl.loop);
-        }
-        this.forceUpdate();
-    }
-
-    componentDidMount() {
-        this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(() => {
-            if (!this._isPlaying || !this._mainAnimatable) {
-                return;
+          if (keys && keys.length > 0) {
+            if (keys[0].frame < this._animationControl.from) {
+              this._animationControl.from = keys[0].frame;
+            }
+            const lastKeyIndex = keys.length - 1;
+            if (keys[lastKeyIndex].frame > this._animationControl.to) {
+              this._animationControl.to = keys[lastKeyIndex].frame;
             }
-            this.setState({ currentFrame: this._mainAnimatable.masterFrame });
+          }
         });
+      }
     }
 
-    componentWillUnmount() {
-        if (this._onBeforeRenderObserver) {
-            this.props.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver);
-            this._onBeforeRenderObserver = null;
-        }
-    }
+    this.timelineRef = React.createRef();
+  }
 
-    onCurrentFrameChange(value: number) {
-        if (!this._mainAnimatable) {
-            return;
-        }
+  playOrPause() {
+    const animatable = this.props.animatable;
+    this._isPlaying =
+      this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
 
-        this._mainAnimatable.goToFrame(value);
-        this.setState({ currentFrame: value });
+    if (this._isPlaying) {
+      this.props.scene.stopAnimation(this.props.animatable);
+      this._mainAnimatable = null;
+    } else {
+      this._mainAnimatable = this.props.scene.beginAnimation(
+        this.props.animatable,
+        this._animationControl.from,
+        this._animationControl.to,
+        this._animationControl.loop
+      );
     }
+    this.forceUpdate();
+  }
 
-    onChangeFromOrTo() {
-        this.playOrPause();
-        if (this._isPlaying) {
-            this.playOrPause();
+  componentDidMount() {
+    this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(
+      () => {
+        if (!this._isPlaying || !this._mainAnimatable) {
+          return;
         }
+        this.setState({ currentFrame: this._mainAnimatable.masterFrame });
+      }
+    );
+  }
+
+  componentWillUnmount() {
+    if (this._onBeforeRenderObserver) {
+      this.props.scene.onBeforeRenderObservable.remove(
+        this._onBeforeRenderObserver
+      );
+      this._onBeforeRenderObserver = null;
     }
+  }
 
-    onOpenAnimationCurveEditor() {
-        this._isCurveEditorOpen = true;
+  onCurrentFrameChange(value: number) {
+    if (!this._mainAnimatable) {
+      return;
     }
 
-    onCloseAnimationCurveEditor(window: Window | null) {
-        this._isCurveEditorOpen = false;
-        if (window === null) {
-            console.log("Window already closed");
-        } else {
-            window.close();
-        }
+    this._mainAnimatable.goToFrame(value);
+    this.setState({ currentFrame: value });
+  }
+
+  onChangeFromOrTo() {
+    this.playOrPause();
+    if (this._isPlaying) {
+      this.playOrPause();
     }
+  }
 
-    render() {
-        const animatable = this.props.animatable;
-        const animatableAsAny = this.props.animatable as any;
+  onOpenAnimationCurveEditor() {
+    this._isCurveEditorOpen = true;
+  }
 
-        let animatablesForTarget = this.props.scene.getAllAnimatablesByTarget(animatable);
-        this._isPlaying = animatablesForTarget.length > 0;
+  onCloseAnimationCurveEditor(window: Window | null) {
+    this._isCurveEditorOpen = false;
+    if (window === null) {
+      console.log('Window already closed');
+    } else {
+      window.close();
+    }
+  }
 
-        if (this._isPlaying && !this._mainAnimatable) {
-            this._mainAnimatable = animatablesForTarget[0];
-            if (this._mainAnimatable) {
-                this._animationControl.from = this._mainAnimatable.fromFrame;
-                this._animationControl.to = this._mainAnimatable.toFrame;
-                this._animationControl.loop = this._mainAnimatable.loopAnimation;
-            }
-        }
+  render() {
+    const animatable = this.props.animatable;
+    const animatableAsAny = this.props.animatable as any;
 
-        let animations = animatable.animations;
+    let animatablesForTarget = this.props.scene.getAllAnimatablesByTarget(
+      animatable
+    );
+    this._isPlaying = animatablesForTarget.length > 0;
 
-        return (
-            <div>
-                {
-                    this._ranges.length > 0 &&
-                    <LineContainerComponent globalState={this.props.globalState} title="ANIMATION RANGES">
-                        {
-                            this._ranges.map((range, i) => {
-                                return (
-                                    <ButtonLineComponent key={range.name + i} label={range.name}
-                                        onClick={() => {
-                                            this._mainAnimatable = null;
-                                            this.props.scene.beginAnimation(animatable, range.from, range.to, true)
-                                        }} />
-                                );
-                            })
-                        }
-                    </LineContainerComponent>
-                }
-                {
-                    animations &&
-                    <>
-                        <LineContainerComponent globalState={this.props.globalState} title="ANIMATIONS">
-                            <TextLineComponent label="Count" value={animations.length.toString()} />
-                            <ButtonLineComponent label="Edit" onClick={() => this.onOpenAnimationCurveEditor()} />
-                            {
-                                animations.map((anim, i) => {
-                                    return (
-                                        <TextLineComponent key={anim.targetProperty + i} label={"#" + i + " >"} value={anim.targetProperty} />
-                                    )
-                                })
-                            }
+    if (this._isPlaying && !this._mainAnimatable) {
+      this._mainAnimatable = animatablesForTarget[0];
+      if (this._mainAnimatable) {
+        this._animationControl.from = this._mainAnimatable.fromFrame;
+        this._animationControl.to = this._mainAnimatable.toFrame;
+        this._animationControl.loop = this._mainAnimatable.loopAnimation;
+      }
+    }
 
-                            {
+    let animations = animatable.animations;
 
-                                this._isCurveEditorOpen && <PopupComponent
-                                    id="curve-editor"
-                                    title="Curve Animation Editor"
-                                    size={{ width: 1024, height: 490 }}
-                                    onOpen={(window: Window) => { }}
-                                    onClose={(window: Window) => this.onCloseAnimationCurveEditor(window)}>
+    return (
+      <div>
+        {this._ranges.length > 0 && (
+          <LineContainerComponent
+            globalState={this.props.globalState}
+            title='ANIMATION RANGES'
+          >
+            {this._ranges.map((range, i) => {
+              return (
+                <ButtonLineComponent
+                  key={range.name + i}
+                  label={range.name}
+                  onClick={() => {
+                    this._mainAnimatable = null;
+                    this.props.scene.beginAnimation(
+                      animatable,
+                      range.from,
+                      range.to,
+                      true
+                    );
+                  }}
+                />
+              );
+            })}
+          </LineContainerComponent>
+        )}
+        {animations && (
+          <>
+            <LineContainerComponent
+              globalState={this.props.globalState}
+              title='ANIMATIONS'
+            >
+              <TextLineComponent
+                label='Count'
+                value={animations.length.toString()}
+              />
+              <ButtonLineComponent
+                label='Edit'
+                onClick={() => this.onOpenAnimationCurveEditor()}
+              />
+              {animations.map((anim, i) => {
+                return (
+                  <TextLineComponent
+                    key={anim.targetProperty + i}
+                    label={'#' + i + ' >'}
+                    value={anim.targetProperty}
+                  />
+                );
+              })}
 
-                                    <AnimationCurveEditorComponent
-                                        scene={this.props.scene}
-                                        entity={animatableAsAny}
-                                        close={(event) => this.onCloseAnimationCurveEditor(event.view)}
-                                        lockObject={this.props.lockObject}
-                                        playOrPause={() => this.playOrPause()} />
-                                </PopupComponent>
-                            }
-                        </LineContainerComponent>
-                        {
-                            animations.length > 0 &&
-                            <LineContainerComponent globalState={this.props.globalState} title="ANIMATION GENERAL CONTROL">
-                                <FloatLineComponent lockObject={this.props.lockObject} isInteger={true} label="From" target={this._animationControl} propertyName="from" onChange={() => this.onChangeFromOrTo()} />
-                                <FloatLineComponent lockObject={this.props.lockObject} isInteger={true} label="To" target={this._animationControl} propertyName="to" onChange={() => this.onChangeFromOrTo()} />
-                                <CheckBoxLineComponent label="Loop" onSelect={value => this._animationControl.loop = value} isSelected={() => this._animationControl.loop} />
-                                {
-                                    this._isPlaying &&
-                                    <SliderLineComponent ref={this.timelineRef} label="Current frame" minimum={this._animationControl.from} maximum={this._animationControl.to}
-                                        step={(this._animationControl.to - this._animationControl.from) / 1000.0} directValue={this.state.currentFrame}
-                                        onInput={value => this.onCurrentFrameChange(value)}
-                                    />
-                                }
-                                <ButtonLineComponent label={this._isPlaying ? "Stop" : "Play"} onClick={() => this.playOrPause()} />
-                                {
-                                    (this._ranges.length > 0 || this._animations && this._animations.length > 0) &&
-                                    <>
-                                        <CheckBoxLineComponent label="Enable override" onSelect={value => {
-                                            if (value) {
-                                                animatableAsAny.animationPropertiesOverride = new AnimationPropertiesOverride();
-                                                animatableAsAny.animationPropertiesOverride.blendingSpeed = 0.05;
-                                            } else {
-                                                animatableAsAny.animationPropertiesOverride = null;
-                                            }
-                                            this.forceUpdate();
-                                        }} isSelected={() => animatableAsAny.animationPropertiesOverride != null}
-                                            onValueChanged={() => this.forceUpdate()}
-                                        />
-                                        {
-                                            animatableAsAny.animationPropertiesOverride != null &&
-                                            <div>
-                                                <CheckBoxLineComponent label="Enable blending" target={animatableAsAny.animationPropertiesOverride} propertyName="enableBlending" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                                                <SliderLineComponent label="Blending speed" target={animatableAsAny.animationPropertiesOverride} propertyName="blendingSpeed" minimum={0} maximum={0.1} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                                            </div>
-                                        }
-                                    </>
-                                }
-                            </LineContainerComponent>
+              {this._isCurveEditorOpen && (
+                <PopupComponent
+                  id='curve-editor'
+                  title='Curve Animation Editor'
+                  size={{ width: 1024, height: 490 }}
+                  onOpen={(window: Window) => {}}
+                  onClose={(window: Window) =>
+                    this.onCloseAnimationCurveEditor(window)
+                  }
+                >
+                  <AnimationCurveEditorComponent
+                    scene={this.props.scene}
+                    entity={animatableAsAny}
+                    close={(event) =>
+                      this.onCloseAnimationCurveEditor(event.view)
+                    }
+                    lockObject={this.props.lockObject}
+                    playOrPause={() => this.playOrPause()}
+                  />
+                </PopupComponent>
+              )}
+            </LineContainerComponent>
+            {animations.length > 0 && (
+              <LineContainerComponent
+                globalState={this.props.globalState}
+                title='ANIMATION GENERAL CONTROL'
+              >
+                <FloatLineComponent
+                  lockObject={this.props.lockObject}
+                  isInteger={true}
+                  label='From'
+                  target={this._animationControl}
+                  propertyName='from'
+                  onChange={() => this.onChangeFromOrTo()}
+                />
+                <FloatLineComponent
+                  lockObject={this.props.lockObject}
+                  isInteger={true}
+                  label='To'
+                  target={this._animationControl}
+                  propertyName='to'
+                  onChange={() => this.onChangeFromOrTo()}
+                />
+                <CheckBoxLineComponent
+                  label='Loop'
+                  onSelect={(value) => (this._animationControl.loop = value)}
+                  isSelected={() => this._animationControl.loop}
+                />
+                {this._isPlaying && (
+                  <SliderLineComponent
+                    ref={this.timelineRef}
+                    label='Current frame'
+                    minimum={this._animationControl.from}
+                    maximum={this._animationControl.to}
+                    step={
+                      (this._animationControl.to -
+                        this._animationControl.from) /
+                      1000.0
+                    }
+                    directValue={this.state.currentFrame}
+                    onInput={(value) => this.onCurrentFrameChange(value)}
+                  />
+                )}
+                <ButtonLineComponent
+                  label={this._isPlaying ? 'Stop' : 'Play'}
+                  onClick={() => this.playOrPause()}
+                />
+                {(this._ranges.length > 0 ||
+                  (this._animations && this._animations.length > 0)) && (
+                  <>
+                    <CheckBoxLineComponent
+                      label='Enable override'
+                      onSelect={(value) => {
+                        if (value) {
+                          animatableAsAny.animationPropertiesOverride = new AnimationPropertiesOverride();
+                          animatableAsAny.animationPropertiesOverride.blendingSpeed = 0.05;
+                        } else {
+                          animatableAsAny.animationPropertiesOverride = null;
                         }
-                    </>
-                }
-            </div>
-        );
-    }
-}
+                        this.forceUpdate();
+                      }}
+                      isSelected={() =>
+                        animatableAsAny.animationPropertiesOverride != null
+                      }
+                      onValueChanged={() => this.forceUpdate()}
+                    />
+                    {animatableAsAny.animationPropertiesOverride != null && (
+                      <div>
+                        <CheckBoxLineComponent
+                          label='Enable blending'
+                          target={animatableAsAny.animationPropertiesOverride}
+                          propertyName='enableBlending'
+                          onPropertyChangedObservable={
+                            this.props.onPropertyChangedObservable
+                          }
+                        />
+                        <SliderLineComponent
+                          label='Blending speed'
+                          target={animatableAsAny.animationPropertiesOverride}
+                          propertyName='blendingSpeed'
+                          minimum={0}
+                          maximum={0.1}
+                          step={0.01}
+                          onPropertyChangedObservable={
+                            this.props.onPropertyChangedObservable
+                          }
+                        />
+                      </div>
+                    )}
+                  </>
+                )}
+              </LineContainerComponent>
+            )}
+          </>
+        )}
+      </div>
+    );
+  }
+}

+ 132 - 55
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss

@@ -1,3 +1,7 @@
+body {
+  background-color: rgb(51, 51, 51);
+}
+
 #animation-curve-editor {
   font-family: acumin-pro-condensed;
 
@@ -17,14 +21,14 @@
       height: 20px;
     }
     &.babylon-logo {
-      background-image: url("./assets/babylonLogo.svg");
+      background-image: url('./assets/babylonLogo.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
     }
 
     &.close {
-      background-image: url("./assets/closeWindowIcon.svg");
+      background-image: url('./assets/closeWindowIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -33,61 +37,103 @@
     }
 
     &.auto-tangent {
-      background-image: url("./assets/autoTangentIcon.svg");
+      background-image: url('./assets/autoTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.break-tangent {
-      background-image: url("./assets/breakTangentIcon.svg");
+      background-image: url('./assets/breakTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.flat-tangent {
-      background-image: url("./assets/flatTangentIcon.svg");
+      background-image: url('./assets/flatTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.frame {
-      background-image: url("./assets/frameIcon.svg");
+      background-image: url('./assets/frameIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.linear-tangent {
-      background-image: url("./assets/linearTangentIcon.svg");
+      background-image: url('./assets/linearTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.unify-tangent {
-      background-image: url("./assets/unifyTangentIcon.svg");
+      background-image: url('./assets/unifyTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.add-animation {
-      background-image: url("./assets/addAnimationIcon.svg");
+      background-image: url('./assets/addAnimationIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -96,7 +142,7 @@
     }
 
     &.animation-bullet {
-      background-image: url("./assets/animationBulletIcon.svg");
+      background-image: url('./assets/animationBulletIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -104,7 +150,7 @@
     }
 
     &.animation-delete {
-      background-image: url("./assets/animationDeleteIcon.svg");
+      background-image: url('./assets/animationDeleteIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -113,7 +159,7 @@
     }
 
     &.animation-edit {
-      background-image: url("./assets/editIcon.svg");
+      background-image: url('./assets/editIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -121,7 +167,7 @@
     }
 
     &.animation-end {
-      background-image: url("./assets/animationEndIcon.svg");
+      background-image: url('./assets/animationEndIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -132,7 +178,7 @@
     }
 
     &.animation-lastkey {
-      background-image: url("./assets/animationLastKeyIcon.svg");
+      background-image: url('./assets/animationLastKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -140,12 +186,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationLastKeyHoverIcon.svg");
+        background-image: url('./assets/animationLastKeyHoverIcon.svg');
       }
     }
 
     &.animation-nextkey {
-      background-image: url("./assets/animationNextKeyIcon.svg");
+      background-image: url('./assets/animationNextKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -153,12 +199,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationNextKeyHoverIcon.svg");
+        background-image: url('./assets/animationNextKeyHoverIcon.svg');
       }
     }
 
     &.animation-options {
-      background-image: url("./assets/animationOptionsIcon.svg");
+      background-image: url('./assets/animationOptionsIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -171,7 +217,7 @@
     }
 
     &.animation-playfwd {
-      background-image: url("./assets/animationPlayFwdIcon.svg");
+      background-image: url('./assets/animationPlayFwdIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -181,12 +227,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationPlayFwdHoverIcon.svg");
+        background-image: url('./assets/animationPlayFwdHoverIcon.svg');
       }
     }
 
     &.animation-playrev {
-      background-image: url("./assets/animationPlayRevIcon.svg");
+      background-image: url('./assets/animationPlayRevIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -196,12 +242,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationPlayRevHoverIcon.svg");
+        background-image: url('./assets/animationPlayRevHoverIcon.svg');
       }
     }
 
     &.animation-start {
-      background-image: url("./assets/animationStartIcon.svg");
+      background-image: url('./assets/animationStartIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -213,7 +259,7 @@
     }
 
     &.animation-stop {
-      background-image: url("./assets/animationStopIcon.svg");
+      background-image: url('./assets/animationStopIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -222,10 +268,11 @@
       cursor: pointer;
       background-position: center;
       width: 20px;
+      margin-left: 10px;
     }
 
     &.animation-triangle {
-      background-image: url("./assets/animationTriangleIcon.svg");
+      background-image: url('./assets/animationTriangleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -233,7 +280,7 @@
     }
 
     &.key-active {
-      background-image: url("./assets/keyActiveIcon.svg");
+      background-image: url('./assets/keyActiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -241,7 +288,7 @@
     }
 
     &.key-inactive {
-      background-image: url("./assets/keyInactiveIcon.svg");
+      background-image: url('./assets/keyInactiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -249,7 +296,7 @@
     }
 
     &.key-selected {
-      background-image: url("./assets/keySelectedIcon.svg");
+      background-image: url('./assets/keySelectedIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -257,7 +304,7 @@
     }
 
     &.loop-active {
-      background-image: url("./assets/loopActiveIcon.svg");
+      background-image: url('./assets/loopActiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -266,7 +313,7 @@
     }
 
     &.loop-inactive {
-      background-image: url("./assets/loopInactiveIcon.svg");
+      background-image: url('./assets/loopInactiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -275,7 +322,7 @@
     }
 
     &.move {
-      background-image: url("./assets/moveIcon.svg");
+      background-image: url('./assets/moveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -283,7 +330,7 @@
     }
 
     &.save {
-      background-image: url("./assets/saveIcon.svg");
+      background-image: url('./assets/saveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -292,7 +339,7 @@
     }
 
     &.load {
-      background-image: url("./assets/loadIcon.svg");
+      background-image: url('./assets/loadIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -301,7 +348,7 @@
     }
 
     &.checked {
-      background-image: url("./assets/checkboxCheckedIcon.svg");
+      background-image: url('./assets/checkboxCheckedIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -309,7 +356,7 @@
     }
 
     &.unchecked {
-      background-image: url("./assets/checkboxDefaultIcon.svg");
+      background-image: url('./assets/checkboxDefaultIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -317,15 +364,22 @@
     }
 
     &.new-key {
-      background-image: url("./assets/newKeyIcon.svg");
+      background-image: url('./assets/newKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.scale {
-      background-image: url("./assets/scaleIcon.svg");
+      background-image: url('./assets/scaleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -333,7 +387,7 @@
     }
 
     &.scrollbar-handle {
-      background-image: url("./assets/scrollbarHandleIcon.svg");
+      background-image: url('./assets/scrollbarHandleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -423,6 +477,7 @@
 
     .buttons-container {
       display: flex;
+      padding-left: 10px;
     }
 
     .action-input {
@@ -440,6 +495,9 @@
         border: none;
         background-color: black;
         padding: 6px;
+        &:focus {
+          outline: solid 1px #ccc;
+        }
       }
     }
   }
@@ -539,6 +597,12 @@
               height: 20px;
               background-color: #666666;
               justify-content: space-between;
+              position: absolute;
+
+              .scrollbar {
+                cursor: pointer;
+                width: 100%;
+              }
 
               .left-grabber,
               .right-grabber {
@@ -649,7 +713,7 @@
 
       .load-container {
         flex-direction: column;
-        height: 387px;
+        height: 377px;
         padding-top: 10px;
 
         .load-server {
@@ -658,7 +722,7 @@
           background-color: #222222;
           padding-left: 10px;
           height: 20px;
-          margin-top: 287px;
+          margin-top: 277px;
           p {
             margin: 0px;
           }
@@ -729,11 +793,11 @@
 
       .save-container {
         flex-direction: column;
-        height: 387px;
+        height: 377px;
         padding-top: 10px;
 
         .item-list {
-          height: 327px;
+          height: 317px;
           ul {
             list-style: none;
             padding-left: 10px;
@@ -830,7 +894,7 @@
         display: block;
         position: absolute;
         background-color: #111111;
-        height: 367px;
+        height: 377px;
         z-index: 10;
 
         .sub-header {
@@ -885,7 +949,7 @@
               height: auto;
             }
             &:before {
-              content: "";
+              content: '';
               background: none;
             }
             height: 20px;
@@ -906,7 +970,7 @@
             }
 
             .animation-bullet {
-              background-image: url("./assets/animationBulletIcon.svg");
+              background-image: url('./assets/animationBulletIcon.svg');
               background-repeat: no-repeat;
               background-color: transparent;
               background-size: contain;
@@ -920,7 +984,7 @@
 
             .animation-arrow {
               width: 30px;
-              background-image: url("./assets/animationTriangleIcon.svg");
+              background-image: url('./assets/animationTriangleIcon.svg');
               background-repeat: no-repeat;
               background-color: transparent;
               background-size: 10px;
@@ -942,7 +1006,7 @@
 
               &.show {
                 display: block;
-                background-image: url("./assets/keySelectedIcon.svg");
+                background-image: url('./assets/keySelectedIcon.svg');
                 background-repeat: no-repeat;
                 background-color: transparent;
                 background-size: 10px;
@@ -952,7 +1016,7 @@
               }
 
               &.hide {
-                display: none;
+                display: block;
               }
             }
 
@@ -986,8 +1050,7 @@
           font-size: 10px;
           &:focus {
             border-radius: 0px;
-            outline-style: auto;
-            outline-color: lightgrey;
+            outline: 1px solid #ccc;
           }
           font-family: acumin-pro-condensed;
         }
@@ -1001,8 +1064,7 @@
           color: white;
           &:focus {
             border-radius: 0px;
-            outline-style: auto;
-            outline-color: lightgrey;
+            outline: 1px solid #ccc;
           }
           font-family: acumin-pro-condensed;
         }
@@ -1045,7 +1107,7 @@
         }
         text {
           fill: #555555;
-          font-family: "acumin-pro-condensed";
+          font-family: 'acumin-pro-condensed';
         }
 
         .control-point {
@@ -1059,15 +1121,29 @@
         }
       }
 
-      .playhead-wrapper {
+      .playhead-container {
         position: relative;
+      }
+
+      .playhead-wrapper {
         left: -13px;
         bottom: 366px;
+        position: relative;
+      }
+
+      .playhead-scrollable {
+        width: 100px;
+        height: 33px;
+        position: absolute;
+        top: 335px;
+        left: -39px;
       }
 
       .playhead-handle {
         position: relative;
         top: 340px;
+        width: 22px;
+        height: 30px;
         .playhead {
           width: 22px;
           background-color: transparent;
@@ -1076,6 +1152,7 @@
           font-size: 12px;
           position: absolute;
           top: 1px;
+          cursor: pointer;
         }
 
         .playhead-circle {
@@ -1104,7 +1181,7 @@
     align-items: center;
     justify-items: stretch;
 
-    input[type="file"] {
+    input[type='file'] {
       display: none;
     }
 

+ 29 - 28
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -1,17 +1,17 @@
-import * as React from "react";
+import * as React from 'react';
 
-import { Observable } from "babylonjs/Misc/observable";
-import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
-import { Animation } from "babylonjs/Animations/animation";
-import { IconButtonLineComponent } from "../../../lines/iconButtonLineComponent";
-import { NumericInputComponent } from "../../../lines/numericInputComponent";
-import { AddAnimation } from "./addAnimation";
-import { AnimationListTree, SelectedCoordinate } from "./animationListTree";
-import { IAnimatable } from "babylonjs/Animations/animatable.interface";
-import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
-import { LoadSnippet } from "./loadsnippet";
-import { SaveSnippet } from "./saveSnippet";
-import { LockObject } from "../lockObject";
+import { Observable } from 'babylonjs/Misc/observable';
+import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
+import { Animation } from 'babylonjs/Animations/animation';
+import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
+import { NumericInputComponent } from '../../../lines/numericInputComponent';
+import { AddAnimation } from './addAnimation';
+import { AnimationListTree, SelectedCoordinate } from './animationListTree';
+import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
+import { LoadSnippet } from './loadsnippet';
+import { SaveSnippet } from './saveSnippet';
+import { LockObject } from '../lockObject';
 
 interface IEditorControlsProps {
   isTargetedAnimation: boolean;
@@ -52,6 +52,7 @@ export class EditorControls extends React.Component<
   }
 
   animationAdded() {
+    // select recently created animation/first coordinate...
     this.setState({
       animationsCount: this.recountAnimations(),
       isEditTabOpen: true,
@@ -123,40 +124,40 @@ export class EditorControls extends React.Component<
 
   render() {
     return (
-      <div className="animation-list">
-        <div className="controls-header">
+      <div className='animation-list'>
+        <div className='controls-header'>
           {this.props.isTargetedAnimation ? null : (
             <IconButtonLineComponent
               active={this.state.isAnimationTabOpen}
-              tooltip="Add Animation"
-              icon="medium add-animation"
+              tooltip='Add Animation'
+              icon='medium add-animation'
               onClick={() => this.handleTabs(0)}
             ></IconButtonLineComponent>
           )}
           <IconButtonLineComponent
             active={this.state.isLoadTabOpen}
-            tooltip="Load Animation"
-            icon="medium load"
+            tooltip='Load Animation'
+            icon='medium load'
             onClick={() => this.handleTabs(1)}
           ></IconButtonLineComponent>
           <IconButtonLineComponent
             active={this.state.isSaveTabOpen}
-            tooltip="Save Animation"
-            icon="medium save"
+            tooltip='Save Animation'
+            icon='medium save'
             onClick={() => this.handleTabs(2)}
           ></IconButtonLineComponent>
           {this.state.animationsCount === 0 ? null : (
             <IconButtonLineComponent
               active={this.state.isEditTabOpen}
-              tooltip="Edit Animations"
-              icon="medium animation-edit"
+              tooltip='Edit Animations'
+              icon='medium animation-edit'
               onClick={() => this.handleTabs(3)}
             ></IconButtonLineComponent>
           )}
           {this.state.isEditTabOpen ? (
-            <div className="input-fps">
+            <div className='input-fps'>
               <NumericInputComponent
-                label={""}
+                label={''}
                 precision={0}
                 value={this.state.framesPerSecond}
                 onChange={(framesPerSecond: number) =>
@@ -168,11 +169,11 @@ export class EditorControls extends React.Component<
           ) : null}
           {this.state.isEditTabOpen ? (
             <IconButtonLineComponent
-              tooltip="Loop/Unloop"
+              tooltip='Loop/Unloop'
               icon={`medium ${
                 this.state.isLoopActive
-                  ? "loop-active last"
-                  : "loop-inactive last"
+                  ? 'loop-active last'
+                  : 'loop-inactive last'
               }`}
               onClick={() => {
                 this.setState({ isLoopActive: !this.state.isLoopActive });

+ 85 - 47
inspector/src/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar.tsx

@@ -1,52 +1,90 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
 
 interface IGraphActionsBarProps {
-   addKeyframe: () => void;
-   removeKeyframe: () => void;
-   handleValueChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
-   handleFrameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
-   flatTangent: () => void;
-   brokeTangents: () => void;
-   setLerpMode: () => void;
-   brokenMode: boolean;
-   lerpMode: boolean;
-   currentValue: number;
-   currentFrame: number;
-   title: string;
-   close: (event: any) => void;
-   enabled: boolean;
+  addKeyframe: () => void;
+  removeKeyframe: () => void;
+  handleValueChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+  handleFrameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+  flatTangent: () => void;
+  brokeTangents: () => void;
+  setLerpMode: () => void;
+  brokenMode: boolean;
+  lerpMode: boolean;
+  currentValue: number;
+  currentFrame: number;
+  title: string;
+  close: (event: any) => void;
+  enabled: boolean;
 }
 
-export class GraphActionsBar extends React.Component<IGraphActionsBarProps>{ 
-    constructor(props: IGraphActionsBarProps) {
-        super(props);
-    }
-     
-    render() { 
-       return (
-           <div className="actions-wrapper">
-               <div className="title-container">
-               <div className="icon babylon-logo"></div>
-               <div className="title">{this.props.title}</div>
-               </div>
-               <div className="buttons-container" style={{display: this.props.enabled ? 'flex' : 'none'}}>
-               <div className="action-input">
-               <input type="number" value={this.props.currentFrame} onChange={this.props.handleFrameChange} step="1"/>
-               </div>
-               <div className="action-input">
-               <input type="number" value={this.props.currentValue.toFixed(3)} onChange={this.props.handleValueChange} step="0.001"/>
-               </div>
-              <IconButtonLineComponent tooltip={"Add Keyframe"} icon="new-key" onClick={this.props.addKeyframe} />
-              <IconButtonLineComponent tooltip={"Remove Keyframe"} icon="frame" onClick={this.props.removeKeyframe} />
-              <IconButtonLineComponent tooltip={"Flat Tangents"} icon="flat-tangent" onClick={this.props.flatTangent} />
-              <IconButtonLineComponent tooltip={this.props.brokenMode ? "Broken Mode On" : "Broken Mode Off" } icon={this.props.brokenMode ? "break-tangent" : "unify-tangent" } onClick={this.props.brokeTangents} />
-              <IconButtonLineComponent tooltip={this.props.lerpMode ? "Lerp On" : "lerp Off" } icon="linear-tangent" onClick={this.props.setLerpMode} />
-              </div>
-              <div className="icon close" onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => this.props.close(event)}>
-              </div>
-           </div>
-        )
-    }
-} 
+export class GraphActionsBar extends React.Component<IGraphActionsBarProps> {
+  constructor(props: IGraphActionsBarProps) {
+    super(props);
+  }
+
+  render() {
+    return (
+      <div className='actions-wrapper'>
+        <div className='title-container'>
+          <div className='icon babylon-logo'></div>
+          <div className='title'>{this.props.title}</div>
+        </div>
+        <div
+          className='buttons-container'
+          style={{ display: this.props.enabled ? 'flex' : 'none' }}
+        >
+          <div className='action-input'>
+            <input
+              type='number'
+              value={this.props.currentFrame}
+              onChange={this.props.handleFrameChange}
+              step='1'
+            />
+          </div>
+          <div className='action-input'>
+            <input
+              type='number'
+              value={this.props.currentValue.toFixed(3)}
+              onChange={this.props.handleValueChange}
+              step='0.001'
+            />
+          </div>
+          <IconButtonLineComponent
+            tooltip={'Add Keyframe'}
+            icon='new-key'
+            onClick={this.props.addKeyframe}
+          />
+          <IconButtonLineComponent
+            tooltip={'Remove Keyframe'}
+            icon='frame'
+            onClick={this.props.removeKeyframe}
+          />
+          <IconButtonLineComponent
+            tooltip={'Flat Tangents'}
+            icon='flat-tangent'
+            onClick={this.props.flatTangent}
+          />
+          <IconButtonLineComponent
+            tooltip={
+              this.props.brokenMode ? 'Broken Mode On' : 'Broken Mode Off'
+            }
+            icon={this.props.brokenMode ? 'break-tangent' : 'unify-tangent'}
+            onClick={this.props.brokeTangents}
+          />
+          <IconButtonLineComponent
+            tooltip={this.props.lerpMode ? 'Lerp On' : 'lerp Off'}
+            icon='linear-tangent'
+            onClick={this.props.setLerpMode}
+          />
+        </div>
+        <div
+          className='icon close'
+          onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
+            this.props.close(event)
+          }
+        ></div>
+      </div>
+    );
+  }
+}

+ 72 - 19
inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx

@@ -1,27 +1,80 @@
-
-import * as React from "react";
+import * as React from 'react';
 
 interface IPlayheadProps {
-    frame: number;
-    offset: number;
+  frame: number;
+  offset: number;
+  onCurrentFrameChange: (frame: number) => void;
 }
 
-export class Playhead extends React.Component<IPlayheadProps>{
-    constructor(props: IPlayheadProps) {
-        super(props);
-    }
+export class Playhead extends React.Component<IPlayheadProps> {
+  private _direction: number;
+  private _active: boolean;
+  constructor(props: IPlayheadProps) {
+    super(props);
+  }
+
+  dragStart(e: React.TouchEvent<HTMLDivElement>): void;
+  dragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  dragStart(e: any) {
+    e.preventDefault();
+    this._direction = e.clientX;
+    this._active = true;
+  }
+
+  drag(e: React.TouchEvent<HTMLDivElement>): void;
+  drag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  drag(e: any) {
+    e.preventDefault();
+    if (this._active) {
+      let moved = e.pageX - this._direction;
 
-    render() {
-        return (
-            <div className="playhead-wrapper" id="playhead" style={{ left: `calc(${this.props.frame * (this.props.offset)}px - 13px)` }}>
-                <div className="playhead-line"></div>
-                <div className="playhead-handle">
-                    <div className="playhead-circle"></div>
-                    <div className="playhead">{this.props.frame}</div>
-                </div>
-            </div>
-        )
+      let framesToMove = Math.round(Math.abs(moved) / 2);
+      console.log(framesToMove);
+      if (Math.sign(moved) === -1) {
+        this.props.onCurrentFrameChange(this.props.frame - 1);
+      } else {
+        this.props.onCurrentFrameChange(this.props.frame + 1);
+      }
     }
-}
+  }
+
+  dragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+  dragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  dragEnd(e: any) {
+    e.preventDefault();
+    this._direction = 0;
+    this._active = false;
+  }
 
+  calculateMove() {
+    return `calc(${this.props.frame * this.props.offset}px - 13px)`;
+  }
 
+  render() {
+    return (
+      <div
+        className='playhead-wrapper'
+        id='playhead'
+        style={{
+          left: this.calculateMove(),
+        }}
+      >
+        <div className='playhead-line'></div>
+        <div
+          className='playhead-handle'
+          onMouseMove={(e) => this.drag(e)}
+          onTouchMove={(e) => this.drag(e)}
+          onTouchStart={(e) => this.dragStart(e)}
+          onTouchEnd={(e) => this.dragEnd(e)}
+          onMouseDown={(e) => this.dragStart(e)}
+          onMouseUp={(e) => this.dragEnd(e)}
+          onMouseLeave={(e) => this.dragEnd(e)}
+          onDragStart={() => false}
+        >
+          <div className='playhead-circle'></div>
+          <div className='playhead'>{this.props.frame}</div>
+        </div>
+      </div>
+    );
+  }
+}

+ 290 - 184
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -1,215 +1,321 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
 import { Controls } from './controls';
 
 interface ITimelineProps {
-    keyframes: IAnimationKey[] | null;
-    selected: IAnimationKey | null;
-    currentFrame: number;
-    onCurrentFrameChange: (frame: number) => void;
-    dragKeyframe: (frame: number, index: number) => void;
-    playPause: (direction: number) => void;
-    isPlaying: boolean;
+  keyframes: IAnimationKey[] | null;
+  selected: IAnimationKey | null;
+  currentFrame: number;
+  onCurrentFrameChange: (frame: number) => void;
+  dragKeyframe: (frame: number, index: number) => void;
+  playPause: (direction: number) => void;
+  isPlaying: boolean;
 }
 
-export class Timeline extends React.Component<ITimelineProps, { selected: IAnimationKey, activeKeyframe: number | null }>{
-    readonly _frames: object[] = Array(300).fill({});
-    private _scrollable: React.RefObject<HTMLDivElement>;
-    private _direction: number;
-    constructor(props: ITimelineProps) {
-        super(props);
-        if (this.props.selected !== null) {
-            this.state = { selected: this.props.selected, activeKeyframe: null };
-        }
-        this._scrollable = React.createRef();
-        this._direction = 0;
+export class Timeline extends React.Component<
+  ITimelineProps,
+  { selected: IAnimationKey; activeKeyframe: number | null }
+> {
+  readonly _frames: object[] = Array(300).fill({});
+  private _scrollable: React.RefObject<HTMLDivElement>;
+  private _scrollbarHandle: React.RefObject<HTMLDivElement>;
+  private _direction: number;
+  private _scrolling: boolean;
+  private _shiftX: number;
+  constructor(props: ITimelineProps) {
+    super(props);
+    if (this.props.selected !== null) {
+      this.state = { selected: this.props.selected, activeKeyframe: null };
     }
+    this._scrollable = React.createRef();
+    this._scrollbarHandle = React.createRef();
+    this._direction = 0;
+    this._scrolling = false;
+    this._shiftX = 0;
+  }
 
-    playBackwards(event: React.MouseEvent<HTMLDivElement>) {
-        this.props.playPause(-1);
-    }
+  playBackwards(event: React.MouseEvent<HTMLDivElement>) {
+    this.props.playPause(-1);
+  }
 
-    play(event: React.MouseEvent<HTMLDivElement>) {
-        this.props.playPause(1);
-    }
+  play(event: React.MouseEvent<HTMLDivElement>) {
+    this.props.playPause(1);
+  }
 
-    pause(event: React.MouseEvent<HTMLDivElement>) {
-        if (this.props.isPlaying) {
-            this.props.playPause(1);
-        }
+  pause(event: React.MouseEvent<HTMLDivElement>) {
+    if (this.props.isPlaying) {
+      this.props.playPause(1);
     }
+  }
 
-    handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
-        this.props.onCurrentFrameChange(parseInt(event.target.value));
-        event.preventDefault();
+  handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
+    this.props.onCurrentFrameChange(parseInt(event.target.value));
+    event.preventDefault();
+  }
+
+  nextFrame(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    this.props.onCurrentFrameChange(this.props.currentFrame + 1);
+    (this._scrollable.current as HTMLDivElement).scrollLeft =
+      this.props.currentFrame * 5;
+  }
+
+  previousFrame(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    if (this.props.currentFrame !== 0) {
+      this.props.onCurrentFrameChange(this.props.currentFrame - 1);
+      (this._scrollable.current as HTMLDivElement).scrollLeft = -(
+        this.props.currentFrame * 5
+      );
     }
+  }
 
-    nextFrame(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        this.props.onCurrentFrameChange(this.props.currentFrame + 1);
-        (this._scrollable.current as HTMLDivElement).scrollLeft = this.props.currentFrame * 5;
+  nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    if (this.props.keyframes !== null) {
+      let first = this.props.keyframes.find(
+        (kf) => kf.frame > this.props.currentFrame
+      );
+      if (first) {
+        this.props.onCurrentFrameChange(first.frame);
+        this.setState({ selected: first });
+        (this._scrollable.current as HTMLDivElement).scrollLeft =
+          first.frame * 5;
+      }
     }
+  }
 
-    previousFrame(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        if (this.props.currentFrame !== 0) {
-            this.props.onCurrentFrameChange(this.props.currentFrame - 1);
-            (this._scrollable.current as HTMLDivElement).scrollLeft = -(this.props.currentFrame * 5);
-        }
+  previousKeyframe(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    if (this.props.keyframes !== null) {
+      let keyframes = [...this.props.keyframes];
+      let first = keyframes
+        .reverse()
+        .find((kf) => kf.frame < this.props.currentFrame);
+      if (first) {
+        this.props.onCurrentFrameChange(first.frame);
+        this.setState({ selected: first });
+        (this._scrollable.current as HTMLDivElement).scrollLeft = -(
+          first.frame * 5
+        );
+      }
     }
+  }
 
-    nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        if (this.props.keyframes !== null) {
-            let first = this.props.keyframes.find(kf => kf.frame > this.props.currentFrame);
-            if (first) {
-                this.props.onCurrentFrameChange(first.frame);
-                this.setState({ selected: first });
-                (this._scrollable.current as HTMLDivElement).scrollLeft = first.frame * 5;
-            }
+  dragStart(e: React.TouchEvent<SVGSVGElement>): void;
+  dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragStart(e: any): void {
+    e.preventDefault();
+    this.setState({ activeKeyframe: parseInt(e.target.id.replace('kf_', '')) });
+    this._direction = e.clientX;
+  }
+
+  drag(e: React.TouchEvent<SVGSVGElement>): void;
+  drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  drag(e: any): void {
+    e.preventDefault();
+    if (this.props.keyframes) {
+      if (
+        this.state.activeKeyframe === parseInt(e.target.id.replace('kf_', ''))
+      ) {
+        let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
+        if (this._direction > e.clientX) {
+          console.log(`Dragging left ${this.state.activeKeyframe}`);
+          let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
+          if (used) {
+            updatedKeyframe.frame = used;
+          }
+        } else {
+          console.log(`Dragging Right ${this.state.activeKeyframe}`);
+          let used = this.isFrameBeingUsed(updatedKeyframe.frame + 1, 1);
+          if (used) {
+            updatedKeyframe.frame = used;
+          }
         }
+
+        this.props.dragKeyframe(
+          updatedKeyframe.frame,
+          this.state.activeKeyframe
+        );
+      }
     }
+  }
 
-    previousKeyframe(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        if (this.props.keyframes !== null) {
-            let keyframes = [...this.props.keyframes]
-            let first = keyframes.reverse().find(kf => kf.frame < this.props.currentFrame);
-            if (first) {
-                this.props.onCurrentFrameChange(first.frame);
-                this.setState({ selected: first });
-                (this._scrollable.current as HTMLDivElement).scrollLeft = -(first.frame * 5);
-            }
-        }
+  isFrameBeingUsed(frame: number, direction: number) {
+    let used = this.props.keyframes?.find((kf) => kf.frame === frame);
+    if (used) {
+      this.isFrameBeingUsed(used.frame + direction, direction);
+      return false;
+    } else {
+      return frame;
     }
+  }
 
-    dragStart(e: React.TouchEvent<SVGSVGElement>): void;
-    dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragStart(e: any): void {
-        e.preventDefault();
-        this.setState({ activeKeyframe: parseInt(e.target.id.replace('kf_', '')) });
-        this._direction = e.clientX;
+  dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
+  dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragEnd(e: any): void {
+    e.preventDefault();
+    this._direction = 0;
+    this.setState({ activeKeyframe: null });
+  }
 
+  scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
+  scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  scrollDragStart(e: any) {
+    e.preventDefault();
+    if ((e.target.class = 'scrollbar') && this._scrollbarHandle.current) {
+      this._scrolling = true;
+      this._shiftX =
+        e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left;
+      this._scrollbarHandle.current.style.left = e.pageX - this._shiftX + 'px';
     }
+  }
 
-    drag(e: React.TouchEvent<SVGSVGElement>): void;
-    drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    drag(e: any): void {
-        e.preventDefault();
-        if (this.props.keyframes) {
-            if (this.state.activeKeyframe === parseInt(e.target.id.replace('kf_', ''))) {
-                let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
-                if (this._direction > e.clientX) {
-                    console.log(`Dragging left ${this.state.activeKeyframe}`);
-                    let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
-                    if (used) {
-                        updatedKeyframe.frame = used
-                    }
-                } else {
-                    console.log(`Dragging Right ${this.state.activeKeyframe}`)
-                    let used = this.isFrameBeingUsed(updatedKeyframe.frame + 1, 1);
-                    if (used) {
-                        updatedKeyframe.frame = used
-                    }
-                }
-
-                this.props.dragKeyframe(updatedKeyframe.frame, this.state.activeKeyframe);
-
-            }
-        }
+  scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
+  scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  scrollDrag(e: any) {
+    e.preventDefault();
+    if (this._scrolling && this._scrollbarHandle.current) {
+      let moved = e.pageX - this._shiftX;
+      if (moved > 233 && moved < 630) {
+        this._scrollbarHandle.current.style.left = moved + 'px';
+        (this._scrollable.current as HTMLDivElement).scrollLeft = moved + 10;
+      }
     }
+  }
 
-    isFrameBeingUsed(frame: number, direction: number) {
-        let used = this.props.keyframes?.find(kf => kf.frame === frame);
-        if (used) {
-            this.isFrameBeingUsed(used.frame + direction, direction);
-            return false;
-        } else {
-            return frame;
-        }
-    }
+  scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+  scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  scrollDragEnd(e: any) {
+    e.preventDefault();
+    this._scrolling = false;
+    this._shiftX = 0;
+  }
 
-    dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
-    dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragEnd(e: any): void {
-        e.preventDefault();
-        this._direction = 0;
-        this.setState({ activeKeyframe: null })
-    }
+  render() {
+    return (
+      <>
+        <div className='timeline'>
+          <Controls
+            keyframes={this.props.keyframes}
+            selected={this.props.selected}
+            currentFrame={this.props.currentFrame}
+            onCurrentFrameChange={this.props.onCurrentFrameChange}
+            playPause={this.props.playPause}
+            isPlaying={this.props.isPlaying}
+            scrollable={this._scrollable}
+          />
+          <div className='timeline-wrapper'>
+            <div ref={this._scrollable} className='display-line'>
+              <svg
+                viewBox='0 0 2010 40'
+                style={{ width: 2000, height: 40, backgroundColor: '#222222' }}
+                onMouseMove={(e) => this.drag(e)}
+                onTouchMove={(e) => this.drag(e)}
+                onTouchStart={(e) => this.dragStart(e)}
+                onTouchEnd={(e) => this.dragEnd(e)}
+                onMouseDown={(e) => this.dragStart(e)}
+                onMouseUp={(e) => this.dragEnd(e)}
+                onMouseLeave={(e) => this.dragEnd(e)}
+                onDragStart={() => false}
+              >
+                <line
+                  x1={this.props.currentFrame * 10}
+                  y1='0'
+                  x2={this.props.currentFrame * 10}
+                  y2='40'
+                  style={{ stroke: '#12506b', strokeWidth: 6 }}
+                />
+                {this.props.keyframes &&
+                  this.props.keyframes.map((kf, i) => {
+                    return (
+                      <svg
+                        key={`kf_${i}`}
+                        style={{ cursor: 'pointer' }}
+                        tabIndex={i + 40}
+                      >
+                        <line
+                          id={`kf_${i.toString()}`}
+                          x1={kf.frame * 10}
+                          y1='0'
+                          x2={kf.frame * 10}
+                          y2='40'
+                          style={{ stroke: 'red', strokeWidth: 6 }}
+                        />
+                      </svg>
+                    );
+                  })}
+                {this._frames.map((frame, i) => {
+                  return (
+                    <svg key={`tl_${i}`}>
+                      {i % 5 === 0 ? (
+                        <>
+                          <text
+                            x={i * 5 - 3}
+                            y='18'
+                            style={{ fontSize: 10, fill: '#555555' }}
+                          >
+                            {i}
+                          </text>
+                          <line
+                            x1={i * 5}
+                            y1='22'
+                            x2={i * 5}
+                            y2='40'
+                            style={{ stroke: '#555555', strokeWidth: 0.5 }}
+                          />
+                        </>
+                      ) : null}
+                    </svg>
+                  );
+                })}
+              </svg>
+            </div>
+
+            <div className='timeline-scroll-handle'>
+              <div className='scroll-handle'>
+                <div
+                  className='handle'
+                  ref={this._scrollbarHandle}
+                  style={{ width: 300 }}
+                >
+                  <div className='left-grabber'>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='text'>20</div>
+                  </div>
+                  <div
+                    className='scrollbar'
+                    onMouseMove={(e) => this.scrollDrag(e)}
+                    onTouchMove={(e) => this.scrollDrag(e)}
+                    onTouchStart={(e) => this.scrollDragStart(e)}
+                    onTouchEnd={(e) => this.scrollDragEnd(e)}
+                    onMouseDown={(e) => this.scrollDragStart(e)}
+                    onMouseUp={(e) => this.scrollDragEnd(e)}
+                    onMouseLeave={(e) => this.scrollDragEnd(e)}
+                    onDragStart={() => false}
+                  ></div>
 
-    render() {
-        return (
-            <>
-                <div className="timeline">
-                    <Controls keyframes={this.props.keyframes}
-                        selected={this.props.selected}
-                        currentFrame={this.props.currentFrame}
-                        onCurrentFrameChange={this.props.onCurrentFrameChange}
-                        playPause={this.props.playPause}
-                        isPlaying={this.props.isPlaying}
-                        scrollable={this._scrollable} />
-                    <div className="timeline-wrapper">
-                        <div ref={this._scrollable} className="display-line" >
-                            <svg viewBox="0 0 2010 40" style={{ width: 2000, height: 40, backgroundColor: '#222222' }} onMouseMove={(e) => this.drag(e)}
-                                onTouchMove={(e) => this.drag(e)}
-                                onTouchStart={(e) => this.dragStart(e)}
-                                onTouchEnd={(e) => this.dragEnd(e)}
-                                onMouseDown={(e) => this.dragStart(e)}
-                                onMouseUp={(e) => this.dragEnd(e)}
-                                onMouseLeave={(e) => this.dragEnd(e)}>
-
-                                <line x1={this.props.currentFrame * 10} y1="0" x2={this.props.currentFrame * 10} y2="40" style={{ stroke: '#12506b', strokeWidth: 6 }} />
-                                {
-                                    this.props.keyframes && this.props.keyframes.map((kf, i) => {
-
-                                        return <svg key={`kf_${i}`} style={{ cursor: 'pointer' }} tabIndex={i + 40} >
-                                            <line id={`kf_${i.toString()}`} x1={kf.frame * 10} y1="0" x2={kf.frame * 10} y2="40" style={{ stroke: 'red', strokeWidth: 6 }} />
-                                        </svg>
-                                    })
-                                }
-                                {
-                                    this._frames.map((frame, i) => {
-
-                                        return <svg key={`tl_${i}`}>
-                                            {i % 5 === 0 ?
-                                                <>
-                                                    <text x={(i * 5) - 3} y="18" style={{ fontSize: 10, fill: '#555555' }}>{i}</text>
-                                                    <line x1={i * 5} y1="22" x2={i * 5} y2="40" style={{ stroke: '#555555', strokeWidth: 0.5 }} />
-                                                </> : null}
-
-                                        </svg>
-                                    })
-                                }
-                            </svg>
-                        </div>
-
-                        <div className="timeline-scroll-handle">
-                            <div className="scroll-handle">
-                                <div className="handle" style={{ width: 300, marginLeft: 20 }}>
-                                    <div className="left-grabber">
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="text">20</div>
-                                    </div>
-
-
-                                    <div className="right-grabber">
-                                        <div className="text">100</div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                    </div>
-                                </div>
-                            </div>
-                            <div className="input-frame">
-                                <input type="number" value={this.props.currentFrame} onChange={(e) => this.handleInputChange(e)}></input>
-                            </div>
-                        </div>
-                    </div>
+                  <div className='right-grabber'>
+                    <div className='text'>100</div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                  </div>
                 </div>
-            </>
-        )
-    }
-} 
+              </div>
+              <div className='input-frame'>
+                <input
+                  type='number'
+                  value={this.props.currentFrame}
+                  onChange={(e) => this.handleInputChange(e)}
+                ></input>
+              </div>
+            </div>
+          </div>
+        </div>
+      </>
+    );
+  }
+}

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/cameras/commonCameraPropertyGridComponent.tsx

@@ -49,6 +49,7 @@ export class CommonCameraPropertyGridComponent extends React.Component<ICommonCa
                     <FloatLineComponent lockObject={this.props.lockObject} label="Near plane" target={camera} propertyName="minZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <FloatLineComponent lockObject={this.props.lockObject} label="Far plane" target={camera} propertyName="maxZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <SliderLineComponent label="Inertia" target={camera} propertyName="inertia" minimum={0} maximum={1} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    <FloatLineComponent isInteger lockObject={this.props.lockObject} label="Layer mask" target={camera} propertyName="layerMask" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <OptionsLineComponent label="Mode" options={modeOptions} target={camera} propertyName="mode" onPropertyChangedObservable={this.props.onPropertyChangedObservable} onSelect={(value) => this.setState({ mode: value })} />
                     {
                         camera.mode === Camera.PERSPECTIVE_CAMERA &&

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/meshes/meshPropertyGridComponent.tsx

@@ -395,6 +395,7 @@ export class MeshPropertyGridComponent extends React.Component<IMeshPropertyGrid
                         <CheckBoxLineComponent label="Infinite distance" target={mesh} propertyName="infiniteDistance" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     }
                     <SliderLineComponent label="Rendering group ID" decimalCount={0} target={mesh} propertyName="renderingGroupId" minimum={RenderingManager.MIN_RENDERINGGROUPS} maximum={RenderingManager.MAX_RENDERINGGROUPS - 1} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />                    
+                    <FloatLineComponent isInteger lockObject={this.props.lockObject} label="Layer mask" target={mesh} propertyName="layerMask" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                 </LineContainerComponent>
                 {
                     mesh.morphTargetManager != null &&

+ 2 - 0
inspector/src/components/globalState.ts

@@ -11,6 +11,7 @@ import { PropertyChangedEvent } from "./propertyChangedEvent";
 import { ReplayRecorder } from './replayRecorder';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
 import { CodeChangedEvent } from './codeChangedEvent';
+import { Inspector } from '../inspector';
 
 export class GlobalState {
     public onSelectionChangedObservable: Observable<any>;
@@ -110,6 +111,7 @@ export class GlobalState {
             this.onValidationResultsUpdatedObservable.notifyObservers(results);
 
             if (results.issues.numErrors || results.issues.numWarnings) {
+                Inspector.MarkLineContainerTitleForHighlighting("GLTF VALIDATION");
                 this.onTabChangedObservable.notifyObservers(3);
             }
         });

+ 4 - 4
loaders/src/OBJ/objFileLoader.ts

@@ -449,10 +449,10 @@ export class OBJFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlugi
                 unwrappedPositionsForBabylon.push(wrappedPositionForBabylon[l].x, wrappedPositionForBabylon[l].y, wrappedPositionForBabylon[l].z);
                 unwrappedNormalsForBabylon.push(wrappedNormalsForBabylon[l].x, wrappedNormalsForBabylon[l].y, wrappedNormalsForBabylon[l].z);
                 unwrappedUVForBabylon.push(wrappedUvsForBabylon[l].x, wrappedUvsForBabylon[l].y); //z is an optional value not supported by BABYLON
-            }
-            if (this._meshLoadOptions.ImportVertexColors === true) {
-                //Push the r, g, b, a values of each element in the unwrapped array
-                unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                if (this._meshLoadOptions.ImportVertexColors === true) {
+                    //Push the r, g, b, a values of each element in the unwrapped array
+                    unwrappedColorsForBabylon.push(wrappedColorsForBabylon[l].r, wrappedColorsForBabylon[l].g, wrappedColorsForBabylon[l].b, wrappedColorsForBabylon[l].a);
+                }
             }
             // Reset arrays for the next new meshes
             wrappedPositionForBabylon = [];

+ 12 - 16
loaders/src/glTF/2.0/Extensions/KHR_materials_sheen.ts

@@ -10,10 +10,9 @@ import { Color3 } from 'babylonjs/Maths/math.color';
 const NAME = "KHR_materials_sheen";
 
 interface IKHR_materials_sheen {
-    intensityFactor: number;
-    colorFactor: number[];
-    colorIntensityTexture: ITextureInfo;
-    roughnessFactor: number;
+    sheenColorFactor: number[];
+    sheenTexture: ITextureInfo;
+    sheenRoughnessFactor: number;
 }
 
 /**
@@ -68,27 +67,24 @@ export class KHR_materials_sheen implements IGLTFLoaderExtension {
         const promises = new Array<Promise<any>>();
 
         babylonMaterial.sheen.isEnabled = true;
+        babylonMaterial.sheen.intensity = 1;
 
-        if (properties.intensityFactor != undefined) {
-            babylonMaterial.sheen.intensity = properties.intensityFactor;
+        if (properties.sheenColorFactor != undefined) {
+            babylonMaterial.sheen.color = Color3.FromArray(properties.sheenColorFactor);
         }
         else {
-            babylonMaterial.sheen.intensity = 0;
+            babylonMaterial.sheen.color = Color3.Black();
         }
 
-        if (properties.colorFactor != undefined) {
-            babylonMaterial.sheen.color = Color3.FromArray(properties.colorFactor);
-        }
-
-        if (properties.colorIntensityTexture) {
-            promises.push(this._loader.loadTextureInfoAsync(`${context}/sheenTexture`, properties.colorIntensityTexture, (texture) => {
-                texture.name = `${babylonMaterial.name} (Sheen Intensity)`;
+        if (properties.sheenTexture) {
+            promises.push(this._loader.loadTextureInfoAsync(`${context}/sheenTexture`, properties.sheenTexture, (texture) => {
+                texture.name = `${babylonMaterial.name} (Sheen Color)`;
                 babylonMaterial.sheen.texture = texture;
             }));
         }
 
-        if (properties.roughnessFactor !== undefined) {
-            babylonMaterial.sheen.roughness = properties.roughnessFactor;
+        if (properties.sheenRoughnessFactor !== undefined) {
+            babylonMaterial.sheen.roughness = properties.sheenRoughnessFactor;
         } else {
             babylonMaterial.sheen.roughness = 0;
         }

+ 1 - 1
loaders/src/glTF/2.0/glTFLoader.ts

@@ -356,7 +356,7 @@ export class GLTFLoader implements IGLTFLoader {
             });
 
             return resultPromise;
-        }, (error) => {
+        }).catch((error) => {
             if (!this._disposed) {
                 this._parent.onErrorObservable.notifyObservers(error);
                 this._parent.onErrorObservable.clear();

+ 1 - 1
materialsLibrary/src/custom/customMaterial.ts

@@ -188,7 +188,7 @@ export class CustomMaterial extends StandardMaterial {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 (<any>this._newSamplerInstances)[kind + "-" + name] = param;
             }
             else {

+ 1 - 1
materialsLibrary/src/custom/pbrCustomMaterial.ts

@@ -190,7 +190,7 @@ export class PBRCustomMaterial extends PBRMaterial {
             this._newUniformInstances = {};
         }
         if (param) {
-            if (kind.indexOf("sampler") == -1) {
+            if (kind.indexOf("sampler") != -1) {
                 (<any>this._newSamplerInstances)[kind + "-" + name] = param;
             }
             else {

+ 3 - 0
sandbox/README-ES6.md

@@ -0,0 +1,3 @@
+# Babylon.js Sandbox
+
+An extension to easily create a full page viewer (ala sandbox.babylonjs.com)

+ 16 - 0
sandbox/README.md

@@ -0,0 +1,16 @@
+# Babylon.js Sandbox
+
+An extension to easily create a full page viewer (ala sandbox.babylonjs.com)
+
+## Usage
+### Online method
+Call the method `Show` of the `BABYLON.Sandbox` class: 
+```
+BABYLON.Sandbox.Show({hostElement: document.getElementById("host")});
+```
+
+### Offline method
+If you don't have access to internet, the node editor should be imported manually in your HTML page :
+```
+<script src="babylon.sandbox.js" />
+``` 

+ 0 - 23
sandbox/index.css

@@ -174,29 +174,6 @@ a:visited {
     right: 30px;
 }
 
-#loadingText {
-    width: 100%;
-    height: 60px;
-    position: absolute;
-    top: 50%;
-    left: 0;
-    margin-top: -30px;
-    color: white;
-    text-align: center;
-    padding-top: 10px;
-    font-size: 30px;
-    transition: transform 0.25s ease-in-out;
-    -webkit-transition: -webkit-transform 0.25s ease-in-out;
-    z-index: 3;
-    cursor: default;
-    background-color: var(--footer-background);
-}
-
-.loadingText {
-    transform: translateX(120%);
-    -webkit-transform: translateX(120%);
-}
-
 #errorZone {
     display:none;
     position: absolute;

+ 45 - 0
sandbox/public/index-local.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <title>Babylon.js Sandbox - View glTF, glb, obj and babylon files</title>
+    <meta name="description" content="Viewer for glTF, glb, obj and babylon files powered by Babylon.js" />
+    <meta name="keywords"
+        content="Babylon.js, Babylon, BabylonJS, glTF, glb, obj, viewer, online viewer, 3D model viewer, 3D, webgl" />
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
+    <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css">
+    <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>
+    
+    <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(() => {
+            });
+    </script>
+</body>
+
+</html>

+ 54 - 0
sandbox/public/index.html

@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <title>Babylon.js Sandbox - View glTF, glb, obj and babylon files</title>
+    <meta name="description" content="Viewer for glTF, glb, obj and babylon files powered by Babylon.js" />
+    <meta name="keywords"
+        content="Babylon.js, Babylon, BabylonJS, glTF, glb, obj, viewer, online viewer, 3D model viewer, 3D, webgl" />
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
+    <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css">
+    <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="https://preview.babylonjs.com/ammo.js"></script>
+    <script src="https://preview.babylonjs.com/cannon.js"></script>
+    <script src="https://preview.babylonjs.com/Oimo.js"></script>
+    <script src="https://preview.babylonjs.com/libktx.js"></script>
+    <script src="https://preview.babylonjs.com/babylon.js"></script>
+    <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
+
+    <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
+    <script src="https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
+    <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
+
+    
+    <script src="https://preview.babylonjs.com/sandbox/babylon.sandbox.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>

+ 3 - 0
sandbox/public/index.js

@@ -0,0 +1,3 @@
+var hostElement = document.getElementById("host-element");
+
+BABYLON.Sandbox.Show(hostElement);

+ 70 - 0
sandbox/src/components/dropUpButton.tsx

@@ -0,0 +1,70 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+import { Nullable } from 'babylonjs/types';
+import { Observer } from 'babylonjs/Misc/observable';
+
+
+interface IDropUpButtonProps {
+    globalState: GlobalState;
+    enabled: boolean;
+    icon: any;
+    label: string;
+    options: string[];
+    onOptionPicked: (option: string) => void;
+}
+
+export class DropUpButton extends React.Component<IDropUpButtonProps, {isOpen: boolean}> {
+    private _onClickInterceptorClickedObserver: Nullable<Observer<void>>;
+
+    public constructor(props: IDropUpButtonProps) {    
+        super(props);
+
+        this.state = {isOpen: false};
+
+        this._onClickInterceptorClickedObserver = props.globalState.onClickInterceptorClicked.add(() => {
+            this.switchDropUp();
+        });
+    }
+
+    componentWillUnmount() {
+        this.props.globalState.onClickInterceptorClicked.remove(this._onClickInterceptorClickedObserver);
+    }
+
+    switchDropUp() {
+        this.props.globalState.onRequestClickInterceptor.notifyObservers();
+        this.setState({isOpen: !this.state.isOpen});
+    }
+
+    clickOption(option: string) {
+        this.switchDropUp()
+        this.props.onOptionPicked(option);
+    }
+
+    public render() {
+        if (!this.props.enabled) {
+            return null;
+        }
+
+        return (
+            <>
+                <div className="button" onClick={() => this.switchDropUp()}>
+                    <img src={this.props.icon} alt={this.props.label} title={this.props.label}  />
+                </div>
+                {
+                    this.state.isOpen &&
+                    <div className="dropup-content">
+                    {
+                        this.props.options.map(o => {
+                            return(
+                                <div key={o} onClick={() => this.clickOption(o)}>
+                                    {o}
+                                </div>
+                            )
+                        })
+                    }
+                    </div>
+                }
+            </>
+        )
+    }
+}

+ 68 - 0
sandbox/src/components/footer.tsx

@@ -0,0 +1,68 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+import { FooterButton } from './footerButton';
+import { DropUpButton } from './dropUpButton';
+import { EnvironmentTools } from '../tools/environmentTools';
+import { FooterFileButton } from './footerFileButton';
+
+require("../scss/footer.scss");
+var babylonIdentity = require("../img/babylon-identity.svg");
+var iconEdit = require("../img/icon-edit.svg");
+var iconOpen = require("../img/icon-open.svg");
+var iconIBL = require("../img/icon-ibl.svg");
+
+interface IFooterProps {
+    globalState: GlobalState;
+}
+
+export class Footer extends React.Component<IFooterProps> {
+    
+        
+    public constructor(props: IFooterProps) {    
+        super(props);
+        props.globalState.onSceneLoaded.add(info => {
+            this.forceUpdate();
+        });
+    }
+
+    showInspector() {
+        if (this.props.globalState.currentScene) {
+            if (this.props.globalState.currentScene.debugLayer.isVisible()) {
+                this.props.globalState.hideDebugLayer();
+            }
+            else {
+                this.props.globalState.showDebugLayer();
+            }
+        }
+    }
+
+    render() {
+        return (            
+            <div id="footer" className="footer">
+                <div className="footerLeft">
+                    <img id="logoImg" src={babylonIdentity}/>
+                </div>
+                <div className="footerRight">
+                    <FooterFileButton globalState={this.props.globalState} 
+                                enabled={true}
+                                icon={iconOpen}
+                                onFilesPicked={(evt, files) => {
+                                    this.props.globalState.filesInput.loadFiles(evt);
+                                }}
+                                label="Open your scene from your hard drive (.babylon, .gltf, .glb, .obj)"/>
+                    <DropUpButton globalState={this.props.globalState} 
+                                    icon={iconIBL}
+                                    label="Select environment"
+                                    options={EnvironmentTools.SkyboxesNames}
+                                    onOptionPicked={option => this.props.globalState.onEnvironmentChanged.notifyObservers(option)}
+                                    enabled={!!this.props.globalState.currentScene}/>
+                    <FooterButton globalState={this.props.globalState} 
+                                    icon={iconEdit}
+                                    label="Display inspector"
+                                    onClick={() => this.showInspector()}
+                                    enabled={!!this.props.globalState.currentScene}/>
+                </div>
+            </div>
+        )
+    }
+}

+ 25 - 0
sandbox/src/components/footerButton.tsx

@@ -0,0 +1,25 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+
+interface IFooterButtonProps {
+    globalState: GlobalState;
+    enabled: boolean;
+    onClick: () => void;
+    icon: any;
+    label: string;
+}
+
+export class FooterButton extends React.Component<IFooterButtonProps> {
+
+    public render() {
+        if (!this.props.enabled) {
+            return null;
+        }
+
+        return (
+            <div className="button" onClick={() => this.props.onClick()}>
+                <img src={this.props.icon} alt={this.props.label} title={this.props.label} />
+            </div>
+        )
+    }
+}

+ 30 - 0
sandbox/src/components/footerFileButton.tsx

@@ -0,0 +1,30 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+
+interface IFooterFileButtonProps {
+    globalState: GlobalState;
+    enabled: boolean;
+    icon: any;
+    label: string;
+    onFilesPicked: (evt: Event,files: FileList | null) => void;
+}
+
+export class FooterFileButton extends React.Component<IFooterFileButtonProps> {
+
+    onFilePicked(evt: React.ChangeEvent<HTMLInputElement>) {
+        this.props.onFilesPicked(evt.nativeEvent, evt.target.files);
+    }
+
+    public render() {
+        if (!this.props.enabled) {
+            return null;
+        }
+
+        return (
+            <div className="custom-upload" title={this.props.label}>
+                <img src={this.props.icon}/>
+                <input type="file" id="files" multiple onChange={evt => this.onFilePicked(evt)}/>
+            </div>
+        )
+    }
+}

+ 271 - 0
sandbox/src/components/renderingZone.tsx

@@ -0,0 +1,271 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+
+import { Engine } from 'babylonjs/Engines/engine';
+import { SceneLoader } from 'babylonjs/Loading/sceneLoader';
+import { GLTFFileLoader } from "babylonjs-loaders/glTF/glTFFileLoader";
+import { Scene } from 'babylonjs/scene';
+import { Vector3 } from 'babylonjs/Maths/math.vector';
+import { ArcRotateCamera } from 'babylonjs/Cameras/arcRotateCamera';
+import { FramingBehavior } from 'babylonjs/Behaviors/Cameras/framingBehavior';
+import { EnvironmentTools } from '../tools/environmentTools';
+import { Tools } from 'babylonjs/Misc/tools';
+import { FilesInput } from 'babylonjs/Misc/filesInput';
+import {Animation} from 'babylonjs/Animations/animation';
+
+require("../scss/renderingZone.scss");
+
+interface IRenderingZoneProps {
+    globalState: GlobalState;
+    assetUrl?: string;
+    cameraPosition?: Vector3;
+    expanded: boolean;
+}
+
+export class RenderingZone extends React.Component<IRenderingZoneProps> {
+    private _currentPluginName: string;
+    private _engine: Engine;
+    private _scene: Scene;
+    private _canvas: HTMLCanvasElement;
+
+    public constructor(props: IRenderingZoneProps) {
+        super(props);
+    }
+
+    initEngine() {
+        this._canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
+        this._engine = new Engine(this._canvas, true, { premultipliedAlpha: false, preserveDrawingBuffer: true });
+   
+        this._engine.loadingUIBackgroundColor = "#2A2342";
+
+        // Resize
+        window.addEventListener("resize", () => {
+            this._engine.resize();
+        });
+
+        this.loadAsset();
+
+        // File inputs
+        let filesInput = new FilesInput(this._engine, null, 
+            (sceneFile: File, scene: Scene) => {
+                this._scene = scene;
+                this.onSceneLoaded(sceneFile.name);
+            },
+            null, null, null, 
+            () => {
+                Tools.ClearLogCache();
+                if (this._scene) {
+                    this.props.globalState.isDebugLayerEnabled = this.props.globalState.currentScene.debugLayer.isVisible();
+
+                    if (this.props.globalState.isDebugLayerEnabled) {
+                        this._scene.debugLayer.hide();
+                    }
+                }
+            }, null, null);
+
+        filesInput.onProcessFileCallback = (file, name, extension) => {
+            if (filesInput.filesToLoad && filesInput.filesToLoad.length === 1 && extension) {
+                if (extension.toLowerCase() === "dds" ||
+                    extension.toLowerCase() === "env" ||
+                    extension.toLowerCase() === "hdr") {
+                    FilesInput.FilesToLoad[name] = file;
+                    EnvironmentTools.SkyboxPath = "file:" + (file as any).correctName;
+                    return false;
+                }
+            }
+            return true;
+        };
+        filesInput.monitorElementForDragNDrop(this._canvas);
+
+        this.props.globalState.filesInput = filesInput;
+    }
+
+    prepareCamera() {
+        let camera: ArcRotateCamera;
+
+        // Attach camera to canvas inputs
+        if (!this._scene.activeCamera || this._scene.lights.length === 0) {
+            this._scene.createDefaultCamera(true);
+
+            camera = this._scene.activeCamera! as ArcRotateCamera;
+
+            if (this.props.cameraPosition) {
+                camera.setPosition(this.props.cameraPosition);
+            }
+            else {
+                if (this._currentPluginName === "gltf") {
+                    // glTF assets use a +Z forward convention while the default camera faces +Z. Rotate the camera to look at the front of the asset.
+                    camera.alpha += Math.PI;
+                }
+
+                // Enable camera's behaviors
+                camera.useFramingBehavior = true;
+
+                var framingBehavior = camera.getBehaviorByName("Framing") as FramingBehavior;
+                framingBehavior.framingTime = 0;
+                framingBehavior.elevationReturnTime = -1;
+
+                if (this._scene.meshes.length) {
+                   camera.lowerRadiusLimit = null;
+
+                    var worldExtends = this._scene.getWorldExtends(function (mesh) {
+                        return mesh.isVisible && mesh.isEnabled();
+                    });
+                    framingBehavior.zoomOnBoundingInfo(worldExtends.min, worldExtends.max);
+                }
+            }
+
+            camera.pinchPrecision = 200 / camera.radius;
+            camera.upperRadiusLimit = 5 * camera.radius;
+
+            camera.wheelDeltaPercentage = 0.01;
+            camera.pinchDeltaPercentage = 0.01;
+        }
+
+        this._scene.activeCamera!.attachControl(this._canvas);     
+    }
+
+    handleErrors() {
+        // In case of error during loading, meshes will be empty and clearColor is set to red
+        if (this._scene.meshes.length === 0 && this._scene.clearColor.r === 1 && this._scene.clearColor.g === 0 && this._scene.clearColor.b === 0) {
+            this._canvas.style.opacity = "0";
+            this.props.globalState.onError.notifyObservers({scene: this._scene, message: "No mesh found in your scene"});
+        }
+        else {
+            if (Tools.errorsCount > 0) {
+                this.props.globalState.onError.notifyObservers({scene: this._scene, message: "Scene loaded but several errors were found"});
+            }
+        //    this._canvas.style.opacity = "1";
+            let camera = this._scene.activeCamera! as ArcRotateCamera;
+            if (camera.keysUp) {
+                camera.keysUp.push(90); // Z
+                camera.keysUp.push(87); // W
+                camera.keysDown.push(83); // S
+                camera.keysLeft.push(65); // A
+                camera.keysLeft.push(81); // Q
+                camera.keysRight.push(69); // E
+                camera.keysRight.push(68); // D
+            }
+        }      
+    }
+
+    prepareLighting() {
+        if (this._currentPluginName === "gltf") {
+            if (!this._scene.environmentTexture) {
+                this._scene.environmentTexture = EnvironmentTools.LoadSkyboxPathTexture(this._scene);
+            }
+
+            if (this._scene.environmentTexture) {
+                this._scene.createDefaultSkybox(this._scene.environmentTexture, true, (this._scene.activeCamera!.maxZ - this._scene.activeCamera!.minZ) / 2, 0.3, false);
+            }
+        }
+        else {
+            var pbrPresent = false;
+            for (var i = 0; i < this._scene.materials.length; i++) {
+                if (this._scene.materials[i].transparencyMode !== undefined) {
+                    pbrPresent = true;
+                    break;
+                }
+            }
+
+            if (pbrPresent) {
+                if (!this._scene.environmentTexture) {
+                    this._scene.environmentTexture = EnvironmentTools.LoadSkyboxPathTexture(this._scene);
+                }
+            }
+            else {
+                this._scene.createDefaultLight();
+            }
+        }
+    }
+
+    onSceneLoaded(filename: string) {
+        this._engine.clearInternalTexturesCache();
+
+        this._scene.skipFrustumClipping = true;
+
+        this.props.globalState.onSceneLoaded.notifyObservers({scene: this._scene, filename: filename});
+
+        this.prepareCamera();
+        this.prepareLighting();
+        this.handleErrors();
+
+        if (this.props.globalState.isDebugLayerEnabled) {
+            this.props.globalState.showDebugLayer();
+        }
+    }
+
+    loadAssetFromUrl() {
+        let assetUrl = this.props.assetUrl!;
+        let rootUrl = Tools.GetFolderPath(assetUrl);
+        let fileName = Tools.GetFilename(assetUrl);
+        SceneLoader.LoadAsync(rootUrl, fileName, this._engine).then((scene) => {
+            if (this._scene) {
+                this._scene.dispose();
+            }
+
+            this._scene = scene;
+
+            this.onSceneLoaded(fileName);
+
+            scene.whenReadyAsync().then(() => {
+                this._engine.runRenderLoop(() => {
+                    scene.render();
+                });
+            });
+        }).catch((reason) => {
+            this.props.globalState.onError.notifyObservers({ message : reason.message});
+            //TODO sceneError({ name: fileName }, null, reason.message || reason);
+        });
+    }
+
+    loadAsset() {
+        if (this.props.assetUrl) {
+            this.loadAssetFromUrl();
+            return;
+        }
+    }
+
+    componentDidMount() {
+        if (!Engine.isSupported()) {
+            return;
+        }
+
+        Engine.ShadersRepository = "/src/Shaders/";
+
+        // This is really important to tell Babylon.js to use decomposeLerp and matrix interpolation
+        Animation.AllowMatricesInterpolation = true;
+    
+        // Setting up some GLTF values
+        GLTFFileLoader.IncrementalLoading = false;
+        SceneLoader.OnPluginActivatedObservable.add((plugin) =>{
+            this._currentPluginName = plugin.name;
+            if (this._currentPluginName === "gltf") {
+                (plugin as GLTFFileLoader).onValidatedObservable.add((results) =>{
+                    if (results.issues.numErrors > 0) {
+                        this.props.globalState.showDebugLayer();
+                    }
+                });
+            }
+        });
+
+        this.initEngine();
+    }
+
+    shouldComponentUpdate(nextProps: IRenderingZoneProps) {
+        if (nextProps.expanded !== this.props.expanded) {
+            setTimeout(() => this._engine.resize());
+            return true;
+        }
+        return false;
+    }
+
+    public render() {
+        return (
+            <div id="canvasZone" className={this.props.expanded ? "expanded" : ""}>
+                <canvas id="renderCanvas" touch-action="none" 
+                    onContextMenu={evt => evt.preventDefault()}></canvas>
+            </div>
+        )
+    }
+}

+ 29 - 0
sandbox/src/globalState.ts

@@ -0,0 +1,29 @@
+import { Observable } from 'babylonjs/Misc/observable';
+import { Scene } from 'babylonjs/scene';
+import { FilesInput } from 'babylonjs/Misc/filesInput';
+
+export class GlobalState {
+    currentScene: Scene;
+    onSceneLoaded = new Observable<{scene: Scene, filename: string}>();
+    onError = new Observable<{scene?: Scene, message?: string}>();
+    onEnvironmentChanged = new Observable<string>();    
+    onRequestClickInterceptor = new Observable<void>();
+    onClickInterceptorClicked = new Observable<void>();
+
+    filesInput: FilesInput;
+    isDebugLayerEnabled = false;
+
+    public showDebugLayer() {
+        this.isDebugLayerEnabled = true;
+        if (this.currentScene) {
+            this.currentScene.debugLayer.show();
+        }    
+    }
+
+    public hideDebugLayer() {
+        this.isDebugLayerEnabled = false;
+        if (this.currentScene) {
+            this.currentScene.debugLayer.hide();
+        }     
+    }
+}

File diff suppressed because it is too large
+ 1 - 0
sandbox/src/img/babylon-identity.svg


File diff suppressed because it is too large
+ 1 - 0
sandbox/src/img/icon-edit.svg


File diff suppressed because it is too large
+ 1 - 0
sandbox/src/img/icon-ibl.svg


+ 1 - 0
sandbox/src/img/icon-open.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:none;}</style></defs><title>Asset 11</title><g id="Layer_2" data-name="Layer 2"><g id="Page_Elements" data-name="Page Elements"><path class="cls-1" d="M26.67,44.77H41.88v1.52H25.15V22H38.4l6.52,6.52v5.64H43.4V29.56H37.31V23.48H26.67ZM38.83,28h3.49l-3.49-3.48Zm8,12.13-1.93-1.92v8H43.4v-8l-1.94,1.92L40.39,39.1l3.77-3.77,3.76,3.77Z"/><rect class="cls-2" width="70" height="70"/></g></g></svg>

File diff suppressed because it is too large
+ 1 - 0
sandbox/src/img/logo-fullscreen.svg


+ 1 - 0
sandbox/src/index.ts

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

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

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

+ 150 - 0
sandbox/src/sandbox.tsx

@@ -0,0 +1,150 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import { GlobalState } from './globalState';
+import { RenderingZone } from './components/renderingZone';
+import { Footer } from './components/footer';
+import { EnvironmentTools } from './tools/environmentTools';
+import { Vector3 } from 'babylonjs/Maths/math.vector';
+
+require("./scss/main.scss");
+var fullScreenLogo = require("./img/logo-fullscreen.svg");
+
+interface ISandboxProps {
+}
+
+export class Sandbox extends React.Component<ISandboxProps, {isFooterVisible: boolean, errorMessage: string}> {
+    private _globalState: GlobalState;
+    private _assetUrl?: string;    
+    private _cameraPosition?: Vector3;
+    private _logoRef: React.RefObject<HTMLImageElement>;    
+    private _dropTextRef: React.RefObject<HTMLDivElement>;
+    private _clickInterceptorRef: React.RefObject<HTMLDivElement>;
+    
+    public constructor(props: ISandboxProps) {
+        super(props);
+        this._globalState = new GlobalState();
+        this._logoRef = React.createRef();
+        this._dropTextRef = React.createRef();
+        this._clickInterceptorRef = React.createRef();
+
+        this.state = {isFooterVisible: true, errorMessage: ""};
+        
+        this.checkUrl();
+
+        EnvironmentTools.HookWithEnvironmentChange(this._globalState);
+
+        // Events
+        this._globalState.onSceneLoaded.add(info => {
+            document.title = "Babylon.js - " + info.filename;
+
+            this._globalState.currentScene = info.scene;
+            if (this._globalState.currentScene.meshes.length === 0 && this._globalState.currentScene.clearColor.r === 1 && this._globalState.currentScene.clearColor.g === 0 && this._globalState.currentScene.clearColor.b === 0) {
+                this._logoRef.current!.className = "";
+            }
+            else {
+                this._logoRef.current!.className = "hidden";
+                this._dropTextRef.current!.className = "hidden";
+            }
+        });
+
+        this._globalState.onError.add(error => {
+            if (error.scene) {
+                this._globalState.showDebugLayer();
+            }
+
+            if (error.message) {
+                this.setState({errorMessage: error.message});
+            }
+        });
+
+        this._globalState.onRequestClickInterceptor.add(() => {
+            let div = this._clickInterceptorRef.current!;
+
+            if (div.classList.contains("hidden")) {
+                div.classList.remove("hidden");
+            } else {
+                div.classList.add("hidden");
+            }
+        });
+
+        // Keyboard
+        window.addEventListener("keydown", (event: KeyboardEvent) =>{
+            // Press space to toggle footer
+            if (event.keyCode === 32 && event.target && (event.target as HTMLElement).nodeName !== "INPUT") {
+                this.setState({isFooterVisible: !this.state.isFooterVisible});
+            }
+        });
+    }
+
+    checkUrl() {
+        // Check URL
+        var indexOf = location.href.indexOf("?");
+        if (indexOf !== -1) {
+            var params = location.href.substr(indexOf + 1).split("&");
+            for (var index = 0; index < params.length; index++) {
+                var param = params[index].split("=");
+                var name = param[0];
+                var value = param[1];
+                switch (name) {
+                    case "assetUrl": {
+                        this._assetUrl = value;
+                        break;
+                    }
+                    case "cameraPosition": {
+                        this._cameraPosition = Vector3.FromArray(value.split(",").map(function(component) { return +component; }));
+                        break;
+                    }
+                    case "kiosk": {
+                        this.state = {isFooterVisible: value === "true" ? false : true, errorMessage: ""};
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    componentDidUpdate() {
+        this._assetUrl = undefined;
+        this._cameraPosition = undefined;
+    }
+
+    public render() {
+
+        return (
+            <div id="root">
+                <p id="droptext" ref={this._dropTextRef}>Drag and drop gltf, glb, obj or babylon files to view them</p>
+                <RenderingZone globalState={this._globalState} 
+                    assetUrl={this._assetUrl} 
+                    cameraPosition={this._cameraPosition} 
+                    expanded={!this.state.isFooterVisible}/>                
+                <div ref={this._clickInterceptorRef} 
+                    onClick={() => this._globalState.onClickInterceptorClicked.notifyObservers()}
+                    className="clickInterceptor hidden"></div>
+                {
+                    this.state.isFooterVisible &&
+                    <Footer globalState={this._globalState} />
+                }
+                <div id="logoContainer">
+                    <img id="logo" src={fullScreenLogo} ref={this._logoRef}/>
+                </div>                      
+                {
+                    this.state.errorMessage &&
+                    <div id="errorZone">
+                        <div className="message">
+                            {this.state.errorMessage}
+                        </div>
+                        <button type="button" className="close" 
+                            onClick={() => this.setState({errorMessage: ""})}
+                            data-dismiss="alert">&times;</button>
+                    </div>                           
+                } 
+            </div>   
+        )
+    }
+
+    public static Show(hostElement: HTMLElement) {
+        const sandBox = React.createElement(Sandbox, {});
+        
+        ReactDOM.render(sandBox, hostElement);
+    }
+}

+ 133 - 0
sandbox/src/scss/footer.scss

@@ -0,0 +1,133 @@
+.footer {
+    position: relative;
+    width: 100%;
+    height: var(--footer-height);
+    margin: 0;
+    padding: 0;
+    background-color:var(--footer-background);
+    font-size: 0;
+    display: grid;
+    grid-template-rows: 100%;
+    grid-template-columns: 201px 1fr 210px;
+    
+    .footerLeft {
+        display: grid;
+        grid-column: 1;
+        grid-row: 1;
+        padding-left: 40px;
+        align-content: center; 
+        overflow: hidden;
+
+        #logoImg {
+            height: var(--footer-height);
+            width: 161px;
+        }
+    }
+
+    .footerRight {
+        display: flex;
+        flex-direction: row-reverse;
+        grid-column: 3;
+        grid-row: 1;
+
+        .button {
+            float: left; /* Float links side by side */
+            width: var(--footer-height);
+            height: var(--footer-height);
+            margin: 0px;
+            padding: 0;
+            transition: all 0.3s ease; /* Add transition for hover effects */
+            display: grid;
+            align-content: center;
+            justify-content: center;
+            cursor: pointer;
+
+            img {
+                width: var(--footer-height);
+                height: var(--footer-height);
+            }
+            
+            &:hover {
+                background-color: var(--button-hover-color);
+            }
+            
+            &:active {
+                background-color: var(--button-hover-background);
+            }
+        }         
+        
+        .dropup-content {
+            position: absolute;
+            bottom: var(--footer-height);
+            right: 0px;     
+            z-index: 100;
+
+            div  {
+                background-color: var(--button-hover-color);
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                font-size: var(--font-size);
+                width: calc(2 * var(--footer-height));
+                color: white;
+                cursor: pointer;
+                height: 40px;
+                box-sizing: border-box;
+                padding: 0;
+                margin: 0;
+                display: grid;
+                align-content: center;
+                justify-content: center;
+            
+                &:hover {
+                    background-color: var(--button-hover-hover); 
+                    transition: all 0.3s ease;
+                }
+                &:active {
+                    background-color: var(--button-hover-background);
+                    transition: all 0.3s ease;
+                }
+            }
+        }    
+        
+        .custom-upload {
+            position: relative;
+            background-position: center right;
+            background-repeat: no-repeat;
+            width: var(--footer-height);
+            height: var(--footer-height);
+            cursor: pointer;
+            display: grid;
+
+            img {
+                grid-row: 1;
+                grid-column: 1;
+                width: var(--footer-height);
+                height: var(--footer-height);
+                pointer-events: none;
+            }
+            
+            &:hover {
+                background-color: var(--button-hover-color);
+            }
+            
+            &:active {
+                background-color: var(--button-hover-background);
+            }
+
+            input[type=file] {
+                grid-row: 1;
+                grid-column: 1;
+                outline:none;
+                position: relative;
+                text-align: right;    
+                -moz-opacity:0 ;
+                opacity: 0;
+                z-index: 2;
+                width:100%;
+                height:100%;
+                filter:alpha(opacity=0);
+            }
+        }
+    }
+}

+ 98 - 0
sandbox/src/scss/main.scss

@@ -0,0 +1,98 @@
+html {
+    --background: #2A2342;
+    --footer-background: #201936;
+    --footer-height: 70px;
+    --button-hover-color: #BB464B;
+    --button-hover-hover: #e0684b;
+    --button-hover-background:  #162D3A;
+    --font-size: 20px;
+}
+
+html, body, #root {
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+    overflow: hidden;
+    font-size: var(--font-size);
+    background: var(--background);
+    font-family: "acumin-pro-condensed";
+    font-weight: normal;    
+}
+
+.hidden {
+    display: none !important;
+}
+
+.clickInterceptor {
+    position: absolute;
+    width: 100%;
+    height: 100%;    
+    z-index: 99;
+    top:0;
+    left:0;
+}
+
+#droptext {
+    position: absolute;
+    text-align: center;
+    color: #fff;
+    height: 50px;
+    width: 100%;
+    bottom: 50px;    
+}
+
+#logoContainer {
+    position: absolute;
+    top:0;
+    left:0;
+    width: 100%;   
+    height: calc(100% - 70px);
+    pointer-events: none;
+}
+
+#logo {
+    position: absolute;
+    width: 20%;
+    height: 20%;
+    top: 40%;
+    left: 40%;
+    pointer-events: none;
+}
+
+#errorZone {
+    position: absolute;
+    width: 50%;
+    left: 25%;
+    bottom: 80px;
+    background-color: #C73228;
+    padding:20px;
+    border-radius: 5px;
+    color:white;
+    display: grid;
+    grid-template-columns: 1fr 32px;
+
+        .message {
+            grid-column: 1;
+            grid-row: 1;
+            align-self: center;
+        }
+
+        button {
+            grid-column: 2;
+            grid-row: 1;
+            padding: 0;
+            cursor: pointer;
+            background: transparent;
+            border: 0;
+            -webkit-appearance: none;
+            color: #000;
+            text-shadow: 0 1px 0 #fff;
+            opacity: .4;
+            font-size: 1.8em;
+
+            &:hover {
+                transform: scale(1.2);
+            }
+        }
+}

+ 26 - 0
sandbox/src/scss/renderingZone.scss

@@ -0,0 +1,26 @@
+#canvasZone {
+    display: block;
+    padding: 0;
+    margin: 0;
+    overflow: hidden;
+    width: 100%;
+    height: calc(100% - var(--footer-height));   
+
+    &.expanded {
+       height: 100%; 
+    }
+}
+
+#renderCanvas {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+    touch-action: none;
+    -ms-touch-action: none;  
+    display: block;
+    border: 0;
+    outline: 0;
+}

+ 57 - 0
sandbox/src/tools/environmentTools.ts

@@ -0,0 +1,57 @@
+import { HDRCubeTexture } from 'babylonjs/Materials/Textures/hdrCubeTexture';
+import { CubeTexture } from 'babylonjs/Materials/Textures/cubeTexture';
+import { Scene } from 'babylonjs/scene';
+import { LocalStorageHelper } from './localStorageHelper';
+import { GlobalState } from '../globalState';
+import { Engine } from 'babylonjs/Engines/engine';
+import { StandardMaterial } from 'babylonjs/Materials/standardMaterial';
+import { PBRMaterial } from 'babylonjs/Materials/PBR/pbrMaterial';
+import { Texture } from 'babylonjs/Materials/Textures/texture';
+
+export class EnvironmentTools {
+    public static SkyboxPath = "";
+    public static Skyboxes = [
+        "https://assets.babylonjs.com/environments/environmentSpecular.env",
+        "https://assets.babylonjs.com/environments/studio.env",
+    ];
+    
+    public static SkyboxesNames = [
+        "Default",
+        "Studio",
+    ];    
+
+    public static LoadSkyboxPathTexture(scene: Scene) {                
+        var defaultSkyboxIndex = LocalStorageHelper.ReadLocalStorageValue("defaultSkyboxId", 0);
+        let path = this.SkyboxPath || this.Skyboxes[defaultSkyboxIndex];
+        if (path.indexOf(".hdr") === (path.length - 4)) {
+            return new HDRCubeTexture(path, scene, 256, false, true, false, true);
+        }
+        return CubeTexture.CreateFromPrefilteredData(path, scene);
+    }
+
+    public static HookWithEnvironmentChange(globalState: GlobalState) {
+        globalState.onEnvironmentChanged.add(option => {
+            this.SkyboxPath = "";
+            let index = EnvironmentTools.SkyboxesNames.indexOf(option);
+
+            if (typeof (Storage) !== "undefined") {
+                localStorage.setItem("defaultSkyboxId", index.toString());
+            }
+
+            var currentScene = Engine.LastCreatedScene!;
+            currentScene.environmentTexture = this.LoadSkyboxPathTexture(currentScene);
+            for (var i = 0; i < currentScene.materials.length; i++) {
+                var material = currentScene.materials[i] as (StandardMaterial | PBRMaterial);
+                if (material.name === "skyBox") {
+                    var reflectionTexture = material.reflectionTexture;
+                    if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
+                        material.reflectionTexture = currentScene.environmentTexture.clone();
+                        if (material.reflectionTexture) {
+                            material.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE;
+                        }
+                    }
+                }
+            }
+        });        
+    }
+}

+ 9 - 0
sandbox/src/tools/localStorageHelper.ts

@@ -0,0 +1,9 @@
+export class LocalStorageHelper {
+    public static ReadLocalStorageValue(key: string, defaultValue: number) {
+        if (typeof (Storage) !== "undefined" && localStorage.getItem(key) !== null) {
+            return parseInt(localStorage.getItem(key)!);
+        }
+    
+        return defaultValue;
+    }
+}

+ 28 - 0
sandbox/tsconfig.json

@@ -0,0 +1,28 @@
+{
+    "extends": "../tsconfigRules",
+    "compilerOptions": {
+        "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-materials/*": [
+                "../../dist/preview release/materialsLibrary/babylonjs.materials.module.d.ts"
+            ],
+            "babylonjs-serializers/*": [
+                "../../dist/preview release/serializers/babylonjs.serializers.module.d.ts"
+            ],
+            "babylonjs/*": [
+                "../../dist/preview release/babylon.module.d.ts"
+            ]
+        }
+    }
+}

+ 46 - 0
sandbox/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: "sandbox",
+    resolve: {
+        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;

+ 2 - 2
src/Engines/thinEngine.ts

@@ -2799,8 +2799,8 @@ export class ThinEngine {
         const extension = forcedExtension ? forcedExtension : (lastDot > -1 ? url.substring(lastDot).toLowerCase() : "");
         let loader: Nullable<IInternalTextureLoader> = null;
 
-        for (let availableLoader of ThinEngine._TextureLoaders) {
-            if (availableLoader.canLoad(extension)) {
+        for (const availableLoader of ThinEngine._TextureLoaders) {
+            if (availableLoader.canLoad(extension, mimeType)) {
                 loader = availableLoader;
                 break;
             }

+ 1 - 1
src/Materials/PBR/pbrSheenConfiguration.ts

@@ -61,7 +61,7 @@ export class PBRSheenConfiguration {
     /**
      * Stores the sheen tint values in a texture.
      * rgb is tint
-     * a is a intensity
+     * a is a intensity or roughness if roughness has been defined
      */
     @serializeAsTexture()
     @expandToProperty("_markAllSubMeshesAsTexturesDirty")

+ 1 - 1
src/Materials/Textures/Loaders/basisTextureLoader.ts

@@ -13,7 +13,7 @@ export class _BasisTextureLoader implements IInternalTextureLoader {
     /**
      * Defines whether the loader supports cascade loading the different faces.
      */
-    public readonly supportCascades = true;
+    public readonly supportCascades = false;
 
     /**
      * This returns if the loader support the current file information.

+ 3 - 2
src/Materials/Textures/Loaders/ktxTextureLoader.ts

@@ -20,11 +20,12 @@ export class _KTXTextureLoader implements IInternalTextureLoader {
     /**
      * This returns if the loader support the current file information.
      * @param extension defines the file extension of the file being loaded
+     * @param mimeType defines the optional mime type of the file being loaded
      * @returns true if the loader can load the specified file
      */
-    public canLoad(extension: string): boolean {
+    public canLoad(extension: string, mimeType?: string): boolean {
         // The ".ktx2" file extension is still up for debate: https://github.com/KhronosGroup/KTX-Specification/issues/18
-        return StringTools.EndsWith(extension, ".ktx") || StringTools.EndsWith(extension, ".ktx2");
+        return StringTools.EndsWith(extension, ".ktx") || StringTools.EndsWith(extension, ".ktx2") || mimeType === "image/ktx" || mimeType === "image/ktx2";
     }
 
     /**

+ 2 - 1
src/Materials/Textures/internalTextureLoader.ts

@@ -13,9 +13,10 @@ export interface IInternalTextureLoader {
     /**
      * This returns if the loader support the current file information.
      * @param extension defines the file extension of the file being loaded
+     * @param mimeType defines the optional mime type of the file being loaded
      * @returns true if the loader can load the specified file
      */
-    canLoad(extension: string): boolean;
+    canLoad(extension: string, mimeType?: string): boolean;
 
     /**
      * Uploads the cube texture data to the WebGL texture. It has already been bound.

+ 2 - 1
src/Misc/fileTools.ts

@@ -197,7 +197,8 @@ export class FileTools {
             img.removeEventListener("error", errorHandler);
 
             if (onError) {
-                onError("Error while trying to load image: " + input, err);
+                const inputText = input.toString();
+                onError("Error while trying to load image: " + (inputText.length < 32 ? inputText : inputText.slice(0, 32) + "..."), err);
             }
 
             if (usingObjectURL && img.src) {

+ 24 - 11
src/Misc/filesInput.ts

@@ -3,6 +3,7 @@ import { Scene } from "../scene";
 import { ISceneLoaderProgressEvent, SceneLoader } from "../Loading/sceneLoader";
 import { Logger } from "../Misc/logger";
 import { FilesInputStore } from "./filesInputStore";
+import { Nullable } from '../types';
 
 /**
  * Class used to help managing file picking and drag'n'drop
@@ -18,17 +19,17 @@ export class FilesInput {
     /**
      * Callback called when a file is processed
      */
-    public onProcessFileCallback: (file: File, name: string, extension: string) => true = () => { return true; };
+    public onProcessFileCallback: (file: File, name: string, extension: string) => boolean = () => { return true; };
 
     private _engine: Engine;
-    private _currentScene: Scene;
-    private _sceneLoadedCallback: (sceneFile: File, scene: Scene) => void;
-    private _progressCallback: (progress: ISceneLoaderProgressEvent) => void;
-    private _additionalRenderLoopLogicCallback: () => void;
-    private _textureLoadingCallback: (remaining: number) => void;
-    private _startingProcessingFilesCallback: (files?: File[]) => void;
-    private _onReloadCallback: (sceneFile: File) => void;
-    private _errorCallback: (sceneFile: File, scene: Scene, message: string) => void;
+    private _currentScene: Nullable<Scene>;
+    private _sceneLoadedCallback: Nullable<(sceneFile: File, scene: Scene) => void>;
+    private _progressCallback: Nullable<(progress: ISceneLoaderProgressEvent) => void>;
+    private _additionalRenderLoopLogicCallback: Nullable<() => void>;
+    private _textureLoadingCallback: Nullable<(remaining: number) => void>;
+    private _startingProcessingFilesCallback: Nullable<(files?: File[]) => void>;
+    private _onReloadCallback: Nullable<(sceneFile: File) => void>;
+    private _errorCallback: Nullable<(sceneFile: File, scene: Nullable<Scene>, message: string) => void>;
     private _elementToMonitor: HTMLElement;
 
     private _sceneFileToLoad: File;
@@ -46,8 +47,14 @@ export class FilesInput {
      * @param onReloadCallback callback called when a reload is requested
      * @param errorCallback callback call if an error occurs
      */
-    constructor(engine: Engine, scene: Scene, sceneLoadedCallback: (sceneFile: File, scene: Scene) => void, progressCallback: (progress: ISceneLoaderProgressEvent) => void, additionalRenderLoopLogicCallback: () => void,
-        textureLoadingCallback: (remaining: number) => void, startingProcessingFilesCallback: (files?: File[]) => void, onReloadCallback: (sceneFile: File) => void, errorCallback: (sceneFile: File, scene: Scene, message: string) => void) {
+    constructor(engine: Engine, scene: Nullable<Scene>,
+        sceneLoadedCallback: Nullable<(sceneFile: File, scene: Scene) => void>,
+        progressCallback: Nullable<(progress: ISceneLoaderProgressEvent) => void>,
+        additionalRenderLoopLogicCallback: Nullable<() => void>,
+        textureLoadingCallback: Nullable<(remaining: number) => void>,
+        startingProcessingFilesCallback: Nullable<(files?: File[]) => void>,
+        onReloadCallback: Nullable<(sceneFile: File) => void>,
+        errorCallback: Nullable<(sceneFile: File, scene: Nullable<Scene>, message: string) => void>) {
         this._engine = engine;
         this._currentScene = scene;
 
@@ -82,6 +89,11 @@ export class FilesInput {
         }
     }
 
+    /** Gets the current list of files to load */
+    public get filesToLoad() {
+        return this._filesToLoad;
+    }
+
     /**
      * Release all associated resources
      */
@@ -289,6 +301,7 @@ export class FilesInput {
                     });
                 });
             }).catch((error) => {
+                this._engine.hideLoadingUI();
                 if (this._errorCallback) {
                     this._errorCallback(this._sceneFileToLoad, this._currentScene, error.message);
                 }

+ 6 - 2
src/PostProcesses/RenderPipeline/Pipelines/defaultRenderingPipeline.ts

@@ -334,9 +334,8 @@ export class DefaultRenderingPipeline extends PostProcessRenderPipeline implemen
         if (this._imageProcessingEnabled === enabled) {
             return;
         }
-        this._imageProcessingEnabled = enabled;
 
-        this._buildPipeline();
+        this._scene.imageProcessingConfiguration.isEnabled = enabled;
     }
 
     @serialize()
@@ -460,6 +459,11 @@ export class DefaultRenderingPipeline extends PostProcessRenderPipeline implemen
 
         this._imageProcessingConfigurationObserver = this._scene.imageProcessingConfiguration.onUpdateParameters.add(() => {
             this.bloom._downscale._exposure = this._scene.imageProcessingConfiguration.exposure;
+
+            if (this.imageProcessingEnabled !== this._scene.imageProcessingConfiguration.isEnabled) {
+                this._imageProcessingEnabled = this._scene.imageProcessingConfiguration.isEnabled;
+                this._buildPipeline();
+            }
         });
 
         this._buildPipeline();

+ 10 - 1
src/Shaders/ShadersInclude/pbrBlockSheen.fx

@@ -75,7 +75,6 @@
         float sheenIntensity = vSheenColor.a;
 
         #ifdef SHEEN_TEXTURE
-            sheenIntensity *= sheenMapData.a;
             #if DEBUGMODE > 0
                 outParams.sheenMapData = sheenMapData;
             #endif
@@ -86,6 +85,10 @@
             vec3 sheenColor = baseColor.rgb*(1.0-sheenFactor);
             float sheenRoughness = sheenIntensity;
             outParams.surfaceAlbedo = surfaceAlbedo * sheenFactor;
+
+            #ifdef SHEEN_TEXTURE
+                sheenIntensity *= sheenMapData.a;
+            #endif
         #else
             vec3 sheenColor = vSheenColor.rgb;
             #ifdef SHEEN_TEXTURE
@@ -94,8 +97,14 @@
             
             #ifdef SHEEN_ROUGHNESS
                 float sheenRoughness = vSheenRoughness;
+                #ifdef SHEEN_TEXTURE
+                    sheenRoughness *= sheenMapData.a;
+                #endif
             #else
                 float sheenRoughness = roughness;
+                #ifdef SHEEN_TEXTURE
+                    sheenIntensity *= sheenMapData.a;
+                #endif
             #endif
 
             // Sheen Lobe Layering.