Animation/AnimationGate.cs
namespace Goo.Animation;

/// <summary>Fuses the two rebuild triggers (state <see cref="Invalidate"/> and in-motion animations) into one per-frame <see cref="Tick"/>; one extra rebuild fires on the motion-to-settled edge.</summary>
public sealed class AnimationGate
{
    readonly AnimationSet _anims = new();
    bool _dirty = true;     // mount dirty: the first Tick rebuilds
    bool _wasMoving;

    /// <summary>Register an animator tick (returns true while still in motion). Register once, where the animator is created.</summary>
    public void Add(TickFn tick) => _anims.Add(tick);

    /// <summary>Mark dirty for exactly one upcoming Tick. Call from event handlers that mutate state.</summary>
    public void Invalidate() => _dirty = true;

    /// <summary>Drop all registered animator ticks.</summary>
    public void Clear() => _anims.Clear();

    /// <summary>Ticks every registered animator; returns true if a rebuild is needed this frame.</summary>
    public bool Tick(float dt)
    {
        bool moving = _anims.UpdateAll(dt);
        bool settledEdge = _wasMoving && !moving;   // render the final settled value once
        _wasMoving = moving;
        bool needsRebuild = _dirty || moving || settledEdge;
        _dirty = false;
        return needsRebuild;
    }
}