Demos/Compass/CompassMath.cs
using System;
using System.Collections.Generic;
namespace Sandbox.Compass;
// Pure compass angle math. No s&box dependencies so it is unit-testable.
public static class CompassMath
{
// Folds any degree value into [0, 360).
public static float Normalize360(float deg)
{
deg %= 360f;
if (deg < 0f) deg += 360f;
return deg;
}
// Shortest signed angle from heading to mark, in (-180, 180]; makes the 360-to-0 wrap seamless.
public static float SignedDelta(float headingDeg, float markDeg)
{
float d = Normalize360(markDeg - headingDeg);
if (d > 180f) d -= 360f;
return d;
}
// 8-point label rounded to the nearest 45 deg; 0 = N, increasing clockwise.
public static string CardinalLabel(float deg)
{
int idx = ((int)MathF.Round(Normalize360(deg) / 45f)) % 8;
return idx switch
{
0 => "N", 1 => "NE", 2 => "E", 3 => "SE",
4 => "S", 5 => "SW", 6 => "W", 7 => "NW",
_ => "",
};
}
// One strip mark: angle, XNorm (0 at center caret, +/-1 at window edges), cardinal at 45 deg steps.
public readonly record struct Mark(float AngleDeg, float XNorm, bool IsCardinal);
// Marks inside the visible arc [heading - windowDeg/2, heading + windowDeg/2].
public static List<Mark> VisibleMarks(float headingDeg, float windowDeg, float stepDeg)
{
var marks = new List<Mark>();
float half = windowDeg * 0.5f;
int count = (int)MathF.Round(360f / stepDeg);
for (int i = 0; i < count; i++)
{
float a = Normalize360(i * stepDeg);
float d = SignedDelta(headingDeg, a);
if (MathF.Abs(d) <= half)
{
int deg = (int)MathF.Round(a);
bool cardinal = deg % 45 == 0;
marks.Add(new Mark(a, d / half, cardinal));
}
}
return marks;
}
}