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:
2026-08-01 23:12:01 -04:00
parent 943bf4324e
commit 842ef75ddf
6 changed files with 158 additions and 75 deletions

View File

@@ -53,36 +53,44 @@ drawn with a non-zero `angle` still collides as its unrotated box.
**Edges count as touching.** A point exactly on a boundary is inside; two rectangles sharing
an edge and no area overlap. Both comparisons are `>=` and `<=`.
**Coordinates are truncated from `float` to `int` on the way in.** A rectangle at
`x = 10.9` has its corners at 10. That is right for tile-grid work and wrong for sub-pixel
work; if you need the latter, do not round-trip through `akgl_rectangle_points`.
**`akgl_rectangle_points` truncates from `float` to `int`.** A rectangle at `x = 10.9` has
its corners at 10. That is right for tile-grid work and wrong for sub-pixel work; if you
need the latter, do not round-trip through it. **`akgl_collide_rectangles` no longer does**
— it compares the spans in `float` directly, so a sub-pixel overlap registers.
**`z` is carried and never used.** `akgl_Point` has one, `akgl_rectangle_points` never
writes it, and neither collision test reads it. These are 2D tests.
### The arrangement it misses
### The arrangement it used to miss
`akgl_collide_rectangles` works corner by corner: it tests each rectangle's four corners
against the other, eight tests, stopping at the first hit. Testing both directions is what
catches one rectangle wholly inside the other, which has no corner in its neighbour.
**It still misses the cross.** A tall thin rectangle crossing a short wide one overlaps
without either enclosing a corner of the other, and both are reported as not colliding:
`akgl_collide_rectangles` compares the two rectangles' spans on both axes. Until 0.8.0 it
asked a different question — whether either rectangle enclosed one of the other's four
corners, eight tests, stopping at the first hit — and that question has the wrong answer for
one arrangement:
```text
+---+ +---+
| | | |
+---|---|---+ +---+---+---+ <- overlaps, but
| | | | is | | | | no corner of
| | | | was | | | | no corner of
+---|---|---+ reported +---+---+---+ either is inside
| | as | | the other
+---+ "no hit" +---+
```
The fix is an edge-span comparison instead of a corner-containment one — `r1.x < r2.x + r2.w
&& r2.x < r1.x + r1.w` on both axes — and it is not made here. If your game can produce that
shape, and a long thin platform crossing a tall thin character is exactly that shape, test
for it yourself. Recorded as an `@note` on the function.
A tall thin rectangle crossing a short wide one overlaps in a plus sign with no corner of
either inside the other, so all eight tests said no — and a long thin platform crossing a
tall thin character is exactly that shape. The header carried a `@note` describing it rather
than a fix.
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
`<=` rather than `<` because [`akgl_collide_point_rectangle`](#collision-helpers) is
inclusive on all four edges and these two have always agreed: touching counts.
**If you are upgrading from 0.7.x, two answers change.** The cross now reports `true`, which
is the point. And because the comparison no longer round-trips through `akgl_Point`'s `int`
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
This is separate from the fact that **nothing in the library calls these during a frame.**
`akgl_physics_simulate` never calls `collide`, and `akgl_physics_arcade_collide` raises