shapes source

← shapes · API reference · version 0.5.4

3 file(s), 471 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.

Public items

A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.

ItemDefinedWorked exampleCall sites
Rect struct9—28 48 75 135 145 169 … (40 total)
Circle struct17—35 48 26 27 28 35 … (15 total)
rects_overlap fn28SHP-00176 147 14 15 22 82 … (14 total)
circles_overlap fn35SHP-00129 30 39 41
rect_circle_overlap fn48SHP-00338 39 40 47 121 125 … (7 total)
Overlap struct62—75 76 79 93 95
rect_overlap_depth fn75SHP-00246 51 76
aabb_overlap fn85SHP-00156 58 33 117
aabb_depth_x fn98SHP-00292 93 96
aabb_depth_y fn109SHP-00294 95 98
Proxy struct134—145 157 169 191 212 74 … (10 total)
proxy_hits fn145—159 83 86 102 110
proxies_overlap fn157—123 125
bounds fn169—82 86 89 112 113 122
area fn191—214 92 111
overshoot fn212—96 97 98

src/shapes.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// 2D collision detection primitives.  Pure geometry — no drawing or
// rendering dependencies; drawing is the `graphics` library's.

// ── G4: 2D Collision Detection ──────────────────────────────────────

/// Axis-aligned bounding box for 2D collision.
pub struct Rect {
  pub rx: float,
  pub ry: float,
  pub rw: float,
  pub rh: float,
}

/// Circle shape for 2D collision.
pub struct Circle {
  pub cx: float,
  pub cy: float,
  pub cr: float,
}

/// Test whether two axis-aligned rectangles overlap.
/// The comparison is STRICT, so rectangles sharing an edge do NOT overlap.
// Example: @SHP-001 — touching is not overlapping, and what that buys: a row of
// abutting tiles is quiet, and a mover pushed out by exactly its penetration
// depth is done.
pub fn rects_overlap(a: const Rect, b: const Rect) -> boolean {
  a.rx < b.rx + b.rw && a.rx + a.rw > b.rx && a.ry < b.ry + b.rh && a.ry + a.rh > b.ry
}

/// Test whether two circles overlap.
/// Strict, like `rects_overlap`: tangent circles do NOT overlap.
// Example: @SHP-001 — the shared strict boundary across all three predicates.
pub fn circles_overlap(a: const Circle, b: const Circle) -> boolean {
  co_dx = a.cx - b.cx;
  co_dy = a.cy - b.cy;
  co_dist_sq = co_dx * co_dx + co_dy * co_dy;
  co_r_sum = a.cr + b.cr;
  co_dist_sq < co_r_sum * co_r_sum
}

/// Test whether a rectangle and a circle overlap.
/// A true circle test — the circle's bounding box is NOT a substitute for it, and
/// a circle wholly inside the rectangle counts as overlapping.
// Example: @SHP-003 — where the box approximation disagrees (every corner), and
// why containment answers true.
pub fn rect_circle_overlap(r: const Rect, c: const Circle) -> boolean {
  // Find the closest point on the rectangle to the circle center
  rc_closest_x = c.cx;
  if rc_closest_x < r.rx { rc_closest_x = r.rx; }
  if rc_closest_x > r.rx + r.rw { rc_closest_x = r.rx + r.rw; }
  rc_closest_y = c.cy;
  if rc_closest_y < r.ry { rc_closest_y = r.ry; }
  if rc_closest_y > r.ry + r.rh { rc_closest_y = r.ry + r.rh; }
  rc_dx = c.cx - rc_closest_x;
  rc_dy = c.cy - rc_closest_y;
  rc_dx * rc_dx + rc_dy * rc_dy < c.cr * c.cr
}

/// Overlap depth between two rectangles on each axis.
pub struct Overlap {
  pub ox: float,
  pub oy: float,
}

/// Compute the overlap depth between two rectangles on each axis: how far one must
/// move along that axis to stop overlapping, the smaller of the two ways out — not
/// how much of it lies inside the other.
/// Both depths are UNSIGNED distances — the caller derives the direction from the
/// relative positions.  Returns (0.0, 0.0) when they don't overlap.
/// Useful for bounce direction: if ox < oy → horizontal bounce.
// Example: @SHP-002 — the sign is the caller's to work out (applying it the wrong
// way pushes deeper), and the smaller axis is the minimal correction.
pub fn rect_overlap_depth(a: const Rect, b: const Rect) -> Overlap {
  if !rects_overlap(a, b) { return Overlap { ox: 0.0, oy: 0.0 }; }
  od_x = min(a.rx + a.rw - b.rx, b.rx + b.rw - a.rx);
  od_y = min(a.ry + a.rh - b.ry, b.ry + b.rh - a.ry);
  Overlap { ox: od_x, oy: od_y }
}

/// Test AABB overlap using raw coordinates (no struct allocation — safe in loops).
/// Same strict boundary as `rects_overlap`.
// Example: @SHP-001 — touching is not overlapping, raw form included.
pub fn aabb_overlap(ax: float, ay: float, aw: float, ah: float,
                    bx: float, by: float, bw: float, bh: float) -> boolean {
  ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by
}

/// Compute the AABB overlap depth on the X axis using raw coordinates —
/// `aabb_depth_y` is the other half of the pair, and each call re-runs the overlap
/// test on its own.  Unsigned, like `rect_overlap_depth`.  `0.0` means the two do
/// not overlap AT ALL (a real overlap always penetrates by strictly more than
/// zero), not "they meet with no penetration on this axis".
/// Safe in loops (no struct allocation).
// Example: @SHP-002 — the depth pair, its 0.0 sentinel, and how a caller turns it
// into a correction.
pub fn aabb_depth_x(ax: float, ay: float, aw: float, ah: float,
                    bx: float, by: float, bw: float, bh: float) -> float {
  if !(ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by) { return 0.0; }
  min(ax + aw - bx, bx + bw - ax)
}

/// Compute the AABB overlap depth on the Y axis using raw coordinates — the other
/// half of the `aabb_depth_x` pair, with the same unsigned `0.0`-is-no-overlap
/// contract.  Safe in loops (no struct allocation).
// Example: @SHP-002 — the depth pair, its 0.0 sentinel, and how a caller turns it
// into a correction.
pub fn aabb_depth_y(ax: float, ay: float, aw: float, ah: float,
                    bx: float, by: float, bw: float, bh: float) -> float {
  if !(ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by) { return 0.0; }
  min(ay + ah - by, by + bh - ay)
}

// ── A derived collision proxy ───────────────────────────────────────

/// A shape approximated by a SET of boxes rather than one.
///
/// A sprite's silhouette is rarely a rectangle, and a single bounding box around
/// a diagonal or an L is mostly empty — a mover well clear of the art still reads
/// as a hit.  A handful of boxes tracks the silhouette closely enough to fix that
/// while staying axis-aligned, so every test below is still the cheap one.
///
/// The contract a producer owes is CONTAINMENT: every box together must cover
/// everything solid, so substituting the proxy for the art can only ever over-
/// report a hit, never miss one. A system validated against the proxy therefore
/// stays valid when the art changes. Containment alone is satisfied by a
/// screen-sized box, which is why a producer also bounds the OVERSHOOT — see
/// `overshoot`.
///
/// Measured over a 36-sprite corpus (@PLN146 F7a): 16 boxes banded along the
/// tighter axis beat a convex hull on mean, median and worst case, and need no
/// shape kind this library does not already have.
pub struct Proxy {
  pub boxes: vector<Rect>,
}

/// Does anything in the proxy overlap `r`?
///
/// The set test every consumer would otherwise write, with the same STRICT
/// boundary as `rects_overlap` — a box sharing an edge does not overlap.  An
/// EMPTY proxy answers false: nothing solid was derived, so nothing can be hit,
/// which is the answer that keeps a sprite with no opaque texels from colliding
/// with the world.
pub fn proxy_hits(self: const Proxy, r: const Rect) -> boolean {
  for b in self.boxes {
    if rects_overlap(b, r) { return true }
  }
  false
}

/// Do two proxies overlap anywhere?
///
/// Pairwise, because a proxy is small by construction — 16 boxes against 16 is
/// 256 rect tests in the worst case and stops at the first hit.  Cull with the
/// cheap `bounds()` box first when both are large.
pub fn proxies_overlap(self: const Proxy, other: const Proxy) -> boolean {
  for b in self.boxes {
    if other.proxy_hits(b) { return true }
  }
  false
}

/// The single box containing every box in the proxy — the AABB it replaces.
///
/// Useful as a broad-phase cull before `proxies_overlap`, and as the denominator
/// when judging whether the proxy earned its extra boxes.  An empty proxy answers
/// a zero-size box at the origin.
pub fn bounds(self: const Proxy) -> Rect {
  if len(self.boxes) == 0 { return Rect{rx: 0.0, ry: 0.0, rw: 0.0, rh: 0.0} }
  first = self.boxes[0]?;
  bx0 = first.rx;
  by0 = first.ry;
  bx1 = first.rx + first.rw;
  by1 = first.ry + first.rh;
  for b in self.boxes {
    if b.rx < bx0 { bx0 = b.rx; }
    if b.ry < by0 { by0 = b.ry; }
    if b.rx + b.rw > bx1 { bx1 = b.rx + b.rw; }
    if b.ry + b.rh > by1 { by1 = b.ry + b.rh; }
  }
  Rect{rx: bx0, ry: by0, rw: bx1 - bx0, rh: by1 - by0}
}

/// Total area of the proxy's boxes.
///
/// Bands from one derivation do not overlap, so for a banded proxy this is the
/// covered area exactly.  A proxy whose boxes DO overlap counts the shared part
/// twice, which overstates the area and therefore the overshoot — the honest
/// direction for a bound.
pub fn area(self: const Proxy) -> float {
  pa_total = 0.0;
  for b in self.boxes { pa_total += b.rw * b.rh; }
  pa_total
}

/// How much bigger the proxy is than the thing it stands for, as a fraction.
///
/// `solid` is the area actually covered by art — the opaque texel count for a
/// sprite. `0.0` is exact, `1.0` is twice the size. This is the number a packer
/// gates on, and the reason containment alone is not the contract: a
/// screen-sized box contains everything and is useless.
///
/// A `solid` of zero or less answers `0.0` rather than dividing: there is nothing
/// to be a multiple OF, and a null here would propagate into a comparison that
/// then silently passes.
///
/// The trailing `?` is not redundant with that guard.  A divide types `float?`
/// whatever the code above it proves, so without the discharge this returns a
/// null into a non-null `float` — which is the very failure the guard is there to
/// prevent, arriving by the other door.
pub fn overshoot(self: const Proxy, solid: float) -> float {
  if solid <= 0.0 { return 0.0 }
  ((self.area() - solid) / solid)?
}

tests/shapes.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// Tests for the shapes library — pure-geometry collision detection.
// Drawing tests (bordered_rect / diamond / etc.) live in the
// graphics library now.
use shapes::*;

// ── G4: Collision detection tests ───────────────────────────────────

fn test_rects_overlap() {
  a1 = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  b1 = shapes::Rect { rx: 5.0, ry: 5.0, rw: 10.0, rh: 10.0 };
  c1 = shapes::Rect { rx: 20.0, ry: 20.0, rw: 5.0, rh: 5.0 };
  assert(shapes::rects_overlap(a1, b1), "overlapping rects");
  assert(!shapes::rects_overlap(a1, c1), "non-overlapping rects");
}

fn test_rects_touching_edge() {
  // Touching but not overlapping (edge-to-edge)
  r_left = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  r_right = shapes::Rect { rx: 10.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  assert(!shapes::rects_overlap(r_left, r_right), "edge-touching rects should not overlap");
}

fn test_circles_overlap() {
  ca = shapes::Circle { cx: 0.0, cy: 0.0, cr: 5.0 };
  cb = shapes::Circle { cx: 8.0, cy: 0.0, cr: 5.0 };
  cc = shapes::Circle { cx: 20.0, cy: 0.0, cr: 3.0 };
  assert(shapes::circles_overlap(ca, cb), "overlapping circles");
  assert(!shapes::circles_overlap(ca, cc), "non-overlapping circles");
}

fn test_rect_circle_overlap() {
  rc_r = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  rc_inside = shapes::Circle { cx: 5.0, cy: 5.0, cr: 2.0 };
  rc_corner = shapes::Circle { cx: 12.0, cy: 12.0, cr: 4.0 };
  rc_far = shapes::Circle { cx: 20.0, cy: 20.0, cr: 2.0 };
  assert(shapes::rect_circle_overlap(rc_r, rc_inside), "circle inside rect");
  assert(shapes::rect_circle_overlap(rc_r, rc_corner), "circle overlaps corner");
  assert(!shapes::rect_circle_overlap(rc_r, rc_far), "circle far from rect");
}

fn test_rect_overlap_depth() {
  od_a = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  od_b = shapes::Rect { rx: 7.0, ry: 8.0, rw: 10.0, rh: 10.0 };
  od = shapes::rect_overlap_depth(od_a, od_b);
  assert(od.ox > 2.9 && od.ox < 3.1, "overlap x ~3.0");
  assert(od.oy > 1.9 && od.oy < 2.1, "overlap y ~2.0");
  // Non-overlapping returns (0,0)
  od_c = shapes::Rect { rx: 20.0, ry: 20.0, rw: 5.0, rh: 5.0 };
  od2 = shapes::rect_overlap_depth(od_a, od_c);
  assert(od2.ox == 0.0 && od2.oy == 0.0, "no overlap returns zero");
}

fn test_aabb_overlap() {
  assert(shapes::aabb_overlap(0.0, 0.0, 10.0, 10.0, 5.0, 5.0, 10.0, 10.0),
         "overlapping aabbs");
  assert(!shapes::aabb_overlap(0.0, 0.0, 10.0, 10.0, 20.0, 20.0, 5.0, 5.0),
         "non-overlapping aabbs");
}

fn test_clamp() {
  // clamp is a stdlib builtin now (a lib must not redefine it); call it directly.
  assert(clamp(5.0, 0.0, 10.0) == 5.0, "in-range clamp");
  assert(clamp(-1.0, 0.0, 10.0) == 0.0, "below-range clamp");
  assert(clamp(15.0, 0.0, 10.0) == 10.0, "above-range clamp");
}

// A proxy is a SET of boxes, and the point of it is disagreeing with the single
// box where the single box is wrong.  The shape here is @PLN146 F7a's measured
// case: a diagonal stick whose AABB is over six times its own art, banded into
// four boxes down the diagonal.
fn test_a_proxy_disagrees_with_its_own_bounds_where_it_must() {
  diag = Proxy{boxes: [
    Rect{rx:  0.0, ry:  0.0, rw: 4.0, rh: 4.0},
    Rect{rx:  4.0, ry:  4.0, rw: 4.0, rh: 4.0},
    Rect{rx:  8.0, ry:  8.0, rw: 4.0, rh: 4.0},
    Rect{rx: 12.0, ry: 12.0, rw: 4.0, rh: 4.0},
  ]};
  // The empty CORNER is the whole reason to carry four boxes instead of one.
  corner = Rect{rx: 13.0, ry: 1.0, rw: 2.0, rh: 2.0};
  assert(rects_overlap(diag.bounds(), corner), "the single box says hit");
  assert(!diag.proxy_hits(corner), "and the proxy says miss — that is the gain");
  // On the stick itself they agree, or the proxy would just be wrong.
  on_it = Rect{rx: 5.0, ry: 5.0, rw: 2.0, rh: 2.0};
  assert(rects_overlap(diag.bounds(), on_it) && diag.proxy_hits(on_it), "both hit");

  // `bounds` is the AABB it replaces.
  bb = diag.bounds();
  assert(bb.rx == 0.0 && bb.ry == 0.0 && bb.rw == 16.0 && bb.rh == 16.0, "16x16 hull");
  // Four 4x4 boxes cover 64 of the 256 the single box claims.
  assert(diag.area() == 64.0, "area is the sum of the boxes");
  assert(bb.rw * bb.rh == 256.0, "against 256 for the AABB");

  // Overshoot is measured against the SOLID area, not against the AABB.
  assert(diag.overshoot(64.0) == 0.0, "exact when the boxes are all art");
  assert(diag.overshoot(32.0) == 1.0, "twice the art is +100%, F7's bound");
  assert(diag.overshoot(0.0) == 0.0, "no art to be a multiple of — 0, not a null");

  // The strict boundary is the one the rest of this library uses.
  touching = Rect{rx: 4.0, ry: 0.0, rw: 2.0, rh: 2.0};
  assert(!diag.proxy_hits(touching), "sharing an edge is not overlapping");
}

// An empty proxy hits nothing.  A sprite with no opaque texels derives no boxes,
// and the answer that keeps it from colliding with the world is `false` — not a
// zero-size box at the origin, which would collide with anything there.
fn test_an_empty_proxy_hits_nothing() {
  empty = Proxy{boxes: []};
  assert(!empty.proxy_hits(Rect{rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0}), "no boxes, no hit");
  assert(empty.area() == 0.0, "no area");
  eb = empty.bounds();
  assert(eb.rw == 0.0 && eb.rh == 0.0, "and a zero-size bounds rather than a null");
}

// Two proxies meet only where a box of each meets.
fn test_two_proxies_meet_only_where_boxes_do() {
  left  = Proxy{boxes: [Rect{rx: 0.0, ry: 0.0, rw: 4.0, rh: 4.0},
                        Rect{rx: 0.0, ry: 8.0, rw: 4.0, rh: 4.0}]};
  // Sits in the GAP between left's two bands: bounds overlap, boxes do not.
  gap   = Proxy{boxes: [Rect{rx: 1.0, ry: 5.0, rw: 2.0, rh: 2.0}]};
  assert(rects_overlap(left.bounds(), gap.bounds()), "the hulls overlap");
  assert(!left.proxies_overlap(gap), "the boxes do not");
  hit   = Proxy{boxes: [Rect{rx: 1.0, ry: 9.0, rw: 2.0, rh: 2.0}]};
  assert(left.proxies_overlap(hit) && hit.proxies_overlap(left), "and symmetric when they do");
}

tests/worked-examples.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `shapes` library (@PLN141).  Each `test_*` below is a
// REAL, run-every-CI demonstration of a contract that the function's signature
// cannot state; the function documents itself by CITING the tag with
// `// Example: @SHP-00N`, and `check_doc_drift.sh examples` fails if a citation
// goes dangling (no test carries the tag).  These are teaching examples, distinct
// from the per-function assertion suite in `shapes.loft`.
//
// Everything here is `float -> boolean` or `float -> float`, so the type checker
// can see none of it.  What a caller gets wrong is which BOUNDARY the predicates
// draw, what the depth numbers do NOT say, and how far a circle test differs from
// the box test it looks like.
use shapes::*;

// @SHP-001 — every overlap predicate here is STRICT: shapes that TOUCH do not
// overlap.  `rects_overlap` compares `a.rx < b.rx + b.rw` (not `<=`),
// `circles_overlap` compares squared distance `<` the squared radius sum, and
// `rect_circle_overlap` compares `<` the squared radius.  So a box resting exactly
// against a wall, two tangent circles, and two tiles sharing an edge all answer
// FALSE.  That is the useful half of the convention — a grid of abutting tiles
// never reports itself as one huge collision, and a mover pushed out to exactly
// touching is done, not still stuck (see @SHP-002).  A caller who wants "touching
// counts as a hit" gets no such answer from this library and must inflate one
// shape by an epsilon itself.  It is also what makes 0.0 an unambiguous
// no-overlap sentinel in the depth functions: a real overlap always penetrates by
// strictly more than zero.
fn test_touching_is_not_overlapping() {
  left = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  right = shapes::Rect { rx: 10.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  assert(!shapes::rects_overlap(left, right), "edge-to-edge rects do not overlap");
  assert(!shapes::aabb_overlap(0.0, 0.0, 10.0, 10.0, 10.0, 0.0, 10.0, 10.0),
         "the raw-coordinate form draws the same boundary");

  // Two circles exactly a diameter apart touch at one point — not an overlap.
  a = shapes::Circle { cx: 0.0, cy: 0.0, cr: 5.0 };
  b = shapes::Circle { cx: 10.0, cy: 0.0, cr: 5.0 };
  assert(!shapes::circles_overlap(a, b), "tangent circles do not overlap");
  nudged = shapes::Circle { cx: 9.999, cy: 0.0, cr: 5.0 };
  assert(shapes::circles_overlap(a, nudged), "any real interpenetration does");

  // A circle resting on the rectangle's right edge: closest point (10, 5),
  // distance 2.0, radius 2.0 — equal, so false.
  r = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  resting = shapes::Circle { cx: 12.0, cy: 5.0, cr: 2.0 };
  assert(!shapes::rect_circle_overlap(r, resting), "a circle resting on the edge is clear");

  // The consequence worth having: a row of tiles laid edge to edge is quiet.
  for i in 0..4 {
    tile = shapes::Rect { rx: (i as float) * 10.0, ry: 0.0, rw: 10.0, rh: 10.0 };
    next_tile = shapes::Rect { rx: ((i + 1) as float) * 10.0, ry: 0.0, rw: 10.0, rh: 10.0 };
    assert(!shapes::rects_overlap(tile, next_tile), "abutting tiles never collide");
  }
}

// @SHP-002 — the depth functions answer HOW FAR, never WHICH WAY.  `Overlap.ox` /
// `Overlap.oy` are unsigned penetration distances, so the caller derives the sign
// from the relative positions: the mover is left of the wall, therefore the
// correction is `-ox`.  Applying `+ox` — the same number, the other way — is a
// well-typed call that drives the mover deeper in.  And the axis to resolve on is
// the one with the SMALLER depth: both separate, but the larger one moves 3.5×
// further here, which on screen reads as a teleport rather than a stop.  After
// correcting by exactly the depth the two are edge to edge, which the strict
// predicate of @SHP-001 reads as clear — that is why "push out by exactly ox"
// terminates instead of leaving a one-epsilon residue.  `aabb_depth_x` /
// `aabb_depth_y` answer the same numbers without allocating an `Overlap`, but they
// are independent calls that each re-run the overlap test, and their 0.0 means
// "these do not overlap at all" — never "they meet with zero penetration on this
// axis", which the strict predicate makes impossible.
fn test_depth_says_how_far_not_which_way() {
  mover = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };
  wall = shapes::Rect { rx: 8.0, ry: 3.0, rw: 10.0, rh: 10.0 };
  assert(shapes::rects_overlap(mover, wall), "the mover is inside the wall");

  d = shapes::rect_overlap_depth(mover, wall);
  assert(d.ox == 2.0, "2 units of penetration on x");
  assert(d.oy == 7.0, "7 units of penetration on y");

  // The sign is the caller's: -ox is out, +ox is further in.
  wrong_way = shapes::Rect { rx: mover.rx + d.ox, ry: mover.ry, rw: mover.rw, rh: mover.rh };
  assert(shapes::rects_overlap(wrong_way, wall), "the wrong sign pushes deeper, not out");
  resolved = shapes::Rect { rx: mover.rx - d.ox, ry: mover.ry, rw: mover.rw, rh: mover.rh };
  assert(!shapes::rects_overlap(resolved, wall), "smaller axis, correct sign: clear");

  // Resolving on the larger axis is not wrong, it is just 3.5× the correction.
  overshot = shapes::Rect { rx: mover.rx, ry: mover.ry - d.oy, rw: mover.rw, rh: mover.rh };
  assert(!shapes::rects_overlap(overshot, wall), "the y correction separates them too");
  assert(d.oy > d.ox, "...but moves 7 where 2 would have done");

  // The struct-free pair: same numbers, two calls, and 0.0 means no overlap.
  assert(shapes::aabb_depth_x(0.0, 0.0, 10.0, 10.0, 8.0, 3.0, 10.0, 10.0) == d.ox,
         "aabb_depth_x agrees with Overlap.ox");
  assert(shapes::aabb_depth_y(0.0, 0.0, 10.0, 10.0, 8.0, 3.0, 10.0, 10.0) == d.oy,
         "aabb_depth_y agrees with Overlap.oy");
  assert(shapes::aabb_depth_x(0.0, 0.0, 10.0, 10.0, 20.0, 20.0, 5.0, 5.0) == 0.0,
         "no overlap at all reads 0.0 on x");
  assert(shapes::aabb_depth_y(0.0, 0.0, 10.0, 10.0, 20.0, 20.0, 5.0, 5.0) == 0.0,
         "no overlap at all reads 0.0 on y");
}

// @SHP-003 — `rect_circle_overlap` is a real circle test, and the cheap
// substitute for it disagrees exactly where circles are used: at the corners.  It
// clamps the circle's centre into the BOX and measures from there, so a circle
// sitting off a corner can have its bounding box well inside the rectangle while
// the circle itself misses by a diagonal.  Approximating with `aabb_overlap` on
// the circle's bounding box therefore reports a hit in every corner region — an
// invisible square shoulder on each corner of every obstacle.  The same clamp is
// why containment answers TRUE: a circle wholly inside clamps to its own centre,
// distance 0, so "overlap" here includes "one contains the other", which a
// nearest-point-on-the-PERIMETER reading would get backwards.
fn test_circle_is_not_its_bounding_box() {
  r = shapes::Rect { rx: 0.0, ry: 0.0, rw: 10.0, rh: 10.0 };

  // Off the (10, 10) corner by (3, 3): distance 4.24 with a radius of 4.
  near = shapes::Circle { cx: 13.0, cy: 13.0, cr: 4.0 };
  assert(shapes::aabb_overlap(0.0, 0.0, 10.0, 10.0,
                              near.cx - near.cr, near.cy - near.cr,
                              near.cr * 2.0, near.cr * 2.0),
         "the circle's BOUNDING BOX reaches into the rectangle");
  assert(!shapes::rect_circle_overlap(r, near), "...the circle itself does not");

  // One unit closer along the diagonal and the corner is genuinely hit.
  hit = shapes::Circle { cx: 12.0, cy: 12.0, cr: 4.0 };
  assert(shapes::rect_circle_overlap(r, hit), "distance 2.83 under a radius of 4");

  // Containment is overlap: the clamp lands on the centre, distance 0.
  inside = shapes::Circle { cx: 5.0, cy: 5.0, cr: 2.0 };
  assert(shapes::rect_circle_overlap(r, inside), "a circle wholly inside overlaps");
}