""" #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()