Skip to content

Implement realistic pool physics with friction, spin, and Han 2005 cushion model#14

Open
TimBeyer wants to merge 27 commits intomasterfrom
claude/plan-pool-physics-7B0IT
Open

Implement realistic pool physics with friction, spin, and Han 2005 cushion model#14
TimBeyer wants to merge 27 commits intomasterfrom
claude/plan-pool-physics-7B0IT

Conversation

@TimBeyer
Copy link
Owner

@TimBeyer TimBeyer commented Mar 25, 2026

Summary

  • Implement realistic pool physics with friction, spin, and Han 2005 cushion model
  • Replace constant-velocity linear trajectories with quadratic trajectories (r(t) = at² + bt + c) driven by friction
  • Add full 3D physics: Vector3D type, Ball class with motion states, per-ball configurable physics params
  • Implement quartic polynomial solver for ball-ball collision detection with curved trajectories
  • Add analytical state transition events as first-class events in the priority queue
  • Implement Han 2005 cushion collision model with spin transfer and two friction regimes
  • Add ContactResolver for systemic simultaneous collision handling — fixes ball pass-through in dense clusters

ContactResolver Architecture

The event queue (heap + epoch-based invalidation) is the wrong abstraction for zero-time-delta collisions. When two collisions fire at the exact same time and share a ball, processing the first increments the shared ball's epoch, invalidating the second event.

The ContactResolver separates future event scheduling (heap) from instant contact resolution (inline):

Event Queue (heap)                     Contact Resolver (inline)
──────────���──────                      ────────────���───────────
Schedules future collisions            Handles instant contacts
Epoch-based lazy invalidation          No epochs involved
One event at a time                    Iterates until no contacts remain
Quartic polynomial (t > 0)             Distance + velocity check (t = 0)
                                       Convergence: pair limit + max iterations

Test plan

  • All 134 tests pass (polynomial-solver, circle, collision, spatial-grid, simulation, physics, invariants, stress)
  • No overlaps at collision events (150-ball stress test within 0.5mm tolerance)
  • All balls stay in bounds (within 1mm tolerance)
  • Event count bounded (< 50,000 for 150 balls, 5s)
  • Triangle break, Newton's cradle, 3-ball simultaneous all work correctly
  • Lint clean, build clean

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr

…shion model

Replace constant-velocity linear trajectories with quadratic trajectories
(r(t) = at² + bt + c) driven by friction. Balls now decelerate and stop.

Core additions:
- Vector3D type and utilities for 3D physics (position, velocity, angular velocity)
- Ball class replacing Circle with motion states (Stationary/Spinning/Rolling/Sliding)
- Per-ball configurable physics params (mass, radius, friction coefficients, restitution)
- Quartic polynomial solver (Ferrari's method) for ball-ball collision detection
- Analytical state transition events in the priority queue
- Han 2005 cushion collision model with spin transfer and cushion height angle
- Trajectory coefficient computation per motion state (sliding/rolling deceleration)

Key changes:
- Ball-ball collision detection: quadratic→quartic polynomial (deceleration curves)
- Ball-cushion collision detection: linear→quadratic (deceleration)
- Spatial grid cell transitions: velocity-direction-aware boundary crossing
- Simulation loop processes StateTransition events alongside collisions
- Balls eventually come to rest (finite simulation time)

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
@cloudflare-workers-and-pages
Copy link
Contributor

cloudflare-workers-and-pages bot commented Mar 25, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
balls e7a72b6 Commit Preview URL

Branch Preview URL
Mar 26 2026, 03:33 PM

@github-actions
Copy link

github-actions bot commented Mar 25, 2026

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 208.16 1729.26 +730.75% 🚀
20 circles / 60s 140.21 735.82 +424.80% 🚀
40 circles / 60s 51.49 321.20 +523.76% 🚀
80 circles / 60s 13.65 84.69 +520.52% 🚀
150 circles / 60s 3.43 14.83 +332.80% 🚀
300 circles / 60s 1.47 8.96 +510.97% 🚀
500 circles / 60s 0.75 4.84 +543.60% 🚀
1000 circles / 60s 0.32 2.22 +596.74% 🚀

Overall: +591.78% 🚀


Merge base: 46a5cce | PR commit: e7a72b6 | 2026-03-26 15:34 UTC

Previous runs

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 254.68 1776.06 +597.37% 🚀
20 circles / 60s 128.78 563.36 +337.46% 🚀
40 circles / 60s 46.19 234.51 +407.64% 🚀
80 circles / 60s 12.83 69.14 +438.91% 🚀
150 circles / 60s 3.12 11.58 +270.55% 🚀
300 circles / 60s 1.27 8.00 +528.14% 🚀
500 circles / 60s 0.66 4.13 +528.86% 🚀
1000 circles / 60s 0.28 2.03 +625.23% 🚀

Overall: +495.96% 🚀


Merge base: 46a5cce | PR commit: 49f59c7 | 2026-03-26 15:11 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 244.80 1599.94 +553.57% 🚀
20 circles / 60s 151.07 911.05 +503.08% 🚀
40 circles / 60s 54.59 301.12 +451.63% 🚀
80 circles / 60s 14.63 89.02 +508.41% 🚀
150 circles / 60s 3.40 14.52 +327.18% 🚀
300 circles / 60s 1.42 8.98 +533.02% 🚀
500 circles / 60s 0.74 4.64 +526.17% 🚀
1000 circles / 60s 0.31 2.52 +706.47% 🚀

Overall: +522.52% 🚀


Merge base: 46a5cce | PR commit: 737fc2f | 2026-03-26 12:46 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 242.00 1556.29 +543.11% 🚀
20 circles / 60s 135.42 728.18 +437.72% 🚀
40 circles / 60s 53.67 288.42 +437.43% 🚀
80 circles / 60s 13.71 85.90 +526.47% 🚀
150 circles / 60s 3.33 15.21 +357.21% 🚀
300 circles / 60s 1.45 8.50 +485.43% 🚀
500 circles / 60s 0.75 4.75 +536.00% 🚀
1000 circles / 60s 0.32 2.49 +683.74% 🚀

Overall: +496.88% 🚀


Merge base: 46a5cce | PR commit: a4d34ae | 2026-03-26 11:43 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 229.14 1817.76 +693.29% 🚀
20 circles / 60s 149.88 926.67 +518.27% 🚀
40 circles / 60s 53.45 312.54 +484.73% 🚀
80 circles / 60s 14.33 90.07 +528.36% 🚀
150 circles / 60s 3.54 14.02 +296.03% 🚀
300 circles / 60s 1.50 9.26 +518.46% 🚀
500 circles / 60s 0.78 4.74 +508.06% 🚀
1000 circles / 60s 0.34 2.38 +606.16% 🚀

Overall: +601.48% 🚀


Merge base: 46a5cce | PR commit: 8eff7eb | 2026-03-26 11:09 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 255.23 1582.14 +519.88% 🚀
20 circles / 60s 142.80 749.97 +425.18% 🚀
40 circles / 60s 51.88 314.56 +506.26% 🚀
80 circles / 60s 14.77 85.88 +481.65% 🚀
150 circles / 60s 3.47 17.03 +391.05% 🚀
300 circles / 60s 1.47 9.04 +513.73% 🚀
500 circles / 60s 0.76 4.94 +548.45% 🚀
1000 circles / 60s 0.33 2.38 +614.45% 🚀

Overall: +487.60% 🚀


Merge base: 46a5cce | PR commit: f438d60 | 2026-03-26 10:41 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 259.16 1782.09 +587.65% 🚀
20 circles / 60s 142.86 721.95 +405.35% 🚀
40 circles / 60s 56.09 311.86 +456.02% 🚀
80 circles / 60s 14.79 96.11 +550.02% 🚀
150 circles / 60s 3.51 28.34 +706.79% 🚀
300 circles / 60s 1.50 11.99 +697.45% 🚀
500 circles / 60s 0.78 5.96 +668.55% 🚀
1000 circles / 60s 0.33 3.21 +878.51% 🚀

Overall: +518.25% 🚀


Merge base: 46a5cce | PR commit: 45e323e | 2026-03-26 08:46 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 246.34 1711.71 +594.84% 🚀
20 circles / 60s 143.36 740.76 +416.72% 🚀
40 circles / 60s 49.37 310.67 +529.32% 🚀
80 circles / 60s 13.60 94.05 +591.34% 🚀
150 circles / 60s 3.32 29.89 +798.99% 🚀
300 circles / 60s 1.43 12.91 +800.78% 🚀
500 circles / 60s 0.74 6.46 +775.83% 🚀
1000 circles / 60s 0.31 2.99 +855.40% 🚀

Overall: +534.58% 🚀


Merge base: 46a5cce | PR commit: 87d631b | 2026-03-26 07:56 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 239.69 1316.45 +449.24% 🚀
20 circles / 60s 148.94 663.26 +345.31% 🚀
40 circles / 60s 56.22 307.82 +447.49% 🚀
80 circles / 60s 14.91 80.71 +441.25% 🚀
150 circles / 60s 3.46 20.65 +497.14% 🚀
300 circles / 60s 1.50 9.94 +564.30% 🚀
500 circles / 60s 0.76 5.25 +590.36% 🚀
1000 circles / 60s 0.33 2.33 +596.86% 🚀

Overall: +416.60% 🚀


Merge base: 46a5cce | PR commit: 94e26dd | 2026-03-26 05:53 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 223.00 1761.57 +689.93% 🚀
20 circles / 60s 143.63 710.20 +394.45% 🚀
40 circles / 60s 54.89 19.31 -64.83% 🔴
80 circles / 60s 14.16 67.73 +378.33% 🚀
150 circles / 60s 3.44 1.19 -65.48% 🔴
300 circles / 60s 1.49 0.44 -70.52% 🔴
500 circles / 60s 0.75 2.78 +268.88% 🚀
1000 circles / 60s 0.32 0.13 -58.49% 🔴

Overall: +480.34% 🚀


Merge base: 46a5cce | PR commit: 3391b49 | 2026-03-26 05:44 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 229.58 1776.40 +673.78% 🚀
20 circles / 60s 144.69 775.96 +436.28% 🚀
40 circles / 60s 53.60 353.47 +559.42% 🚀
80 circles / 60s 14.44 94.83 +556.87% 🚀
150 circles / 60s 3.39 25.70 +657.89% 🚀
300 circles / 60s 1.44 10.83 +653.82% 🚀
500 circles / 60s 0.72 4.94 +582.10% 🚀
1000 circles / 60s 0.31 1.94 +522.83% 🚀

Overall: +579.22% 🚀


Merge base: 46a5cce | PR commit: f96ee0c | 2026-03-25 15:35 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 247.92 1828.34 +637.47% 🚀
20 circles / 60s 138.73 740.89 +434.04% 🚀
40 circles / 60s 53.05 338.70 +538.44% 🚀
80 circles / 60s 14.62 93.50 +539.48% 🚀
150 circles / 60s 3.40 23.41 +588.99% 🚀
300 circles / 60s 1.49 10.44 +602.02% 🚀
500 circles / 60s 0.77 4.73 +515.86% 🚀
1000 circles / 60s 0.32 1.99 +527.32% 🚀

Overall: +560.88% 🚀


Merge base: 46a5cce | PR commit: 0f1bd5b | 2026-03-25 15:03 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 251.36 1792.41 +613.08% 🚀
20 circles / 60s 137.10 784.04 +471.87% 🚀
40 circles / 60s 51.42 358.48 +597.16% 🚀
80 circles / 60s 13.38 81.77 +510.96% 🚀
150 circles / 60s 3.41 24.06 +606.25% 🚀
300 circles / 60s 1.49 10.48 +601.27% 🚀
500 circles / 60s 0.77 4.37 +467.04% 🚀
1000 circles / 60s 0.33 1.93 +492.81% 🚀

Overall: +565.75% 🚀


Merge base: 46a5cce | PR commit: 4397293 | 2026-03-25 14:50 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 258.04 1790.50 +593.90% 🚀
20 circles / 60s 149.06 752.76 +405.01% 🚀
40 circles / 60s 54.15 365.27 +574.51% 🚀
80 circles / 60s 14.39 86.45 +500.95% 🚀
150 circles / 60s 3.45 23.08 +569.56% 🚀
300 circles / 60s 1.49 9.60 +545.74% 🚀
500 circles / 60s 0.76 4.33 +467.63% 🚀
1000 circles / 60s 0.32 1.99 +515.37% 🚀

Overall: +529.91% 🚀


Merge base: 46a5cce | PR commit: c9f3139 | 2026-03-25 11:26 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 270.91 9878.57 +3546.48% 🚀
20 circles / 60s 145.88 1825.52 +1151.42% 🚀
40 circles / 60s 56.13 2583.29 +4502.25% 🚀
80 circles / 60s 14.85 865.48 +5726.56% 🚀
150 circles / 60s 3.53 445.18 +12502.42% 🚀
300 circles / 60s 1.49 205.73 +13735.80% 🚀
500 circles / 60s 0.78 113.96 +14558.58% 🚀
1000 circles / 60s 0.31 46.43 +14784.21% 🚀

Overall: +3132.42% 🚀


Merge base: 46a5cce | PR commit: 6f01a6f | 2026-03-25 08:08 UTC

claude added 26 commits March 25, 2026 08:48
… render loop alive

- Scale ball velocities from ~0-1.4 mm/s to ~0-1400 mm/s so balls actually
  move visibly before friction stops them
- Keep the requestAnimationFrame loop running after simulation ends so the
  camera controls remain interactive

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Physics engine uses seconds (gravity=9810 mm/s²) but requestAnimationFrame
timestamps are in milliseconds. Divide progress by 1000 and adjust PRECALC
buffer from 10000ms to 10s so events play back at real-time speed.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ctories

The event loop was calling advanceTime() on ALL balls at every event, which
updated ball.time but left trajectory.c and trajectory.b stale. This caused
positionAtTime() to compute positions from the wrong reference point, making
balls twitch, tunnel through walls, and jump to incorrect positions.

Fix: only update balls that are involved in each event (from their snapshots).
Non-involved balls keep their existing trajectory which remains valid for
positionAtTime() interpolation from their own reference time.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
… checks

Two simulation-level fixes for balls passing through each other and walls:

1. Cell transitions no longer call updateTrajectory(), which was prematurely
   re-determining motion state (e.g. Sliding→Rolling) and changing trajectory
   acceleration coefficients. This corrupted collision detection. Now manually
   rebases trajectory to new reference point without changing motion state.

2. Cushion collision detection now verifies the ball is moving TOWARD the wall
   at the computed collision time (velocity direction check). Prevents false
   collisions when a ball is at the wall after a bounce but moving away.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Root cause: after a cushion bounce with high spin, the Han 2005 model
produces near-zero perpendicular velocity (vy ≈ 0) but spin friction
creates massive perpendicular acceleration (ay = 926 mm/s²) pushing the
ball back into the wall. The cushion collision equation gives only t=0
(filtered out), so no future collision is detected. The ball accelerates
through the wall.

Fix: after a cushion bounce, clamp the trajectory's perpendicular
acceleration to prevent it from pointing back into the wall. This
approximates the ball "rolling along the rail" — physically correct
behavior when spin friction would otherwise cause infinite micro-bounces.

Also:
- Remove overly strict velocity-direction checks from getCushionCollision
  (epoch-based invalidation already handles "return" events correctly)
- Add friction-enabled simulation tests for boundary containment and
  ball-ball collision detection

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ce rolling

Two critical bugs in the collision resolution:

1. Stationary balls couldn't receive momentum. The code scaled the
   existing velocity by (v_after / v_before), but when v_before = 0
   (stationary ball), the scale was 0 and the ball stayed still.
   Fix: directly compute post-collision velocity as normal_component +
   tangential_remainder, no division needed.

2. Angular velocity was never updated after ball-ball collisions.
   A rolling ball that collided kept its old spin, causing friction to
   re-accelerate it back toward its original speed. This created
   perpetual bouncing cycles where balls never came to rest.
   Fix: enforce the rolling constraint after collision (set angular
   velocity to match the new linear velocity).

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Ball-ball collisions were not zeroing the z-component of angular velocity,
causing spin to accumulate across collisions. After a cushion bounce (which
generates z-spin via Han 2005 model), that spin would persist through
subsequent ball-ball collisions, keeping balls in Spinning state instead
of coming to rest. Also zero velocity z-component to prevent drift.

Added tests: momentum transfer to stationary balls, z-spin zeroing,
and energy conservation through collisions.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
When simulation completes (all balls stationary), the worker sends back
only an initial snapshot. The main thread's render loop would crash
accessing nextEvent.snapshots when nextEvent was undefined. Fixed by:
- Guarding renderer calls when nextEvent is undefined
- Detecting simulation-done state to stop requesting more data from worker

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Extract physics logic from monolithic simulation.ts into composable,
strategy-based classes organized under src/lib/physics/:

- MotionModel interface: SlidingMotion, RollingMotion, SpinningMotion,
  StationaryMotion each own their trajectory computation, angular
  trajectory, state transition timing, and transition application
- CollisionResolver interface: ElasticBallResolver (ball-ball),
  Han2005CushionResolver (pool cushions), SimpleCushionResolver (2D)
- CollisionDetector interface: QuarticBallBallDetector, QuadraticCushionDetector
- PhysicsProfile composes all components into swappable profiles
  (createPoolPhysicsProfile, createSimple2DProfile)

simulation.ts is now a thin event coordinator that delegates all
physics to the profile. Ball.updateTrajectory takes a PhysicsProfile.
CollisionFinder uses profile detectors and motion models.

Adds physics profile selector to UI (Pool/Simple 2D dropdown).
Removes state-transitions.ts (logic moved to motion models).
Strips trajectory.ts to types + evaluate helpers only.

All 74 tests pass, lint clean.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
- ElasticBallResolver: preserve angular velocity unchanged through
  ball-ball collisions (was incorrectly forcing rolling constraint)
- Han 2005 cushion model: fix sx/sy intermediate formulas (swapped
  components, wrong omega references), fix velocity formulas to be
  impulse-based deltas (were incorrectly absolute, causing energy gain),
  fix angular velocity to absolute values from post-collision velocity
- Add MotionState.Airborne and AirborneMotion model for balls with
  upward velocity after cushion bounce (gravity in z, no friction,
  landing with table restitution)
- Skip cushion detection for airborne balls in CollisionFinder
- Render ball height using 3D position (z-component lifts ball)
- Add eTableRestitution to PhysicsConfig
- Update tests: verify spin preservation, add cushion bounce, energy
  conservation, and airborne state tests

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Based on pooltool source code analysis:
- Fix sx intermediate: use vZ (vertical) not vPar, matching reference
  sx = vPerp*sinθ - vZ*cosθ + R*ωy (vZ=0 for grounded balls)
- Fix angular velocity to be impulse-based DELTAS (added to initial ω),
  not absolute values. Derived from Δω = (R × J) / I for both
  no-sliding and sliding cases.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
- Add recomputeMinor() to CollisionFinder for Sliding→Rolling transitions
  that skips expensive ball-ball neighbor scan (fixes 310x event amplification)
- Don't increment epoch for minor state transitions to preserve existing
  ball-ball predictions
- Handle corner bounces: resolve immediate cushion collision when ball is
  at a wall boundary with velocity into it after another cushion bounce
- Clamp airborne balls to table bounds on landing
- Add performance diagnostic and comparison test suites

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
The previous recomputeMinor approach incorrectly skipped ball-ball
re-prediction for Sliding→Rolling transitions. Since Rolling has
different friction and spin deflection, this produced stale predictions.

Root cause: quadratic trajectory approximation is exact within a single
motion state, but state transitions (Sliding→Rolling) change the
trajectory. Predictions computed before a transition fire at the wrong
time — balls are millimeters apart, not touching.

Fix: verify actual contact distance before resolving ball-ball collisions.
If the gap exceeds 0.05mm (trajectory approximation error), skip the
phantom collision and recompute with fresh trajectories.

Also adds overlap guard margin (1e-4mm) to the ball-ball detector to
prevent re-detecting barely-separated balls after state transitions.

Results: 150 balls, 20s simulation — 460ms wall-clock, 711 ball
collisions (vs 723 for zero friction baseline).

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Replace cascade analysis with focused phantom gap measurement tests
for both zero-friction and pool-physics configurations.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
After a ball-ball collision, both balls are at touching distance. Sliding
acceleration (along slip direction, not velocity direction) can push them
back together in ~72 nanoseconds, causing an infinite collision loop.

Fix: after A-B collision, skip re-predicting A-B in recompute(). The pair
gets re-checked naturally when either ball's trajectory changes from a
state transition or collision with another ball. This reduces 224K phantom
collisions to ~330 real collisions for 150 balls in 1s.

Also removes the contact verification gap logic and MIN_GAP overlap guard
margin — the math is clean and these were masking the root cause.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
The friction torque τ = r_contact × F gives angular acceleration
α = (5μg/2R)·(-ûy, +ûx, 0), but the code had the signs flipped:
(+ûy, -ûx, 0). This caused angular velocity to diverge FROM the
rolling constraint during sliding instead of converging toward it.

The slip was increasing at +(3/2)μg·t instead of decreasing at
-(7/2)μg·t, meaning balls never naturally reached rolling condition.
When collisions occurred during sliding, the wrong angular velocity
propagated through preserved-ω collision resolution, producing wrong
friction directions and causing balls to enter each other.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Below 5 mm/s approach speed, ball-ball collisions become perfectly
inelastic along the normal — both balls receive the center-of-mass
velocity (no bounce). At this speed a ball travels <0.2mm before
friction stops it, so no visible bounce would occur in reality either.

This eliminates the Zeno cascade at its source: no bounce means no
re-approach, so the infinite collision loop cannot form. Works for
both isolated pairs and multi-ball clusters.

Removes the skip-pair band-aid (skipPairId in recompute) which was
masking the cascade but could miss legitimate re-collisions.

5s/150-ball benchmark: 2974 events in 64ms (was 1.9M events in 65s).

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Floating-point evaluation of the trajectory polynomial can leave balls
slightly overlapping at the predicted collision time. Without snapping,
the overlap guard silently skips future collisions for that pair,
causing balls to pass through each other.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Replace 6 ad-hoc test files with 6 systematic test files (65 tests)
organized by progressive complexity:
- single-ball-motion: motion states and transitions (12 tests)
- cushion-collision: Han 2005 cushion physics (12 tests)
- ball-ball-collision: elastic/inelastic collisions (15 tests)
- multi-ball: Newton's cradle through 150-ball stress (10 tests)
- invariants: cross-cutting physics invariants (8 tests)
- edge-cases: boundary conditions and numerics (8 tests)

Shared scenario definitions (scenarios.ts) are plain data objects
consumed by both tests and the visual simulation. Add LOAD_SCENARIO
worker message, UI scenario dropdown, and ?scenario=name URL support
so any test scenario can be visualized for debugging.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ation damping

When two ball-ball collisions fire at exactly the same time (e.g., left→center
and right→center in a Newton's cradle), processing the first invalidates the
second via epoch increment. The quartic detector then sees the balls at exact
touching distance, producing a root at t=0 which smallestPositiveRoot filters
out (requires t > 1e-9), returning the *separation* time instead. This causes
balls to pass through each other.

Two-part fix:
1. Contact guard in ball-ball detector: when dist ≈ rSum and balls are
   approaching (negative dot product), return refTime (collision is NOW)
2. Pair oscillation guard in simulation loop: in dense clusters (triangle
   break), contact chains can bounce back and forth between pairs. Track
   pair resolution count at same time; after 2 resolutions, force inelastic
   (COM velocity along normal) to guarantee convergence.

Also removes debug-3ball.test.ts diagnostic file.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Replace ad-hoc oscillation tracking with a dedicated ContactResolver component
that handles instant/simultaneous contact collisions inline, outside the event
queue. This fixes ball pass-through in dense clusters (triangle breaks, 3-ball
simultaneous collisions) caused by epoch invalidation dropping same-time events.

Architecture: the event queue handles future collisions (t > now), while the
ContactResolver handles instant contacts (t = now) that cascade from a primary
collision. After each ball-ball collision, the resolver checks all neighbors of
affected balls for touching + approaching pairs and resolves them iteratively
until no more contacts exist.

Convergence guarantees:
- Pair tracking: after 2 resolutions of same pair, force inelastic
- Pair skip: after 4 resolutions, skip entirely (wall-locked pairs)
- Max iterations: totalBalls * 5 safety limit
- Wall-aware separation: iterative push-apart with bounds clamping

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Three bugs fixed:

1. Quartic solver fails for near-contact collisions: Ferrari's method loses
   precision when balls are nearly touching (gap < 0.1mm), producing complex
   roots for what should be real collisions. Added Newton's method fallback
   in smallestPositiveRoot that detects this case (small positive constant
   term + negative linear term) and finds the root the algebraic solver
   missed. This fixes balls passing through each other after triangle breaks
   and similar dense interactions.

2. Stale trajectory.c after position modifications: clampToBounds and the
   iterative push-apart loop ran AFTER updateTrajectory, leaving trajectory.c
   out of sync with ball.position. The quartic detector then computed wrong
   collision times from the stale trajectory, causing visual teleportation.
   Added trajectory.c rebasing after all position modifications.

3. Over-separation in push-apart: each ball was pushed by the FULL overlap
   instead of half, doubling the total separation when no wall was involved.
   Fixed to use half-overlap with 5 iterations for proper convergence.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ctor

- FutureTrailRenderer: draws trajectory-interpolated paths for future events
  per ball with accessible color + dash-pattern coding by event type
- Phantom balls at each future event point with configurable opacity
- PlaybackController: pause/resume and step-through-events state machine
- BallInspector: click-to-inspect ball properties via raycasting overlay
- Tweakpane UI controls for all debug features in new Debug/Playback folders
- All settings configurable: events per ball, interpolation steps, opacity

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
- Add React 19, Tailwind v4, and Vite plugins for JSX/CSS
- SimulationBridge: connects animation loop to React via useSyncExternalStore
- TransportBar: play/pause, step, speed presets, time display (bottom center)
- Sidebar: collapsible right panel with scenario picker, debug viz toggles,
  2D overlay toggles, and simulation stats with motion state distribution bar
- BallInspectorPanel: slide-out left panel replacing canvas overlay, shows
  position, velocity, speed, angular velocity, motion state, acceleration
- EventLog: collapsible bottom-right panel showing last 50 events color-coded
- Keyboard shortcuts: Space=pause, →=step, ±=speed, 1-5=presets, I/F/T/C=toggles
- Tweakpane stripped to "Advanced" panel (lighting, camera, shadows, table color)

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Store complete event history and initial ball states. On step-back,
restore all balls to initial state then replay events up to the
previous event. Simple approach trades memory for fewer moving parts.

- Add step-back button to transport bar (←  icon, ArrowLeft shortcut)
- Track canStepBack in SimulationSnapshot for UI disable state
- Wire onStepBack callback through SimulationBridge

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
PlaybackController retained stale paused state and frozenProgress
after simulation restart, causing empty table and time jumps.

https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants