Demos/Compass/CompassView.cs
using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Sandbox.Compass;

// Scrolling compass strip. Reads look-yaw each Tick; emits the band content (host anchors it).
public sealed class CompassView
{
    const float WindowDeg      = 120f;
    const float StepDeg        = 15f;
    const float BandWidth      = 520f;
    const float BandHeight     = 40f;
    const float TopPx          = 24f;
    const float YawEpsilon     = 0.05f;
    // Tunable: s&box yaw is CCW-positive; sign so turning right scrolls the strip left.
    const float DirectionSign  = -1f;
    // Tunable: world yaw that should read as North.
    const float NorthOffsetDeg = 0f;

    float _heading = float.NaN;
    bool  _dirty   = true;
    void Invalidate() => _dirty = true;

    public bool Tick( Scene? scene, float dt )
    {
        var cam = scene?.Camera;
        if ( cam is not null )
        {
            float raw = cam.WorldRotation.Angles().yaw;
            float yaw = CompassMath.Normalize360( raw * DirectionSign - NorthOffsetDeg );
            if ( float.IsNaN( _heading ) ||
                 MathF.Abs( CompassMath.SignedDelta( _heading, yaw ) ) > YawEpsilon )
            {
                _heading = yaw;
                Invalidate();
            }
        }
        bool d = _dirty; _dirty = false; return d;
    }

    public Container Build()
    {
        var band = new Container
        {
            Position        = PositionMode.Relative,
            Top             = TopPx,
            Width           = BandWidth,
            Height          = BandHeight,
            BackgroundColor = Color.Black.WithAlpha( 0.35f ),
            BorderRadius    = 6f,
            Overflow        = OverflowMode.Visible,
            PointerEvents   = PointerEvents.None,
        };

        if ( float.IsNaN( _heading ) )
            return band;

        float halfWidth = BandWidth * 0.5f;
        foreach ( var mark in CompassMath.VisibleMarks( _heading, WindowDeg, StepDeg ) )
        {
            float x = halfWidth + mark.XNorm * halfWidth;
            float opacity = 1f - MathF.Abs( mark.XNorm );
            band.Children.Add( MarkView.Build( mark, x, BandHeight, opacity ) );
        }

        // Fixed center caret marking current heading.
        band.Children.Add( new Container
        {
            Key            = "caret",
            Position       = PositionMode.Absolute,
            Top            = -6f,
            Left           = halfWidth - 6f,
            Width          = 12f,
            Height         = 12f,
            JustifyContent = Justify.Center,
            AlignItems     = Align.Center,
            FontColor      = Color.White,
            Children       = { new Text( "▼" ) },
        } );

        return band;
    }
}