drawing a gauge

stack a track ring and a fill arc to show a 0-to-1 value as a circular gauge

by the end of this guide you will have a circular gauge component that accepts a float value between 0 and 1 and renders a grey track ring behind a green fill arc. both shapes are Sector blobs stacked concentrically inside a single container. you will know exactly which layout rule makes the overlap work, and you will know why the fill angle is computed in Build() rather than animated through a geometry field.

the two shapes

a circular gauge needs exactly two layers: a full-circle track that shows the whole range, and a partial-circle fill that sweeps from 0 up to the current value. goo provides Sector for both. a Sector draws an annular wedge defined by StartAngle, EndAngle, InnerRadius, and OuterRadius. a full-circle track is just a Sector where EndAngle = 360f.

using Goo;
using Sandbox.UI;

public class GaugeUI : GooPanel<Container>
{
    private float _value = 0.75f;

    protected override Container Build()
    {
        return new Container
        {
            Width    = 140,
            Height   = 140,
            Position = PositionMode.Relative,
            Children =
            {
                new Sector
                {
                    Position    = PositionMode.Absolute, Top = 0, Left = 0,
                    StartAngle  = 0f, EndAngle = 360f,
                    InnerRadius = 0.55f, OuterRadius = 0.8f,
                    BackgroundColor = Color.Gray,
                },
            },
        };
    }
}

press play and drop GaugeUI on a panel. you will see a grey donut ring. that is the track.

why both children must be absolute

add the fill Sector as a sibling of the track. the catch: sibling blobs default to flex flow, not overlap. if you add the fill as a plain sibling without Position = PositionMode.Absolute, the engine places them side by side and each is squashed to half the container width, producing what looks like two tilted ovals. it is not a 3D artifact - the shapes are flat alpha masks. it is a layout problem.

the rule is: any two shapes you want to overlap concentrically both need Position = PositionMode.Absolute, Top = 0, Left = 0, and their parent needs Position = PositionMode.Relative so it is the positioned ancestor they resolve against.

protected override Container Build()
{
    float fillAngle = _value * 360f;

    return new Container
    {
        Width    = 140,
        Height   = 140,
        Position = PositionMode.Relative,
        Children =
        {
            new Sector
            {
                Position    = PositionMode.Absolute, Top = 0, Left = 0,
                StartAngle  = 0f, EndAngle = 360f,
                InnerRadius = 0.55f, OuterRadius = 0.8f,
                BackgroundColor = Color.Gray,
            },
            new Sector
            {
                Position    = PositionMode.Absolute, Top = 0, Left = 0,
                StartAngle  = 0f, EndAngle = fillAngle,
                InnerRadius = 0.55f, OuterRadius = 0.8f,
                BackgroundColor = Color.Green,
            },
        },
    };
}

the track and fill now share exactly the same bounding box. the fill arc sweeps clockwise from 12 o'clock (angle 0) to the fraction of the full circle defined by _value.

make it respond to a value

expose a property so the gauge can be driven from outside. the setter mutates the backing field and calls Rebuild(), which schedules a fresh Build() on the next tick. the fill angle is computed once inside Build() from the current _value, so the rendered state always matches the field.

public class GaugeUI : GooPanel<Container>
{
    private float _value;

    public float Value
    {
        get => _value;
        set
        {
            _value = Math.Clamp( value, 0f, 1f );
            Rebuild();
        }
    }

    protected override Container Build()
    {
        float fillAngle = _value * 360f;

        return new Container
        {
            Width    = 140,
            Height   = 140,
            Position = PositionMode.Relative,
            Children =
            {
                new Sector
                {
                    Position    = PositionMode.Absolute, Top = 0, Left = 0,
                    StartAngle  = 0f, EndAngle = 360f,
                    InnerRadius = 0.55f, OuterRadius = 0.8f,
                    BackgroundColor = Color.Gray,
                },
                new Sector
                {
                    Position    = PositionMode.Absolute, Top = 0, Left = 0,
                    StartAngle  = 0f, EndAngle = fillAngle,
                    InnerRadius = 0.55f, OuterRadius = 0.8f,
                    BackgroundColor = Color.Green,
                },
            },
        };
    }
}

set gauge.Value = 0.3f and the fill arc sweeps to 30 percent. the track stays full-circle behind it.

do not animate geometry fields

geometry fields (StartAngle, EndAngle, InnerRadius, OuterRadius) re-bake the alpha mask texture on every change, so do not drive them per frame. for discrete state changes the Value setter is exactly right - one re-bake per event. for continuous smooth animation, keep the geometry fixed and animate transforms or color instead; see shapes guidance for details.

what just happened

see also