Add rounded-rect fill, arc stroke and clip-rect drawing primitives

The UI subsystem's render executor needs all three -- clay emits rectangles
with corner radii, per-side borders, and scissor commands -- but they earn
their place in draw.h on the same terms as everything else there: a rounded
panel, a ring gauge and a clipped viewport are things a game wants with or
without a layout engine.

akgl_draw_filled_rounded_rect decomposes into three band fills plus four
quarter-circle triangle fans through SDL_RenderGeometry, with the radius
clamped to half the shorter side. akgl_draw_arc strokes between an outer and
inner radius as one triangle strip, stepping in proportion to the swept angle
under a fixed vertex cap -- no VLAs, unlike clay's own SDL3 reference
renderer. akgl_draw_set_clip wraps SDL_SetRenderClipRect and is the one draw
entry point where a NULL rectangle is legal, because that is how a clip is
cleared.

Pixel-readback tests cover the corner geometry (inside the arc filled,
outside it untouched), the radius clamp, the zero-radius fall-through, ring
and quarter-arc coverage at mid-stroke, sweep bounds, and clip set/clear
round trips.

Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
2026-08-02 10:57:23 -04:00
parent b7ff54b09f
commit 064e6569e8
3 changed files with 507 additions and 0 deletions

View File

@@ -36,6 +36,25 @@
*/
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
/**
* @brief Triangles each corner of akgl_draw_filled_rounded_rect() is built from.
*
* A quarter circle approximated by this many segments is visually round at any
* radius a UI panel plausibly uses; the vertex arrays are fixed at this size,
* so it is a compile-time cost, not a per-call one.
*/
#define AKGL_DRAW_ROUNDED_RECT_SEGMENTS 16
/**
* @brief Most vertices akgl_draw_arc() will spend on one arc.
*
* An arc is a triangle strip between its inner and outer edge, two vertices
* per step, so half of these are positions along the curve. The step count
* scales with the swept angle and is clamped to fit this array -- a full
* circle gets all of them, a sliver gets a few -- rather than allocating.
*/
#define AKGL_DRAW_ARC_MAX_POINTS 64
/**
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
*
@@ -242,4 +261,86 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
/**
* @brief Fill a rectangle whose corners are rounded off with quarter circles.
*
* The body is three axis-aligned fills and the corners are triangle fans
* through `SDL_RenderGeometry` -- #AKGL_DRAW_ROUNDED_RECT_SEGMENTS triangles
* each -- so nothing here is anti-aliased, matching every other primitive in
* this header.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param rect The rectangle, in render-target pixels. Required. A zero or
* negative width or height fills nothing and is not reported.
* @param radius Corner radius in pixels. Zero or negative falls through to
* akgl_draw_filled_rect() -- a square corner is not an error.
* Larger than half the shorter side is clamped to it, which at
* the limit turns a square into a circle rather than folding the
* corners over each other.
* @param color Colour to fill with, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
* `NULL`.
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if a band
* or corner cannot be drawn. A failure partway leaves the shape
* partially drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color);
/**
* @brief Stroke a circular arc of a given thickness between two angles.
*
* Angles are in degrees, 0 pointing along +x (three o'clock) and increasing
* clockwise on screen -- the same convention as the rotation argument the
* render backend's `draw_texture` takes. The stroke runs from the arc's outer
* edge at @p radius inward by @p thickness, drawn as one triangle strip, not
* anti-aliased.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer`.
* @param x Horizontal position of the arc's centre.
* @param y Vertical position of the arc's centre.
* @param radius Outer radius in pixels. Must be positive.
* @param start_deg Angle the arc starts at.
* @param end_deg Angle the arc ends at. At or below @p start_deg nothing is
* drawn and nothing is reported, matching the zero-area
* rectangle fills above. Neither angle is normalized, so an
* arc crossing three o'clock is 350 to 370, not 350 to 10; a
* sweep beyond 360 degrees is clamped to one full turn.
* @param thickness Stroke width in pixels, measured inward from @p radius.
* Must be positive; at or above @p radius the arc becomes a
* filled wedge to the centre.
* @param color Colour to stroke in, including alpha.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p radius or @p thickness is not positive. The
* message reports the offending value.
* @throws AKGL_ERR_SDL If the strip cannot be drawn.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color);
/**
* @brief Restrict every subsequent draw to a rectangle of the render target.
*
* Everything drawn through @p self after this -- primitives here, textures,
* text -- is clipped to @p rect until the restriction is cleared. This is the
* only entry point in this header where a `NULL` rectangle is legal rather
* than an error: it means "clear the restriction", matching what
* `SDL_SetRenderClipRect` does with it, and there is no other value that
* could.
*
* The clip is renderer state, not saved and restored around anything: a
* caller that sets it owns putting it back, the way a `CLEANUP` block puts
* back a draw colour. Leaving it set clips the next frame's world too.
*
* @param self The backend to clip. Required, along with its `sdl_renderer`.
* @param rect Rectangle to clip to, in render-target pixels -- integer, since
* a clip boundary is a pixel boundary. `NULL` clears the clip.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
* @throws AKGL_ERR_SDL If the clip cannot be set or cleared.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect);
#endif //_AKGL_DRAW_H_