Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
d49ffeb42This file is pinned to the selected release. Download the original to open it in your design tool.
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>
where T : struct
{
#region lifecycle
protected CurveBuilder() { }
protected CurveBuilder(CurveBuilder<T> other)
{
if (other == null) return;
foreach (var kvp in other._Keys)
{
this._Keys[kvp.Key] = kvp.Value;
}
}
public abstract CurveBuilder<T> Clone();
#endregion
#region data
[System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
private SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
#endregion
#region properties
internal IReadOnlyDictionary<float, _CurveNode<T>> _DebugKeys => _Keys;
[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
/// <summary>
/// Creates a <typeparamref name="T"/> instance from an <see cref="Single"/>[] array.
/// </summary>
/// <param name="values">An array of floats.</param>
/// <returns>A <typeparamref name="T"/> instance.</returns>
protected abstract T CreateValue(params float[] values);
/// <summary>
/// Samples the curve at a given <paramref name="offset"/>
/// </summary>
/// <param name="offset">The curve offset to sample.</param>
/// <returns>A curve <typeparamref name="T"/> point.</returns>
public abstract T GetPoint(float offset);
protected abstract T GetTangent(T fromValue, T toValue);
#endregion
#region API
public void Clear() { _Keys.Clear(); }
public void RemoveKey(float offset) { _Keys.Remove(offset); }
public void SetPoint(float offset, T value, bool isLinear = true)
{
if (_Keys.TryGetValue(offset, out _CurveNode<T> existing))
{
existing.Point = value;
}
else
{
existing = new _CurveNode<T>(value, isLinear);
}
_Keys[offset] = existing;
}
/// <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));
offset -= float.Epsilon;
var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
var a = _Keys[keyA];
var b = _Keys[keyB];
if (a.Degree == 1) a.OutgoingTangent = GetTangent(a.Point, b.Point);
a.Degree = 3;
b.IncomingTangent = tangent;
_Keys[keyA] = a;
_Keys[keyB] = 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));
var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
var a = _Keys[keyA];
var b = _Keys[keyB];
if (keyA != keyB)
{
if (a.Degree == 1) b.IncomingTangent = GetTangent(a.Point, b.Point);
_Keys[keyB] = b;
}
a.Degree = 3;
a.OutgoingTangent = tangent;
_Keys[keyA] = a;
}
private protected (_CurveNode<T> A, _CurveNode<T> B, float Amount) FindSample(float offset)
{
if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
var (keyA, keyB, amount) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
return (_Keys[keyA], _Keys[keyB], amount);
}
public void SetCurve(ICurveSampler<T> curve)
{
if (curve is IConvertibleCurve<T> convertible)
{
if (convertible.MaxDegree == 0)
{
var linear = convertible.ToStepCurve();
foreach (var p in linear) this.SetPoint(p.Key, p.Value, false);
return;
}
if (convertible.MaxDegree == 1)
{
var linear = convertible.ToLinearCurve();
foreach (var p in linear) this.SetPoint(p.Key, p.Value);
return;
}
if (convertible.MaxDegree == 3)
{
var spline = convertible.ToSplineCurve();
foreach (var ppp in spline)
{
this.SetPoint(ppp.Key, ppp.Value.Item2);
this.SetIncomingTangent(ppp.Key, ppp.Value.Item1);
this.SetOutgoingTangent(ppp.Key, ppp.Value.Item3);
}
return;
}
}
throw new NotImplementedException();
}
#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>();
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();
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 TangentIn, T Value, T TangentOut)> 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>
where T : struct
{
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;
}
}