Code/Compiled/Track.cs
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace Sandbox.MovieMaker.Compiled;
#nullable enable
/// <inheritdoc cref="ITrack"/>
public interface ICompiledTrack : ITrack
{
new ICompiledTrack? Parent { get; }
}
/// <inheritdoc cref="IReferenceTrack"/>
public interface ICompiledReferenceTrack : ICompiledTrack, IReferenceTrack
{
new CompiledReferenceTrack<GameObject>? Parent { get; }
ICompiledTrack? ICompiledTrack.Parent => Parent;
IReferenceTrack<GameObject>? IReferenceTrack.Parent => Parent;
}
public interface ICompiledBlockTrack : ICompiledTrack
{
IReadOnlyList<ICompiledBlock> Blocks { get; }
public MovieTimeRange TimeRange => Blocks.Count == 0 ? default : (Blocks[0].TimeRange.Start, Blocks[^1].TimeRange.End);
}
public sealed record CompiledReferenceTrack<T>(
Guid Id,
string Name,
CompiledReferenceTrack<GameObject>? Parent = null,
Guid? ReferenceId = null )
: ICompiledReferenceTrack, IReferenceTrack<T> where T : class, IValid;
/// <inheritdoc cref="IActionTrack"/>
public sealed record CompiledActionTrack(
string Name,
Type TargetType,
ICompiledTrack Parent,
ImmutableArray<CompiledActionBlock> Blocks )
: IActionTrack, ICompiledBlockTrack
{
ITrack IActionTrack.Parent => Parent;
IReadOnlyList<ICompiledBlock> ICompiledBlockTrack.Blocks => Blocks;
}
/// <inheritdoc cref="IPropertyTrack"/>
public interface ICompiledPropertyTrack : IPropertyTrack, ICompiledBlockTrack
{
new ICompiledTrack Parent { get; }
new IReadOnlyList<ICompiledPropertyBlock> Blocks { get; }
ICompiledPropertyBlock? GetBlock( MovieTime time );
ITrack IPropertyTrack.Parent => Parent;
IReadOnlyList<ICompiledBlock> ICompiledBlockTrack.Blocks => Blocks;
}
/// <inheritdoc cref="IPropertyTrack{T}"/>
[method: JsonConstructor]
public sealed record CompiledPropertyTrack<T>(
string Name,
ICompiledTrack Parent,
ImmutableArray<ICompiledPropertyBlock<T>> Blocks )
: ICompiledPropertyTrack, IPropertyTrack<T>
{
public CompiledPropertyTrack( string name, ICompiledTrack parent, IEnumerable<ICompiledPropertyBlock>? blocks )
: this( name, parent, blocks?.Cast<ICompiledPropertyBlock<T>>().ToImmutableArray() ?? ImmutableArray<ICompiledPropertyBlock<T>>.Empty )
{
}
private readonly ImmutableArray<ICompiledPropertyBlock<T>> _blocks = Validate( Blocks );
public ImmutableArray<ICompiledPropertyBlock<T>> Blocks
{
get => _blocks;
init => _blocks = Validate( value );
}
public ICompiledPropertyBlock<T>? GetBlock( MovieTime time )
{
// TODO: binary search?
// We go backwards because if we're exactly on a block boundary, we want to use the later block
for ( var i = Blocks.Length - 1; i >= 0; --i )
{
var block = Blocks[i];
if ( block.TimeRange.Start > time ) continue;
if ( block.TimeRange.End < time ) break;
return block;
}
return default;
}
public bool TryGetValue( MovieTime time, [MaybeNullWhen( false )] out T value )
{
if ( GetBlock( time ) is { } block )
{
value = block.GetValue( time );
return true;
}
value = default;
return false;
}
IReadOnlyList<ICompiledPropertyBlock> ICompiledPropertyTrack.Blocks => Blocks;
ICompiledPropertyBlock? ICompiledPropertyTrack.GetBlock( MovieTime time ) => GetBlock( time );
private static ImmutableArray<ICompiledPropertyBlock<T>> Validate( ImmutableArray<ICompiledPropertyBlock<T>> blocks )
{
if ( blocks.IsDefault )
{
throw new ArgumentException( "Blocks must be an array.", nameof(Blocks) );
}
if ( blocks.Length == 0 ) return blocks;
var prevTime = blocks[0].TimeRange.Start;
if ( prevTime < 0d )
{
throw new ArgumentException( "Blocks must have non-negative start times.", nameof(Blocks) );
}
foreach ( var block in blocks )
{
if ( block.TimeRange.Start < prevTime )
{
throw new ArgumentException( "Blocks must not overlap.", nameof(Blocks) );
}
prevTime = block.TimeRange.End;
}
return blocks;
}
}