using System;
namespace IFSRenderer.AvaloniaShell;
/// <summary>
/// The window of time the animation panes are showing, shared by the dopesheet and the curve
/// editor so a keyframe sits at the same x in both.
/// </summary>
/// <remarks>
/// <para>
/// The panes used to map the WHOLE clip onto their width, which is why neither could zoom: the
/// mapping had nowhere to put a magnified span. This holds the visible window instead -
/// <see cref="Start"/> plus <see cref="Span"/> seconds - and both panes project through it.
/// </para>
/// <para>
/// The canonical WPF panel does this with a <c>ViewScale</c> in px-per-second and a
/// <c>ScrollViewer</c> around a widened canvas (AnimationPanel.xaml:131, ViewModels/
/// AnimationViewModel.cs:61). A visible window is the same idea without needing the canvas to
/// grow, and it keeps both panes in register by construction rather than by scroll-syncing two
/// viewers.
/// </para>
/// </remarks>
internal sealed class TimeView
{
public const double LeftPad = 8;
public const double RightPad = 14;
/// <summary>Never zoom in past this fraction of the clip - the panes stay navigable.</summary>
private const double MinSpanFraction = 1.0 / 400;
private double _length = 10;
private double _start;
private double _span = 10;
/// <summary>Raised when the window moved or resized, so the panes repaint.</summary>
public event Action Changed;
public double Start => _start;
public double Span => _span;
public double End => _start + _span;
/// <summary>Whether the whole clip is visible, i.e. there is nothing to scroll.</summary>
public bool IsFullyZoomedOut => _span >= _length - 1e-9;
public double Length
{
get => _length;
set
{
double length = Math.Max(value, 0.001);
if (Math.Abs(length - _length) < 1e-9)
return;
_length = length;
Clamp();
Changed?.Invoke();
}
}
public static double TrackWidth(double width) => Math.Max(width - LeftPad - RightPad, 1);
public double TimeToX(double t, double width) =>
LeftPad + (t - _start) / _span * TrackWidth(width);
public double XToTime(double x, double width) =>
Math.Clamp(_start + (x - LeftPad) / TrackWidth(width) * _span, 0, _length);
/// <summary>Shows the whole clip.</summary>
public void Reset()
{
_start = 0;
_span = _length;
Changed?.Invoke();
}
/// <summary>
/// Zooms about a fixed point in time, so whatever is under the cursor stays under it.
/// </summary>
public void ZoomAt(double anchor, double factor)
{
double span = Math.Clamp(_span * factor, _length * MinSpanFraction, _length);
if (Math.Abs(span - _span) < 1e-12)
return;
// Keep `anchor` at the same fraction across the track.
double fraction = _span <= 0 ? 0.5 : (anchor - _start) / _span;
_start = anchor - fraction * span;
_span = span;
Clamp();
Changed?.Invoke();
}
public void PanBy(double seconds)
{
if (seconds == 0)
return;
_start += seconds;
Clamp();
Changed?.Invoke();
}
public void ScrollTo(double start)
{
double clamped = Math.Clamp(start, 0, Math.Max(_length - _span, 0));
if (Math.Abs(clamped - _start) < 1e-12)
return;
_start = clamped;
Changed?.Invoke();
}
/// <summary>Scrolls the least amount that brings a time into view, for playback.</summary>
public void EnsureVisible(double t)
{
if (t < _start)
ScrollTo(t);
else if (t > End)
ScrollTo(t - _span);
}
private void Clamp()
{
_span = Math.Clamp(_span, _length * MinSpanFraction, _length);
_start = Math.Clamp(_start, 0, Math.Max(_length - _span, 0));
}
}