sandpypi/python/sandpypi.py

492 lines
17 KiB
Python

"""
#File Name: sandpypi.py
# Sandpypi by Stanton.
# Project name is a placeholder.
# This has been a multi year project i have time blindness.
# This is my most functional system for falling sand in python yet i took
# some things i learned in JS.
# This needs further optimizations to core performance sections.
The main function to run the Sandpypi program.
This function initializes the Pygame environment, creates the simulation and
rendering objects, and enters the main event loop.
It also updates the simulation, draws the particles, buttons, and other UI
elements, and manages the settings menu.
The main loop runs at a target frame rate of 60 FPS
(this fps varies on my mood and the testing), with the actual frame rate
displayed in the debug overlay.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent / "python"))
# Import Require files for the Engine.
from typing import TypedDict
import pygame
import time
from src.config.settings import (
cProfile,
engine_settings,
pstats,
)
from src.physics.sim import Simulation
from src.rendering.rendering import Rendering
class InputAction(TypedDict, total=False):
mouse_down_left: bool
mouse_down_right: bool
mouse_down_middle: bool
settings_visible: bool
over_button: bool
zoom_active: bool
zoom_locked: bool
zoom_pos: tuple[int, int]
running: bool
def handle_input(
event: pygame.event.Event,
sim: Simulation,
rendering: Rendering,
settings_visible: bool,
zoom_active: bool, # zoom_locked, zoom_pos
) -> InputAction | None:
"""Handle all input events"""
if event.type == pygame.MOUSEBUTTONDOWN:
return handle_mouse_down(
event, sim, rendering, settings_visible, zoom_active
)
if event.type == pygame.MOUSEBUTTONUP:
return handle_mouse_up(event)
if event.type == pygame.KEYDOWN:
# Cast to event with key attribute safely or assume it has checks
return handle_key_press(event, rendering, sim)
return None
def handle_mouse_down(
event: pygame.event.Event,
sim: Simulation,
rendering: Rendering,
settings_visible: bool,
zoom_active: bool,
) -> InputAction:
"""Handle mouse button down events"""
mouse_pos = pygame.mouse.get_pos()
in_settings_area = False
if settings_visible:
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400)
in_settings_area = settings_rect.collidepoint(mouse_pos)
# event.button is int
if event.button == 4: # Mouse wheel up
sim.brush_size = min(sim.brush_size + 1, sim.max_brush_size)
elif event.button == 5: # Mouse wheel down
sim.brush_size = max(sim.brush_size - 1, 1)
elif event.button == 1: # Left click
return handle_left_click(
mouse_pos,
sim,
rendering,
settings_visible,
in_settings_area,
zoom_active,
)
elif event.button == 3: # Right click
return {"mouse_down_right": True}
elif event.button == 2: # Middle click
return {"mouse_down_middle": True}
return {}
def handle_left_click(
mouse_pos: tuple[int, int],
sim: Simulation,
rendering: Rendering,
settings_visible: bool,
in_settings_area: bool,
zoom_active: bool,
) -> InputAction:
"""Handle left click interactions"""
result: InputAction = {
"mouse_down_left": False,
"settings_visible": settings_visible,
"over_button": False,
}
# Sidebar UI Check
if mouse_pos[0] < rendering.sidebar_width:
if handle_ui_click(mouse_pos, sim, rendering):
result["over_button"] = True
# Check if settings was toggled
if rendering.settings_button.collidepoint(mouse_pos):
result["settings_visible"] = not settings_visible
return result
if zoom_active:
current_locked = bool(result.get("zoom_locked", False))
result["zoom_locked"] = not current_locked
if result["zoom_locked"]:
result["zoom_pos"] = mouse_pos
return result
if settings_visible and in_settings_area:
handle_settings_click(mouse_pos, rendering, sim)
result["over_button"] = True
return result
if not in_settings_area:
result["mouse_down_left"] = True
return result
def handle_settings_click(mouse_pos: tuple[int, int], rendering: Rendering, sim: Simulation) -> None:
"""Handle clicks in settings menu"""
# Settings menu is now drawn at rendering.width - 320, 100
settings_menu_y = 100
relative_y = mouse_pos[1] - settings_menu_y
setting_index = relative_y // 30
if 0 <= setting_index < len(engine_settings):
setting_name = list(engine_settings.keys())[setting_index]
engine_settings[setting_name] = not engine_settings[setting_name]
# Handle side effects
if setting_name == "enable_acceleration":
from src.acceleration.simulation_bridge import AcceleratedSimulation
if engine_settings["enable_acceleration"]:
if not sim.acceleration_wrapper:
sim.acceleration_wrapper = AcceleratedSimulation(sim)
else:
sim.acceleration_wrapper.enable_acceleration = True
elif sim.acceleration_wrapper:
sim.acceleration_wrapper.enable_acceleration = False
if setting_name == "fast_sim":
sim.fast_mode = bool(engine_settings["fast_sim"])
if sim.fast_mode:
sim._init_fast_storage()
# Clear state when switching modes
sim.particles = [
[None for _ in range(sim.height)] for _ in range(sim.width)
]
if sim.fast_type_id is not None:
sim.fast_type_id.fill(0)
if sim.fast_temp is not None:
sim.fast_temp.fill(sim.ambient_temperature)
if sim.fast_burn_time is not None:
sim.fast_burn_time.fill(0)
if sim.fast_burning is not None:
sim.fast_burning.fill(0)
if sim.fast_spark_time is not None:
sim.fast_spark_time.fill(0)
if sim.fast_lifetime is not None:
sim.fast_lifetime.fill(0)
sim.active_particles.clear()
sim.occupied_cells.clear()
sim.particle_count = 0
def handle_ui_click(
mouse_pos: tuple[int, int], sim: Simulation, rendering: Rendering
) -> bool:
"""Handle clicks on UI elements"""
for category, button in rendering.category_buttons.items():
if button.collidepoint(mouse_pos):
rendering.current_category = category
return True
for particle_type, button in rendering.buttons.items():
if button.collidepoint(mouse_pos):
sim.current_particle_type = particle_type
return True
if rendering.clear_screen_button.collidepoint(mouse_pos):
rendering.clear_screen(sim)
return True
return False
def handle_mouse_up(event: pygame.event.Event) -> InputAction:
"""Handle mouse button up events"""
if event.button == 1:
return {"mouse_down_left": False}
elif event.button == 3:
return {"mouse_down_right": False}
elif event.button == 2:
return {"mouse_down_middle": False}
return {}
def handle_key_press(
event: pygame.event.Event, rendering: Rendering, sim: Simulation
) -> InputAction:
"""Handle keyboard press events"""
if event.key == pygame.K_ESCAPE:
print("Escape button pressed")
print(f"Exiting Program {__file__}")
return {"running": False}
elif event.key == pygame.K_SPACE:
engine_settings["pause_sim"] = not engine_settings["pause_sim"]
elif event.key == pygame.K_c:
rendering.clear_screen(sim)
sim.reset_particle_count()
elif event.key == pygame.K_z:
return {
"zoom_active": True,
"zoom_locked": False,
"zoom_pos": pygame.mouse.get_pos(),
}
return {}
def main() -> None:
"""Main function to run the simulation"""
pygame.init()
clock = pygame.time.Clock()
width = 1024
height = 768
particle_size = 3
screen = pygame.display.set_mode(
(width, height), pygame.HWSURFACE | pygame.DOUBLEBUF
)
sim = Simulation(width // particle_size, height // particle_size, particle_size=particle_size)
# Initialize acceleration if enabled by default
if engine_settings.get("enable_acceleration"):
from src.acceleration.simulation_bridge import AcceleratedSimulation
sim.acceleration_wrapper = AcceleratedSimulation(sim)
rendering = Rendering(width, height)
# num_cores = multiprocessing.cpu_count()
# pool = multiprocessing.Pool(processes=num_cores)
# State variables
mouse_down_left = False
mouse_down_right = False
mouse_down_middle = False
over_button = False
zoom_active = False
zoom_locked = False
# zoom_pos = None
settings_visible = False
running = True
last_particle_time = 0.0
while running:
# Clear screen at start of frame
screen.fill(color=(0, 0, 0))
fps = clock.get_fps()
dt = clock.tick(120) / 1000
keys = pygame.key.get_pressed()
mouse_pos = pygame.mouse.get_pos()
zoom_active = keys[pygame.K_z]
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
continue
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
in_settings_area = False
if settings_visible:
settings_rect = pygame.Rect(
rendering.width - 320, 100, 300, 400
)
in_settings_area = settings_rect.collidepoint(mouse_pos)
if event.button == 4: # Mouse wheel up
sim.brush_size = min(
sim.brush_size + 1, sim.max_brush_size
)
elif event.button == 5: # Mouse wheel down
sim.brush_size = max(sim.brush_size - 1, 1)
elif event.button == 1: # Left click
over_button = False
# Check settings button first
if rendering.settings_button.collidepoint(mouse_pos):
settings_visible = not settings_visible
over_button = True
elif zoom_active:
zoom_locked = not zoom_locked
if zoom_locked:
# zoom_pos = mouse_pos
pass
# Handle settings menu interactions
elif settings_visible and in_settings_area:
handle_settings_click(mouse_pos, rendering, sim)
over_button = True
elif not in_settings_area:
# Check category buttons
for (
category,
button,
) in rendering.category_buttons.items():
if button.collidepoint(mouse_pos):
rendering.current_category = category
over_button = True
break
# Check particle buttons
if not over_button:
for (
particle_type,
button,
) in rendering.buttons.items():
if button.collidepoint(mouse_pos):
sim.current_particle_type = particle_type
over_button = True
break
# Check clear screen button
if rendering.clear_screen_button.collidepoint(
mouse_pos
):
rendering.clear_screen(sim)
# particle_count = 0
over_button = True
if not over_button and not in_settings_area:
mouse_down_left = True
elif event.button == 3: # Right click
mouse_down_right = True
elif event.button == 2: # Middle click
mouse_down_middle = True
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
mouse_down_left = False
elif event.button == 3:
mouse_down_right = False
elif event.button == 2:
mouse_down_middle = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_SPACE:
engine_settings["pause_sim"] = not engine_settings[
"pause_sim"
]
elif event.key == pygame.K_c:
rendering.clear_screen(sim)
elif event.key == pygame.K_z:
zoom_active = True
zoom_locked = False
# zoom_pos = pygame.mouse.get_pos()
# Update simulation if not paused
if not engine_settings["pause_sim"]:
sim.simulate_step(dt)
# regions = sim.grid_system.split_into_processing_regions(num_cores)
# # Process each region's physics
# for region_data in regions.values():
# for particles in region_data.values():
# for x, y in particles:
# # Process physics for particles in this region
# sim.handle_temperature(dt)
# sim.handle_particle_interactions()
# # Update spatial grid after processing
# sim.grid_system.update_spatial_grid()
# Handle continuous mouse input
if mouse_down_left and not over_button:
x, y = mouse_pos
current_time = time.time()
if sim.current_particle_type not in ["wind", "air"]:
# Limit particle creation to every 16ms (approximately 60 FPS)
if current_time - last_particle_time >= 0.016:
sim.create_particle_circle(x, y)
last_particle_time = current_time # just works.
if mouse_down_right:
x, y = mouse_pos
sim.clear_particles_circle(x, y)
if mouse_down_middle:
x, y = mouse_pos
sim.create_particle(x, y)
# Handle zoom window
if zoom_active or zoom_locked:
"""current_zoom_pos = zoom_pos if zoom_locked else mouse_pos
zoom_surface = rendering.draw_zoom_window(
sim.particles,
sim.particle_size,
rendering.particle_colors,
current_zoom_pos,
)
zoom_x: int = (
80 if current_zoom_pos[0] > width / 2 else width - 110
)
zoom_y: int = (
80 if current_zoom_pos[1] > height / 2 else height - 110
)
screen.blit(zoom_surface, (zoom_x, zoom_y))
"""
pass
# Draw everything in correct order
if getattr(sim, "fast_mode", False) and getattr(sim, "fast_type_id", None) is not None:
rendering.draw_particles_fast(
sim.fast_type_id,
sim.particle_size,
sim.fast_color_lut,
sim.fast_spark_time,
)
else:
rendering.draw_particles(
sim.particles,
sim.occupied_cells,
sim.particle_size,
rendering.particle_colors,
)
rendering.draw_buttons(mouse_pos, sim.current_particle_type)
rendering.draw_brush_size_slider(sim.brush_size)
rendering.render_brush_cursor(
mouse_pos[0], mouse_pos[1], sim.brush_size * sim.particle_size
)
# Draw settings and debug overlay last
if settings_visible:
settings_menu = rendering.draw_settings_menu()
rendering.screen.blit(settings_menu, (rendering.width - 320, 100))
rendering.draw_debug_overlay(fps, sim)
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
# Profile the application
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
# Write profiling results to file
with open("profile_results.log", "w", encoding="utf-8") as f:
stats = pstats.Stats(profiler, stream=f)
stats.sort_stats("cumulative")
stats.print_stats()