Browse Source

Merge pull request #8669 from toledoal/canvas-drag

Canvas Drag Issues
sebavan 5 years ago
parent
commit
d3cc2cd4ad

+ 4 - 4
.prettierrc

@@ -1,5 +1,5 @@
 {
-    "trailingComma": "es5",
-    "tabWidth": 4,
-    "printWidth": 300
-  }
+  "trailingComma": "es5",
+  "tabWidth": 4,
+  "printWidth": 300
+}

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

@@ -9,7 +9,7 @@
 - Reflection probes can now be used to give accurate shading with PBR ([CraigFeldpsar](https://github.com/craigfeldspar) and ([Sebavan](https://github.com/sebavan/)))
 - Added SubSurfaceScattering on PBR materials ([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))
 

+ 236 - 381
inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx

@@ -1,425 +1,280 @@
-import * as React from 'react';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-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';
-import { Color3, Color4 } from 'babylonjs/Maths/math.color';
-import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-import { IAnimationKey } from 'babylonjs/Animations/animationKey';
+import * as React from "react";
+import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
+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 { Color3, Color4 } from "babylonjs/Maths/math.color";
+import { IAnimatable } from "babylonjs/Animations/animatable.interface";
+import { IAnimationKey } from "babylonjs/Animations/animationKey";
 
 interface IAddAnimationProps {
-  isOpen: boolean;
-  close: () => void;
-  entity: IAnimatable;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-  setNotificationMessage: (message: string) => void;
-  finishedUpdate: () => void;
-  addedNewAnimation: () => void;
-  fps: number;
-  selectedToUpdate?: Animation | undefined;
+    isOpen: boolean;
+    close: () => void;
+    entity: IAnimatable;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    setNotificationMessage: (message: string) => void;
+    finishedUpdate: () => void;
+    addedNewAnimation: () => void;
+    fps: number;
+    selectedToUpdate?: Animation | undefined;
 }
 
 export class AddAnimation extends React.Component<
-  IAddAnimationProps,
-  {
-    animationName: string;
-    animationTargetProperty: string;
-    animationType: number;
-    loopMode: number;
-    animationTargetPath: string;
-    isUpdating: boolean;
-  }
+    IAddAnimationProps,
+    {
+        animationName: string;
+        animationTargetProperty: string;
+        animationType: number;
+        loopMode: number;
+        animationTargetPath: string;
+        isUpdating: boolean;
+    }
 > {
-  constructor(props: IAddAnimationProps) {
-    super(props);
-    this.state = this.setInitialState(this.props.selectedToUpdate);
-  }
+    constructor(props: IAddAnimationProps) {
+        super(props);
+        this.state = this.setInitialState(this.props.selectedToUpdate);
+    }
 
-  setInitialState(editingAnimation?: Animation) {
-    return {
-      animationName: editingAnimation ? editingAnimation.name : '',
-      animationTargetPath: '',
-      animationType: editingAnimation
-        ? editingAnimation.dataType
-        : Animation.ANIMATIONTYPE_FLOAT,
-      loopMode: editingAnimation
-        ? editingAnimation.loopMode ?? Animation.ANIMATIONLOOPMODE_CYCLE
-        : Animation.ANIMATIONLOOPMODE_CYCLE,
-      animationTargetProperty: editingAnimation
-        ? editingAnimation.targetProperty
-        : '',
-      isUpdating: editingAnimation ? true : false,
-    };
-  }
+    setInitialState(editingAnimation?: Animation) {
+        return {
+            animationName: editingAnimation ? editingAnimation.name : "",
+            animationTargetPath: "",
+            animationType: editingAnimation ? editingAnimation.dataType : Animation.ANIMATIONTYPE_FLOAT,
+            loopMode: editingAnimation ? editingAnimation.loopMode ?? Animation.ANIMATIONLOOPMODE_CYCLE : Animation.ANIMATIONLOOPMODE_CYCLE,
+            animationTargetProperty: editingAnimation ? editingAnimation.targetProperty : "",
+            isUpdating: editingAnimation ? true : false,
+        };
+    }
 
-  componentWillReceiveProps(nextProps: IAddAnimationProps) {
-    if (
-      nextProps.selectedToUpdate !== undefined &&
-      nextProps.selectedToUpdate !== this.props.selectedToUpdate
-    ) {
-      this.setState(this.setInitialState(nextProps.selectedToUpdate));
-    } else {
-      if (nextProps.isOpen === true && nextProps.isOpen !== this.props.isOpen)
-        this.setState(this.setInitialState());
+    componentWillReceiveProps(nextProps: IAddAnimationProps) {
+        if (nextProps.selectedToUpdate !== undefined && nextProps.selectedToUpdate !== this.props.selectedToUpdate) {
+            this.setState(this.setInitialState(nextProps.selectedToUpdate));
+        } else {
+            if (nextProps.isOpen === true && nextProps.isOpen !== this.props.isOpen) {
+                this.setState(this.setInitialState());
+            }
+        }
     }
-  }
 
-  updateAnimation() {
-    if (this.props.selectedToUpdate !== undefined) {
-      const oldNameValue = this.props.selectedToUpdate.name;
-      this.props.selectedToUpdate.name = this.state.animationName;
-      this.raiseOnPropertyUpdated(
-        oldNameValue,
-        this.state.animationName,
-        'name'
-      );
+    updateAnimation() {
+        if (this.props.selectedToUpdate !== undefined) {
+            const oldNameValue = this.props.selectedToUpdate.name;
+            this.props.selectedToUpdate.name = this.state.animationName;
+            this.raiseOnPropertyUpdated(oldNameValue, this.state.animationName, "name");
 
-      const oldLoopModeValue = this.props.selectedToUpdate.loopMode;
-      this.props.selectedToUpdate.loopMode = this.state.loopMode;
-      this.raiseOnPropertyUpdated(
-        oldLoopModeValue,
-        this.state.loopMode,
-        'loopMode'
-      );
+            const oldLoopModeValue = this.props.selectedToUpdate.loopMode;
+            this.props.selectedToUpdate.loopMode = this.state.loopMode;
+            this.raiseOnPropertyUpdated(oldLoopModeValue, this.state.loopMode, "loopMode");
 
-      const oldTargetPropertyValue = this.props.selectedToUpdate.targetProperty;
-      this.props.selectedToUpdate.targetProperty = this.state.animationTargetProperty;
-      this.raiseOnPropertyUpdated(
-        oldTargetPropertyValue,
-        this.state.animationTargetProperty,
-        'targetProperty'
-      );
+            const oldTargetPropertyValue = this.props.selectedToUpdate.targetProperty;
+            this.props.selectedToUpdate.targetProperty = this.state.animationTargetProperty;
+            this.raiseOnPropertyUpdated(oldTargetPropertyValue, this.state.animationTargetProperty, "targetProperty");
 
-      this.props.finishedUpdate();
+            this.props.finishedUpdate();
+        }
     }
-  }
 
-  getTypeAsString(type: number) {
-    switch (type) {
-      case Animation.ANIMATIONTYPE_FLOAT:
-        return 'Float';
-      case Animation.ANIMATIONTYPE_QUATERNION:
-        return 'Quaternion';
-      case Animation.ANIMATIONTYPE_VECTOR3:
-        return 'Vector3';
-      case Animation.ANIMATIONTYPE_VECTOR2:
-        return 'Vector2';
-      case Animation.ANIMATIONTYPE_SIZE:
-        return 'Size';
-      case Animation.ANIMATIONTYPE_COLOR3:
-        return 'Color3';
-      case Animation.ANIMATIONTYPE_COLOR4:
-        return 'Color4';
-      default:
-        return 'Float';
+    getTypeAsString(type: number) {
+        switch (type) {
+            case Animation.ANIMATIONTYPE_FLOAT:
+                return "Float";
+            case Animation.ANIMATIONTYPE_QUATERNION:
+                return "Quaternion";
+            case Animation.ANIMATIONTYPE_VECTOR3:
+                return "Vector3";
+            case Animation.ANIMATIONTYPE_VECTOR2:
+                return "Vector2";
+            case Animation.ANIMATIONTYPE_SIZE:
+                return "Size";
+            case Animation.ANIMATIONTYPE_COLOR3:
+                return "Color3";
+            case Animation.ANIMATIONTYPE_COLOR4:
+                return "Color4";
+            default:
+                return "Float";
+        }
     }
-  }
 
-  addAnimation() {
-    if (
-      this.state.animationName != '' &&
-      this.state.animationTargetProperty != ''
-    ) {
-      let matchTypeTargetProperty = this.state.animationTargetProperty.split(
-        '.'
-      );
-      let animationDataType = this.state.animationType;
+    addAnimation() {
+        if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
+            let matchTypeTargetProperty = this.state.animationTargetProperty.split(".");
+            let animationDataType = this.state.animationType;
 
-      let matched = false;
+            let matched = false;
 
-      if (matchTypeTargetProperty.length === 1) {
-        let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
+            if (matchTypeTargetProperty.length === 1) {
+                let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
 
-        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;
-          }
-        } 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);
-          }
-        }
-      }
+                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;
+                    }
+                } 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);
+                    }
+                }
+            }
 
-      if (matched) {
-        let startValue;
-        let outTangent;
-
-        // Default start and end values for new animations
-        switch (animationDataType) {
-          case Animation.ANIMATIONTYPE_FLOAT:
-            startValue = 1;
-            outTangent = 0;
-            break;
-          case Animation.ANIMATIONTYPE_VECTOR2:
-            startValue = new Vector2(1, 1);
-            outTangent = Vector2.Zero();
-            break;
-          case Animation.ANIMATIONTYPE_VECTOR3:
-            startValue = new Vector3(1, 1, 1);
-            outTangent = Vector3.Zero();
-            break;
-          case Animation.ANIMATIONTYPE_QUATERNION:
-            startValue = new Quaternion(1, 1, 1, 1);
-            outTangent = Quaternion.Zero();
-            break;
-          case Animation.ANIMATIONTYPE_COLOR3:
-            startValue = new Color3(1, 1, 1);
-            outTangent = new Color3(0, 0, 0);
-            break;
-          case Animation.ANIMATIONTYPE_COLOR4:
-            startValue = new Color4(1, 1, 1, 1);
-            outTangent = new Color4(0, 0, 0, 0);
-            break;
-          case Animation.ANIMATIONTYPE_SIZE:
-            startValue = new Size(1, 1);
-            outTangent = Size.Zero();
-            break;
-        }
+            if (matched) {
+                let alreadyAnimatedProperty = (this.props.entity as IAnimatable).animations?.find((anim) => anim.targetProperty === this.state.animationTargetProperty, this);
 
-        let alreadyAnimatedProperty = (this.props
-          .entity as IAnimatable).animations?.find(
-          (anim) => anim.targetProperty === this.state.animationTargetProperty,
-          this
-        );
-
-        let alreadyAnimationName = (this.props
-          .entity as IAnimatable).animations?.find(
-          (anim) => anim.name === this.state.animationName,
-          this
-        );
+                let alreadyAnimationName = (this.props.entity as IAnimatable).animations?.find((anim) => anim.name === this.state.animationName, 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 animation = new Animation(
-            this.state.animationName,
-            this.state.animationTargetProperty,
-            this.props.fps,
-            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, this.props.fps, animationDataType);
 
-          // Start with two keyframes
-          var keys: IAnimationKey[] = [];
-          keys.push({
-            frame: 0,
-            value: startValue,
-            outTangent: outTangent,
-          });
+                    // Start with two keyframes
+                    var keys: IAnimationKey[] = [];
 
-          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.addedNewAnimation();
-            //Cleaning form fields
-            this.setState({
-              animationName: '',
-              animationTargetPath: '',
-              animationType: Animation.ANIMATIONTYPE_FLOAT,
-              loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
-              animationTargetProperty: '',
-            });
-          }
+                    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.addedNewAnimation();
+                        //Cleaning form fields
+                        this.setState({
+                            animationName: "",
+                            animationTargetPath: "",
+                            animationType: animationDataType,
+                            loopMode: this.state.loopMode,
+                            animationTargetProperty: "",
+                        });
+                    }
+                }
+            } else {
+                this.props.setNotificationMessage(`The property "${this.state.animationTargetProperty}" is not a "${this.getTypeAsString(this.state.animationType)}" type`);
+            }
+        } else {
+            this.props.setNotificationMessage(`You need to provide a name and target property.`);
         }
-      } else {
-        this.props.setNotificationMessage(
-          `The property "${
-            this.state.animationTargetProperty
-          }" is not a "${this.getTypeAsString(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;
+    raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.entity,
+            property: "animations",
+            value: newValue,
+            initialValue: previousValue,
+        });
     }
 
-    this.props.onPropertyChangedObservable.notifyObservers({
-      object: this.props.entity,
-      property: 'animations',
-      value: newValue,
-      initialValue: previousValue,
-    });
-  }
+    raiseOnPropertyUpdated(newValue: string | number | undefined, previousValue: string | number, property: string) {
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
 
-  raiseOnPropertyUpdated(
-    newValue: string | number | undefined,
-    previousValue: string | number,
-    property: string
-  ) {
-    if (!this.props.onPropertyChangedObservable) {
-      return;
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.selectedToUpdate,
+            property: property,
+            value: newValue,
+            initialValue: previousValue,
+        });
     }
 
-    this.props.onPropertyChangedObservable.notifyObservers({
-      object: this.props.selectedToUpdate,
-      property: property,
-      value: newValue,
-      initialValue: previousValue,
-    });
-  }
-
-  handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
-    event.preventDefault();
-    this.setState({ animationName: event.target.value.trim() });
-  }
+    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() });
-  }
+    handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
+        event.preventDefault();
+        this.setState({ animationTargetPath: event.target.value.trim() });
+    }
 
-  handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
-    event.preventDefault();
-    this.setState({ animationType: parseInt(event.target.value) });
-  }
+    handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+        event.preventDefault();
+        this.setState({ animationType: parseInt(event.target.value) });
+    }
 
-  handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
-    event.preventDefault();
-    this.setState({ animationTargetProperty: 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) });
-  }
+    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>Display Name</label>
-            <input
-              type='text'
-              value={this.state.animationName}
-              onChange={(e) => this.handleNameChange(e)}
-            ></input>
-          </div>
-          {this.state.isUpdating ? null : (
-            <div className='label-input'>
-              <label>Property</label>
-              <input
-                type='text'
-                value={this.state.animationTargetProperty}
-                onChange={(e) => this.handlePropertyChange(e)}
-              ></input>
-            </div>
-          )}
-          {this.state.isUpdating ? null : (
-            <div className='label-input'>
-              <label>Type</label>
-              <select
-                onChange={(e) => this.handleTypeChange(e)}
-                value={this.state.animationType}
-              >
-                <option value={Animation.ANIMATIONTYPE_COLOR3}>Color3</option>
-                <option value={Animation.ANIMATIONTYPE_COLOR4}>Color4</option>
-                <option value={Animation.ANIMATIONTYPE_FLOAT}>Float</option>
-                <option value={Animation.ANIMATIONTYPE_VECTOR3}>Vector3</option>
+    render() {
+        return (
+            <div className="new-animation" style={{ display: this.props.isOpen ? "block" : "none" }}>
+                <div className="sub-content">
+                    <div className="label-input">
+                        <label>Display Name</label>
+                        <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
+                    </div>
+                    {this.state.isUpdating ? null : (
+                        <div className="label-input">
+                            <label>Property</label>
+                            <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
+                        </div>
+                    )}
+                    {this.state.isUpdating ? null : (
+                        <div className="label-input">
+                            <label>Type</label>
+                            <select onChange={(e) => this.handleTypeChange(e)} value={this.state.animationType}>
+                                {/* <option value={Animation.ANIMATIONTYPE_COLOR3}>Color3</option>
+                <option value={Animation.ANIMATIONTYPE_COLOR4}>Color4</option> */}
+                                <option value={Animation.ANIMATIONTYPE_FLOAT}>Float</option>
+                                {/* <option value={Animation.ANIMATIONTYPE_VECTOR3}>Vector3</option>
                 <option value={Animation.ANIMATIONTYPE_VECTOR2}>Vector2</option>
                 <option value={Animation.ANIMATIONTYPE_QUATERNION}>
                   Quaternion
-                </option>
-              </select>
+                </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={this.state.isUpdating ? "Update" : "Create"} onClick={this.state.isUpdating ? () => this.updateAnimation() : () => this.addAnimation()} />
+                        {this.props.entity.animations?.length !== 0 ? <ButtonLineComponent label={"Cancel"} onClick={this.props.close} /> : null}
+                    </div>
+                </div>
             </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={this.state.isUpdating ? 'Update' : 'Create'}
-              onClick={
-                this.state.isUpdating
-                  ? () => this.updateAnimation()
-                  : () => this.addAnimation()
-              }
-            />
-            {this.props.entity.animations?.length !== 0 ? (
-              <ButtonLineComponent
-                label={'Cancel'}
-                onClick={this.props.close}
-              />
-            ) : null}
-          </div>
-        </div>
-      </div>
-    );
-  }
+        );
+    }
 }

+ 74 - 57
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -89,6 +89,7 @@ export class AnimationCurveEditorComponent extends React.Component<
         repositionCanvas: boolean;
         actionableKeyframe: IActionableKeyFrame;
         valueScale: CurveScale;
+        canvasLength: number;
     }
 > {
     private _snippetUrl = "https://snippet.babylonjs.com";
@@ -98,10 +99,11 @@ export class AnimationCurveEditorComponent extends React.Component<
     private _currentScale: number = 10;
     // Canvas Length *Review this functionality
     readonly _entityName: string;
-    readonly _canvasLength: number = 20;
+    //private _canvasLength: number;
     private _svgKeyframes: IKeyframeSvgPoint[] = [];
     private _isPlaying: boolean = false;
     private _graphCanvas: React.RefObject<HTMLDivElement>;
+    private _editor: React.RefObject<HTMLDivElement>;
 
     //private _selectedCurve: React.RefObject<SVGPathElement>;
     private _svgCanvas: React.RefObject<SvgDraggableArea>;
@@ -115,6 +117,7 @@ export class AnimationCurveEditorComponent extends React.Component<
         super(props);
         this._entityName = (this.props.entity as any).id;
 
+        this._editor = React.createRef();
         this._graphCanvas = React.createRef();
         this._svgCanvas = React.createRef();
 
@@ -139,7 +142,7 @@ export class AnimationCurveEditorComponent extends React.Component<
             initialPathData = initialPathData === null || initialPathData === undefined ? undefined : initialPathData;
         }
 
-        this._canvasLength = 240;
+        const _canvasLength = 240;
 
         this.stopAnimation();
 
@@ -155,8 +158,8 @@ export class AnimationCurveEditorComponent extends React.Component<
             isTangentMode: false,
             isBrokenMode: false,
             lerpMode: initialLerpMode,
-            playheadOffset: this._graphCanvas.current ? this._graphCanvas.current.children[0].clientWidth / (this._canvasLength * 10) : 0,
-            frameAxisLength: this.setFrameAxis(this._canvasLength),
+            playheadOffset: this._graphCanvas.current ? this._graphCanvas.current.children[0].clientWidth / (_canvasLength * 10) : 0,
+            frameAxisLength: this.setFrameAxis(_canvasLength),
             valueAxisLength: new Array(10).fill(0).map((s, i) => {
                 return { value: i * 10, label: valueInd[i] };
             }),
@@ -167,7 +170,8 @@ export class AnimationCurveEditorComponent extends React.Component<
             isPlaying: false,
             selectedPathData: initialPathData,
             selectedCoordinate: 0,
-            animationLimit: this._canvasLength / 2,
+            animationLimit: _canvasLength / 2,
+            canvasLength: _canvasLength,
             fps: 60,
             isLooping: true,
             panningY: 0,
@@ -218,11 +222,11 @@ export class AnimationCurveEditorComponent extends React.Component<
     }
 
     setFrameAxis(currentLength: number) {
-        let halfNegative = new Array(currentLength / 2).fill(0).map((s, i) => {
+        let halfNegative = new Array(currentLength).fill(0).map((s, i) => {
             return { value: -i * 10, label: -i };
         });
 
-        let halfPositive = new Array(currentLength / 2).fill(0).map((s, i) => {
+        let halfPositive = new Array(currentLength).fill(0).map((s, i) => {
             return { value: i * 10, label: i };
         });
 
@@ -297,7 +301,7 @@ export class AnimationCurveEditorComponent extends React.Component<
     resetPlayheadOffset() {
         if (this._graphCanvas && this._graphCanvas.current) {
             this.setState({
-                playheadOffset: this._graphCanvas.current.children[0].clientWidth / (this._canvasLength * 10 * this.state.scale),
+                playheadOffset: this._graphCanvas.current.children[0].clientWidth / (this.state.canvasLength * 10 * this.state.scale),
             });
         }
     }
@@ -703,60 +707,60 @@ export class AnimationCurveEditorComponent extends React.Component<
 
             let keys = currentAnimation.getKeys();
             let x = this.state.currentFrame;
-            let y = this.state.actionableKeyframe.value ?? 1;
-            // check if value exists...
-            let arrayValue: any = [];
-            let emptyValue = this.returnZero(currentAnimation.dataType);
+
             let existValue = keys.find((k) => k.frame === x);
-            if (existValue !== undefined) {
-                arrayValue = this.getValueAsArray(currentAnimation.dataType, existValue.value);
-            } else {
-                // Set empty if doesn't exist
+
+            if (existValue === undefined) {
+                let y = this.state.actionableKeyframe.value ?? 1;
+                // check if value exists...
+                let arrayValue: any = [];
+                let emptyValue = this.returnZero(currentAnimation.dataType);
+
                 if (emptyValue) {
                     arrayValue = this.getValueAsArray(currentAnimation.dataType, emptyValue);
                 }
-            }
 
-            // calculate point between prevkeyframe and nextkeyframe.
-            const previousKFs = keys.filter((kf) => kf.frame < x);
-            const nextKFs = keys.filter((kf) => kf.frame > x);
-            const prev = previousKFs.slice(-1)[0];
-            const next = nextKFs[0];
+                // calculate point between prevkeyframe and nextkeyframe.
+                const previousKFs = keys.filter((kf) => kf.frame < x);
+                const nextKFs = keys.filter((kf) => kf.frame > x);
+                const prev = previousKFs.slice(-1)[0];
+                const next = nextKFs[0];
 
-            if (prev === undefined && next) {
-                y = next.value;
-            }
+                if (prev === undefined && next) {
+                    y = next.value;
+                }
 
-            if (prev && next === undefined) {
-                y = prev.value;
-            }
+                if (prev && next === undefined) {
+                    y = prev.value;
+                }
 
-            if (prev && next) {
-                const value1 = new Vector2(prev.frame, prev.value);
-                const tangent1 = new Vector2(prev.outTangent, prev.outTangent);
-                const value2 = new Vector2(next.frame, next.value);
-                const tangent2 = new Vector2(next.inTangent, next.inTangent);
+                if (prev && next) {
+                    const value1 = new Vector2(prev.frame, prev.value);
+                    const tangent1 = new Vector2(prev.outTangent, prev.outTangent);
+                    const value2 = new Vector2(next.frame, next.value);
+                    const tangent2 = new Vector2(next.inTangent, next.inTangent);
 
-                const amount = (x - prev.frame) / (next.frame - prev.frame);
-                const newV = Vector2.Hermite(value1, tangent1, value2, tangent2, amount);
-                y = newV.y;
-            }
+                    const amount = (x - prev.frame) / (next.frame - prev.frame);
+                    const newV = Vector2.Hermite(value1, tangent1, value2, tangent2, amount);
+                    y = newV.y;
+                }
 
-            arrayValue[this.state.selectedCoordinate] = y;
+                arrayValue[this.state.selectedCoordinate] = y;
 
-            let actualValue = this.setValueAsType(currentAnimation.dataType, arrayValue);
+                let actualValue = this.setValueAsType(currentAnimation.dataType, arrayValue);
 
-            keys.push({
-                frame: x,
-                value: actualValue,
-                inTangent: emptyValue,
-                outTangent: emptyValue,
-            });
-            keys.sort((a, b) => a.frame - b.frame);
+                keys.push({
+                    frame: x,
+                    value: actualValue,
+                    inTangent: emptyValue,
+                    outTangent: emptyValue,
+                });
+                keys.sort((a, b) => a.frame - b.frame);
 
-            currentAnimation.setKeys(keys);
+                currentAnimation.setKeys(keys);
 
-            this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
+                this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
+            }
         }
     }
 
@@ -950,7 +954,7 @@ export class AnimationCurveEditorComponent extends React.Component<
 
         var keyframes = animation.getKeys();
 
-        if (keyframes === undefined) {
+        if (keyframes === undefined || keyframes.length === 0) {
             return undefined;
         } else {
             const { easingMode, easingType, usesTangents, valueType, highestFrame, name, targetProperty } = this.getAnimationData(animation);
@@ -1327,10 +1331,12 @@ export class AnimationCurveEditorComponent extends React.Component<
 
             if (this._mainAnimatable?.target !== target) {
                 const keys = this.state.selected.getKeys();
-                const firstFrame = keys[0].frame;
-                const LastFrame = this.state.selected.getHighestFrame();
-                this._mainAnimatable = this.props.scene.beginAnimation(target, firstFrame, LastFrame, this.state.isLooping);
-                this._mainAnimatable.pause();
+                if (keys.length !== 0) {
+                    const firstFrame = keys[0].frame;
+                    const LastFrame = this.state.selected.getHighestFrame();
+                    this._mainAnimatable = this.props.scene.beginAnimation(target, firstFrame, LastFrame, this.state.isLooping);
+                    this._mainAnimatable.pause();
+                }
             }
         }
     }
@@ -1418,8 +1424,11 @@ export class AnimationCurveEditorComponent extends React.Component<
     }
 
     changeAnimationLimit(limit: number) {
+        const doubleLimit = limit * 2;
         this.setState({
             animationLimit: limit,
+            canvasLength: doubleLimit,
+            frameAxisLength: this.setFrameAxis(doubleLimit),
         });
     }
 
@@ -1461,6 +1470,7 @@ export class AnimationCurveEditorComponent extends React.Component<
                 if (direction === -1) {
                     this._mainAnimatable = this.props.scene.beginAnimation(target, LastFrame, firstFrame, this.state.isLooping);
                 }
+
                 this._isPlaying = true;
                 this.setState({ isPlaying: true });
                 this.forceUpdate();
@@ -1504,7 +1514,7 @@ export class AnimationCurveEditorComponent extends React.Component<
 
     render() {
         return (
-            <div id="animation-curve-editor">
+            <div ref={this._editor} id="animation-curve-editor">
                 <Notification message={this.state.notification} open={this.state.notification !== "" ? true : false} close={() => this.clearNotification()} />
                 <GraphActionsBar
                     setKeyframeValue={() => this.setKeyframeValue()}
@@ -1610,9 +1620,16 @@ export class AnimationCurveEditorComponent extends React.Component<
 
                                     {this.state.frameAxisLength.map((f, i) => (
                                         <svg key={i} x="0" y={96 + this.state.panningY + "%"} className="frame-contain">
-                                            <text x={f.value} y="0" dx="2px" style={{ fontSize: `${0.17 * this.state.scale}em` }}>
-                                                {f.label}
-                                            </text>
+                                            {f.label < 10 && f.label > -10 ? (
+                                                <text x={f.value} y="-1.5px" dx="-0.5px" style={{ fontSize: `${0.17 * this.state.scale}em` }}>
+                                                    {f.label}
+                                                </text>
+                                            ) : (
+                                                <text x={f.value} y="-1.5px" dx="-1px" style={{ fontSize: `${0.17 * this.state.scale}em` }}>
+                                                    {f.label}
+                                                </text>
+                                            )}
+
                                             <line x1={f.value} y1="0" x2={f.value} y2="5%"></line>
 
                                             {f.value % this.state.fps === 0 && f.value !== 0 ? <line x1={f.value} y1="-100%" x2={f.value} y2="5%"></line> : null}

+ 179 - 284
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationListTree.tsx

@@ -1,316 +1,211 @@
-import * as React from 'react';
-import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
-import { Observable } from 'babylonjs/Misc/observable';
-import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
-import { Animation } from 'babylonjs/Animations/animation';
-import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
-import { Nullable } from 'babylonjs/types';
+import * as React from "react";
+import { IAnimatable } from "babylonjs/Animations/animatable.interface";
+import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Animation } from "babylonjs/Animations/animation";
+import { IconButtonLineComponent } from "../../../lines/iconButtonLineComponent";
+import { Nullable } from "babylonjs/types";
 
 interface IAnimationListTreeProps {
-  isTargetedAnimation: boolean;
-  entity: IAnimatable | TargetedAnimation;
-  selected: Animation | null;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-  selectAnimation: (
-    selected: Animation,
-    coordinate?: SelectedCoordinate
-  ) => void;
-  empty: () => void;
-  editAnimation: (selected: Animation) => void;
-  deselectAnimation: () => void;
+    isTargetedAnimation: boolean;
+    entity: IAnimatable | TargetedAnimation;
+    selected: Animation | null;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    selectAnimation: (selected: Animation, coordinate?: SelectedCoordinate) => void;
+    empty: () => void;
+    editAnimation: (selected: Animation) => void;
+    deselectAnimation: () => void;
 }
 
 interface Item {
-  index: number;
-  name: string;
-  property: string;
-  selected: boolean;
-  open: boolean;
+    index: number;
+    name: string;
+    property: string;
+    selected: boolean;
+    open: boolean;
 }
 
 export enum SelectedCoordinate {
-  x = 0,
-  y = 1,
-  z = 2,
-  w = 3,
-  r = 0,
-  g = 1,
-  b = 2,
-  a = 3,
-  width = 0,
-  height = 1,
+    x = 0,
+    y = 1,
+    z = 2,
+    w = 3,
+    r = 0,
+    g = 1,
+    b = 2,
+    a = 3,
+    width = 0,
+    height = 1,
 }
 
 interface ItemCoordinate {
-  id: string;
-  color: string;
-  coordinate: SelectedCoordinate;
+    id: string;
+    color: string;
+    coordinate: SelectedCoordinate;
 }
 
 export class AnimationListTree extends React.Component<
-  IAnimationListTreeProps,
-  {
-    selectedCoordinate: SelectedCoordinate;
-    selectedAnimation: number;
-    animationList: Item[] | null;
-  }
+    IAnimationListTreeProps,
+    {
+        selectedCoordinate: SelectedCoordinate;
+        selectedAnimation: number;
+        animationList: Item[] | null;
+    }
 > {
-  constructor(props: IAnimationListTreeProps) {
-    super(props);
+    constructor(props: IAnimationListTreeProps) {
+        super(props);
 
-    this.state = {
-      selectedCoordinate: 0,
-      selectedAnimation: 0,
-      animationList: this.generateList(),
-    };
-  }
+        this.state = {
+            selectedCoordinate: 0,
+            selectedAnimation: 0,
+            animationList: this.generateList(),
+        };
+    }
 
-  deleteAnimation() {
-    let currentSelected = this.props.selected;
-    if (this.props.entity instanceof TargetedAnimation) {
-      console.log('no animation remove allowed');
-    } else {
-      let animations = (this.props.entity as IAnimatable).animations;
-      if (animations) {
-        let updatedAnimations = animations.filter(
-          (anim) => anim !== currentSelected
-        );
-        (this.props
-          .entity as IAnimatable).animations = updatedAnimations as Nullable<
-          Animation[]
-        >;
-        this.props.deselectAnimation();
-        this.setState({ animationList: this.generateList() });
-      }
+    deleteAnimation() {
+        let currentSelected = this.props.selected;
+        if (this.props.entity instanceof TargetedAnimation) {
+            console.log("no animation remove allowed");
+        } else {
+            let animations = (this.props.entity as IAnimatable).animations;
+            if (animations) {
+                let updatedAnimations = animations.filter((anim) => anim !== currentSelected);
+                (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
+                this.props.deselectAnimation();
+                this.setState({ animationList: this.generateList() });
+            }
+        }
     }
-  }
 
-  generateList() {
-    let animationList =
-      (this.props.entity as IAnimatable).animations &&
-      (this.props.entity as IAnimatable).animations?.map((animation, i) => {
-        return {
-          index: i,
-          name: animation.name,
-          property: animation.targetProperty,
-          selected: false,
-          open: false,
-        } as Item;
-      });
-    if (animationList?.length === 0) {
-      this.props.empty();
+    generateList() {
+        let animationList =
+            (this.props.entity as IAnimatable).animations &&
+            (this.props.entity as IAnimatable).animations?.map((animation, i) => {
+                return {
+                    index: i,
+                    name: animation.name,
+                    property: animation.targetProperty,
+                    selected: false,
+                    open: false,
+                } as Item;
+            });
+        if (animationList?.length === 0) {
+            this.props.empty();
+        }
+        return animationList ?? null;
     }
-    return animationList ?? null;
-  }
 
-  toggleProperty(index: number) {
-    if (this.state.animationList) {
-      const updated = this.state.animationList.map((a) => {
-        if (a.index === index) {
-          a.open = !a.open;
+    toggleProperty(index: number) {
+        if (this.state.animationList) {
+            const updated = this.state.animationList.map((a) => {
+                if (a.index === index) {
+                    a.open = !a.open;
+                }
+                return a;
+            });
+            this.setState({ animationList: updated });
         }
-        return a;
-      });
-      this.setState({ animationList: updated });
     }
-  }
 
-  setSelectedCoordinate(
-    animation: Animation,
-    coordinate: SelectedCoordinate,
-    index: number
-  ) {
-    this.setState({ selectedCoordinate: coordinate, selectedAnimation: index });
-    this.props.selectAnimation(animation, coordinate);
-  }
+    setSelectedCoordinate(animation: Animation, coordinate: SelectedCoordinate, index: number) {
+        this.setState({ selectedCoordinate: coordinate, selectedAnimation: index });
+        this.props.selectAnimation(animation, coordinate);
+    }
 
-  coordinateItem(
-    i: number,
-    animation: Animation,
-    coordinate: string,
-    color: string,
-    selectedCoordinate: SelectedCoordinate
-  ) {
-    return (
-      <li
-        key={`${i}_${coordinate}`}
-        id={`${i}_${coordinate}`}
-        className='property'
-        style={{ color: color }}
-        onClick={() =>
-          this.setSelectedCoordinate(animation, selectedCoordinate, i)
-        }
-      >
-        <div
-          className={`handle-indicator ${
-            this.state.selectedCoordinate === selectedCoordinate &&
-            this.state.selectedAnimation === i
-              ? 'show'
-              : 'hide'
-          }`}
-        ></div>
-        {animation.targetProperty} {coordinate.toUpperCase()}
-      </li>
-    );
-  }
+    coordinateItem(i: number, animation: Animation, coordinate: string, color: string, selectedCoordinate: SelectedCoordinate) {
+        return (
+            <li key={`${i}_${coordinate}`} id={`${i}_${coordinate}`} className="property" style={{ color: color }} onClick={() => this.setSelectedCoordinate(animation, selectedCoordinate, i)}>
+                <div className={`handle-indicator ${this.state.selectedCoordinate === selectedCoordinate && this.state.selectedAnimation === i ? "show" : "hide"}`}></div>
+                {animation.targetProperty} {coordinate.toUpperCase()}
+            </li>
+        );
+    }
+
+    typeAnimationItem(animation: Animation, i: number, childrenElements: ItemCoordinate[]) {
+        return (
+            <li className={this.props.selected && this.props.selected.name === animation.name ? "property sub active" : "property sub"} key={i}>
+                <div className={`animation-arrow ${this.state.animationList && this.state.animationList[i].open ? "" : "flip"}`} onClick={() => this.toggleProperty(i)}></div>
+                <p onClick={() => this.props.selectAnimation(animation)}>{animation.targetProperty}</p>
+                <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={() => this.props.editAnimation(animation)} />
+                {!(this.props.entity instanceof TargetedAnimation) ? this.props.selected && this.props.selected.name === animation.name ? <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={() => this.deleteAnimation()} /> : <div className="spacer"></div> : null}
+                <ul className={`sub-list ${this.state.animationList && this.state.animationList[i].open ? "" : "hidden"}`}>{childrenElements.map((c) => this.coordinateItem(i, animation, c.id, c.color, c.coordinate))}</ul>
+            </li>
+        );
+    }
 
-  typeAnimationItem(
-    animation: Animation,
-    i: number,
-    childrenElements: ItemCoordinate[]
-  ) {
-    return (
-      <li
-        className={
-          this.props.selected && this.props.selected.name === animation.name
-            ? 'property sub active'
-            : 'property sub'
+    setListItem(animation: Animation, i: number) {
+        switch (animation.dataType) {
+            case Animation.ANIMATIONTYPE_FLOAT:
+                return (
+                    <li className={this.props.selected && this.props.selected.name === animation.name ? "property active" : "property"} key={i} onClick={() => this.props.selectAnimation(animation, 0)}>
+                        <div className={`animation-bullet`}></div>
+                        <p>{animation.targetProperty}</p>
+                        <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={() => this.props.editAnimation(animation)} />
+                        {!(this.props.entity instanceof TargetedAnimation) ? this.props.selected && this.props.selected.name === animation.name ? <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={() => this.deleteAnimation()} /> : <div className="spacer"></div> : null}
+                    </li>
+                );
+            case Animation.ANIMATIONTYPE_VECTOR2:
+                return this.typeAnimationItem(animation, i, [
+                    { id: "x", color: "#db3e3e", coordinate: SelectedCoordinate.x },
+                    { id: "y", color: "#51e22d", coordinate: SelectedCoordinate.y },
+                ]);
+            case Animation.ANIMATIONTYPE_VECTOR3:
+                return this.typeAnimationItem(animation, i, [
+                    { id: "x", color: "#db3e3e", coordinate: SelectedCoordinate.x },
+                    { id: "y", color: "#51e22d", coordinate: SelectedCoordinate.y },
+                    { id: "z", color: "#00a3ff", coordinate: SelectedCoordinate.z },
+                ]);
+            case Animation.ANIMATIONTYPE_QUATERNION:
+                return this.typeAnimationItem(animation, i, [
+                    { id: "x", color: "#db3e3e", coordinate: SelectedCoordinate.x },
+                    { id: "y", color: "#51e22d", coordinate: SelectedCoordinate.y },
+                    { id: "z", color: "#00a3ff", coordinate: SelectedCoordinate.z },
+                    { id: "w", color: "#7a4ece", coordinate: SelectedCoordinate.w },
+                ]);
+            case Animation.ANIMATIONTYPE_COLOR3:
+                return this.typeAnimationItem(animation, i, [
+                    { id: "r", color: "#db3e3e", coordinate: SelectedCoordinate.r },
+                    { id: "g", color: "#51e22d", coordinate: SelectedCoordinate.g },
+                    { id: "b", color: "#00a3ff", coordinate: SelectedCoordinate.b },
+                ]);
+            case Animation.ANIMATIONTYPE_COLOR4:
+                return this.typeAnimationItem(animation, i, [
+                    { id: "r", color: "#db3e3e", coordinate: SelectedCoordinate.r },
+                    { id: "g", color: "#51e22d", coordinate: SelectedCoordinate.g },
+                    { id: "b", color: "#00a3ff", coordinate: SelectedCoordinate.b },
+                    { id: "a", color: "#7a4ece", coordinate: SelectedCoordinate.a },
+                ]);
+            case Animation.ANIMATIONTYPE_SIZE:
+                return this.typeAnimationItem(animation, i, [
+                    {
+                        id: "width",
+                        color: "#db3e3e",
+                        coordinate: SelectedCoordinate.width,
+                    },
+                    {
+                        id: "height",
+                        color: "#51e22d",
+                        coordinate: SelectedCoordinate.height,
+                    },
+                ]);
+            default:
+                return null;
         }
-        key={i}
-      >
-        <div
-          className={`animation-arrow ${
-            this.state.animationList && this.state.animationList[i].open
-              ? ''
-              : 'flip'
-          }`}
-          onClick={() => this.toggleProperty(i)}
-        ></div>
-        <p onClick={() => this.props.selectAnimation(animation)}>
-          {animation.targetProperty}
-        </p>
-        <IconButtonLineComponent
-          tooltip='Options'
-          icon='small animation-options'
-          onClick={() => this.props.editAnimation(animation)}
-        />
-        {!(this.props.entity instanceof TargetedAnimation) ? (
-          this.props.selected && this.props.selected.name === animation.name ? (
-            <IconButtonLineComponent
-              tooltip='Remove'
-              icon='small animation-delete'
-              onClick={() => this.deleteAnimation()}
-            />
-          ) : (
-            <div className='spacer'></div>
-          )
-        ) : null}
-        <ul
-          className={`sub-list ${
-            this.state.animationList && this.state.animationList[i].open
-              ? ''
-              : 'hidden'
-          }`}
-        >
-          {childrenElements.map((c) =>
-            this.coordinateItem(i, animation, c.id, c.color, c.coordinate)
-          )}
-        </ul>
-      </li>
-    );
-  }
+    }
 
-  setListItem(animation: Animation, i: number) {
-    switch (animation.dataType) {
-      case Animation.ANIMATIONTYPE_FLOAT:
+    render() {
         return (
-          <li
-            className={
-              this.props.selected && this.props.selected.name === animation.name
-                ? 'property active'
-                : 'property'
-            }
-            key={i}
-            onClick={() => this.props.selectAnimation(animation)}
-          >
-            <div className={`animation-bullet`}></div>
-            <p>{animation.targetProperty}</p>
-            <IconButtonLineComponent
-              tooltip='Options'
-              icon='small animation-options'
-              onClick={() => this.props.editAnimation(animation)}
-            />
-            {!(this.props.entity instanceof TargetedAnimation) ? (
-              this.props.selected &&
-              this.props.selected.name === animation.name ? (
-                <IconButtonLineComponent
-                  tooltip='Remove'
-                  icon='small animation-delete'
-                  onClick={() => this.deleteAnimation()}
-                />
-              ) : (
-                <div className='spacer'></div>
-              )
-            ) : null}
-          </li>
+            <div className="object-tree">
+                <ul>
+                    {this.props.isTargetedAnimation
+                        ? this.setListItem((this.props.entity as TargetedAnimation).animation, 0)
+                        : (this.props.entity as IAnimatable).animations &&
+                          (this.props.entity as IAnimatable).animations?.map((animation, i) => {
+                              return this.setListItem(animation, i);
+                          })}
+                </ul>
+            </div>
         );
-      case Animation.ANIMATIONTYPE_VECTOR2:
-        return this.typeAnimationItem(animation, i, [
-          { id: 'x', color: '#db3e3e', coordinate: SelectedCoordinate.x },
-          { id: 'y', color: '#51e22d', coordinate: SelectedCoordinate.y },
-        ]);
-      case Animation.ANIMATIONTYPE_VECTOR3:
-        return this.typeAnimationItem(animation, i, [
-          { id: 'x', color: '#db3e3e', coordinate: SelectedCoordinate.x },
-          { id: 'y', color: '#51e22d', coordinate: SelectedCoordinate.y },
-          { id: 'z', color: '#00a3ff', coordinate: SelectedCoordinate.z },
-        ]);
-      case Animation.ANIMATIONTYPE_QUATERNION:
-        return this.typeAnimationItem(animation, i, [
-          { id: 'x', color: '#db3e3e', coordinate: SelectedCoordinate.x },
-          { id: 'y', color: '#51e22d', coordinate: SelectedCoordinate.y },
-          { id: 'z', color: '#00a3ff', coordinate: SelectedCoordinate.z },
-          { id: 'w', color: '#7a4ece', coordinate: SelectedCoordinate.w },
-        ]);
-      case Animation.ANIMATIONTYPE_COLOR3:
-        return this.typeAnimationItem(animation, i, [
-          { id: 'r', color: '#db3e3e', coordinate: SelectedCoordinate.r },
-          { id: 'g', color: '#51e22d', coordinate: SelectedCoordinate.g },
-          { id: 'b', color: '#00a3ff', coordinate: SelectedCoordinate.b },
-        ]);
-      case Animation.ANIMATIONTYPE_COLOR4:
-        return this.typeAnimationItem(animation, i, [
-          { id: 'r', color: '#db3e3e', coordinate: SelectedCoordinate.r },
-          { id: 'g', color: '#51e22d', coordinate: SelectedCoordinate.g },
-          { id: 'b', color: '#00a3ff', coordinate: SelectedCoordinate.b },
-          { id: 'a', color: '#7a4ece', coordinate: SelectedCoordinate.a },
-        ]);
-      case Animation.ANIMATIONTYPE_SIZE:
-        return this.typeAnimationItem(animation, i, [
-          {
-            id: 'width',
-            color: '#db3e3e',
-            coordinate: SelectedCoordinate.width,
-          },
-          {
-            id: 'height',
-            color: '#51e22d',
-            coordinate: SelectedCoordinate.height,
-          },
-        ]);
-      default:
-        return null;
     }
-  }
-
-  render() {
-    return (
-      <div className='object-tree'>
-        <ul>
-          {this.props.isTargetedAnimation
-            ? this.setListItem(
-                (this.props.entity as TargetedAnimation).animation,
-                0
-              )
-            : (this.props.entity as IAnimatable).animations &&
-              (this.props.entity as IAnimatable).animations?.map(
-                (animation, i) => {
-                  return this.setListItem(animation, i);
-                }
-              )}
-        </ul>
-      </div>
-    );
-  }
 }

+ 95 - 140
inspector/src/components/actionTabs/tabs/propertyGrids/animations/controls.tsx

@@ -1,158 +1,113 @@
-import * as React from 'react';
-import { IAnimationKey } from 'babylonjs/Animations/animationKey';
-import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
+import * as React from "react";
+import { IAnimationKey } from "babylonjs/Animations/animationKey";
+import { IconButtonLineComponent } from "../../../lines/iconButtonLineComponent";
 
 interface IControlsProps {
-  keyframes: IAnimationKey[] | null;
-  selected: IAnimationKey | null;
-  currentFrame: number;
-  onCurrentFrameChange: (frame: number) => void;
-  playPause: (direction: number) => void;
-  isPlaying: boolean;
-  scrollable: React.RefObject<HTMLDivElement>;
+    keyframes: IAnimationKey[] | null;
+    selected: IAnimationKey | null;
+    currentFrame: number;
+    onCurrentFrameChange: (frame: number) => void;
+    repositionCanvas: (frame: number) => void;
+    playPause: (direction: number) => void;
+    isPlaying: boolean;
+    scrollable: React.RefObject<HTMLDivElement>;
 }
 
-export class Controls extends React.Component<
-  IControlsProps,
-  { selected: IAnimationKey; playingType: string }
-> {
-  constructor(props: IControlsProps) {
-    super(props);
-    if (this.props.selected !== null) {
-      this.state = { selected: this.props.selected, playingType: '' };
+export class Controls extends React.Component<IControlsProps, { selected: IAnimationKey; playingType: string }> {
+    readonly _sizeOfKeyframe: number = 5;
+    constructor(props: IControlsProps) {
+        super(props);
+        if (this.props.selected !== null) {
+            this.state = { selected: this.props.selected, playingType: "" };
+        }
     }
-  }
 
-  playBackwards() {
-    this.setState({ playingType: 'reverse' });
-    this.props.playPause(-1);
-  }
+    playBackwards() {
+        this.setState({ playingType: "reverse" });
+        this.props.playPause(-1);
+    }
+
+    play() {
+        this.setState({ playingType: "forward" });
+        this.props.playPause(1);
+    }
 
-  play() {
-    this.setState({ playingType: 'forward' });
-    this.props.playPause(1);
-  }
+    pause() {
+        if (this.props.isPlaying) {
+            this.setState({ playingType: "" });
+            this.props.playPause(0);
+        }
+    }
 
-  pause() {
-    if (this.props.isPlaying) {
-      this.setState({ playingType: '' });
-      this.props.playPause(0);
+    moveToAnimationStart() {
+        const start = this.props.keyframes && this.props.keyframes[0].frame;
+        if (start !== undefined && typeof start === "number") {
+            this.props.onCurrentFrameChange(start);
+            this.props.repositionCanvas(start);
+        }
     }
-  }
 
-  nextFrame() {
-    this.props.onCurrentFrameChange(this.props.currentFrame + 1);
-    (this.props.scrollable.current as HTMLDivElement).scrollLeft =
-      this.props.currentFrame * 5;
-  }
+    moveToAnimationEnd() {
+        const end = this.props.keyframes && this.props.keyframes[this.props.keyframes.length - 1].frame;
+        if (end !== undefined && typeof end === "number") {
+            this.props.onCurrentFrameChange(end);
+            this.props.repositionCanvas(end);
+        }
+    }
 
-  previousFrame() {
-    if (this.props.currentFrame !== 0) {
-      this.props.onCurrentFrameChange(this.props.currentFrame - 1);
-      (this.props.scrollable.current as HTMLDivElement).scrollLeft = -(
-        this.props.currentFrame * 5
-      );
+    nextKeyframe() {
+        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.props.repositionCanvas(first.frame);
+                this.setState({ selected: first });
+                (this.props.scrollable.current as HTMLDivElement).scrollLeft = first.frame * this._sizeOfKeyframe;
+            }
+        }
     }
-  }
 
-  nextKeyframe() {
-    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.props.scrollable.current as HTMLDivElement).scrollLeft =
-          first.frame * 5;
-      }
+    previousKeyframe() {
+        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.props.repositionCanvas(first.frame);
+                this.setState({ selected: first });
+                (this.props.scrollable.current as HTMLDivElement).scrollLeft = -(first.frame * this._sizeOfKeyframe);
+            }
+        }
     }
-  }
 
-  previousKeyframe() {
-    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.props.scrollable.current as HTMLDivElement).scrollLeft = -(
-          first.frame * 5
+    render() {
+        return (
+            <div className="controls">
+                <IconButtonLineComponent tooltip="Animation Start" icon="animation-start" onClick={() => this.moveToAnimationStart()}></IconButtonLineComponent>
+                <IconButtonLineComponent tooltip="Previous Keyframe" icon="animation-lastkey" onClick={() => this.previousKeyframe()}></IconButtonLineComponent>
+                {this.props.isPlaying ? (
+                    <div className="stop-container">
+                        {this.state.playingType === "reverse" ? (
+                            <>
+                                <IconButtonLineComponent tooltip="Pause" icon="animation-stop" onClick={() => this.pause()}></IconButtonLineComponent>
+                                <IconButtonLineComponent tooltip="Play Forward" icon="animation-playfwd" onClick={() => this.play()}></IconButtonLineComponent>
+                            </>
+                        ) : (
+                            <>
+                                <IconButtonLineComponent tooltip="Play Reverse" icon="animation-playrev" onClick={() => this.playBackwards()}></IconButtonLineComponent>
+                                <IconButtonLineComponent tooltip="Pause" icon="animation-stop" onClick={() => this.pause()}></IconButtonLineComponent>
+                            </>
+                        )}
+                    </div>
+                ) : (
+                    <div className="stop-container">
+                        <IconButtonLineComponent tooltip="Play Reverse" icon="animation-playrev" onClick={() => this.playBackwards()}></IconButtonLineComponent>
+                        <IconButtonLineComponent tooltip="Play Forward" icon="animation-playfwd" onClick={() => this.play()}></IconButtonLineComponent>
+                    </div>
+                )}
+                <IconButtonLineComponent tooltip="Next Keyframe" icon="animation-nextkey" onClick={() => this.nextKeyframe()}></IconButtonLineComponent>
+                <IconButtonLineComponent tooltip="Animation End" icon="animation-end" onClick={() => this.moveToAnimationEnd()}></IconButtonLineComponent>
+            </div>
         );
-      }
     }
-  }
-
-  render() {
-    return (
-      <div className='controls'>
-        <IconButtonLineComponent
-          tooltip='Animation Start'
-          icon='animation-start'
-          onClick={() => this.previousFrame()}
-        ></IconButtonLineComponent>
-        <IconButtonLineComponent
-          tooltip='Previous Keyframe'
-          icon='animation-lastkey'
-          onClick={() => this.previousKeyframe()}
-        ></IconButtonLineComponent>
-        {this.props.isPlaying ? (
-          <div className='stop-container'>
-            {this.state.playingType === 'reverse' ? (
-              <>
-                <IconButtonLineComponent
-                  tooltip='Pause'
-                  icon='animation-stop'
-                  onClick={() => this.pause()}
-                ></IconButtonLineComponent>
-                <IconButtonLineComponent
-                  tooltip='Play Forward'
-                  icon='animation-playfwd'
-                  onClick={() => this.play()}
-                ></IconButtonLineComponent>
-              </>
-            ) : (
-              <>
-                <IconButtonLineComponent
-                  tooltip='Play Reverse'
-                  icon='animation-playrev'
-                  onClick={() => this.playBackwards()}
-                ></IconButtonLineComponent>
-                <IconButtonLineComponent
-                  tooltip='Pause'
-                  icon='animation-stop'
-                  onClick={() => this.pause()}
-                ></IconButtonLineComponent>
-              </>
-            )}
-          </div>
-        ) : (
-          <div className='stop-container'>
-            <IconButtonLineComponent
-              tooltip='Play Reverse'
-              icon='animation-playrev'
-              onClick={() => this.playBackwards()}
-            ></IconButtonLineComponent>
-            <IconButtonLineComponent
-              tooltip='Play Forward'
-              icon='animation-playfwd'
-              onClick={() => this.play()}
-            ></IconButtonLineComponent>
-          </div>
-        )}
-        <IconButtonLineComponent
-          tooltip='Next Keyframe'
-          icon='animation-nextkey'
-          onClick={() => this.nextKeyframe()}
-        ></IconButtonLineComponent>
-        <IconButtonLineComponent
-          tooltip='Animation End'
-          icon='animation-end'
-          onClick={() => this.nextFrame()}
-        ></IconButtonLineComponent>
-      </div>
-    );
-  }
 }

+ 44 - 21
inspector/src/components/actionTabs/tabs/propertyGrids/animations/svgDraggableArea.tsx

@@ -29,6 +29,10 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
     private _panStop: Vector2;
     private _playheadDrag: number;
     private _playheadSelected: boolean;
+    private _movedX: number;
+    private _movedY: number;
+    readonly _dragBuffer: number;
+    readonly _draggingMultiplier: number;
 
     constructor(props: ISvgDraggableAreaProps) {
         super(props);
@@ -39,6 +43,10 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         this._panStop = new Vector2(0, 0);
         this._playheadDrag = 0;
         this._playheadSelected = false;
+        this._movedX = 0;
+        this._movedY = 0;
+        this._dragBuffer = 4;
+        this._draggingMultiplier = 3;
 
         this.state = { panX: 0, panY: 0 };
     }
@@ -80,7 +88,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
 
         if (e.target.classList.contains("pannable")) {
             this._active = true;
-            this._panStart.set(e.clientX, e.clientY);
+            this._panStart.set(e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top);
         }
     }
 
@@ -95,7 +103,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
             if (coord !== undefined) {
                 if (e.target.classList.contains("pannable")) {
                     if (this._panStart.x !== 0 && this._panStart.y !== 0) {
-                        this._panStop.set(e.clientX, e.clientY);
+                        this._panStop.set(e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top);
                         this.panDirection();
                     }
                 }
@@ -146,6 +154,8 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         this._panStop.set(0, 0);
         this._playheadDrag = 0;
         this._playheadSelected = false;
+        this._movedX = 0;
+        this._movedY = 0;
     }
 
     getMousePosition(e: React.TouchEvent<SVGSVGElement>): Vector2 | undefined;
@@ -169,35 +179,48 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
     }
 
     panDirection() {
-        let movementX = this._panStart.x - this._panStop.x;
-        let movementY = this._panStart.y - this._panStop.y;
+        let directionX = 1;
+        if (this._movedX < this._panStop.x) {
+            directionX = -1; //left
+        } else {
+            directionX = 1; //right
+        }
+
+        let directionY = 1;
+        if (this._movedY < this._panStop.y) {
+            directionY = -1; //top
+        } else {
+            directionY = 1; //bottom
+        }
+
+        const bufferX = Math.abs(this._movedX - this._panStop.x);
+        const bufferY = Math.abs(this._movedY - this._panStop.y);
 
-        let newX = this.state.panX + movementX / 20;
-        let newY = this.state.panY + movementY / 20;
+        let xMulti = 0;
+        if (bufferX > this._dragBuffer) {
+            xMulti = this._draggingMultiplier;
+        }
+
+        let yMulti = 0;
+        if (bufferY > this._dragBuffer) {
+            yMulti = this._draggingMultiplier;
+        }
+
+        this._movedX = this._panStop.x;
+        this._movedY = this._panStop.y;
+
+        let newX = this.state.panX + directionX * xMulti;
+        let newY = this.state.panY + directionY * yMulti;
 
         this.setState({
             panX: Math.round(newX),
             panY: Math.round(newY),
         });
+
         this.props.panningY(Math.round(newY));
         this.props.panningX(Math.round(newX));
     }
 
-    panTo(direction: string, value: number) {
-        switch (direction) {
-            case "left":
-                (this._draggableArea.current?.parentElement as HTMLDivElement).scrollLeft -= value * 1;
-                break;
-            case "right":
-                (this._draggableArea.current?.parentElement as HTMLDivElement).scrollLeft += value * 1;
-                break;
-            case "top":
-                break;
-            case "down":
-                break;
-        }
-    }
-
     keyDown(e: KeyboardEvent) {
         e.preventDefault();
         if (e.keyCode === 17) {

File diff suppressed because it is too large
+ 429 - 559
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx