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:
@@ -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_
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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