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:
226
src/draw.c
226
src/draw.c
@@ -619,3 +619,229 @@ akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fill one quarter-circle corner as a triangle fan.
|
||||
*
|
||||
* `SDL_RenderGeometry` colours from its vertices rather than the renderer's
|
||||
* draw colour, so this neither pushes nor pops it.
|
||||
*
|
||||
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
|
||||
* the caller has already checked both.
|
||||
* @param cx Horizontal position of the corner's centre -- the point the
|
||||
* fan radiates from, one radius inside the rectangle.
|
||||
* @param cy Vertical position of the centre.
|
||||
* @param radius Radius of the quarter circle. Assumed positive.
|
||||
* @param start_deg Angle the quarter starts at; it sweeps 90 degrees clockwise
|
||||
* from there.
|
||||
* @param color Colour to fill with.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the fan cannot be drawn.
|
||||
*/
|
||||
static akerr_ErrorContext *fill_corner_fan(akgl_RenderBackend *self, float32_t cx, float32_t cy, float32_t radius, float32_t start_deg, SDL_Color color)
|
||||
{
|
||||
SDL_Vertex vertices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2];
|
||||
int indices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3];
|
||||
SDL_FColor fcolor;
|
||||
float32_t angle = 0.0f;
|
||||
int i = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
fcolor.r = (float)color.r / 255.0f;
|
||||
fcolor.g = (float)color.g / 255.0f;
|
||||
fcolor.b = (float)color.b / 255.0f;
|
||||
fcolor.a = (float)color.a / 255.0f;
|
||||
|
||||
vertices[0].position.x = cx;
|
||||
vertices[0].position.y = cy;
|
||||
vertices[0].color = fcolor;
|
||||
vertices[0].tex_coord.x = 0.0f;
|
||||
vertices[0].tex_coord.y = 0.0f;
|
||||
for ( i = 0; i <= AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
|
||||
angle = (start_deg + ((90.0f / AKGL_DRAW_ROUNDED_RECT_SEGMENTS) * (float)i)) * (SDL_PI_F / 180.0f);
|
||||
vertices[i + 1].position.x = cx + (radius * SDL_cosf(angle));
|
||||
vertices[i + 1].position.y = cy + (radius * SDL_sinf(angle));
|
||||
vertices[i + 1].color = fcolor;
|
||||
vertices[i + 1].tex_coord.x = 0.0f;
|
||||
vertices[i + 1].tex_coord.y = 0.0f;
|
||||
}
|
||||
for ( i = 0; i < AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
|
||||
indices[(i * 3) + 0] = 0;
|
||||
indices[(i * 3) + 1] = i + 1;
|
||||
indices[(i * 3) + 2] = i + 2;
|
||||
}
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_RenderGeometry(
|
||||
self->sdl_renderer,
|
||||
NULL,
|
||||
vertices,
|
||||
AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2,
|
||||
indices,
|
||||
AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
SDL_FRect band;
|
||||
float32_t half = 0.0f;
|
||||
bool pushed = false;
|
||||
bool drawfailed = false;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL rectangle");
|
||||
if ( (rect->w <= 0.0f) || (rect->h <= 0.0f) ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
if ( radius <= 0.0f ) {
|
||||
PASS(errctx, akgl_draw_filled_rect(self, rect, color));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
half = (((rect->w < rect->h) ? rect->w : rect->h) / 2.0f);
|
||||
if ( radius > half ) {
|
||||
radius = half;
|
||||
}
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
pushed = true;
|
||||
|
||||
// The body is three bands: one the full width between the corner rows,
|
||||
// and one between the corners along each of the top and bottom edges.
|
||||
// A radius of exactly half the shorter side leaves one or more of them
|
||||
// with no area, which SDL fills as nothing -- at the limit the whole
|
||||
// shape is the four fans.
|
||||
band.x = rect->x;
|
||||
band.y = rect->y + radius;
|
||||
band.w = rect->w;
|
||||
band.h = rect->h - (radius * 2.0f);
|
||||
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||
drawfailed = true;
|
||||
}
|
||||
band.x = rect->x + radius;
|
||||
band.y = rect->y;
|
||||
band.w = rect->w - (radius * 2.0f);
|
||||
band.h = radius;
|
||||
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||
drawfailed = true;
|
||||
}
|
||||
band.y = rect->y + rect->h - radius;
|
||||
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||
drawfailed = true;
|
||||
}
|
||||
FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + radius, radius, 180.0f, color));
|
||||
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + radius, radius, 270.0f, color));
|
||||
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + rect->h - radius, radius, 0.0f, color));
|
||||
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + rect->h - radius, radius, 90.0f, color));
|
||||
} CLEANUP {
|
||||
if ( pushed == true ) {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *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)
|
||||
{
|
||||
SDL_Vertex vertices[AKGL_DRAW_ARC_MAX_POINTS];
|
||||
int indices[(AKGL_DRAW_ARC_MAX_POINTS - 2) * 3];
|
||||
SDL_FColor fcolor;
|
||||
float32_t span = 0.0f;
|
||||
float32_t inner = 0.0f;
|
||||
float32_t angle = 0.0f;
|
||||
int segments = 0;
|
||||
int base = 0;
|
||||
int i = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_OUTOFBOUNDS, "Arc radius %f is not positive", (double)radius);
|
||||
FAIL_NONZERO_RETURN(errctx, (thickness <= 0.0f), AKERR_OUTOFBOUNDS, "Arc thickness %f is not positive", (double)thickness);
|
||||
span = end_deg - start_deg;
|
||||
if ( span <= 0.0f ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
if ( span > 360.0f ) {
|
||||
span = 360.0f;
|
||||
}
|
||||
inner = radius - thickness;
|
||||
if ( inner < 0.0f ) {
|
||||
inner = 0.0f;
|
||||
}
|
||||
|
||||
// Steps in proportion to the angle swept, so a sliver of arc does not
|
||||
// spend the whole vertex budget and a full circle uses all of it: the
|
||||
// quarter-circle density of AKGL_DRAW_ROUNDED_RECT_SEGMENTS, capped by
|
||||
// what AKGL_DRAW_ARC_MAX_POINTS vertices can carry at two per step.
|
||||
segments = (int)((span / 90.0f) * (float)AKGL_DRAW_ROUNDED_RECT_SEGMENTS) + 1;
|
||||
if ( segments > ((AKGL_DRAW_ARC_MAX_POINTS / 2) - 1) ) {
|
||||
segments = (AKGL_DRAW_ARC_MAX_POINTS / 2) - 1;
|
||||
}
|
||||
|
||||
fcolor.r = (float)color.r / 255.0f;
|
||||
fcolor.g = (float)color.g / 255.0f;
|
||||
fcolor.b = (float)color.b / 255.0f;
|
||||
fcolor.a = (float)color.a / 255.0f;
|
||||
for ( i = 0; i <= segments; i++ ) {
|
||||
angle = (start_deg + ((span / (float)segments) * (float)i)) * (SDL_PI_F / 180.0f);
|
||||
vertices[i * 2].position.x = x + (radius * SDL_cosf(angle));
|
||||
vertices[i * 2].position.y = y + (radius * SDL_sinf(angle));
|
||||
vertices[i * 2].color = fcolor;
|
||||
vertices[i * 2].tex_coord.x = 0.0f;
|
||||
vertices[i * 2].tex_coord.y = 0.0f;
|
||||
vertices[(i * 2) + 1].position.x = x + (inner * SDL_cosf(angle));
|
||||
vertices[(i * 2) + 1].position.y = y + (inner * SDL_sinf(angle));
|
||||
vertices[(i * 2) + 1].color = fcolor;
|
||||
vertices[(i * 2) + 1].tex_coord.x = 0.0f;
|
||||
vertices[(i * 2) + 1].tex_coord.y = 0.0f;
|
||||
}
|
||||
for ( i = 0; i < segments; i++ ) {
|
||||
base = i * 2;
|
||||
indices[(i * 6) + 0] = base;
|
||||
indices[(i * 6) + 1] = base + 2;
|
||||
indices[(i * 6) + 2] = base + 1;
|
||||
indices[(i * 6) + 3] = base + 1;
|
||||
indices[(i * 6) + 4] = base + 2;
|
||||
indices[(i * 6) + 5] = base + 3;
|
||||
}
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_RenderGeometry(
|
||||
self->sdl_renderer,
|
||||
NULL,
|
||||
vertices,
|
||||
(segments + 1) * 2,
|
||||
indices,
|
||||
segments * 6),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
// NULL rect deliberately passes straight through: it is how the clip is
|
||||
// cleared, and the header says so.
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderClipRect(self->sdl_renderer, rect),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user