Track/RacingPath.Debug.cs

Editor/debug partial for RacingPath that draws debug gizmos for optimal/left/right racing lines, path segments, hints, camera spots and shortcuts. It toggles drawing via ConVar booleans and uses Gizmo drawing API to render lines, labels, arrows and spheres.

File Access
namespace Machines.Race;

/// <summary>
/// Debug gizmo drawing for <see cref="RacingPath"/>, toggled via <c>race_debug_*</c> convars.
/// </summary>
public sealed partial class RacingPath
{
	[ConVar( "race_debug_paths" )]
	public static bool DebugPaths { get; set; } = false;

	[ConVar( "race_debug_segments" )]
	public static bool DebugSegments { get; set; } = false;

	[ConVar( "race_debug_hints" )]
	public static bool DebugHints { get; set; } = false;

	[ConVar( "race_debug_cameras" )]
	public static bool DebugCameras { get; set; } = false;

	[ConVar( "race_debug_shortcuts" )]
	public static bool DebugShortcuts { get; set; } = false;

	protected override void DrawGizmos()
	{
		if ( !DebugPaths && !DebugSegments && !DebugHints && !DebugCameras && !DebugShortcuts )
			return;

		Gizmo.Transform = global::Transform.Zero;

		if ( DebugPaths )
		{
			DrawLine( Optimal, Color.Green );
			DrawLine( Left, new Color( 0.3f, 0.6f, 1f ) );
			DrawLine( Right, new Color( 1f, 0.4f, 0.2f ) );
		}

		if ( DebugSegments )
		{
			DrawSegments();
		}

		if ( DebugHints )
		{
			DrawHints();
		}

		if ( DebugCameras )
		{
			DrawCameraSpots();
		}

		if ( DebugShortcuts )
		{
			DrawShortcuts();
		}
	}

	/// <summary>
	/// Draw the optimal line color-coded by segment type, with boundary labels.
	/// </summary>
	private void DrawSegments()
	{
		if ( Optimal == null || !Optimal.IsValid || Optimal.Segments.Count == 0 )
			return;

		const int subdivisions = 6;
		const float labelHeight = 40f;
		const float dashLength = 30f;

		for ( int i = 0; i < Optimal.Points.Count; i++ )
		{
			var dist = i < Optimal.CumulativeDistances.Count ? Optimal.CumulativeDistances[i] : 0f;
			var seg = Optimal.GetSegmentAtDistance( dist );

			var color = GetSegmentColor( seg );
			Gizmo.Draw.Color = color;
			Gizmo.Draw.LineThickness = seg?.IsGap == true ? 1f : 2f;

			var prev = TrackSpline.GetPoint( Optimal.Points, i, 0f );

			for ( int s = 1; s <= subdivisions; s++ )
			{
				var t = s / (float)subdivisions;
				var curr = TrackSpline.GetPoint( Optimal.Points, i, t );

				// For gaps, draw dashed: skip every other sub-segment
				if ( seg?.IsGap == true )
				{
					var subDist = dist + (Optimal.CumulativeDistances[Math.Min( i + 1, Optimal.CumulativeDistances.Count - 1 )] - dist) * t;
					var phase = (subDist % dashLength) / dashLength;
					if ( phase < 0.5f )
						Gizmo.Draw.Line( prev, curr );
				}
				else
				{
					Gizmo.Draw.Line( prev, curr );
				}

				prev = curr;
			}
		}

		// Labels at segment boundaries
		foreach ( var seg in Optimal.Segments )
		{
			var pos = Optimal.GetPointAtDistance( seg.StartDistance );
			var label = seg.IsGap ? "GAP" : $"{seg.Type}";
			if ( !string.IsNullOrEmpty( seg.SourceLabel ) && !seg.IsGap )
				label += $" ({seg.SourceLabel})";

			Gizmo.Draw.Color = GetSegmentColor( seg );
			Gizmo.Draw.Text( label, new Transform( pos + Vector3.Up * labelHeight ) );

			// Vertical marker at boundary
			Gizmo.Draw.Line( pos, pos + Vector3.Up * labelHeight * 0.8f );
		}
	}

	private static Color GetSegmentColor( PathSegmentInfo seg )
	{
		if ( seg == null )
			return Color.Gray;

		if ( seg.IsGap )
			return new Color( 1f, 0.2f, 0.2f ); // Red

		return seg.Type switch
		{
			SplineType.Road => new Color( 0.2f, 0.9f, 0.3f ),     // Green
			SplineType.Bridge => new Color( 0.2f, 0.85f, 0.9f ),  // Cyan
			SplineType.OffRoad => new Color( 0.9f, 0.75f, 0.2f ), // Yellow
			SplineType.Tunnel => new Color( 0.7f, 0.3f, 0.9f ),   // Purple
			_ => Color.White
		};
	}

	private void DrawLine( RacingLine line, Color color )
	{
		if ( line == null || !line.IsValid )
			return;

		Gizmo.Draw.Color = color;

		const int subdivisions = 6;

		for ( int i = 0; i < line.Points.Count; i++ )
		{
			var prev = TrackSpline.GetPoint( line.Points, i, 0f );

			for ( int s = 1; s <= subdivisions; s++ )
			{
				var t = s / (float)subdivisions;
				var curr = TrackSpline.GetPoint( line.Points, i, t );
				Gizmo.Draw.Line( prev, curr );
				prev = curr;
			}

			// Direction arrow every 10 waypoints
			if ( i % 10 == 0 )
			{
				var pos = line.Points[i];
				var nextIdx = (i + 1) % line.Points.Count;
				var dir = (line.Points[nextIdx] - pos).Normal;
				var arrowEnd = pos + dir * 20f;
				Gizmo.Draw.Arrow( pos + Vector3.Up * 10f, arrowEnd + Vector3.Up * 10f, 4f, 2f );
			}
		}
	}

	/// <summary>
	/// Draw path hints as color-coded vertical bars (green=grip, yellow=reduced, red=low/airborne).
	/// </summary>
	private void DrawHints()
	{
		if ( Optimal == null || !Optimal.IsValid || Optimal.Hints.Count == 0 )
			return;

		const float barHeight = 30f;
		var step = RacingLine.CurvatureStep;

		for ( int i = 0; i < Optimal.Hints.Count; i++ )
		{
			var hint = Optimal.Hints[i];
			var d = i * step;
			var pos = Optimal.GetPointAtDistance( d );

			// Friction: green (1.0) -> yellow (0.5) -> red (0.0)
			var color = hint.Friction > 0.7f
				? Color.Lerp( new Color( 1f, 1f, 0f ), new Color( 0.2f, 1f, 0.3f ), (hint.Friction - 0.7f) / 0.3f )
				: Color.Lerp( new Color( 1f, 0.1f, 0.1f ), new Color( 1f, 1f, 0f ), hint.Friction / 0.7f );

			if ( hint.Flags.HasFlag( PathHintFlags.Airborne ) )
				color = new Color( 0.5f, 0.5f, 1f ); // Blue for airborne

			if ( hint.Flags.HasFlag( PathHintFlags.Corner ) )
				color = new Color( 1f, 0.6f, 0.1f ); // Orange for corners

			Gizmo.Draw.Color = color;

			// Bar height scales with friction
			var height = barHeight * MathF.Max( 0.2f, hint.Friction );
			Gizmo.Draw.Line( pos, pos + Vector3.Up * height );

			// Flag labels at notable points (skip every-sample to reduce noise)
			if ( hint.Flags != PathHintFlags.None && i % 3 == 0 )
			{
				var label = "";
				if ( hint.Flags.HasFlag( PathHintFlags.LowGrip ) ) label += "LOW ";
				if ( hint.Flags.HasFlag( PathHintFlags.Airborne ) ) label += "AIR ";
				if ( hint.Flags.HasFlag( PathHintFlags.Narrow ) ) label += "NAR ";
				if ( hint.Flags.HasFlag( PathHintFlags.SlowZone ) ) label += "SLOW ";

				Gizmo.Draw.Text( label.Trim(), new Transform( pos + Vector3.Up * (barHeight + 10f) ) );
			}
		}
	}

	/// <summary>
	/// Draw baked camera spots as spheres with lines to the track point they watch.
	/// </summary>
	private void DrawCameraSpots()
	{
		if ( CameraSpots.Count == 0 )
			return;

		for ( int i = 0; i < CameraSpots.Count; i++ )
		{
			var spot = CameraSpots[i];
			var trackPoint = Optimal.GetPointAtDistance( spot.Distance );

			// Camera sphere
			Gizmo.Draw.Color = new Color( 1f, 0.85f, 0.1f ); // Gold
			Gizmo.Draw.LineSphere( spot.Position, 8f );

			// Line to track point
			Gizmo.Draw.Color = new Color( 1f, 0.85f, 0.1f, 0.4f );
			Gizmo.Draw.Line( spot.Position, trackPoint );

			// Label
			Gizmo.Draw.Color = Color.White;
			Gizmo.Draw.Text( $"CAM {i}", new Transform( spot.Position + Vector3.Up * 12f ) );
		}
	}

	/// <summary>
	/// Draw baked shortcut paths with entry/exit markers.
	/// </summary>
	private void DrawShortcuts()
	{
		if ( Shortcuts.Count == 0 || Optimal == null || !Optimal.IsValid )
			return;

		for ( int s = 0; s < Shortcuts.Count; s++ )
		{
			var shortcut = Shortcuts[s];
			var color = new Color( 1f, 0.2f, 1f ); // Magenta

			// Shortcut path
			Gizmo.Draw.Color = color;
			Gizmo.Draw.LineThickness = 3f;
			for ( int i = 0; i < shortcut.Points.Count - 1; i++ )
				Gizmo.Draw.Line( shortcut.Points[i], shortcut.Points[i + 1] );

			// Waypoint spheres
			Gizmo.Draw.Color = color.WithAlpha( 0.6f );
			foreach ( var pt in shortcut.Points )
				Gizmo.Draw.LineSphere( pt, 4f );

			// Entry marker
			var entryPoint = Optimal.GetPointAtDistance( shortcut.EntryDistance );
			Gizmo.Draw.Color = new Color( 0.2f, 1f, 0.2f );
			Gizmo.Draw.LineSphere( shortcut.Points[0], 10f );
			Gizmo.Draw.Line( shortcut.Points[0], entryPoint );
			Gizmo.Draw.Text( $"SC{s} ENTRY", new Transform( shortcut.Points[0] + Vector3.Up * 20f ) );

			// Exit marker
			var exitPoint = Optimal.GetPointAtDistance( shortcut.ExitDistance );
			Gizmo.Draw.Color = new Color( 1f, 0.3f, 0.2f );
			Gizmo.Draw.LineSphere( shortcut.Points[^1], 10f );
			Gizmo.Draw.Line( shortcut.Points[^1], exitPoint );
			Gizmo.Draw.Text( $"SC{s} EXIT", new Transform( shortcut.Points[^1] + Vector3.Up * 20f ) );

			// Direction arrows
			Gizmo.Draw.Color = color;
			for ( int i = 0; i < shortcut.Points.Count - 1; i += Math.Max( 1, shortcut.Points.Count / 5 ) )
			{
				var from = shortcut.Points[i];
				var to = shortcut.Points[Math.Min( i + 1, shortcut.Points.Count - 1 )];
				var dir = (to - from).Normal;
				var mid = (from + to) * 0.5f;
				Gizmo.Draw.Line( mid, mid + dir * 15f );
			}
		}
	}
}