Browse Source

Wrapping up animation curve editor...

Vicente Penades 6 years ago
parent
commit
a8f8bdc96f

+ 170 - 0
src/SharpGLTF.Core/Animations/CubicSamplers.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with CUBIC interpolation.
+    /// </summary>
+    struct Vector3CubicSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
+    {
+        #region lifecycle
+
+        public Vector3CubicSampler(IEnumerable<(float, (Vector3, Vector3, Vector3))> sequence)
+        {
+            _Sequence = sequence;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, (Vector3, Vector3, Vector3))> _Sequence;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => 3;
+
+        public Vector3 GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            return SamplerFactory.CubicLerp
+                (
+                segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
+                segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
+                segment.Item3                               // amount
+                );
+        }
+
+        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, (Vector3, Vector3, Vector3)> ToSplineCurve()
+        {
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with CUBIC interpolation.
+    /// </summary>
+    struct QuaternionCubicSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
+    {
+        #region lifecycle
+
+        public QuaternionCubicSampler(IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> sequence)
+        {
+            _Sequence = sequence;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> _Sequence;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => 3;
+
+        public Quaternion GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            return SamplerFactory.CubicLerp
+                (
+                segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
+                segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
+                segment.Item3                               // amount
+                );
+        }
+
+        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, (Quaternion, Quaternion, Quaternion)> ToSplineCurve()
+        {
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
+    /// </summary>
+    struct ArrayCubicSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
+    {
+        #region lifecycle
+
+        public ArrayCubicSampler(IEnumerable<(float, (float[], float[], float[]))> sequence)
+        {
+            _Sequence = sequence;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, (float[], float[], float[]))> _Sequence;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => 3;
+
+        public float[] GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            return SamplerFactory.CubicLerp
+                (
+                segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
+                segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
+                segment.Item3                               // amount
+                );
+        }
+
+        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, (float[], float[], float[])> ToSplineCurve()
+        {
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        #endregion
+    }
+}

+ 34 - 0
src/SharpGLTF.Core/Animations/Interfaces.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Defines a curve that can be sampled at specific points.
+    /// </summary>
+    /// <typeparam name="T">The type of a point in the curve.</typeparam>
+    public interface ICurveSampler<T>
+    {
+        T GetPoint(float offset);
+    }
+
+    /// <summary>
+    /// Defines methods that convert the current curve to a Step, Linear or Spline curve.
+    /// </summary>
+    /// <typeparam name="T">The type of a point of the curve</typeparam>
+    public interface IConvertibleCurve<T>
+    {
+        /// <summary>
+        /// Gets a value indicating the maximum degree of the curve, current values are:
+        /// 0: STEP.
+        /// 1: LINEAR.
+        /// 3: CUBIC.
+        /// </summary>
+        int MaxDegree { get; }
+
+        IReadOnlyDictionary<float, T> ToStepCurve();
+        IReadOnlyDictionary<float, T> ToLinearCurve();
+        IReadOnlyDictionary<float, (T, T, T)> ToSplineCurve();
+    }
+}

+ 173 - 0
src/SharpGLTF.Core/Animations/LinearSamplers.cs

@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
+    /// </summary>
+    struct Vector3LinearSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
+    {
+        #region lifecycle
+
+        public Vector3LinearSampler(IEnumerable<(float, Vector3)> sequence, bool isLinear)
+        {
+            _Sequence = sequence;
+            _Linear = isLinear;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, Vector3)> _Sequence;
+        private readonly Boolean _Linear;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => _Linear ? 1 : 0;
+
+        public Vector3 GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            if (!_Linear) return segment.Item1;
+
+            return Vector3.Lerp(segment.Item1, segment.Item2, segment.Item3);
+        }
+
+        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
+        {
+            Guard.IsFalse(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
+        {
+            Guard.IsTrue(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, (Vector3, Vector3, Vector3)> ToSplineCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
+    /// </summary>
+    struct QuaternionLinearSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
+    {
+        #region lifecycle
+
+        public QuaternionLinearSampler(IEnumerable<(float, Quaternion)> sequence, bool isLinear)
+        {
+            _Sequence = sequence;
+            _Linear = isLinear;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, Quaternion)> _Sequence;
+        private readonly Boolean _Linear;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => _Linear ? 1 : 0;
+
+        public Quaternion GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            if (!_Linear) return segment.Item1;
+
+            return Quaternion.Slerp(segment.Item1, segment.Item2, segment.Item3);
+        }
+
+        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
+        {
+            Guard.IsFalse(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
+        {
+            Guard.IsTrue(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, (Quaternion, Quaternion, Quaternion)> ToSplineCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with STEP or LINEAR interpolations.
+    /// </summary>
+    struct ArrayLinearSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
+    {
+        #region lifecycle
+
+        public ArrayLinearSampler(IEnumerable<(float, float[])> sequence, bool isLinear)
+        {
+            _Sequence = sequence;
+            _Linear = isLinear;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, float[])> _Sequence;
+        private readonly Boolean _Linear;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => _Linear ? 1 : 0;
+
+        public float[] GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            if (!_Linear) return segment.Item1;
+
+            return SamplerFactory.Lerp(segment.Item1, segment.Item2, segment.Item3);
+        }
+
+        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        {
+            Guard.IsFalse(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
+        {
+            Guard.IsTrue(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, (float[], float[], float[])> ToSplineCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
+    }
+}

+ 60 - 300
src/SharpGLTF.Core/Animations/SamplerFactory.cs

@@ -2,46 +2,22 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
-using System.Text;
 
 namespace SharpGLTF.Animations
 {
     /// <summary>
-    /// Defines a curve that can be sampled at specific points.
-    /// </summary>
-    /// <typeparam name="T">The type of a point in the curve.</typeparam>
-    public interface ICurveSampler<T>
-    {
-        T GetPoint(float offset);
-    }
-
-    /// <summary>
-    /// Defines methods that convert the current curve to a Step, Linear or Spline curve.
-    /// </summary>
-    /// <typeparam name="T">The type of a point of the curve</typeparam>
-    public interface IConvertibleCurve<T>
-    {
-        /// <summary>
-        /// Gets a value indicating the maximum degree of the curve, current values are:
-        /// 0: STEP.
-        /// 1: LINEAR.
-        /// 3: CUBIC.
-        /// </summary>
-        int MaxDegree { get; }
-
-        IReadOnlyDictionary<float, T> ToStepCurve();
-        IReadOnlyDictionary<float, T> ToLinearCurve();
-        IReadOnlyDictionary<float, (T, T, T)> ToSplineCurve();
-    }
-
-    /// <summary>
-    /// Utility class to convert curve objects to curve samplers.
+    /// Utility class to create samplers from curve collections.
     /// </summary>
     public static class SamplerFactory
     {
         #region sampler utils
 
-        public static Quaternion CreateTangent(this Quaternion fromValue, Quaternion toValue, float scale = 1)
+        public static Vector3 CreateTangent(Vector3 fromValue, Vector3 toValue, Single scale = 1)
+        {
+            return (toValue - fromValue) * scale;
+        }
+
+        public static Quaternion CreateTangent(Quaternion fromValue, Quaternion toValue, Single scale = 1)
         {
             var tangent = Quaternion.Concatenate(toValue, Quaternion.Inverse(fromValue));
 
@@ -54,6 +30,18 @@ namespace SharpGLTF.Animations
             return Quaternion.CreateFromAxisAngle(axis, scale * (float)angle);
         }
 
+        public static Single[] CreateTangent(Single[] fromValue, Single[] toValue, Single scale = 1)
+        {
+            var r = new float[fromValue.Length];
+
+            for (int i = 0; i < r.Length; ++i)
+            {
+                r[i] = (toValue[i] - fromValue[i]) * scale;
+            }
+
+            return r;
+        }
+
         /// <summary>
         /// Calculates the Hermite point weights for a given <paramref name="amount"/>
         /// </summary>
@@ -232,325 +220,97 @@ namespace SharpGLTF.Animations
 
         #endregion
 
-        #region Extensions
-
-        public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(float, Vector3)> collection, bool isLinear = true)
-        {
-            if (collection == null) return null;
-
-            return new Vector3LinearSampler(collection, isLinear);
-        }
-
-        public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(float, Quaternion)> collection, bool isLinear = true)
-        {
-            if (collection == null) return null;
-
-            return new QuaternionLinearSampler(collection, isLinear);
-        }
-
-        public static ICurveSampler<float[]> CreateSampler(this IEnumerable<(float, float[])> collection, bool isLinear = true)
-        {
-            if (collection == null) return null;
-
-            return new ArrayLinearSampler(collection, isLinear);
-        }
-
-        public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(float, (Vector3, Vector3, Vector3))> collection)
-        {
-            if (collection == null) return null;
-
-            return new Vector3CubicSampler(collection);
-        }
-
-        public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> collection)
-        {
-            if (collection == null) return null;
-
-            return new QuaternionCubicSampler(collection);
-        }
-
-        public static ICurveSampler<float[]> CreateSampler(this IEnumerable<(float, (float[], float[], float[]))> collection)
-        {
-            if (collection == null) return null;
-
-            return new ArrayCubicSampler(collection);
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    struct Vector3LinearSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
-    {
-        public Vector3LinearSampler(IEnumerable<(float, Vector3)> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        private readonly IEnumerable<(float, Vector3)> _Sequence;
-        private readonly Boolean _Linear;
-
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public Vector3 GetPoint(float offset)
-        {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
-
-            if (!_Linear) return segment.Item1;
-
-            return Vector3.Lerp(segment.Item1, segment.Item2, segment.Item3);
-        }
-
-        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
-        {
-            Guard.IsFalse(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public IReadOnlyDictionary<float, (Vector3, Vector3, Vector3)> ToSplineCurve()
-        {
-            throw new NotImplementedException();
-        }
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    struct QuaternionLinearSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
-    {
-        public QuaternionLinearSampler(IEnumerable<(float, Quaternion)> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        private readonly IEnumerable<(float, Quaternion)> _Sequence;
-        private readonly Boolean _Linear;
+        #region interpolation utils
 
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public Quaternion GetPoint(float offset)
-        {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
-
-            if (!_Linear) return segment.Item1;
-
-            return Quaternion.Slerp(segment.Item1, segment.Item2, segment.Item3);
-        }
-
-        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
-        {
-            Guard.IsFalse(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public IReadOnlyDictionary<float, (Quaternion, Quaternion, Quaternion)> ToSplineCurve()
-        {
-            throw new NotImplementedException();
-        }
-    }
-
-    /// <summary>
-    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    struct ArrayLinearSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
-    {
-        public ArrayLinearSampler(IEnumerable<(float, float[])> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        private readonly IEnumerable<(float, float[])> _Sequence;
-        private readonly Boolean _Linear;
-
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public float[] GetPoint(float offset)
+        public static Single[] Lerp(Single[] start, Single[] end, Single amount)
         {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
-
-            if (!_Linear) return segment.Item1;
+            var startW = 1 - amount;
+            var endW = amount;
 
-            var result = new float[segment.Item1.Length];
+            var result = new float[start.Length];
 
             for (int i = 0; i < result.Length; ++i)
             {
-                result[i] = (segment.Item1[i] * (1 - segment.Item3)) + (segment.Item2[i] * segment.Item3);
+                result[i] = (start[i] * startW) + (end[i] * endW);
             }
 
             return result;
         }
 
-        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        public static Vector3 CubicLerp(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
+            var hermite = SamplerFactory.CreateHermitePointWeights(amount);
 
-        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(_Linear));
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+            return (start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4);
         }
 
-        public IReadOnlyDictionary<float, (float[], float[], float[])> ToSplineCurve()
+        public static Quaternion CubicLerp(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
         {
-            throw new NotImplementedException();
-        }
-    }
+            var hermite = CreateHermitePointWeights(amount);
 
-    /// <summary>
-    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    struct Vector3CubicSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
-    {
-        public Vector3CubicSampler(IEnumerable<(float, (Vector3, Vector3, Vector3))> sequence)
-        {
-            _Sequence = sequence;
+            return Quaternion.Normalize((start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4));
         }
 
-        private readonly IEnumerable<(float, (Vector3, Vector3, Vector3))> _Sequence;
-
-        public int MaxDegree => 3;
-
-        public Vector3 GetPoint(float offset)
+        public static Single[] CubicLerp(Single[] start, Single[] outgoingTangent, Single[] end, Single[] incomingTangent, Single amount)
         {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var hermite = CreateHermitePointWeights(amount);
 
-            var hermite = SamplerFactory.CreateHermitePointWeights(segment.Item3);
+            var result = new float[start.Length];
 
-            var start = segment.Item1.Item2;
-            var tangentOut = segment.Item1.Item3;
-            var tangentIn = segment.Item2.Item1;
-            var end = segment.Item2.Item2;
+            for (int i = 0; i < result.Length; ++i)
+            {
+                result[i] = (start[i] * hermite.Item1) + (end[i] * hermite.Item2) + (outgoingTangent[i] * hermite.Item3) + (incomingTangent[i] * hermite.Item4);
+            }
 
-            return (start * hermite.Item1) + (end * hermite.Item2) + (tangentOut * hermite.Item3) + (tangentIn * hermite.Item4);
+            return result;
         }
 
-        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
-        {
-            throw new NotImplementedException();
-        }
+        #endregion
 
-        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
-        {
-            throw new NotImplementedException();
-        }
+        #region sampler creation
 
-        public IReadOnlyDictionary<float, (Vector3, Vector3, Vector3)> ToSplineCurve()
+        public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true)
         {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-    }
+            if (collection == null) return null;
 
-    /// <summary>
-    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    struct QuaternionCubicSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
-    {
-        public QuaternionCubicSampler(IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> sequence)
-        {
-            _Sequence = sequence;
+            return new Vector3LinearSampler(collection, isLinear);
         }
 
-        private readonly IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> _Sequence;
-
-        public int MaxDegree => 3;
-
-        public Quaternion GetPoint(float offset)
+        public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, Quaternion)> collection, bool isLinear = true)
         {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
-
-            var hermite = SamplerFactory.CreateHermitePointWeights(segment.Item3);
-
-            var start = segment.Item1.Item2;
-            var tangentOut = segment.Item1.Item3;
-            var tangentIn = segment.Item2.Item1;
-            var end = segment.Item2.Item2;
+            if (collection == null) return null;
 
-            return Quaternion.Normalize((start * hermite.Item1) + (end * hermite.Item2) + (tangentOut * hermite.Item3) + (tangentIn * hermite.Item4));
+            return new QuaternionLinearSampler(collection, isLinear);
         }
 
-        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
+        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, Single[])> collection, bool isLinear = true)
         {
-            throw new NotImplementedException();
-        }
+            if (collection == null) return null;
 
-        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
-        {
-            throw new NotImplementedException();
+            return new ArrayLinearSampler(collection, isLinear);
         }
 
-        public IReadOnlyDictionary<float, (Quaternion, Quaternion, Quaternion)> ToSplineCurve()
+        public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, (Vector3, Vector3, Vector3))> collection)
         {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-    }
+            if (collection == null) return null;
 
-    /// <summary>
-    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    struct ArrayCubicSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
-    {
-        public ArrayCubicSampler(IEnumerable<(float, (float[], float[], float[]))> sequence)
-        {
-            _Sequence = sequence;
+            return new Vector3CubicSampler(collection);
         }
 
-        private readonly IEnumerable<(float, (float[], float[], float[]))> _Sequence;
-
-        public int MaxDegree => 3;
-
-        public float[] GetPoint(float offset)
+        public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> collection)
         {
-            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
-
-            var hermite = SamplerFactory.CreateHermitePointWeights(segment.Item3);
-
-            var start = segment.Item1.Item2;
-            var tangentOut = segment.Item1.Item3;
-            var tangentIn = segment.Item2.Item1;
-            var end = segment.Item2.Item2;
-
-            var result = new float[start.Length];
-
-            for (int i = 0; i < result.Length; ++i)
-            {
-                result[i] = (start[i] * hermite.Item1) + (end[i] * hermite.Item2) + (tangentOut[i] * hermite.Item3) + (tangentIn[i] * hermite.Item4);
-            }
+            if (collection == null) return null;
 
-            return result;
+            return new QuaternionCubicSampler(collection);
         }
 
-        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, (Single[], Single[], Single[]))> collection)
         {
-            throw new NotImplementedException();
-        }
+            if (collection == null) return null;
 
-        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
-        {
-            throw new NotImplementedException();
+            return new ArrayCubicSampler(collection);
         }
 
-        public IReadOnlyDictionary<float, (float[], float[], float[])> ToSplineCurve()
-        {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
+        #endregion
     }
 }

+ 286 - 0
src/SharpGLTF.Toolkit/Animations/CurveBuilder.cs

@@ -0,0 +1,286 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+using System.Linq;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Represents an editable curve of <typeparamref name="T"/> elements.
+    /// </summary>
+    /// <typeparam name="T">An element of the curve.</typeparam>
+    public abstract class CurveBuilder<T>
+        : ICurveSampler<T>,
+        IConvertibleCurve<T>
+    {
+        #region data
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        internal SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
+
+        #endregion
+
+        #region properties
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public IReadOnlyCollection<float> Keys => _Keys.Keys;
+
+        public int MaxDegree => _Keys.Count == 0 ? 0 : _Keys.Values.Max(item => item.Degree);
+
+        #endregion
+
+        #region abstract API
+
+        protected abstract bool CheckValue(T value);
+
+        protected abstract T CreateValue(params float[] values);
+
+        public abstract T GetPoint(float offset);
+
+        protected abstract T GetTangent(T fromValue, T toValue);
+
+        #endregion
+
+        #region API
+
+        public void RemoveKey(float offset) { _Keys.Remove(offset); }
+
+        public void SetPoint(float offset, T value, bool isLinear = true)
+        {
+            Guard.IsTrue(CheckValue(value), nameof(value));
+
+            _Keys[offset] = new _CurveNode<T>(value, isLinear);
+        }
+
+        /// <summary>
+        /// Sets the incoming tangent to an existing point.
+        /// </summary>
+        /// <param name="offset">The offset of the existing point.</param>
+        /// <param name="tangent">The tangent value.</param>
+        public void SetIncomingTangent(float offset, T tangent)
+        {
+            Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
+            Guard.IsTrue(CheckValue(tangent), nameof(tangent));
+
+            offset -= float.Epsilon;
+
+            var offsets = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+
+            var a = _Keys[offsets.Item1];
+            var b = _Keys[offsets.Item2];
+
+            if (a.Degree == 1) a.OutgoingTangent = GetTangent(a.Point, b.Point);
+
+            a.Degree = 3;
+            b.IncomingTangent = tangent;
+
+            _Keys[offsets.Item1] = a;
+            _Keys[offsets.Item2] = b;
+        }
+
+        /// <summary>
+        /// Sets the outgoing tangent to an existing point.
+        /// </summary>
+        /// <param name="offset">The offset of the existing point.</param>
+        /// <param name="tangent">The tangent value.</param>
+        public void SetOutgoingTangent(float offset, T tangent)
+        {
+            Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
+            Guard.IsTrue(CheckValue(tangent), nameof(tangent));
+
+            var offsets = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+
+            var a = _Keys[offsets.Item1];
+            var b = _Keys[offsets.Item2];
+
+            if (offsets.Item1 != offsets.Item2)
+            {
+                if (a.Degree == 1) b.IncomingTangent = GetTangent(a.Point, b.Point);
+                _Keys[offsets.Item2] = b;
+            }
+
+            a.Degree = 3;
+            a.OutgoingTangent = tangent;
+
+            _Keys[offsets.Item1] = a;
+        }
+
+        private protected (_CurveNode<T>, _CurveNode<T>, float) FindSample(float offset)
+        {
+            if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
+
+            var offsets = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+
+            return (_Keys[offsets.Item1], _Keys[offsets.Item2], offsets.Item3);
+        }
+
+        #endregion
+
+        #region With* API
+
+        public CurveBuilder<T> WithPoint(float offset, T value, bool isLinear = true)
+        {
+            SetPoint(offset, value, isLinear);
+            return this;
+        }
+
+        public CurveBuilder<T> WithIncomingTangent(float offset, T tangent)
+        {
+            SetIncomingTangent(offset, tangent);
+            return this;
+        }
+
+        public CurveBuilder<T> WithOutgoingTangent(float offset, T tangent)
+        {
+            SetOutgoingTangent(offset, tangent);
+            return this;
+        }
+
+        public CurveBuilder<T> WithPoint(float offset, params float[] values)
+        {
+            return WithPoint(offset, CreateValue(values));
+        }
+
+        public CurveBuilder<T> WithOutgoingTangent(float offset, params float[] values)
+        {
+            return WithOutgoingTangent(offset, CreateValue(values));
+        }
+
+        public CurveBuilder<T> WithIncomingTangent(float offset, params float[] values)
+        {
+            return WithIncomingTangent(offset, CreateValue(values));
+        }
+
+        #endregion
+
+        #region IConvertibleCurve API
+
+        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToStepCurve()
+        {
+            if (MaxDegree != 0) throw new NotSupportedException();
+
+            return _Keys.ToDictionary(item => item.Key, item => item.Value.Point);
+        }
+
+        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToLinearCurve()
+        {
+            var d = new Dictionary<float, T>();
+
+            var orderedKeys = _Keys.Keys.ToList();
+
+            for (int i = 0; i < orderedKeys.Count - 1; ++i)
+            {
+                var a = orderedKeys[i + 0];
+                var b = orderedKeys[i + 1];
+
+                var sa = _Keys[a];
+                var sb = _Keys[b];
+
+                switch (sa.Degree)
+                {
+                    case 0: // simulate a step with an extra key
+                        d[a] = sa.Point;
+                        d[b - float.Epsilon] = sa.Point;
+                        d[b] = sb.Point;
+                        break;
+
+                    case 1:
+                        d[a] = sa.Point;
+                        d[b] = sb.Point;
+                        break;
+
+                    case 3:
+                        var t = a;
+                        while (t < b)
+                        {
+                            d[t] = this.GetPoint(t);
+                            t += 1.0f / 30.0f;
+                        }
+
+                        break;
+
+                    default: throw new NotImplementedException();
+                }
+            }
+
+            return d;
+        }
+
+        IReadOnlyDictionary<float, (T, T, T)> IConvertibleCurve<T>.ToSplineCurve()
+        {
+            var d = new Dictionary<float, (T, T, T)>();
+
+            var orderedKeys = _Keys.Keys.ToList();
+
+            for (int i = 0; i < orderedKeys.Count - 1; ++i)
+            {
+                var a = orderedKeys[i + 0];
+                var b = orderedKeys[i + 1];
+
+                var sa = _Keys[a];
+                var sb = _Keys[b];
+
+                if (!d.TryGetValue(a, out (T, T, T) da)) da = default;
+                if (!d.TryGetValue(b, out (T, T, T) db)) db = default;
+
+                da.Item2 = sa.Point;
+                db.Item2 = sb.Point;
+
+                var delta = GetTangent(da.Item2, db.Item2);
+
+                switch (sa.Degree)
+                {
+                    case 0: // simulate a step with an extra key
+                        da.Item3 = default;
+                        d[b - float.Epsilon] = (default, sa.Point, delta);
+                        db.Item1 = delta;
+                        break;
+
+                    case 1: // tangents are the delta between points
+                        da.Item3 = db.Item1 = delta;
+                        break;
+
+                    case 3: // actual tangents
+                        da.Item3 = sa.OutgoingTangent;
+                        db.Item1 = sb.IncomingTangent;
+                        break;
+
+                    default: throw new NotImplementedException();
+                }
+
+                d[a] = da;
+                d[b] = db;
+            }
+
+            return d;
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("{IncomingTangent} -> {Point}[{Degree}] -> {OutgoingTangent}")]
+    struct _CurveNode<T>
+    {
+        public _CurveNode(T value, bool isLinear)
+        {
+            IncomingTangent = default;
+            Point = value;
+            OutgoingTangent = default;
+            Degree = isLinear ? 1 : 0;
+        }
+
+        public _CurveNode(T incoming, T value, T outgoing)
+        {
+            IncomingTangent = incoming;
+            Point = value;
+            OutgoingTangent = outgoing;
+            Degree = 3;
+        }
+
+        public T IncomingTangent;
+        public T Point;
+        public T OutgoingTangent;
+        public int Degree;
+    }
+}

+ 162 - 0
src/SharpGLTF.Toolkit/Animations/CurveFactory.cs

@@ -0,0 +1,162 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    static class CurveFactory
+    {
+        public static CurveBuilder<T> CreateCurveBuilder<T>()
+        {
+            if (typeof(T) == typeof(Vector3)) return new Vector3CurveBuilder() as CurveBuilder<T>;
+            if (typeof(T) == typeof(Quaternion)) return new QuaternionCurveBuilder() as CurveBuilder<T>;
+            if (typeof(T) == typeof(Single[])) return new ArrayCurveBuilder() as CurveBuilder<T>;
+
+            throw new ArgumentException(nameof(T), "Generic argument not supported");
+        }
+    }
+
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxyVector3))]
+    sealed class Vector3CurveBuilder : CurveBuilder<Vector3>, ICurveSampler<Vector3>
+    {
+        protected override bool CheckValue(Vector3 value)
+        {
+            return true;
+        }
+
+        protected override Vector3 CreateValue(params float[] values)
+        {
+            Guard.NotNull(values, nameof(values));
+            Guard.IsTrue(values.Length == 3, nameof(values));
+            return new Vector3(values[0], values[1], values[2]);
+        }
+
+        protected override Vector3 GetTangent(Vector3 fromValue, Vector3 toValue)
+        {
+            return SamplerFactory.CreateTangent(fromValue, toValue);
+        }
+
+        public override Vector3 GetPoint(Single offset)
+        {
+            var sample = FindSample(offset);
+
+            switch (sample.Item1.Degree)
+            {
+                case 0:
+                    return sample.Item1.Point;
+
+                case 1:
+                    return Vector3.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
+
+                case 3:
+                    return SamplerFactory.CubicLerp
+                            (
+                            sample.Item1.Point, sample.Item1.OutgoingTangent,
+                            sample.Item2.Point, sample.Item2.IncomingTangent,
+                            sample.Item3
+                            );
+
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+    }
+
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxyQuaternion))]
+    sealed class QuaternionCurveBuilder : CurveBuilder<Quaternion>, ICurveSampler<Quaternion>
+    {
+        protected override bool CheckValue(Quaternion value)
+        {
+            return true;
+        }
+
+        protected override Quaternion CreateValue(params float[] values)
+        {
+            Guard.NotNull(values, nameof(values));
+            Guard.IsTrue(values.Length == 4, nameof(values));
+            return new Quaternion(values[0], values[1], values[2], values[3]);
+        }
+
+        protected override Quaternion GetTangent(Quaternion fromValue, Quaternion toValue)
+        {
+            return SamplerFactory.CreateTangent(fromValue, toValue);
+        }
+
+        public override Quaternion GetPoint(float offset)
+        {
+            var sample = FindSample(offset);
+
+            switch (sample.Item1.Degree)
+            {
+                case 0:
+                    return sample.Item1.Point;
+
+                case 1:
+                    return Quaternion.Slerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
+
+                case 3:
+                    return SamplerFactory.CubicLerp
+                            (
+                            sample.Item1.Point, sample.Item1.OutgoingTangent,
+                            sample.Item2.Point, sample.Item2.IncomingTangent,
+                            sample.Item3
+                            );
+
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+    }
+
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxyArray))]
+    sealed class ArrayCurveBuilder : CurveBuilder<Single[]>, ICurveSampler<Single[]>
+    {
+        // the first "CheckValue" will fix any further calls to this value.
+        private int _ValueLength = 0;
+
+        protected override bool CheckValue(Single[] value)
+        {
+            if (value == null || value.Length == 0) return false;
+
+            if (_ValueLength == 0) _ValueLength = value.Length;
+
+            return value.Length == _ValueLength;
+        }
+
+        protected override Single[] CreateValue(params Single[] values)
+        {
+            return values;
+        }
+
+        protected override Single[] GetTangent(Single[] fromValue, Single[] toValue)
+        {
+            return SamplerFactory.CreateTangent(fromValue, toValue);
+        }
+
+        public override Single[] GetPoint(Single offset)
+        {
+            var sample = FindSample(offset);
+
+            switch (sample.Item1.Degree)
+            {
+                case 0:
+                    return sample.Item1.Point;
+
+                case 1:
+                    return SamplerFactory.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
+
+                case 3:
+                    return SamplerFactory.CubicLerp
+                            (
+                            sample.Item1.Point, sample.Item1.OutgoingTangent,
+                            sample.Item2.Point, sample.Item2.IncomingTangent,
+                            sample.Item3
+                            );
+
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+    }
+}

+ 0 - 781
src/SharpGLTF.Toolkit/Animations/Curves.cs

@@ -1,781 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Text;
-using System.Linq;
-
-namespace SharpGLTF.Animations
-{
-    struct _CurveNode<T>
-    {
-        public _CurveNode(T value, bool isLinear)
-        {
-            IncomingTangent = default;
-            Point = value;
-            OutgoingTangent = default;
-            Degree = isLinear ? 1 : 0;
-        }
-
-        public _CurveNode(T incoming, T value, T outgoing)
-        {
-            IncomingTangent = incoming;
-            Point = value;
-            OutgoingTangent = outgoing;
-            Degree = 3;
-        }
-
-        public T IncomingTangent;
-        public T Point;
-        public T OutgoingTangent;
-        public int Degree;
-    }
-
-    // the idea is that depending on the calls we do to this interface, it upgrades the data under the hood.
-    public abstract class CurveBuilder<T> : IConvertibleCurve<T>
-    {
-        #region data
-
-        internal SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
-
-        #endregion
-
-        #region properties
-
-        public IReadOnlyCollection<float> Keys => _Keys.Keys;
-
-        public int MaxDegree => _Keys.Values.Max(item => item.Degree);
-
-        #endregion
-
-        #region API
-
-        public void RemoveKey(float offset) { _Keys.Remove(offset); }
-
-        public void SetKey(float offset, T value, bool isLinear = true)
-        {
-            _Keys[offset] = new _CurveNode<T>(value, isLinear);
-        }
-
-        public void SetKey(float offset, T value, T incomingTangent, T outgoingTangent)
-        {
-            _Keys[offset] = new _CurveNode<T>(incomingTangent, value, outgoingTangent);
-        }
-
-        public CurveBuilder<T> WithKey(float offset, T value, bool isLinear = true)
-        {
-            SetKey(offset, value, isLinear);
-            return this;
-        }
-
-        public CurveBuilder<T> WithKey(float offset, T value, T incomingTangent, T outgoingTangent)
-        {
-            SetKey(offset, value, incomingTangent, outgoingTangent);
-            return this;
-        }
-
-        private protected (_CurveNode<T>, _CurveNode<T>, float) FindSample(float offset)
-        {
-            if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
-
-            var offsets = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
-
-            return (_Keys[offsets.Item1], _Keys[offsets.Item2], offsets.Item3);
-        }
-
-        public abstract T GetPoint(float offset);
-
-        protected abstract T GetTangent(T fromValue, T toValue);
-
-        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToStepCurve()
-        {
-            if (MaxDegree != 0) throw new NotSupportedException();
-
-            return _Keys.ToDictionary(item => item.Key, item => item.Value.Point);
-        }
-
-        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToLinearCurve()
-        {
-            var d = new Dictionary<float, T>();
-
-            var orderedKeys = _Keys.Keys.ToList();
-
-            for (int i = 0; i < orderedKeys.Count - 1; ++i)
-            {
-                var a = orderedKeys[i + 0];
-                var b = orderedKeys[i + 1];
-
-                var sa = _Keys[a];
-                var sb = _Keys[b];
-
-                switch (sa.Degree)
-                {
-                    case 0: // simulate a step with an extra key
-                        d[a] = sa.Point;
-                        d[b - float.Epsilon] = sa.Point;
-                        d[b] = sb.Point;
-                        break;
-
-                    case 1:
-                        d[a] = sa.Point;
-                        d[b] = sb.Point;
-                        break;
-
-                    case 3:
-                        var t = a;
-                        while (t < b)
-                        {
-                            d[t] = this.GetPoint(t);
-                            t += 1.0f / 30.0f;
-                        }
-
-                        break;
-
-                    default: throw new NotImplementedException();
-                }
-            }
-
-            return d;
-        }
-
-        IReadOnlyDictionary<float, (T, T, T)> IConvertibleCurve<T>.ToSplineCurve()
-        {
-            var d = new Dictionary<float, (T, T, T)>();
-
-            var orderedKeys = _Keys.Keys.ToList();
-
-            for (int i = 0; i < orderedKeys.Count - 1; ++i)
-            {
-                var a = orderedKeys[i + 0];
-                var b = orderedKeys[i + 1];
-
-                var sa = _Keys[a];
-                var sb = _Keys[b];
-
-                if (!d.TryGetValue(a, out (T, T, T) da)) da = default;
-                if (!d.TryGetValue(b, out (T, T, T) db)) db = default;
-
-                da.Item2 = sa.Point;
-                db.Item2 = sb.Point;
-
-                var delta = GetTangent(da.Item2, da.Item2);
-
-                switch (sa.Degree)
-                {
-                    case 0: // simulate a step with an extra key
-                        da.Item3 = default;
-                        d[b - float.Epsilon] = (default, sa.Point, delta);
-                        db.Item1 = delta;
-                        break;
-
-                    case 1: // tangents are the delta between points
-                        da.Item3 = db.Item1 = delta;
-                        break;
-
-                    case 3: // actual tangents
-                        da.Item3 = sa.OutgoingTangent;
-                        db.Item1 = sb.IncomingTangent;
-                        break;
-
-                    default: throw new NotImplementedException();
-                }
-
-                d[a] = da;
-                d[b] = db;
-            }
-
-            return d;
-        }
-
-        #endregion
-    }
-
-    static class CurveFactory
-    {
-        // TODO: we could support conversions between linear and cubic (with hermite regression)
-
-        public static CurveBuilder<T> CreateCurveBuilder<T>()
-        {
-            if (typeof(T) == typeof(Vector3)) return new Vector3CurveBuilder() as CurveBuilder<T>;
-            if (typeof(T) == typeof(Quaternion)) return new QuaternionCurveBuilder() as CurveBuilder<T>;
-            if (typeof(T) == typeof(float[])) throw new NotImplementedException();
-
-            throw new ArgumentException(nameof(T), "Generic argument not supported");
-        }
-    }
-
-    sealed class Vector3CurveBuilder : CurveBuilder<Vector3>, ICurveSampler<Vector3>
-    {
-        protected override Vector3 GetTangent(Vector3 fromValue, Vector3 toValue)
-        {
-            return toValue - fromValue;
-        }
-
-        public override Vector3 GetPoint(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return sample.Item1.Point;
-
-            if (sample.Item1.Degree == 1)
-            {
-                return Vector3.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
-            }
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermitePointWeights(sample.Item3);
-
-            return (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-        }
-    }
-
-    sealed class QuaternionCurveBuilder : CurveBuilder<Quaternion>, ICurveSampler<Quaternion>
-    {
-        protected override Quaternion GetTangent(Quaternion fromValue, Quaternion toValue)
-        {
-            return SamplerFactory.CreateTangent(fromValue, toValue, 1);
-        }
-
-        public override Quaternion GetPoint(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return sample.Item1.Point;
-
-            if (sample.Item1.Degree == 1) return Quaternion.Slerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermitePointWeights(sample.Item3);
-
-            var q = (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-
-            return Quaternion.Normalize(q);
-        }
-    }
-
-    //--------------------------------
-    // unused code
-    //--------------------------------
-
-    [System.Diagnostics.DebuggerDisplay("[{_Offset}] = {Sample}")]
-    struct CurvePoint<T>
-        where T : struct
-    {
-        #region lifecycle
-
-        public CurvePoint(Curve<T> curve, float offset)
-        {
-            _Curve = curve;
-            _Offset = offset;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly Curve<T> _Curve;
-        private readonly float _Offset;
-
-        #endregion
-
-        #region properties
-
-        public T Point => _Curve.GetPoint(_Offset);
-
-        public T Tangent => _Curve.GetTangent(_Offset);
-
-        public float LerpAmount => _Curve.FindLerp(_Offset).Item3;
-
-        #endregion
-
-        #region API
-
-        public CurvePoint<T> Split()
-        {
-            // https://pomax.github.io/bezierinfo/#splitting
-
-            _Curve.SplitAt(_Offset);
-
-            return this;
-        }
-
-        public CurvePoint<T> GetAt(float offset) { return new CurvePoint<T>(_Curve, offset); }
-
-        public CurvePoint<T>? GetCurrent()
-        {
-            var offsets = _Curve.FindLerp(_Offset);
-
-            if (_Offset < offsets.Item1) return null;
-
-            return new CurvePoint<T>(_Curve, offsets.Item1);
-        }
-
-        public CurvePoint<T>? GetNext()
-        {
-            var offsets = _Curve.FindLerp(_Offset);
-
-            if (_Offset >= offsets.Item2) return null;
-
-            return new CurvePoint<T>(_Curve, offsets.Item2);
-        }
-
-        public CurvePoint<T> MovePointTo(T value)
-        {
-            Split();
-
-            _Curve.SetPoint(_Offset, value);
-            return this;
-        }
-
-        public CurvePoint<T> MoveIncomingTangentTo(T value)
-        {
-            Split();
-
-            _Curve.SetTangentIn(_Offset, value, 1);
-            return this;
-        }
-
-        public CurvePoint<T> MoveOutgoingTangentTo(T value)
-        {
-            Split();
-
-            _Curve.SetTangentOut(_Offset, value, 1);
-            return this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Represents a collection of consecutive nodes that can be sampled into a continuous curve.
-    /// </summary>
-    /// <typeparam name="T">The type of value evaluated at any point in the curve.</typeparam>
-    abstract class Curve<T> : IConvertibleCurve<T>, ICurveSampler<T>
-        where T : struct
-    {
-        #region lifecycle
-
-        public Curve() { }
-
-        protected Curve(Curve<T> other)
-        {
-            foreach (var kvp in other._Keys)
-            {
-                this._Keys.Add(kvp.Key, kvp.Value);
-            }
-        }
-
-        #endregion
-
-        #region data
-
-        internal SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
-
-        #endregion
-
-        #region properties
-
-        public IReadOnlyCollection<float> Keys => _Keys.Keys;
-
-        /// <summary>
-        /// Gets a value indicating if the keys of this curve are at least Step, Linear, or Spline.
-        /// </summary>
-        public int MaxDegree => _Keys.Values.Select(item => item.Degree).Max();
-
-        #endregion
-
-        #region API
-
-        public void RemoveKey(float key) { _Keys.Remove(key); }
-
-        internal _CurveNode<T>? GetKey(float key) { return _Keys.TryGetValue(key, out _CurveNode<T> value) ? value : (_CurveNode<T>?)null; }
-
-        internal void SetKey(float key, _CurveNode<T> value) { _Keys[key] = value; }
-
-        internal (_CurveNode<T>, _CurveNode<T>, float) FindSample(float offset)
-        {
-            if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
-
-            var offsets = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
-
-            return (_Keys[offsets.Item1], _Keys[offsets.Item2], offsets.Item3);
-        }
-
-        public (float, float, float) FindLerp(float offset) { return SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset); }
-
-        public abstract T GetPoint(float offset);
-
-        public abstract void SetPoint(float offset, T value);
-
-        public abstract T GetTangent(float offset);
-
-        public abstract void SetTangentIn(float key, T value, float scale);
-
-        public abstract void SetTangentOut(float key, T value, float scale);
-
-        public bool SplitAt(float offset)
-        {
-            // https://pomax.github.io/bezierinfo/#splitting
-
-            var lerp = FindLerp(offset);
-
-            if (offset == lerp.Item1) return false;
-
-            var v0 = _Keys[lerp.Item1];
-            var v1 = _Keys[lerp.Item2];
-
-            var p = GetPoint(offset);
-            var t = GetTangent(offset);
-
-            // v0.OutgoingTangent *= lerp.Item3;
-
-            SetTangentIn(offset, t, -lerp.Item3);
-            SetPoint(offset, p);
-            SetTangentOut(offset, t, 1 - lerp.Item3);
-
-            // v1.IncomingTangent *= (1 - lerp.Item3);
-
-            return true;
-        }
-
-        public IReadOnlyDictionary<float, T> ToStepCurve()
-        {
-            Guard.IsTrue(MaxDegree == 0, nameof(MaxDegree));
-
-            // todo: if Degree is not zero we might export sampled data at 60FPS
-
-            return _Keys.ToDictionary(item => item.Key, item => item.Value.Point);
-        }
-
-        public IReadOnlyDictionary<float, T> ToLinearCurve()
-        {
-            var d = new Dictionary<float, T>();
-
-            if (_Keys.Count == 0) return d;
-
-            var v0 = _Keys.First();
-            d[v0.Key] = v0.Value.Point;
-
-            foreach (var v1 in _Keys.Skip(1))
-            {
-                d[v1.Key] = v1.Value.Point;
-
-                if (v0.Value.Degree == 0)
-                {
-                    d[v1.Key - float.Epsilon] = v0.Value.Point;
-                }
-
-                if (v0.Value.Degree == 2)
-                {
-                    var ll = v1.Key - v0.Key;
-
-                    var l = 1 + (int)Math.Ceiling( ll *  15);
-
-                    for (int i = 1; i < l; ++i)
-                    {
-                        var k = v0.Key + (ll * (float)l / (float)i);
-
-                        d[k] = GetPoint(k);
-                    }
-                }
-
-                v0 = v1;
-            }
-
-            return d;
-        }
-
-        public IReadOnlyDictionary<float, (T, T, T)> ToSplineCurve()
-        {
-            throw new NotImplementedException();
-
-            var d = new Dictionary<float, (T, T, T)>();
-
-            return d;
-        }
-
-        #endregion
-    }
-
-    sealed class ScalarSplineCurve : Curve<Single>
-    {
-        #region lifecycle
-
-        public ScalarSplineCurve() { }
-
-        protected ScalarSplineCurve(ScalarSplineCurve other) : base(other) { }
-
-        #endregion
-
-        #region API
-
-        public override float GetPoint(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return sample.Item1.Point;
-
-            if (sample.Item1.Degree == 1)
-            {
-                return (sample.Item1.Point * (1 - sample.Item3)) + (sample.Item2.Point * sample.Item3);
-            }
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermitePointWeights(sample.Item3);
-
-            return (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-        }
-
-        public override float GetTangent(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return 0;
-
-            if (sample.Item1.Degree == 1) return sample.Item2.Point - sample.Item1.Point;
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermiteTangentWeights(sample.Item3);
-
-            return (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-        }
-
-        public override void SetPoint(float key, Single value)
-        {
-            var val = GetKey(key) ?? default;
-            val.Point = value;
-            SetKey(key, val);
-        }
-
-        public override void SetTangentIn(float key, Single value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.IncomingTangent = value * scale;
-            SetKey(key, val);
-        }
-
-        public override void SetTangentOut(float key, Single value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.OutgoingTangent = value * scale;
-            SetKey(key, val);
-        }
-
-        #endregion
-    }
-
-    sealed class Vector3SplineCurve : Curve<Vector3>
-    {
-        #region lifecycle
-
-        public Vector3SplineCurve() { }
-
-        protected Vector3SplineCurve(Vector3SplineCurve other) : base(other) { }
-
-        #endregion
-
-        #region API
-
-        public override Vector3 GetPoint(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return sample.Item1.Point;
-
-            if (sample.Item1.Degree == 1)
-            {
-                return Vector3.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
-            }
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermitePointWeights(sample.Item3);
-
-            return (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-        }
-
-        public override Vector3 GetTangent(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return Vector3.Zero;
-
-            if (sample.Item1.Degree == 1) return sample.Item2.Point - sample.Item1.Point;
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermiteTangentWeights(sample.Item3);
-
-            return (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-        }
-
-        public override void SetPoint(float key, Vector3 value)
-        {
-            var val = GetKey(key) ?? default;
-            val.Point = value;
-            SetKey(key, val);
-        }
-
-        public override void SetTangentIn(float key, Vector3 value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.IncomingTangent = value * scale;
-            SetKey(key, val);
-        }
-
-        public override void SetTangentOut(float key, Vector3 value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.OutgoingTangent = value * scale;
-            SetKey(key, val);
-        }
-
-        #endregion
-    }
-
-    sealed class QuaternionSplineCurve : Curve<Quaternion>
-    {
-        #region lifecycle
-
-        public QuaternionSplineCurve() { }
-
-        protected QuaternionSplineCurve(QuaternionSplineCurve other) : base(other) { }
-
-        #endregion
-
-        #region API
-
-        public override Quaternion GetPoint(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return sample.Item1.Point;
-
-            if (sample.Item1.Degree == 1) return Quaternion.Slerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermitePointWeights(sample.Item3);
-
-            var q = (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-
-            return Quaternion.Normalize(q);
-        }
-
-        public override Quaternion GetTangent(float offset)
-        {
-            var sample = FindSample(offset);
-
-            if (sample.Item1.Degree == 0) return Quaternion.Identity;
-
-            if (sample.Item1.Degree == 1) throw new NotImplementedException();
-
-            System.Diagnostics.Debug.Assert(sample.Item1.Degree == 3, "invalid interpolation mode");
-
-            var pointStart = sample.Item1.Point;
-            var tangentOut = sample.Item1.OutgoingTangent;
-            var pointEnd = sample.Item2.Point;
-            var tangentIn = sample.Item2.IncomingTangent;
-
-            var basis = SamplerFactory.CreateHermiteTangentWeights(sample.Item3);
-
-            var q = (pointStart * basis.Item1) + (pointEnd * basis.Item2) + (tangentOut * basis.Item3) + (tangentIn * basis.Item4);
-
-            return Quaternion.Normalize(q);
-        }
-
-        public override void SetPoint(float key, Quaternion value)
-        {
-            var val = GetKey(key) ?? default;
-            val.Point = Quaternion.Normalize(value);
-            SetKey(key, val);
-        }
-
-        /*
-        public void SetCardinalPointIn(float key, Quaternion value)
-        {
-            var val = GetKey(key) ?? default;
-
-            var inv = Quaternion.Inverse(value);
-            value = Quaternion.Concatenate(val.Point, inv); // *4? => convert to axisradians; angle * 4, back to Q
-            value = Quaternion.Normalize(value);
-
-            val.IncomingTangent = value;
-            SetKey(key, val);
-        }
-
-        public void SetCardinalPointOut(float key, Quaternion value)
-        {
-            var val = GetKey(key) ?? default;
-
-            var inv = Quaternion.Inverse(val.Point);
-            value = Quaternion.Concatenate(value, inv); // *4? => convert to axisradians; angle * 4, back to Q
-            value = Quaternion.Normalize(value);
-
-            val.OutgoingTangent = value;
-            SetKey(key, val);
-        }*/
-
-        public override void SetTangentIn(float key, Quaternion value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.IncomingTangent = _Scale(value, scale);
-            SetKey(key, val);
-        }
-
-        public override void SetTangentOut(float key, Quaternion value, float scale)
-        {
-            var val = GetKey(key) ?? default;
-            val.OutgoingTangent = _Scale(value, scale);
-            SetKey(key, val);
-        }
-
-        internal Quaternion _Scale(Quaternion q, float scale)
-        {
-            var axis = Vector3.Normalize(new Vector3(q.X, q.Y, q.Z));
-            var angle = Math.Acos(q.W) * 2 * scale;
-
-            return Quaternion.CreateFromAxisAngle(axis, (float)angle);
-        }
-
-        #endregion
-    }
-}

+ 108 - 1
src/SharpGLTF.Toolkit/Debug/DebugViews.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
@@ -7,4 +7,111 @@ using System.Text;
 
 namespace SharpGLTF.Debug
 {
+    internal abstract class _CurveBuilderDebugProxy<T>
+    {
+        #region lifecycle
+
+        public _CurveBuilderDebugProxy(Animations.CurveBuilder<T> curve)
+        {
+            _Curve = curve;
+            _CreateItems(curve);
+        }
+
+        private void _CreateItems(Animations.CurveBuilder<T> curve)
+        {
+            Animations._CurveNode<T>? prev = null;
+
+            foreach (var kvp in curve._Keys)
+            {
+                if (prev.HasValue)
+                {
+                    var d = prev.Value.Degree;
+
+                    switch (d)
+                    {
+                        case 0:
+
+                            break;
+
+                        case 1:
+                            _Items.Add(new _OutTangent { Tangent = GetTangent(prev.Value.Point, kvp.Value.Point) });
+                            break;
+
+                        case 3:
+                            _Items.Add(new _OutTangent { Tangent = prev.Value.OutgoingTangent });
+                            _Items.Add(new _InTangent { Tangent = kvp.Value.IncomingTangent });
+                            break;
+
+                        default:
+                            _Items.Add("ERROR: {d}");
+                            break;
+                    }
+                }
+
+                _Items.Add(new _Point { Key = kvp.Key, Point = kvp.Value.Point });
+
+                prev = kvp.Value;
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerDisplay("{Key} => {Point}")]
+        private struct _Point
+        {
+            public float Key;
+            public T Point;
+        }
+
+        [System.Diagnostics.DebuggerDisplay("               🡖 {Tangent}")]
+        private struct _OutTangent { public T Tangent; }
+
+        [System.Diagnostics.DebuggerDisplay("               🡗 {Tangent}")]
+        private struct _InTangent { public T Tangent; }
+
+        private readonly Animations.CurveBuilder<T> _Curve;
+        private readonly List<Object> _Items = new List<object>();
+
+        #endregion
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        public Object[] Items => _Items.ToArray();
+
+        protected abstract T GetTangent(T a, T b);
+    }
+
+    sealed class _CurveBuilderDebugProxyVector3 : _CurveBuilderDebugProxy<Vector3>
+    {
+        public _CurveBuilderDebugProxyVector3(Animations.CurveBuilder<Vector3> curve)
+            : base(curve) { }
+
+        protected override Vector3 GetTangent(Vector3 a, Vector3 b)
+        {
+            return b - a;
+        }
+    }
+
+    sealed class _CurveBuilderDebugProxyQuaternion : _CurveBuilderDebugProxy<Quaternion>
+    {
+        public _CurveBuilderDebugProxyQuaternion(Animations.CurveBuilder<Quaternion> curve)
+            : base(curve) { }
+
+        protected override Quaternion GetTangent(Quaternion a, Quaternion b)
+        {
+            return Animations.SamplerFactory.CreateTangent(a, b);
+        }
+    }
+
+    sealed class _CurveBuilderDebugProxyArray : _CurveBuilderDebugProxy<float[]>
+    {
+        public _CurveBuilderDebugProxyArray(Animations.CurveBuilder<float[]> curve)
+            : base(curve) { }
+
+        protected override float[] GetTangent(float[] a, float[] b)
+        {
+            return Animations.SamplerFactory.CreateTangent(a, b);
+        }
+    }
 }

+ 1 - 1
tests/SharpGLTF.Tests/AnimationSamplingTests.cs

@@ -9,7 +9,7 @@ using NUnit.Framework;
 namespace SharpGLTF
 {
     [TestFixture]
-    [Category("Core")]
+    [Category("Core.Animations")]
     public class AnimationSamplingTests
     {
         [TestCase(0, 0, 0, 1, 1, 1, 1, 0)]

+ 153 - 0
tests/SharpGLTF.Tests/Animations/CurveBuilderTests.cs

@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Animations
+{
+    [Category("Toolkit.Animations")]
+    public class CurveBuilderTests
+    {
+        [Test]
+        public void CreateTranslationCurve1()
+        {
+            // Create a Vector3 curve
+
+            var curve = CurveFactory.CreateCurveBuilder<Vector3>();
+
+            curve
+                .WithPoint(0, 0, 0, 0);
+
+            curve
+                .WithPoint(1, 1, 1, 1)
+                .WithOutgoingTangent(1, 0, 4, 0);
+
+            curve
+                .WithPoint(2, 2, 1, 1)
+                .WithIncomingTangent(2, 0, -4, 0);
+
+            // convert and resample the curve to a linear and cubic curves.
+
+            var convertible = curve as IConvertibleCurve<Vector3>;
+            Assert.NotNull(convertible);
+
+            var linear = convertible.ToLinearCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+            var spline = convertible.ToSplineCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+
+            // check if both curves are roughly the same.
+
+            for (float t=0; t < 2; t+=0.01f)
+            {
+                var ls = linear.GetPoint(t);
+                var ss = spline.GetPoint(t);
+
+                NumericsAssert.AreEqual(ls, ss, 0.002f);
+            }
+
+            // plot the curve.
+
+            convertible
+                .ToLinearCurve()
+                .Select(kvp => new Vector2(kvp.Value.X, kvp.Value.Y))
+                .ToPointSeries()
+                .WithLineType(Plotting.LineType.Continuous)
+                .AttachToCurrentTest("plot.png");
+        }
+
+        [Test]
+        public void CreateRotationCurve1()
+        {
+            // Create a Quaternion curve
+
+            var curve = CurveFactory.CreateCurveBuilder<Quaternion>();
+
+            curve
+                .WithPoint(0, Quaternion.Identity);
+
+            curve
+                .WithPoint(1, Quaternion.CreateFromAxisAngle(Vector3.UnitX,1) )
+                .WithOutgoingTangent(1, Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1) );
+
+            curve
+                .WithPoint(2, Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1) )
+                .WithIncomingTangent(2, Quaternion.CreateFromAxisAngle(Vector3.UnitX, -1) );
+
+            // convert and resample the curve to a linear and cubic curves.
+
+            var convertible = curve as IConvertibleCurve<Quaternion>;
+            Assert.NotNull(convertible);
+
+            var linear = convertible.ToLinearCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+            var spline = convertible.ToSplineCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+
+            // check if both curves are roughly the same.
+
+            for (float t = 0; t < 2; t += 0.01f)
+            {
+                var ls = linear.GetPoint(t);
+                var ss = spline.GetPoint(t);
+
+                NumericsAssert.AreEqual(ls, ss, 0.05f);
+            }
+
+            // plot the curve.
+
+            convertible
+                .ToLinearCurve()
+                .Select(kvp => new Vector2(kvp.Key, kvp.Value.W))
+                .ToPointSeries()
+                .WithLineType(Plotting.LineType.Continuous)
+                .AttachToCurrentTest("plot.png");
+        }
+
+        [Test]
+        public void CreateMorphCurve1()
+        {
+            // Create a Quaternion curve
+
+            var curve = CurveFactory.CreateCurveBuilder<Single[]>();
+
+            curve
+                .WithPoint(0, 0f, 0f);
+
+            curve
+                .WithPoint(1, 1f, 1f)
+                .WithOutgoingTangent(1, 0f, 4f);
+
+            curve
+                .WithPoint(2, 2f, 1f)
+                .WithIncomingTangent(2, 0f, -4f);
+
+            // convert and resample the curve to a linear and cubic curves.
+
+            var convertible = curve as IConvertibleCurve<Single[]>;
+            Assert.NotNull(convertible);
+
+            var linear = convertible.ToLinearCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+            var spline = convertible.ToSplineCurve().Select(kvp => (kvp.Key, kvp.Value)).CreateSampler();
+
+            // check if both curves are roughly the same.
+
+            for (float t = 0; t < 2; t += 0.01f)
+            {
+                var ls = linear.GetPoint(t);
+                var ss = spline.GetPoint(t);
+
+                Assert.AreEqual(ls[0], ss[0], 0.02f);
+                Assert.AreEqual(ls[1], ss[1], 0.02f);
+            }
+
+            // plot the curve.
+
+            convertible
+                .ToLinearCurve()
+                .Select(kvp => new Vector2(kvp.Key, kvp.Value[1]))
+                .ToPointSeries()
+                .WithLineType(Plotting.LineType.Continuous)
+                .AttachToCurrentTest("plot.png");
+        }
+    }
+}

+ 90 - 21
tests/SharpGLTF.Tests/NumericsAssert.cs

@@ -38,63 +38,132 @@ namespace SharpGLTF
             Assert.IsTrue(float.IsFinite(quaternion.W), "W");
         }
 
-        public static void AreEqual(Vector2 expected, Vector2 actual, double delta = 0)
+        public static void IsFinite(Matrix4x4 matrix)
         {
-            Assert.AreEqual(expected.X, actual.X, delta, "X");
-            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
+            Assert.IsTrue(float.IsFinite(matrix.M11), "M11");
+            Assert.IsTrue(float.IsFinite(matrix.M12), "M12");
+            Assert.IsTrue(float.IsFinite(matrix.M13), "M13");
+            Assert.IsTrue(float.IsFinite(matrix.M14), "M14");
+
+            Assert.IsTrue(float.IsFinite(matrix.M21), "M21");
+            Assert.IsTrue(float.IsFinite(matrix.M22), "M22");
+            Assert.IsTrue(float.IsFinite(matrix.M23), "M23");
+            Assert.IsTrue(float.IsFinite(matrix.M24), "M24");
+
+            Assert.IsTrue(float.IsFinite(matrix.M31), "M31");
+            Assert.IsTrue(float.IsFinite(matrix.M32), "M32");
+            Assert.IsTrue(float.IsFinite(matrix.M33), "M33");
+            Assert.IsTrue(float.IsFinite(matrix.M34), "M34");
+
+            Assert.IsTrue(float.IsFinite(matrix.M41), "M41");
+            Assert.IsTrue(float.IsFinite(matrix.M42), "M42");
+            Assert.IsTrue(float.IsFinite(matrix.M43), "M43");
+            Assert.IsTrue(float.IsFinite(matrix.M44), "M44");
         }
 
-        public static void AreEqual(Vector3 expected, Vector3 actual, double delta = 0)
+        public static void AreEqual(Vector2 expected, Vector2 actual, double tolerance = 0)
         {
-            Assert.AreEqual(expected.X, actual.X, delta, "X");
-            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
-            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
+            Assert.AreEqual(expected.X, actual.X, tolerance, "X");
+            Assert.AreEqual(expected.Y, actual.Y, tolerance, "Y");
         }
 
-        public static void AreEqual(Vector4 expected, Vector4 actual, double delta = 0)
+        public static void AreEqual(Vector3 expected, Vector3 actual, double tolerance = 0)
         {
-            Assert.AreEqual(expected.X, actual.X, delta, "X");
-            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
-            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
-            Assert.AreEqual(expected.W, actual.W, delta, "W");
+            Assert.AreEqual(expected.X, actual.X, tolerance, "X");
+            Assert.AreEqual(expected.Y, actual.Y, tolerance, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, tolerance, "Z");
         }
 
-        public static void AreEqual(Quaternion expected, Quaternion actual, double delta = 0)
+        public static void AreEqual(Vector4 expected, Vector4 actual, double tolerance = 0)
         {
-            Assert.AreEqual(expected.X, actual.X, delta, "X");
-            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
-            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
-            Assert.AreEqual(expected.W, actual.W, delta, "W");
+            Assert.AreEqual(expected.X, actual.X, tolerance, "X");
+            Assert.AreEqual(expected.Y, actual.Y, tolerance, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, tolerance, "Z");
+            Assert.AreEqual(expected.W, actual.W, tolerance, "W");
         }
 
-        public static void LengthIsOne(Vector2 vector, double delta = 0)
+        public static void AreEqual(Quaternion expected, Quaternion actual, double tolerance = 0)
+        {
+            Assert.AreEqual(expected.X, actual.X, tolerance, "X");
+            Assert.AreEqual(expected.Y, actual.Y, tolerance, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, tolerance, "Z");
+            Assert.AreEqual(expected.W, actual.W, tolerance, "W");
+        }
+
+        public static void AreEqual(Matrix4x4 expected, Matrix4x4 actual, double delta = 0)
+        {
+            Assert.AreEqual(expected.M11, actual.M11, delta, "M11");
+            Assert.AreEqual(expected.M12, actual.M12, delta, "M12");
+            Assert.AreEqual(expected.M13, actual.M13, delta, "M13");
+            Assert.AreEqual(expected.M14, actual.M14, delta, "M14");
+
+            Assert.AreEqual(expected.M21, actual.M21, delta, "M21");
+            Assert.AreEqual(expected.M22, actual.M22, delta, "M22");
+            Assert.AreEqual(expected.M23, actual.M23, delta, "M23");
+            Assert.AreEqual(expected.M24, actual.M24, delta, "M24");
+
+            Assert.AreEqual(expected.M31, actual.M31, delta, "M31");
+            Assert.AreEqual(expected.M32, actual.M32, delta, "M32");
+            Assert.AreEqual(expected.M33, actual.M33, delta, "M33");
+            Assert.AreEqual(expected.M34, actual.M34, delta, "M34");
+
+            Assert.AreEqual(expected.M41, actual.M41, delta, "M41");
+            Assert.AreEqual(expected.M42, actual.M42, delta, "M42");
+            Assert.AreEqual(expected.M43, actual.M43, delta, "M43");
+            Assert.AreEqual(expected.M44, actual.M44, delta, "M44");
+        }
+
+        public static void IsInvertible(Matrix4x4 matrix)
+        {
+            Assert.IsTrue(Matrix4x4.Invert(matrix, out Matrix4x4 inverted));
+        }
+
+        public static void IsNormalized(Vector2 vector, double delta = 0)
         {
             var lenSquared = vector.X * vector.X + vector.Y * vector.Y;
 
             Assert.AreEqual(1, lenSquared, delta * delta, "Length");
         }
 
-        public static void LengthIsOne(Vector3 vector, double delta = 0)
+        public static void IsNormalized(Vector3 vector, double delta = 0)
         {
             var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
 
             Assert.AreEqual(1, lenSquared, delta * delta, "Length");
         }
 
-        public static void LengthIsOne(Vector4 vector, double delta = 0)
+        public static void IsNormalized(Vector4 vector, double delta = 0)
         {
             var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
 
             Assert.AreEqual(1, lenSquared, delta * delta, "Length");
         }
 
-        public static void LengthIsOne(Quaternion vector, double delta = 0)
+        public static void IsNormalized(Quaternion vector, double delta = 0)
         {
             var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
 
             Assert.AreEqual(1, lenSquared, delta * delta, "Length");
         }
 
+        public static void InRange(Vector2 value, Vector2 min, Vector2 max)
+        {
+            GreaterOrEqual(value, min);
+            LessOrEqual(value, max);
+        }
+
+        public static void InRange(Vector3 value, Vector3 min, Vector3 max)
+        {
+            GreaterOrEqual(value, min);
+            LessOrEqual(value, max);
+        }
+
+        public static void InRange(Vector4 value, Vector4 min, Vector4 max)
+        {
+            GreaterOrEqual(value, min);
+            LessOrEqual(value, max);
+        }
+
         public static void Less(Vector2 arg1, Vector2 arg2)
         {
             Assert.Less(arg1.X, arg2.X, "X");

+ 2 - 2
tests/SharpGLTF.Tests/Scenes/SceneBuilderTests.cs

@@ -47,8 +47,8 @@ namespace SharpGLTF.Scenes
             var pivot = new NodeBuilder();
 
             pivot.UseTranslation("track1")
-                .WithKey(0, Vector3.Zero)
-                .WithKey(1, Vector3.One);
+                .WithPoint(0, Vector3.Zero)
+                .WithPoint(1, Vector3.One);
 
             var scene = new SceneBuilder();