Files
sbox-public/game/addons/tools/Code/Curves/CurveEditor.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

514 lines
15 KiB
C#

using Editor.GraphicsItems;
using Sandbox.Diagnostics;
namespace Editor;
/// <summary>
/// A widget which contains an editable curve
/// </summary>
public class CurveEditor : GraphicsView
{
GraphicsItems.ChartBackground Background;
public Vector2 AxisX
{
get
{
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
if ( editableCurve.TimeRange != Vector2.Zero )
return editableCurve.TimeRange;
}
return Background.RangeX;
}
}
public Vector2 AxisY
{
get
{
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
if ( editableCurve.ValueRange != Vector2.Zero )
return editableCurve.ValueRange;
}
return Background.RangeY;
}
}
readonly Widget TimeMaxWidget;
readonly Widget TimeMinWidget;
readonly Widget ValueMaxWidget;
readonly Widget ValueMinWidget;
readonly Widget ButtonsWidget;
bool MoveRangeWhenPanning = false;
bool MoveRangeWhenSettingMinMax = false;
public bool CanEditTimeRange => TimeMinWidget.Enabled && TimeMaxWidget.Enabled;
public bool CanEditValueRange => ValueMinWidget.Enabled && ValueMaxWidget.Enabled;
float TimeMin
{
get => AxisX.x;
set
{
var axis = AxisX;
if ( value >= axis.y )
return;
var diff = axis.x - value;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
var timeRange = editableCurve.TimeRange;
editableCurve.UpdateTimeRange( new Vector2( timeRange.x - diff, timeRange.y ), MoveRangeWhenSettingMinMax, false );
editableCurve.UpdateZoomRangeX( new Vector2( editableCurve.ViewportRangeX.x - diff, editableCurve.ViewportRangeX.y ) );
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
}
float TimeMax
{
get => AxisX.y;
set
{
var axis = AxisX;
if ( value <= axis.x )
return;
var diff = value - axis.y;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
var timeRange = editableCurve.TimeRange;
editableCurve.UpdateTimeRange( new Vector2( timeRange.x, timeRange.y + diff ), MoveRangeWhenSettingMinMax, false );
editableCurve.UpdateZoomRangeX( new Vector2( editableCurve.ViewportRangeX.x, editableCurve.ViewportRangeX.y + diff ) );
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
}
float ValueMin
{
get => AxisY.x;
set
{
var axis = AxisY;
if ( value >= axis.y )
return;
var diff = axis.x - value;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
var valueRange = editableCurve.ValueRange;
editableCurve.UpdateValueRange( new Vector2( valueRange.x - diff, valueRange.y ), MoveRangeWhenSettingMinMax, false );
editableCurve.UpdateZoomRangeY( new Vector2( editableCurve.ViewportRangeY.x - diff, editableCurve.ViewportRangeY.y ) );
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
}
float ValueMax
{
get => AxisY.y;
set
{
var axis = AxisY;
if ( value <= axis.x )
return;
var diff = value - axis.y;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
var valueRange = editableCurve.ValueRange;
editableCurve.UpdateValueRange( new Vector2( valueRange.x, valueRange.y + diff ), MoveRangeWhenSettingMinMax, false );
editableCurve.UpdateZoomRangeY( new Vector2( editableCurve.ViewportRangeY.x, editableCurve.ViewportRangeY.y + diff ) );
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
}
public CurveEditor( Widget parent ) : base( parent )
{
SceneRect = new( 0, Size );
HorizontalScrollbar = ScrollbarMode.Off;
VerticalScrollbar = ScrollbarMode.Off;
Scale = 1;
CenterOn( new Vector2( 100, 10 ) );
Background = new GraphicsItems.ChartBackground
{
Size = SceneRect.Size,
RangeX = new Vector2( 0, 1 ),
RangeY = new Vector2( 1, 0 )
};
Background.AxisX.Width = 100;
Add( Background );
var so = this.GetSerialized();
TimeMinWidget = new Widget( this );
TimeMinWidget.Layout = Layout.Row();
var xMinWidget = TimeMinWidget.Layout.Add( new FloatControlWidget( so.GetProperty( nameof( TimeMin ) ) ) );
xMinWidget.Label = null;
xMinWidget.Icon = "first_page";
xMinWidget.HighlightColor = Theme.Red;
TimeMaxWidget = new Widget( this );
TimeMaxWidget.Layout = Layout.Row();
var xMaxWidget = TimeMaxWidget.Layout.Add( new FloatControlWidget( so.GetProperty( nameof( TimeMax ) ) ) );
xMaxWidget.Label = null;
xMaxWidget.Icon = "last_page";
xMaxWidget.HighlightColor = Theme.Red;
ValueMinWidget = new Widget( this );
ValueMinWidget.Layout = Layout.Row();
var yMinWidget = ValueMinWidget.Layout.Add( new FloatControlWidget( so.GetProperty( nameof( ValueMin ) ) ) );
yMinWidget.Label = null;
yMinWidget.Icon = "vertical_align_bottom";
ValueMaxWidget = new Widget( this );
ValueMaxWidget.Layout = Layout.Row();
var yMaxWidget = ValueMaxWidget.Layout.Add( new FloatControlWidget( so.GetProperty( nameof( ValueMax ) ) ) );
yMaxWidget.Label = null;
yMaxWidget.Icon = "vertical_align_top";
ButtonsWidget = new Widget( this );
ButtonsWidget.Layout = Layout.Row();
ButtonsWidget.Layout.Spacing = 4;
var btnPanTool = ButtonsWidget.Layout.Add( new IconButton( "pan_tool" ) );
btnPanTool.ToolTip = "Change Range when Panning/Zooming";
btnPanTool.OnClick = () =>
{
MoveRangeWhenPanning = !MoveRangeWhenPanning;
btnPanTool.Background = MoveRangeWhenPanning ? Theme.Highlight : Theme.ButtonBackground;
};
var btnChangeRange = ButtonsWidget.Layout.Add( new IconButton( "height" ) );
btnChangeRange.ToolTip = "Retain Values when changing Min/Max";
btnChangeRange.OnClick = () =>
{
MoveRangeWhenSettingMinMax = !MoveRangeWhenSettingMinMax;
btnChangeRange.Background = MoveRangeWhenSettingMinMax ? Theme.Highlight : Theme.ButtonBackground;
};
var btnFitToScreen = ButtonsWidget.Layout.Add( new IconButton( "settings_overscan" ) );
btnFitToScreen.ToolTip = "Fit to Screen";
btnFitToScreen.OnClick = () =>
{
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
editableCurve.FitViewportToCurve();
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
};
}
protected override void OnWheel( WheelEvent e )
{
base.OnWheel( e );
if ( e.Delta != 0 )
{
var mult = MathF.Sign( e.Delta ) * 0.1f;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
if ( e.HasCtrl || (!e.HasCtrl && !e.HasAlt && !e.HasShift) )
{
if ( MoveRangeWhenPanning && CanEditTimeRange )
{
var timeRange = editableCurve.TimeRange;
var tLen = MathF.Abs( timeRange.x - timeRange.y );
editableCurve.UpdateTimeRange( new Vector2(
timeRange.x + (tLen * mult),
timeRange.y - (tLen * mult)
), false, false );
}
var rangeX = editableCurve.ViewportRangeX;
var xLen = MathF.Abs( rangeX.x - rangeX.y );
editableCurve.UpdateZoomRangeX( new Vector2(
rangeX.x + (xLen * mult),
rangeX.y - (xLen * mult)
) );
}
if ( e.HasShift || (!e.HasCtrl && !e.HasAlt && !e.HasShift) )
{
if ( MoveRangeWhenPanning && CanEditValueRange )
{
var valueRange = editableCurve.ValueRange;
var vLen = MathF.Abs( valueRange.x - valueRange.y );
editableCurve.UpdateValueRange( new Vector2(
valueRange.x + (vLen * mult),
valueRange.y - (vLen * mult)
), false, false );
}
var rangeY = editableCurve.ViewportRangeY;
var yLen = MathF.Abs( rangeY.x - rangeY.y );
editableCurve.UpdateZoomRangeY( new Vector2(
rangeY.x + (yLen * mult),
rangeY.y - (yLen * mult)
) );
}
editableCurve.Update();
foreach ( var handle in editableCurve.Handles )
{
handle.UpdatePositionFromValue();
}
UpdateBackgroundFromCurve( editableCurve );
}
e.Accept();
}
}
bool isDraggingBackground = false;
Vector2 dragStartPosition;
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( e.MiddleMouseButton )
{
isDraggingBackground = true;
dragStartPosition = e.LocalPosition;
}
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
if ( isDraggingBackground )
{
isDraggingBackground = false;
return;
}
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
if ( isDraggingBackground )
{
var delta = e.LocalPosition - dragStartPosition;
//delta *= new Vector2( Background.RangeX.Length, Background.RangeY.Length ) * 1.33f;
dragStartPosition = e.LocalPosition;
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
var rangeX = editableCurve.ViewportRangeX;
var rangeY = editableCurve.ViewportRangeY;
var xLen = MathF.Abs( rangeX.x - rangeX.y );
var yLen = MathF.Abs( rangeY.x - rangeY.y );
// Remap the delta based on the current viewport size
var _deltaX = delta.x / SceneRect.Size.x * xLen;
var _deltaY = delta.y / SceneRect.Size.y * yLen;
if ( MoveRangeWhenPanning )
{
var timeRange = editableCurve.TimeRange;
var valueRange = editableCurve.ValueRange;
if ( CanEditTimeRange )
{
editableCurve.UpdateTimeRange( new Vector2( timeRange.x - _deltaX, timeRange.y - _deltaX ), false, false );
}
if ( CanEditValueRange )
{
editableCurve.UpdateValueRange( new Vector2( valueRange.x + _deltaY, valueRange.y + _deltaY ), false, false );
}
}
editableCurve.UpdateZoomRangeX( new Vector2( rangeX.x - _deltaX, rangeX.y - _deltaX ) );
editableCurve.UpdateZoomRangeY( new Vector2( rangeY.x + _deltaY, rangeY.y + _deltaY ) );
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
}
protected override void OnResize()
{
base.OnResize();
foreach ( var editableCurve in Items.OfType<GraphicsItems.EditableCurve>() )
{
editableCurve.UpdateHandlePositions();
UpdateBackgroundFromCurve( editableCurve );
}
}
protected override void DoLayout()
{
base.DoLayout();
SceneRect = new Rect( 0, 0, Width, Height );
Background.Position = new Vector2( 0, 0 );
Background.Size = SceneRect.Size - new Vector2( 28, 32 );
float textHeight = 22;
float textWidth = Background.AxisX.Width - 20;
ValueMinWidget.Position = new Vector2( 8, Background.Position.y + Background.Size.y - (textHeight / 2f) - Background.AxisY.Width );
ValueMinWidget.Size = new Vector2( textWidth - 4, textHeight );
ValueMaxWidget.Position = new Vector2( 8, 8 + Background.Position.y );
ValueMaxWidget.Size = new Vector2( textWidth - 4, textHeight );
TimeMaxWidget.Position = new Vector2( Background.Position.x + Background.Size.x - textWidth, Background.Position.y + Background.Size.y );
TimeMaxWidget.Size = new Vector2( textWidth, textHeight );
TimeMinWidget.Position = new Vector2( Background.Position.x + Background.AxisX.Width, Background.Position.y + Background.Size.y );
TimeMinWidget.Size = new Vector2( textWidth, textHeight );
ButtonsWidget.Position = new Vector2( Background.Position.x + 8, Background.Position.y + Background.Size.y );
foreach ( var i in Items )
{
//
// When resizing the curve we save the curve, resize, then set the curve back
//
if ( i is GraphicsItems.EditableCurve curve )
{
if ( curve.SceneRect == Background.ChartRect ) continue;
var c = curve.Value;
curve.SceneRect = Background.ChartRect;
curve.Value = c;
}
if ( i is GraphicsItems.RangePolygon poly )
{
if ( poly.SceneRect == Background.ChartRect ) continue;
poly.SceneRect = Background.ChartRect;
}
}
}
public void UpdateBackgroundFromCurve( EditableCurve editableCurve )
{
if ( editableCurve is null ) return;
Background.Highlight = new Vector4(
editableCurve.TimeRange.x,
editableCurve.ValueRange.x,
editableCurve.TimeRange.y,
editableCurve.ValueRange.y
);
Background.RangeX = editableCurve.ViewportRangeX;
Background.RangeY = editableCurve.ViewportRangeY;
Background.Update();
}
public float GetBackgroundIncrementX() => Background.GetCurrentIncrementX();
public float GetBackgroundIncrementY() => Background.GetCurrentIncrementY();
internal void AddCurve( Func<Curve> get, Action<Curve> set )
{
var curve = new GraphicsItems.EditableCurve( this );
Add( curve );
curve.SceneRect = Background.ChartRect;
curve.Size = Background.ChartRect.Size;
curve.CanEditTimeRange = CanEditTimeRange;
curve.CanEditValueRange = CanEditValueRange;
var c = get();
curve.Value = c;
curve.Bind( "Value" ).From( get, set );
curve.FitViewportToCurve();
}
public void UpdateTimeRange( Vector2 r, bool retainValues = false )
{
foreach ( var i in Items.OfType<GraphicsItems.EditableCurve>() )
{
i.UpdateTimeRange( r, retainValues, false );
i.UpdateHandlePositions();
UpdateBackgroundFromCurve( i );
}
}
public void UpdateValueRange( Vector2 r, bool retainValues = false )
{
foreach ( var i in Items.OfType<GraphicsItems.EditableCurve>() )
{
i.UpdateValueRange( r, retainValues, false );
i.UpdateHandlePositions();
UpdateBackgroundFromCurve( i );
}
}
public void ClearHandles()
{
Curve c = 0.5f;
foreach ( var i in Items.OfType<GraphicsItems.EditableCurve>() )
{
i.Value = i.Value.WithFrames( c.Frames );
i.Update();
}
}
/// <summary>
/// Set this editor to be a range editor
/// </summary>
public void SetIsRange()
{
var items = Items.OfType<GraphicsItems.EditableCurve>().ToList();
Assert.True( items.Count() == 2 );
foreach ( var item in items )
{
item.IsPartOfRange = true;
}
var e = new GraphicsItems.RangePolygon( items[0], items[1] );
e.SceneRect = Background.ChartRect;
Add( e );
}
/// <summary>
/// Set whether or not the user can edit the time range of the curves in this editor.
/// </summary>
public void SetCanEditTimeRange( bool canEdit )
{
TimeMinWidget.Enabled = canEdit;
TimeMaxWidget.Enabled = canEdit;
foreach ( var i in Items.OfType<GraphicsItems.EditableCurve>() )
{
i.CanEditTimeRange = canEdit;
}
}
/// <summary>
/// Set whether or not the user can edit the value range of the curves in this editor.
/// </summary>
public void SetCanEditValueRange( bool canEdit )
{
ValueMinWidget.Enabled = canEdit;
ValueMaxWidget.Enabled = canEdit;
foreach ( var i in Items.OfType<GraphicsItems.EditableCurve>() )
{
i.CanEditValueRange = canEdit;
}
}
[WidgetGallery]
[Title( "Curve Editor" )]
[Icon( "web" )]
internal static Widget WidgetGallery()
{
var canvas = new CurveEditor( null );
var a = new Curve( new Curve.Frame( 0.0f, 0.5f ), new Curve.Frame( 0.5f, 1.0f ), new Curve.Frame( 1.0f, 0.8f ) ) { ValueRange = new( 0, 10 ) };
canvas.AddCurve( () => a, v => a = v );
var b = new Curve( new Curve.Frame( 0.0f, 0.2f ), new Curve.Frame( 1.0f, 0.8f ) );
canvas.AddCurve( () => b, v => b = v );
canvas.SetIsRange();
canvas.SetCanEditTimeRange( false );
return canvas;
}
}