sandpypi/sandpypi.py
Stan44 d05e64839f - gravity and physics are the same thing now.
- __init__.py added to all folders
- preparing docs
- .gitignore updated
- .vscode/settings.json updated
- mypy.ini updated
- pyproject.toml updated
- plansandideas/Coding optimization plan.md added
- plansandideas/REORGANIZATION.md added
- scripts/lint.py added
- scripts/setup_dev.py added
- requirements-dev.txt added
- and more.
2025-01-05 03:35:17 -06:00

381 lines
13 KiB
Python

"""
#File Name: sandpypi.py
# Sandpypi by Stanton.
# Project name is a placeholder.
# This has been a multimonth or year project i have time blindness sorta.
# 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 Require files for the Engine.
import pygame
from src.config.settings import cProfile, engine_settings, pstats, time
from src.physics.sim import Simulation
from src.rendering.rendering import Rendering
def handle_input(
event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos
):
"""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:
return handle_key_press(event, rendering, sim)
return None
def handle_mouse_down(event, sim, rendering, settings_visible, zoom_active):
"""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)
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, sim, rendering, settings_visible, in_settings_area, zoom_active
):
"""Handle left click interactions"""
result = {
"mouse_down_left": False,
"settings_visible": settings_visible,
"over_button": False,
}
if rendering.settings_button.collidepoint(mouse_pos):
result["settings_visible"] = not settings_visible
result["over_button"] = True
return result
if zoom_active:
result["zoom_locked"] = not result.get("zoom_locked", False)
if result["zoom_locked"]:
result["zoom_pos"] = mouse_pos
return result
if settings_visible and in_settings_area:
handle_settings_click(mouse_pos)
result["over_button"] = True
return result
if not in_settings_area:
if handle_ui_click(mouse_pos, sim, rendering):
result["over_button"] = True
else:
result["mouse_down_left"] = True
return result
def handle_settings_click(mouse_pos):
"""Handle clicks in settings menu"""
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]
def handle_ui_click(mouse_pos, sim, rendering):
"""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):
"""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, rendering, sim):
"""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():
"""Main function to run the simulation"""
pygame.init()
clock = pygame.time.Clock()
width = 1024
height = 768
screen = pygame.display.set_mode(
(width, height), pygame.HWSURFACE | pygame.DOUBLEBUF
)
sim = Simulation(width, height)
rendering = Rendering(width, height)
# 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
while running:
# Clear screen at start of frame
screen.fill((0, 0, 0))
fps = clock.get_fps()
dt = clock.tick(60) / 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
# Handle settings menu interactions
elif settings_visible and in_settings_area:
relative_y = mouse_pos[1] - 100 # 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]
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, engine_settings)
# 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
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 = 80 if current_zoom_pos[0] > width / 2 else width - 110
zoom_y = 80 if current_zoom_pos[1] > height / 2 else height - 110
screen.blit(zoom_surface, (zoom_x, zoom_y))
# Draw everything in correct order
rendering.draw_particles(
sim.particles,
sim.active_particles,
sim.particle_size,
rendering.particle_colors,
)
rendering.draw_buttons()
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()