namespace Editor.GraphicsItems; /// /// Anatomy of an EditableCurve: https://files.facepunch.com/garry/72bd3a5f-c9e4-40fe-8748-05e782d3a230.png /// public partial class EditableCurve : GraphicsItem { public List Handles { get; } = new(); public Vector2 TimeRange => _curve.TimeRange; public Vector2 ValueRange => _curve.ValueRange; public Color CurveColor { get; set; } = Theme.Green; public Color SelectionColor { get; set; } = Color.White; public float CurveThickness { get; set; } = 1.0f; public float HandleDistance { get; set; } = 50.0f; public bool IsPartOfRange { get; set; } public bool CanEditTimeRange { get; set; } = true; public bool CanEditValueRange { get; set; } = true; public Vector4 Viewport { get; set; } = Vector4.Zero; public Vector2 ViewportRangeX => new Vector2( Viewport.x, Viewport.z ); public Vector2 ViewportRangeY => new Vector2( Viewport.y, Viewport.w ); CurveEditor editor; Curve _curve; public Curve Value { get { return _curve; } set { _curve = value; _curve.Fix(); FitViewportToCurve(); DeleteAllHandles(); foreach ( var k in _curve.Frames ) { var handle = new Handle( this, k ); Handles.Add( handle ); } editor?.UpdateBackgroundFromCurve( this ); Update(); } } public EditableCurve( CurveEditor parent ) : base( null ) { editor = parent; HoverEvents = true; Cursor = CursorShape.DragCopy; Clip = true; //ClipChildren = true; } protected override void OnPaint() { PaintCurve(); PaintHandleExtras(); base.OnPaint(); } void PaintCurve() { if ( Handles.Count == 0 ) return; float alpha = 0.8f; if ( IsPartOfRange ) alpha *= 0.5f; if ( !Hovered ) alpha *= 0.5f; Paint.SetPen( CurveColor.WithAlpha( alpha ), CurveThickness, PenStyle.Solid ); var viewportCurve = GetViewportAdjustedCurve(); viewportCurve.DrawLine( LocalRect, 3.0f ); } void PaintHandleExtras() { foreach ( var handle in Handles ) { Paint.ResetTransform(); Paint.Translate( Position ); Paint.Translate( handle.Position ); handle.PaintExtras(); } Paint.ResetTransform(); } public void DeleteAllHandles() { foreach ( var handle in Handles ) { handle?.Destroy(); } Handles.Clear(); UpdateTooltip(); } public void OnEdited() { SaveCurve(); BindSystem.Flush(); } public void OnHandleMoved() { OnEdited(); UpdateTooltip(); } public void UpdateZoomRangeX( Vector2 range ) { // Zoom the viewport to the new range Viewport = new Vector4( range.x, Viewport.y, range.y, Viewport.w ); } public void UpdateZoomRangeY( Vector2 range ) { // Zoom the viewport to the new range Viewport = new Vector4( Viewport.x, range.x, Viewport.z, range.y ); } public void FitViewportToCurve() { Viewport = new Vector4( TimeRange.x, ValueRange.x, TimeRange.y, ValueRange.y ); } /// /// Write from editor to the curve (_value) /// public void SaveCurve() { if ( Value.Length > 0 ) { var timeRange = new Vector2( float.MaxValue, float.MinValue ); var valueRange = new Vector2( float.MaxValue, float.MinValue ); foreach ( var frame in Value.Frames ) { var time = frame.Time.Remap( 0, 1, TimeRange.x, TimeRange.y, false ); var value = frame.Value.Remap( 0, 1, ValueRange.x, ValueRange.y, false ); timeRange = timeRange.WithX( System.MathF.Min( timeRange.x, time ) ) .WithY( System.MathF.Max( timeRange.y, time ) ); valueRange = valueRange.WithX( System.MathF.Min( valueRange.x, value ) ) .WithY( System.MathF.Max( valueRange.y, value ) ); } if ( !CanEditTimeRange || IsPartOfRange ) { timeRange = TimeRange; } if ( !CanEditValueRange || IsPartOfRange ) { valueRange = ValueRange; } var newCurve = Value.WithFrames( new List() ); var timeRangeLength = timeRange.y - timeRange.x; var valueRangeLength = valueRange.y - valueRange.x; foreach ( var handle in Handles.OrderBy( x => x.Position.x ) ) { newCurve.AddPoint( handle.GetKeyFrame() ); if ( timeRangeLength != 0 ) { handle.Frame.Time = handle.Frame.Time.Remap( 0, 1, MathF.Min( TimeRange.x, TimeRange.y ), MathF.Max( TimeRange.x, TimeRange.y ), false ); handle.Frame.Time = handle.Frame.Time.Remap( timeRange.x, timeRange.y, 0, 1, false ); } if ( valueRangeLength != 0 ) { handle.Frame.Value = handle.Frame.Value.Remap( 0, 1, MathF.Min( ValueRange.x, ValueRange.y ), MathF.Max( ValueRange.x, ValueRange.y ), false ); handle.Frame.Value = handle.Frame.Value.Remap( valueRange.x, valueRange.y, 0, 1, false ); } } if ( timeRangeLength != 0 ) { newCurve.UpdateTimeRange( timeRange, true ); } if ( valueRangeLength != 0 ) { newCurve.UpdateValueRange( valueRange, true ); } _curve = newCurve; } editor?.UpdateBackgroundFromCurve( this ); Update(); } internal Curve GetViewportAdjustedCurve() { var viewportCurve = Value.WithFrames( Value.Frames ); viewportCurve.UpdateTimeRange( ViewportRangeX, true ); viewportCurve.UpdateValueRange( ViewportRangeY, true ); return viewportCurve; } internal void Delete( Handle editableCurveHandle ) { // Keep at least one around if ( Handles.Count == 1 ) return; if ( Handles.Remove( editableCurveHandle ) ) { editableCurveHandle.Destroy(); UpdateTooltip(); SaveCurve(); } } internal void FocusedHandleKeyPress( KeyEvent e ) { var selectedHandles = Handles.Where( x => x.Selected ).ToArray(); if ( e.Key == KeyCode.Delete ) { foreach ( var selected in selectedHandles ) { Delete( selected ); } } } public void UpdateTimeRange( Vector2 range, bool retainTimes, bool fitViewport = false ) { var oldViewport = Viewport; var c = Value; c.UpdateTimeRange( range, retainTimes ); Value = c; if ( !fitViewport ) { Viewport = oldViewport; } } public void UpdateValueRange( Vector2 range, bool retainValues, bool fitViewport = false ) { var oldViewport = Viewport; var c = Value; c.UpdateValueRange( range, retainValues ); Value = c; if ( !fitViewport ) { Viewport = oldViewport; } } public override bool Contains( Vector2 localPos ) { var x = localPos.x / Size.x; var curve = GetViewportAdjustedCurve(); var f = curve.Evaluate( ViewportRangeX.x.LerpTo( ViewportRangeX.y, x ) ); f = f.LerpInverse( ViewportRangeY.y, ViewportRangeY.x ); f = f * Size.y; var distance = System.MathF.Abs( f - localPos.y ); return distance < 10.0f; } protected override void OnMousePressed( GraphicsMouseEvent e ) { base.OnMousePressed( e ); if ( e.LeftMouseButton ) { var curve = GetViewportAdjustedCurve(); var x = e.LocalPosition.x / Size.x; x = x.Remap( 0, 1, ViewportRangeX.x, ViewportRangeX.y, false ); x = x.Remap( TimeRange.x, TimeRange.y, 0, 1, false ); var v = Value.EvaluateDelta( x ); var k = new Curve.Frame( x, v ); var handle = new Handle( this, k ); Handles.Add( handle ); SaveCurve(); } } HandlePopup ValuePopup; Handle popupTarget; public virtual void HandleSelectionChanged() { UpdateTooltip(); } internal void UpdateHandlePositions() { Update(); foreach ( var handle in Handles ) { handle.UpdatePositionFromValue(); } } public void UpdateTooltip() { if ( !GraphicsView.IsValid() ) return; popupTarget = Handles.Where( x => x.Selected || x.Hovered ).OrderBy( x => !x.Hovered ).FirstOrDefault(); if ( popupTarget.IsValid() && GraphicsView.IsValid() ) { ValuePopup ??= new HandlePopup( GraphicsView ); ValuePopup.UpdateFrom( popupTarget ); ValuePopup.ConstrainTo( GraphicsView.LocalRect.Shrink( 8, 2 ) ); ValuePopup.Visible = true; } else { ValuePopup?.Destroy(); ValuePopup = null; } } } //}