Detailed technical reference for the billiards simulation. For a quick overview, see CLAUDE.md.
Two swappable profiles bundle motion models, collision resolvers, and state determination:
- Pool (
createPoolPhysicsProfile): 5 motion states (Stationary, Spinning, Rolling, Sliding, Airborne), Han 2005 cushion resolver, 4-state friction model - Simple 2D (
createSimple2DProfile): 2 motion states (Stationary, Rolling), simple cushion reflection, no friction
Per-ball physics parameters (BallPhysicsParams):
mass: 0.17 kg (pool), 100 kg (zero-friction tests)radius: 37.5 mmmuSliding: 0.2,muRolling: 0.01,muSpinning: 0.044eRestitution: 0.85 (cushion),eBallBall: 0.93 (ball-ball)
Global config (PhysicsConfig): gravity = 9810 mm/s², cushionHeight = 10.1 mm
Three presets: defaultPhysicsConfig (pool), zeroFrictionConfig (ideal elastic, e=1.0, µ=0)
Stationary → Spinning → Rolling → Sliding → Airborne
Each state has a motion model that computes trajectory coefficients and transition times. State transitions are scheduled as events in the priority queue, just like collisions.
Position: r(t) = a·t² + b·t + c (polynomial relative to ball's reference time). Angular velocity: ω(t) = α·t + ω₀. Each trajectory has a maxDt validity horizon beyond which extrapolation is unphysical.
When a ball-ball collision fires, the solver:
- BFS discovery — finds all balls within
CONTACT_TOL(0.001 mm) via spatial grid - Snap-apart — iteratively resolves overlaps (5 passes)
- Constraint building — creates constraints only for approaching pairs (vRelN < 0)
- Sequential impulse (Gauss-Seidel) — iterates up to 20 times until convergence (0.01 mm/s threshold)
- Atomic application — updates trajectories once for all affected balls
Key constants: V_LOW = 5 mm/s (below this, e=0 perfectly inelastic), MAX_CLUSTER_SIZE = 200.
Accumulated impulse clamping (≥ 0) guarantees convergence — impulses can only push balls apart.
Prevents Zeno cascades (infinite collisions in finite time) for ball pairs:
- Tier 0 (≤ 30 collisions per 0.2s window): normal physics
- Tier 1 (31–60): force fully inelastic
- Tier 2 (> 60): suppress pair entirely until window resets
src/
├── index.ts # Entry point, animation loop, worker management
├── benchmark.ts # Performance benchmarking
├── lib/
│ ├── ball.ts # Ball class: 3D position/velocity, trajectory, epoch, physicsParams
│ ├── circle.ts # Legacy Circle class (still used by some renderers)
│ ├── collision.ts # CollisionFinder: MinHeap priority queue, spatial grid integration
│ ├── simulation.ts # simulate() — core event loop, cluster solver integration
│ ├── simulation.worker.ts # Web Worker: initialization, scenario loading, simulation
│ ├── config.ts # SimulationConfig interface + defaults
│ ├── physics-config.ts # BallPhysicsParams, PhysicsConfig, default/zeroFriction presets
│ ├── motion-state.ts # MotionState enum (Stationary, Spinning, Rolling, Sliding, Airborne)
│ ├── trajectory.ts # TrajectoryCoeffs (a·t²+b·t+c), evaluation functions
│ ├── spatial-grid.ts # SpatialGrid: broadphase, cell transitions
│ ├── min-heap.ts # Array-backed binary min-heap sorted by (time, seq)
│ ├── generate-circles.ts # Non-overlapping circle generation (brute-force placement)
│ ├── scenarios.ts # 50+ test/demo scenarios (single ball, multi-ball, edge cases)
│ ├── polynomial-solver.ts # Cubic/quartic algebraic solvers for collision detection
│ ├── vector2d.ts # Vector2D = [number, number]
│ ├── vector3d.ts # Vector3D = [number, number, number]
│ ├── string-to-rgb.ts # Deterministic ID → color mapping
│ ├── worker-request.ts # Worker request message types
│ ├── worker-response.ts # Worker response message types
│ ├── ui.ts # Tweakpane UI controls
│ ├── physics/
│ │ ├── physics-profile.ts # PhysicsProfile interface, Pool + Simple2D factories
│ │ ├── detection/
│ │ │ ├── collision-detector.ts # Unified detector: dispatches to ball-ball and cushion
│ │ │ ├── ball-ball-detector.ts # Quartic D(t) via Cardano + bisection
│ │ │ └── cushion-detector.ts # Linear/quadratic cushion collision times
│ │ ├── collision/
│ │ │ ├── collision-resolver.ts # Dispatcher: routes to ball/cushion resolvers
│ │ │ ├── contact-cluster-solver.ts # Simultaneous constraint solver (BFS + Gauss-Seidel)
│ │ │ ├── contact-resolver.ts # Post-collision contact resolution (legacy, kept for simple2d)
│ │ │ ├── elastic-ball-resolver.ts # Two-ball impulse resolver (used by simple2d profile)
│ │ │ ├── han2005-cushion-resolver.ts # Han 2005 cushion physics (spin effects, realistic angles)
│ │ │ └── simple-cushion-resolver.ts # Simple reflection cushion resolver
│ │ └── motion/
│ │ ├── motion-model.ts # MotionModel interface (getTrajectory, getTransitionTime)
│ │ ├── sliding-motion.ts # Sliding: friction decelerates, computes rolling transition
│ │ ├── rolling-motion.ts # Rolling: muRolling deceleration to stationary
│ │ ├── spinning-motion.ts # Spinning: z-axis spin decay via muSpinning
│ │ ├── stationary-motion.ts # Stationary: no motion, no transitions
│ │ └── airborne-motion.ts # Airborne: ballistic trajectory with gravity
│ ├── debug/
│ │ ├── playback-controller.ts # Pause, step, step-back, step-to-ball-event
│ │ ├── simulation-bridge.ts # Connects debug UI to simulation state
│ │ └── ball-inspector.ts # Per-ball state inspection
│ ├── renderers/
│ │ ├── renderer.ts # Base renderer class
│ │ ├── circle-renderer.ts # Ball rendering with collision indicators
│ │ ├── tail-renderer.ts # Motion trails
│ │ ├── future-trail-renderer.ts # Predicted future paths
│ │ ├── collision-renderer.ts # Next collision visualization
│ │ └── collision-preview-renderer.ts # Future collision previews
│ ├── scene/
│ │ └── simulation-scene.ts # Three.js 3D scene, lights, camera
│ └── __tests__/ # See Testing section
└── ui/
├── index.tsx # React UI entry point
├── components/
│ ├── Sidebar.tsx # Main debug sidebar
│ ├── BallInspectorPanel.tsx # Per-ball inspector with "Next Ball Event" button
│ ├── EventDetailPanel.tsx # Collision event details (collapsible on mobile)
│ ├── EventLog.tsx # Event history
│ ├── DebugOverlay.tsx # Debug overlay
│ ├── DebugVisualizationPanel.tsx # Debug visualization controls
│ ├── OverlayTogglesPanel.tsx # Renderer toggle controls
│ ├── PhysicsPanel.tsx # Physics preset buttons + parameter sliders
│ ├── ScenarioPanel.tsx # Scenario selection UI
│ ├── SimulationStatsPanel.tsx # Performance stats
│ └── TransportBar.tsx # Play/pause/step controls
└── hooks/
├── use-simulation.ts # Simulation state management hook
└── use-keyboard-shortcuts.ts # Keyboard shortcuts (Space, arrows, Shift+→)
Tests are in src/lib/__tests__/ using Vitest with globals enabled. Run with npm test.
Test files:
single-ball-motion.test.ts— friction deceleration, sliding→rolling, spin decay, energy conservationcushion-collision.test.ts— head-on, angled, with spin, airborne, corner bouncesball-ball-collision.test.ts— velocity swap, mass ratios, glancing, spin preservation, inelastic threshold, energy conservationmulti-ball.test.ts— Newton's cradle (3 & 5 ball), V-shape, triangle break, 4-ball convergence, 150-ball stress testedge-cases.test.ts— exactly-touching, at cushion, zero-velocity-z-spin, simultaneous collisionsinvariants.test.ts— no-overlap, monotonic time, momentum conservation, bounds enforcementseek-replay.test.ts— seek + replay position accuracy across scenarioscollision.test.ts— collision detection unit testscircle.test.ts— Circle/Ball class unit testsspatial-grid.test.ts— spatial grid unit testspolynomial-solver.test.ts— cubic/quartic solver accuracyperf-150.test.ts,perf-quick.test.ts,perf-compare.test.ts— performance benchmarksfuzz.test.ts— randomized stress testing
Test helpers: test-helpers.ts provides ball factories, runScenario(), and assertion helpers (assertNoOverlaps, assertInBounds, assertMonotonicTime).
- Space — pause/resume
- → — step to next event (when paused)
- ← — step back (when paused)
- Shift+→ — step to next event for selected ball (when paused, ball inspector open)
- + / - — increase/decrease simulation speed (by 0.5x)
- 1–5 — set simulation speed to 1x–5x
- I — toggle ball inspector
- F — toggle future trails
- T — toggle tails
- C — toggle collision visualization
- Escape — clear ball selection