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¶
Sectordraws an annular wedge using four geometry fields:StartAngle,EndAngle,InnerRadius,OuterRadius. a full ring is0fto360fwith a non-zeroInnerRadius.- stacking two shapes concentrically requires both to carry
Position = PositionMode.Absolute, Top = 0, Left = 0and their parent to beRelative. without this the flex layout places them side by side. - the fill angle is pure arithmetic inside
Build(). the setter clamps, assigns, and callsRebuild(). the displayed value is always a function of the field - never stale. - geometry changes re-bake the mask texture. use them for discrete state updates, not per-frame animation.
see also¶
- shapes guidance - the full shapes surface: color quirk,
Shapes.Ringcompositor, stacking rules, and when to animate with transforms vs geometry. - build method - how
Rebuild()schedules a freshBuild(), structural diff, and theKeyproperty. - container reference - the full layout and style surface that
Sectordraws a subset from. - panel transforms - rotate, scale, and skew a shape after building it, for animation that avoids geometry re-bake.