Files
sbox-public/game/editor/MovieMaker/Code/Session/TrackView.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

523 lines
12 KiB
C#

using System.Diagnostics.CodeAnalysis;
using Sandbox.MovieMaker;
using System.Linq;
namespace Editor.MovieMaker;
#nullable enable
/// <summary>
/// Describes how a track should be displayed in the track list / dope sheet.
/// </summary>
public sealed partial class TrackView : IComparable<TrackView>
{
public TrackListView TrackList { get; }
public float Position { get; private set; } = -1f;
public float Height { get; private set; } = -1f;
public TrackView? Parent { get; }
public IProjectTrack Track { get; }
public ITrackTarget Target { get; }
public MovieTime Duration => _blocks.Select( x => x.TimeRange.End )
.Concat( _previewBlocks.Select( x => x.TimeRange.End ) )
.DefaultIfEmpty( MovieTime.Zero )
.Max();
private IPropertyTrack<Transform>? _transformTrack;
public IPropertyTrack<Transform> TransformTrack => _transformTrack ??= CreateTransformTrack();
private bool _isExpanded;
private bool _isLockedSelf;
private bool _isHovered;
private bool _wasExpanded;
public bool IsSelected { get; internal set; }
public bool IsHovered
{
get => _isHovered;
set
{
if ( _isHovered == value ) return;
_isHovered = value;
DispatchChanged( true );
}
}
public bool IsExpanded
{
get => _isExpanded;
set
{
if ( _isExpanded == value ) return;
_isExpanded = value;
SetCookie( nameof( IsExpanded ), value );
TrackList.Update();
}
}
public bool IsLockedSelf
{
get => _isLockedSelf;
set
{
if ( _isLockedSelf == value ) return;
_isLockedSelf = value;
SetCookie( nameof( IsLockedSelf ), value );
DispatchChanged( true );
}
}
public bool IsLocked => IsLockedSelf || Parent?.IsLocked is true;
public string Title => Track.Name;
public string Description
{
get
{
var path = Track.GetPath();
string[] propertyNames = [path.ReferenceTrack.Name, .. path.PropertyNames];
return string.Join( $" \u2192 ", propertyNames );
}
}
private readonly SynchronizedSet<IProjectTrack, TrackView> _children;
private bool _dispatchValueChanged = false;
public IReadOnlyList<TrackView> Children => _children;
public int StateHash { get; private set; }
public bool IsEmpty => _children.Count == 0 && Track.IsEmpty;
/// <summary>
/// Invoked when properties of this track are changed.
/// </summary>
public event Action<TrackView>? Changed;
/// <summary>
/// Invoked when the contents of the track are modified.
/// </summary>
public event Action<TrackView>? ValueChanged;
/// <summary>
/// Invoked when this track is removed.
/// </summary>
public event Action<TrackView>? Removed;
public TrackView( TrackListView trackList, TrackView? parent, IProjectTrack track, ITrackTarget target )
{
TrackList = trackList;
Parent = parent;
Track = track;
Target = target;
_isExpanded = GetCookie( nameof( IsExpanded ), true );
_isLockedSelf = GetCookie( nameof( IsLockedSelf ), false );
_children = new SynchronizedSet<IProjectTrack, TrackView>(
AddChildTrack, RemoveChildTrack, UpdateChildTrack );
}
private void DispatchChanged( bool recurse )
{
Changed?.Invoke( this );
if ( !recurse ) return;
foreach ( var child in _children )
{
child.DispatchChanged( true );
}
}
private static PropertySignal<object?> DefaultSignal { get; } = (object?)null;
private TrackView AddChildTrack( IProjectTrack source )
{
return new( TrackList, this, source, TrackList.Session.Binder.Get( source ) );
}
private void RemoveChildTrack( TrackView item ) => item.OnRemoved();
private bool UpdateChildTrack( IProjectTrack source, TrackView item ) => item.Update();
public void Select()
{
TrackList.DeselectAll();
IsSelected = true;
Update();
}
private readonly record struct SubOrderedTrack( IProjectTrack Track, int SubOrder ) : IComparable<SubOrderedTrack>
{
public int CompareTo( SubOrderedTrack other )
{
if ( Track.Order != other.Track.Order )
{
return Track.Order - other.Track.Order;
}
if ( SubOrder != other.SubOrder )
{
return SubOrder - other.SubOrder;
}
return Track.CompareTo( other.Track );
}
}
[ThreadStatic]
private static List<SubOrderedTrack>? _tempOrderedTracks;
public bool Update()
{
_transformTrack = null;
var ordered = _tempOrderedTracks ??= [];
ordered.Clear();
ordered.AddRange( Track.Children.Select( x => new SubOrderedTrack( x, GetSubOrder( x ) ) ) );
ordered.Sort();
return _children.Update( ordered.Select( x => x.Track ) ) || _wasExpanded != IsExpanded;
}
private int GetSubOrder( IProjectTrack track )
{
// Keep Component tracks ordered the same as in the inspector
if ( Track.TargetType != typeof(GameObject) ) return 0;
if ( Target is not { IsBound: true, Value: GameObject go } ) return 0;
if ( track is not IProjectReferenceTrack refTrack ) return 0;
if ( !refTrack.TargetType.IsAssignableTo( typeof(Component) ) ) return 0;
var index = 0;
foreach ( var cmp in go.Components.GetAll() )
{
if ( refTrack.TargetType.IsInstanceOfType( cmp ) )
{
return index;
}
index++;
}
return int.MaxValue;
}
public bool UpdatePosition( ref float position )
{
var changed = !Position.Equals( position ) || _wasExpanded != IsExpanded;
var hashCode = new HashCode();
hashCode.Add( Track );
hashCode.Add( IsExpanded );
Position = position;
_wasExpanded = IsExpanded;
position += Timeline.TrackHeight;
var childPosition = position;
foreach ( var child in _children )
{
changed |= child.UpdatePosition( ref childPosition );
hashCode.Add( child.StateHash );
}
if ( IsExpanded )
{
position = childPosition;
}
Height = position - Position;
StateHash = hashCode.ToHashCode();
if ( changed ) Changed?.Invoke( this );
return changed;
}
private bool _removed;
internal void OnRemoved()
{
if ( _removed ) return;
_removed = true;
_children.Clear();
Removed?.Invoke( this );
}
public void Remove()
{
Track.Remove();
TrackList.Update();
}
public bool MarkValueChanged()
{
_blocksInvalid = true;
_previewBlocksInvalid = true;
_dispatchValueChanged = true;
Parent?.MarkValueChanged();
TrackList.Session.ClipModified();
TrackList.Session.RefreshNextFrame();
return true;
}
public void Frame()
{
if ( _dispatchValueChanged )
{
_dispatchValueChanged = false;
ValueChanged?.Invoke( this );
}
foreach ( var child in _children )
{
child.Frame();
}
}
public int CompareTo( TrackView? other )
{
if ( ReferenceEquals( this, other ) )
{
return 0;
}
if ( other is null )
{
return 1;
}
var childrenCompare = (Children.Count > 0).CompareTo( other.Children.Count > 0 );
if ( childrenCompare != 0 ) return childrenCompare;
return string.Compare( Track.Name, other.Track.Name, StringComparison.Ordinal );
}
private T GetCookie<T>( string name, T fallback ) =>
TrackList.Session.GetCookie( $"{Track.Id}.{name}", fallback );
private void SetCookie<T>( string name, T value ) =>
TrackList.Session.SetCookie( $"{Track.Id}.{name}", value );
public void InspectProperty()
{
Select();
if ( Target is not { } property ) return;
if ( property.GetTargetGameObject() is not { } go ) return;
SceneEditorSession.Active.Selection.Clear();
SceneEditorSession.Active.Selection.Add( go );
if ( Track.Parent is not IReferenceTrack<GameObject> )
{
return;
}
EditorToolManager.SetTool( nameof(ObjectEditorTool) );
switch ( property.Name )
{
case nameof( GameObject.LocalPosition ):
EditorToolManager.SetSubTool( nameof( PositionEditorTool ) );
break;
case nameof( GameObject.LocalRotation ):
EditorToolManager.SetSubTool( nameof( RotationEditorTool ) );
break;
case nameof( GameObject.LocalScale ):
EditorToolManager.SetSubTool( nameof( ScaleEditorTool ) );
break;
}
}
public TrackView? Find( string propertyPath )
{
var parent = this;
while ( parent is not null && propertyPath.Length > 0 )
{
var propertyName = propertyPath;
// TODO: Hack for anim graph parameters including periods
if ( parent.Track.TargetType != typeof( SkinnedModelRenderer.ParameterAccessor ) && propertyPath.IndexOf( '.' ) is var index and > -1 )
{
propertyName = propertyPath[..index];
propertyPath = propertyPath[(index + 1)..];
}
else
{
propertyPath = string.Empty;
}
parent = parent.Children.FirstOrDefault( x => x.Track.Name == propertyName );
}
return parent;
}
public void ApplyFrame( MovieTime time )
{
switch ( Track )
{
case ProjectSequenceTrack sequenceTrack:
var session = TrackList.Session;
var binder = session.Binder;
var sequenceBlock = sequenceTrack.Blocks.GetBlock( time );
if ( sequenceBlock is null ) break;
// If we're editing this sequence, its Session will handle applying so we don't need
// to do it here
if ( session.Editor.IsMovieOpen( sequenceBlock.Resource ) ) break;
foreach ( var propertyTrack in sequenceTrack.PropertyTracks )
{
propertyTrack.Update( time, binder );
}
break;
case IPropertyTrack propertyTrack:
if ( Target is not ITrackProperty { CanWrite: true } property ) break;
UpdatePreviewBlocks();
if ( _previewBlocks.GetBlock( time ) is IPropertySignal block )
{
property.Value = block.GetValue( time );
}
else
{
property.Update( propertyTrack, time );
}
break;
}
}
public bool TryGetValue<T>( MovieTime time, [MaybeNullWhen( false )] out T value )
{
value = default;
if ( Track is not IPropertyTrack<T> track ) return false;
UpdatePreviewBlocks();
if ( _previewBlocks.GetBlock( time ) is IPropertySignal<T> signal )
{
value = signal.GetValue( time );
return true;
}
return track.TryGetValue( time, out value );
}
private IPropertyTrack<Transform> CreateTransformTrack()
{
if ( Track is not IReferenceTrack<GameObject> )
{
return Parent?.TransformTrack ?? new TransformTrack( this );
}
return new TransformTrack( this,
Find( nameof(GameObject.Enabled) ),
Find( nameof(GameObject.LocalPosition) ),
Find( nameof(GameObject.LocalRotation) ),
Find( nameof(GameObject.LocalScale) ) );
}
}
file sealed class TransformTrack : IPropertyTrack<Transform>
{
public string Name => "Transform";
public ITrack Parent => View.Track;
public TrackView View { get; }
public TrackView? Enabled { get; }
public TrackView? LocalPosition { get; }
public TrackView? LocalRotation { get; }
public TrackView? LocalScale { get; }
public TransformTrack( TrackView view,
TrackView? enabled = null,
TrackView? localPosition = null,
TrackView? localRotation = null,
TrackView? localScale = null )
{
View = view;
Enabled = enabled;
LocalPosition = localPosition;
LocalRotation = localRotation;
LocalScale = localScale;
}
public bool TryGetValue( MovieTime time, out Transform value )
{
value = Transform.Zero;
// This track only returns a value if:
// 1) Enabled is true, or
// 2) Enabled is undefined, and any component track is defined
if ( Enabled?.TryGetValue( time, out bool enabled ) is true )
{
if ( !enabled ) return false;
}
else
{
enabled = false;
}
if ( LocalPosition?.TryGetValue( time, out Vector3 pos ) is true )
{
value.Position = pos;
enabled = true;
}
if ( LocalRotation?.TryGetValue( time, out Rotation rot ) is true )
{
value.Rotation = rot;
enabled = true;
}
if ( LocalScale?.TryGetValue( time, out Vector3 scale ) is true )
{
value.Scale = scale;
enabled = true;
}
if ( View.Parent?.TransformTrack.TryGetValue( time, out var parentTransform ) is true )
{
value = parentTransform.ToWorld( value );
}
return enabled;
}
}