Kaynağa Gözat

Multi curves (#8341)

* refactoring

* Multiselect

* whats new

Co-authored-by: Alejandro Toledo <alex@pixelspace.com>
Alejandro Toledo Martinez 5 yıl önce
ebeveyn
işleme
2fe48d96db

+ 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))
 

+ 349 - 237
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -1,6 +1,8 @@
 import * as React from "react";
 import { Animation } from 'babylonjs/Animations/animation';
-import { Vector2 } from 'babylonjs/Maths/math.vector';
+import { Vector2, Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
+import { Color3, Color4 } from 'babylonjs/Maths/math.color';
+import { Size } from 'babylonjs/Maths/math.size';
 import { EasingFunction } from 'babylonjs/Animations/easing';
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
 import { IKeyframeSvgPoint } from './keyframeSvgPoint';
@@ -13,6 +15,7 @@ import { Scene } from "babylonjs/scene";
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
 import { EditorControls } from './editorControls';
+import { SelectedCoordinate } from './animationListTree';
 
 require("./curveEditor.scss");
 
@@ -28,10 +31,17 @@ interface ICanvasAxis {
     label: number;
 }
 
+interface ICurveData {
+    pathData: string;
+    pathLength: number;
+    domCurve: React.RefObject<SVGPathElement>;
+    color: string;
+    id: string;
+}
+
 export class AnimationCurveEditorComponent extends React.Component<IAnimationCurveEditorComponentProps, {
     isOpen: boolean,
     selected: Animation | null,
-    currentPathData: string | undefined,
     svgKeyframes: IKeyframeSvgPoint[] | undefined,
     currentFrame: number,
     currentValue: number,
@@ -45,9 +55,9 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     playheadOffset: number,
     notification: string,
     currentPoint: SVGPoint | undefined,
-    lastFrame: number,
     playheadPos: number,
     isPlaying: boolean,
+    selectedPathData: ICurveData[] | undefined
 }> {
 
     // Height scale *Review this functionaliy
@@ -56,7 +66,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     readonly _entityName: string;
     readonly _canvasLength: number = 20;
     private _svgKeyframes: IKeyframeSvgPoint[] = [];
-    private _frames: Vector2[] = [];
     private _isPlaying: boolean = false;
     private _graphCanvas: React.RefObject<HTMLDivElement>;
     private _selectedCurve: React.RefObject<SVGPathElement>;
@@ -78,7 +87,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         if (this.props.entity instanceof TargetedAnimation) {
             this._isTargetedAnimation = true;
             initialSelection = this.props.entity.animation;
-            initialLerpMode = initialSelection !== undefined ? this.analizeAnimation(initialSelection) : false;
+            initialLerpMode = initialSelection !== undefined ? this.analizeAnimationForLerp(initialSelection) : false;
             initialPathData = initialSelection !== undefined ? this.getPathData(initialSelection) : "";
         } else {
             this._isTargetedAnimation = false;
@@ -87,7 +96,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             initialSelection = hasAnimations !== false ? hasAnimations && hasAnimations[0] : null;
 
 
-            initialLerpMode = initialSelection !== undefined ? this.analizeAnimation(this.props.entity.animations && initialSelection) : false;
+            initialLerpMode = initialSelection !== undefined ? this.analizeAnimationForLerp(this.props.entity.animations && initialSelection) : false;
             initialPathData = initialSelection && this.getPathData(initialSelection);
             initialPathData = initialPathData === null || initialPathData === undefined ? "" : initialPathData;
         }
@@ -97,7 +106,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         this.state = {
             selected: initialSelection,
             isOpen: true,
-            currentPathData: initialPathData,
             svgKeyframes: this._svgKeyframes,
             currentFrame: 0,
             currentValue: 1,
@@ -109,11 +117,11 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             frameAxisLength: (new Array(this._canvasLength)).fill(0).map((s, i) => { return { value: i * 10, label: i * 10 } }),
             valueAxisLength: (new Array(10)).fill(0).map((s, i) => { return { value: i * 10, label: valueInd[i] } }),
             notification: "",
-            lastFrame: 0,
             currentPoint: undefined,
             scale: 1,
             playheadPos: 0,
             isPlaying: this.isAnimationPlaying(),
+            selectedPathData: []
         }
     }
 
@@ -149,7 +157,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     }
 
     setAxesLength() {
-        
+
         let length = Math.round(this._canvasLength * this.state.scale);// Check Undefined, or NaN
         let highestFrame = 100;
         if (this.state.selected !== null) {
@@ -221,10 +229,108 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         this.setState({ svgKeyframes: updatedKeyframes });
     }
 
-    renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) {
+    updateValuePerCoordinate(dataType: number, value: number | Vector2 | Vector3 | Color3 | Color4 | Size | Quaternion, newValue: number, coordinate?: number) {
+
+
+        if (dataType === Animation.ANIMATIONTYPE_FLOAT) {
+            value = newValue;
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_VECTOR2) {
+            switch (coordinate) {
+                case SelectedCoordinate.x:
+                    (value as Vector2).x = newValue
+                    break;
+                case SelectedCoordinate.y:
+                    (value as Vector2).y = newValue
+                    break;
+            }
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_VECTOR3) {
+            switch (coordinate) {
+                case SelectedCoordinate.x:
+                    (value as Vector3).x = newValue
+                    break;
+                case SelectedCoordinate.y:
+                    (value as Vector3).y = newValue
+                    break;
+                case SelectedCoordinate.z:
+                    (value as Vector3).z = newValue
+                    break;
+            }
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_QUATERNION) {
+            switch (coordinate) {
+                case SelectedCoordinate.x:
+                    (value as Quaternion).x = newValue
+                    break;
+                case SelectedCoordinate.y:
+                    (value as Quaternion).y = newValue
+                    break;
+                case SelectedCoordinate.z:
+                    (value as Quaternion).z = newValue
+                    break;
+                case SelectedCoordinate.w:
+                    (value as Quaternion).w = newValue
+                    break;
+            }
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_COLOR3) {
+            switch (coordinate) {
+                case SelectedCoordinate.r:
+                    (value as Color3).r = newValue
+                    break;
+                case SelectedCoordinate.g:
+                    (value as Color3).g = newValue
+                    break;
+                case SelectedCoordinate.b:
+                    (value as Color3).b = newValue
+                    break;
+            }
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_COLOR4) {
+            switch (coordinate) {
+                case SelectedCoordinate.r:
+                    (value as Color4).r = newValue
+                    break;
+                case SelectedCoordinate.g:
+                    (value as Color4).g = newValue
+                    break;
+                case SelectedCoordinate.b:
+                    (value as Color4).b = newValue
+                    break;
+                case SelectedCoordinate.a:
+                    (value as Color4).a = newValue
+                    break;
+            }
+        }
+
+        if (dataType === Animation.ANIMATIONTYPE_SIZE) {
+            switch (coordinate) {
+                case SelectedCoordinate.width:
+                    (value as Size).width = newValue
+                    break;
+                case SelectedCoordinate.g:
+                    (value as Size).height = newValue
+                    break;
+            }
+        }
+
+
+        return value;
+    }
+
+    renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string) {
 
         let animation = this.state.selected as Animation;
         // Bug: After play/stop we get an extra keyframe at 0
+        let index = parseInt(id.split('_')[3]);
+
+        let coordinate = parseInt(id.split('_')[2]);
 
         let keys = [...animation.getKeys()];
 
@@ -238,24 +344,30 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
 
         keys[index].frame = newFrame; // This value comes as percentage/frame/time
-        keys[index].value = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2; // this value comes inverted svg from 0 = 100 to 100 = 0
 
+        // Calculate value for Vector3...
+
+        let updatedValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2; // this value comes inverted svg from 0 = 100 to 100 = 0
+
+        keys[index].value = this.updateValuePerCoordinate(animation.dataType, keys[index].value, updatedValue, coordinate);
 
         if (updatedSvgKeyFrame.isLeftActive) {
 
             if (updatedSvgKeyFrame.leftControlPoint !== null) {
                 // Rotate 
-                let updatedValue = ((this._heightScale - updatedSvgKeyFrame.leftControlPoint.y) / this._heightScale) * 2;
+                let newValue = ((this._heightScale - updatedSvgKeyFrame.leftControlPoint.y) / this._heightScale) * 2;
 
                 let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
 
-                keys[index].inTangent = keyframeValue - updatedValue;
+                let updatedValue = keyframeValue - newValue;
+
+                keys[index].inTangent = this.updateValuePerCoordinate(animation.dataType, keys[index].inTangent, updatedValue, coordinate);
 
                 if (!this.state.isBrokenMode) {
                     // Right control point if exists
                     if (updatedSvgKeyFrame.rightControlPoint !== null) {
                         // Sets opposite value
-                        keys[index].outTangent = keys[index].inTangent * -1
+                        keys[index].outTangent = keys[index].inTangent * -1;
                     }
                 }
             }
@@ -265,11 +377,13 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
             if (updatedSvgKeyFrame.rightControlPoint !== null) {
                 // Rotate 
-                let updatedValue = ((this._heightScale - updatedSvgKeyFrame.rightControlPoint.y) / this._heightScale) * 2;
+                let newValue = ((this._heightScale - updatedSvgKeyFrame.rightControlPoint.y) / this._heightScale) * 2;
 
                 let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
 
-                keys[index].outTangent = keyframeValue - updatedValue;
+                let updatedValue = keyframeValue - newValue;
+
+                keys[index].outTangent = this.updateValuePerCoordinate(animation.dataType, keys[index].outTangent, updatedValue, coordinate);
 
                 if (!this.state.isBrokenMode) {
                     if (updatedSvgKeyFrame.leftControlPoint !== null) {   // Sets opposite value
@@ -282,7 +396,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
         animation.setKeys(keys);
 
-        this.selectAnimation(animation);
+        this.selectAnimation(animation, coordinate);
 
     }
 
@@ -425,60 +539,14 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
-    updateKeyframe(keyframe: Vector2, index: number) {
-
-        let anim = this.state.selected as Animation;
-        var keys: IAnimationKey[] = [];
-
-        var svgKeyframes = this.state.svgKeyframes?.map((k, i) => {
-            if (i === index) {
-                k.keyframePoint.x = keyframe.x;
-                k.keyframePoint.y = keyframe.y;
-            }
-
-            var height = 100;
-            var middle = (height / 2);
-
-            var keyValue;
-
-            if (k.keyframePoint.y < middle) {
-                keyValue = 1 + ((100 / k.keyframePoint.y) * .1)
-            }
-
-            if (k.keyframePoint.y > middle) {
-                keyValue = 1 - ((100 / k.keyframePoint.y) * .1)
-            }
-
-            keys.push({ frame: k.keyframePoint.x, value: keyValue })
-            return k;
-        });
-
-        anim.setKeys(keys);
-
-        this.setState({ svgKeyframes: svgKeyframes });
-
-    }
-
     /**
     * Curve Rendering Functions
     * This section handles how to render curves.
     */
-    getAnimationProperties(animation: Animation) {
-        let easingType, easingMode;
-        let easingFunction: EasingFunction = animation.getEasingFunction() as EasingFunction;
-        if (easingFunction === undefined) {
-            easingType = undefined
-            easingMode = undefined;
-        } else {
-            easingType = easingFunction.constructor.name;
-            easingMode = easingFunction.getEasingMode();
-        }
-        return { easingType, easingMode }
-    }
-
     linearInterpolation(keyframes: IAnimationKey[], data: string, middle: number): string {
         keyframes.forEach((key, i) => {
 
+            // identify type of value and split...
             var point = new Vector2(0, 0);
             point.x = key.frame;
             point.y = this._heightScale - (key.value * middle);
@@ -492,82 +560,145 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     }
 
     setKeyframePointLinear(point: Vector2, index: number) {
+        // here set the ID to a unique id
         let svgKeyframe = { keyframePoint: point, rightControlPoint: null, leftControlPoint: null, id: index.toString(), selected: false, isLeftActive: false, isRightActive: false }
         this._svgKeyframes.push(svgKeyframe);
     }
 
-    getPathData(animation: Animation | null) {
-
-        if (animation === null){
-            return "";
-        }
-
-        // Check if Tangent mode is active and broken mode is active. (Only one tangent moves)
-        let keyframes = animation.getKeys();
-
-        if (keyframes === undefined) {
-            return "";
-        }
-
+    flatTangents(keyframes: IAnimationKey[], dataType: number) {
         // Checks if Flat Tangent is active (tangents are set to zero)
+        let flattened;
         if (this.state && this.state.isFlatTangentMode) {
-            keyframes = animation.getKeys().map(kf => {
+
+            flattened = keyframes.map(kf => {
 
                 if (kf.inTangent !== undefined) {
-                    kf.inTangent = 0;
+                    kf.inTangent = this.returnZero(dataType);
                 }
 
                 if (kf.outTangent !== undefined) {
-                    kf.outTangent = 0;
+                    kf.outTangent = this.returnZero(dataType);
                 }
-
                 return kf;
             });
         } else {
-            keyframes = animation.getKeys();
+            flattened = keyframes;
         }
+        return flattened;
+    }
 
-        const startKey = keyframes[0];
+    returnZero(dataType: number) {
+        let type;
+        switch (dataType) {
+            case Animation.ANIMATIONTYPE_FLOAT:
+                type = 0;
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR3:
+                type = Vector3.Zero();
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR2:
+                type = Vector2.Zero();
+                break;
+        }
+        return type;
+    }
 
-        let middle = this._heightScale / 2;
+    getValueAsArray(valueType: number, value: number | Vector2 | Vector3 | Color3 | Color4 | Size | Quaternion) {
+        let valueAsArray: number[] = [];
+        switch (valueType) {
+            case Animation.ANIMATIONTYPE_FLOAT:
+                valueAsArray = [value as number];
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR3:
+                valueAsArray = (value as Vector3).asArray();
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR2:
+                valueAsArray = (value as Vector2).asArray();
+                break;
+            case Animation.ANIMATIONTYPE_QUATERNION:
+                valueAsArray = (value as Quaternion).asArray();
+                break;
+            case Animation.ANIMATIONTYPE_COLOR3:
+                valueAsArray = (value as Color3).asArray();
+                break;
+            case Animation.ANIMATIONTYPE_COLOR4:
+                valueAsArray = (value as Color4).asArray();
+                break;
+            case Animation.ANIMATIONTYPE_SIZE:
+                valueAsArray = [(value as Size).width, (value as Size).height];
+                break;
+        }
+        return valueAsArray;
+    }
 
-        // START OF LINE/CURVE
-        let data: string | undefined = `M${startKey.frame}, ${this._heightScale - (startKey.value * middle)}`;
+    getPathData(animation: Animation | null) {
+
+        if (animation === null) {
+            return undefined;
+        }
+
+        var keyframes = animation.getKeys();
+
+        if (keyframes === undefined) {
+            return undefined;
 
-        if (this.state && this.state.lerpMode) {
-            data = this.linearInterpolation(keyframes, data, middle);
         } else {
-            if (this.getAnimationData(animation).usesTangents) {
-                data = this.curvePathWithTangents(keyframes, data, middle, animation.dataType);
-            } else {
-                const { easingMode, easingType } = this.getAnimationProperties(animation);
-                if (easingType !== undefined && easingMode !== undefined) {
-                    let easingFunction = animation.getEasingFunction();
-                    data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction)
+
+            const { easingMode, easingType, usesTangents, valueType, highestFrame, name, targetProperty } = this.getAnimationData(animation);
+
+            keyframes = this.flatTangents(keyframes, valueType);
+            const startKey = keyframes[0];
+            let middle = this._heightScale / 2;
+            let collection: ICurveData[] = [];
+            const colors = ['red', 'green', 'blue', 'white', '#7a4ece'];
+            const startValue = this.getValueAsArray(valueType, startKey.value);
+
+
+            for (var d = 0; d < startValue.length; d++) {
+
+                const id = `${name}_${targetProperty}_${d}`;
+
+                const curveColor = valueType === Animation.ANIMATIONTYPE_FLOAT ? colors[4] : colors[d];
+                // START OF LINE/CURVE
+                let data: string | undefined = `M${startKey.frame}, ${this._heightScale - (startValue[d] * middle)}`; // 
+
+                if (this.state && this.state.lerpMode) {
+                    data = this.linearInterpolation(keyframes, data, middle);
                 } else {
-                    if (this.state !== undefined) {
-                        let emptyTangents = keyframes.map((kf, i) => {
-                            if (i === 0) {
-                                kf.outTangent = 0;
-                            } else if (i === keyframes.length - 1) {
-                                kf.inTangent = 0;
+                    if (usesTangents) {
+                        data = this.curvePathWithTangents(keyframes, data, middle, valueType, d, id);
+                    } else {
+                        if (easingType !== undefined && easingMode !== undefined) {
+                            let easingFunction = animation.getEasingFunction();
+                            data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction)
+                        } else {
+                            if (this.state !== undefined) {
+                                let emptyTangents = keyframes.map((kf, i) => {
+                                    if (i === 0) {
+                                        kf.outTangent = 0;
+                                    } else if (i === keyframes.length - 1) {
+                                        kf.inTangent = 0;
+                                    } else {
+                                        kf.inTangent = 0;
+                                        kf.outTangent = 0;
+                                    }
+                                    return kf;
+                                });
+                                data = this.curvePathWithTangents(emptyTangents, data, middle, valueType, d, id);
                             } else {
-                                kf.inTangent = 0;
-                                kf.outTangent = 0;
+                                data = this.linearInterpolation(keyframes, data, middle);
                             }
-                            return kf;
-                        });
-                        data = this.curvePathWithTangents(emptyTangents, data, middle, animation.dataType);
-                    } else {
-                        data = this.linearInterpolation(keyframes, data, middle);
+                        }
                     }
-
                 }
+
+                collection.push({ pathData: data, pathLength: highestFrame, domCurve: React.createRef(), color: curveColor, id: id })
+
             }
-        }
 
-        return data;
-        
+            return collection;
+
+        }
     }
 
     getAnimationData(animation: Animation) {
@@ -580,86 +711,43 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         let targetPropertyPath = animation.targetPropertyPath;
         let framesPerSecond = animation.framePerSecond;
         let highestFrame = animation.getHighestFrame();
-        let serialized = animation.serialize();
+        //let serialized = animation.serialize();
         let usesTangents = animation.getKeys().find(kf => kf.hasOwnProperty('inTangent') || kf.hasOwnProperty('outTangent')) !== undefined ? true : false;
+        let valueType = animation.dataType;
+        // easing properties
+        let easingType, easingMode;
+        let easingFunction: EasingFunction = animation.getEasingFunction() as EasingFunction;
+        if (easingFunction === undefined) {
+            easingType = undefined
+            easingMode = undefined;
+        } else {
+            easingType = easingFunction.constructor.name;
+            easingMode = easingFunction.getEasingMode();
+        }
 
-        return { loopMode, name, blendingSpeed, targetPropertyPath, targetProperty, framesPerSecond, highestFrame, serialized, usesTangents }
-
-    }
-
-    drawAllFrames(initialKey: IAnimationKey, endKey: IAnimationKey, easingFunction: EasingFunction) {
-
-        let i = initialKey.frame;
-
-        for (i; i < endKey.frame; i++) {
-
-            (i * 100 / endKey.frame)
-
-            let dy = easingFunction.easeInCore(i);
-            let value = this._heightScale - (dy * (this._heightScale / 2));
-            this._frames.push(new Vector2(i, value));
+        return { loopMode, name, blendingSpeed, targetPropertyPath, targetProperty, framesPerSecond, highestFrame, usesTangents, easingType, easingMode, valueType }
 
-        }
     }
 
-    curvePathFlat(keyframes: IAnimationKey[], data: string, middle: number, dataType: number) {
+    curvePathWithTangents(keyframes: IAnimationKey[], data: string, middle: number, type: number, coordinate: number, animationName: string) {
 
         keyframes.forEach((key, i) => {
 
-            if (dataType === Animation.ANIMATIONTYPE_FLOAT) {
-
-                var pointA = new Vector2(0, 0);
-                if (i === 0) {
-                    pointA.set(key.frame, this._heightScale - (key.value * middle));
-                    this.setKeyframePoint([pointA], i, keyframes.length);
-                } else {
-                    pointA.set(keyframes[i - 1].frame, this._heightScale - (keyframes[i - 1].value * middle));
-
-                    let defaultWeight = 10;
-
-                    let nextKeyframe = keyframes[i + 1];
-                    let prevKeyframe = keyframes[i - 1];
-                    if (nextKeyframe !== undefined) {
-                        let distance = keyframes[i + 1].frame - key.frame;
-                        defaultWeight = distance * .33;
-                    }
-
-                    if (prevKeyframe !== undefined) {
-                        let distance = key.frame - keyframes[i - 1].frame;
-                        defaultWeight = distance * .33;
-                    }
-
-                    let tangentA = new Vector2(pointA.x + defaultWeight, pointA.y);
-
-                    let pointB = new Vector2(key.frame, this._heightScale - (key.value * middle));
-
-                    let tangentB = new Vector2(pointB.x - defaultWeight, pointB.y);
-
-                    this.setKeyframePoint([pointA, tangentA, tangentB, pointB], i, keyframes.length);
-
-                    data += ` C${tangentA.x} ${tangentA.y} ${tangentB.x} ${tangentB.y} ${pointB.x} ${pointB.y} `
+            // Create a unique id for curve
+            const curveId = animationName + "_" + i
 
-                }
-            }
-        });
-
-        return data;
-
-    }
-
-    curvePathWithTangents(keyframes: IAnimationKey[], data: string, middle: number, type: number) {
-
-        keyframes.forEach((key, i) => {
+            // identify type of value and split...
+            const keyframe_valueAsArray = this.getValueAsArray(type, key.value)[coordinate];
 
             let svgKeyframe;
             let outTangent;
             let inTangent;
             let defaultWeight = 5;
 
-            var inT = key.inTangent === undefined ? null : key.inTangent;
-            var outT = key.outTangent === undefined ? null : key.outTangent;
+            var inT = key.inTangent === undefined ? null : this.getValueAsArray(type, key.inTangent)[coordinate];
+            var outT = key.outTangent === undefined ? null : this.getValueAsArray(type, key.outTangent)[coordinate];
 
-            let y = this._heightScale - (key.value * middle);
+            let y = this._heightScale - (keyframe_valueAsArray * middle);
 
             let nextKeyframe = keyframes[i + 1];
             let prevKeyframe = keyframes[i - 1];
@@ -688,14 +776,14 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             }
 
             if (i === 0) {
-                svgKeyframe = { keyframePoint: new Vector2(key.frame, this._heightScale - (key.value * middle)), rightControlPoint: outTangent, leftControlPoint: null, id: i.toString(), selected: false, isLeftActive: false, isRightActive: false }
+                svgKeyframe = { keyframePoint: new Vector2(key.frame, this._heightScale - (keyframe_valueAsArray * middle)), rightControlPoint: outTangent, leftControlPoint: null, id: curveId, selected: false, isLeftActive: false, isRightActive: false }
                 if (outTangent !== null) {
                     data += ` C${outTangent.x} ${outTangent.y} `;
                 }
 
             } else {
 
-                svgKeyframe = { keyframePoint: new Vector2(key.frame, this._heightScale - (key.value * middle)), rightControlPoint: outTangent, leftControlPoint: inTangent, id: i.toString(), selected: false, isLeftActive: false, isRightActive: false }
+                svgKeyframe = { keyframePoint: new Vector2(key.frame, this._heightScale - (keyframe_valueAsArray * middle)), rightControlPoint: outTangent, leftControlPoint: inTangent, id: curveId, selected: false, isLeftActive: false, isRightActive: false }
 
                 if (outTangent !== null && inTangent !== null) {
                     data += ` ${inTangent.x} ${inTangent.y} ${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} C${outTangent.x} ${outTangent.y} `
@@ -706,7 +794,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             }
 
             if (this.state) {
-                let prev = this.state.svgKeyframes?.find(kf => kf.id === i.toString());
+                let prev = this.state.svgKeyframes?.find(kf => kf.id === curveId);
                 if (prev) {
                     svgKeyframe.isLeftActive = prev?.isLeftActive;
                     svgKeyframe.isRightActive = prev?.isRightActive;
@@ -730,6 +818,8 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
         keyframes.forEach((key, i) => {
 
+            // identify type of value and split...
+
             // Gets previous initial point of curve segment
             var pointA = new Vector2(0, 0);
             if (i === 0) {
@@ -847,25 +937,50 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     * Core functions
     * This section handles main Curve Editor Functions.
     */
-    selectAnimation(animation: Animation, axis?: string) {
+    selectAnimation(animation: Animation, coordinate?: SelectedCoordinate) {
 
-        if (!axis){
-            this.playStopAnimation();
-
-            this._svgKeyframes = [];
+        this._svgKeyframes = [];
+        let updatedPath;
+        let filteredSvgKeys;
 
-            const pathData = this.getPathData(animation);
+        if (coordinate === undefined) {
+            this.playStopAnimation();
 
-            let lastFrame = animation.getHighestFrame();
+            updatedPath = this.getPathData(animation);
 
-            if (pathData === "") {
+            if (updatedPath === undefined) {
                 console.log("no keyframes in this animation");
             }
 
-            this.setState({ selected: animation, currentPathData: pathData, svgKeyframes: this._svgKeyframes, lastFrame: lastFrame });
         } else {
-            console.log(axis); // This will handle animations that are not Float type
+            let curves = this.getPathData(animation);
+            if (curves === undefined) {
+                console.log("no keyframes in this animation");
+            }
+
+            updatedPath = [];
+
+            filteredSvgKeys = this._svgKeyframes?.filter(curve => {
+                let id = parseInt(curve.id.split('_')[2]);
+                if (id === coordinate) {
+                    return true
+                } else {
+                    return false
+                }
+            })
+
+
+            curves?.map(curve => {
+                let id = parseInt(curve.id.split('_')[2]);
+                if (id === coordinate) {
+                    updatedPath.push(curve);
+                }
+            })
         }
+
+        // check for empty svgKeyframes, lastframe, selected
+        this.setState({ selected: animation, svgKeyframes: coordinate !== undefined ? filteredSvgKeys : this._svgKeyframes, selectedPathData: updatedPath });
+
     }
 
     isAnimationPlaying() {
@@ -877,34 +992,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         return this.props.scene.getAllAnimatablesByTarget(target).length > 0;
     }
 
-    playPause(direction: number) {
-        if (this.state.selected) {
-            let target = this.props.entity;
-            if (this.props.entity instanceof TargetedAnimation) {
-                target = this.props.entity.target;
-            }
-            if (this.state.isPlaying) {
-                this.props.scene.stopAnimation(target);
-                this.setState({ isPlaying: false })
-                this._isPlaying = false;
-                this.forceUpdate();
-            } else {
-                let keys = this.state.selected.getKeys();
-                let firstFrame = keys[0].frame;
-                let LastFrame = keys[keys.length - 1].frame;
-                if (direction === 1){
-                    this.props.scene.beginAnimation(target, firstFrame, LastFrame, true);
-                } 
-                if (direction === -1){
-                    this.props.scene.beginAnimation(target, LastFrame, firstFrame, true);
-                } 
-                this._isPlaying = true;
-                this.setState({ isPlaying: true });
-                this.forceUpdate();
-            }
-        }
-    }
-
     playStopAnimation() {
         let target = this.props.entity;
         if (this.props.entity instanceof TargetedAnimation) {
@@ -920,12 +1007,10 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
-    analizeAnimation(animation: Animation | null) {
+    analizeAnimationForLerp(animation: Animation | null) {
         if (animation !== null) {
-            const { easingMode, easingType } = this.getAnimationProperties(animation);
-            let hasDefinedTangents = this.getAnimationData(animation).usesTangents;
-
-            if (easingType === undefined && easingMode === undefined && !hasDefinedTangents) {
+            const { easingMode, easingType, usesTangents } = this.getAnimationData(animation);
+            if (easingType === undefined && easingMode === undefined && !usesTangents) {
                 return true;
             } else {
                 return false;
@@ -975,6 +1060,34 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
+    playPause(direction: number) {
+        if (this.state.selected) {
+            let target = this.props.entity;
+            if (this.props.entity instanceof TargetedAnimation) {
+                target = this.props.entity.target;
+            }
+            if (this.state.isPlaying) {
+                this.props.scene.stopAnimation(target);
+                this.setState({ isPlaying: false })
+                this._isPlaying = false;
+                this.forceUpdate();
+            } else {
+                let keys = this.state.selected.getKeys();
+                let firstFrame = keys[0].frame;
+                let LastFrame = keys[keys.length - 1].frame;
+                if (direction === 1) {
+                    this.props.scene.beginAnimation(target, firstFrame, LastFrame, true);
+                }
+                if (direction === -1) {
+                    this.props.scene.beginAnimation(target, LastFrame, firstFrame, true);
+                }
+                this._isPlaying = true;
+                this.setState({ isPlaying: true });
+                this.forceUpdate();
+            }
+        }
+    }
+
     render() {
         return (
             <div id="animation-curve-editor">
@@ -996,16 +1109,16 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                     lerpMode={this.state.lerpMode}
                     setLerpMode={() => this.setLerpMode()}
                     flatTangent={() => this.setFlatTangent()} />
-                    
+
                 <div className="content">
                     <div className="row">
-                        <EditorControls selectAnimation={(animation: Animation, axis?: string) => this.selectAnimation(animation, axis)} 
-                        isTargetedAnimation={this._isTargetedAnimation} 
-                        entity={this.props.entity} 
-                        selected={this.state.selected} 
-                        setNotificationMessage={(message: string) => { this.setState({notification: message})}}
+                        <EditorControls selectAnimation={(animation: Animation, axis?: SelectedCoordinate) => this.selectAnimation(animation, axis)}
+                            isTargetedAnimation={this._isTargetedAnimation}
+                            entity={this.props.entity}
+                            selected={this.state.selected}
+                            setNotificationMessage={(message: string) => { this.setState({ notification: message }) }}
                         />
-                        
+
                         <div ref={this._graphCanvas} className="graph-chart" onWheel={(e) => this.zoom(e)} >
 
                             <Playhead frame={this.state.currentFrame} offset={this.state.playheadOffset} />
@@ -1015,7 +1128,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                                 viewBoxScale={this.state.frameAxisLength.length} scale={this.state.scale}
                                 keyframeSvgPoints={this.state.svgKeyframes}
                                 selectedControlPoint={(type: string, id: string) => this.selectedControlPoint(type, id)}
-                                updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) => this.renderPoints(updatedSvgKeyFrame, index)}>
+                                updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string) => this.renderPoints(updatedSvgKeyFrame, id)}>
 
                                 {/* Frame Labels  */}
                                 { /* Vertical Grid  */}
@@ -1034,14 +1147,13 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
                                 })}
 
-                                { /* Single Curve -Modify this for multiple selection and view  */}
-                                <path ref={this._selectedCurve} pathLength={this.state.lastFrame} id="curve" d={this.state.currentPathData} style={{ stroke: 'red', fill: 'none', strokeWidth: '0.5' }}></path>
+                                { /* Multiple Curves  */}
+                                {
+                                    this.state.selectedPathData?.map((curve, i) =>
+                                        <path key={i} ref={curve.domCurve} pathLength={curve.pathLength} id="curve" d={curve.pathData} style={{ stroke: curve.color, fill: 'none', strokeWidth: '0.5' }}></path>
+                                    )
+                                }
 
-                                {this._frames && this._frames.map(frame =>
-                                    <svg x={frame.x} y={frame.y} style={{ overflow: 'visible' }}>
-                                        <circle cx="0" cy="0" r="2" stroke="black" strokeWidth="1" fill="white" />
-                                    </svg>
-                                )}
 
                             </SvgDraggableArea>
 

+ 43 - 19
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationListTree.tsx

@@ -13,7 +13,7 @@ interface IAnimationListTreeProps {
     entity: IAnimatable | TargetedAnimation;
     selected: Animation | null
     onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-    selectAnimation: (selected: Animation, axis?: string) => void;
+    selectAnimation: (selected: Animation, coordinate?: SelectedCoordinate) => void;
     empty: () => void;
 }
 
@@ -25,15 +25,28 @@ interface Item {
     open: boolean;
 }
 
-export class AnimationListTree extends React.Component<IAnimationListTreeProps, { list:Item[] } >{
+export enum SelectedCoordinate {
+    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)
+            return ({ index: i, name: animation.name, property: animation.targetProperty, selected: false, open: false } as Item)
         });
-        this.state = { list: animationList ?? [] }
+        this.state = { list: animationList ?? [], selectedCoordinate: 0, selectedAnimation: 0 }
     }
-    
+
     deleteAnimation() {
         let currentSelected = this.props.selected;
         if (this.props.entity instanceof TargetedAnimation) {
@@ -50,9 +63,9 @@ export class AnimationListTree extends React.Component<IAnimationListTreeProps,
 
     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)
+            return ({ index: i, name: animation.name, property: animation.targetProperty, selected: false, open: false } as Item)
         });
-        if (animationList?.length === 0){
+        if (animationList?.length === 0) {
             this.props.empty();
         }
         this.setState({ list: animationList ?? [] });
@@ -67,6 +80,11 @@ export class AnimationListTree extends React.Component<IAnimationListTreeProps,
         item.open = !item.open;
     }
 
+    setSelectedCoordinate(animation: Animation, coordinate: SelectedCoordinate, index: number) {
+        this.setState({ selectedCoordinate: coordinate, selectedAnimation: index });
+        this.props.selectAnimation(animation, SelectedCoordinate.x)
+    }
+
     setListItem(animation: Animation, i: number) {
         let element;
 
@@ -89,15 +107,21 @@ export class AnimationListTree extends React.Component<IAnimationListTreeProps,
                 </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} onClick={() => this.props.selectAnimation(animation, '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>{animation.targetProperty}</p>
+                    <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`} className="property" style={{color: '#db3e3e'}} onClick={() => this.props.selectAnimation(animation, 'x')}><div className={`handle-indicator ${''}`}></div>{animation.targetProperty} X</li>
-                        <li key={`${i}_y`} className="property" style={{color: '#51e22d'}} onClick={() => this.props.selectAnimation(animation, 'y')}><div className={`handle-indicator ${''}`}></div>{animation.targetProperty} Y</li>
-                        <li key={`${i}_z`} className="property" style={{color: '#00a3ff'}} onClick={() => this.props.selectAnimation(animation, 'z')}><div className={`handle-indicator ${''}`}></div>{animation.targetProperty} Z</li>
+                        <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;
@@ -149,14 +173,14 @@ export class AnimationListTree extends React.Component<IAnimationListTreeProps,
     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>
+                    {
+                        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>
+                </ul>
             </div>
         )
     }

+ 15 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss

@@ -640,6 +640,21 @@
 
                         .handle-indicator{
                             width: 30px;
+
+                            &.show {
+                                display: block;
+                                background-image: url('./assets/keySelectedIcon.svg');
+                                background-repeat: no-repeat;
+                                background-color: transparent;
+                                background-size: 10px;
+                                color: white;
+                                background-position-x: 9px;
+                                background-position-y: 5px;
+                            }
+
+                            &.hide {
+                                display: none;
+                            }
                         }
 
                         ul.sub-list {

+ 63 - 62
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -7,108 +7,109 @@ import { Animation } from 'babylonjs/Animations/animation';
 import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
 import { NumericInputComponent } from '../../../lines/numericInputComponent';
 import { AddAnimation } from './addAnimation';
-import { AnimationListTree } from './animationListTree';
+import { AnimationListTree, SelectedCoordinate } from './animationListTree';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
 
+
 interface IEditorControlsProps {
-   isTargetedAnimation: boolean;
-   entity: IAnimatable | TargetedAnimation;
-   selected: Animation | null
-   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-   setNotificationMessage: (message: string) => void;
-   selectAnimation: (selected: Animation, axis?: string) => void;
+    isTargetedAnimation: boolean;
+    entity: IAnimatable | TargetedAnimation;
+    selected: Animation | null
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    setNotificationMessage: (message: string) => void;
+    selectAnimation: (selected: Animation, axis?: SelectedCoordinate) => void;
 }
 
-export class EditorControls extends React.Component<IEditorControlsProps, {isAnimationTabOpen: boolean, isEditTabOpen: boolean, isLoadTabOpen: boolean, isSaveTabOpen: boolean, isLoopActive: boolean, animationsCount: number; framesPerSecond: number}>{ 
-    
+export class EditorControls extends React.Component<IEditorControlsProps, { isAnimationTabOpen: boolean, isEditTabOpen: boolean, isLoadTabOpen: boolean, isSaveTabOpen: boolean, isLoopActive: boolean, animationsCount: number; framesPerSecond: number }>{
+
     constructor(props: IEditorControlsProps) {
         super(props);
         let count = this.props.isTargetedAnimation ? 1 : (this.props.entity as IAnimatable).animations?.length ?? 0;
         this.state = { isAnimationTabOpen: count === 0 ? true : false, isEditTabOpen: count === 0 ? false : true, isSaveTabOpen: false, isLoadTabOpen: false, isLoopActive: false, animationsCount: count, framesPerSecond: 60 }
     }
 
-    animationAdded(){
-        this.setState( { animationsCount: this.recountAnimations(), isEditTabOpen: true, isAnimationTabOpen: false } );
+    animationAdded() {
+        this.setState({ animationsCount: this.recountAnimations(), isEditTabOpen: true, isAnimationTabOpen: false });
     }
 
     recountAnimations() {
         return (this.props.entity as IAnimatable).animations?.length ?? 0;
     }
 
-    handleTabs(tab: number){
+    handleTabs(tab: number) {
 
-        let state = {isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
+        let state = { isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
 
-        switch(tab){
+        switch (tab) {
             case 0:
-                state = {isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
+                state = { isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
                 break;
             case 1:
-                state = {isAnimationTabOpen: false, isLoadTabOpen: true, isSaveTabOpen: false, isEditTabOpen: false };
+                state = { isAnimationTabOpen: false, isLoadTabOpen: true, isSaveTabOpen: false, isEditTabOpen: false };
                 break;
             case 2:
-                state = {isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: true, isEditTabOpen: false };
+                state = { isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: true, isEditTabOpen: false };
                 break;
             case 3:
-                state = {isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: true };
+                state = { isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: true };
                 break;
         }
 
         this.setState(state);
     }
 
-    handleChangeFps(fps: number){
-        this.setState({framesPerSecond: fps});
+    handleChangeFps(fps: number) {
+        this.setState({ framesPerSecond: fps });
     }
 
-    emptiedList(){
-        this.setState( { animationsCount: this.recountAnimations(), isEditTabOpen: false, isAnimationTabOpen: true } );
+    emptiedList() {
+        this.setState({ animationsCount: this.recountAnimations(), isEditTabOpen: false, isAnimationTabOpen: true });
     }
 
-    render() { 
+    render() {
         return (
             <div className="animation-list">
-            <div className="controls-header">
-            {this.props.isTargetedAnimation ? null : <IconButtonLineComponent active={this.state.isAnimationTabOpen} tooltip="Add Animation" icon="medium add-animation" onClick={() => this.handleTabs(0)}></IconButtonLineComponent>}
-            <IconButtonLineComponent active={this.state.isLoadTabOpen} tooltip="Load Animation" icon="medium load" onClick={() => this.handleTabs(1)}></IconButtonLineComponent>
-            <IconButtonLineComponent active={this.state.isSaveTabOpen} tooltip="Save Animation" icon="medium save" onClick={() => this.handleTabs(2)}></IconButtonLineComponent>
-            {this.state.animationsCount === 0 ? null : 
-            <IconButtonLineComponent active={this.state.isEditTabOpen} tooltip="Edit Animations" icon="medium animation-edit" onClick={() => this.handleTabs(3)}></IconButtonLineComponent> 
-            }
-            { this.state.isEditTabOpen ?
-            <div className="input-fps">
-                <NumericInputComponent label={""} precision={0} value={this.state.framesPerSecond} onChange={(framesPerSecond: number) => this.handleChangeFps(framesPerSecond)}/>
-                <p>fps</p>
-            </div> : null
-            }
-            { this.state.isEditTabOpen ?
-            <IconButtonLineComponent tooltip="Loop/Unloop" icon={`medium ${this.state.isLoopActive ? 'loop-active last' : 'loop-inactive last'}`} onClick={() => { this.setState({ isLoopActive: !this.state.isLoopActive })}}></IconButtonLineComponent> : null
-            }
+                <div className="controls-header">
+                    {this.props.isTargetedAnimation ? null : <IconButtonLineComponent active={this.state.isAnimationTabOpen} tooltip="Add Animation" icon="medium add-animation" onClick={() => this.handleTabs(0)}></IconButtonLineComponent>}
+                    <IconButtonLineComponent active={this.state.isLoadTabOpen} tooltip="Load Animation" icon="medium load" onClick={() => this.handleTabs(1)}></IconButtonLineComponent>
+                    <IconButtonLineComponent active={this.state.isSaveTabOpen} tooltip="Save Animation" icon="medium save" onClick={() => this.handleTabs(2)}></IconButtonLineComponent>
+                    {this.state.animationsCount === 0 ? null :
+                        <IconButtonLineComponent active={this.state.isEditTabOpen} tooltip="Edit Animations" icon="medium animation-edit" onClick={() => this.handleTabs(3)}></IconButtonLineComponent>
+                    }
+                    {this.state.isEditTabOpen ?
+                        <div className="input-fps">
+                            <NumericInputComponent label={""} precision={0} value={this.state.framesPerSecond} onChange={(framesPerSecond: number) => this.handleChangeFps(framesPerSecond)} />
+                            <p>fps</p>
+                        </div> : null
+                    }
+                    {this.state.isEditTabOpen ?
+                        <IconButtonLineComponent tooltip="Loop/Unloop" icon={`medium ${this.state.isLoopActive ? 'loop-active last' : 'loop-inactive last'}`} onClick={() => { this.setState({ isLoopActive: !this.state.isLoopActive }) }}></IconButtonLineComponent> : null
+                    }
+                </div>
+                {this.props.isTargetedAnimation ? null :
+                    <AddAnimation
+                        isOpen={this.state.isAnimationTabOpen}
+                        close={() => { this.setState({ isAnimationTabOpen: false }) }}
+                        entity={(this.props.entity as IAnimatable)}
+                        setNotificationMessage={(message: string) => { this.props.setNotificationMessage(message) }}
+                        changed={() => this.animationAdded()}
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                }
+
+                {this.state.isLoadTabOpen ? <div>Load</div> : null}
+
+                {this.state.isSaveTabOpen ? <div>Save</div> : null}
+
+                {this.state.isEditTabOpen ? <AnimationListTree
+                    isTargetedAnimation={this.props.isTargetedAnimation}
+                    entity={this.props.entity}
+                    selected={this.props.selected}
+                    onPropertyChangedObservable={this.props.onPropertyChangedObservable}
+                    empty={() => this.emptiedList()}
+                    selectAnimation={this.props.selectAnimation} />
+                    : null}
             </div>
-            { this.props.isTargetedAnimation ? null : 
-                <AddAnimation 
-                    isOpen={this.state.isAnimationTabOpen} 
-                    close={() => { this.setState({isAnimationTabOpen: false})}} 
-                    entity={(this.props.entity as IAnimatable)} 
-                    setNotificationMessage={(message: string) => { this.props.setNotificationMessage(message) }}
-                    changed={() => this.animationAdded() }
-                    onPropertyChangedObservable={this.props.onPropertyChangedObservable}/>
-            }
-
-            { this.state.isLoadTabOpen ? <div>Load</div> : null }
-
-            { this.state.isSaveTabOpen ? <div>Save</div> : null }
-
-            { this.state.isEditTabOpen ? <AnimationListTree 
-                isTargetedAnimation={this.props.isTargetedAnimation} 
-                entity={this.props.entity} 
-                selected={this.props.selected} 
-                onPropertyChangedObservable={this.props.onPropertyChangedObservable} 
-                empty={() => this.emptiedList() }
-                selectAnimation={this.props.selectAnimation}/>
-            : null }
-        </div>
         );
     }
 }

+ 7 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/keyframeSvgPoint.tsx

@@ -10,6 +10,13 @@ export interface IKeyframeSvgPoint {
     selected: boolean;
     isLeftActive: boolean;
     isRightActive: boolean;
+    curveId?: ICurveMetaData;
+}
+
+export interface ICurveMetaData {
+    id: number;
+    animationName: string;
+    property: string;
 }
 
 interface IKeyframeSvgPointProps {

+ 22 - 20
inspector/src/components/actionTabs/tabs/propertyGrids/animations/svgDraggableArea.tsx

@@ -4,7 +4,7 @@ import { KeyframeSvgPoint, IKeyframeSvgPoint } from './keyframeSvgPoint';
 
 interface ISvgDraggableAreaProps {
     keyframeSvgPoints: IKeyframeSvgPoint[];
-    updatePosition: (updatedKeyframe: IKeyframeSvgPoint, index: number) => void;
+    updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void;
     scale: number;
     viewBoxScale: number;
     selectKeyframe: (id: string) => void;
@@ -15,7 +15,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
 
     private _active: boolean;
     private _isCurrentPointControl: string;
-    private _currentPointIndex: number;
+    private _currentPointId: string;
     private _draggableArea: React.RefObject<SVGSVGElement>;
     private _panStart: Vector2;
     private _panStop: Vector2;
@@ -23,7 +23,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
 
     constructor(props: ISvgDraggableAreaProps) {
         super(props);
-        this._currentPointIndex = -1;
+        this._currentPointId = "";
         this._isCurrentPointControl = "";
         this._draggableArea = React.createRef();
         this._panStart = new Vector2(0, 0);
@@ -45,7 +45,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
         e.preventDefault();
         if (e.target.classList.contains("draggable")) {
             this._active = true;
-            this._currentPointIndex = parseInt(e.target.getAttribute('data-id'));
+            this._currentPointId = e.target.getAttribute('data-id');
 
             if (e.target.classList.contains("control-point")) {
                 this._isCurrentPointControl = e.target.getAttribute("type");
@@ -71,17 +71,19 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
             if (coord !== undefined) {
 
                 var newPoints = [...this.props.keyframeSvgPoints];
-                // Check for NaN values here. 
-                if (this._isCurrentPointControl === "left") {
-                    newPoints[this._currentPointIndex].leftControlPoint = coord;
-                } else if (this._isCurrentPointControl === "right") {
-                    newPoints[this._currentPointIndex].rightControlPoint = coord;
-                } else {
-                    newPoints[this._currentPointIndex].keyframePoint = coord;
-                }
-
-                this.props.updatePosition(newPoints[this._currentPointIndex], this._currentPointIndex);
 
+                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);
+                }
             }
         }
     }
@@ -91,7 +93,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
     dragEnd(e: any): void {
         e.preventDefault();
         this._active = false;
-        this._currentPointIndex = -1;
+        this._currentPointId = "";
         this._isCurrentPointControl = "";
 
         if (e.target.classList.contains("pannable")) {
@@ -206,14 +208,14 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
                     {this.props.children}
                     {this.props.keyframeSvgPoints.map((keyframe, i) =>
                         <KeyframeSvgPoint
-                            key={i}
-                            id={i.toString()}
-                            keyframePoint={keyframe.keyframePoint} 
-                            leftControlPoint={keyframe.leftControlPoint} 
+                            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} 
+                            selected={keyframe.selected}
                             selectedControlPoint={(type: string, id: string) => this.props.selectedControlPoint(type, id)}
                             selectKeyframe={(id: string) => this.props.selectKeyframe(id)} />
                     )}