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:
180
tests/draw.c
180
tests/draw.c
@@ -628,6 +628,183 @@ akerr_ErrorContext *test_draw_background(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_filled_rounded_rect(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
SDL_FRect rect = { 8.0f, 8.0f, 40.0f, 24.0f };
|
||||
SDL_FRect square = { 40.0f, 40.0f, 16.0f, 16.0f };
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 8.0f, testred),
|
||||
"filling a rounded rectangle");
|
||||
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 28, 20, testred),
|
||||
"the middle of the rounded rectangle is unfilled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 28, 8, testred),
|
||||
"the top edge between the corners is unfilled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 20, testred),
|
||||
"the left edge between the corners is unfilled");
|
||||
// 12,12 is inside the corner's quarter circle (5.7px from its centre
|
||||
// at 16,16); 8,8 and 9,9 are outside it (11.3px and 9.9px). The fan's
|
||||
// triangles are chords of the circle, so they cover nothing beyond it.
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 12, 12, testred),
|
||||
"the inside of the rounded corner is unfilled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testblack),
|
||||
"the square corner pixel was filled despite the rounding");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 9, 9, testblack),
|
||||
"a pixel outside the corner arc was filled");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A radius beyond half the shorter side clamps to it, which on a
|
||||
// square is a circle: centre filled, corners untouched.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &square, 100.0f, testgreen),
|
||||
"filling a rounded rectangle with an oversized radius");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 48, 48, testgreen),
|
||||
"the centre of the clamped-radius square is unfilled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 41, 41, testblack),
|
||||
"clamping the radius still filled the square's corner");
|
||||
|
||||
// Radius zero is the plain fill, corners and all.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 0.0f, testred),
|
||||
"a zero radius falling through to the plain fill");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
|
||||
"a zero radius did not fill the square corner");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_filled_rounded_rect(NULL, &rect, 4.0f, testred),
|
||||
"rounded fill through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_filled_rounded_rect(akgl_renderer, NULL, 4.0f, testred),
|
||||
"rounded fill of a NULL rectangle");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_arc(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
// A full ring: outer radius 20, stroke 4, so mid-stroke is radius 18.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 360.0f, 4.0f, testred),
|
||||
"stroking a full circle");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 50, 32, testred),
|
||||
"the ring is missing at three o'clock");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 50, testred),
|
||||
"the ring is missing at six o'clock");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 14, 32, testred),
|
||||
"the ring is missing at nine o'clock");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testred),
|
||||
"the ring is missing at twelve o'clock");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
|
||||
"the centre of the ring was filled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testblack),
|
||||
"the stroke bled inside its inner radius");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A quarter arc sweeps clockwise from three o'clock to six o'clock;
|
||||
// 44,44 is on its mid-stroke at 45 degrees, twelve o'clock is not in
|
||||
// the sweep at all.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testgreen),
|
||||
"stroking a quarter arc");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 44, 44, testgreen),
|
||||
"the quarter arc is missing at its midpoint");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testblack),
|
||||
"the quarter arc drew outside its sweep");
|
||||
|
||||
// An empty sweep draws nothing and reports nothing.
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 90.0f, 90.0f, 4.0f, testred),
|
||||
"an arc with no sweep");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 0.0f, 0.0f, 90.0f, 4.0f, testred),
|
||||
"an arc with no radius");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, -1.0f, testred),
|
||||
"an arc with a negative thickness");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_arc(NULL, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testred),
|
||||
"an arc through a NULL backend");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_set_clip(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
SDL_Rect clip = { 0, 0, 16, 16 };
|
||||
SDL_FRect everything = { 0.0f, 0.0f, (float)TEST_TARGET_SIZE, (float)TEST_TARGET_SIZE };
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, &clip),
|
||||
"setting the clip rectangle");
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testred),
|
||||
"filling the whole target under a clip");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
|
||||
"the fill did not reach inside the clip rectangle");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
|
||||
"the fill escaped the clip rectangle");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, NULL),
|
||||
"clearing the clip rectangle");
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testgreen),
|
||||
"filling the whole target after clearing the clip");
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testgreen),
|
||||
"clearing the clip did not restore the full target");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_set_clip(NULL, &clip),
|
||||
"clipping through a NULL backend");
|
||||
} CLEANUP {
|
||||
// The clip is renderer state; leaving it set would clip every test
|
||||
// after this one.
|
||||
IGNORE(akgl_draw_set_clip(akgl_renderer, NULL));
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -668,6 +845,9 @@ int main(void)
|
||||
CATCH(errctx, test_draw_preserves_render_draw_color());
|
||||
CATCH(errctx, test_draw_backend_without_a_renderer());
|
||||
CATCH(errctx, test_draw_background());
|
||||
CATCH(errctx, test_draw_filled_rounded_rect());
|
||||
CATCH(errctx, test_draw_arc());
|
||||
CATCH(errctx, test_draw_set_clip());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
|
||||
Reference in New Issue
Block a user