Explorar o código

Loading and Saving snippets (#8409)

* loading1

* pending observer changes

* whats new

* Fixing naming

* Fixing bug

Co-authored-by: Alejandro Toledo <alex@pixelspace.com>
Alejandro Toledo Martinez %!s(int64=5) %!d(string=hai) anos
pai
achega
2ac37c6050

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

@@ -8,7 +8,7 @@
 - Added HDR texture filtering tools to the sandbox ([Sebavan](https://github.com/sebavan/))
 - Reflection probes can now be used to give accurate shading with PBR ([CraigFeldpsar](https://github.com/craigfeldspar) and ([Sebavan](https://github.com/sebavan/)))
 - Added editing of PBR materials, Post processes and Particle fragment shaders in the node material editor ([Popov72](https://github.com/Popov72))
-- Added Curve editor to manage selected entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
+- Added Curve editor to manage 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))
 

+ 30 - 15
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -18,6 +18,7 @@ import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
 import { EditorControls } from './editorControls';
 import { SelectedCoordinate } from './animationListTree';
 import { LockObject } from '../lockObject';
+import { GlobalState } from '../../../../globalState';
 import { Nullable } from 'babylonjs/types';
 import { Observer } from 'babylonjs/Misc/observable';
 
@@ -29,6 +30,7 @@ interface IAnimationCurveEditorComponentProps {
   scene: Scene;
   entity: IAnimatable | TargetedAnimation;
   lockObject: LockObject;
+  globalState: GlobalState;
 }
 
 interface ICanvasAxis {
@@ -68,6 +70,7 @@ export class AnimationCurveEditorComponent extends React.Component<
     selectedCoordinate: number;
   }
 > {
+  private _snippetUrl = 'https://snippet.babylonjs.com';
   // Height scale *Review this functionaliy
   private _heightScale: number = 100;
   // Canvas Length *Review this functionality
@@ -692,6 +695,18 @@ export class AnimationCurveEditorComponent extends React.Component<
       case Animation.ANIMATIONTYPE_VECTOR2:
         type = Vector2.Zero();
         break;
+      case Animation.ANIMATIONTYPE_QUATERNION:
+        type = Quaternion.Zero();
+        break;
+      case Animation.ANIMATIONTYPE_COLOR3:
+        type = new Color3(0, 0, 0);
+        break;
+      case Animation.ANIMATIONTYPE_COLOR4:
+        type = new Color4(0, 0, 0, 0);
+        break;
+      case Animation.ANIMATIONTYPE_SIZE:
+        type = new Size(0, 0);
+        break;
     }
     return type;
   }
@@ -789,12 +804,12 @@ export class AnimationCurveEditorComponent extends React.Component<
               if (this.state !== undefined) {
                 let emptyTangents = keyframes.map((kf, i) => {
                   if (i === 0) {
-                    kf.outTangent = 0;
+                    kf.outTangent = this.returnZero(valueType);
                   } else if (i === keyframes.length - 1) {
-                    kf.inTangent = 0;
+                    kf.inTangent = this.returnZero(valueType);
                   } else {
-                    kf.inTangent = 0;
-                    kf.outTangent = 0;
+                    kf.inTangent = this.returnZero(valueType);
+                    kf.outTangent = this.returnZero(valueType);
                   }
                   return kf;
                 });
@@ -1410,6 +1425,8 @@ export class AnimationCurveEditorComponent extends React.Component<
               setNotificationMessage={(message: string) => {
                 this.setState({ notification: message });
               }}
+              globalState={this.props.globalState}
+              snippetServer={this._snippetUrl}
             />
 
             <div
@@ -1475,17 +1492,15 @@ export class AnimationCurveEditorComponent extends React.Component<
                     );
                   })}
 
-                  <svg>
-                    <rect
-                      onClick={(e) => this.moveFrameTo(e)}
-                      x='0%'
-                      y='91%'
-                      width='105%'
-                      height='10%'
-                      fill='#222'
-                      style={{ cursor: 'pointer' }}
-                    ></rect>
-                  </svg>
+                  <rect
+                    onClick={(e) => this.moveFrameTo(e)}
+                    x='0%'
+                    y='91%'
+                    width='105%'
+                    height='10%'
+                    fill='#222'
+                    style={{ cursor: 'pointer' }}
+                  ></rect>
 
                   {this.state.frameAxisLength.map((f, i) => (
                     <svg key={i} x='0' y='96%'>

+ 373 - 160
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationListTree.tsx

@@ -1,187 +1,400 @@
-
-import * as React from "react";
+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 { 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;
+  isTargetedAnimation: boolean;
+  entity: IAnimatable | TargetedAnimation;
+  selected: Animation | null;
+  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+  selectAnimation: (
+    selected: Animation,
+    coordinate?: SelectedCoordinate
+  ) => void;
+  empty: () => 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,
 }
 
-export class AnimationListTree extends React.Component<IAnimationListTreeProps, { list: Item[], selectedCoordinate: SelectedCoordinate, selectedAnimation: number }>{
-    constructor(props: IAnimationListTreeProps) {
-        super(props);
-        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)
-        });
-        this.state = { list: animationList ?? [], selectedCoordinate: 0, selectedAnimation: 0 }
-    }
+export class AnimationListTree extends React.Component<
+  IAnimationListTreeProps,
+  {
+    selectedCoordinate: SelectedCoordinate;
+    selectedAnimation: number;
+  }
+> {
+  private _list: Item[] | null;
+  constructor(props: IAnimationListTreeProps) {
+    super(props);
 
-    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.generateList();
-            }
-        }
-    }
+    this._list = 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();
-        }
-        this.setState({ list: animationList ?? [] });
-    }
+    this.state = {
+      selectedCoordinate: 0,
+      selectedAnimation: 0,
+    };
+  }
 
-    editAnimation() {
-        console.log('Edit animation');// TODO. Implement the edit options here
+  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._list = this.generateList();
+      }
     }
+  }
 
-    toggleProperty(index: number) {
-        let item = this.state.list[index];
-        item.open = !item.open;
+  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;
+  }
 
-    setSelectedCoordinate(animation: Animation, coordinate: SelectedCoordinate, index: number) {
-        this.setState({ selectedCoordinate: coordinate, selectedAnimation: index });
-        this.props.selectAnimation(animation, SelectedCoordinate.x)
-    }
+  editAnimation() {
+    console.log('Edit animation'); // TODO. Implement the edit options here
+  }
 
-    setListItem(animation: Animation, i: number) {
-        let element;
+  toggleProperty(index: number) {
+    if (this._list !== null) {
+      let item = this._list[index];
+      item.open = !item.open;
+    }
+  }
 
-        switch (animation.dataType) {
-            case Animation.ANIMATIONTYPE_FLOAT:
-                element = <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.editAnimation()} />
-                    {!(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>
-                break;
-            case Animation.ANIMATIONTYPE_VECTOR2:
-                element = <li className={this.props.selected && this.props.selected.name === animation.name ? 'property active' : 'property'} key={i} onClick={() => this.props.selectAnimation(animation)}>
-                    <p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_x`}>Property <strong>X</strong></li>
-                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_VECTOR3:
-                element = <li className={this.props.selected && this.props.selected.name === animation.name ? 'property sub active' : 'property sub'} key={i}>
-                    <div className={`animation-arrow ${this.state.list[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.editAnimation()} />
-                    {!(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.list[i].open ? '' : 'hidden'}`}>
-                        <li key={`${i}_x`} id={`${i}_x`} className="property" style={{ color: '#db3e3e' }} onClick={() => this.setSelectedCoordinate(animation, SelectedCoordinate.x, i)}>
-                            <div className={`handle-indicator ${this.state.selectedCoordinate === SelectedCoordinate.x && this.state.selectedAnimation === i ? 'show' : 'hide'}`}></div>
-                            {animation.targetProperty} X</li>
-                        <li key={`${i}_y`} id={`${i}_y`} className="property" style={{ color: '#51e22d' }} onClick={() => this.setSelectedCoordinate(animation, SelectedCoordinate.y, i)}>
-                            <div className={`handle-indicator ${this.state.selectedCoordinate === SelectedCoordinate.y && this.state.selectedAnimation === i ? 'show' : 'hide'}`}></div>
-                            {animation.targetProperty} Y</li>
-                        <li key={`${i}_z`} id={`${i}_z`} className="property" style={{ color: '#00a3ff' }} onClick={() => this.setSelectedCoordinate(animation, SelectedCoordinate.z, i)}>
-                            <div className={`handle-indicator ${this.state.selectedCoordinate === SelectedCoordinate.z && this.state.selectedAnimation === i ? 'show' : 'hide'}`}></div>
-                            {animation.targetProperty} Z</li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_QUATERNION:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_x`}>Property <strong>X</strong></li>
-                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
-                        <li key={`${i}_z`}>Property <strong>Z</strong></li>
-                        <li key={`${i}_w`}>Property <strong>W</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_COLOR3:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_r`}>Property <strong>R</strong></li>
-                        <li key={`${i}_g`}>Property <strong>G</strong></li>
-                        <li key={`${i}_b`}>Property <strong>B</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_COLOR4:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_r`}>Property <strong>R</strong></li>
-                        <li key={`${i}_g`}>Property <strong>G</strong></li>
-                        <li key={`${i}_b`}>Property <strong>B</strong></li>
-                        <li key={`${i}_a`}>Property <strong>A</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_SIZE:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_width`}>Property <strong>Width</strong></li>
-                        <li key={`${i}_height`}>Property <strong>Height</strong></li>
-                    </ul>
-                </li>
-                break;
-            default: console.log("not recognized");
-                element = null;
-                break;
-        }
+  setSelectedCoordinate(
+    animation: Animation,
+    coordinate: SelectedCoordinate,
+    index: number
+  ) {
+    this.setState({ selectedCoordinate: coordinate, selectedAnimation: index });
+    this.props.selectAnimation(animation, SelectedCoordinate.x);
+  }
 
-        return element;
-    }
+  setListItem(animation: Animation, i: number) {
+    let element;
+    this._list = this.generateList();
 
-    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);
-                            })}
+    if (this._list !== null) {
+      switch (animation.dataType) {
+        case Animation.ANIMATIONTYPE_FLOAT:
+          element = (
+            <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.editAnimation()}
+              />
+              {!(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>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_VECTOR2:
+          element = (
+            <li
+              className={
+                this.props.selected &&
+                this.props.selected.name === animation.name
+                  ? 'property active'
+                  : 'property'
+              }
+              key={i}
+              onClick={() => this.props.selectAnimation(animation)}
+            >
+              <p>{animation.targetProperty}</p>
+              <ul>
+                <li key={`${i}_x`}>
+                  Property <strong>X</strong>
+                </li>
+                <li key={`${i}_y`}>
+                  Property <strong>Y</strong>
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_VECTOR3:
+          element = (
+            <li
+              className={
+                this.props.selected &&
+                this.props.selected.name === animation.name
+                  ? 'property sub active'
+                  : 'property sub'
+              }
+              key={i}
+            >
+              <div
+                className={`animation-arrow ${
+                  this._list[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.editAnimation()}
+              />
+              {!(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._list[i].open ? '' : 'hidden'}`}>
+                <li
+                  key={`${i}_x`}
+                  id={`${i}_x`}
+                  className='property'
+                  style={{ color: '#db3e3e' }}
+                  onClick={() =>
+                    this.setSelectedCoordinate(
+                      animation,
+                      SelectedCoordinate.x,
+                      i
+                    )
+                  }
+                >
+                  <div
+                    className={`handle-indicator ${
+                      this.state.selectedCoordinate === SelectedCoordinate.x &&
+                      this.state.selectedAnimation === i
+                        ? 'show'
+                        : 'hide'
+                    }`}
+                  ></div>
+                  {animation.targetProperty} X
+                </li>
+                <li
+                  key={`${i}_y`}
+                  id={`${i}_y`}
+                  className='property'
+                  style={{ color: '#51e22d' }}
+                  onClick={() =>
+                    this.setSelectedCoordinate(
+                      animation,
+                      SelectedCoordinate.y,
+                      i
+                    )
+                  }
+                >
+                  <div
+                    className={`handle-indicator ${
+                      this.state.selectedCoordinate === SelectedCoordinate.y &&
+                      this.state.selectedAnimation === i
+                        ? 'show'
+                        : 'hide'
+                    }`}
+                  ></div>
+                  {animation.targetProperty} Y
+                </li>
+                <li
+                  key={`${i}_z`}
+                  id={`${i}_z`}
+                  className='property'
+                  style={{ color: '#00a3ff' }}
+                  onClick={() =>
+                    this.setSelectedCoordinate(
+                      animation,
+                      SelectedCoordinate.z,
+                      i
+                    )
+                  }
+                >
+                  <div
+                    className={`handle-indicator ${
+                      this.state.selectedCoordinate === SelectedCoordinate.z &&
+                      this.state.selectedAnimation === i
+                        ? 'show'
+                        : 'hide'
+                    }`}
+                  ></div>
+                  {animation.targetProperty} Z
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_QUATERNION:
+          element = (
+            <li className='property' key={i}>
+              <p>{animation.targetProperty}</p>
+              <ul>
+                <li key={`${i}_x`}>
+                  Property <strong>X</strong>
+                </li>
+                <li key={`${i}_y`}>
+                  Property <strong>Y</strong>
+                </li>
+                <li key={`${i}_z`}>
+                  Property <strong>Z</strong>
+                </li>
+                <li key={`${i}_w`}>
+                  Property <strong>W</strong>
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_COLOR3:
+          element = (
+            <li className='property' key={i}>
+              <p>{animation.targetProperty}</p>
+              <ul>
+                <li key={`${i}_r`}>
+                  Property <strong>R</strong>
+                </li>
+                <li key={`${i}_g`}>
+                  Property <strong>G</strong>
+                </li>
+                <li key={`${i}_b`}>
+                  Property <strong>B</strong>
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_COLOR4:
+          element = (
+            <li className='property' key={i}>
+              <p>{animation.targetProperty}</p>
+              <ul>
+                <li key={`${i}_r`}>
+                  Property <strong>R</strong>
+                </li>
+                <li key={`${i}_g`}>
+                  Property <strong>G</strong>
+                </li>
+                <li key={`${i}_b`}>
+                  Property <strong>B</strong>
+                </li>
+                <li key={`${i}_a`}>
+                  Property <strong>A</strong>
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        case Animation.ANIMATIONTYPE_SIZE:
+          element = (
+            <li className='property' key={i}>
+              <p>{animation.targetProperty}</p>
+              <ul>
+                <li key={`${i}_width`}>
+                  Property <strong>Width</strong>
+                </li>
+                <li key={`${i}_height`}>
+                  Property <strong>Height</strong>
+                </li>
+              </ul>
+            </li>
+          );
+          break;
+        default:
+          console.log('not recognized');
+          element = null;
+          break;
+      }
 
-                </ul>
-            </div>
-        )
+      return element;
+    } else {
+      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>
+    );
+  }
+}

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx

@@ -247,6 +247,7 @@ export class AnimationGridComponent extends React.Component<
                     }
                     lockObject={this.props.lockObject}
                     playOrPause={() => this.playOrPause()}
+                    globalState={this.props.globalState}
                   />
                 </PopupComponent>
               )}

+ 20 - 8
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss

@@ -757,15 +757,16 @@ body {
           margin-left: 75px;
           height: 20px;
           margin-top: 10px;
-          button {
+          button,
+          label {
             border: none;
             font-size: 12px;
             margin: 0px;
-            width: 60px;
+            width: 127px;
             background-color: #444;
             height: 20px;
             font-family: acumin-pro-condensed;
-            padding-top: 1px;
+            padding-top: 0px;
           }
         }
 
@@ -786,6 +787,7 @@ body {
               font-family: acumin-pro-condensed;
               padding-bottom: 6px;
               padding-left: 6px;
+              color: white;
             }
           }
         }
@@ -852,7 +854,7 @@ body {
         }
 
         .buttonLine {
-          width: 80px;
+          width: 105px;
           padding: 0px;
           display: block;
           margin-left: 75px;
@@ -862,12 +864,22 @@ body {
             border: none;
             font-size: 12px;
             margin: 0px;
-            width: 80px;
             background-color: #444444;
             height: 20px;
+            width: 105px;
             font-family: acumin-pro-condensed;
             padding-top: 1px;
           }
+
+          &:first-child {
+            width: 105px;
+          }
+          &:nth-child(2) {
+            width: 60px;
+            button {
+              width: 60px;
+            }
+          }
         }
 
         .textInputLine {
@@ -1089,8 +1101,8 @@ body {
       .linear {
         overflow: visible;
         border: 0px solid white;
-        height: 362px;
-        width: 780px;
+        height: 100%;
+        width: 100%;
         outline: none;
 
         svg {
@@ -1126,7 +1138,7 @@ body {
       }
 
       .playhead-wrapper {
-        left: -13px;
+        left: 20px;
         bottom: 366px;
         position: relative;
       }

+ 34 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -12,6 +12,7 @@ import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
 import { LoadSnippet } from './loadsnippet';
 import { SaveSnippet } from './saveSnippet';
 import { LockObject } from '../lockObject';
+import { GlobalState } from '../../../../globalState';
 
 interface IEditorControlsProps {
   isTargetedAnimation: boolean;
@@ -21,6 +22,8 @@ interface IEditorControlsProps {
   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
   setNotificationMessage: (message: string) => void;
   selectAnimation: (selected: Animation, axis?: SelectedCoordinate) => void;
+  globalState: GlobalState;
+  snippetServer: string;
 }
 
 export class EditorControls extends React.Component<
@@ -33,6 +36,7 @@ export class EditorControls extends React.Component<
     isLoopActive: boolean;
     animationsCount: number;
     framesPerSecond: number;
+    snippetId: string;
   }
 > {
   constructor(props: IEditorControlsProps) {
@@ -48,6 +52,7 @@ export class EditorControls extends React.Component<
       isLoopActive: false,
       animationsCount: count,
       framesPerSecond: 60,
+      snippetId: '',
     };
   }
 
@@ -122,6 +127,16 @@ export class EditorControls extends React.Component<
     });
   }
 
+  animationsLoaded(numberOfAnimations: number) {
+    this.setState({
+      animationsCount: numberOfAnimations,
+      isEditTabOpen: true,
+      isAnimationTabOpen: false,
+      isLoadTabOpen: false,
+      isSaveTabOpen: false,
+    });
+  }
+
   render() {
     return (
       <div className='animation-list'>
@@ -197,11 +212,28 @@ export class EditorControls extends React.Component<
         )}
 
         {this.state.isLoadTabOpen ? (
-          <LoadSnippet lockObject={this.props.lockObject} animations={[]} />
+          <LoadSnippet
+            animationsLoaded={(numberOfAnimations: number) =>
+              this.animationsLoaded(numberOfAnimations)
+            }
+            lockObject={this.props.lockObject}
+            animations={[]}
+            snippetServer={this.props.snippetServer}
+            globalState={this.props.globalState}
+            setSnippetId={(id: string) => this.setState({ snippetId: id })}
+            entity={this.props.entity}
+            setNotificationMessage={this.props.setNotificationMessage}
+          />
         ) : null}
 
         {this.state.isSaveTabOpen ? (
-          <SaveSnippet lockObject={this.props.lockObject} animations={[]} />
+          <SaveSnippet
+            lockObject={this.props.lockObject}
+            animations={(this.props.entity as IAnimatable).animations}
+            snippetServer={this.props.snippetServer}
+            globalState={this.props.globalState}
+            snippetId={this.state.snippetId}
+          />
         ) : null}
 
         {this.state.isEditTabOpen ? (

+ 95 - 20
inspector/src/components/actionTabs/tabs/propertyGrids/animations/loadsnippet.tsx

@@ -1,49 +1,124 @@
-import * as React from "react";
-import { Observable } from "babylonjs/Misc/observable";
-import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
-import { Animation } from "babylonjs/Animations/animation";
-import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
-import { TextInputLineComponent } from "../../../lines/textInputLineComponent";
-import { LockObject } from "../lockObject";
+import * as React from 'react';
+import { Observable } from 'babylonjs/Misc/observable';
+import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
+import { Animation } from 'babylonjs/Animations/animation';
+import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { FileButtonLineComponent } from '../../../lines/fileButtonLineComponent';
+import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
+import { LockObject } from '../lockObject';
+import { Tools } from 'babylonjs/Misc/tools';
+import { GlobalState } from '../../../../globalState';
+import { ReadFileError } from 'babylonjs/Misc/fileTools';
+import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
 
 interface ILoadSnippetProps {
   animations: Animation[];
   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
   lockObject: LockObject;
+  globalState: GlobalState;
+  snippetServer: string;
+  setSnippetId: (id: string) => void;
+  entity: IAnimatable | TargetedAnimation;
+  setNotificationMessage: (message: string) => void;
+  animationsLoaded: (numberOfAnimations: number) => void;
 }
 
 export class LoadSnippet extends React.Component<
   ILoadSnippetProps,
-  { server: string }
+  { snippetId: string }
 > {
   private _serverAddress: string;
   constructor(props: ILoadSnippetProps) {
     super(props);
-    this._serverAddress = "-";
-    this.state = { server: "" };
+    this._serverAddress = this.props.snippetServer;
+    this.state = { snippetId: '' };
   }
 
   change(value: string) {
-    this.setState({ server: value });
+    this.setState({ snippetId: value });
+    this.props.setSnippetId(value);
+  }
+
+  loadFromFile(file: File) {
+    Tools.ReadFile(
+      file,
+      (data) => {
+        let decoder = new TextDecoder('utf-8');
+        let jsonObject = JSON.parse(decoder.decode(data));
+        var result: Animation[] = [];
+
+        for (var i in jsonObject) {
+          result.push(Animation.Parse(jsonObject[i]));
+        }
+
+        if (this.props.entity) {
+          (this.props.entity as IAnimatable).animations = result;
+          var e = new PropertyChangedEvent();
+          e.object = this.props.entity;
+          e.property = 'animations';
+          e.value = (this.props.entity as IAnimatable).animations;
+          this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
+          this.props.animationsLoaded(result.length);
+        }
+      },
+      undefined,
+      true,
+      (error: ReadFileError) => {
+        console.log(error.message);
+      }
+    );
+  }
+
+  loadFromSnippet() {
+    if (this.state.snippetId !== '') {
+      //How to dispose() previous animations;
+      //How to notify observers
+      Animation.CreateFromSnippetAsync(this.state.snippetId)
+        .then((newAnimations) => {
+          // Explore how observers are notified from snippet
+          if (newAnimations instanceof Array) {
+            (this.props.entity as IAnimatable).animations = newAnimations;
+          }
+
+          if (newAnimations instanceof Animation) {
+            (this.props.entity as IAnimatable).animations?.push(newAnimations);
+          }
+        })
+        .catch((err) => {
+          this.props.setNotificationMessage(
+            `Unable to load your animations: ${err}`
+          );
+        });
+    } else {
+      this.props.setNotificationMessage(`You need to add an snippet id`);
+    }
   }
 
   render() {
     return (
-      <div className="load-container">
+      <div className='load-container'>
         <TextInputLineComponent
-          label="Snippet Server"
+          label='Snippet Id'
           lockObject={this.props.lockObject}
-          value={this.state.server}
+          value={this.state.snippetId}
           onChange={(value: string) => this.change(value)}
         />
-        <ButtonLineComponent label="Load" onClick={() => {}} />
-        <div className="load-browse">
+        <ButtonLineComponent
+          label='Load from snippet server'
+          onClick={() => this.loadFromSnippet()}
+        />
+        <div className='load-browse'>
           <p>Local File</p>
-          <ButtonLineComponent label="Browse" onClick={() => {}} />
+          <FileButtonLineComponent
+            label='Load'
+            onClick={(file) => this.loadFromFile(file)}
+            accept='.json'
+          />
         </div>
-        <div className="load-server">
-          <p>Snippet Server : </p>
-          <p>{this._serverAddress}</p>
+        <div className='load-server'>
+          <p>Snippet Server: </p>&nbsp;
+          <p> {this._serverAddress ?? '-'}</p>
         </div>
       </div>
     );

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

@@ -47,7 +47,7 @@ export class Playhead extends React.Component<IPlayheadProps> {
   }
 
   calculateMove() {
-    return `calc(${this.props.frame * this.props.offset}px - 13px)`;
+    return `calc(${this.props.frame * this.props.offset}px + 20px)`;
   }
 
   render() {

+ 116 - 24
inspector/src/components/actionTabs/tabs/propertyGrids/animations/saveSnippet.tsx

@@ -1,15 +1,26 @@
-import * as React from "react";
-import { Observable } from "babylonjs/Misc/observable";
-import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
-import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
-import { LockObject } from "../lockObject";
+import * as React from 'react';
+import { Observable } from 'babylonjs/Misc/observable';
+import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
+import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { Tools } from 'babylonjs/Misc/tools';
+import { Animation } from 'babylonjs/Animations/animation';
+import { LockObject } from '../lockObject';
+import { Nullable } from 'babylonjs/types';
+import { GlobalState } from '../../../../globalState';
 
 interface ISaveSnippetProps {
-  animations: any[];
+  animations: Nullable<Animation[]>;
   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
   lockObject: LockObject;
+  globalState: GlobalState;
+  snippetServer: string;
+  snippetId: string;
 }
 
+export interface Snippet {
+  url: string;
+  id: string;
+}
 interface SelectedAnimation {
   id: string;
   name: string;
@@ -21,11 +32,9 @@ export class SaveSnippet extends React.Component<
   ISaveSnippetProps,
   { selectedAnimations: SelectedAnimation[] }
 > {
-  private _serverAddress: string;
   constructor(props: ISaveSnippetProps) {
     super(props);
-    this._serverAddress = "-";
-    let animList = this.props.animations.map((animation, i) => {
+    let animList = this.props.animations?.map((animation, i) => {
       return {
         id: `${animation.name}_${animation.targetProperty}`,
         name: animation.name,
@@ -33,15 +42,15 @@ export class SaveSnippet extends React.Component<
         selected: false,
       };
     });
-    this.state = { selectedAnimations: animList };
+    this.state = { selectedAnimations: animList ?? [] };
   }
 
   handleCheckboxChange(e: React.ChangeEvent<HTMLInputElement>) {
     e.preventDefault();
 
-    let index = parseInt(e.target.id.replace("save_", ""));
+    let index = parseInt(e.target.id.replace('save_', ''));
 
-    let updated = this.state.selectedAnimations.map((item) => {
+    let updated = this.state.selectedAnimations?.map((item) => {
       if (item.index === index) {
         item.selected = !item.selected;
       }
@@ -51,24 +60,102 @@ export class SaveSnippet extends React.Component<
     this.setState({ selectedAnimations: updated });
   }
 
+  stringifySelectedAnimations() {
+    const content: string[] = [];
+    this.state.selectedAnimations.forEach((animation) => {
+      if (animation.selected) {
+        const selected =
+          this.props.animations && this.props.animations[animation.index];
+        if (selected) {
+          content.push(selected.serialize());
+        }
+      }
+    });
+    return JSON.stringify(content);
+  }
+
+  saveToFile() {
+    const content = this.stringifySelectedAnimations();
+    Tools.Download(new Blob([content]), 'animations.json');
+  }
+
+  saveToSnippet() {
+    if (this.props.snippetId !== '') {
+      let serverId = this.props.snippetId;
+      const serverUrl = this.props.snippetServer;
+      const content = this.stringifySelectedAnimations();
+
+      var xmlHttp = new XMLHttpRequest();
+      xmlHttp.onreadystatechange = () => {
+        if (xmlHttp.readyState == 4) {
+          if (xmlHttp.status == 200) {
+            var snippet = JSON.parse(xmlHttp.responseText);
+            const oldId = serverId;
+            serverId = snippet.id;
+            if (snippet.version && snippet.version != '0') {
+              serverId += '#' + snippet.version;
+            }
+            this.forceUpdate();
+            if (navigator.clipboard) {
+              navigator.clipboard.writeText(serverId);
+            }
+
+            let windowAsAny = window as any;
+
+            if (windowAsAny.Playground && oldId) {
+              windowAsAny.Playground.onRequestCodeChangeObservable.notifyObservers(
+                {
+                  regex: new RegExp(oldId, 'g'),
+                  replace: serverId,
+                }
+              );
+            }
+
+            alert(
+              'Animations saved with ID: ' +
+                serverId +
+                ' (please note that the id was also saved to your clipboard)'
+            );
+          } else {
+            alert('Unable to save your animations');
+          }
+        }
+      };
+
+      xmlHttp.open('POST', serverUrl + (serverId ? '/' + serverId : ''), true);
+      xmlHttp.setRequestHeader('Content-Type', 'application/json');
+
+      var dataToSend = {
+        payload: JSON.stringify({
+          animations: content,
+        }),
+        name: '',
+        description: '',
+        tags: '',
+      };
+
+      xmlHttp.send(JSON.stringify(dataToSend));
+    }
+  }
+
   render() {
     return (
-      <div className="save-container">
-        <div className="item-list">
+      <div className='save-container'>
+        <div className='item-list'>
           <ul>
-            {this.props.animations.map((animation, i) => {
+            {this.props.animations?.map((animation, i) => {
               return (
                 <li key={i}>
                   <div>
                     <label>
                       <input
                         id={`save_${i}`}
-                        name={`save_${animation.name}`}
-                        type="checkbox"
+                        name={`save_${animation?.name}`}
+                        type='checkbox'
                         checked={this.state.selectedAnimations[i].selected}
                         onChange={(e) => this.handleCheckboxChange(e)}
                       />
-                      {animation.name}
+                      {animation?.name}
                     </label>
                   </div>
                 </li>
@@ -76,13 +163,18 @@ export class SaveSnippet extends React.Component<
             })}
           </ul>
         </div>
-        <div className="save-buttons">
-          <ButtonLineComponent label="Save Snippet" onClick={() => {}} />
-          <ButtonLineComponent label="Save File" onClick={() => {}} />
+        <div className='save-buttons'>
+          {this.props.snippetId !== '' ? (
+            <ButtonLineComponent
+              label='Save to snippet server'
+              onClick={() => this.saveToSnippet()}
+            />
+          ) : null}
+          <ButtonLineComponent label='Save' onClick={() => this.saveToFile()} />
         </div>
-        <div className="save-server">
-          <p>Snippet Server:</p>
-          <p>{this._serverAddress}</p>
+        <div className='save-server'>
+          <p>Snippet Server: </p>&nbsp;
+          <p> {this.props.snippetServer ?? '-'}</p>
         </div>
       </div>
     );

+ 213 - 201
inspector/src/components/actionTabs/tabs/propertyGrids/animations/svgDraggableArea.tsx

@@ -1,230 +1,242 @@
-import * as React from "react";
+import * as React from 'react';
 import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { KeyframeSvgPoint, IKeyframeSvgPoint } from './keyframeSvgPoint';
 
 interface ISvgDraggableAreaProps {
-    keyframeSvgPoints: IKeyframeSvgPoint[];
-    updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void;
-    scale: number;
-    viewBoxScale: number;
-    selectKeyframe: (id: string) => void;
-    selectedControlPoint: (type: string, id: string) => void;
+  keyframeSvgPoints: IKeyframeSvgPoint[];
+  updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void;
+  scale: number;
+  viewBoxScale: number;
+  selectKeyframe: (id: string) => void;
+  selectedControlPoint: (type: string, id: string) => void;
 }
 
-export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
-
-    private _active: boolean;
-    private _isCurrentPointControl: string;
-    private _currentPointId: string;
-    private _draggableArea: React.RefObject<SVGSVGElement>;
-    private _panStart: Vector2;
-    private _panStop: Vector2;
-    private _width: number;
-
-    constructor(props: ISvgDraggableAreaProps) {
-        super(props);
-        this._currentPointId = "";
-        this._isCurrentPointControl = "";
-        this._draggableArea = React.createRef();
-        this._panStart = new Vector2(0, 0);
-        this._panStop = new Vector2(0, 0);
+export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps> {
+  private _active: boolean;
+  private _isCurrentPointControl: string;
+  private _currentPointId: string;
+  private _draggableArea: React.RefObject<SVGSVGElement>;
+  private _panStart: Vector2;
+  private _panStop: Vector2;
+  private _width: number;
+
+  constructor(props: ISvgDraggableAreaProps) {
+    super(props);
+    this._currentPointId = '';
+    this._isCurrentPointControl = '';
+    this._draggableArea = React.createRef();
+    this._panStart = new Vector2(0, 0);
+    this._panStop = new Vector2(0, 0);
+  }
+
+  componentDidMount() {
+    this._draggableArea.current?.addEventListener(
+      'keydown',
+      this.keyDown.bind(this)
+    );
+    this._draggableArea.current?.addEventListener(
+      'keyup',
+      this.keyUp.bind(this)
+    );
+    setTimeout(() => {
+      this._width =
+        this._draggableArea.current?.clientWidth !== undefined
+          ? this._draggableArea.current?.clientWidth
+          : 0;
+      console.log(this._width);
+    }, 500);
+  }
+
+  dragStart(e: React.TouchEvent<SVGSVGElement>): void;
+  dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragStart(e: any): void {
+    e.preventDefault();
+    if (e.target.classList.contains('draggable')) {
+      this._active = true;
+      this._currentPointId = e.target.getAttribute('data-id');
+
+      if (e.target.classList.contains('control-point')) {
+        this._isCurrentPointControl = e.target.getAttribute('type');
+      }
     }
 
-    componentDidMount() {
-        this._draggableArea.current?.addEventListener("keydown", this.keyDown.bind(this));
-        this._draggableArea.current?.addEventListener("keyup", this.keyUp.bind(this));
-        setTimeout(() => {
-            this._width = this._draggableArea.current?.clientWidth !== undefined ? this._draggableArea.current?.clientWidth : 0;
-            console.log(this._width);
-        }, 500);
+    if (e.target.classList.contains('pannable')) {
+      if (e.buttons === 1 && e.ctrlKey) {
+        this._panStart.set(e.clientX, e.clientY);
+      }
     }
-
-    dragStart(e: React.TouchEvent<SVGSVGElement>): void;
-    dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragStart(e: any): void {
-        e.preventDefault();
-        if (e.target.classList.contains("draggable")) {
-            this._active = true;
-            this._currentPointId = e.target.getAttribute('data-id');
-
-            if (e.target.classList.contains("control-point")) {
-                this._isCurrentPointControl = e.target.getAttribute("type");
-            }
-        }
-
-        if (e.target.classList.contains("pannable")) {
-            if (e.buttons === 1 && e.ctrlKey) {
-                this._panStart.set(e.clientX, e.clientY);
-            }
+  }
+
+  drag(e: React.TouchEvent<SVGSVGElement>): void;
+  drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  drag(e: any): void {
+    if (this._active) {
+      e.preventDefault();
+
+      var coord = this.getMousePosition(e);
+
+      if (coord !== undefined) {
+        var newPoints = [...this.props.keyframeSvgPoints];
+
+        let point = newPoints.find((kf) => kf.id === this._currentPointId);
+        if (point) {
+          // Check for NaN values here.
+          if (this._isCurrentPointControl === 'left') {
+            point.leftControlPoint = coord;
+          } else if (this._isCurrentPointControl === 'right') {
+            point.rightControlPoint = coord;
+          } else {
+            point.keyframePoint = coord;
+          }
+          this.props.updatePosition(point, this._currentPointId);
         }
+      }
     }
-
-    drag(e: React.TouchEvent<SVGSVGElement>): void;
-    drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    drag(e: any): void {
-        if (this._active) {
-
-            e.preventDefault();
-
-            var coord = this.getMousePosition(e);
-
-            if (coord !== undefined) {
-
-                var newPoints = [...this.props.keyframeSvgPoints];
-
-                let point = newPoints.find(kf => kf.id === this._currentPointId);
-                if (point) {
-                    // Check for NaN values here. 
-                    if (this._isCurrentPointControl === "left") {
-                        point.leftControlPoint = coord;
-                    } else if (this._isCurrentPointControl === "right") {
-                        point.rightControlPoint = coord;
-                    } else {
-                        point.keyframePoint = coord;
-                    }
-                    this.props.updatePosition(point, this._currentPointId);
-                }
-            }
-        }
+  }
+
+  dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
+  dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragEnd(e: any): void {
+    e.preventDefault();
+    this._active = false;
+    this._currentPointId = '';
+    this._isCurrentPointControl = '';
+
+    if (e.target.classList.contains('pannable')) {
+      if (this._panStart.x !== 0 && this._panStart.y !== 0) {
+        this._panStop.set(e.clientX, e.clientY);
+        this.panDirection();
+      }
     }
-
-    dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
-    dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragEnd(e: any): void {
-        e.preventDefault();
-        this._active = false;
-        this._currentPointId = "";
-        this._isCurrentPointControl = "";
-
-        if (e.target.classList.contains("pannable")) {
-            if (this._panStart.x !== 0 && this._panStart.y !== 0) {
-                this._panStop.set(e.clientX, e.clientY);
-                this.panDirection();
-            }
-        }
+  }
+
+  getMousePosition(e: React.TouchEvent<SVGSVGElement>): Vector2 | undefined;
+  getMousePosition(
+    e: React.MouseEvent<SVGSVGElement, MouseEvent>
+  ): Vector2 | undefined;
+  getMousePosition(e: any): Vector2 | undefined {
+    if (e.touches) {
+      e = e.touches[0];
     }
 
-    getMousePosition(e: React.TouchEvent<SVGSVGElement>): Vector2 | undefined;
-    getMousePosition(e: React.MouseEvent<SVGSVGElement, MouseEvent>): Vector2 | undefined;
-    getMousePosition(e: any): Vector2 | undefined {
-
-        if (e.touches) { e = e.touches[0]; }
-
-        if (this._draggableArea.current) {
-            var svg = this._draggableArea.current as SVGSVGElement;
-            var CTM = svg.getScreenCTM();
-            if (CTM) {
-                return new Vector2((e.clientX - CTM.e) / CTM.a, (e.clientY - CTM.f) / CTM.d);
-            } else {
-                return undefined;
-            }
-        } else {
-            return undefined;
-        }
+    if (this._draggableArea.current) {
+      var svg = this._draggableArea.current as SVGSVGElement;
+      var CTM = svg.getScreenCTM();
+      if (CTM) {
+        return new Vector2(
+          (e.clientX - CTM.e) / CTM.a,
+          (e.clientY - CTM.f) / CTM.d
+        );
+      } else {
+        return undefined;
+      }
+    } else {
+      return undefined;
     }
+  }
 
-    panDirection() {
-
-        // Movement Right to Left
-        if (this._panStart.x > this._panStop.x) {
-            console.log("right to left");
-            this.panTo("right", Math.abs(this._panStart.x - this._panStop.x));
-        }
-
-        // Movement Right to Left
-        if (this._panStart.x < this._panStop.x) {
-            this.panTo("left", Math.abs(this._panStart.x - this._panStop.x));
-            console.log("left to right");
-        }
-
-        // Movement Bottom to Up
-        if (this._panStart.y > this._panStop.y) {
-            console.log("down up");
-        }
-
-        // Movement Up to Bottom
-        if (this._panStart.y < this._panStop.y) {
-            console.log("up down");
-        }
-
-        this._panStart.set(0, 0);
-        this._panStop.set(0, 0);
+  panDirection() {
+    // Movement Right to Left
+    if (this._panStart.x > this._panStop.x) {
+      console.log('right to left');
+      this.panTo('right', Math.abs(this._panStart.x - this._panStop.x));
+    }
 
+    // Movement Right to Left
+    if (this._panStart.x < this._panStop.x) {
+      this.panTo('left', Math.abs(this._panStart.x - this._panStop.x));
+      console.log('left to right');
     }
 
-    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;
-        }
+    // Movement Bottom to Up
+    if (this._panStart.y > this._panStop.y) {
+      console.log('down up');
     }
 
-    keyDown(e: KeyboardEvent) {
-        e.preventDefault();
-        if (e.keyCode === 17) {
-            this._draggableArea.current?.style.setProperty("cursor", "grab");
-        }
+    // Movement Up to Bottom
+    if (this._panStart.y < this._panStop.y) {
+      console.log('up down');
     }
 
-    keyUp(e: KeyboardEvent) {
-        e.preventDefault();
-        if (e.keyCode === 17) {
-            this._draggableArea.current?.style.setProperty("cursor", "initial");
-        }
+    this._panStart.set(0, 0);
+    this._panStop.set(0, 0);
+  }
+
+  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;
     }
+  }
 
-    focus(e: React.MouseEvent<SVGSVGElement>) {
-        e.preventDefault();
-        this._draggableArea.current?.focus();
+  keyDown(e: KeyboardEvent) {
+    e.preventDefault();
+    if (e.keyCode === 17) {
+      this._draggableArea.current?.style.setProperty('cursor', 'grab');
     }
+  }
 
-    render() {
-        return (
-            <>
-                <svg className="linear pannable" ref={this._draggableArea} tabIndex={0}
-
-                    onMouseMove={(e) => this.drag(e)}
-                    onTouchMove={(e) => this.drag(e)}
-                    onTouchStart={(e) => this.dragStart(e)}
-                    onTouchEnd={(e) => this.dragEnd(e)}
-
-                    onMouseDown={(e) => this.dragStart(e)}
-                    onMouseUp={(e) => this.dragEnd(e)}
-                    onMouseLeave={(e) => this.dragEnd(e)}
-                    // Add way to add new keyframe
-                    onClick={(e) => this.focus(e)}
-
-                    viewBox={`0 0 ${Math.round(this.props.scale * 200)} ${Math.round(this.props.scale * 100)}`}>
-
-                    {this.props.children}
-                    {this.props.keyframeSvgPoints.map((keyframe, i) =>
-                        <KeyframeSvgPoint
-                            key={`${keyframe.id}_${i}`}
-                            id={keyframe.id}
-                            keyframePoint={keyframe.keyframePoint}
-                            leftControlPoint={keyframe.leftControlPoint}
-                            rightControlPoint={keyframe.rightControlPoint}
-                            isLeftActive={keyframe.isLeftActive}
-                            isRightActive={keyframe.isRightActive}
-                            selected={keyframe.selected}
-                            selectedControlPoint={(type: string, id: string) => this.props.selectedControlPoint(type, id)}
-                            selectKeyframe={(id: string) => this.props.selectKeyframe(id)} />
-                    )}
-                </svg>
-            </>)
+  keyUp(e: KeyboardEvent) {
+    e.preventDefault();
+    if (e.keyCode === 17) {
+      this._draggableArea.current?.style.setProperty('cursor', 'initial');
     }
+  }
+
+  focus(e: React.MouseEvent<SVGSVGElement>) {
+    e.preventDefault();
+    this._draggableArea.current?.focus();
+  }
+
+  render() {
+    return (
+      <>
+        <svg
+          className='linear pannable'
+          ref={this._draggableArea}
+          tabIndex={0}
+          onMouseMove={(e) => this.drag(e)}
+          onTouchMove={(e) => this.drag(e)}
+          onTouchStart={(e) => this.dragStart(e)}
+          onTouchEnd={(e) => this.dragEnd(e)}
+          onMouseDown={(e) => this.dragStart(e)}
+          onMouseUp={(e) => this.dragEnd(e)}
+          onMouseLeave={(e) => this.dragEnd(e)}
+          // Add way to add new keyframe
+          onClick={(e) => this.focus(e)}
+          viewBox={`0 0 ${Math.round(this.props.scale * 200)} ${Math.round(
+            this.props.scale * 100
+          )}`}
+        >
+          {this.props.children}
+          {this.props.keyframeSvgPoints.map((keyframe, i) => (
+            <KeyframeSvgPoint
+              key={`${keyframe.id}_${i}`}
+              id={keyframe.id}
+              keyframePoint={keyframe.keyframePoint}
+              leftControlPoint={keyframe.leftControlPoint}
+              rightControlPoint={keyframe.rightControlPoint}
+              isLeftActive={keyframe.isLeftActive}
+              isRightActive={keyframe.isRightActive}
+              selected={keyframe.selected}
+              selectedControlPoint={(type: string, id: string) =>
+                this.props.selectedControlPoint(type, id)
+              }
+              selectKeyframe={(id: string) => this.props.selectKeyframe(id)}
+            />
+          ))}
+        </svg>
+      </>
+    );
+  }
 }
-
-
-
-
-

+ 25 - 24
inspector/src/components/actionTabs/tabs/propertyGrids/animations/targetedAnimationPropertyGridComponent.tsx

@@ -1,19 +1,19 @@
-import * as React from "react";
+import * as React from 'react';
 
-import { Observable } from "babylonjs/Misc/observable";
-import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
-import { Scene } from "babylonjs/scene";
+import { Observable } from 'babylonjs/Misc/observable';
+import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
+import { Scene } from 'babylonjs/scene';
 
-import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
-import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
-import { LineContainerComponent } from "../../../lineContainerComponent";
-import { TextLineComponent } from "../../../lines/textLineComponent";
-import { LockObject } from "../lockObject";
-import { GlobalState } from "../../../../globalState";
-import { TextInputLineComponent } from "../../../lines/textInputLineComponent";
-import { PopupComponent } from "../animations/popupComponent";
-import { AnimationCurveEditorComponent } from "../animations/animationCurveEditorComponent";
-import { AnimationGroup } from "babylonjs/Animations/animationGroup";
+import { PropertyChangedEvent } from '../../../../propertyChangedEvent';
+import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { LineContainerComponent } from '../../../lineContainerComponent';
+import { TextLineComponent } from '../../../lines/textLineComponent';
+import { LockObject } from '../lockObject';
+import { GlobalState } from '../../../../globalState';
+import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
+import { PopupComponent } from '../animations/popupComponent';
+import { AnimationCurveEditorComponent } from '../animations/animationCurveEditorComponent';
+import { AnimationGroup } from 'babylonjs/Animations/animationGroup';
 
 interface ITargetedAnimationGridComponentProps {
   globalState: GlobalState;
@@ -83,25 +83,25 @@ export class TargetedAnimationGridComponent extends React.Component<
     const targetedAnimation = this.props.targetedAnimation;
 
     return (
-      <div className="pane">
+      <div className='pane'>
         <LineContainerComponent
           globalState={this.props.globalState}
-          title="GENERAL"
+          title='GENERAL'
         >
           <TextLineComponent
-            label="Class"
+            label='Class'
             value={targetedAnimation.getClassName()}
           />
           <TextInputLineComponent
             lockObject={this.props.lockObject}
-            label="Name"
+            label='Name'
             target={targetedAnimation.animation}
-            propertyName="name"
+            propertyName='name'
             onPropertyChangedObservable={this.props.onPropertyChangedObservable}
           />
           {targetedAnimation.target.name && (
             <TextLineComponent
-              label="Target"
+              label='Target'
               value={targetedAnimation.target.name}
               onLink={() =>
                 this.props.globalState.onSelectionChangedObservable.notifyObservers(
@@ -111,13 +111,13 @@ export class TargetedAnimationGridComponent extends React.Component<
             />
           )}
           <ButtonLineComponent
-            label="Edit animation"
+            label='Edit animation'
             onClick={() => this.onOpenAnimationCurveEditor()}
           />
           {this._isCurveEditorOpen && (
             <PopupComponent
-              id="curve-editor"
-              title="Curve Animation Editor"
+              id='curve-editor'
+              title='Curve Animation Editor'
               size={{ width: 1024, height: 512 }}
               onOpen={(window: Window) => {}}
               onClose={(window: Window) =>
@@ -129,12 +129,13 @@ export class TargetedAnimationGridComponent extends React.Component<
                 entity={targetedAnimation as any}
                 playOrPause={() => this.playOrPause()}
                 lockObject={this.props.lockObject}
+                globalState={this.props.globalState}
                 close={(event) => this.onCloseAnimationCurveEditor(event.view)}
               />
             </PopupComponent>
           )}
           <ButtonLineComponent
-            label="Dispose"
+            label='Dispose'
             onClick={() => this.deleteAnimation()}
           />
         </LineContainerComponent>

+ 21 - 19
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -226,25 +226,7 @@ export class Timeline extends React.Component<
                   y2='40'
                   style={{ stroke: '#12506b', strokeWidth: 6 }}
                 />
-                {this.props.keyframes &&
-                  this.props.keyframes.map((kf, i) => {
-                    return (
-                      <svg
-                        key={`kf_${i}`}
-                        style={{ cursor: 'pointer' }}
-                        tabIndex={i + 40}
-                      >
-                        <line
-                          id={`kf_${i.toString()}`}
-                          x1={kf.frame * 10}
-                          y1='0'
-                          x2={kf.frame * 10}
-                          y2='40'
-                          style={{ stroke: 'red', strokeWidth: 6 }}
-                        />
-                      </svg>
-                    );
-                  })}
+
                 {this._frames.map((frame, i) => {
                   return (
                     <svg key={`tl_${i}`}>
@@ -269,6 +251,26 @@ export class Timeline extends React.Component<
                     </svg>
                   );
                 })}
+
+                {this.props.keyframes &&
+                  this.props.keyframes.map((kf, i) => {
+                    return (
+                      <svg
+                        key={`kf_${i}`}
+                        style={{ cursor: 'pointer' }}
+                        tabIndex={i + 40}
+                      >
+                        <line
+                          id={`kf_${i.toString()}`}
+                          x1={kf.frame * 10}
+                          y1='0'
+                          x2={kf.frame * 10}
+                          y2='40'
+                          style={{ stroke: '#ffc017', strokeWidth: 1 }}
+                        />
+                      </svg>
+                    );
+                  })}
               </svg>
             </div>