Files
sbox-public/game/editor/MovieMaker/Code/Modifications/Blend.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

199 lines
6.3 KiB
C#

using System.Collections.Immutable;
using System.Linq;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
public class BlendModification() : PerTrackModification<BlendOptions>( BlendOptions.Default, false )
{
public override MovieTimeRange? SourceTimeRange => Options.SourceRange is { } sourceRange
? sourceRange + Options.Offset
: null;
protected override void OnInitialize( MotionEditMode editMode )
{
Options = Options with { IsAdditive = editMode.DefaultIsAdditive };
}
public void SetFromClipboard( ClipboardData clipboard, MovieTime offset, MovieProject project )
{
Options = Options with
{
SourceRange = clipboard.Selection.TotalTimeRange,
Offset = offset
};
foreach ( var (id, blocks) in clipboard.Tracks )
{
if ( blocks.Count == 0 ) continue;
if ( project.GetTrack( id ) is not IProjectPropertyTrack track ) continue;
var state = GetOrCreateTrackModificationPreview( track );
state.Modification = blocks.AsModification();
}
}
public override void AddControls( ToolBarGroup group )
{
var additiveDisplay = new ToolBarItemDisplay( "Additive", "layers",
"When enabled, changes will be additively blended to existing track contents instead of overwriting." );
group.AddToggle( additiveDisplay,
() => Options.IsAdditive,
state => Options = Options with { IsAdditive = EditMode.DefaultIsAdditive = state } );
}
public bool PreChange( IProjectPropertyTrack track, ITrackProperty property )
{
if ( GetTrackModificationPreview( track ) is not null )
{
return false;
}
var preview = GetOrCreateTrackModificationPreview( track );
// We create modifications in PreChange so we can capture the pre-change value,
// used for additive blending
preview.Modification = property.Value.AsSignal( property.TargetType ).AsModification();
return true;
}
public bool PostChange( IProjectPropertyTrack track, ITrackProperty property )
{
if ( GetTrackModificationPreview( track ) is not { Modification: ISignalBlendModification blend } preview )
{
return false;
}
preview.Modification = blend.WithSignal( property.Value.AsSignal( property.TargetType ) );
return true;
}
}
public record ClipboardData( TimeSelection Selection, IReadOnlyDictionary<Guid, IReadOnlyList<IProjectPropertyBlock>> Tracks );
public record BlendOptions( bool IsAdditive, MovieTime Offset, MovieTimeRange? SourceRange ) : ITranslatableOptions
{
public static BlendOptions Default { get; } = new( true, default, null );
public ITranslatableOptions WithOffset( MovieTime offset ) => this with { Offset = offset };
}
public abstract class BlendTrackModification<T> : ITrackModification<T, BlendOptions>
{
protected PropertyBlock<T> Blend( PropertySignal<T>? original, PropertySignal<T>? overlay,
PropertySignal<T> relativeTo, MovieTimeRange timeRange, TimeSelection selection, BlendOptions options )
{
if ( original is null && overlay is null )
{
throw new ArgumentNullException( nameof( overlay ), "Expected at least one signal." );
}
if ( overlay is not null )
{
overlay += options.Offset;
}
if ( original is null || overlay is null )
{
return new PropertyBlock<T>( (original ?? overlay)!.Reduce( timeRange ), timeRange );
}
if ( options.IsAdditive )
{
overlay = original + (overlay - relativeTo);
}
return new PropertyBlock<T>( original.CrossFade( overlay, selection ).Reduce( timeRange ), timeRange );
}
public abstract IEnumerable<PropertyBlock<T>> Apply( IReadOnlyList<PropertyBlock<T>> original, TimeSelection selection, BlendOptions options );
}
public interface ISignalBlendModification : ITrackModification
{
ISignalBlendModification WithSignal( IPropertySignal signal );
}
public sealed class SignalBlendModification<T>( PropertySignal<T> signal, PropertySignal<T> relativeTo ) : BlendTrackModification<T>, ISignalBlendModification
{
public override IEnumerable<PropertyBlock<T>> Apply( IReadOnlyList<PropertyBlock<T>> original, TimeSelection selection, BlendOptions options )
{
var timeRange = selection.TotalTimeRange;
// Fill in gaps between blocks in original track with AsSignal()
if ( original.AsSignal() is not { } originalSignal )
{
yield return new PropertyBlock<T>( signal, timeRange );
yield break;
}
yield return Blend( originalSignal, signal, relativeTo, timeRange, selection, options );
}
public ISignalBlendModification WithSignal( IPropertySignal newSignal ) =>
new SignalBlendModification<T>( (PropertySignal<T>)newSignal, relativeTo );
}
public sealed class ClipboardBlendModification<T>( ImmutableArray<PropertyBlock<T>> sourceBlocks ) : BlendTrackModification<T>
{
public ClipboardBlendModification( IEnumerable<IProjectPropertyBlock> blocks )
: this( [..blocks.Cast<PropertyBlock<T>>()] )
{
}
public override IEnumerable<PropertyBlock<T>> Apply( IReadOnlyList<PropertyBlock<T>> original, TimeSelection selection, BlendOptions options )
{
var blocks = sourceBlocks;
var timeRanges = original.Select( x => x.TimeRange )
.Union( blocks.Select( x => x.TimeRange + options.Offset ) );
PropertySignal<T> relativeTo = blocks[0].GetValue( blocks[0].TimeRange.Start );
foreach ( var timeRange in timeRanges )
{
var originalSignal = original
.Where( x => timeRange.Contains( x.TimeRange ) )
.AsSignal();
var overlaySignal = blocks
.Where( x => timeRange.Contains( x.TimeRange + options.Offset ) )
.AsSignal();
var clamped = timeRange.Clamp( selection.TotalTimeRange );
yield return Blend( originalSignal, overlaySignal, relativeTo, clamped, selection, options );
}
}
}
public static class TrackModificationExtensions
{
public static ITrackModification AsModification( this IPropertySignal signal )
{
var modificationType = typeof( SignalBlendModification<> ).MakeGenericType( signal.PropertyType );
return (ITrackModification)Activator.CreateInstance( modificationType, signal, signal )!;
}
public static ITrackModification AsModification( this IEnumerable<IProjectPropertyBlock> sourceBlocks )
{
var untypedArray = sourceBlocks.ToArray();
if ( untypedArray.Length == 0 ) throw new ArgumentException( "Expected at least one block.", nameof( sourceBlocks ) );
var propertyType = untypedArray[0].PropertyType;
var modificationType = typeof( ClipboardBlendModification<> ).MakeGenericType( propertyType );
return (ITrackModification)Activator.CreateInstance( modificationType, [untypedArray] )!;
}
}