Path.cs
namespace PathTool;

[Title("Linear Path")]
public class Path : Component
{
	public List<PathNode> Nodes { get; set; }
	public int Length => Nodes.Count;
	public PathNode GetNode( int index )
	{
		if ( Nodes == null || Nodes.Count == 0 ) return null;

		return Nodes[index % Nodes.Count];
	}

	/// <summary>
	/// Get position at specified index, decimal points = between points.
	/// </summary>
	public Vector3 GetPosition( float index )
	{
		if ( Nodes == null || !Nodes.Any() ) return default;

		if ( float.IsInteger( index ) )
		{
			return Nodes[(int)index].Position;
		}

		PathNode previous = GetNode( MathX.FloorToInt( index ) );
		PathNode next = GetNode( MathX.CeilToInt( index ) );

		if ( next == null )
		{
			return previous.Position;
		}

		float lerp = index % 1;

		return previous.GetPointBetween( next, lerp );
	}
	public Vector3 GetPositionWorld( float index )
	{
		return Transform.World.PointToWorld( GetPosition( index ) );
	}
	public int GetClosestNode( Vector3 position )
	{
		return Nodes.IndexOf( Nodes.SmallestBy( n => n.Position.DistanceSquared( position ) ) );
	}
	public PathNode GetNext( PathNode node )
	{
		int index = Nodes.IndexOf( node );
		if ( index == -1 ) return null;

		if ( index >= Nodes.Count - 1 ) return Nodes[0];

		return Nodes[index + 1];
	}
	public PathNode GetPrevious( PathNode node )
	{
		int index = Nodes.IndexOf( node );
		if ( index == -1 ) return null;

		if ( index <= 0 ) return Nodes[Nodes.Count - 1];

		return Nodes[index - 1];
	}

	protected override void DrawGizmos()
	{
		if ( Nodes == null || !Nodes.Any() ) return;

		const float NODE_RADIUS = 2.5f;

		const float DETAIL = 0.1f;
		float textSize = 22 * Gizmo.Settings.GizmoScale;
		Vector3 previous = GetPosition( 0 );
		Vector3 point;


		for ( float fraction = DETAIL; fraction <= Nodes.Count; fraction += DETAIL )
		{
			point = GetPosition( fraction );
			Gizmo.Draw.Line( previous, point );
			previous = point;
		}

		for ( int i = 0; i < Nodes.Count; i++ )
		{
			var node = Nodes[i];

			using ( Gizmo.Scope( $"Node{i}", node.Position ) )
			{
				Sphere nodeDisplay = new( Vector3.Zero, NODE_RADIUS );
				Gizmo.Draw.SolidSphere( nodeDisplay.Center, nodeDisplay.Radius );
				Gizmo.Hitbox.Sphere( nodeDisplay );
				if ( Gizmo.IsSelected )
				{
					Gizmo.Draw.ScreenText( $"{i + 1}", Gizmo.Camera.ToScreen( node.Position + Transform.Position ) + Vector2.Up * NODE_RADIUS * 3, size: textSize );
				}

				if ( Gizmo.Pressed.This )
				{
					Gizmo.Select( true, false );
				}
			}
		}
	}
}
[Hide]
public class Path<T> : Path where T : PathNode
{
	public new List<T> Nodes { get; set; }
	public new T GetNode( int index )
	{
		if ( Nodes == null || Nodes.Count == 0) return null;

		return Nodes[index % Nodes.Count];
	}
	public T GetNext( T node )
	{
		var next = base.GetNext( node );
		if ( next is T valid )
			return valid;

		return null;
	}
	public T GetPrevious( T node )
	{
		var previous = base.GetPrevious( node );
		if ( previous is T valid )
			return valid;

		return null;
	}
}