Editor/MovieMaker/Project/PropertyBlock.cs
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
using Sandbox.MovieMaker;
using Sandbox.MovieMaker.Compiled;
namespace Editor.MovieMaker;
#nullable enable
/// <summary>
/// A <see cref="ITrackBlock"/> that has hints for UI painting.
/// </summary>
public interface IPaintHintBlock : ITrackBlock
{
/// <summary>
/// Gets time regions, within <paramref name="timeRange"/>, that have constantly changing values.
/// </summary>
IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange );
}
/// <summary>
/// A <see cref="ITrackBlock"/> that can change dynamically, usually for previewing edits / live recordings.
/// </summary>
public interface IDynamicBlock : ITrackBlock
{
event Action? Changed;
}
/// <summary>
/// A <see cref="IPropertyBlock"/> that can be added to a <see cref="IProjectPropertyTrack"/>.
/// </summary>
public interface IProjectPropertyBlock : IPropertyBlock, IPaintHintBlock
{
IProjectPropertyBlock? Slice( MovieTimeRange timeRange );
IProjectPropertyBlock Shift( MovieTime offset );
IProjectPropertyBlock WithSignal( PropertySignal signal );
PropertySignal Signal { get; }
}
public static class PropertyBlock
{
public static IProjectPropertyBlock FromSignal( PropertySignal signal, MovieTimeRange timeRange )
{
var propertyType = signal.PropertyType;
var blockType = typeof(PropertyBlock<>).MakeGenericType( propertyType );
return (IProjectPropertyBlock)Activator.CreateInstance( blockType, signal, timeRange )!;
}
}
public sealed partial record PropertyBlock<T>( [property: JsonPropertyOrder( 100 )] PropertySignal<T> Signal, MovieTimeRange TimeRange )
: IPropertyBlock<T>, IProjectPropertyBlock
{
public T GetValue( MovieTime time ) => Signal.GetValue( time.Clamp( TimeRange ) );
public IEnumerable<MovieTimeRange> GetPaintHints( MovieTimeRange timeRange ) =>
Signal.GetPaintHints( timeRange.Clamp( TimeRange ) );
public PropertyBlock<T>? Slice( MovieTimeRange timeRange )
{
if ( timeRange == TimeRange ) return this;
if ( timeRange.Intersect( TimeRange ) is not { } intersection )
{
return null;
}
return new PropertyBlock<T>( Signal.Reduce( intersection ), intersection );
}
IProjectPropertyBlock? IProjectPropertyBlock.Slice( MovieTimeRange timeRange ) => Slice( timeRange );
IProjectPropertyBlock IProjectPropertyBlock.Shift( MovieTime offset ) => new MovieTransform( offset ) * this;
public IProjectPropertyBlock WithSignal( PropertySignal signal ) => this with { Signal = (PropertySignal<T>)signal };
PropertySignal IProjectPropertyBlock.Signal => Signal;
public ICompiledPropertyBlock<T> Compile( ProjectPropertyTrack<T> track )
{
var sampleRate = track.Project.SampleRate;
var samples = Signal.Sample( TimeRange, sampleRate );
var comparer = EqualityComparer<T>.Default;
if ( samples.All( x => comparer.Equals( x, samples[0] ) ) )
{
return new CompiledConstantBlock<T>( TimeRange, samples[0] );
}
return new CompiledSampleBlock<T>( TimeRange, 0d, sampleRate, [..samples] );
}
}