Files
sbox-public/game/editor/MovieMaker/Code/Modes/Motion/TimeSelectionItem.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

522 lines
14 KiB
C#

using Sandbox;
using Sandbox.MovieMaker;
using System.Linq;
namespace Editor.MovieMaker;
#nullable enable
partial class MotionEditMode
{
private abstract class TimeSelectionItem : GraphicsItem, IMovieDraggable, IMovieContextMenu
{
/// <summary>
/// Capture time selection before being dragged so we can revert etc.
/// </summary>
protected TimeSelection? OriginalSelection { get; private set; }
protected IModificationOptions? OriginalModificationOptions { get; private set; }
public MotionEditMode EditMode { get; }
protected TimeSelectionItem( MotionEditMode editMode )
{
EditMode = editMode;
HoverEvents = true;
Focusable = true;
Selectable = true;
}
public abstract void UpdatePosition( TimeSelection value, Rect viewRect );
public override bool Contains( Vector2 localPos )
{
return BoundingRect.Shrink( 0f, 24f, 0f, 24f ).IsInside( localPos );
}
public abstract MovieTimeRange TimeRange { get; }
public virtual (MovieTime? Min, MovieTime? Max) DragLimits => (-TimeRange.Start, null);
public virtual CursorShape DragCursor => CursorShape.Finger;
public virtual IEnumerable<MovieTime> SnapSources => [TimeRange.Start, TimeRange.End];
void IMovieDraggable.StartDrag()
{
OriginalSelection = EditMode.TimeSelection;
OriginalModificationOptions = EditMode.Modification?.Options;
OnStartDrag();
}
protected virtual void OnStartDrag() { }
void IMovieDraggable.Drag( MovieTime delta ) => OnDrag( delta );
protected virtual void OnDrag( MovieTime delta ) { }
void IMovieDraggable.EndDrag()
{
try
{
OnEndDrag();
}
finally
{
OriginalSelection = null;
OriginalModificationOptions = null;
}
}
protected virtual void OnEndDrag() { }
bool IMovieDraggable.SnapFilter( ISnapSource source ) => OnSnapFilter( source );
protected virtual bool OnSnapFilter( ISnapSource source ) => source is not TimeSelectionItem;
public void ShowContextMenu( ContextMenuEvent ev )
{
if ( EditMode.TimeSelection is not { } selection )
{
return;
}
ev.Title = selection.TotalTimeRange.ToString();
ev.Accepted = true;
EditMode.AddTimeSelectionContextMenu( ev, selection );
OnShowContextMenu( ev );
EditMode.AddClipboardContextMenu( ev, selection.TotalTimeRange );
}
protected virtual void OnShowContextMenu( ContextMenuEvent ev )
{
}
}
/// <summary>
/// Inner region of the timeline selection. Dragging it moves the whole selection left / right.
/// </summary>
private sealed class TimeSelectionPeakItem : TimeSelectionItem
{
public override MovieTimeRange TimeRange => EditMode.TimeSelection?.PeakTimeRange ?? default;
public override (MovieTime? Min, MovieTime? Max) DragLimits => (-EditMode.TimeSelection?.TotalStart, null);
public override IEnumerable<MovieTime> SnapSources
{
get
{
if ( EditMode.TimeSelection is not { } selection ) return [];
return [selection.TotalStart, selection.PeakStart, selection.PeakEnd, selection.TotalEnd];
}
}
public TimeSelectionPeakItem( MotionEditMode editMode )
: base( editMode )
{
ZIndex = 10000;
Cursor = CursorShape.Finger;
}
protected override void OnDrag( MovieTime delta )
{
if ( EditMode.TimeSelection is not { } current ) return;
if ( EditMode.Modification?.Options is ITranslatableOptions translatable )
{
EditMode.Modification.Options = translatable.WithOffset( translatable.Offset + delta );
}
EditMode.TimeSelection = current + delta;
}
public override void UpdatePosition( TimeSelection value, Rect viewRect )
{
PrepareGeometryChange();
var timeRange = value.PeakTimeRange;
Position = new Vector2( EditMode.Session.TimeToPixels( timeRange.Start ), viewRect.Top );
Size = new Vector2( EditMode.Session.TimeToPixels( timeRange.Duration ), viewRect.Height );
Update();
}
protected override void OnPaint()
{
var color = EditMode.SelectionColor;
if ( Hovered ) color = color.Lighten( 0.1f );
Paint.Antialiasing = true;
Paint.SetBrush( color );
Paint.SetPen( Color.White.WithAlpha( 0.5f ), 0.5f );
Paint.DrawRect( LocalRect.Grow( 0f, 16f ) );
if ( EditMode.LastActionIcon is { } icon && EditMode._lastActionTime < 1f )
{
var t = 1f - EditMode._lastActionTime;
Paint.SetPen( Color.White.WithAlpha( t * t * t * t ) );
Paint.DrawIcon( LocalRect.Grow( 32f, 0f ), icon, 32f );
}
}
}
private enum FadeKind
{
FadeIn,
FadeOut
}
/// <summary>
/// Fade in / out region of the timeline selection. Dragging it moves the fade left / right.
/// If the selection has a zero-width peak (it fades out right after fading in), then
/// you can move the whole selection by starting a drag in the direction of the other
/// fade item.
/// </summary>
private sealed class TimeSelectionFadeItem : TimeSelectionItem
{
private bool? _moveWholeSelection;
public FadeKind Kind { get; }
public override MovieTimeRange TimeRange
{
get
{
if ( EditMode.TimeSelection is not { } selection )
{
return default;
}
if ( _moveWholeSelection is not false )
{
return selection.TotalTimeRange;
}
return Kind == FadeKind.FadeIn
? selection.FadeInTimeRange
: selection.FadeOutTimeRange;
}
}
public InterpolationMode? Interpolation
{
get => Kind == FadeKind.FadeIn
? EditMode.TimeSelection?.FadeIn.Interpolation
: EditMode.TimeSelection?.FadeOut.Interpolation;
set
{
if ( EditMode.TimeSelection is not { } selection ) return;
if ( value is not { } mode ) return;
EditMode.TimeSelection = Kind == FadeKind.FadeIn
? selection with { FadeIn = selection.FadeIn with { Interpolation = mode } }
: selection with { FadeOut = selection.FadeOut with { Interpolation = mode } };
}
}
public override (MovieTime? Min, MovieTime? Max) DragLimits
{
get
{
if ( EditMode.TimeSelection is not { } selection )
{
return default;
}
if ( _moveWholeSelection is not false )
{
return (-selection.TotalStart, null);
}
return Kind == FadeKind.FadeIn
? (-selection.TotalStart, selection.PeakTimeRange.Duration)
: (-selection.PeakTimeRange.Duration, null);
}
}
public override IEnumerable<MovieTime> SnapSources
{
get
{
if ( EditMode.TimeSelection is not { } selection )
{
return [];
}
return _moveWholeSelection is not false
? [selection.TotalStart, selection.PeakStart, selection.TotalEnd]
: base.SnapSources;
}
}
public TimeSelectionFadeItem( MotionEditMode editMode, FadeKind kind )
: base( editMode )
{
Kind = kind;
ZIndex = 10001;
HandlePosition = kind == FadeKind.FadeIn ? new Vector2( 1f, 0f ) : new Vector2( 0f, 0f );
Cursor = CursorShape.Finger;
}
public override void UpdatePosition( TimeSelection value, Rect viewRect )
{
PrepareGeometryChange();
if ( EditMode.TimeSelection is not { } selection )
{
Position = new Vector2( -50000f, 0f );
}
else
{
var timeRange = Kind == FadeKind.FadeIn
? selection.FadeInTimeRange
: selection.FadeOutTimeRange;
Position = new Vector2( EditMode.Session.TimeToPixels( Kind == FadeKind.FadeIn ? timeRange.End : timeRange.Start ), viewRect.Top );
Size = new Vector2( EditMode.Session.TimeToPixels( timeRange.Duration ), viewRect.Height );
}
Update();
}
protected override void OnStartDrag()
{
if ( OriginalSelection is not { } selection ) return;
// We can't move the whole selection if there's a peak range between the fade in / fade out.
_moveWholeSelection = selection.PeakTimeRange.IsEmpty ? null : false;
}
protected override void OnDrag( MovieTime delta )
{
if ( EditMode.TimeSelection is not { } selection ) return;
// We want to move the whole time selection if the fade in and fade out are adjacent,
// and we start trying to "push" the fade in into the fade out (or vice versa).
_moveWholeSelection ??= Kind == FadeKind.FadeIn && delta.IsPositive || Kind == FadeKind.FadeOut && delta.IsNegative;
if ( _moveWholeSelection is true )
{
if ( EditMode.Modification?.Options is ITranslatableOptions translatable )
{
EditMode.Modification.Options = translatable.WithOffset( translatable.Offset + delta );
}
EditMode.TimeSelection = selection + delta;
}
else if ( Kind == FadeKind.FadeIn )
{
EditMode.TimeSelection = selection.WithTimes( totalStart: selection.TotalStart + delta, peakStart: selection.PeakStart + delta );
}
else
{
EditMode.TimeSelection = selection.WithTimes( peakEnd: selection.PeakEnd + delta, totalEnd: selection.TotalEnd + delta );
}
}
protected override void OnPaint()
{
if ( Width < 1f || Interpolation is not { } interpolation ) return;
var color = EditMode.SelectionColor;
if ( Hovered ) color = color.Lighten( 0.1f );
var fadeColor = color.WithAlpha( 0.02f );
var (x0, x1) = Kind == FadeKind.FadeIn
? (0f, Width)
: (Width, 0f);
Paint.Antialiasing = true;
Paint.SetBrushLinear( new Vector2( x0, 0f ), new Vector2( x1, 0f ), fadeColor, color );
Paint.SetPen( color.WithAlpha( 0.5f ), 0.5f );
PaintExtensions.PaintMirroredCurve( t => interpolation.Apply( t ), LocalRect, ScrubBar.Height, Kind == FadeKind.FadeOut );
}
}
/// <summary>
/// One of the boundaries between the 3 parts of the selection (fade in / peak / fade out). Drag it to move
/// just that boundary. If sections of the selection are zero-width (no peak / no fade in / no fade out),
/// then we work out which handle you wanted to drag based on the direction the drag starts. You can't move
/// these handles past one another.
/// </summary>
private sealed class TimeSelectionHandleItem : TimeSelectionItem, ISnapSource
{
private enum Index
{
TotalStart,
PeakStart,
PeakEnd,
TotalEnd
}
private Index _minIndex;
private Index _maxIndex;
private MovieTime? _dragTime;
public override CursorShape DragCursor => CursorShape.SizeH;
public TimeSelectionHandleItem( MotionEditMode editMode )
: base( editMode )
{
HandlePosition = new Vector2( 0.5f, 0f );
ZIndex = 10002;
}
public override MovieTimeRange TimeRange
{
get
{
if ( EditMode.TimeSelection is not { } current )
{
return default;
}
return _dragTime ?? GetTime( current, GetIndexRange().Min );
}
}
public override (MovieTime? Min, MovieTime? Max) DragLimits
{
get
{
if ( OriginalSelection is not { } original || EditMode.TimeSelection is not { } current )
{
return base.DragLimits;
}
// Limit dragging to neighbouring handles
var curTime = GetTime( current, _minIndex );
var minTime = GetTime( original, _minIndex - 1 );
var maxTime = GetTime( original, _maxIndex + 1 );
return (minTime - curTime, maxTime - curTime);
}
}
protected override void OnStartDrag()
{
(_minIndex, _maxIndex) = GetIndexRange();
if ( OriginalSelection is not { } original ) return;
_dragTime = GetTime( original, _minIndex );
}
protected override void OnDrag( MovieTime delta )
{
if ( OriginalSelection is not { } original ) return;
if ( EditMode.TimeSelection is not { } current ) return;
if ( _dragTime is not { } time ) return;
time += delta;
_dragTime = time;
// If it's ambiguous which handle we are, pick a side
// based on which direction we're dragged
if ( delta.IsNegative ) _maxIndex = _minIndex;
if ( delta.IsPositive ) _minIndex = _maxIndex;
EditMode.TimeSelection = SetTime( original, _minIndex, time );
}
protected override void OnEndDrag()
{
_dragTime = null;
}
private static MovieTime GetTime( TimeSelection value, Index index ) => index switch
{
Index.TotalStart => value.TotalStart,
Index.PeakStart => value.PeakStart,
Index.PeakEnd => value.PeakEnd,
Index.TotalEnd => value.TotalEnd,
< 0 => MovieTime.Zero,
> Index.TotalEnd => MovieTime.MaxValue
};
private static TimeSelection SetTime( TimeSelection value, Index index, MovieTime time ) => index switch
{
Index.TotalStart => value.WithTimes( totalStart: time ),
Index.PeakStart => value.WithTimes( peakStart: time ),
Index.PeakEnd => value.WithTimes( peakEnd: time ),
Index.TotalEnd => value.WithTimes( totalEnd: time ),
_ => value
};
public override void UpdatePosition( TimeSelection value, Rect viewRect )
{
PrepareGeometryChange();
var time = GetTime( value, GetIndexRange().Min );
var margin = _maxIndex == Index.TotalStart || _minIndex == Index.TotalEnd
? ScrubBar.Height
: 0f;
Position = new Vector2( EditMode.Session.TimeToPixels( time ), viewRect.Top + margin );
Size = new Vector2( 8f, viewRect.Height - margin * 2f );
}
/// <summary>
/// Get the possible range of handles this could be, if the time selection has
/// overlapping control points.
/// </summary>
private (Index Min, Index Max) GetIndexRange()
{
if ( EditMode.TimeSelection is not { } value ) return default;
var handles = EditMode.Timeline.Items.OfType<TimeSelectionHandleItem>()
.OrderBy( x => x.Position.x );
var index = Index.TotalStart + handles.TakeWhile( x => x != this ).Count();
var minIndex = index;
var maxIndex = index;
var time = GetTime( value, index );
while ( minIndex > Index.TotalStart && GetTime( value, minIndex - 1 ) == time )
{
minIndex -= 1;
}
while ( maxIndex < Index.TotalEnd && GetTime( value, maxIndex + 1 ) == time )
{
maxIndex += 1;
}
return (minIndex, maxIndex);
}
public IEnumerable<SnapTarget> GetSnapTargets( MovieTime sourceTime, bool isPrimary ) => [TimeRange.Start];
protected override void OnPaint()
{
if ( Hovered )
{
Paint.SetPen( EditMode.SelectionColor.Lighten( 0.1f ).WithAlpha( 1f ) );
Paint.DrawLine( new Vector2( LocalRect.Center.x, LocalRect.Top ), new Vector2( LocalRect.Center.x, LocalRect.Bottom ) );
}
}
}
}