using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using IFSEngine.Animation;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// The curve editor crashed the app after dragging a keyframe for a second or two.
/// </summary>
/// <remarks>
/// <para>
/// <c>System.OverflowException: Value was either too large or too small for a Decimal</c>, from
/// <c>AnimationPanel.RefreshKeyframeFields</c> on pointer release. The cast was only where it
/// surfaced. The cause was a feedback loop between the drag and the auto-fitted value axis:
/// </para>
/// <para>
/// auto-fit widens the axis to include the dragged key AND pads it by 15% -> the unchanged
/// pointer position now maps through a wider axis to a larger value -> which widens the axis
/// again. About 1.3x per frame, so a held drag runs past decimal's ~7.9e28 ceiling in seconds.
/// </para>
/// <para>
/// The engine was cleared first: <c>Channel.EvaluateAt</c> stayed finite on coincident keys,
/// one-ULP-apart keys, all-keys-on-one-time and 1e300 values, so nothing here is a workaround
/// for an engine defect.
/// </para>
/// </remarks>
public class CurveDragStabilityTests
{
private static (CurveCanvas Curve, Channel Channel) Laid()
{
var sheet = new Dopesheet { Length = TimeSpan.FromSeconds(10), Fps = 30 };
var channel = new Channel { Name = "Brightness" };
channel.Keyframes.Add(new Keyframe { t = 0, Value = -5 });
channel.Keyframes.Add(new Keyframe { t = 10, Value = 5 });
sheet.Channels["Brightness"] = channel;
var curve = new CurveCanvas { Width = 800, Height = 400 };
var window = new Window { Width = 820, Height = 420, Content = curve };
window.Show();
curve.SetDopesheet(sheet);
curve.SetChannel("Brightness");
window.Measure(new Size(820, 420));
window.Arrange(new Rect(0, 0, 820, 420));
return (curve, channel);
}
/// <summary>Holding a drag still must not move the value it is reporting.</summary>
[AvaloniaFact]
public void AHeldDragDoesNotRunAway()
{
(CurveCanvas curve, Channel channel) = Laid();
Keyframe key = channel.Keyframes[1];
curve.ApplyAutoFit();
var at = new Point(curve.TimeToX(10), curve.ValueToY(2));
curve.BeginDrag(key, at);
double first = double.NaN;
for (int frame = 0; frame < 200; frame++)
{
curve.ApplyAutoFit(); // what Render does every frame
curve.DragTo(at, KeyModifiers.None);
if (frame == 0)
first = key.Value;
}
Assert.Equal(first, key.Value, 6);
Assert.True(Math.Abs(key.Value) < 1e6,
$"value ran away to {key.Value:0.###e+00} while the pointer never moved");
}
/// <summary>Whatever a drag produces, the panel must be able to display it.</summary>
[AvaloniaFact]
public void ADraggedValueAlwaysSurvivesTheDecimalConversion()
{
(CurveCanvas curve, Channel channel) = Laid();
Keyframe key = channel.Keyframes[1];
curve.ApplyAutoFit();
curve.BeginDrag(key, new Point(curve.TimeToX(10), curve.ValueToY(5)));
// Includes positions far outside the control: a captured pointer is not clamped to it.
foreach (double y in new[] { -1e6, -5000.0, -1.0, 0.0, 200.0, 400.0, 5000.0, 1e6 })
{
curve.ApplyAutoFit();
curve.DragTo(new Point(curve.TimeToX(5), y), KeyModifiers.None);
Exception thrown = Record.Exception(() =>
AnimationPanel.ToDecimal(key.Value, -1_000_000_000m, 1_000_000_000m, 0m));
Assert.Null(thrown);
}
}
[Theory]
[InlineData(double.NaN, 0)]
[InlineData(double.PositiveInfinity, 100)]
[InlineData(double.NegativeInfinity, -100)]
[InlineData(1e300, 100)]
[InlineData(-1e300, -100)]
[InlineData(2.5, 2.5)]
public void OutOfRangeDoublesAreClampedRatherThanThrown(double value, double expected) =>
Assert.Equal((decimal)expected, AnimationPanel.ToDecimal(value, -100m, 100m, 0m));
/// <summary>A poisoned sample must not take the whole axis with it.</summary>
[Fact]
public void NonFiniteSamplesDoNotPoisonTheFittedRange()
{
var channel = new Channel { Name = "n" };
channel.Keyframes.Add(new Keyframe { t = 0, Value = 1 });
channel.Keyframes.Add(new Keyframe { t = 5, Value = double.NaN });
channel.Keyframes.Add(new Keyframe { t = 10, Value = 3 });
(double min, double max) = CurveCanvas.FitRange(channel, 10);
Assert.False(double.IsNaN(min) || double.IsNaN(max), "NaN propagated into the axis");
Assert.True(min < max);
}
/// <summary>Auto-fit still works the moment the drag ends.</summary>
[AvaloniaFact]
public void ReleasingTheDragLetsTheAxisFitAgain()
{
(CurveCanvas curve, Channel channel) = Laid();
Keyframe key = channel.Keyframes[1];
curve.ApplyAutoFit();
curve.BeginDrag(key, new Point(curve.TimeToX(10), curve.ValueToY(5)));
curve.DragTo(new Point(curve.TimeToX(10), curve.ValueToY(40)), KeyModifiers.None);
curve.EndDrag();
curve.ApplyAutoFit();
// The key is now at 40, so it has to be inside the plot rather than off the top.
double y = curve.ValueToY(key.Value);
Assert.InRange(y, 0, curve.Bounds.Height);
}
}