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 | 33b9d44 | Commit Preview URL Branch Preview URL |
Mar 28 2026, 12:21 PM |
Benchmark Comparison
Overall: +369.37% 🚀 Merge base: Previous runsBenchmark Comparison
Overall: +417.68% 🚀 Merge base: Benchmark Comparison
Overall: +379.49% 🚀 Merge base: Benchmark Comparison
Overall: +356.88% 🚀 Merge base: Benchmark Comparison
Overall: +328.62% 🚀 Merge base: Benchmark Comparison
Overall: +338.24% 🚀 Merge base: Benchmark Comparison
Overall: +310.44% 🚀 Merge base: Benchmark Comparison
Overall: +362.63% 🚀 Merge base: Benchmark Comparison
Overall: +291.81% 🚀 Merge base: Benchmark Comparison
Overall: +359.45% 🚀 Merge base: Benchmark Comparison
Overall: +265.74% 🚀 Merge base: Benchmark Comparison
Overall: +389.80% 🚀 Merge base: Benchmark Comparison
Overall: +374.17% 🚀 Merge base: Benchmark Comparison
Overall: +405.28% 🚀 Merge base: Benchmark Comparison
Overall: +368.71% 🚀 Merge base: Benchmark Comparison
Overall: +446.49% 🚀 Merge base: Benchmark Comparison
Overall: +440.02% 🚀 Merge base: Benchmark Comparison
Overall: +596.85% 🚀 Merge base: Benchmark Comparison
Overall: +357.88% 🚀 Merge base: Benchmark Comparison
Overall: +500.67% 🚀 Merge base: Benchmark Comparison
Overall: +461.62% 🚀 Merge base: Benchmark Comparison
Overall: +545.01% 🚀 Merge base: Benchmark Comparison
Overall: +591.78% 🚀 Merge base: Benchmark 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
When paused, an event detail panel appears showing pre/post state for each involved ball: velocity, speed, angular velocity, acceleration, position, and motion state changes. Acceleration jumps >2x are flagged. Ball inspector now shows a scrollable event history for the selected ball with compact state change summaries and speed/acceleration deltas. EventEntry now carries full before/after snapshots (BallEventSnapshot) captured in applyEventSnapshots before and after applying state changes. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Slider appears in the transport bar, enabled when paused. Dragging it seeks to any point by replaying all events from initial state up to the target time. Shows 0s to maxTime range with current progress. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Cell transitions and contact resolution advanced ball time and updated trajectory.b and trajectory.c, but left trajectory.a unchanged. For rolling/sliding motion, trajectory.a depends on velocity direction (friction opposes motion), which changes along the quadratic path. The stale acceleration direction made the quartic polynomial fed to the collision detector inconsistent, producing spurious roots where balls were predicted to collide despite being far apart (~121mm vs required 75mm). Fixes: - Add Ball.rebaseTrajectory() that recomputes trajectory coefficients from the current motion model without re-determining motion state - Use rebaseTrajectory() in CellTransition handling and contact resolution instead of manually patching b/c coefficients - Add verification step in quartic detector: reject roots where the actual distance at the predicted time exceeds rSum by >1% This eliminates phantom collisions that caused balls to teleport together and receive physically impossible accelerations. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
- Transport bar: fluid width with flex-wrap, slider stretches to fill available space instead of fixed 256px - Event detail panel: stacks ball deltas vertically on small screens, max-height with overflow scroll - Ball inspector: smaller width and top-positioned on mobile - Event log: adjusted positioning to not overlap transport bar https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…nsitions rebaseTrajectory during cell transitions changed trajectory.a (acceleration direction), which invalidated already-scheduled state transition events. At the old scheduled time, advanceTime would evaluate the new trajectory where the ball's velocity is NOT zero, causing STATIONARY balls to spontaneously gain ~195 mm/s velocity from nothing. Reverted to the old approach: only update trajectory.b (velocity) and trajectory.c (position) during cell transitions and contact resolver advancement, leaving trajectory.a unchanged. The quartic verification step in ball-ball-detector.ts remains as the safety net against spurious roots from stale acceleration. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Root cause: cell transitions updated trajectory.b and .c but left .a (acceleration) stale. For Sliding/Rolling states, acceleration depends on velocity direction, which changes along the quadratic path. This caused spurious collision detections and incorrect advanceTime() evals. Previous fix attempt called rebaseTrajectory() but failed because it didn't invalidate stale state transition events. Fix: rebaseTrajectory + epoch++ + recompute() so stale events are skipped and new ones are scheduled with correct trajectory coefficients. Also adds: - Simulation validator with 8 physics invariant checks (NaN, speed sanity, spontaneous energy, overlaps, bounds, monotonic time, energy conservation, stationary stays stationary) - 100-seed fuzz test suite running 5s simulations with 5-50 balls each https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…fuzz tests
Architectural fix for ball teleportation through cushions:
1. Ball.syncTrajectoryOrigin() — single method replacing 6 scattered
manual trajectory.c patches across simulation.ts, collision.ts, and
contact-resolver.ts. Prevents stale trajectory.c from being missed
when new code paths modify ball position.
2. Ball.clampToBounds() — moves free function into Ball class and
auto-syncs trajectory origin after clamping. Every position clamp
now automatically keeps trajectory coherent.
3. Cell transition handler now calls clampToBounds() before
rebaseTrajectory(). Previously, advanceTime() could evaluate a
ball past the table wall, and the trajectory was rebased from that
out-of-bounds position.
4. assertBallInvariants() — runtime debug assertions checking bounds,
NaN, and trajectory.c coherence after every event. Enabled via
simulate({ debug: true }).
5. Enhanced fuzz tests: 100 quick (5s) + 20 long (30s) + 20 dense
corner (15s) seeds, all with debug assertions. 274 total tests pass.
https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ounds check Root cause of ball teleporting through cushions: advanceTime() evaluates the trajectory polynomial which can produce positions past the wall boundary. If the position is past the wall when the trajectory is rebased, the cushion detector computes the time when the ball crosses BACK through the wall (not the original collision time). This back-crossing time can be after the state transition time, so the state transition fires first, and clampToBounds teleports the ball back. Fixes: 1. ensureAdvanced() in contact resolver now calls clampToBounds() before rebaseTrajectory(). Previously, neighbors advanced past walls had their trajectory rebased from out-of-bounds positions. 2. Collision events in simulation.ts now clamp after advanceTime(), before snap-apart. Previously, balls could be past walls during snap-apart. 3. New TrajectoryOutOfBounds validator check: samples each ball's trajectory at 5 points between consecutive events and flags any wall penetration. This catches missed cushion events in the fuzz tests. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…r misses Root cause: when clampToBounds places a ball exactly at a wall boundary (position = R), the cushion detector's quadratic equation has roots at t=0 and t=-v/a. The t=0 root is filtered by `dt > Number.EPSILON`, and t=-v/a is the time the ball returns AFTER passing through the wall. Between these times, the trajectory goes past the wall — causing visible tunneling and eventual teleportation when a state transition clamps back. Fix: add direct contact checks after the root-finding loops. If the ball is within 0.01mm of a wall and velocity > 0.01mm/s into it, schedule an immediate collision (dt=1e-12). The Han2005 resolver then bounces the ball normally. No loops because the resolver sets velocity away from wall. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Previously the timeline slider was disabled during playback and only worked when paused. Now it works at all times: dragging while playing seeks to that point and continues playback from there, like a video player. Removed the paused guard from onSeek, enabled the slider unconditionally, and adjusted the start time after seeking during playback so the clock continues from the new position. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…vements The quartic (Ferrari's method) produced numerically spurious roots when balls were at near-contact distance, causing the verification step to reject valid collision predictions. This left ball pairs unmonitored, allowing overlap to grow unchecked — especially visible in break-22-with-spin where the cue's sliding trajectory reversed direction into rack-1. Three fixes: 1. Overlap guard now distinguishes approaching vs separating overlaps instead of blanket-skipping all overlapping pairs 2. Near-contact pairs (coeff0 ≈ 0) use elevated minimum root threshold to skip spurious near-zero roots from the current contact point 3. Bisection fallback when Ferrari's root fails verification — scans the exact distance function to find the true collision time Also adds inter-event trajectory sampling test that checks ALL ball pairs between events, catching overlaps the per-event check misses. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
The ball-ball detector's overlap guard (D0 <= 0, c1 < 0) was too sensitive to floating-point noise. After collision resolution snaps balls to exact touching distance, tiny numerical errors in position and velocity could make D'(0) barely negative, causing the same pair to re-collide infinitely at the same time. Added a scaled tolerance (relative to rSumSq) so only genuinely approaching balls trigger immediate collisions. Also added maxDt to trajectory coefficients for all motion models (previously only SlidingMotion set it), ensuring the quartic detector respects trajectory validity horizons across all motion states. Adjusted test timeouts for 150-ball pool physics scenarios which legitimately produce ~500k events (state transitions dominate). All 276 tests pass. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Two changes to dramatically reduce Zeno-like micro-collision cascades: 1. Progressive restitution: replace binary elastic/inelastic switch with smooth e(v) = clamp((|v| - 5) / (50 - 5), 0, 1). Balls at 30 mm/s now settle in ~3 bounces instead of ~20+. High-speed collisions (>50 mm/s) remain fully elastic. 2. Energy quiescence: balls with friction moving < 2 mm/s snap directly to Stationary, skipping the Sliding→Rolling→Stationary chain. Only applies when friction is present (zero-friction test balls unaffected). Results: 150-ball 20s simulation drops from 47s to 8.2s wall-clock (5.7x). All 276 tests pass. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
When external forces (from neighboring balls in a cluster) keep pushing
the same pair back together, the event queue floods with hundreds of
thousands of micro-collisions for that pair. The pair rate limiter
tracks collision frequency per pair within a sliding time window and
applies three tiers:
1. Normal (≤6 per 0.2s): progressive restitution as usual
2. Over budget (7-12): force fully inelastic (no bounce)
3. Suppressed (>12): pair excluded from ball-ball detection during
recompute() until the time window expires
Suppressed pairs are tracked globally and respected by ALL recompute
calls (state transitions, cushion bounces, contact resolution), not
just the primary collision. This prevents other code paths from
re-discovering and re-scheduling the suppressed pair.
Results for 150 balls, 20s simulation:
Before: 527k events, 47s
After: 8.6k events, 272ms (61x fewer events, 173x faster)
Worst-case seed (101 balls, previously 740k events / 14.7s):
Now: 3.2k events, 131ms
All 276 tests pass in 3s total.
https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Adds a per-ball coefficient of restitution for ball-ball collisions (eBallBall) to the physics config. Default 0.93 for pool balls, 1.0 for zero-friction test config. Not yet wired into the resolver — using it to cap the progressive ramp causes pair-suppression pass-through in dense clusters. Requires continuous contact handling first. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Ball event debugger: select a ball in the inspector, then click "Next Ball Event" (or Shift+→) to advance to the next event involving that ball. Skips unrelated events automatically. Mobile fix: EventDetailPanel is now collapsible (tap header to toggle) with a max-height of 48 on mobile, and uses z-10 so it no longer covers other UI elements. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Introduce contact-cluster-solver.ts: when a ball-ball collision fires, BFS discovers all touching neighbors, builds contact constraints, and solves them simultaneously via sequential impulse (Gauss-Seidel). This replaces both ElasticBallResolver (for the primary pair) and resolveContacts() (for cascade contacts). Key changes: - Fixed eBallBall restitution (0.93 default) replaces progressive ramp - Cluster solver handles diamonds, chains, triangles atomically - Pair rate limiter budget raised to 30 (safety net, rarely triggers) - Elastic resolver updated to use fixed eBallBall for simple 2D profile The cluster solver prevents the oscillation that occurred with sequential pair resolution in dense clusters, enabling use of the physical eBallBall without pair suppression pass-through. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…ectly The worker ignored the scenario's `physics: 'zero-friction'` field and always used pool physics (eBallBall=0.93). With pool physics, the striker retained residual velocity AND retained forward spin, causing friction to accelerate it back into the cluster (~179mm/s over ~228mm). - Move zeroFrictionConfig to production code (physics-config.ts) - Handle 'zero-friction' in worker scenario handler (profile + config) - Reduce lineOfBalls gap from 0.1mm to 0.5μm (within CONTACT_TOL) so cluster solver discovers the full Newton's cradle chain - Add striker-stops assertion to 5-ball Newton's cradle test https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
…t structure Documents the physics profile system, contact cluster solver, trajectory model, motion states, pair rate limiter, scenarios, debug UI, keyboard shortcuts, and updated known gotchas. Reflects the current architecture after all recent changes. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Physics panel with presets (Ice Table, Velvet Cloth, Super Elastic, Clay Balls, Moon/Jupiter Gravity, Zero Friction) and individual sliders for gravity, friction coefficients, and restitution. Overrides are sent to the worker on restart via PhysicsOverrides in the init message. Camera position and orbit target are now saved before restart and restored after scene re-initialization, so zooming/panning persists. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Ball was 50mm from the cushion — backspin friction could bleed too much energy before impact. Now starts 500mm away at 2000mm/s so it clearly reaches the cushion while still sliding. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
Two bugs in playback: 1. Unpausing jumped to current wall-clock time instead of resuming from where it was paused. Added _justUnpaused flag so the first unpaused frame returns frozenProgress, letting the animation loop adjust its start time. 2. Step-back, seek, and step-to-ball-event updated frozenProgress but the local progress variable used for rendering was stale. Changed to let and update after each operation. https://claude.ai/code/session_01Xr3jgFb8DViZsw4tGuWdqr
The step-back system replays events from initial state but wasn't saving/restoring trajectory.maxDt in snapshots. After replay, stale maxDt values caused positionAtTime() to either extrapolate wildly (pre-clamp) or freeze at wrong positions (post-clamp). Now maxDt is included in both CircleSnapshot and BallStateSnapshot, and restored in restoreInitialState(), applyEventSnapshots(), and initial load. 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