Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PhysicsLab

A polished educational 2D physics sandbox for learning vectors, mechanics, momentum, and collision physics. Built with Python 3.12+, pygame-ce, and numpy.

Spawn circles and rectangles, drag and throw them, build pendulums, switch between Earth-style and planet (N-body) gravity, and toggle overlays that decompose every collision into the impulses and velocity components that drive it.

Features

  • Bodies — circles and AABB rectangles with mass, elasticity, friction, velocity, and (for circles) angular velocity / moment of inertia.
  • Engine — semi-implicit Euler integrator with sub-stepping, configurable collision iterations, broad-phase uniform spatial grid, and Baumgarte position correction for stable stacks.
  • Collisions — circle↔circle, circle↔rect, rect↔rect detection plus an impulse-based resolver with normal/friction impulses, restitution blending, and Coulomb-capped friction. Circles also pick up spin from lever-arm friction at the contact point.
  • Constraints — two-body springs and anchored springs (Hooke + viscous damping), with a one-key pendulum spawn at the cursor.
  • Two gravity modes — constant linear gravity (default), or N-body inverse-square planet gravity with softening to keep close encounters sane.
  • Interaction — drag bodies with the mouse (they go kinematic and become infinite mass for the resolver); release with motion to throw at velocity sampled from the last 150 ms of cursor history.
  • Stats — real-time FPS, body count, broad-phase candidate-pair count, contact count, total kinetic energy (linear + rotational), and total linear-momentum magnitude.
  • Visualization — velocity / force / contact-normal arrows, position trails fading toward the background, collision spark particles, and an educational overlay that draws the resolved normal & friction impulse vectors at every contact alongside the pre-impulse approach speed.
  • Modern dark theme — single Theme dataclass drives every color in the renderer and HUD.

Architecture

The project is split into small, single-responsibility modules. Physics never imports rendering, and rendering never mutates physics state.

PhysicsLab/
├── main.py                    entry point
├── app.py                     Application — owns window, world, renderer,
│                              hud, particles, main loop
├── config.py                  immutable config dataclasses (window /
│                              simulation / physics / theme)
├── physics/
│   ├── vector.py              Vector2 — immutable 2D vector primitive
│   ├── bounds.py              Bounds — axis-aligned world boundary
│   ├── manifold.py            Contact — single-point collision manifold
│   ├── collision.py           pairwise detection + dispatcher
│   ├── resolver.py            impulse resolution (linear + angular) +
│   │                          positional correction
│   ├── broad_phase.py         UniformGrid — spatial pair candidate
│   ├── constraints.py         Spring + AnchoredSpring (Hooke + damping)
│   ├── particles.py           ParticleSystem — spark emitter
│   ├── stats.py               compute_stats(world) — KE / momentum
│   └── world.py               World — engine, gravity modes, sub-stepping
├── entities/
│   ├── body.py                abstract Body
│   ├── static_body.py         StaticBody — fixed-position
│   ├── dynamic_body.py        DynamicBody — linear + angular state +
│   │                          semi-implicit Euler integrator
│   ├── circle_body.py         CircleBody — overrides inverse_inertia
│   ├── rectangle_body.py      RectangleBody — AABB (no rotation)
│   └── factory.py             BodyFactory — palette-driven spawning
├── rendering/
│   ├── theme.py               Theme dataclass — dark color palette
│   ├── vectors.py             arrow-drawing helper for overlays
│   ├── trails.py              TrailRecorder — per-body position history
│   └── renderer.py            Renderer — draw pipeline, consumes a World
├── ui/
│   ├── fonts.py               FontCache — lazy-loaded system fonts
│   ├── hud.py                 Hud — stats panel, controls hint, banners
│   └── interaction.py         InteractionController — drag/throw/spawn
└── utils/
    └── clock.py               FrameClock — pygame clock + smoothed FPS

Design rules

  • Physics is pure. physics/ and entities/ import nothing from pygame.
  • Rendering is read-only. Renderer consumes a World; it never mutates it.
  • UI is a layer. The Hud draws on top of the rendered world; the world has no awareness of it.
  • Config is immutable. All config objects are frozen dataclasses with slots.
  • Type hints everywhere. from __future__ import annotations is used uniformly so forward references stay clean.
  • Vector2 operations don't silently broadcast. vec + 5 raises TypeError; the math reads exactly as it's intended.

Running locally

Requires Python 3.12 or newer.

python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate

pip install -r requirements.txt
python main.py

The window opens at 1280×720, is resizable, runs capped at 60 FPS.

Controls

Input Action
Left click On a body → drag it; on empty space → spawn a circle
Left release Release a dragged body; sustained motion → throw
Right click Spawn a rectangle
P Spawn a pendulum (anchored spring) at the cursor
Space Pause / resume the simulation
R Reset — clears all bodies, constraints, trails, sparks
G Toggle linear gravity on / off
O Toggle planet (inverse-square) gravity mode
Tab Toggle 0.25× slow-motion
V Toggle velocity / force / contact-normal arrows
T Toggle motion trails behind dynamic bodies
E Toggle educational overlay (impulse decomposition)
Esc Quit

Physics

The World is the engine. Each frame's dt is clamped to max_dt and divided into substeps sub-steps. Per sub-step:

  1. Apply pair-wise planet gravity (if gravity_mode == PLANET).
  2. Apply spring / pendulum constraints (Hooke + damping).
  3. For each dynamic body: add linear gravity, linear damping, angular damping; integrate position & orientation via semi-implicit Euler.
  4. Broad-phase candidates via the uniform spatial grid → narrow-phase detect → resolver, repeated collision_iterations times for stable stacks.
  5. Resolve world-boundary collisions (clamp + reflect + tangential friction).

Collision detection

Three pairwise detectors live in physics/collision.py — circle–circle, AABB–AABB, and circle–AABB — plus a detect() dispatcher that preserves the caller's (A, B) ordering and flips the contact normal when the shape order is reversed. Every contact carries one point, a unit normal pointing A → B, and a positive penetration depth.

Resolution

Resolution lives in physics/resolver.py:

Step Math
Approach test v_n = (v_b_at_p − v_a_at_p) · n ; if v_n > 0 → only correct overlap
Effective mass m⁻¹_eff = 1/m_a + 1/m_b + (r_a×n)²/I_a + (r_b×n)²/I_b
Normal impulse j = −(1+e) · v_n / m⁻¹_eff
Friction (tangent) j_t = −v_rel·t / m⁻¹_eff_t, capped at μ · j
Coefficient blending e = min(e_a, e_b), μ = √(μ_a · μ_b)
Positional correction Δp = max(d − slop, 0) · % · n

Both normal and tangent impulses also feed Δω = (r×J) · I⁻¹ into each body's angular velocity, so glancing collisions spin circles correctly. Static bodies are modelled by inverse_mass = 0; RectangleBody.inverse_inertia = 0 keeps rectangles non-rotating (their AABB collision shape would be a lie under spin until SAT is implemented).

Constraints (springs, pendulum)

physics/constraints.py defines a Constraint protocol with apply() and endpoints(). Two implementations:

  • Spring(body_a, body_b, rest_length, stiffness, damping) — two-body damped Hooke: F = (k · ext + c · v_radial) along A → B.
  • AnchoredSpring(body, anchor, rest_length, stiffness, damping) — same math, but one end is a fixed world-space point. P spawns one of these as an instant pendulum.

Constraints apply via apply_force, so spring tension flows through the same acceleration accumulator as gravity — students see Hooke's law acting on the integrator, not hidden behind a positional solver.

Gravity modes

Mode Force
LINEAR constant m · g_vec on every dynamic body
PLANET pair-wise G · m_a · m_b / max(r, softening)² along A → B

Total linear momentum is conserved in PLANET mode by Newton's third law.

Vector math

Vector2 is the foundational geometry primitive: positions, velocities, forces, normals, and impulses are all Vector2 instances. It's an immutable frozen dataclass — operations return new instances, which keeps integration and collision math free of aliasing bugs.

import math
from physics import Vector2

position = Vector2(100.0, 200.0)
velocity = Vector2.from_angle(math.pi / 4, magnitude=50.0)
gravity  = Vector2(0.0, 980.0)

new_pos = position + velocity * dt          # operator overloaded
speed   = velocity.magnitude
unit    = velocity.normalized()
turn    = velocity.cross(target_velocity)   # signed: + CCW, − CW
normal  = surface.perpendicular()
bounced = velocity.reflected(normal)

Running tests

python -m unittest discover tests

The tests/ suite covers Vector2 (construction, arithmetic, geometry, type guards); body integration; collision detection for all three shape pairs; impulse resolution including momentum & energy conservation invariants; rotation (inertia, friction-induced spin, head-on no-spin); the broad-phase grid (dedup, cell-spanning bodies, mixed shapes); constraints; world stats; trails; particles; planet gravity (inverse square, softening, kinematic exclusion, mode switching); the educational overlay's impulse-capture; and the interaction system (hit-test, drag, throw cap, kinematic invariants).

Status

All ten parts of the development roadmap are complete: foundation → vector math → physics bodies → engine core → collision system → interaction → stats & visualization → optimization → advanced physics (springs, pendulum, rotation, planet gravity, particles) → educational overlays + polish.

About

Interactive 2D physics sandbox and educational simulation engine built with Python. Features real-time collision physics, momentum visualization, vector mechanics, impulse resolution, and modular architecture for learning mathematics and mechanics.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages