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

261 lines
5.8 KiB
C#

using Sandbox.MovieMaker;
namespace Editor.MovieMaker.BlockDisplays;
#nullable enable
/// <summary>
/// A <see cref="CurveBlockItem{T}"/> is broken up into fixed-width tiles that get repainted separately.
/// </summary>
public sealed class CurveTile : GraphicsItem
{
public const int TileWidth = 256;
private readonly Pixmap _pixmap = new( TileWidth, Timeline.BlockHeight );
private readonly List<Vector2>[] _lines;
private readonly (float Min, float Max)[] _ranges;
/// <summary>
/// Min and max value for each decomposed element within this tile's time range.
/// Will always be up to date.
/// </summary>
public IReadOnlyList<(float Min, float Max)> Ranges
{
get
{
UpdateRanges();
return _ranges;
}
}
/// <summary>
/// Parent <see cref="CurveBlockItem{T}"/>.
/// </summary>
public ICurveBlockItem BlockItem { get; }
/// <summary>
/// Which tile is this in the parent block? First tile is index 0, next is 1, etc.
/// </summary>
public int Index { get; }
public MovieTimeRange TimeRange { get; set; }
private MovieTimeRange _pixmapTimeRange;
private MovieTimeRange _rangesTimeRange;
public CurveTile( ICurveBlockItem blockItem, int index )
: base( (GraphicsItem)blockItem )
{
BlockItem = blockItem;
Index = index;
Height = Timeline.BlockHeight;
var elementCount = blockItem.Elements.Count;
_lines = new List<Vector2>[elementCount];
_ranges = new (float Min, float Max)[elementCount];
for ( var i = 0; i < elementCount; ++i )
{
_lines[i] = [];
}
}
public void MarkDirty( MovieTimeRange timeRange )
{
if ( timeRange.Intersect( TimeRange ) is not { IsEmpty: false } clamped )
{
return;
}
_pixmapTimeRange = default;
_rangesTimeRange = default;
}
protected override void OnPaint()
{
UpdatePixmap();
var scale = LocalRect.Width / _pixmap.Width;
var clampedRect = LocalRect;
clampedRect.Right = Math.Min( clampedRect.Right, Parent.Width - Position.x );
Paint.ClearPen();
Paint.Scale( scale, 1f );
Paint.SetBrush( _pixmap );
Paint.DrawRect( clampedRect with { Left = clampedRect.Left / scale, Right = clampedRect.Right / scale } );
}
[field: ThreadStatic]
public static List<MovieTime>? UpdatePixmap_Times { get; set; }
private void UpdatePixmap()
{
if ( _pixmapTimeRange == TimeRange ) return;
_pixmapTimeRange = TimeRange;
_pixmap.Clear( Color.Transparent );
var elementCount = BlockItem.Elements.Count;
if ( elementCount <= 0 ) return;
var times = UpdatePixmap_Times ??= new List<MovieTime>();
times.Clear();
GetCurveTimes( times );
if ( times.Count == 0 ) return;
var ranges = BlockItem.Ranges;
for ( var i = 0; i < elementCount; ++i )
{
_lines[i].Clear();
}
const float margin = 2f;
var height = LocalRect.Height - margin * 2f;
Span<float> mids = stackalloc float[elementCount];
var range = 0f;
for ( var j = 0; j < elementCount; ++j )
{
range = Math.Max( range, ranges[j].Max - ranges[j].Min );
mids[j] = (ranges[j].Min + ranges[j].Max) * 0.5f;
}
var scale = range <= 0f ? 0f : height / range;
var t0 = TimeRange.Start;
var t1 = TimeRange.End;
var width = _pixmap.Width;
var dxdt = width / (t1 - t0).TotalSeconds;
Span<float> floats = stackalloc float[elementCount];
foreach ( var t in times )
{
BlockItem.Read( t - BlockItem.Offset, floats );
var x = (float)((t - t0).TotalSeconds * dxdt);
for ( var j = 0; j < elementCount; ++j )
{
var y = (mids[j] - floats[j]) * scale + 0.5f * height + margin;
_lines[j].Add( new Vector2( x, y ) );
}
}
using ( Paint.ToPixmap( _pixmap ) )
{
Paint.Antialiasing = true;
for ( var i = 0; i < elementCount; ++i )
{
Paint.SetPen( BlockItem.Elements[i].Color.WithAlpha( 0.25f ), 2f );
Paint.DrawLine( _lines[i] );
}
}
}
[field: ThreadStatic]
public static List<MovieTime>? UpdateRanges_Times { get; set; }
private void UpdateRanges()
{
if ( _rangesTimeRange == TimeRange ) return;
_rangesTimeRange = TimeRange;
var elementCount = BlockItem.Elements.Count;
if ( elementCount <= 0 ) return;
for ( var i = 0; i < elementCount; ++i )
{
_ranges[i].Min = BlockItem.Elements[i].Min ?? float.PositiveInfinity;
_ranges[i].Max = BlockItem.Elements[i].Max ?? float.NegativeInfinity;
}
var times = UpdateRanges_Times ??= new List<MovieTime>();
times.Clear();
GetCurveTimes( times );
if ( times.Count == 0 ) return;
Span<float> floats = stackalloc float[elementCount];
foreach ( var t in times )
{
if ( t < _rangesTimeRange.Start ) continue;
if ( t > _rangesTimeRange.End ) break;
BlockItem.Read( t - BlockItem.Offset, floats );
for ( var i = 0; i < elementCount; ++i )
{
_ranges[i].Min = Math.Min( _ranges[i].Min, floats[i] );
_ranges[i].Max = Math.Max( _ranges[i].Max, floats[i] );
}
}
}
/// <summary>
/// Controls curve resolution based on zoom level.
/// </summary>
[FromTheme]
public static float PixelsPerSample { get; set; } = 2f;
private void GetCurveTimes( List<MovieTime> times )
{
var timeRange = TimeRange;
times.Add( timeRange.Start );
void TryAddTime( MovieTime time )
{
time += BlockItem.Offset;
if ( time < timeRange.Start ) return;
if ( time > timeRange.End ) return;
if ( times[^1] < time )
{
times.Add( time );
}
}
var sampleRate = Math.Max( (int)Math.Round( TileWidth / (TimeRange.Duration.TotalSeconds * PixelsPerSample) ), 1 );
foreach ( var hintRange in BlockItem.GetPaintHints( timeRange ) )
{
TryAddTime( hintRange.Start );
// We subtract epsilon from the end so we can more accurately see
// sudden value changes going into the next paint hint time range
var clamped = hintRange with { End = hintRange.End - MovieTime.Epsilon };
foreach ( var time in clamped.GetSampleTimes( sampleRate ) )
{
TryAddTime( time );
}
TryAddTime( hintRange.End - MovieTime.Epsilon );
}
TryAddTime( timeRange.End );
}
}