Editor/CurveWidgetTransform.cs
namespace AltCurves;

/// <summary>
/// CurveWidgetTransform helps in transforming between curve and widget coordinate systems.
/// It's an important distinction that in "Curve Space" +Y axis is up (larger curve values are above lower curve values), but in "Widget Space" +Y axis is down.
/// CurveRange acts as a sliding window into the curve data, and WidgetSize is the size of the widget we're transforming between.
/// </summary>
public readonly record struct CurveWidgetTransform
{
	/// <summary>
	/// The section of the curve we're transforming between
	/// X axis being time bounds, Y axis being value bounds
	/// </summary>
	public CoordinateRange2D CurveRange { get; init; }

	/// <summary>
	/// The pixel size of the widget we're transforming between
	/// </summary>
	public Vector2 WidgetSize { get; init; }

	/// <summary>
	/// The aspect ratio between pixels per value/pixels per second, for use in proportional widget/curve scaling
	/// </summary>
	public float WidgetCurveAspectRatio => PixelsPerValue / PixelsPerSecond;

	/// <summary>
	/// How many pixels represent one unit of time in the curve's range.
	/// </summary>
	public float PixelsPerSecond => (float)(WidgetSize.x / (CurveRange.MaxX - CurveRange.MinX));

	/// <summary>
	/// How many pixels represent one unit of value in the curve's range.
	/// </summary>
	public float PixelsPerValue => (float)(WidgetSize.y / (CurveRange.MaxY - CurveRange.MinY));

	/// <summary>
	/// Get a CurveWidgetTransform with an updated range translating us by a given input widget-space pixel amount
	/// </summary>
	public readonly CurveWidgetTransform WithTranslatedRange( Vector2 widgetSpaceTranslation )
	{
		var adjustedX = new Vector2( (float)CurveRange.MinX, (float)CurveRange.MaxX ) - widgetSpaceTranslation.x / (double)WidgetSize.x * (CurveRange.MaxX - CurveRange.MinX);
		var adjustedY = new Vector2( (float)CurveRange.MinY, (float)CurveRange.MaxY ) + widgetSpaceTranslation.y / (double)WidgetSize.y * (CurveRange.MaxY - CurveRange.MinY);
		return this with { CurveRange = new( adjustedX.x, adjustedX.y, adjustedY.x, adjustedY.y ) };
	}

	/// <summary>
	/// Get a CurveWidgetTransform zooming the range by zoomAmount based on zoomOrigin (in widget space)
	/// </summary>
	public readonly CurveWidgetTransform WithZoomedRange( Vector2 zoomAmount, Vector2 zoomOriginWidgetSpace )
	{
		var zoomOriginCurveSpace = WidgetToCurvePosition( zoomOriginWidgetSpace );
		return this with
		{
			CurveRange = new()
			{
				MinX = (zoomOriginCurveSpace.x + (CurveRange.MinX - zoomOriginCurveSpace.x) * (double)zoomAmount.x),
				MaxX = (zoomOriginCurveSpace.x + (CurveRange.MaxX - zoomOriginCurveSpace.x) * (double)zoomAmount.x),
				MinY = (zoomOriginCurveSpace.y + (CurveRange.MinY - zoomOriginCurveSpace.y) * (double)zoomAmount.y),
				MaxY = (zoomOriginCurveSpace.y + (CurveRange.MaxY - zoomOriginCurveSpace.y) * (double)zoomAmount.y)
			}
		};
	}

	/// <summary>
	/// Transform a 2d widget position into curve space.
	/// </summary>
	public readonly Vector2 WidgetToCurvePosition( Vector2 position ) => new( WidgetToCurveX( position.x ), WidgetToCurveY( position.y ) );

	/// <summary>
	/// Transform a 2d curve position into widget space.
	/// </summary>
	public readonly Vector2 CurveToWidgetPosition( Vector2 localCurveVert ) => new( CurveToWidgetX( localCurveVert.x ), CurveToWidgetY( localCurveVert.y ) );

	/// <summary>
	/// Transform curve time into widget space.
	/// </summary>
	public readonly float CurveToWidgetX( float x ) => (float)((x - CurveRange.MinX) / (CurveRange.MaxX - CurveRange.MinX) * WidgetSize.x);

	/// <summary>
	/// Transform curve value into widget space.
	/// </summary>
	public readonly float CurveToWidgetY( float y ) => (float)(WidgetSize.y - (y - CurveRange.MinY) / (CurveRange.MaxY - CurveRange.MinY) * WidgetSize.y);

	/// <summary>
	/// Transform widget X position into curve space.
	/// </summary>
	public readonly float WidgetToCurveX( float screenX ) => (float)(CurveRange.MinX + screenX / WidgetSize.x * (CurveRange.MaxX - CurveRange.MinX));

	/// <summary>
	/// Transform widget Y position into curve space.
	/// </summary>
	public readonly float WidgetToCurveY( float screenY ) => (float)(CurveRange.MaxY - screenY / WidgetSize.y * (CurveRange.MaxY - CurveRange.MinY));
}