Editor/AltCurveUtils.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AltCurves;
internal static class AltCurveUtils
{
/// <summary>
/// Get the distance between pX,pY and the line segment defined by x1,y1 and x2,y2.
/// </summary>
internal static float DistanceToLineSegment( float x1, float y1, float x2, float y2, float px, float py )
{
float A = px - x1;
float B = x2 - x1;
float C = py - y1;
float D = y2 - y1;
float dotProduct = A * B + C * D;
float lenSq = B * B + D * D;
float param = -1f;
if ( lenSq != 0f )
{
param = dotProduct / lenSq;
}
float closestX, closestY;
if ( param < 0f )
{
closestX = x1;
closestY = y1;
}
else if ( param > 1f )
{
closestX = x2;
closestY = y2;
}
else
{
closestX = x1 + param * B;
closestY = y1 + param * D;
}
return MathF.Sqrt( MathF.Pow( closestX - px, 2 ) + MathF.Pow( closestY - py, 2 ) );
}
/// <summary>
/// Draw the full curve (from the first to last keyframes) onto the given rect
/// </summary>
// Honestly I didn't profile this to see if it was worth it, but this caching pattern is used by the stock curve drawing widgets so we'll use it for now
static List<Vector2> pointCache = new();
internal static void DrawFullCurve( this in AltCurve curve, in Rect rect, float spacing = 2.0f )
{
pointCache.Clear();
var (timeMin, timeMax) = curve.TimeRange;
var (valueMin, valueMax) = curve.ValueRange;
var step = 1.0f / (rect.Width / spacing);
for ( float f = 0; f < 1.0f + step * 0.1f; f += step )
{
var stepTime = timeMin + (timeMax - timeMin) * f;
var stepValue = curve.Evaluate( stepTime ).Remap( valueMin, valueMax );
pointCache.Add( new Vector2( rect.Left + f * rect.Width, rect.Top + rect.Height - stepValue * rect.Height ) );
}
Paint.DrawLine( pointCache );
pointCache.Clear();
}
/// <summary>
/// Draw a portion of a curve onto a widget, using the given widget/curve transform
/// </summary>
internal static void DrawPartialCurve( this in AltCurve curve, in CurveWidgetTransform transform, float spacing = 2.0f, float infinityAlpha = 0.4f )
{
pointCache.Clear();
double widgetWidth = (double)transform.WidgetSize.x;
double widgetHeight = (double)transform.WidgetSize.y;
double step = spacing / widgetWidth;
double curveRangeX = transform.CurveRange.MaxX - transform.CurveRange.MinX;
double curveRangeY = transform.CurveRange.MaxY - transform.CurveRange.MinY;
for ( double t = 0; t <= 1.0; t += step )
{
double time = transform.CurveRange.MinX + t * curveRangeX;
double value = curve.Evaluate( (float)time );
double normalizedValue = (value - transform.CurveRange.MinY) / curveRangeY;
pointCache.Add( new Vector2(
(float)(t * widgetWidth),
(float)(widgetHeight - normalizedValue * widgetHeight)
) );
}
var (minTime, maxTime) = curve.TimeRange;
var minTimePixelX = transform.CurveToWidgetX( minTime );
var maxTimePixelX = transform.CurveToWidgetX( maxTime );
// Ok, we have a pointCache full of vector poisitons that form the line of our curve
// It's possible that some segments of the line fall either before/after the first/last keyframe,
// and if so we want to tint it with a different alpha.
// lastIdxPreCurve is the index of the last point that falls before the first keyframe (or -1)
// firstIdxPostCurve is the index of the first point that falls after the last keyframe (or -1)
int lastIdxPreCurve = pointCache.FindLastIndex( point => point.x < minTimePixelX );
int firstIdxPostCurve = pointCache.FindIndex( point => point.x > maxTimePixelX );
// Pre-infinity segment
var initialPen = Paint.Pen;
if (lastIdxPreCurve > -1)
{
Paint.SetPen( initialPen.WithAlpha( infinityAlpha ), Paint.PenSize, Paint.PenStyle );
Paint.DrawLine( pointCache.Take( lastIdxPreCurve + 1 ) );
}
Paint.SetPen( initialPen, Paint.PenSize, Paint.PenStyle );
if ( firstIdxPostCurve > -1 && lastIdxPreCurve > -1 )
Paint.DrawLine( pointCache.Skip( lastIdxPreCurve ).Take( firstIdxPostCurve - lastIdxPreCurve + 1 ) );
else if ( firstIdxPostCurve > -1 )
Paint.DrawLine( pointCache.Take( firstIdxPostCurve + 1 ) );
else if ( lastIdxPreCurve > -1 )
Paint.DrawLine( pointCache.Skip( lastIdxPreCurve ) );
else
Paint.DrawLine( pointCache );
// Post-infinity segment
if ( firstIdxPostCurve > -1 )
{
Paint.SetPen( initialPen.WithAlpha( infinityAlpha ), Paint.PenSize, Paint.PenStyle );
Paint.DrawLine( pointCache.Skip( firstIdxPostCurve ) );
}
pointCache.Clear();
}
}