from __future__ import annotations
import sys
import time
import pygame
from electrosim import config
from electrosim.simulation.engine import Simulation
from electrosim.rendering.draw import (
draw_field_grid,
draw_meter_grid,
draw_force_vectors,
draw_overlay,
draw_particle_glows,
draw_particles,
draw_trails,
draw_velocity_vectors,
)
from electrosim.ui.controls import InputState, handle_events, render_placement_preview, render_hover_tooltip
from electrosim.rendering.trails import draw_polyline_world
[docs]
def main() -> None:
"""Run the main event loop: handle input, step simulation, and render.
Initializes Pygame and the window, constructs the `Simulation` and `InputState`,
and iterates frames by processing input, stepping the simulation by substeps,
drawing the scene, and overlaying performance and state metrics.
"""
pygame.init()
screen = pygame.display.set_mode((config.WINDOW_WIDTH_PX, config.WINDOW_HEIGHT_PX))
pygame.display.set_caption("ElectroSim - Charged particle simulator")
clock = pygame.time.Clock()
font = pygame.font.SysFont("consolas,sans-serif", 16)
ppm = config.PIXELS_PER_METER
sim = Simulation()
input_state = InputState()
while True:
frame_t0 = time.perf_counter()
# Events and controls
handle_events(pygame, sim, input_state, ppm)
# Logic and physics step
t_ph0 = time.perf_counter()
sim.step_frame()
t_ph1 = time.perf_counter()
physics_ms = (t_ph1 - t_ph0) * 1000.0
# Drawing
field_ms = 0.0
t_draw0 = time.perf_counter()
draw_force_vectors(screen, sim.particles, sim.last_forces, ppm)
screen.fill(config.COLOR_BG)
if sim.show_meter_grid:
draw_meter_grid(screen, sim.world_size_m, ppm)
if sim.show_field or getattr(sim, "validation_active", False) or getattr(config, "UNIFORM_FIELD_VISUAL_OVERRIDE", False):
FIELD_GRID_STEP_PX = config.FIELD_GRID_STEP_PX
if config.FIELD_VIS_MODE != "brightness":
FIELD_GRID_STEP_PX = int(config.FIELD_GRID_STEP_PX*2.6)
t_f0 = time.perf_counter()
draw_field_grid(screen, sim.particles, sim.world_size_m, ppm, FIELD_GRID_STEP_PX, config.SOFTENING_FRACTION)
t_f1 = time.perf_counter()
field_ms += (t_f1 - t_f0) * 1000.0
# Glow under trajectories and particles
draw_particle_glows(screen, sim.particles, ppm)
if sim.show_trails:
draw_trails(screen, sim.particles, ppm)
# Theory: draw analytical trajectory when validation is active
if getattr(sim, "validation_active", False) and sim.validation_theory_traj:
points = [xy for (_, xy) in sim.validation_theory_traj]
draw_polyline_world(screen, points, config.COLOR_THEORY_TRAJECTORY, 2, ppm)
draw_particles(screen, sim.particles, ppm, sim.selected_index)
if sim.show_velocities:
draw_velocity_vectors(screen, sim.particles, ppm)
if sim.show_forces:
draw_force_vectors(screen, sim.particles, sim.last_forces, ppm)
render_placement_preview(screen, input_state, ppm)
t_draw1 = time.perf_counter()
draw_ms_total = (t_draw1 - t_draw0) * 1000.0
draw_ms = max(0.0, draw_ms_total - field_ms)
# Overlay
fps = clock.get_fps() or float(config.FPS_TARGET)
speed_label = {0: "0.5×", 1: "1×", 2: "2×", 3: "4×"}[sim.speed_index]
sim_state = {
"fps": fps,
"n": len(sim.particles),
"speed_label": speed_label,
"dt_s": config.DT_S,
"substeps": max(1, int(config.SUBSTEPS_BASE_PER_FRAME * config.SPEED_MULTIPLIERS[sim.speed_index])),
"E_kin": sim.energy_kin,
"E_pot": sim.energy_pot,
"E_tot": sim.energy_tot,
}
# Validation overlay payload
if getattr(sim, "validation_active", False):
E = (float(config.UNIFORM_FIELD_VECTOR_NC[0]), float(config.UNIFORM_FIELD_VECTOR_NC[1]))
# Acceleration a = (q/m)E from sim state if available
if getattr(sim, "validation_accel_mps2", None) is not None:
a = (float(sim.validation_accel_mps2[0]), float(sim.validation_accel_mps2[1]))
else:
a = (0.0, 0.0)
cur = getattr(sim, "validation_current_errors", {}) or {}
val_payload = {
"active": True,
"E": E,
"a": a,
"t": float(cur.get("t", 0.0)),
"pos_err": float(cur.get("pos_err", 0.0)),
"vel_err": float(cur.get("vel_err", 0.0)),
"dt_s": float(config.DT_S),
"duration_s": float(getattr(config, "VALIDATION_DURATION_S", 0.0)),
}
# Final comparison values
val_payload["reached_end"] = bool(getattr(sim, "validation_reached_end", False))
pos_th = getattr(sim, "validation_final_theory_pos_m", None)
vel_th = getattr(sim, "validation_final_theory_vel_mps", None)
pos_sim = getattr(sim, "validation_final_sim_pos_m", None)
vel_sim = getattr(sim, "validation_final_sim_vel_mps", None)
if pos_th is not None and vel_th is not None:
val_payload["pos_th"] = (float(pos_th[0]), float(pos_th[1]))
val_payload["vel_th"] = (float(vel_th[0]), float(vel_th[1]))
if pos_sim is not None and vel_sim is not None:
val_payload["pos_sim"] = (float(pos_sim[0]), float(pos_sim[1]))
val_payload["vel_sim"] = (float(vel_sim[0]), float(vel_sim[1]))
sim_state["validation"] = val_payload
if getattr(config, "PROFILE_OVERLAY_ENABLED", False):
tot_ms = (time.perf_counter() - frame_t0) * 1000.0
sim_state["profile"] = {
"physics_ms": float(physics_ms),
"field_ms": float(field_ms),
"draw_ms": float(draw_ms),
"total_ms": float(tot_ms),
}
draw_overlay(screen, font, sim_state, input_state.overlay_enabled)
render_hover_tooltip(screen, font, sim, input_state, ppm)
pygame.display.flip()
clock.tick(config.FPS_TARGET)
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
raise