Skip to content

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

Open
TimBeyer wants to merge 50 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 50 commits intomasterfrom
claude/plan-pool-physics-7B0IT

Conversation

@TimBeyer
Copy link
Copy Markdown
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
Copy Markdown
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 33b9d44 Commit Preview URL

Branch Preview URL
Mar 28 2026, 12:21 PM

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 25, 2026

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 231.14 1297.22 +461.23% 🚀
20 circles / 60s 151.05 565.63 +274.46% 🚀
40 circles / 60s 52.47 215.80 +311.29% 🚀
80 circles / 60s 14.13 46.67 +230.28% 🚀
150 circles / 60s 3.48 5.04 +44.80% 🚀
300 circles / 60s 1.48 2.33 +57.18% 🚀
500 circles / 60s 0.77 1.35 +74.95% 🚀
1000 circles / 60s 0.33 0.87 +166.91% 🚀

Overall: +369.37% 🚀


Merge base: 46a5cce | PR commit: 33b9d44 | 2026-03-28 12:22 UTC

Previous runs

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 207.38 1326.12 +539.47% 🚀
20 circles / 60s 138.68 572.29 +312.68% 🚀
40 circles / 60s 54.04 219.58 +306.34% 🚀
80 circles / 60s 13.44 44.08 +227.97% 🚀
150 circles / 60s 3.41 5.13 +50.68% 🚀
300 circles / 60s 1.48 2.36 +58.93% 🚀
500 circles / 60s 0.76 1.32 +72.50% 🚀
1000 circles / 60s 0.33 0.87 +162.89% 🚀

Overall: +417.68% 🚀


Merge base: 46a5cce | PR commit: aa40e4a | 2026-03-28 12:11 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 254.11 1342.95 +428.49% 🚀
20 circles / 60s 135.41 573.37 +323.43% 🚀
40 circles / 60s 49.86 228.44 +358.17% 🚀
80 circles / 60s 13.53 45.82 +238.57% 🚀
150 circles / 60s 3.40 5.10 +49.79% 🚀
300 circles / 60s 1.48 2.40 +61.99% 🚀
500 circles / 60s 0.76 1.34 +76.55% 🚀
1000 circles / 60s 0.32 0.87 +167.48% 🚀

Overall: +379.49% 🚀


Merge base: 46a5cce | PR commit: 944421a | 2026-03-28 12:05 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 250.85 1288.20 +413.53% 🚀
20 circles / 60s 140.68 563.81 +300.77% 🚀
40 circles / 60s 54.30 223.80 +312.16% 🚀
80 circles / 60s 14.14 43.73 +209.18% 🚀
150 circles / 60s 3.44 5.06 +47.17% 🚀
300 circles / 60s 1.50 2.36 +57.28% 🚀
500 circles / 60s 0.78 1.35 +73.24% 🚀
1000 circles / 60s 0.33 0.87 +162.24% 🚀

Overall: +356.88% 🚀


Merge base: 46a5cce | PR commit: 36dd867 | 2026-03-28 11:35 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 246.63 1111.77 +350.78% 🚀
20 circles / 60s 142.74 610.12 +327.44% 🚀
40 circles / 60s 55.05 213.50 +287.86% 🚀
80 circles / 60s 13.72 44.00 +220.73% 🚀
150 circles / 60s 3.36 4.92 +46.42% 🚀
300 circles / 60s 1.44 2.32 +60.90% 🚀
500 circles / 60s 0.74 1.31 +76.59% 🚀
1000 circles / 60s 0.31 0.84 +167.70% 🚀

Overall: +328.62% 🚀


Merge base: 46a5cce | PR commit: 4af7041 | 2026-03-28 11:28 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 236.05 1107.23 +369.05% 🚀
20 circles / 60s 140.49 579.68 +312.60% 🚀
40 circles / 60s 52.87 230.49 +335.94% 🚀
80 circles / 60s 13.84 42.61 +207.93% 🚀
150 circles / 60s 3.50 4.99 +42.51% 🚀
300 circles / 60s 1.54 2.34 +51.57% 🚀
500 circles / 60s 0.79 1.36 +71.56% 🚀
1000 circles / 60s 0.34 0.89 +163.13% 🚀

Overall: +338.24% 🚀


Merge base: 46a5cce | PR commit: cc0e9c7 | 2026-03-28 10:49 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 249.31 1049.33 +320.89% 🚀
20 circles / 60s 143.16 598.91 +318.36% 🚀
40 circles / 60s 54.89 216.85 +295.07% 🚀
80 circles / 60s 13.66 42.30 +209.69% 🚀
150 circles / 60s 3.37 4.51 +33.83% 🚀
300 circles / 60s 1.44 2.20 +52.95% 🚀
500 circles / 60s 0.74 1.28 +73.39% 🚀
1000 circles / 60s 0.31 0.83 +171.89% 🚀

Overall: +310.44% 🚀


Merge base: 46a5cce | PR commit: f7e5041 | 2026-03-28 10:27 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 250.85 1316.95 +424.99% 🚀
20 circles / 60s 143.13 591.30 +313.12% 🚀
40 circles / 60s 53.70 201.68 +275.57% 🚀
80 circles / 60s 14.43 45.22 +213.43% 🚀
150 circles / 60s 3.41 4.24 +24.24% 🚀
300 circles / 60s 1.47 3.10 +110.66% 🚀
500 circles / 60s 0.75 1.92 +154.01% 🚀
1000 circles / 60s 0.32 1.00 +218.21% 🚀

Overall: +362.63% 🚀


Merge base: 46a5cce | PR commit: df15876 | 2026-03-28 09:58 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 252.68 1079.77 +327.33% 🚀
20 circles / 60s 151.73 554.20 +265.26% 🚀
40 circles / 60s 56.55 203.87 +260.49% 🚀
80 circles / 60s 14.06 37.01 +163.27% 🚀
150 circles / 60s 3.56 4.40 +23.33% 🚀
300 circles / 60s 1.51 3.16 +109.78% 🚀
500 circles / 60s 0.79 1.97 +149.65% 🚀
1000 circles / 60s 0.34 1.04 +207.89% 🚀

Overall: +291.81% 🚀


Merge base: 46a5cce | PR commit: 4f1d2d1 | 2026-03-28 09:48 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 244.59 1248.91 +410.62% 🚀
20 circles / 60s 138.30 563.12 +307.18% 🚀
40 circles / 60s 50.03 206.01 +311.76% 🚀
80 circles / 60s 12.87 46.79 +263.51% 🚀
150 circles / 60s 3.39 4.42 +30.20% 🚀
300 circles / 60s 1.43 3.04 +112.62% 🚀
500 circles / 60s 0.75 1.91 +154.42% 🚀
1000 circles / 60s 0.30 0.95 +220.77% 🚀

Overall: +359.45% 🚀


Merge base: 46a5cce | PR commit: 61c1967 | 2026-03-28 09:20 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 203.67 750.20 +268.34% 🚀
20 circles / 60s 149.57 636.04 +325.24% 🚀
40 circles / 60s 54.46 143.97 +164.39% 🚀
80 circles / 60s 14.14 33.61 +137.66% 🚀
150 circles / 60s 3.42 0.33 -90.38% 🔴
300 circles / 60s 1.48 0.41 -72.36% 🔴
500 circles / 60s 0.76 0.13 -83.33% 🔴
1000 circles / 60s 0.32 0.02 -92.82% 🔴

Overall: +265.74% 🚀


Merge base: 46a5cce | PR commit: 86f25bb | 2026-03-28 09:13 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 213.06 1400.26 +557.21% 🚀
20 circles / 60s 140.35 507.69 +261.72% 🚀
40 circles / 60s 55.71 156.22 +180.39% 🚀
80 circles / 60s 14.08 27.56 +95.77% 🚀
150 circles / 60s 3.47 4.86 +40.03% 🚀
300 circles / 60s 1.49 2.95 +97.96% 🚀
500 circles / 60s 0.76 2.09 +173.78% 🚀
1000 circles / 60s 0.33 0.89 +166.69% 🚀

Overall: +389.80% 🚀


Merge base: 46a5cce | PR commit: e0dc193 | 2026-03-28 06:29 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 216.51 904.48 +317.74% 🚀
20 circles / 60s 150.90 782.50 +418.54% 🚀
40 circles / 60s 55.46 295.47 +432.75% 🚀
80 circles / 60s 14.95 89.39 +497.83% 🚀
150 circles / 60s 3.50 16.33 +366.76% 🚀
300 circles / 60s 1.52 9.75 +543.04% 🚀
500 circles / 60s 0.78 4.77 +513.89% 🚀
1000 circles / 60s 0.33 2.42 +624.29% 🚀

Overall: +374.17% 🚀


Merge base: 46a5cce | PR commit: 739d030 | 2026-03-27 19:32 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 230.93 1080.72 +367.98% 🚀
20 circles / 60s 145.62 790.65 +442.97% 🚀
40 circles / 60s 53.22 284.56 +434.66% 🚀
80 circles / 60s 14.57 87.47 +500.19% 🚀
150 circles / 60s 3.38 16.28 +381.67% 🚀
300 circles / 60s 1.48 9.19 +521.53% 🚀
500 circles / 60s 0.76 4.19 +454.55% 🚀
1000 circles / 60s 0.32 2.12 +571.11% 🚀

Overall: +405.28% 🚀


Merge base: 46a5cce | PR commit: 50826e8 | 2026-03-27 19:28 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 247.40 1088.03 +339.78% 🚀
20 circles / 60s 149.95 700.47 +367.14% 🚀
40 circles / 60s 52.56 306.54 +483.24% 🚀
80 circles / 60s 14.56 76.49 +425.39% 🚀
150 circles / 60s 3.44 17.50 +408.16% 🚀
300 circles / 60s 1.50 9.90 +561.73% 🚀
500 circles / 60s 0.77 4.10 +431.22% 🚀
1000 circles / 60s 0.33 2.30 +598.05% 🚀

Overall: +368.71% 🚀


Merge base: 46a5cce | PR commit: f14a01b | 2026-03-27 18:54 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 234.38 1457.41 +521.82% 🚀
20 circles / 60s 157.05 678.81 +332.21% 🚀
40 circles / 60s 54.99 292.22 +431.40% 🚀
80 circles / 60s 14.55 90.36 +520.93% 🚀
150 circles / 60s 3.49 17.55 +403.49% 🚀
300 circles / 60s 1.51 9.96 +559.13% 🚀
500 circles / 60s 0.78 4.11 +423.14% 🚀
1000 circles / 60s 0.33 2.18 +556.16% 🚀

Overall: +446.49% 🚀


Merge base: 46a5cce | PR commit: ec52fb3 | 2026-03-27 18:25 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 247.65 1367.12 +452.04% 🚀
20 circles / 60s 116.92 603.08 +415.80% 🚀
40 circles / 60s 54.63 293.62 +437.46% 🚀
80 circles / 60s 13.42 73.70 +448.97% 🚀
150 circles / 60s 3.43 16.00 +366.20% 🚀
300 circles / 60s 1.49 8.97 +503.50% 🚀
500 circles / 60s 0.76 3.91 +413.07% 🚀
1000 circles / 60s 0.33 2.29 +601.56% 🚀

Overall: +440.02% 🚀


Merge base: 46a5cce | PR commit: abb402c | 2026-03-27 18:08 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 203.32 1768.11 +769.60% 🚀
20 circles / 60s 144.91 707.32 +388.12% 🚀
40 circles / 60s 50.75 306.25 +503.40% 🚀
80 circles / 60s 13.31 95.66 +618.71% 🚀
150 circles / 60s 3.38 18.48 +446.30% 🚀
300 circles / 60s 1.46 10.68 +631.69% 🚀
500 circles / 60s 0.75 5.12 +587.17% 🚀
1000 circles / 60s 0.31 2.55 +730.33% 🚀

Overall: +596.85% 🚀


Merge base: 46a5cce | PR commit: 4e3df33 | 2026-03-27 17:38 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 210.40 948.79 +350.94% 🚀
20 circles / 60s 148.32 581.27 +291.89% 🚀
40 circles / 60s 54.50 325.13 +496.55% 🚀
80 circles / 60s 14.50 92.86 +540.47% 🚀
150 circles / 60s 3.50 18.79 +436.49% 🚀
300 circles / 60s 1.49 11.14 +646.94% 🚀
500 circles / 60s 0.79 5.84 +637.22% 🚀
1000 circles / 60s 0.34 2.73 +701.41% 🚀

Overall: +357.88% 🚀


Merge base: 46a5cce | PR commit: 6867354 | 2026-03-27 17:30 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 254.03 1679.23 +561.02% 🚀
20 circles / 60s 144.72 744.50 +414.44% 🚀
40 circles / 60s 53.96 300.83 +457.52% 🚀
80 circles / 60s 14.65 83.42 +469.28% 🚀
150 circles / 60s 3.52 17.54 +397.79% 🚀
300 circles / 60s 1.51 10.64 +602.05% 🚀
500 circles / 60s 0.78 5.45 +602.55% 🚀
1000 circles / 60s 0.33 2.63 +695.81% 🚀

Overall: +500.67% 🚀


Merge base: 46a5cce | PR commit: 2580457 | 2026-03-27 15:46 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 256.95 1535.42 +497.57% 🚀
20 circles / 60s 145.34 727.47 +400.54% 🚀
40 circles / 60s 55.02 313.28 +469.35% 🚀
80 circles / 60s 14.25 73.85 +418.35% 🚀
150 circles / 60s 3.39 15.04 +343.77% 🚀
300 circles / 60s 1.45 9.25 +540.28% 🚀
500 circles / 60s 0.77 5.00 +548.46% 🚀
1000 circles / 60s 0.33 2.36 +621.30% 🚀

Overall: +461.62% 🚀


Merge base: 46a5cce | PR commit: 9b01094 | 2026-03-27 15:40 UTC

Benchmark Comparison

Benchmark Baseline (ops/sec) PR (ops/sec) Change
10 circles / 60s 243.10 1735.16 +613.77% 🚀
20 circles / 60s 138.72 762.27 +449.50% 🚀
40 circles / 60s 52.81 311.99 +490.74% 🚀
80 circles / 60s 13.56 87.31 +543.99% 🚀
150 circles / 60s 3.31 14.95 +352.34% 🚀
300 circles / 60s 1.42 9.50 +569.01% 🚀
500 circles / 60s 0.74 4.62 +525.83% 🚀
1000 circles / 60s 0.31 2.28 +646.50% 🚀

Overall: +545.01% 🚀


Merge base: 46a5cce | PR commit: c34c20f | 2026-03-27 15:34 UTC

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

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 27 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
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
claude added 22 commits March 27, 2026 15:39
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
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