117 lines
4.4 KiB
C#
117 lines
4.4 KiB
C#
namespace LindtLeerformPlugin.Services;
|
||
|
||
/// <summary>
|
||
/// Monotone cubic Hermite spline (Fritsch–Carlson). Five control points with
|
||
/// fixed, evenly spaced X positions; only Y is editable. Output never overshoots
|
||
/// the input range, so envelope curves stay inside [0, 1] when knots are.
|
||
/// </summary>
|
||
public static class SplineCurve
|
||
{
|
||
public const int KnotCount = 5;
|
||
|
||
/// <summary>
|
||
/// Evaluate the spline at parameter <paramref name="t"/> in [0, 1].
|
||
/// Knots are positioned at t = i / (knots.Length - 1).
|
||
/// </summary>
|
||
public static float Evaluate(float[]? knots, double t)
|
||
{
|
||
if (knots == null || knots.Length == 0)
|
||
return 0f;
|
||
if (knots.Length == 1)
|
||
return knots[0];
|
||
|
||
if (t <= 0) return knots[0];
|
||
if (t >= 1) return knots[^1];
|
||
|
||
var n = knots.Length - 1;
|
||
var pos = t * n;
|
||
var i = (int)System.Math.Floor(pos);
|
||
if (i >= n) return knots[n];
|
||
var localT = pos - i;
|
||
|
||
// Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only.
|
||
var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0;
|
||
var dxMid = (double)(knots[i + 1] - knots[i]);
|
||
var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0;
|
||
|
||
var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true);
|
||
var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n);
|
||
|
||
var t2 = localT * localT;
|
||
var t3 = t2 * localT;
|
||
var h00 = 2 * t3 - 3 * t2 + 1;
|
||
var h10 = t3 - 2 * t2 + localT;
|
||
var h01 = -2 * t3 + 3 * t2;
|
||
var h11 = t3 - t2;
|
||
|
||
var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1;
|
||
return (float)y;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Convenience: evaluate the spline at the position of bin <paramref name="binIndex"/>
|
||
/// of a histogram with <paramref name="totalBins"/> bins.
|
||
/// </summary>
|
||
public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins)
|
||
{
|
||
if (totalBins <= 1)
|
||
return Evaluate(knots, 0);
|
||
var t = (double)binIndex / (totalBins - 1);
|
||
return Evaluate(knots, t);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Build a default envelope spline from a reference histogram. For each knot
|
||
/// the segment max around the knot position is taken, multiplied by 1.2 with a
|
||
/// small floor added, and clamped to [0, 1].
|
||
/// </summary>
|
||
public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount)
|
||
{
|
||
var knots = new float[knotCount];
|
||
if (referenceHistogram == null || referenceHistogram.Length == 0)
|
||
{
|
||
for (var k = 0; k < knotCount; k++) knots[k] = 0.05f;
|
||
return knots;
|
||
}
|
||
|
||
var totalBins = referenceHistogram.Length;
|
||
var span = totalBins - 1;
|
||
// Half-width of the window we look at around each knot.
|
||
var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1)));
|
||
|
||
for (var k = 0; k < knotCount; k++)
|
||
{
|
||
var center = knotCount == 1 ? 0 : k * span / (knotCount - 1);
|
||
var lo = System.Math.Max(0, center - segHalf);
|
||
var hi = System.Math.Min(totalBins - 1, center + segHalf);
|
||
var max = 0f;
|
||
for (var i = lo; i <= hi; i++)
|
||
if (referenceHistogram[i] > max) max = referenceHistogram[i];
|
||
|
||
var y = max * 1.2f + 0.01f;
|
||
if (y < 0f) y = 0f;
|
||
if (y > 1f) y = 1f;
|
||
knots[k] = y;
|
||
}
|
||
return knots;
|
||
}
|
||
|
||
private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight)
|
||
{
|
||
if (!hasLeft) return secRight;
|
||
if (!hasRight) return secLeft;
|
||
// If the signs differ (or either is zero), the knot is an extremum: tangent must be zero
|
||
// to preserve monotonicity locally.
|
||
if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight))
|
||
return 0;
|
||
// Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our
|
||
// small (5 knot) curves the simple average plus zero-at-extrema rule already prevents
|
||
// overshoot in practice.
|
||
var avg = 0.5 * (secLeft + secRight);
|
||
var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight));
|
||
if (System.Math.Abs(avg) > limit)
|
||
avg = System.Math.Sign(avg) * limit;
|
||
return avg;
|
||
}
|
||
}
|