Jelajahi Sumber

animation support for sandbox

David `Deltakosh` Catuhe 5 tahun lalu
induk
melakukan
f31c74ccbc

+ 8 - 2
inspector/src/components/actionTabs/lineContainerComponent.tsx

@@ -26,11 +26,15 @@ export class LineContainerComponent extends React.Component<ILineContainerCompon
     }
 
     componentDidMount() {
-        if (this.props.globalState && this.props.globalState.selectedLineContainerTitles.length === 0) {
+        if (!this.props.globalState) {
             return;
         }
 
-        if (this.props.globalState && this.props.globalState.selectedLineContainerTitles.indexOf(this.props.title) > -1) {
+        if (this.props.globalState.selectedLineContainerTitles.length === 0 && this.props.globalState.selectedLineContainerTitlesNoFocus.length === 0) {
+            return;
+        }
+
+        if (this.props.globalState.selectedLineContainerTitles.indexOf(this.props.title) > -1) {
             setTimeout(() => {
                 this.props.globalState!.selectedLineContainerTitles = [];
             });
@@ -40,6 +44,8 @@ export class LineContainerComponent extends React.Component<ILineContainerCompon
             window.setTimeout(() => {
                 this.setState({ isHighlighted: false });
             }, 5000);
+        } else if (this.props.globalState.selectedLineContainerTitlesNoFocus.indexOf(this.props.title) > -1) {
+            this.setState({ isExpanded: true, isHighlighted: false });
         } else {
             this.setState({isExpanded: false});
         }

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

@@ -32,7 +32,8 @@ export class GlobalState {
     public glTFLoaderDefaults: { [key: string]: any } = { "validate": true };
 
     public blockMutationUpdates = false;
-    public selectedLineContainerTitles:Array<string> = [];
+    public selectedLineContainerTitles:Array<string> = [];    
+    public selectedLineContainerTitlesNoFocus:Array<string> = [];
 
     public recorder = new ReplayRecorder();
 
@@ -111,7 +112,7 @@ export class GlobalState {
             this.onValidationResultsUpdatedObservable.notifyObservers(results);
 
             if (results.issues.numErrors || results.issues.numWarnings) {
-                Inspector.MarkLineContainerTitleForHighlighting("GLTF VALIDATION");
+                this.selectedLineContainerTitlesNoFocus.push("GLTF VALIDATION");
                 this.onTabChangedObservable.notifyObservers(3);
             }
         });

+ 2 - 0
sandbox/index.css

@@ -212,6 +212,8 @@ a:visited {
     background-color: var(--footer-background);    
     grid-column: 2;
     grid-row: 1;
+    padding: 0px;
+    margin: 0px;
 }
 
 .row {

+ 162 - 0
sandbox/src/components/animationBar.tsx

@@ -0,0 +1,162 @@
+import * as React from "react";
+import { GlobalState } from '../globalState';
+import { DropUpButton } from './dropUpButton';
+import { Scene } from 'babylonjs/scene';
+import { Observer } from 'babylonjs/Misc/observable';
+import { Nullable } from 'babylonjs/types';
+import { AnimationGroup } from 'babylonjs/Animations/animationGroup';
+
+var iconPlay = require("../img/icon-play.svg");
+var iconPause = require("../img/icon-pause.svg");
+
+require("../scss/animationBar.scss");
+
+interface IAnimationBarProps {
+    globalState: GlobalState;
+    enabled: boolean;
+}
+
+export class AnimationBar extends React.Component<IAnimationBarProps, {groupIndex: number}> {
+    private _currentScene: Scene;
+    private _sliderSyncObserver: Nullable<Observer<Scene>>;
+    private _currentGroup: Nullable<AnimationGroup>;
+    private _sliderRef: React.RefObject<HTMLInputElement>;
+    private _currentPlayingState: boolean;
+
+    public constructor(props: IAnimationBarProps) {    
+        super(props);
+
+        this._sliderRef = React.createRef();
+
+        this.state = {groupIndex: 0};
+
+        props.globalState.onSceneLoaded.add(info => {
+            this.registerBeforeRender(info.scene);
+        });
+
+        if (this.props.globalState.currentScene) {
+            this.registerBeforeRender(this.props.globalState.currentScene); 
+        }
+    }
+
+    getCurrentPosition() {
+        if (!this._currentGroup) {
+            return "0";
+
+        }
+        let targetedAnimations = this._currentGroup.targetedAnimations;
+        if (targetedAnimations.length > 0) {
+            let runtimeAnimations = this._currentGroup.targetedAnimations[0].animation.runtimeAnimations;
+            if (runtimeAnimations.length > 0) {
+                return runtimeAnimations[0].currentFrame.toString();
+            }
+        }
+
+        return "0";
+    }
+
+    registerBeforeRender(newScene: Scene) {
+        if (this._currentScene) {
+            this._currentScene.onBeforeRenderObservable.remove(this._sliderSyncObserver);
+        }
+
+        this._currentScene = newScene;
+        this._sliderSyncObserver = this._currentScene.onBeforeRenderObservable.add(() => {
+            if (this._currentGroup && this._sliderRef.current) {
+                this._sliderRef.current.value = this.getCurrentPosition();
+
+                if (this._currentPlayingState !== this._currentGroup.isPlaying) {
+                    this.forceUpdate();
+                }
+            }
+        });
+    }
+
+    pause() {
+        if (!this._currentGroup) {
+            return;
+        }
+
+        this._currentGroup.pause();
+        this.forceUpdate();
+    }
+
+    play() {
+        if (!this._currentGroup) {
+            return;
+        }
+
+        this._currentGroup.play();
+        this.forceUpdate();
+    }
+
+    sliderInput(evt: React.FormEvent<HTMLInputElement>) {
+        if (!this._currentGroup) {
+            return;
+        }
+
+        let value = parseFloat((evt.target as HTMLInputElement).value);
+
+        if (!this._currentGroup.isPlaying) {
+            this._currentGroup.play(true);
+            this._currentGroup.goToFrame(value);
+            this._currentGroup.pause();
+        } else {
+            this._currentGroup.goToFrame(value);
+        }
+    }
+
+    public render() {
+        if (!this.props.enabled) {
+            this._currentGroup = null;
+            return null;
+        }
+        let scene = this.props.globalState.currentScene;
+        
+        if (scene.animationGroups.length === 0) {
+            this._currentGroup = null;
+            return null;
+        }
+        
+        let groupNames = scene.animationGroups.map(g => g.name);
+
+        this._currentGroup = scene.animationGroups[this.state.groupIndex];
+        this._currentPlayingState = this._currentGroup.isPlaying;
+
+        return (
+            <div className="animationBar">
+                <div className="row">
+                    <button id="playBtn">
+                        {
+                            this._currentGroup.isPlaying &&
+                            <img id="pauseImg" src={iconPause} onClick={() => this.pause()}/>
+                        }
+                        {
+                            !this._currentGroup.isPlaying &&
+                            <img id="playImg" src={iconPlay} onClick={() => this.play()}/>
+                        }
+                    </button>
+                    <input ref={this._sliderRef} className="slider" type="range" 
+                        onInput={evt => this.sliderInput(evt)}
+                        min={this._currentGroup.from} 
+                        max={this._currentGroup.to} 
+                        onChange={() => {}}
+                        value={this.getCurrentPosition()} step="any"></input>
+                </div>
+                <DropUpButton globalState={this.props.globalState} 
+                                    label="Active animation group"
+                                    options={groupNames}
+                                    selectedOption={this._currentGroup.name}
+                                    onOptionPicked={option => {
+                                        this._currentGroup!.stop();
+
+                                        let newIndex = groupNames.indexOf(option);
+                                        this.setState({groupIndex: newIndex});
+
+                                        scene.animationGroups[newIndex].play(true);
+                                    }}
+                                    enabled={true}/>
+            </div>            
+        )
+    }
+}

+ 30 - 8
sandbox/src/components/dropUpButton.tsx

@@ -3,13 +3,16 @@ import { GlobalState } from '../globalState';
 import { Nullable } from 'babylonjs/types';
 import { Observer } from 'babylonjs/Misc/observable';
 
+var iconUp = require("../img/icon-up.svg");
+var iconDown = require("../img/icon-down.svg");
 
 interface IDropUpButtonProps {
     globalState: GlobalState;
     enabled: boolean;
-    icon: any;
+    icon?: any;
     label: string;
     options: string[];
+    selectedOption?: string;
     onOptionPicked: (option: string) => void;
 }
 
@@ -22,7 +25,7 @@ export class DropUpButton extends React.Component<IDropUpButtonProps, {isOpen: b
         this.state = {isOpen: false};
 
         this._onClickInterceptorClickedObserver = props.globalState.onClickInterceptorClicked.add(() => {
-            this.switchDropUp();
+            this.setState({isOpen: false});
         });
     }
 
@@ -46,13 +49,32 @@ export class DropUpButton extends React.Component<IDropUpButtonProps, {isOpen: b
         }
 
         return (
-            <>
-                <div className="button" onClick={() => this.switchDropUp()}>
-                    <img src={this.props.icon} alt={this.props.label} title={this.props.label}  />
-                </div>
+            <div className="dropup">
+                {
+                    this.props.icon &&
+                    <div className={"button" + (this.state.isOpen ? " active" : "")} onClick={() => this.switchDropUp()}>
+                        <img src={this.props.icon} alt={this.props.label} title={this.props.label}  />
+                    </div>
+                }
+                {
+                    this.props.selectedOption &&
+                    <div className={"button long" + (this.state.isOpen ? " active" : "")} onClick={() => this.switchDropUp()}> 
+                        {
+                            this.state.isOpen &&
+                            <img className="button-icon" src={iconDown} alt="Close the list" title="Close the list"  />
+                        }            
+                        {
+                            !this.state.isOpen &&
+                            <img className="button-icon" src={iconUp} alt="Open the list" title="Open the list"  />
+                        }           
+                        <div className="button-text" title={this.props.selectedOption}>
+                            {this.props.selectedOption}
+                        </div>                           
+                    </div>
+                }
                 {
                     this.state.isOpen &&
-                    <div className="dropup-content">
+                    <div className={"dropup-content" + (this.props.selectedOption ? " long-mode" : "")}>
                     {
                         this.props.options.map(o => {
                             return(
@@ -64,7 +86,7 @@ export class DropUpButton extends React.Component<IDropUpButtonProps, {isOpen: b
                     }
                     </div>
                 }
-            </>
+            </div>
         )
     }
 }

+ 4 - 2
sandbox/src/components/footer.tsx

@@ -4,6 +4,7 @@ import { FooterButton } from './footerButton';
 import { DropUpButton } from './dropUpButton';
 import { EnvironmentTools } from '../tools/environmentTools';
 import { FooterFileButton } from './footerFileButton';
+import { AnimationBar } from './animationBar';
 
 require("../scss/footer.scss");
 var babylonIdentity = require("../img/babylon-identity.svg");
@@ -16,8 +17,7 @@ interface IFooterProps {
 }
 
 export class Footer extends React.Component<IFooterProps> {
-    
-        
+            
     public constructor(props: IFooterProps) {    
         super(props);
         props.globalState.onSceneLoaded.add(info => {
@@ -42,6 +42,8 @@ export class Footer extends React.Component<IFooterProps> {
                 <div className="footerLeft">
                     <img id="logoImg" src={babylonIdentity}/>
                 </div>
+                <AnimationBar globalState={this.props.globalState} 
+                                enabled={!!this.props.globalState.currentScene}/>
                 <div className="footerRight">
                     <FooterFileButton globalState={this.props.globalState} 
                                 enabled={true}

+ 1 - 0
sandbox/src/img/icon-down.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>DownArrowIcon</title><g id="Layer_2" data-name="Layer 2"><g id="Redlines"><path class="cls-1" d="M23.14,30.07l1.42-1.42L34.85,38.94,45.14,28.65l1.41,1.42L34.85,41.78Z"/><rect class="cls-2" width="70" height="70"/></g></g></svg>

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

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70"><defs><style>.cls-1{fill:none;}.cls-2{fill:#fff;}</style></defs><title>PauseIcon</title><g id="Layer_2" data-name="Layer 2"><g id="Redlines"><rect class="cls-1" width="70" height="70"/><path class="cls-2" d="M28.83,44.37v-20h2v20Zm10-20h2v20h-2Z"/></g></g></svg>

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

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70"><defs><style>.cls-1{fill:none;}.cls-2{fill:#fff;}</style></defs><title>PlayIcon</title><g id="Layer_2" data-name="Layer 2"><g id="Redlines"><rect class="cls-1" width="70" height="70"/><path class="cls-2" d="M28.83,25.13l16,10-16,10Zm2,3.61V41.52l10.22-6.39Z"/></g></g></svg>

+ 1 - 0
sandbox/src/img/icon-up.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>UpArrowIcon</title><g id="Layer_2" data-name="Layer 2"><g id="Redlines"><path class="cls-1" d="M34.85,29.78,24.56,40.06l-1.42-1.41L34.85,26.94l11.7,11.71-1.42,1.41Z"/><rect class="cls-2" width="70" height="70"/></g></g></svg>

+ 4 - 1
sandbox/src/sandbox.tsx

@@ -118,7 +118,10 @@ export class Sandbox extends React.Component<ISandboxProps, {isFooterVisible: bo
                     cameraPosition={this._cameraPosition} 
                     expanded={!this.state.isFooterVisible}/>                
                 <div ref={this._clickInterceptorRef} 
-                    onClick={() => this._globalState.onClickInterceptorClicked.notifyObservers()}
+                    onClick={() => {
+                        this._globalState.onClickInterceptorClicked.notifyObservers();
+                        this._clickInterceptorRef.current!.classList.add("hidden");
+                    }}
                     className="clickInterceptor hidden"></div>
                 {
                     this.state.isFooterVisible &&

+ 129 - 0
sandbox/src/scss/animationBar.scss

@@ -0,0 +1,129 @@
+.animationBar {
+    align-items: center;
+    color: white;
+    min-height: 30px;
+    height: var(--footer-height);
+    background-color: var(--footer-background);    
+    grid-column: 2;
+    grid-row: 1;
+    margin-left: 10px;
+    display: flex;
+
+    * {        
+        padding: 0px;
+        margin: 0px;
+    }
+
+    .row {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        flex-grow: 10;
+        align-items: center
+    }
+
+    #playBtn {    
+        display: flex;
+        align-items: center;
+        height: var(--footer-height);
+        width: var(--footer-height);
+        border: none;
+        background-color: inherit;
+        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);
+        }
+    
+        &:focus {
+            outline: none !important;
+            border: none;
+        }    
+    }
+
+    .slider {
+        -webkit-appearance: none;
+        cursor: pointer;
+        width: 100%;
+        max-width: 820px;
+        height: var(--footer-height);
+        outline: none;
+        margin-left: 20px;
+        margin-right: 10px;
+        background-color: transparent;
+    }
+
+    /*Chrome -webkit */
+    .slider::-webkit-slider-thumb {
+        -webkit-appearance: none;
+        width: 20px;
+        height: 20px;
+        border: 2px solid white;
+        border-radius: 50%;
+        background: var(--footer-background);
+        margin-top: -10px;
+    }
+    .slider::-webkit-slider-runnable-track {
+        height: 2px;
+        -webkit-appearance: none;
+        background-color: white;
+    }
+
+
+    /** FireFox -moz */
+    .slider::-moz-range-progress {
+    background-color: white;
+    height: 2px; 
+    }
+    .slider::-moz-range-thumb{
+        width: 20px;
+        height: 20px;
+        border: 2px solid white;
+        border-radius: 50%;
+        background: var(--footer-background);
+    }
+    .slider::-moz-range-track {
+        background: white;
+        height: 2px;
+    }
+
+    /** IE -ms */
+    .slider::-ms-track {
+        height: 2px;
+        
+        /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */
+        background: transparent;
+        
+        /*leave room for the larger thumb to overflow with a transparent border */
+        border-color: transparent;
+        border-width: 10px 0;
+
+        /*remove default tick marks*/
+        color: transparent;
+    }
+    .slider::-ms-fill-lower {
+        background: white;
+        border-radius: 5px;
+    }
+    .slider::-ms-fill-upper {
+        background: white;
+        border-radius: 5px;
+    }
+    .slider::-ms-thumb {
+        width: 16px;
+        height: 16px;
+        border: 2px solid white;
+        border-radius: 50%;
+        background: var(--footer-background);
+        margin-top: 0px;
+    }
+}

+ 66 - 32
sandbox/src/scss/footer.scss

@@ -24,43 +24,70 @@
         }
     }
 
-    .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;
 
-        .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;
+        &.long {
+            width: 200px;
+            grid-template-columns: var(--footer-height) calc(200px - var(--footer-height));
 
-            img {
-                width: var(--footer-height);
-                height: var(--footer-height);
-            }
-            
-            &:hover {
-                background-color: var(--button-hover-color);
+            .button-icon {
+                grid-row: 1;
+                grid-column: 1;
             }
-            
-            &:active {
-                background-color: var(--button-hover-background);
+
+            .button-text {
+                grid-row: 1;
+                grid-column: 2;
+                align-self: center;
+                justify-self: left;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                font-size: var(--font-size)
             }
-        }         
+        }
+
+        &.active {
+            background-color: var(--button-hover-color);
+        }
+
+        img {
+            width: var(--footer-height);
+            height: var(--footer-height);
+        }
         
+        &:hover {
+            background-color: var(--button-hover-color);
+        }
+        
+        &:active {
+            background-color: var(--button-hover-background);
+        }
+    }   
+
+    .dropup {
+        position: relative;        
+
         .dropup-content {
             position: absolute;
             bottom: var(--footer-height);
-            right: 0px;     
-            z-index: 100;
+            left: 0px;     
+            z-index: 100;            
+            width: calc(2 * var(--footer-height));
+
+            &.long-mode {
+                width: 200px;
+            }
 
             div  {
                 background-color: var(--button-hover-color);
@@ -68,7 +95,7 @@
                 text-overflow: ellipsis;
                 white-space: nowrap;
                 font-size: var(--font-size);
-                width: calc(2 * var(--footer-height));
+                width: 100%;
                 color: white;
                 cursor: pointer;
                 height: 40px;
@@ -88,8 +115,15 @@
                     transition: all 0.3s ease;
                 }
             }
-        }    
-        
+        }     
+    }
+
+    .footerRight {
+        display: flex;
+        flex-direction: row-reverse;
+        grid-column: 3;
+        grid-row: 1;
+                     
         .custom-upload {
             position: relative;
             background-position: center right;

+ 1 - 1
sandbox/src/tools/environmentTools.ts

@@ -21,7 +21,7 @@ export class EnvironmentTools {
     ];    
 
     public static LoadSkyboxPathTexture(scene: Scene) {                
-        var defaultSkyboxIndex = LocalStorageHelper.ReadLocalStorageValue("defaultSkyboxId", 0);
+        var defaultSkyboxIndex = Math.max(0, 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);