Components/Camera/CameraMarker.cs

A scene Component representing a pre-race camera marker. It stores references to an end target and optional orbit target, computes interpolated camera position/rotation along a linear or orbital arc, and draws gizmos showing the path in the editor.

File Access
namespace Machines.Components;

/// <summary>
/// Pre-race camera viewpoint; interpolates from this transform to EndTarget while facing OrbitTarget.
/// </summary>
public sealed class CameraMarker : Component
{
	/// <summary>
	/// All active camera markers in the current scene.
	/// </summary>
	public static IReadOnlyList<CameraMarker> All => _all;
	private static readonly List<CameraMarker> _all = new();

	/// <summary>
	/// The end position/rotation the camera interpolates toward over the dwell time.
	/// </summary>
	[Property]
	public GameObject EndTarget { get; set; }

	/// <summary>
	/// Optional look-at target during dwell; if null, rotation interpolates between start and end.
	/// </summary>
	[Property]
	public GameObject OrbitTarget { get; set; }

	/// <summary>
	/// Arc blend around OrbitTarget: 0 = straight line, 1 = full orbital arc.
	/// </summary>
	[Property, Range( 0f, 1f )]
	public float ArcAmount { get; set; } = 1f;

	/// <summary>
	/// Extra degrees added to the arc angle; forces a wider swing.
	/// </summary>
	[Property]
	public float ArcAngleOffset { get; set; } = 0f;

	/// <summary>
	/// When true, the arc goes the long way around the orbit target.
	/// </summary>
	[Property]
	public bool InvertArc { get; set; } = false;

	protected override void OnEnabled()
	{
		_all.Add( this );
	}

	protected override void OnDisabled()
	{
		_all.Remove( this );
	}

	/// <summary>
	/// Evaluate position and rotation at normalized time t (0-1).
	/// </summary>
	public void Evaluate( float t, out Vector3 position, out Rotation rotation )
	{
		var startPos = WorldPosition;
		var startRot = WorldRotation;

		var endPos = EndTarget.IsValid() ? EndTarget.WorldPosition : startPos;
		var endRot = EndTarget.IsValid() ? EndTarget.WorldRotation : startRot;

		if ( OrbitTarget.IsValid() )
		{
			var pivot = OrbitTarget.WorldPosition;
			var startOffset = startPos - pivot;
			var endOffset = endPos - pivot;

			// Arc angle: shortest path + offset, optionally inverted
			var startAngle = MathF.Atan2( startOffset.y, startOffset.x );
			var endAngle = MathF.Atan2( endOffset.y, endOffset.x );
			var delta = endAngle - startAngle;
			if ( delta > MathF.PI ) delta -= MathF.PI * 2f;
			if ( delta < -MathF.PI ) delta += MathF.PI * 2f;

			// Invert: take the long way around
			if ( InvertArc )
			{
				if ( delta > 0 ) delta -= MathF.PI * 2f;
				else delta += MathF.PI * 2f;
			}

			delta += MathX.DegreeToRadian( ArcAngleOffset );

			var angle = startAngle + delta * t;
			var radius = MathX.Lerp( startOffset.WithZ( 0 ).Length, endOffset.WithZ( 0 ).Length, t );
			var height = MathX.Lerp( startOffset.z, endOffset.z, t );
			var arcPos = pivot + new Vector3( MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius, height );

			// Blend between linear and arc
			var linearPos = Vector3.Lerp( startPos, endPos, t );
			position = Vector3.Lerp( linearPos, arcPos, ArcAmount );

			var dir = (pivot - position).Normal;
			rotation = Rotation.LookAt( dir, Vector3.Up );
		}
		else
		{
			position = Vector3.Lerp( startPos, endPos, t );
			rotation = Rotation.Slerp( startRot, endRot, t );
		}
	}

	protected override void DrawGizmos()
	{
		Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.6f );
		Gizmo.Draw.LineSphere( Vector3.Zero, 16f );

		if ( !EndTarget.IsValid() )
			return;

		var steps = 20;
		var prev = Vector3.Zero;

		Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.8f );

		for ( int i = 1; i <= steps; i++ )
		{
			var t = (float)i / steps;
			Evaluate( t, out var worldPoint, out _ );
			var localPoint = Transform.World.PointToLocal( worldPoint );

			Gizmo.Draw.Line( prev, localPoint );
			prev = localPoint;
		}

		Gizmo.Draw.Color = Color.Red.WithAlpha( 0.6f );
		Gizmo.Draw.LineSphere( prev, 8f );

		if ( OrbitTarget.IsValid() )
		{
			var targetLocal = Transform.World.PointToLocal( OrbitTarget.WorldPosition );
			Gizmo.Draw.Color = Color.Green.WithAlpha( 0.4f );
			Gizmo.Draw.Line( Vector3.Zero, targetLocal );
		}
	}
}