Editor/MovieMaker/Recording/TrackRecorder.cs
using System.Collections;
using System.Collections.Immutable;
using System.Linq;
using Editor.MovieMaker;
using Sandbox.MovieMaker.Compiled;
namespace Sandbox.MovieMaker;
#nullable enable
public record RecorderOptions( int SampleRate = 30 )
{
public static RecorderOptions Default { get; } = new();
}
public interface ITrackRecorder
{
IPropertyTrack Track { get; }
ITrackProperty Target { get; }
IReadOnlyList<ICompiledPropertyBlock> FinishedBlocks { get; }
IPropertyBlock? CurrentBlock { get; }
bool Advance( MovieTime deltaTime );
IReadOnlyList<ICompiledPropertyBlock> ToBlocks();
}
public sealed class TrackRecorderCollection : IReadOnlyList<ITrackRecorder>
{
private readonly MovieClipRecorder _clipRecorder;
private readonly List<ITrackRecorder> _recorders = new();
public int Count => _recorders.Count;
public ITrackRecorder this[int index] => _recorders[index];
internal TrackRecorderCollection( MovieClipRecorder clipRecorder )
{
_clipRecorder = clipRecorder;
}
public void Add( IPropertyTrack track )
{
var property = _clipRecorder.Binder.Get( track );
var recorderType = typeof( TrackRecorder<> ).MakeGenericType( track.TargetType );
_recorders.Add( (ITrackRecorder)Activator.CreateInstance( recorderType, track, property, _clipRecorder.Options, _clipRecorder.Duration )! );
}
public IEnumerator<ITrackRecorder> GetEnumerator() => _recorders.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public sealed class MovieClipRecorder
{
public TrackBinder Binder { get; }
public RecorderOptions Options { get; }
public TrackRecorderCollection Tracks { get; }
public MovieTime Duration { get; private set; }
public MovieClipRecorder( Scene scene, RecorderOptions? options = null )
: this( new TrackBinder( scene ), options )
{
}
public MovieClipRecorder( TrackBinder binder, RecorderOptions? options = null )
{
Binder = binder;
Options = options ?? RecorderOptions.Default;
Tracks = new( this );
}
public bool Advance( MovieTime deltaTime )
{
var anySamplesWritten = false;
foreach ( var recorder in Tracks )
{
anySamplesWritten |= recorder.Advance( deltaTime );
}
Duration += deltaTime;
return anySamplesWritten;
}
public MovieClip ToClip()
{
var compiledDict = new Dictionary<ITrackTarget, ICompiledTrack>();
foreach ( var recorder in Tracks )
{
GetOrCreateCompiledTrack( compiledDict, recorder.Target );
}
return MovieClip.FromTracks( compiledDict.Values );
}
private ICompiledTrack GetOrCreateCompiledTrack( Dictionary<ITrackTarget, ICompiledTrack> dict, ITrackTarget target )
{
if ( dict.TryGetValue( target, out var compiled ) ) return compiled;
var compiledParent = target.Parent is { } parent ? GetOrCreateCompiledTrack( dict, parent ) : null;
return dict[target] = target switch
{
ITrackReference reference => CreateCompiledReferenceTrack( reference, compiledParent ),
ITrackProperty property => CreateCompiledPropertyTrack( property, compiledParent ),
_ => throw new NotImplementedException()
};
}
private ICompiledReferenceTrack CreateCompiledReferenceTrack( ITrackReference target, ICompiledTrack? compiledParent )
{
var gameObjectParent = compiledParent as CompiledReferenceTrack<GameObject>;
return target switch
{
ITrackReference<GameObject> => new CompiledReferenceTrack<GameObject>( target.Id, target.Name, gameObjectParent ),
_ => gameObjectParent?.Component( target.TargetType, target.Id ) ?? MovieClip.RootComponent( target.TargetType, target.Id )
};
}
private ICompiledPropertyTrack CreateCompiledPropertyTrack( ITrackProperty target, ICompiledTrack? compiledParent )
{
ArgumentNullException.ThrowIfNull( compiledParent, nameof(compiledParent) );
var recorder = Tracks.FirstOrDefault( x => x.Target == target );
return compiledParent.Property( target.Name, target.TargetType, recorder?.ToBlocks() );
}
}
public sealed class TrackRecorder<T> : ITrackRecorder
{
private readonly List<ICompiledPropertyBlock<T>> _blocks = new();
private readonly BlockWriter<T> _writer;
private MovieTime _elapsed;
private MovieTime _sampleTime;
private readonly MovieTime _sampleInterval;
private T _lastValue = default!;
public IPropertyTrack<T> Track { get; }
public ITrackProperty<T> Target { get; }
public IReadOnlyList<ICompiledPropertyBlock<T>> FinishedBlocks => _blocks;
public IPropertyBlock<T>? CurrentBlock => _writer.IsEmpty ? null : _writer;
public TrackRecorder( IPropertyTrack<T> track, ITrackProperty<T> target, RecorderOptions options, MovieTime startTime = default )
{
Track = track;
Target = target;
_elapsed = startTime;
_sampleInterval = MovieTime.FromFrames( 1, options.SampleRate );
_sampleTime = _elapsed.Floor( _sampleInterval );
_writer = new BlockWriter<T>( options.SampleRate );
RecordSample();
}
public bool Advance( MovieTime deltaTime )
{
_elapsed += deltaTime;
var anySamplesWritten = false;
while ( _sampleTime <= _elapsed - _sampleInterval )
{
RecordSample();
_sampleTime += _sampleInterval;
anySamplesWritten = true;
}
return anySamplesWritten;
}
private static MovieTime MinimumConstantBlockDuration => 1d;
private bool ShouldFinishBlockEarly( T nextValue )
{
return _writer is { IsEmpty: false, IsConstant: true }
&& _interpolator is null
&& _writer.TimeRange.Duration >= MinimumConstantBlockDuration
&& !_comparer.Equals( _lastValue, nextValue );
}
private void RecordSample()
{
if ( Target.IsActive )
{
var nextValue = Target.Value;
if ( ShouldFinishBlockEarly( nextValue ) )
{
FinishBlock();
}
if ( _writer.IsEmpty )
{
_writer.StartTime = _sampleTime;
}
_writer.Write( nextValue );
_lastValue = nextValue;
}
else
{
FinishBlock();
}
}
private void FinishBlock()
{
if ( _writer.IsEmpty ) return;
_blocks.Add( _writer.Compile( (_writer.StartTime, _sampleTime) ) );
_writer.Clear();
}
public ImmutableArray<ICompiledPropertyBlock<T>> ToBlocks()
{
FinishBlock();
return [.._blocks];
}
IPropertyTrack ITrackRecorder.Track => Track;
ITrackProperty ITrackRecorder.Target => Target;
IReadOnlyList<ICompiledPropertyBlock> ITrackRecorder.FinishedBlocks => FinishedBlocks;
IPropertyBlock? ITrackRecorder.CurrentBlock => CurrentBlock;
IReadOnlyList<ICompiledPropertyBlock> ITrackRecorder.ToBlocks() => ToBlocks();
private static readonly IInterpolator<T>? _interpolator = Interpolator.GetDefault<T>();
private static readonly EqualityComparer<T> _comparer = EqualityComparer<T>.Default;
}
internal class BlockWriter<T>( int sampleRate ) : IPropertyBlock<T>, IDynamicBlock, IPaintHintBlock
{
private readonly List<T> _samples = new();
private T _defaultValue = default!;
public bool IsEmpty => _samples.Count == 0;
public bool IsConstant { get; private set; }
public event Action? Changed;
public MovieTime StartTime { get; set; }
public MovieTimeRange TimeRange => (StartTime, StartTime + MovieTime.FromFrames( _samples.Count, sampleRate ));
public void Clear()
{
_samples.Clear();
}
public void Write( T value )
{
if ( _samples.Count == 0 )
{
IsConstant = true;
}
else if ( IsConstant )
{
IsConstant = _comparer.Equals( _samples[0], value );
}
_samples.Add( value );
_defaultValue = value;
Changed?.Invoke();
}
public ICompiledPropertyBlock<T> Compile( MovieTimeRange timeRange )
{
if ( IsEmpty ) throw new InvalidOperationException( "Block is empty!" );
if ( IsConstant ) return new CompiledConstantBlock<T>( timeRange, _samples[0] );
return new CompiledSampleBlock<T>( timeRange, 0d, sampleRate, [.._samples] );
}
public IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange ) => [TimeRange];
public T GetValue( MovieTime time )
{
return _samples.Count != 0
? _samples.Sample( time - StartTime, sampleRate, _interpolator )
: _defaultValue;
}
private static readonly IInterpolator<T>? _interpolator = Interpolator.GetDefault<T>();
private static readonly EqualityComparer<T> _comparer = EqualityComparer<T>.Default;
}