acron.hud

Instrument widgets for game HUDs — the drawing RCSS cannot do.

RmlUi covers the document half of a HUD (panels, lists, gauges, text) and acron.ui ships that machinery. It does not cover the other half: a minimap, a compass tape, a dial, an oscilloscope trace. Web HUDs reach for <canvas> there; RmlUi has no equivalent and its <img> reads files, not live textures.

This module is that missing half, built out of what Acron already has — aspect2d cards, a Texture, and (for Raster) the engine’s acron.raster.RasterPainter:

  • PixelFrame — an authored pixel space on aspect2d, anchored to a window corner, so a HUD can be laid out in the same numbers its design was drawn in. (Every port so far hand-rolled this constant; acronvehicle’s HUD calls it _PX.)

  • Painter — the drawing interface an instrument writes against: lines, rects, polygons, discs, arcs, text. Raster implements it on an RGBA pixel buffer uploaded to a card (the <canvas> this engine does not have; the pixels are painted in C++, acron.raster), and RecordPainter implements it by writing the calls down — so an instrument’s layout can be pinned against its reference with no window, no GPU and no screenshot.

  • LabelPool — pooled ``TextNode``s over a raster. A pixel buffer can draw everything an instrument needs except its type; Acron already has a text engine, so the two are used side by side rather than a font being rasterised by hand.

  • MapView — a baked top-down map on a card, with the visible world rect driven by a texture transform rather than by re-blitting pixels: pan, zoom and (optionally) rotate cost nothing per frame. The mechanism behind both a north-up survey minimap and a heading-up combat radar.

  • relative_bearing() / tape_offset() — the compass-tape mapping from a world bearing to a position along a strip.

Games supply policy: what to draw, in which colours, at what size.

Three engine facts shape the code (../RUNTIME.md rules 4 and 17):

  • Acron re-uploads a WHOLE texture when any of it changes. Instrument rasters are small (a 300-square minimap is 360 kB) so this is cheap, but it is why Raster.upload() is explicit and skipped when nothing was drawn.

  • Acron’s RAM images are BGRA and bottom-row-first. A raster is indexed [y, x] with y growing DOWN the screen, the way every 2D drawing API is, and the flip happens once, on upload.

  • The RmlUi backend does not render transformed subtrees, so anything that has to rotate or scale per frame belongs here rather than in a document.

LabelPool(parent, *[, scale, font, name])

Pooled ``TextNode``s over a raster, in the raster's own pixel frame.

MapView(parent, width, height, *[, name, node])

A baked top-down map, windowed by a texture transform.

Painter()

The drawing calls an instrument makes — raster pixels, y growing DOWN.

PixelFrame(base, *[, authored_height, ...])

An authored pixel space on aspect2d, anchored to a window corner.

Raster(width, height, *[, name])

An RGBA raster you draw into, on a card.

RecordPainter([width, height])

A Painter that writes the calls down instead of drawing them.

Global Functions

hex_chip_png(cache_dir, name: str, rgba, px: int = 64, supersample: int = 4) → str

A pointy-top hexagon chip PNG in cache_dir, created on first ask.

RCSS has no clip-path, so a shaped status chip has to be pixels; RmlUi loads images from files, so a cache directory stands in. Vertices (50%,0) (100%,25%) (100%,75%) (50%,100%) (0,75%) (0,25%), anti-aliased by supersample``² coverage. Returns the file's absolute path, which is what an ``img src wants.

The colour is part of the FILENAME: a cache keyed by name alone keeps serving the old pixels after a palette edit (the documented cache-poisoning class), so the key covers everything that shapes the image.

relative_bearing(bearing: float, heading: float) → float

bearing - heading wrapped into [-180, 180) degrees.

The signed short way round, which is what a compass tape needs and what a naive subtraction gets wrong exactly once per revolution. Dead astern lands on -180 rather than +180, matching the ((b - h + 540) % 360) - 180 form the ports carry over from their sources.

tape_offset(bearing: float, heading: float, span: float, width: float) → float | None

Where bearing falls along a compass tape, or None if off it.

The tape shows span degrees across width pixels; the answer is measured from the tape’s centre, positive to the right. Iterate ABSOLUTE bearings and ask this — stepping over offsets from the current heading instead means no tick ever lands on a multiple of 45 and the cardinal labels never appear.