Browse Source

Downgraded System.Text.Json to previous version 4.7.2 which is preferred by NetCore3.1
Refactored some underlaying curve sampling classes (Breaking change) - but not many people, if any, will be using these directly.
Added more Guards and docs.
fixed #80.

Vicente Penades 5 years ago
parent
commit
c45a2d0a61

+ 34 - 12
src/SharpGLTF.Core/Animations/SamplerFactory.cs → src/SharpGLTF.Core/Animations/CurveSampler.cs

@@ -8,8 +8,27 @@ namespace SharpGLTF.Animations
     /// <summary>
     /// <summary>
     /// Utility class to create samplers from curve collections.
     /// Utility class to create samplers from curve collections.
     /// </summary>
     /// </summary>
-    public static class SamplerFactory
+    public static class CurveSampler
     {
     {
+        #region constants
+
+        internal const string StepCurveError = "This is a step curve (MaxDegree = 0), use ToStepCurve(); instead.";
+        internal const string LinearCurveError = "This is a linear curve (MaxDegree = 1), use ToLinearCurve(); instead.";
+        internal const string SplineCurveError = "This is a spline curve (MaxDegree = 3), use ToSplineCurve(); instead.";
+
+        internal static string CurveError(int maxDegree)
+        {
+            switch (maxDegree)
+            {
+                case 0: return StepCurveError;
+                case 1: return LinearCurveError;
+                case 3: return SplineCurveError;
+                default: return "Invalid curve degree";
+            }
+        }
+
+        #endregion
+
         #region sampler utils
         #region sampler utils
 
 
         public static Vector3 CreateTangent(Vector3 fromValue, Vector3 toValue, Single scale = 1)
         public static Vector3 CreateTangent(Vector3 fromValue, Vector3 toValue, Single scale = 1)
@@ -125,7 +144,7 @@ namespace SharpGLTF.Animations
         /// <param name="sequence">A sequence of float+<typeparamref name="T"/> pairs sorted in ascending order.</param>
         /// <param name="sequence">A sequence of float+<typeparamref name="T"/> pairs sorted in ascending order.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <returns>Two consecutive <typeparamref name="T"/> values and a float amount to LERP amount.</returns>
         /// <returns>Two consecutive <typeparamref name="T"/> values and a float amount to LERP amount.</returns>
-        public static (T A, T B, Single Amount) FindPairContainingOffset<T>(this IEnumerable<(float Key, T Value)> sequence, float offset)
+        public static (T A, T B, Single Amount) FindRangeContainingOffset<T>(this IEnumerable<(float Key, T Value)> sequence, float offset)
         {
         {
             Guard.NotNull(sequence, nameof(sequence));
             Guard.NotNull(sequence, nameof(sequence));
 
 
@@ -135,12 +154,12 @@ namespace SharpGLTF.Animations
             (float Key, T Value)? right = null;
             (float Key, T Value)? right = null;
             (float Key, T Value)? prev = null;
             (float Key, T Value)? prev = null;
 
 
-            var first = sequence.First();
-            if (offset < first.Key) offset = first.Key;
+            var (firstKey, _) = sequence.First();
+            if (offset < firstKey) offset = firstKey;
 
 
             foreach (var item in sequence)
             foreach (var item in sequence)
             {
             {
-                System.Diagnostics.Debug.Assert(condition: !prev.HasValue || prev.Value.Key < item.Key, "Values in the sequence must be sorted ascending.");
+                System.Diagnostics.Debug.Assert(!prev.HasValue || prev.Value.Key < item.Key, "Values in the sequence must be sorted ascending.");
 
 
                 if (item.Key == offset)
                 if (item.Key == offset)
                 {
                 {
@@ -179,7 +198,7 @@ namespace SharpGLTF.Animations
         /// <param name="sequence">A sequence of offsets sorted in ascending order.</param>
         /// <param name="sequence">A sequence of offsets sorted in ascending order.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <returns>Two consecutive offsets and a LERP amount.</returns>
         /// <returns>Two consecutive offsets and a LERP amount.</returns>
-        public static (Single A, Single B, Single Amount) FindPairContainingOffset(IEnumerable<float> sequence, float offset)
+        public static (Single A, Single B, Single Amount) FindRangeContainingOffset(IEnumerable<float> sequence, float offset)
         {
         {
             Guard.NotNull(sequence, nameof(sequence));
             Guard.NotNull(sequence, nameof(sequence));
 
 
@@ -226,6 +245,15 @@ namespace SharpGLTF.Animations
             return (left.Value, right.Value, amount);
             return (left.Value, right.Value, amount);
         }
         }
 
 
+        /// <summary>
+        /// Splits the input sequence into chunks of 1 second for faster access
+        /// </summary>
+        /// <remarks>
+        ///  The first and last keys outside the range of each chunk are duplicated, so each chunk can be evaluated for the whole second.
+        /// </remarks>
+        /// <typeparam name="T">The curve key type.</typeparam>
+        /// <param name="sequence">A timed sequence of curve keys.</param>
+        /// <returns>A sequence of 1 second chunks.</returns>
         internal static IEnumerable<(float, T)[]> SplitByTime<T>(this IEnumerable<(Single Time, T Value)> sequence)
         internal static IEnumerable<(float, T)[]> SplitByTime<T>(this IEnumerable<(Single Time, T Value)> sequence)
         {
         {
             if (!sequence.Any()) yield break;
             if (!sequence.Any()) yield break;
@@ -323,12 +351,6 @@ namespace SharpGLTF.Animations
 
 
         #region sampler creation
         #region sampler creation
 
 
-        internal static IEnumerable<T> Isolate<T>(this IEnumerable<T> collection, bool isolateMemory)
-        {
-            if (isolateMemory) collection = collection.ToArray();
-            return collection;
-        }
-
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         {
         {
             if (collection == null) return null;
             if (collection == null) return null;

+ 29 - 49
src/SharpGLTF.Core/Animations/CubicSamplers.cs → src/SharpGLTF.Core/Animations/CurveSamplers.Cubic.cs

@@ -32,9 +32,9 @@ namespace SharpGLTF.Animations
 
 
         public Vector3 GetPoint(float offset)
         public Vector3 GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -42,14 +42,14 @@ namespace SharpGLTF.Animations
                 );
                 );
         }
         }
 
 
-        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
+        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToStepCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
+        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToLinearCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
@@ -59,12 +59,7 @@ namespace SharpGLTF.Animations
 
 
         public ICurveSampler<Vector3> ToFastSampler()
         public ICurveSampler<Vector3> ToFastSampler()
         {
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new Vector3CubicSampler(item))
-                .Cast<ICurveSampler<Vector3>>();
-
-            return new FastSampler<Vector3>(split);
+            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3CubicSampler(chunk)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
@@ -96,9 +91,9 @@ namespace SharpGLTF.Animations
 
 
         public Quaternion GetPoint(float offset)
         public Quaternion GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -106,14 +101,14 @@ namespace SharpGLTF.Animations
                 );
                 );
         }
         }
 
 
-        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
+        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToStepCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
+        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToLinearCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
@@ -123,12 +118,7 @@ namespace SharpGLTF.Animations
 
 
         public ICurveSampler<Quaternion> ToFastSampler()
         public ICurveSampler<Quaternion> ToFastSampler()
         {
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new QuaternionCubicSampler(item))
-                .Cast<ICurveSampler<Quaternion>>();
-
-            return new FastSampler<Quaternion>(split);
+            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionCubicSampler(chunk)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
@@ -160,7 +150,7 @@ namespace SharpGLTF.Animations
 
 
         public Transforms.SparseWeight8 GetPoint(float offset)
         public Transforms.SparseWeight8 GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
             return Transforms.SparseWeight8.InterpolateCubic
             return Transforms.SparseWeight8.InterpolateCubic
                 (
                 (
@@ -170,14 +160,14 @@ namespace SharpGLTF.Animations
                 );
                 );
         }
         }
 
 
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
+        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToStepCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
+        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToLinearCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
@@ -187,21 +177,16 @@ namespace SharpGLTF.Animations
 
 
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         {
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new SparseCubicSampler(item))
-                .Cast<ICurveSampler<Transforms.SparseWeight8>>();
-
-            return new FastSampler<Transforms.SparseWeight8>(split);
+            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseCubicSampler(chunk)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
     }
     }
 
 
     /// <summary>
     /// <summary>
-    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
+    /// Defines a <see cref="Single"/>[] curve sampler that can be sampled with CUBIC interpolation.
     /// </summary>
     /// </summary>
-    readonly struct ArrayCubicSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
+    readonly struct ArrayCubicSampler : ICurveSampler<Single[]>, IConvertibleCurve<Single[]>
     {
     {
         #region lifecycle
         #region lifecycle
 
 
@@ -224,9 +209,9 @@ namespace SharpGLTF.Animations
 
 
         public float[] GetPoint(float offset)
         public float[] GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -234,14 +219,14 @@ namespace SharpGLTF.Animations
                 );
                 );
         }
         }
 
 
-        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToStepCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
+        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToLinearCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
@@ -251,12 +236,7 @@ namespace SharpGLTF.Animations
 
 
         public ICurveSampler<float[]> ToFastSampler()
         public ICurveSampler<float[]> ToFastSampler()
         {
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new ArrayCubicSampler(item))
-                .Cast<ICurveSampler<float[]>>();
-
-            return new FastSampler<float[]>(split);
+            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayCubicSampler(chunk)) ?? this;
         }
         }
 
 
         #endregion
         #endregion

+ 26 - 46
src/SharpGLTF.Core/Animations/LinearSamplers.cs → src/SharpGLTF.Core/Animations/CurveSamplers.Linear.cs

@@ -43,19 +43,19 @@ namespace SharpGLTF.Animations
 
 
         public T GetPoint(float offset) { return _Value; }
         public T GetPoint(float offset) { return _Value; }
 
 
-        public IReadOnlyDictionary<float, T> ToLinearCurve()
+        public IReadOnlyDictionary<float, T> ToStepCurve()
         {
         {
             return new Dictionary<float, T> { [0] = _Value };
             return new Dictionary<float, T> { [0] = _Value };
         }
         }
 
 
-        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
         {
         {
-            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
+            return new Dictionary<float, T> { [0] = _Value };
         }
         }
 
 
-        public IReadOnlyDictionary<float, T> ToStepCurve()
+        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
         {
         {
-            return new Dictionary<float, T> { [0] = _Value };
+            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
         }
         }
 
 
         #endregion
         #endregion
@@ -89,7 +89,7 @@ namespace SharpGLTF.Animations
 
 
         public Vector3 GetPoint(float offset)
         public Vector3 GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
             if (!_Linear) return valA;
             if (!_Linear) return valA;
 
 
@@ -98,30 +98,25 @@ namespace SharpGLTF.Animations
 
 
         public IReadOnlyDictionary<float, Vector3> ToStepCurve()
         public IReadOnlyDictionary<float, Vector3> ToStepCurve()
         {
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
         public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
         {
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
         public ICurveSampler<Vector3> ToFastSampler()
         public ICurveSampler<Vector3> ToFastSampler()
         {
         {
             var linear = _Linear;
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new Vector3LinearSampler(item, linear))
-                .Cast<ICurveSampler<Vector3>>();
-
-            return new FastSampler<Vector3>(split);
+            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3LinearSampler(chunk, linear)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
@@ -155,7 +150,7 @@ namespace SharpGLTF.Animations
 
 
         public Quaternion GetPoint(float offset)
         public Quaternion GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
             if (!_Linear) return valA;
             if (!_Linear) return valA;
 
 
@@ -164,30 +159,25 @@ namespace SharpGLTF.Animations
 
 
         public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
         public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
         {
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
         public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
         {
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
         public ICurveSampler<Quaternion> ToFastSampler()
         public ICurveSampler<Quaternion> ToFastSampler()
         {
         {
             var linear = _Linear;
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new QuaternionLinearSampler(item, linear))
-                .Cast<ICurveSampler<Quaternion>>();
-
-            return new FastSampler<Quaternion>(split);
+            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionLinearSampler(chunk, linear)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
@@ -221,7 +211,7 @@ namespace SharpGLTF.Animations
 
 
         public Transforms.SparseWeight8 GetPoint(float offset)
         public Transforms.SparseWeight8 GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
             if (!_Linear) return valA;
             if (!_Linear) return valA;
 
 
@@ -232,29 +222,24 @@ namespace SharpGLTF.Animations
 
 
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
         {
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         {
         {
             var linear = _Linear;
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new SparseLinearSampler(item, linear))
-                .Cast<ICurveSampler<Transforms.SparseWeight8>>();
-
-            return new FastSampler<Transforms.SparseWeight8>(split);
+            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseLinearSampler(chunk, linear)) ?? this;
         }
         }
 
 
         #endregion
         #endregion
@@ -288,39 +273,34 @@ namespace SharpGLTF.Animations
 
 
         public float[] GetPoint(float offset)
         public float[] GetPoint(float offset)
         {
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
             if (!_Linear) return valA;
             if (!_Linear) return valA;
 
 
-            return SamplerFactory.InterpolateLinear(valA, valB, amount);
+            return CurveSampler.InterpolateLinear(valA, valB, amount);
         }
         }
 
 
         public IReadOnlyDictionary<float, float[]> ToStepCurve()
         public IReadOnlyDictionary<float, float[]> ToStepCurve()
         {
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, float[]> ToLinearCurve()
         public IReadOnlyDictionary<float, float[]> ToLinearCurve()
         {
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
         }
 
 
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
         {
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
         public ICurveSampler<float[]> ToFastSampler()
         public ICurveSampler<float[]> ToFastSampler()
         {
         {
             var linear = _Linear;
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new ArrayLinearSampler(item, linear))
-                .Cast<ICurveSampler<float[]>>();
-
-            return new FastSampler<float[]>(split);
+            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayLinearSampler(chunk, linear)) ?? this;
         }
         }
 
 
         #endregion
         #endregion

+ 56 - 0
src/SharpGLTF.Core/Animations/FastCurveSampler.cs

@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Wraps a collection of samplers split over time to speed up key retrieval.
+    /// </summary>
+    /// <typeparam name="T">The value sampled at any offset</typeparam>
+    readonly struct FastCurveSampler<T> : ICurveSampler<T>
+    {
+        /// <summary>
+        /// Creates a new, read only <see cref="ICurveSampler{T}"/> that has been optimized for fast sampling.
+        /// </summary>
+        /// <remarks>
+        /// Sampling a raw curve with a large number of keys can be underperformant. This code splits the keys into 1 second
+        /// chunks that can be accessed at much faster speed.
+        /// </remarks>
+        /// <typeparam name="TKey">The value of a key (may include tangents)</typeparam>
+        /// <param name="sequence">A sequence of Time-Key entries, ordered by Time.</param>
+        /// <param name="chunkFactory">A curve chunk factory function.</param>
+        /// <returns>The new, optimized curve sampler.</returns>
+        public static ICurveSampler<T> CreateFrom<TKey>(IEnumerable<(float, TKey)> sequence, Func<(float, TKey)[], ICurveSampler<T>> chunkFactory)
+        {
+            // not enough keys, or not worth optimizing it.
+            if (!sequence.Skip(3).Any()) return null;
+
+            var split = sequence
+                .SplitByTime()
+                .Select(item => chunkFactory.Invoke(item))
+                .Cast<ICurveSampler<T>>();
+
+            return new FastCurveSampler<T>(split);
+        }
+
+        private FastCurveSampler(IEnumerable<ICurveSampler<T>> samplers)
+        {
+            _Samplers = samplers.ToArray();
+        }
+
+        private readonly ICurveSampler<T>[] _Samplers;
+
+        public T GetPoint(float offset)
+        {
+            if (offset < 0) offset = 0;
+
+            var index = (int)offset;
+
+            if (index >= _Samplers.Length) index = _Samplers.Length - 1;
+
+            return _Samplers[index].GetPoint(offset);
+        }
+    }
+}

+ 0 - 32
src/SharpGLTF.Core/Animations/FastSampler.cs

@@ -1,32 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace SharpGLTF.Animations
-{
-    /// <summary>
-    /// Wraps a collection of samplers split over time to speed up key retrieval.
-    /// </summary>
-    /// <typeparam name="T">The value sampled at any offset</typeparam>
-    readonly struct FastSampler<T> : ICurveSampler<T>
-    {
-        public FastSampler(IEnumerable<ICurveSampler<T>> samplers)
-        {
-            _Samplers = samplers.ToArray();
-        }
-
-        private readonly ICurveSampler<T>[] _Samplers;
-
-        public T GetPoint(float offset)
-        {
-            if (offset < 0) offset = 0;
-
-            var index = (int)offset;
-
-            if (index >= _Samplers.Length) index = _Samplers.Length - 1;
-
-            return _Samplers[index].GetPoint(offset);
-        }
-    }
-}

+ 20 - 1
src/SharpGLTF.Core/Animations/Interfaces.cs

@@ -5,11 +5,16 @@ using System.Text;
 namespace SharpGLTF.Animations
 namespace SharpGLTF.Animations
 {
 {
     /// <summary>
     /// <summary>
-    /// Defines a curve that can be sampled at specific points.
+    /// Defines a curve that can be sampled at any point.
     /// </summary>
     /// </summary>
     /// <typeparam name="T">The type of a point in the curve.</typeparam>
     /// <typeparam name="T">The type of a point in the curve.</typeparam>
     public interface ICurveSampler<T>
     public interface ICurveSampler<T>
     {
     {
+        /// <summary>
+        /// Samples the curve at the given offset.
+        /// </summary>
+        /// <param name="offset">The curve offset to sample.</param>
+        /// <returns>The value of the curve at <paramref name="offset"/>.</returns>
         T GetPoint(Single offset);
         T GetPoint(Single offset);
     }
     }
 
 
@@ -27,8 +32,22 @@ namespace SharpGLTF.Animations
         /// </summary>
         /// </summary>
         int MaxDegree { get; }
         int MaxDegree { get; }
 
 
+        /// <summary>
+        /// Gets a STEP interpolated curve. Use only when <see cref="MaxDegree"/> is 0.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, T> ToStepCurve();
         IReadOnlyDictionary<Single, T> ToStepCurve();
+
+        /// <summary>
+        /// Gets a LINEAR interpolated curve. Use only when <see cref="MaxDegree"/> is 1.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, T> ToLinearCurve();
         IReadOnlyDictionary<Single, T> ToLinearCurve();
+
+        /// <summary>
+        /// Gets a CUBIC interpolated curve. Use only when <see cref="MaxDegree"/> is 3.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, (T TangentIn, T Value, T TangentOut)> ToSplineCurve();
         IReadOnlyDictionary<Single, (T TangentIn, T Value, T TangentOut)> ToSplineCurve();
     }
     }
 }
 }

+ 8 - 8
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -37,14 +37,14 @@ namespace SharpGLTF.Memory
         internal static Byte[] DefaultPngImage => Convert.FromBase64String(DEFAULT_PNG_IMAGE);
         internal static Byte[] DefaultPngImage => Convert.FromBase64String(DEFAULT_PNG_IMAGE);
 
 
         internal static readonly string[] _EmbeddedHeaders =
         internal static readonly string[] _EmbeddedHeaders =
-                { EMBEDDED_OCTET_STREAM
-                , EMBEDDED_GLTF_BUFFER
-                , EMBEDDED_JPEG_BUFFER
-                , EMBEDDED_PNG_BUFFER
-                , EMBEDDED_DDS_BUFFER
-                , EMBEDDED_WEBP_BUFFER
-                , EMBEDDED_KTX2_BUFFER
-                };
+            { EMBEDDED_OCTET_STREAM
+            , EMBEDDED_GLTF_BUFFER
+            , EMBEDDED_JPEG_BUFFER
+            , EMBEDDED_PNG_BUFFER
+            , EMBEDDED_DDS_BUFFER
+            , EMBEDDED_WEBP_BUFFER
+            , EMBEDDED_KTX2_BUFFER
+            };
 
 
         public static MemoryImage Empty => default;
         public static MemoryImage Empty => default;
 
 

+ 48 - 1
src/SharpGLTF.Core/Schema2/gltf.AnimationSampler.cs

@@ -93,7 +93,14 @@ namespace SharpGLTF.Schema2
 
 
         public Accessor Output => this.LogicalParent.LogicalParent.LogicalAccessors[this._output];
         public Accessor Output => this.LogicalParent.LogicalParent.LogicalAccessors[this._output];
 
 
-        public float Duration { get { var keys = Input.AsScalarArray(); return keys.Count == 0 ? 0 : keys[keys.Count - 1]; } }
+        public float Duration
+        {
+            get
+            {
+                var keys = Input.AsScalarArray();
+                return keys.Count == 0 ? 0 : keys[keys.Count - 1];
+            }
+        }
 
 
         #endregion
         #endregion
 
 
@@ -101,6 +108,9 @@ namespace SharpGLTF.Schema2
 
 
         private Accessor _CreateInputAccessor(IReadOnlyList<Single> input)
         private Accessor _CreateInputAccessor(IReadOnlyList<Single> input)
         {
         {
+            Guard.NotNull(input, nameof(input));
+            Guard.MustBeGreaterThan(input.Count, 0, nameof(input.Count));
+
             var root = LogicalParent.LogicalParent;
             var root = LogicalParent.LogicalParent;
 
 
             var buffer = root.CreateBufferView(input.Count * 4);
             var buffer = root.CreateBufferView(input.Count * 4);
@@ -117,6 +127,9 @@ namespace SharpGLTF.Schema2
 
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<Vector3> output)
         private Accessor _CreateOutputAccessor(IReadOnlyList<Vector3> output)
         {
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+
             var root = LogicalParent.LogicalParent;
             var root = LogicalParent.LogicalParent;
 
 
             var buffer = root.CreateBufferView(output.Count * 4 * 3);
             var buffer = root.CreateBufferView(output.Count * 4 * 3);
@@ -136,6 +149,9 @@ namespace SharpGLTF.Schema2
 
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<Quaternion> output)
         private Accessor _CreateOutputAccessor(IReadOnlyList<Quaternion> output)
         {
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+
             var root = LogicalParent.LogicalParent;
             var root = LogicalParent.LogicalParent;
 
 
             var buffer = root.CreateBufferView(output.Count * 4 * 4);
             var buffer = root.CreateBufferView(output.Count * 4 * 4);
@@ -152,6 +168,10 @@ namespace SharpGLTF.Schema2
 
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<SparseWeight8> output, int expandedCount)
         private Accessor _CreateOutputAccessor(IReadOnlyList<SparseWeight8> output, int expandedCount)
         {
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+            Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
+
             var root = LogicalParent.LogicalParent;
             var root = LogicalParent.LogicalParent;
 
 
             var buffer = root.CreateBufferView(output.Count * 4 * expandedCount);
             var buffer = root.CreateBufferView(output.Count * 4 * expandedCount);
@@ -178,6 +198,8 @@ namespace SharpGLTF.Schema2
 
 
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, TValue> keyframes)
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, TValue> keyframes)
         {
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+
             var sorted = keyframes
             var sorted = keyframes
                 .OrderBy(item => item.Key)
                 .OrderBy(item => item.Key)
                 .ToList();
                 .ToList();
@@ -196,6 +218,8 @@ namespace SharpGLTF.Schema2
 
 
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, (TValue TangentIn, TValue Value, TValue TangentOut)> keyframes)
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, (TValue TangentIn, TValue Value, TValue TangentOut)> keyframes)
         {
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+
             var sorted = keyframes
             var sorted = keyframes
                 .OrderBy(item => item.Key)
                 .OrderBy(item => item.Key)
                 .ToList();
                 .ToList();
@@ -216,6 +240,8 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, Vector3> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, Vector3> keyframes)
         {
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
@@ -223,6 +249,8 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, Quaternion> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, Quaternion> keyframes)
         {
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
@@ -230,6 +258,8 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, SparseWeight8> keyframes, int expandedCount)
         internal void SetKeys(IReadOnlyDictionary<Single, SparseWeight8> keyframes, int expandedCount)
         {
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
@@ -237,8 +267,14 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+            Guard.MustBeGreaterThan(keyframes.Count, 0, nameof(keyframes.Count));
+
+            // splits the dictionary into separated input/output collections, also, the output will be flattened to plain Vector3 values.
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
+            System.Diagnostics.Debug.Assert(keys.Length * 3 == values.Length, "keys and values must have 1 to 3 ratio");
 
 
+            // fix for first incoming tangent and last outgoing tangent
             // this might not be true for a looped animation, where first and last might be the same
             // this might not be true for a looped animation, where first and last might be the same
             values[0] = Vector3.Zero;
             values[0] = Vector3.Zero;
             values[values.Length - 1] = Vector3.Zero;
             values[values.Length - 1] = Vector3.Zero;
@@ -249,8 +285,13 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            // splits the dictionary into separated input/output collections, also, the output will be flattened to plain Vector3 values.
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
+            System.Diagnostics.Debug.Assert(keys.Length * 3 == values.Length, "keys and values must have 1 to 3 ratio");
 
 
+            // fix for first incoming tangent and last outgoing tangent
             // this might not be true for a looped animation, where first and last might be the same
             // this might not be true for a looped animation, where first and last might be the same
             values[0] = default;
             values[0] = default;
             values[values.Length - 1] = default;
             values[values.Length - 1] = default;
@@ -261,8 +302,14 @@ namespace SharpGLTF.Schema2
 
 
         internal void SetKeys(IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int expandedCount)
         internal void SetKeys(IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int expandedCount)
         {
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+            Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
+
+            // splits the dictionary into separated input/output collections, also, the output will be flattened to plain Vector3 values.
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
+            System.Diagnostics.Debug.Assert(keys.Length * 3 == values.Length, "keys and values must have 1 to 3 ratio");
 
 
+            // fix for first incoming tangent and last outgoing tangent
             // this might not be true for a looped animation, where first and last might be the same
             // this might not be true for a looped animation, where first and last might be the same
             values[0] = default;
             values[0] = default;
             values[values.Length - 1] = default;
             values[values.Length - 1] = default;

+ 24 - 0
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -69,6 +69,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -79,6 +82,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -89,6 +95,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, Quaternion> keyframes, bool linear = true)
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, Quaternion> keyframes, bool linear = true)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -99,6 +108,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -109,6 +121,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -119,6 +134,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
             sampler.SetKeys(keyframes);
             sampler.SetKeys(keyframes);
@@ -129,6 +147,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
 
             sampler.SetKeys(keyframes, morphCount);
             sampler.SetKeys(keyframes, morphCount);
@@ -139,6 +160,9 @@ namespace SharpGLTF.Schema2
 
 
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int morphCount)
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int morphCount)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
             sampler.SetKeys(keyframes, morphCount);
             sampler.SetKeys(keyframes, morphCount);

+ 2 - 1
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -24,7 +24,8 @@
   </ItemGroup>  
   </ItemGroup>  
   
   
   <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
   <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
-    <PackageReference Include="System.Text.Json" Version="5.0.0" />
+    <!-- TODO: Only target 5.0.0 when we add Net5 -->
+    <PackageReference Include="System.Text.Json" Version="4.7.2" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 1 - 1
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -428,7 +428,7 @@ namespace SharpGLTF.Transforms
         /// <returns>A new <see cref="SparseWeight8"/></returns>
         /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 InterpolateCubic(in SparseWeight8 x, in SparseWeight8 xt, in SparseWeight8 y, in SparseWeight8 yt, float amount)
         public static SparseWeight8 InterpolateCubic(in SparseWeight8 x, in SparseWeight8 xt, in SparseWeight8 y, in SparseWeight8 yt, float amount)
         {
         {
-            var basis = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+            var basis = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
 
             return _OperateCubic(x, xt, y, yt, (xx, xxt, yy, yyt) => (xx * basis.StartPosition) + (yy * basis.EndPosition) + (xxt * basis.StartTangent) + (yyt * basis.EndTangent));
             return _OperateCubic(x, xt, y, yt, (xx, xxt, yy, yyt) => (xx * basis.StartPosition) + (yy * basis.EndPosition) + (xxt * basis.StartTangent) + (yyt * basis.EndTangent));
         }
         }

+ 10 - 9
src/SharpGLTF.Toolkit/Animations/AnimatableProperty.cs

@@ -92,17 +92,15 @@ namespace SharpGLTF.Animations
         /// Assigns an animation curve to a given track.
         /// Assigns an animation curve to a given track.
         /// </summary>
         /// </summary>
         /// <param name="track">The name of the track.</param>
         /// <param name="track">The name of the track.</param>
-        /// <param name="curve">A <see cref="ICurveSampler{T}"/> instance, or null to remove a track.</param>
+        /// <param name="curve">
+        /// A <see cref="ICurveSampler{T}"/> instance which also
+        /// implements <see cref="IConvertibleCurve{T}"/>,
+        /// or null to remove a track.
+        /// </param>
         public void SetTrack(string track, ICurveSampler<T> curve)
         public void SetTrack(string track, ICurveSampler<T> curve)
         {
         {
             Guard.NotNullOrEmpty(track, nameof(track));
             Guard.NotNullOrEmpty(track, nameof(track));
 
 
-            if (curve != null)
-            {
-                var convertible = curve as IConvertibleCurve<T>;
-                Guard.NotNull(convertible, nameof(curve), $"Provided {nameof(ICurveSampler<T>)} {nameof(curve)} must implement {nameof(IConvertibleCurve<T>)} interface.");
-            }
-
             // remove track
             // remove track
             if (curve == null)
             if (curve == null)
             {
             {
@@ -112,6 +110,9 @@ namespace SharpGLTF.Animations
                 return;
                 return;
             }
             }
 
 
+            curve.GetPoint(0); // make a single evaluation to ensure it's an evaluable curve.
+            Guard.IsTrue(curve is IConvertibleCurve<T>, nameof(curve), $"Provided {nameof(ICurveSampler<T>)} {nameof(curve)} must implement {nameof(IConvertibleCurve<T>)} interface.");
+
             // insert track
             // insert track
             if (_Tracks == null) _Tracks = new Dictionary<string, ICurveSampler<T>>();
             if (_Tracks == null) _Tracks = new Dictionary<string, ICurveSampler<T>>();
 
 
@@ -124,13 +125,13 @@ namespace SharpGLTF.Animations
 
 
             if (_Tracks == null || !_Tracks.TryGetValue(track, out ICurveSampler<T> sampler))
             if (_Tracks == null || !_Tracks.TryGetValue(track, out ICurveSampler<T> sampler))
             {
             {
-                sampler = CurveFactory.CreateCurveBuilder<T>() as ICurveSampler<T>;
+                sampler = CurveFactory.CreateCurveBuilder<T>();
                 SetTrack(track, sampler);
                 SetTrack(track, sampler);
             }
             }
 
 
             if (sampler is CurveBuilder<T> builder) return builder;
             if (sampler is CurveBuilder<T> builder) return builder;
 
 
-            throw new NotImplementedException();
+            throw new NotImplementedException($"Underlaying curve must be of type CurveBuilder<{nameof(T)}>");
 
 
             // TODO: CurveFactory.CreateCurveBuilder(sampler);
             // TODO: CurveFactory.CreateCurveBuilder(sampler);
         }
         }

+ 12 - 3
src/SharpGLTF.Toolkit/Animations/CurveBuilder.cs

@@ -102,7 +102,7 @@ namespace SharpGLTF.Animations
 
 
             offset -= float.Epsilon;
             offset -= float.Epsilon;
 
 
-            var (keyA, keyB, _) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
 
             var a = _Keys[keyA];
             var a = _Keys[keyA];
             var b = _Keys[keyB];
             var b = _Keys[keyB];
@@ -126,7 +126,7 @@ namespace SharpGLTF.Animations
         {
         {
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
 
 
-            var (keyA, keyB, _) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
 
             var a = _Keys[keyA];
             var a = _Keys[keyA];
             var b = _Keys[keyB];
             var b = _Keys[keyB];
@@ -148,7 +148,7 @@ namespace SharpGLTF.Animations
         {
         {
             if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
             if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
 
 
-            var (keyA, keyB, amount) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, amount) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
 
             return (_Keys[keyA], _Keys[keyB], amount);
             return (_Keys[keyA], _Keys[keyB], amount);
         }
         }
@@ -240,6 +240,15 @@ namespace SharpGLTF.Animations
         {
         {
             var d = new Dictionary<float, T>();
             var d = new Dictionary<float, T>();
 
 
+            if (_Keys.Count == 0) return d;
+
+            if (Keys.Count == 1)
+            {
+                var k = _Keys.First();
+                d[k.Key] = k.Value.Point;
+                return d;
+            }
+
             var orderedKeys = _Keys.Keys.ToList();
             var orderedKeys = _Keys.Keys.ToList();
 
 
             for (int i = 0; i < orderedKeys.Count - 1; ++i)
             for (int i = 0; i < orderedKeys.Count - 1; ++i)

+ 4 - 4
src/SharpGLTF.Toolkit/Animations/CurveFactory.cs

@@ -54,7 +54,7 @@ namespace SharpGLTF.Animations
 
 
         protected override Vector3 GetTangent(Vector3 fromValue, Vector3 toValue)
         protected override Vector3 GetTangent(Vector3 fromValue, Vector3 toValue)
         {
         {
-            return SamplerFactory.CreateTangent(fromValue, toValue);
+            return CurveSampler.CreateTangent(fromValue, toValue);
         }
         }
 
 
         public override Vector3 GetPoint(Single offset)
         public override Vector3 GetPoint(Single offset)
@@ -70,7 +70,7 @@ namespace SharpGLTF.Animations
                     return Vector3.Lerp(sample.A.Point, sample.B.Point, sample.Amount);
                     return Vector3.Lerp(sample.A.Point, sample.B.Point, sample.Amount);
 
 
                 case 3:
                 case 3:
-                    return SamplerFactory.InterpolateCubic
+                    return CurveSampler.InterpolateCubic
                             (
                             (
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.B.Point, sample.B.IncomingTangent,
                             sample.B.Point, sample.B.IncomingTangent,
@@ -110,7 +110,7 @@ namespace SharpGLTF.Animations
 
 
         protected override Quaternion GetTangent(Quaternion fromValue, Quaternion toValue)
         protected override Quaternion GetTangent(Quaternion fromValue, Quaternion toValue)
         {
         {
-            return SamplerFactory.CreateTangent(fromValue, toValue);
+            return CurveSampler.CreateTangent(fromValue, toValue);
         }
         }
 
 
         public override Quaternion GetPoint(float offset)
         public override Quaternion GetPoint(float offset)
@@ -126,7 +126,7 @@ namespace SharpGLTF.Animations
                     return Quaternion.Slerp(sample.A.Point, sample.B.Point, sample.Amount);
                     return Quaternion.Slerp(sample.A.Point, sample.B.Point, sample.Amount);
 
 
                 case 3:
                 case 3:
-                    return SamplerFactory.InterpolateCubic
+                    return CurveSampler.InterpolateCubic
                             (
                             (
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.B.Point, sample.B.IncomingTangent,
                             sample.B.Point, sample.B.IncomingTangent,

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

@@ -101,7 +101,7 @@ namespace SharpGLTF.Debug
 
 
         protected override Quaternion GetTangent(Quaternion a, Quaternion b)
         protected override Quaternion GetTangent(Quaternion a, Quaternion b)
         {
         {
-            return Animations.SamplerFactory.CreateTangent(a, b);
+            return Animations.CurveSampler.CreateTangent(a, b);
         }
         }
     }
     }
 
 

+ 14 - 0
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -158,4 +158,18 @@ namespace SharpGLTF.Scenes
 
 
         #endregion
         #endregion
     }
     }
+
+    [System.Diagnostics.DebuggerDisplay("Custom")]
+    partial class EmptyContent : ICloneable
+    {
+        #region lifecycle
+
+        public EmptyContent() { }
+
+        public Object Clone() { return new EmptyContent(this); }
+
+        private EmptyContent(EmptyContent other) {  }
+
+        #endregion
+    }
 }
 }

+ 6 - 0
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -286,6 +286,12 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// </summary>
         /// <param name="collection">A collection of <see cref="NodeBuilder"/> elements.</param>
         /// <param name="collection">A collection of <see cref="NodeBuilder"/> elements.</param>
         /// <param name="namePrefix">The name prefix.</param>
         /// <param name="namePrefix">The name prefix.</param>
+        /// <remarks>
+        /// This was originally intended to help in solving the problem that many engines don't
+        /// support two nodes to have the same name. But ultimately, it's these engine's responsability
+        /// to deal with glTF specifications.
+        /// </remarks>
+        [Obsolete("It does not belong here.")]
         public static void Rename(IEnumerable<NodeBuilder> collection, string namePrefix)
         public static void Rename(IEnumerable<NodeBuilder> collection, string namePrefix)
         {
         {
             if (collection == null) return;
             if (collection == null) return;

+ 12 - 0
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -227,6 +227,18 @@ namespace SharpGLTF.Scenes
             return instance;
             return instance;
         }
         }
 
 
+        public InstanceBuilder AddNode(NodeBuilder node)
+        {
+            Guard.NotNull(node, nameof(node));
+
+            var content = new EmptyContent();
+            var instance = new InstanceBuilder(this);
+            _Instances.Add(instance);
+            instance.Content = new RigidTransformer(content, node);
+            return instance;
+        }
+
+        [Obsolete("It does not belong here.")]
         public void RenameAllNodes(string namePrefix)
         public void RenameAllNodes(string namePrefix)
         {
         {
             var allNodes = Instances
             var allNodes = Instances

+ 15 - 0
src/SharpGLTF.Toolkit/Schema2/AnimationExtensions.cs

@@ -30,6 +30,7 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateScaleChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 1) animation.CreateScaleChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateScaleChannel(node, curve.ToSplineCurve());
                 if (degree == 3) animation.CreateScaleChannel(node, curve.ToSplineCurve());
             }
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Vector3>", nameof(sampler));
 
 
             return node;
             return node;
         }
         }
@@ -47,6 +48,7 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateTranslationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 1) animation.CreateTranslationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateTranslationChannel(node, curve.ToSplineCurve());
                 if (degree == 3) animation.CreateTranslationChannel(node, curve.ToSplineCurve());
             }
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Vector3>", nameof(sampler));
 
 
             return node;
             return node;
         }
         }
@@ -83,12 +85,16 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateRotationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 1) animation.CreateRotationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateRotationChannel(node, curve.ToSplineCurve());
                 if (degree == 3) animation.CreateRotationChannel(node, curve.ToSplineCurve());
             }
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Quaternion>", nameof(sampler));
 
 
             return node;
             return node;
         }
         }
 
 
         public static Node WithScaleAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         public static Node WithScaleAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
 
             return node.WithScaleAnimation(animationName, keys);
             return node.WithScaleAnimation(animationName, keys);
@@ -96,6 +102,9 @@ namespace SharpGLTF.Schema2
 
 
         public static Node WithRotationAnimation(this Node node, string animationName, params (Single Key, Quaternion Value)[] keyframes)
         public static Node WithRotationAnimation(this Node node, string animationName, params (Single Key, Quaternion Value)[] keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
 
             return node.WithRotationAnimation(animationName, keys);
             return node.WithRotationAnimation(animationName, keys);
@@ -103,6 +112,9 @@ namespace SharpGLTF.Schema2
 
 
         public static Node WithTranslationAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         public static Node WithTranslationAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         {
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
 
             return node.WithTranslationAnimation(animationName, keys);
             return node.WithTranslationAnimation(animationName, keys);
@@ -111,6 +123,7 @@ namespace SharpGLTF.Schema2
         public static Node WithScaleAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         public static Node WithScaleAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
 
             var root = node.LogicalParent;
             var root = node.LogicalParent;
 
 
@@ -124,6 +137,7 @@ namespace SharpGLTF.Schema2
         public static Node WithRotationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Quaternion> keyframes)
         public static Node WithRotationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Quaternion> keyframes)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
 
             var root = node.LogicalParent;
             var root = node.LogicalParent;
 
 
@@ -137,6 +151,7 @@ namespace SharpGLTF.Schema2
         public static Node WithTranslationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         public static Node WithTranslationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
 
             var root = node.LogicalParent;
             var root = node.LogicalParent;
 
 

+ 12 - 12
tests/SharpGLTF.Tests/Animations/AnimationSamplingTests.cs

@@ -54,22 +54,22 @@ namespace SharpGLTF
             }
             }
 
 
 
 
-            var r0 = Animations.SamplerFactory.SplitByTime(anim0).ToArray();
+            var r0 = Animations.CurveSampler.SplitByTime(anim0).ToArray();
             Assert.AreEqual(1, r0.Length);
             Assert.AreEqual(1, r0.Length);
             Assert.AreEqual(1, r0[0].Length);
             Assert.AreEqual(1, r0[0].Length);
 
 
-            var r1 = Animations.SamplerFactory.SplitByTime(anim1).ToArray();
+            var r1 = Animations.CurveSampler.SplitByTime(anim1).ToArray();
             Assert.AreEqual(1, r1.Length);
             Assert.AreEqual(1, r1.Length);
             Assert.AreEqual(2, r1[0].Length);
             Assert.AreEqual(2, r1[0].Length);
 
 
-            var r2 = Animations.SamplerFactory.SplitByTime(anim2).ToArray();
+            var r2 = Animations.CurveSampler.SplitByTime(anim2).ToArray();
             Assert.AreEqual(4, r2.Length);
             Assert.AreEqual(4, r2.Length);
             Assert.AreEqual(3, r2[0].Length); 
             Assert.AreEqual(3, r2[0].Length); 
             Assert.AreEqual(2, r2[1].Length); checkSegment(1, r2[1]);
             Assert.AreEqual(2, r2[1].Length); checkSegment(1, r2[1]);
             Assert.AreEqual(2, r2[2].Length); checkSegment(2, r2[2]);
             Assert.AreEqual(2, r2[2].Length); checkSegment(2, r2[2]);
             Assert.AreEqual(3, r2[3].Length); checkSegment(3, r2[3]);
             Assert.AreEqual(3, r2[3].Length); checkSegment(3, r2[3]);
 
 
-            var r3 = Animations.SamplerFactory.SplitByTime(anim3).ToArray();
+            var r3 = Animations.CurveSampler.SplitByTime(anim3).ToArray();
             Assert.AreEqual(6, r3.Length);
             Assert.AreEqual(6, r3.Length);
             Assert.AreEqual(1, r3[0].Length); 
             Assert.AreEqual(1, r3[0].Length); 
             Assert.AreEqual(1, r3[1].Length); 
             Assert.AreEqual(1, r3[1].Length); 
@@ -118,7 +118,7 @@ namespace SharpGLTF
 
 
             for (float amount = 0; amount <= 1; amount += 0.01f)
             for (float amount = 0; amount <= 1; amount += 0.01f)
             {
             {
-                var (startPosition, endPosition, startTangent, endTangent) = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var (startPosition, endPosition, startTangent, endTangent) = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
 
                 var p = Vector2.Zero;
                 var p = Vector2.Zero;
 
 
@@ -134,8 +134,8 @@ namespace SharpGLTF
 
 
             float k = 0.3f;
             float k = 0.3f;
 
 
-            var hb = Animations.SamplerFactory.CreateHermitePointWeights(k);
-            var ht = Animations.SamplerFactory.CreateHermiteTangentWeights(k);
+            var hb = Animations.CurveSampler.CreateHermitePointWeights(k);
+            var ht = Animations.CurveSampler.CreateHermiteTangentWeights(k);
 
 
             var pp = p1 * hb.StartPosition + p4 * hb.EndPosition + (p2 - p1) * 4 * hb.StartTangent + (p4 - p3) * 4 * hb.EndTangent;
             var pp = p1 * hb.StartPosition + p4 * hb.EndPosition + (p2 - p1) * 4 * hb.StartTangent + (p4 - p3) * 4 * hb.EndTangent;
             var pt = p1 * ht.StartPosition + p4 * ht.EndPosition + (p2 - p1) * 4 * ht.StartTangent + (p4 - p3) * 4 * ht.EndTangent;
             var pt = p1 * ht.StartPosition + p4 * ht.EndPosition + (p2 - p1) * 4 * ht.StartTangent + (p4 - p3) * 4 * ht.EndTangent;
@@ -160,7 +160,7 @@ namespace SharpGLTF
 
 
             for (float amount = 0; amount <= 1; amount += 0.1f)
             for (float amount = 0; amount <= 1; amount += 0.1f)
             {
             {
-                var (startPosition, endPosition, startTangent, endTangent) = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var (startPosition, endPosition, startTangent, endTangent) = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
 
                 var p = Vector2.Zero;
                 var p = Vector2.Zero;
 
 
@@ -195,7 +195,7 @@ namespace SharpGLTF
             var qt = Quaternion.Concatenate(q2, Quaternion.Conjugate(q1));            
             var qt = Quaternion.Concatenate(q2, Quaternion.Conjugate(q1));            
             var q2bis = Quaternion.Concatenate(qt, q1); // roundtrip; Q2 == Q2BIS
             var q2bis = Quaternion.Concatenate(qt, q1); // roundtrip; Q2 == Q2BIS
 
 
-            NumericsAssert.AreEqual(qt, Animations.SamplerFactory.CreateTangent(q1, q2), 0.000001f);
+            NumericsAssert.AreEqual(qt, Animations.CurveSampler.CreateTangent(q1, q2), 0.000001f);
 
 
             var angles = new List<Vector2>();
             var angles = new List<Vector2>();
 
 
@@ -205,7 +205,7 @@ namespace SharpGLTF
                 var sq = Quaternion.Normalize(Quaternion.Slerp(q1, q2, amount));
                 var sq = Quaternion.Normalize(Quaternion.Slerp(q1, q2, amount));
 
 
                 // hermite interpolation with a unit tangent
                 // hermite interpolation with a unit tangent
-                var hermite = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var hermite = Animations.CurveSampler.CreateHermitePointWeights(amount);
                 var hq = default(Quaternion);
                 var hq = default(Quaternion);
                 hq += q1 * hermite.StartPosition;
                 hq += q1 * hermite.StartPosition;
                 hq += q2 * hermite.EndPosition;
                 hq += q2 * hermite.EndPosition;
@@ -248,7 +248,7 @@ namespace SharpGLTF
         [Test]
         [Test]
         public void TestVector3CubicSplineSampling()
         public void TestVector3CubicSplineSampling()
         {
         {
-            var sampler = Animations.SamplerFactory.CreateSampler(_TransAnim);
+            var sampler = Animations.CurveSampler.CreateSampler(_TransAnim);
 
 
             var points = new List<Vector3>();
             var points = new List<Vector3>();
 
 
@@ -267,7 +267,7 @@ namespace SharpGLTF
         [Test]
         [Test]
         public void TestQuaternionCubicSplineSampling()
         public void TestQuaternionCubicSplineSampling()
         {
         {
-            var sampler = Animations.SamplerFactory.CreateSampler(_RotAnim);
+            var sampler = Animations.CurveSampler.CreateSampler(_RotAnim);
 
 
             var a = sampler.GetPoint(0);
             var a = sampler.GetPoint(0);
             var b = sampler.GetPoint(1);
             var b = sampler.GetPoint(1);

+ 1 - 1
tests/SharpGLTF.Tests/SharpGLTF.Core.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp3;net471</TargetFrameworks>
+    <TargetFrameworks>netcoreapp3.1;net471</TargetFrameworks>
     <IsPackable>false</IsPackable>
     <IsPackable>false</IsPackable>
     <RootNamespace>SharpGLTF</RootNamespace>
     <RootNamespace>SharpGLTF</RootNamespace>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>

+ 30 - 0
tests/SharpGLTF.Toolkit.Tests/Animation/CurveBuilderTests.cs

@@ -163,4 +163,34 @@ namespace SharpGLTF.Animations
                 .AttachToCurrentTest("plot.png");
                 .AttachToCurrentTest("plot.png");
         }
         }
     }
     }
+
+    [Category("Toolkit.Animations")]
+    public class TrackBuilderTests
+    {
+        [Test]
+        public void CreateOneKey()
+        {
+            var node = new Scenes.NodeBuilder("someNode");
+
+            var tb = node.UseTranslation().UseTrackBuilder("track1");
+
+            tb.SetPoint(0, new Vector3(1,2,3));
+
+            var scene = new Scenes.SceneBuilder();
+            scene.AddNode(node);            
+
+            var glTF = scene.ToGltf2();
+
+            var runtime = Runtime.SceneTemplate.Create(glTF.DefaultScene, true);
+            var instance = runtime.CreateInstance();
+
+            var instanceNode = instance.Armature.LogicalNodes.First(n => n.Name == "someNode");
+
+            instanceNode.SetAnimationFrame(0, 7);
+            var nodeMatrix = instanceNode.LocalMatrix;
+
+            Assert.AreEqual(new Vector3(1, 2, 3), nodeMatrix.Translation);
+        }
+
+    }
 }
 }

+ 1 - 1
tests/SharpGLTF.Toolkit.Tests/SharpGLTF.Toolkit.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp3;net471</TargetFrameworks>
+    <TargetFrameworks>netcoreapp3.1;net471</TargetFrameworks>
     <IsPackable>false</IsPackable>
     <IsPackable>false</IsPackable>
     <RootNamespace>SharpGLTF</RootNamespace>
     <RootNamespace>SharpGLTF</RootNamespace>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>