Implement realistic pool physics with friction, spin, and Han 2005 cushion model#14
Implement realistic pool physics with friction, spin, and Han 2005 cushion model#14
Conversation
…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
Deploying with
|
| 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 |
Benchmark Comparison
Overall: +591.78% 🚀 Merge base: Previous runsBenchmark Comparison
Overall: +495.96% 🚀 Merge base: Benchmark Comparison
Overall: +522.52% 🚀 Merge base: Benchmark Comparison
Overall: +496.88% 🚀 Merge base: Benchmark Comparison
Overall: +601.48% 🚀 Merge base: Benchmark Comparison
Overall: +487.60% 🚀 Merge base: Benchmark Comparison
Overall: +518.25% 🚀 Merge base: Benchmark Comparison
Overall: +534.58% 🚀 Merge base: Benchmark Comparison
Overall: +416.60% 🚀 Merge base: Benchmark Comparison
Overall: +480.34% 🚀 Merge base: Benchmark Comparison
Overall: +579.22% 🚀 Merge base: Benchmark Comparison
Overall: +560.88% 🚀 Merge base: Benchmark Comparison
Overall: +565.75% 🚀 Merge base: Benchmark Comparison
Overall: +529.91% 🚀 Merge base: Benchmark Comparison
Overall: +3132.42% 🚀 Merge base: |
… 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
Summary
r(t) = at² + bt + c) driven by frictionVector3Dtype,Ballclass with motion states, per-ball configurable physics paramsContactResolver 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):
Test plan
https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr