Report the overlap akgl_collide_rectangles could not see
It asked whether either rectangle enclosed one of the other's four corners -- eight akgl_collide_point_rectangle calls, stopping at the first hit. That is a different question from "do these overlap", and it has the wrong answer for one arrangement: a tall thin rectangle crossing a short wide one overlaps in a plus sign with no corner of either inside the other, and all eight tests said no. A long thin platform crossing a tall thin character is exactly that shape, so this is a shape a 2D game produces, not a curiosity. util.h carried an @note describing it and docs/18-utilities.md had a diagram of it, both under the heading of a limitation rather than a defect, and there was no test for it at all -- nor for full containment, nor for a shared edge. It is four comparisons now, on both axes. `<=` rather than `<` because akgl_collide_point_rectangle is inclusive on all four edges and these two have always agreed that touching counts; a span test written with `<` would have silently changed a contract both the header and the manual state. The test was written first and failed on the cross before the fix went in. Two answers change for a caller upgrading, and both are in the header note and the chapter: - The cross reports `true`, which is the point. - The comparison is in float rather than through akgl_Point's int members, so a sub-pixel overlap is no longer truncated away. A pickup test that was accidentally forgiving by up to a pixel is no longer forgiving. Both tutorials use this for coins and hazards; both still pass. Faster as a side effect rather than a goal, and worth recording because the numbers move a documented budget: 24.9 ns -> 6.1 overlapping, 57.9 -> 6.1 disjoint, and the all-pairs sweep over 64 actors 115 us -> 12.2. The disjoint case gained most because it was the one that ran all eight tests before answering. The three moved rows are re-recorded in PERFORMANCE.md and nothing else is. akgl_rectangle_points is untouched by this change and reads 6.1 in the same run against the 4.0 recorded, so 6 ns is this run's floor and the new figure means "too cheap to measure" rather than "exactly 6.1" -- said in the prose so the next reader does not re-baseline the table around it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
tests/util.c
84
tests/util.c
@@ -247,6 +247,89 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The arrangements corner containment cannot see, and the ones it must keep.
|
||||
*
|
||||
* Eight corner-in-rectangle tests answer "do these overlap" correctly only when
|
||||
* one rectangle encloses a corner of the other. A tall thin rectangle crossing a
|
||||
* short wide one overlaps in a plus sign with no corner inside either, and eight
|
||||
* corner tests all report false. That was documented on akgl_collide_rectangles
|
||||
* as a known limitation rather than fixed, and there was no test for it.
|
||||
*
|
||||
* The three cases below the cross are the ones a rewrite can break while fixing
|
||||
* it: touching edges must keep counting as a collision, because
|
||||
* akgl_collide_point_rectangle is inclusive on all four edges and both headers
|
||||
* promise it; full containment must keep working; and a separation of less than
|
||||
* one pixel must be seen, which the old implementation could not do because it
|
||||
* routed through akgl_Point and truncated float to int.
|
||||
*/
|
||||
akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void)
|
||||
{
|
||||
SDL_FRect tall = { .x = 10.0f, .y = 0.0f, .w = 4.0f, .h = 40.0f };
|
||||
SDL_FRect wide = { .x = 0.0f, .y = 10.0f, .w = 40.0f, .h = 4.0f };
|
||||
SDL_FRect outer = { .x = 0.0f, .y = 0.0f, .w = 64.0f, .h = 64.0f };
|
||||
SDL_FRect inner = { .x = 16.0f, .y = 16.0f, .w = 8.0f, .h = 8.0f };
|
||||
SDL_FRect left = { .x = 0.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
|
||||
SDL_FRect right = { .x = 10.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
|
||||
SDL_FRect near1 = { .x = 0.0f, .y = 0.0f, .w = 10.9f, .h = 10.0f };
|
||||
SDL_FRect near2 = { .x = 10.5f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
|
||||
bool collide = false;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
// The cross. Neither rectangle encloses a corner of the other, and they
|
||||
// plainly overlap in the middle.
|
||||
CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true),
|
||||
"a tall rectangle crossing a wide one was reported as not colliding");
|
||||
|
||||
// The same pair the other way round. The test is symmetric and has to be.
|
||||
CATCH(errctx, akgl_collide_rectangles(&wide, &tall, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true),
|
||||
"the crossing pair was reported as not colliding with the arguments swapped");
|
||||
|
||||
// Full containment, both orders.
|
||||
CATCH(errctx, akgl_collide_rectangles(&outer, &inner, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true), "a contained rectangle was missed");
|
||||
CATCH(errctx, akgl_collide_rectangles(&inner, &outer, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed");
|
||||
|
||||
// A shared edge and nothing more. akgl_collide_point_rectangle is
|
||||
// inclusive on all four edges, so touching counts; util.h and chapter 18
|
||||
// both say so, and a span test written with < rather than <= silently
|
||||
// changes that.
|
||||
CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true),
|
||||
"two rectangles sharing exactly one edge were reported as not colliding");
|
||||
|
||||
// Overlapping by four tenths of a pixel. The corner tests truncated
|
||||
// float to int on the way in, so this read as a shared edge at 10.
|
||||
CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide));
|
||||
TEST_ASSERT(errctx, (collide == true), "a sub-pixel overlap was missed");
|
||||
|
||||
// Separated by four tenths of a pixel. Truncation read this as touching.
|
||||
near2.x = 11.3f;
|
||||
CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide));
|
||||
TEST_ASSERT(errctx, (collide == false), "a sub-pixel gap was reported as a collision");
|
||||
|
||||
// Disjoint on one axis only, which is the case a span test gets wrong
|
||||
// when it forgets to check both.
|
||||
tall.x = 100.0f;
|
||||
CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
|
||||
TEST_ASSERT(errctx, (collide == false),
|
||||
"rectangles separated on x were reported as colliding");
|
||||
tall.x = 10.0f;
|
||||
tall.y = 100.0f;
|
||||
CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
|
||||
TEST_ASSERT(errctx, (collide == false),
|
||||
"rectangles separated on y were reported as colliding");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
{
|
||||
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
|
||||
@@ -465,6 +548,7 @@ int main(void)
|
||||
CATCH(errctx, test_akgl_collide_point_rectangle_logic());
|
||||
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
|
||||
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
||||
CATCH(errctx, test_akgl_collide_rectangles_arrangements());
|
||||
CATCH(errctx, test_akgl_compare_sdl_surfaces_checks_geometry());
|
||||
CATCH(errctx, test_akgl_path_relative_releases_contexts());
|
||||
} CLEANUP {
|
||||
|
||||
Reference in New Issue
Block a user