Fixed a real, previously-documented-as-acceptable bug: entities visibly crossed the play area boundary before bouncing, because radius was never subtracted from the bounce comparison
Context
User report: balls crossed the square frame's visible boundary during play, 'as if there was an invisible, larger area behind them.' This was a real, previously-known bug: glemy/pe/bounds.gleam's bounce_axis compared an entity's raw *center* position to the box edges, never subtracting radius -- test/glemy/pe/bounds_test.gleam even had a dedicated test, radius_does_not_affect_when_bounce_triggers_test, explicitly documenting this as 'current behavior, not asserting it's the eventual desired behavior.' A large-radius entity's rendered edge could sit well outside bounds while its center hadn't reached the wall yet, exactly matching the reported symptom -- the visible canvas edge (the world 0..100 box fills the entire canvas in clip space, so there's no separate rendered 'frame' distinct from the physics bounds) is where a player expects the wall to be, not `radius` units further out.
Options considered
Decision
glemy/pe/bounds.gleam's bounce now passes `bounds.min.x +. entity.radius`/`bounds.max.x -. entity.radius` (and the same for y) into bounce_axis instead of the raw bounds edges -- the entity's center is clamped to radius inside each wall, so its rendered circle's edge never crosses bounds. The previously-documenting-the-bug test (radius_does_not_affect_when_bounce_triggers_test) was replaced with larger_radius_entities_bounce_sooner_test, which asserts the opposite: a large-radius entity now bounces at a smaller raw-center displacement than a small-radius one would, exactly the fix. This surfaced a second, related bug during the same investigation, both root-caused and fixed together (see decision 0027 continuation below and glemy/pe/collision.gleam): with radius-aware bounds alone, entities resting under continuous gravity would elastically bounce off the wall every single frame forever (restitution 1.0, no energy loss), which is what the user's third report ('balls gaining excessive momentum and never becoming stationary') turned out to be -- both bounds.gleam and collision.gleam gained a resting_contact_velocity threshold (3.0 world units/s, comfortably above one frame's gravity contribution at reasonable frame rates): a wall or entity-entity 'hit' below that closing speed absorbs the velocity to exactly zero instead of elastically reflecting it, which is what actually lets a resting entity come to a genuine, permanent stop.
Verification
144 Erlang / 178 JavaScript tests passing. Every existing bounds_test.gleam/collision_test.gleam test using a velocity magnitude that would now fall under the new resting_contact_velocity threshold (several, since many pre-existing 'genuine bounce' tests happened to use small velocities like 1.0-2.0) was deliberately raised to a clearly-above-threshold magnitude (10.0 for bounds, 5.0 for collision) to preserve their original intent as genuine-impact tests, recomputing their expected values from the same formulas the source uses (not re-typed decimals) -- including the two textbook-fact regression tests (equal-mass head-on collision swaps velocities) whose specific values had to change but whose underlying physical identity did not. New dedicated tests cover the resting-contact absorption path directly (small velocity in, exactly zero out, position still clamped/separated) and its exact boundary (`<`, not `<=`) in both files. Empirically verified via a real headless-browser session (not just gleam test): tracked real GPU-uploaded entity positions over an 8-second run and confirmed the three independent starting-scene entities settle to their exact expected resting height (radius above the floor) with zero measurable jitter, and a 6-ball stack dropped at the same point settles to a small number of stable resting heights with sub-visual residual jitter, not the unbounded bouncing the bug report described.
Consequences
Any future entity, regardless of radius, now visibly stays fully inside the rendered play area -- no more invisible overflow margin. A resting stack of entities (including merged, larger-tier entities) now genuinely comes to rest rather than perpetually jittering, matching normal player expectations for a puzzle game where pieces are meant to settle. The old, narrower entity.settle-based fix attempt (a flat post-hoc velocity-magnitude snap applied every frame regardless of context) was tried first, found to be badly wrong (it prevented free-falling entities from ever building up real fall speed at all, since one frame's gravity-only velocity is indistinguishable in magnitude from resting jitter when checked out of context), and was caught only by actually running the game in a real browser and measuring positions over time -- gleam test alone would never have revealed it, since the flawed version still passed every existing unit test. entity.rest_velocity_threshold now exists only as a tiny (0.05) floating-point-dust cleanup, not the primary settling mechanism.