"""
Simulation runner for physics visualization
Manages the main simulation loop, input handling, and rendering.
"""
import pygame
from .physics_engine_core import PhysicsWorld, Vector2
from .renderer import Renderer
[docs]
class Simulation:
"""Base class for physics simulations
Handles the main loop, input, timing, and rendering.
Subclass this to create specific demo scenarios.
"""
def __init__(self, width=1280, height=720, title="Physics Simulation", record_gif=None, record_duration=10):
"""Initialize simulation
Args:
width: Window width in pixels
height: Window height in pixels
title: Window title
record_gif: Optional path to save GIF recording
record_duration: Duration to record in seconds (default: 10)
"""
# Create renderer
self.renderer = Renderer(width, height, title)
# Create physics world
self.world = PhysicsWorld()
self.world.set_gravity(Vector2(0, -9.81)) # Earth gravity
# Simulation state
self.running = False
self.paused = False
self.show_help = False
# Timing
self.time_scale = 1.0 # Speed multiplier
self.fixed_dt = 1.0 / 60.0 # Fixed timestep (60 Hz)
self.accumulator = 0.0 # For fixed timestep
# Input state
self.mouse_pos = (0, 0)
self.mouse_down = False
self.mouse_drag_start = None
# GIF recording
self.gif_recorder = None
if record_gif:
from .gif_recorder import GifRecorder
self.gif_recorder = GifRecorder(record_gif, duration=record_duration, fps=15)
[docs]
def setup(self):
"""Setup simulation (override in subclasses)
Create bodies, set initial conditions, etc.
"""
pass
[docs]
def update(self, dt):
"""Update simulation logic (override in subclasses)
Args:
dt: Delta time in seconds
"""
pass
[docs]
def on_key_press(self, key):
"""Handle key press events (override in subclasses)
Args:
key: pygame key constant
"""
pass
[docs]
def on_mouse_click(self, pos, button):
"""Handle mouse click events (override in subclasses)
Args:
pos: Mouse position (x, y) in screen space
button: Mouse button (1=left, 2=middle, 3=right)
"""
pass
[docs]
def handle_key_press(self, key):
"""Handle keyboard input
Args:
key: pygame key constant
"""
if key == pygame.K_ESCAPE:
self.running = False
elif key == pygame.K_SPACE:
self.paused = not self.paused
elif key == pygame.K_r:
self.reset()
elif key == pygame.K_g:
self.renderer.show_grid = not self.renderer.show_grid
elif key == pygame.K_v:
self.renderer.show_velocity = not self.renderer.show_velocity
elif key == pygame.K_h:
self.show_help = not self.show_help
elif key == pygame.K_EQUALS or key == pygame.K_PLUS:
self.time_scale *= 1.2
elif key == pygame.K_MINUS:
self.time_scale *= 0.8
# Pass to subclass handler
self.on_key_press(key)
[docs]
def reset(self):
"""Reset simulation to initial state"""
self.world.clear()
self.setup()
self.paused = False
[docs]
def step_physics(self, dt):
"""Step physics simulation with fixed timestep
Uses accumulator for stable integration.
Args:
dt: Delta time in seconds
"""
if self.paused:
return
# Apply time scale
dt *= self.time_scale
# Accumulator for fixed timestep
self.accumulator += dt
# Step physics in fixed increments
while self.accumulator >= self.fixed_dt:
self.world.step(self.fixed_dt)
self.accumulator -= self.fixed_dt
[docs]
def render(self):
"""Render frame"""
self.renderer.clear()
self.renderer.draw_world(self.world)
if self.show_help:
self.renderer.draw_help()
if self.paused:
self.renderer.draw_text("PAUSED", (self.renderer.width // 2 - 50, 10),
font='large', color='highlight')
# Show recording indicator
if self.gif_recorder and self.gif_recorder.is_recording():
self.renderer.draw_text("🎬 RECORDING", (10, 10),
font='large', color='red')
self.renderer.flip()
# Capture frame for GIF
if self.gif_recorder:
if not self.gif_recorder.capture(self.renderer.screen):
# Recording finished
self.gif_recorder.save()
self.gif_recorder = None
self.running = False # Auto-quit after recording
[docs]
def run(self):
"""Run simulation loop"""
self.running = True
# Setup simulation
self.setup()
# Main loop
while self.running:
# Handle input
self.handle_input()
# Get delta time
dt = self.renderer.tick(60)
# Update custom logic
self.update(dt)
# Step physics
self.step_physics(dt)
# Render
self.render()
# Cleanup
self.renderer.quit()
[docs]
class InteractiveSandbox(Simulation):
"""Interactive physics sandbox
Click to spawn objects, drag to apply forces.
"""
def __init__(self):
super().__init__(title="Interactive Physics Sandbox")
self.spawn_mode = 'circle' # Type of object to spawn
[docs]
def setup(self):
"""Setup sandbox with ground"""
from .physics_engine_core import RigidBody, BoxCollider
# Create ground (box platform)
ground = RigidBody()
ground_box = BoxCollider(150.0, 3.0) # Very wide platform, 3m thick
ground.set_collider(ground_box)
ground.set_static()
ground.set_position(Vector2(0, -10))
self.world.add_body(ground)
[docs]
def on_mouse_click(self, pos, button):
"""Spawn object on click
Args:
pos: Mouse position in screen space
button: Mouse button
"""
from .physics_engine_core import RigidBody, CircleCollider
import random
if button == 1: # Left click spawns object
# Convert to world space
world_pos = self.renderer.camera.screen_to_world(pos)
# Create ball
ball = RigidBody()
radius = random.uniform(0.5, 2.0)
collider = CircleCollider(radius)
ball.set_collider(collider)
mass = collider.calculate_mass(1.0) # Density = 1
inertia = collider.calculate_inertia(mass)
ball.set_mass(mass)
ball.set_moment_of_inertia(inertia)
ball.set_position(world_pos)
ball.set_restitution(random.uniform(0.3, 0.9))
ball.set_friction(0.5)
self.world.add_body(ball)
[docs]
def on_key_press(self, key):
"""Handle additional keys
Args:
key: pygame key constant
"""
if key == pygame.K_c:
self.world.clear()
self.setup()