492 lines
17 KiB
Python
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()
|