- 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.
This commit is contained in:
parent
c225632aac
commit
d05e64839f
62
.gitea/workflows/ci.yml
Normal file
62
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,62 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.1"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[dev]
|
||||
|
||||
- name: Run linters
|
||||
run: |
|
||||
black . --check
|
||||
isort . --check
|
||||
# flake8 .
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest tests/ --cov=src --cov-report=xml
|
||||
|
||||
# Deployment job - uncomment and configure when PyPI uploads are needed
|
||||
# This job will build and upload your package to PyPI when merging to main
|
||||
#
|
||||
# deploy:
|
||||
# needs: test
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
#
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v3
|
||||
# with:
|
||||
# python-version: "3.8"
|
||||
#
|
||||
# - name: Build and publish
|
||||
# env:
|
||||
# TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
# TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
# run: |
|
||||
# pip install build twine
|
||||
# python -m build
|
||||
# twine upload dist/*
|
||||
178
.gitignore
vendored
178
.gitignore
vendored
@ -1,12 +1,10 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
__pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
@ -20,158 +18,36 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.venv/
|
||||
.*_venv/
|
||||
.env
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Custom Ignores
|
||||
current_stations.html
|
||||
forecast_data.json
|
||||
openapi.json
|
||||
__pycache__/sim.cpython-312.pyc
|
||||
__pycache__/sim.cpython-312.pyc
|
||||
__pycache__/rendering.cpython-312.pyc
|
||||
sandpypi.dist/
|
||||
sandpypi.build/
|
||||
sandpypi.onefile-build/
|
||||
sandpypi.exe
|
||||
sandpypi.7z
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
unittest/
|
||||
.7z
|
||||
.zip
|
||||
.pytest_cache/
|
||||
|
||||
# Distribution
|
||||
dist/
|
||||
build/
|
||||
*.exe
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
livenotes.txt
|
||||
30
.pre-commit-config.yaml.disabled
Normal file
30
.pre-commit-config.yaml.disabled
Normal file
@ -0,0 +1,30 @@
|
||||
#repos:
|
||||
#- repo: https://github.com/psf/black
|
||||
# rev: 23.12.1
|
||||
# hooks:
|
||||
# - id: black
|
||||
# language_version: python3
|
||||
# args: ["--line-length", "79"]
|
||||
|
||||
#- repo: https://github.com/pycqa/isort
|
||||
# rev: 5.13.2
|
||||
# hooks:
|
||||
# - id: isort
|
||||
# args: ["--profile", "black", "--filter-files"]
|
||||
|
||||
#- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
# rev: v4.5.0
|
||||
# hooks:
|
||||
# - id: trailing-whitespace
|
||||
# - id: end-of-file-fixer
|
||||
# - id: check-yaml
|
||||
# - id: check-added-large-files
|
||||
|
||||
#- repo: local
|
||||
# hooks:
|
||||
# - id: pytest
|
||||
# name: pytest
|
||||
# entry: pytest
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
# always_run: false
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,3 +1,7 @@
|
||||
{
|
||||
"python.linting.pylintArgs": [
|
||||
"--disable=E1101",
|
||||
"--ignored-modules=pygame"
|
||||
],
|
||||
"cody.agentic.context.experimentalShell": true
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
from src.config.settings import cProfile, pstats
|
||||
from src.sandpypi import main
|
||||
|
||||
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') as f:
|
||||
stats = pstats.Stats(profiler, stream=f)
|
||||
stats.sort_stats('cumulative')
|
||||
stats.print_stats()
|
||||
33
__init__.py
Normal file
33
__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""
|
||||
Sandpypi - A falling sand simulation package in Python.
|
||||
|
||||
This package provides modules for particle physics simulation and rendering:
|
||||
- config.settings: Configuration and particle properties
|
||||
- physics.sim: Physics engine and particle behavior
|
||||
- rendering.rendering: Display and visualization components
|
||||
"""
|
||||
|
||||
from src.config.settings import (
|
||||
cProfile,
|
||||
engine_settings,
|
||||
particle_properties,
|
||||
pstats,
|
||||
time,
|
||||
)
|
||||
from src.physics.sim import Simulation
|
||||
from src.rendering.rendering import Rendering
|
||||
|
||||
__all__ = [
|
||||
"cProfile",
|
||||
"pstats",
|
||||
"time",
|
||||
"engine_settings",
|
||||
"particle_properties",
|
||||
"Simulation",
|
||||
"Rendering",
|
||||
]
|
||||
# src\rendering\rendering.py"
|
||||
# src\physics\sim.py
|
||||
# src\config\settings.py
|
||||
# src\physics\particle.py
|
||||
# src\debug\debugger_system.py
|
||||
22
docs/conf.py
Normal file
22
docs/conf.py
Normal file
@ -0,0 +1,22 @@
|
||||
""" Configuration file for Sphinx documentation"""
|
||||
|
||||
PROJECT = "sandpypi"
|
||||
COPYRIGHT = "2024, stan44"
|
||||
AUTHOR = "stan44"
|
||||
VERSION = "0.1.0"
|
||||
RELEASE = "0.1.0"
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.githubpages",
|
||||
"sphinx_rtd_theme",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
HTML_THEME = "sphinx_rtd_theme"
|
||||
html_static_path = ["_static"]
|
||||
# html_logo = "_static/logo.png" # Add your project's logo
|
||||
29
docs/index.rst
Normal file
29
docs/index.rst
Normal file
@ -0,0 +1,29 @@
|
||||
Welcome to Sandpypi Documentation
|
||||
===============================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
guides/getting_started
|
||||
guides/user_guide
|
||||
architecture/overview
|
||||
api/modules
|
||||
|
||||
Development Standards
|
||||
------------------
|
||||
|
||||
This project follows these development standards:
|
||||
|
||||
* Black code formatting (88 characters line length)
|
||||
* Type hints for Python functions
|
||||
* Google style docstrings
|
||||
* Particle simulation physics
|
||||
* Optimized rendering with Pygame
|
||||
|
||||
Quick Links
|
||||
----------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
4
mypy.ini
Normal file
4
mypy.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[mypy]
|
||||
namespace_packages = True
|
||||
explicit_package_bases = True
|
||||
python_version = 3.13
|
||||
46
plansandideas/Coding optimization plan.md
Normal file
46
plansandideas/Coding optimization plan.md
Normal file
@ -0,0 +1,46 @@
|
||||
targeted performance improvements while maintaining core plan:
|
||||
|
||||
1. Spatial Grid Optimizations:
|
||||
|
||||
```python
|
||||
def update_spatial_grid(self):
|
||||
# Use direct array operations instead of dict
|
||||
# Pre-allocate grid arrays for particle positions
|
||||
# Batch update active cells
|
||||
```
|
||||
|
||||
2. Force Calculations:
|
||||
|
||||
```python
|
||||
def calculate_forces(self, particle, x, y):
|
||||
# Vectorize neighbor force calculations
|
||||
# Cache common force values
|
||||
# Use direct array math instead of loops
|
||||
```
|
||||
|
||||
3. Temperature System:
|
||||
|
||||
```python
|
||||
def handle_temperature(self, dt):
|
||||
# Batch temperature updates
|
||||
# Use array operations for heat transfer
|
||||
# Pre-calculate temperature thresholds
|
||||
```
|
||||
|
||||
4. Particle Creation:
|
||||
|
||||
```python
|
||||
def create_particle_circle(self):
|
||||
# Pre-calculate circle bounds
|
||||
# Batch particle creation
|
||||
# Use array operations for position checks
|
||||
```
|
||||
|
||||
5. Phase Transitions:
|
||||
|
||||
```python
|
||||
def handle_phase_transitions(self):
|
||||
# Group similar transitions
|
||||
# Cache property lookups
|
||||
# Batch state changes
|
||||
```
|
||||
59
pyproject.toml
Normal file
59
pyproject.toml
Normal file
@ -0,0 +1,59 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "sandpypi"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
{name = "Stan44", email = "stan44@example.com"},
|
||||
]
|
||||
description = "Falling Sand Simulation in Python"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"numpy>=1.21.0",
|
||||
"pygame>=2.1.0"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-cov>=4.0",
|
||||
"black>=22.0",
|
||||
"flake8>=4.0",
|
||||
"isort>=5.0",
|
||||
"mypy>=0.9",
|
||||
"pylint>=2.10",
|
||||
"sphinx>=4.0",
|
||||
"sphinx-rtd-theme>=1.0"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
line_length = 79
|
||||
force_grid_wrap = 0
|
||||
include_trailing_comma = true
|
||||
use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 79
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
check_untyped_defs = true
|
||||
strict_optional = true
|
||||
|
||||
[tool.pylint]
|
||||
max-line-length = 79
|
||||
ignored-modules = ["pygame"]
|
||||
disable = ["E1101"]
|
||||
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
-r requirements.txt
|
||||
pytest>=7.0
|
||||
pytest-cov>=4.0
|
||||
black>=22.0
|
||||
isort>=5.0
|
||||
mypy>=0.9
|
||||
@ -3,32 +3,40 @@
|
||||
# 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 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 handles user input events such as mouse clicks, mouse wheel scrolling, and keyboard presses.
|
||||
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.
|
||||
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.
|
||||
from config.settings import pygame, engine_settings, cProfile, pstats, time
|
||||
import pygame
|
||||
|
||||
from rendering.rendering import Rendering
|
||||
from physics.sim import Simulation
|
||||
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):
|
||||
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)
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
return handle_mouse_down(
|
||||
event, sim, rendering, settings_visible, zoom_active
|
||||
)
|
||||
if event.type == pygame.MOUSEBUTTONUP:
|
||||
return handle_mouse_up(event)
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.type == pygame.KEYDOWN:
|
||||
return handle_key_press(event, rendering, sim)
|
||||
return None
|
||||
|
||||
@ -47,44 +55,57 @@ def handle_mouse_down(event, sim, rendering, settings_visible, zoom_active):
|
||||
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)
|
||||
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}
|
||||
return {"mouse_down_right": True}
|
||||
elif event.button == 2: # Middle click
|
||||
return {'mouse_down_middle': True}
|
||||
return {"mouse_down_middle": True}
|
||||
return {}
|
||||
|
||||
|
||||
def handle_left_click(mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active):
|
||||
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}
|
||||
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
|
||||
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
|
||||
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, sim)
|
||||
result['over_button'] = True
|
||||
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
|
||||
result["over_button"] = True
|
||||
else:
|
||||
result['mouse_down_left'] = True
|
||||
result["mouse_down_left"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def handle_settings_click(mouse_pos, sim):
|
||||
def handle_settings_click(mouse_pos):
|
||||
"""Handle clicks in settings menu"""
|
||||
settings_menu_y = 100
|
||||
relative_y = mouse_pos[1] - settings_menu_y
|
||||
@ -116,11 +137,11 @@ def handle_ui_click(mouse_pos, sim, rendering):
|
||||
def handle_mouse_up(event):
|
||||
"""Handle mouse button up events"""
|
||||
if event.button == 1:
|
||||
return {'mouse_down_left': False}
|
||||
return {"mouse_down_left": False}
|
||||
elif event.button == 3:
|
||||
return {'mouse_down_right': False}
|
||||
return {"mouse_down_right": False}
|
||||
elif event.button == 2:
|
||||
return {'mouse_down_middle': False}
|
||||
return {"mouse_down_middle": False}
|
||||
return {}
|
||||
|
||||
|
||||
@ -129,23 +150,30 @@ def handle_key_press(event, rendering, sim):
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
print("Escape button pressed")
|
||||
print(f"Exiting Program {__file__}")
|
||||
return {'running': False}
|
||||
return {"running": False}
|
||||
elif event.key == pygame.K_SPACE:
|
||||
engine_settings['pause_sim'] = not engine_settings['pause_sim']
|
||||
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 {
|
||||
"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)
|
||||
screen = pygame.display.set_mode(
|
||||
(width, height), pygame.HWSURFACE | pygame.DOUBLEBUF
|
||||
)
|
||||
sim = Simulation(width, height)
|
||||
rendering = Rendering(width, height)
|
||||
|
||||
@ -159,6 +187,7 @@ def main():
|
||||
zoom_pos = None
|
||||
settings_visible = False
|
||||
running = True
|
||||
last_particle_time = 0
|
||||
|
||||
while running:
|
||||
# Clear screen at start of frame
|
||||
@ -181,11 +210,15 @@ def main():
|
||||
in_settings_area = False
|
||||
|
||||
if settings_visible:
|
||||
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400)
|
||||
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)
|
||||
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
|
||||
@ -206,13 +239,20 @@ def main():
|
||||
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]
|
||||
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():
|
||||
for (
|
||||
category,
|
||||
button,
|
||||
) in rendering.category_buttons.items():
|
||||
if button.collidepoint(mouse_pos):
|
||||
rendering.current_category = category
|
||||
over_button = True
|
||||
@ -220,16 +260,21 @@ def main():
|
||||
|
||||
# Check particle buttons
|
||||
if not over_button:
|
||||
for particle_type, button in rendering.buttons.items():
|
||||
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):
|
||||
if rendering.clear_screen_button.collidepoint(
|
||||
mouse_pos
|
||||
):
|
||||
rendering.clear_screen(sim)
|
||||
particle_count = 0
|
||||
# particle_count = 0
|
||||
over_button = True
|
||||
|
||||
if not over_button and not in_settings_area:
|
||||
@ -252,7 +297,9 @@ def main():
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
running = False
|
||||
elif event.key == pygame.K_SPACE:
|
||||
engine_settings['pause_sim'] = not engine_settings['pause_sim']
|
||||
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:
|
||||
@ -261,21 +308,18 @@ def main():
|
||||
zoom_pos = pygame.mouse.get_pos()
|
||||
|
||||
# Update simulation if not paused
|
||||
if not engine_settings['pause_sim']:
|
||||
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
|
||||
if sim.current_particle_type not in ['wind', 'air']:
|
||||
current_time = time.time()
|
||||
if not hasattr(main, 'last_particle_time'):
|
||||
main.last_particle_time = 0
|
||||
|
||||
if sim.current_particle_type not in ["wind", "air"]:
|
||||
# Limit particle creation to every 16ms (approximately 60 FPS)
|
||||
if current_time - main.last_particle_time >= 0.016:
|
||||
if current_time - last_particle_time >= 0.016:
|
||||
sim.create_particle_circle(x, y)
|
||||
main.last_particle_time = current_time
|
||||
last_particle_time = current_time
|
||||
|
||||
if mouse_down_right:
|
||||
x, y = mouse_pos
|
||||
@ -288,19 +332,28 @@ def main():
|
||||
# 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_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_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)
|
||||
|
||||
|
||||
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:
|
||||
@ -309,9 +362,9 @@ def main():
|
||||
|
||||
rendering.draw_debug_overlay(fps, sim)
|
||||
pygame.display.flip()
|
||||
|
||||
pygame.quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Profile the application
|
||||
profiler = cProfile.Profile()
|
||||
@ -321,7 +374,7 @@ if __name__ == "__main__":
|
||||
|
||||
profiler.disable()
|
||||
# Write profiling results to file
|
||||
with open('profile_results.log', 'w') as f:
|
||||
with open("profile_results.log", "w", encoding="utf-8") as f:
|
||||
stats = pstats.Stats(profiler, stream=f)
|
||||
stats.sort_stats('cumulative')
|
||||
stats.sort_stats("cumulative")
|
||||
stats.print_stats()
|
||||
153
scripts/lint.py
Normal file
153
scripts/lint.py
Normal file
@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rich TUI linting interface for Sandpypi."""
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
from rich.layout import Layout
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
||||
|
||||
|
||||
class LinterTUI:
|
||||
"""Main classs for the linting TUI."""
|
||||
|
||||
def __init__(self):
|
||||
self.console = Console()
|
||||
self.layout = Layout()
|
||||
self.layout.split_column(
|
||||
Layout(name="output", ratio=8), Layout(name="progress", ratio=2)
|
||||
)
|
||||
self.setup_logging()
|
||||
|
||||
def setup_logging(self):
|
||||
"""Logging"""
|
||||
self.log_dir = Path("logs")
|
||||
self.log_dir.mkdir(exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
self.log_file = f"lint_{timestamp}.log"
|
||||
logging.basicConfig(
|
||||
filename=self.log_dir / self.log_file,
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
def get_target_path(self):
|
||||
"""Get thee target path for linting."""
|
||||
paths = input(
|
||||
"\nEnter paths to lint (comma-separated NO SPACES)\
|
||||
or press Enter for default [src,tests]: "
|
||||
).strip()
|
||||
target_paths = paths.split(",") if paths else ["src", "tests"]
|
||||
|
||||
# Convert to absolute paths and include subdirectories
|
||||
abs_paths = []
|
||||
for path in target_paths:
|
||||
base_path = Path(path).absolute()
|
||||
if base_path.is_dir():
|
||||
abs_paths.extend(str(p) for p in base_path.rglob("*.py"))
|
||||
else:
|
||||
abs_paths.append(str(base_path))
|
||||
|
||||
return abs_paths
|
||||
|
||||
def run_linters(self, selected_linters, paths):
|
||||
"""Run the linters on the selected paths."""
|
||||
progress = Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
expand=True,
|
||||
)
|
||||
|
||||
self.layout["progress"].update(progress)
|
||||
|
||||
with Live(self.layout, refresh_per_second=5, screen=True):
|
||||
task = progress.add_task(
|
||||
"[cyan]Linting...", total=len(selected_linters)
|
||||
)
|
||||
|
||||
for linter in selected_linters:
|
||||
progress.update(task, description=f"[cyan]Running {linter}...")
|
||||
|
||||
cmd = [linter] + paths
|
||||
with subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
bufsize=1,
|
||||
) as process:
|
||||
current_output = []
|
||||
linter_name = linter.split()[0]
|
||||
while True:
|
||||
output = process.stdout.readline()
|
||||
if output == "" and process.poll() is not None:
|
||||
break
|
||||
if output:
|
||||
current_output.append(output.strip())
|
||||
# Log each line of output
|
||||
logging.info("Running %s on %s", linter, paths)
|
||||
logging.info("Output: %s", output.strip())
|
||||
|
||||
self.layout["output"].update(
|
||||
Panel(
|
||||
"\n".join(current_output[-20:]),
|
||||
title=f"[bold green]{linter}\
|
||||
{linter_name}",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Log any errors
|
||||
if not current_output:
|
||||
stderr_output = process.stderr.read()
|
||||
output_text = stderr_output or "No output"
|
||||
self.layout["output"].update(
|
||||
Panel(
|
||||
output_text,
|
||||
title=f"[bold green]{linter} Output",
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
if stderr_output:
|
||||
logging.error("Errors: %s", stderr_output)
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
final_message = Panel(
|
||||
"[bold green]✨ Linting Complete! ✨\n"
|
||||
"[cyan]Check the logs for detailed results.\n"
|
||||
f"[green]Logs saved to: {self.log_dir / self.log_file}\n"
|
||||
"[yellow]Press Enter to exit...",
|
||||
title="[bold blue]Sandpypi Linter",
|
||||
border_style="green",
|
||||
padding=(2, 4),
|
||||
)
|
||||
|
||||
self.layout["output"].update(final_message)
|
||||
self.layout["progress"].update(
|
||||
Panel("[bold green]100% Complete!", border_style="green")
|
||||
)
|
||||
|
||||
input()
|
||||
|
||||
|
||||
def main():
|
||||
"""Runner for the Linting TUI."""
|
||||
tui = LinterTUI()
|
||||
linters = ["black", "isort", "mypy", "flake8", "pylint"]
|
||||
paths = tui.get_target_path()
|
||||
tui.run_linters(linters, paths)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# scripts\run_tests.py
|
||||
# !/usr/bin/env python3
|
||||
108
scripts/setup_dev.py
Normal file
108
scripts/setup_dev.py
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Development environment setup script."""
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_python_version():
|
||||
"""Ensure Python version meets minimum requirements."""
|
||||
min_version = (3, 8)
|
||||
current = sys.version_info[:2]
|
||||
if current < min_version:
|
||||
raise SystemError(
|
||||
f"Python {min_version[0]}.{min_version[1]} or higher required"
|
||||
)
|
||||
|
||||
|
||||
def clean_existing_venv():
|
||||
"""Remove existing virtual environment if present."""
|
||||
venv_path = Path(".venv")
|
||||
if venv_path.exists():
|
||||
shutil.rmtree(venv_path)
|
||||
|
||||
|
||||
def initialize_environment():
|
||||
"""Create and initialize development environment."""
|
||||
# Allow configurable venv path
|
||||
venv_path = os.getenv("VENV_PATH", ".sandpypi_venv")
|
||||
|
||||
# Create and activate virtual environment
|
||||
subprocess.run([sys.executable, "-m", "venv", venv_path], check=True)
|
||||
|
||||
# Get the correct paths based on OS
|
||||
is_windows = platform.system() == "Windows"
|
||||
scripts_path = "Scripts" if is_windows else "bin"
|
||||
|
||||
# Setup commands using virtual environment paths
|
||||
pip_cmd = os.path.join(venv_path, scripts_path, "pip")
|
||||
|
||||
# Update pip and install dependencies
|
||||
subprocess.run([pip_cmd, "install", "--upgrade", "pip"], check=True)
|
||||
subprocess.run([pip_cmd, "install", "pre-commit"], check=True)
|
||||
subprocess.run([pip_cmd, "install", "-e", ".[dev]"], check=True)
|
||||
# Install and configure pre-commit hooks
|
||||
precommit_cmd = os.path.join(venv_path, scripts_path, "pre-commit")
|
||||
subprocess.run([precommit_cmd, "install"], check=True)
|
||||
subprocess.run(
|
||||
[precommit_cmd, "install", "--hook-type", "pre-push"], check=True
|
||||
)
|
||||
|
||||
|
||||
def setup_docs():
|
||||
"""Initialize documentation structure."""
|
||||
docs_path = Path("docs")
|
||||
if not docs_path.exists():
|
||||
subprocess.run(
|
||||
[
|
||||
os.path.join(
|
||||
".venv",
|
||||
"Scripts" if platform.system() == "Windows" else "bin",
|
||||
"sphinx-quickstart",
|
||||
),
|
||||
"docs",
|
||||
"--sep",
|
||||
"--project=Sandpypi",
|
||||
"--author=Stan44",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Execute the complete development environment setup."""
|
||||
try:
|
||||
check_python_version()
|
||||
clean_existing_venv()
|
||||
# setup_git_hooks()
|
||||
initialize_environment()
|
||||
setup_docs()
|
||||
|
||||
print("\n✅ Development environment setup complete!")
|
||||
print("\nNext steps:")
|
||||
print("1. Activate your virtual environment:")
|
||||
activate_cmd = (
|
||||
"source .venv/Scripts/activate"
|
||||
if platform.system() == "Windows"
|
||||
else "source .venv/bin/activate"
|
||||
)
|
||||
print(activate_cmd)
|
||||
print("2. Run tests: pytest")
|
||||
print("3. Build docs: cd docs && make html")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n❌ Setup failed: {e}")
|
||||
sys.exit(1)
|
||||
except (OSError, IOError) as e:
|
||||
print(f"\n❌ File system error: {e}")
|
||||
sys.exit(1)
|
||||
except ImportError as e:
|
||||
print(f"\n❌ Dependency error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
setup.py
Normal file
8
setup.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""setup.py"""
|
||||
|
||||
from setuptools import find_packages, setup # type: ignore
|
||||
|
||||
setup(
|
||||
name="sandpypi",
|
||||
packages=find_packages(),
|
||||
)
|
||||
@ -0,0 +1,14 @@
|
||||
"""
|
||||
Sandpypi - A falling sand simulation package in Python.
|
||||
|
||||
This package provides modules for particle physics simulation and rendering:
|
||||
- config.settings: Configuration and particle properties
|
||||
- physics.sim: Physics engine and particle behavior
|
||||
- rendering.rendering: Display and visualization components
|
||||
"""
|
||||
|
||||
# src\rendering\rendering.py
|
||||
# src\physics\sim.py
|
||||
# src\config\settings.py
|
||||
# src\physics\particle.py
|
||||
# src\debug\debugger_system.py
|
||||
0
src/config/py.typed
Normal file
0
src/config/py.typed
Normal file
@ -3,57 +3,65 @@
|
||||
|
||||
Global settings and imports for the project.
|
||||
|
||||
This module defines various settings for the game engine, such as enabling or disabling the cursor, glow effect, gas effect, debug mode, and FPS display. It also provides a function to load particle properties from a JSON file.
|
||||
This module defines various settings for the game engine, such as
|
||||
enabling or disabling the cursor, glow effect, gas effect, debug mode,
|
||||
and FPS display.
|
||||
It also provides a function to load particle properties from a JSON file.
|
||||
|
||||
The `engine_settings` dictionary contains the configurable settings for the game engine. These settings can be used to customize the behavior of the game.
|
||||
The `engine_settings` dictionary contains the configurable settings
|
||||
for the game engine. These settings can be used to
|
||||
customize the behavior of the game.
|
||||
|
||||
The `load_particle_properties()` function attempts to load particle properties from a 'particles.json' file. If the file is not found or the JSON data is invalid, it returns an empty dictionary.
|
||||
The `load_particle_properties()` function attempts to load particle properties
|
||||
from a 'particles.json' file. If the file is not found or the
|
||||
JSON data is invalid, it returns an empty dictionary.
|
||||
|
||||
The `particle_properties` variable is initialized by calling `load_particle_properties()` when the module is imported.
|
||||
The `particle_properties` variable is initialized by calling
|
||||
`load_particle_properties()` when the module is imported.
|
||||
"""
|
||||
|
||||
import cProfile
|
||||
import pstats
|
||||
import sys
|
||||
import os
|
||||
import pygame
|
||||
import json
|
||||
import os
|
||||
import pstats
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pygame
|
||||
|
||||
engine_settings = {
|
||||
'pause_sim': True,
|
||||
'enable_cursor': True,
|
||||
'enable_glow': False,
|
||||
'enable_gas_effect': True,
|
||||
'enable_debug': True,
|
||||
'enable_fps': True,
|
||||
'enable_WVisuals': False,
|
||||
'enable_PVisuals': False,
|
||||
'enable_TempVisuals': False,
|
||||
'outerwall': False,
|
||||
"pause_sim": True,
|
||||
"enable_cursor": True,
|
||||
"enable_glow": False,
|
||||
"enable_gas_effect": True,
|
||||
"enable_debug": True,
|
||||
"enable_fps": True,
|
||||
"enable_WVisuals": False,
|
||||
"enable_PVisuals": False,
|
||||
"enable_TempVisuals": False,
|
||||
"outerwall": False,
|
||||
# 'settings': True/False
|
||||
}
|
||||
|
||||
|
||||
def load_particle_properties():
|
||||
"""Loads particle properties from a JSON file."""
|
||||
particle_data = {}
|
||||
base_path = os.path.join(os.path.dirname(__file__), '..')
|
||||
core_path = os.path.join(base_path, 'part', 'coreparts')
|
||||
mods_path = os.path.join(base_path, 'part', 'mods')
|
||||
base_path = os.path.join(os.path.dirname(__file__), "..")
|
||||
core_path = os.path.join(base_path, "part", "coreparts")
|
||||
mods_path = os.path.join(base_path, "part", "mods")
|
||||
|
||||
def load_json_files(directory):
|
||||
for root, _, files in os.walk(directory):
|
||||
for file in files:
|
||||
if file.endswith('.json'):
|
||||
if file.endswith(".json"):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
"""# Debug: Print specific temperature-related properties
|
||||
for particle_name, props in data.items():
|
||||
if 'temperature' in props:
|
||||
print(f"Loaded {particle_name} with temp: {props['temperature']}")
|
||||
if 'melt_temperature' in props:
|
||||
print(f"Loaded {particle_name} with melt_temp: {props['melt_temperature']}")"""
|
||||
|
||||
particle_data.update(data)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print(f"Error loading {file}: {e}")
|
||||
@ -65,16 +73,21 @@ def load_particle_properties():
|
||||
else:
|
||||
print(f"Directory not found: {path}")
|
||||
|
||||
"""# Final verification of critical properties
|
||||
print("\nVerifying critical properties:")
|
||||
for name, props in particle_data.items():
|
||||
if 'lava' in name.lower():
|
||||
print(f"Lava properties: {props}")"""
|
||||
|
||||
return particle_data
|
||||
|
||||
|
||||
# Load particle properties once when module is imported
|
||||
particle_properties = load_particle_properties()
|
||||
|
||||
__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os']
|
||||
|
||||
__all__ = [
|
||||
"pygame",
|
||||
"np",
|
||||
"random",
|
||||
"time",
|
||||
"engine_settings",
|
||||
"particle_properties",
|
||||
"cProfile",
|
||||
"pstats",
|
||||
"sys",
|
||||
"os",
|
||||
]
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
from config.settings import engine_settings, pygame
|
||||
"""# DebuggerSystem.py"""
|
||||
|
||||
import time
|
||||
|
||||
import psutil
|
||||
|
||||
from src.config.settings import engine_settings, pygame # typing: ignore
|
||||
|
||||
process = psutil.Process()
|
||||
|
||||
|
||||
class DebuggerSystem:
|
||||
"""DebuggerSystem class for tracking and displaying performance metrics."""
|
||||
|
||||
def __init__(self):
|
||||
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||
self.debug_font = pygame.font.SysFont(None, 24)
|
||||
@ -10,9 +20,15 @@ class DebuggerSystem:
|
||||
self.current_fps = 0
|
||||
self.debug_text = []
|
||||
self.performance_metrics = {
|
||||
'particle_updates': 0,
|
||||
'physics_time': 0,
|
||||
'render_time': 0
|
||||
"particle_updates": 0,
|
||||
"physics_time": 0,
|
||||
"render_time": 0,
|
||||
"spatial_grid_updates": 0,
|
||||
"active_particles_near_mouse": 0,
|
||||
"render_regions": 0,
|
||||
"memory_usage": 0,
|
||||
"cpu_usage": 0,
|
||||
"mouse_movement_delta": 0,
|
||||
}
|
||||
|
||||
def track_fps(self):
|
||||
@ -27,30 +43,60 @@ class DebuggerSystem:
|
||||
self.fps_timer = current_time
|
||||
return self.current_fps
|
||||
|
||||
def update_performance_metrics(self, sim):
|
||||
"""Track simulation performance metrics"""
|
||||
self.performance_metrics['particle_updates'] = len(sim.active_particles)
|
||||
return self.performance_metrics
|
||||
def track_spatial_activity(self, sim, mouse_pos):
|
||||
"""Track spatial grid updates near mouse position"""
|
||||
x, y = mouse_pos
|
||||
cell_key = sim.get_cell_key(x, y)
|
||||
radius = 3 # Track 3x3 grid around mouse
|
||||
active_count = 0
|
||||
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dy in range(-radius, radius + 1):
|
||||
check_key = (cell_key[0] + dx, cell_key[1] + dy)
|
||||
if check_key in sim.spatial_grid:
|
||||
active_count += len(sim.spatial_grid[check_key])
|
||||
|
||||
self.performance_metrics["active_particles_near_mouse"] = active_count
|
||||
self.performance_metrics["spatial_grid_updates"] += 1
|
||||
|
||||
def track_render_performance(self, rendering):
|
||||
"""Track rendering system metrics"""
|
||||
self.performance_metrics["render_regions"] = len(
|
||||
rendering.active_particles
|
||||
)
|
||||
|
||||
def update_system_metrics(self):
|
||||
"""Track system resource usage"""
|
||||
self.performance_metrics["memory_usage"] = (
|
||||
process.memory_info().rss / 1024 / 1024
|
||||
) # MB
|
||||
self.performance_metrics["cpu_usage"] = process.cpu_percent()
|
||||
|
||||
def draw_debug_overlay(self, screen, sim):
|
||||
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']:
|
||||
"""Draws the debug overlay on the screen."""
|
||||
if (
|
||||
not engine_settings["enable_fps"]
|
||||
and not engine_settings["enable_debug"]
|
||||
):
|
||||
return
|
||||
|
||||
self.debug_surface.fill((0, 0, 0, 128))
|
||||
y_offset = 10
|
||||
|
||||
if engine_settings['enable_fps']:
|
||||
fps_text = f"FPS: {self.current_fps:.1f} | TPS: {sim.track_tps():.1f}"
|
||||
if engine_settings["enable_fps"]:
|
||||
fps_text = (
|
||||
f"FPS: {self.current_fps:.1f} | TPS: {sim.track_tps():.1f}"
|
||||
)
|
||||
fps_surf = self.debug_font.render(fps_text, True, (255, 255, 255))
|
||||
self.debug_surface.blit(fps_surf, (10, y_offset))
|
||||
y_offset += 25
|
||||
|
||||
if engine_settings['enable_debug']:
|
||||
if engine_settings["enable_debug"]:
|
||||
debug_info = [
|
||||
f"Active Particles: {len(sim.active_particles)}",
|
||||
f"Total Particles: {sim.particle_count}",
|
||||
f"Current Type: {sim.current_particle_type}",
|
||||
f"Brush Size: {sim.brush_size}"
|
||||
f"Brush Size: {sim.brush_size}",
|
||||
]
|
||||
|
||||
for line in debug_info:
|
||||
@ -62,5 +108,5 @@ class DebuggerSystem:
|
||||
|
||||
def log_error(self, error_msg):
|
||||
"""Log errors for debugging"""
|
||||
with open('debug_log.txt', 'a') as f:
|
||||
with open("debug_log.txt", "a", encoding="utf-8") as f:
|
||||
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_msg}\n")
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
"""Load particle properties from json so we know what particles
|
||||
we got and how they should be simulated.
|
||||
"""
|
||||
|
||||
|
||||
# Load particle properties from json so we know what particles we got and how they should be simulated.
|
||||
class Particle:
|
||||
def __init__(self, simulation, position, velocity, mass, particle_type, properties, temperature=20):
|
||||
"""Set up a particle with the given properties."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
simulation,
|
||||
position,
|
||||
velocity,
|
||||
mass,
|
||||
particle_type,
|
||||
properties,
|
||||
temperature=20,
|
||||
):
|
||||
self.position = position # (x, y)
|
||||
self.velocity = velocity # (vx, vy)
|
||||
self.mass = mass
|
||||
@ -32,9 +46,13 @@ class Particle:
|
||||
self.melt = properties.get("melt", None)
|
||||
self.melt_temperature = properties.get("melt_temperature", None)
|
||||
self.solidify = properties.get("solidify", None)
|
||||
self.solidify_temperature = properties.get("solidify_temperature", None)
|
||||
self.solidify_temperature = properties.get(
|
||||
"solidify_temperature", None
|
||||
)
|
||||
self.evaporate = properties.get("evaporate", None)
|
||||
self.evaporate_temperature = properties.get("evaporate_temperature", None)
|
||||
self.evaporate_temperature = properties.get(
|
||||
"evaporate_temperature", None
|
||||
)
|
||||
self.freeze = properties.get("freeze", None)
|
||||
self.freeze_temperature = properties.get("freeze_temperature", None)
|
||||
|
||||
@ -49,7 +67,9 @@ class Particle:
|
||||
self.pressure_resistance = properties.get("pressure_resistance", 0)
|
||||
self.pressure_tolerance = properties.get("pressure_tolerance", 0)
|
||||
self.pressure_threshold = properties.get("pressure_threshold", 0)
|
||||
self.pressure_threshold_duration = properties.get("pressure_threshold_duration", 0)
|
||||
self.pressure_threshold_duration = properties.get(
|
||||
"pressure_threshold_duration", 0
|
||||
)
|
||||
|
||||
# Burning properties
|
||||
self.burning = properties.get("burning", False)
|
||||
@ -62,8 +82,14 @@ class Particle:
|
||||
|
||||
@classmethod
|
||||
def from_type(cls, simulation, position, particle_type, properties):
|
||||
"""Pre-initialize a particle with default values based on its type."""
|
||||
default_velocity = [0, 0]
|
||||
default_mass = properties.get("mass", 1.0)
|
||||
return cls(simulation, position, default_velocity, default_mass, particle_type, properties)
|
||||
|
||||
|
||||
return cls(
|
||||
simulation,
|
||||
position,
|
||||
default_velocity,
|
||||
default_mass,
|
||||
particle_type,
|
||||
properties,
|
||||
)
|
||||
|
||||
0
src/physics/py.typed
Normal file
0
src/physics/py.typed
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,72 +0,0 @@
|
||||
|
||||
from config.settings import np, time, engine_settings
|
||||
from src.physics.particle import Particle, particle_properties
|
||||
|
||||
|
||||
|
||||
class SimulationBase:
|
||||
def __init__(self, width: int, height: int, x: int = 0, y: int = 0):
|
||||
self.dormant_particles = set()
|
||||
self.particle_movement_counter = {}
|
||||
self.DORMANT_THRESHOLD = 10
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.new_x = 0
|
||||
self.new_y = 0
|
||||
|
||||
self.particle_size = 3
|
||||
self.particles = [[None for _ in range(height)] for _ in range(width)]
|
||||
print(f"Base init - particles type: {type(self.particles)}")
|
||||
print(f"After initialization - particles type: {type(self.particles)}")
|
||||
self.active_particles = set()
|
||||
self.particle_count = 0
|
||||
self.brush_size = 1
|
||||
self.max_brush_size = 20
|
||||
self.particle_properties = particle_properties
|
||||
self.particle_types = list(self.particle_properties.keys())
|
||||
self.current_particle_type = self.particle_types[0] if self.particle_types else 'sand'
|
||||
self.gravity = 9.8
|
||||
self.wind_zones = []
|
||||
self.wind = [0.0, 0.0]
|
||||
|
||||
|
||||
def create_particle(self, x, y): # this is where we create the particle.
|
||||
if engine_settings ["enable_debug"]:
|
||||
print(f"Before particle creation - particles type: {type(self.particles)}")
|
||||
|
||||
"""Create a new particle with full property support"""
|
||||
particle_type = self.current_particle_type.lower()
|
||||
# Check if the particle is within the grid boundaries
|
||||
if particle_type in self.particle_properties:
|
||||
grid_x = x // self.particle_size
|
||||
grid_y = y // self.particle_size
|
||||
|
||||
if (0 <= grid_x < self.width and
|
||||
0 <= grid_y < self.height and
|
||||
self.particles[grid_x][grid_y] is None):
|
||||
|
||||
properties = self.particle_properties[particle_type]
|
||||
position = (grid_x, grid_y)
|
||||
new_particle = Particle(
|
||||
position=position,
|
||||
velocity=[0, 0],
|
||||
mass=properties.get('mass', 1.0),
|
||||
particle_type=particle_type,
|
||||
properties=properties
|
||||
)
|
||||
|
||||
self.particles[grid_x][grid_y] = new_particle
|
||||
self.active_particles.add((grid_x, grid_y))
|
||||
self.particle_count += 1
|
||||
|
||||
|
||||
def create_particle_circle(self, center_x, center_y):
|
||||
brush_size = int(self.brush_size)
|
||||
for dx in range(-brush_size, brush_size + 1):
|
||||
for dy in range(-brush_size, brush_size + 1):
|
||||
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
|
||||
grid_x = (center_x + dx * self.particle_size) // self.particle_size
|
||||
grid_y = (center_y + dy * self.particle_size) // self.particle_size
|
||||
self.create_particle(grid_x, grid_y)
|
||||
@ -1,296 +0,0 @@
|
||||
"""
|
||||
Physics force calculations and movement systems for Sandpypi
|
||||
Handles particle movement, forces, and physics calculations
|
||||
"""
|
||||
from config.settings import np
|
||||
from src.physics.particle import Particle
|
||||
|
||||
class ForceSystem:
|
||||
def __init__(self, simulation):
|
||||
self.sim = simulation
|
||||
self.wind_zones = []
|
||||
|
||||
|
||||
def calculate_forces(self, particle, x, y):
|
||||
"""Calculate net forces acting on a particle."""
|
||||
# Initialize forces as numpy array for vectorized operations
|
||||
forces = np.zeros(2, dtype=np.float32) # [fx, fy]
|
||||
|
||||
# Vectorized wind zone calculations
|
||||
if self.wind_zones:
|
||||
positions = np.array([[zone['x'], zone['y']] for zone in self.wind_zones])
|
||||
directions = np.array([zone['direction'] for zone in self.wind_zones])
|
||||
strengths = np.array([zone['strength'] for zone in self.wind_zones])
|
||||
radii = np.array([zone['radius'] for zone in self.wind_zones])
|
||||
|
||||
# Calculate distances vectorized
|
||||
dx = x - positions[:, 0]
|
||||
dy = y - positions[:, 1]
|
||||
distances = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
# Apply wind zone forces where distance <= radius
|
||||
mask = distances <= radii
|
||||
scale_factors = np.where(mask, (1 - distances/radii) * strengths[:, np.newaxis], 0)
|
||||
forces += np.sum(directions * scale_factors[:, np.newaxis], axis=0)
|
||||
|
||||
# Apply global wind with vectorized operation
|
||||
wind_factor = 0.5 if particle.is_gas else 1.0
|
||||
forces += np.array(self.wind) * wind_factor
|
||||
|
||||
# Apply drag force vectorized
|
||||
drag = particle.viscosity * -1
|
||||
forces += drag * np.array(particle.velocity)
|
||||
|
||||
# Neighbor forces using numpy arrays
|
||||
neighbors = np.array(self._get_neighbors_from_grid(x, y))
|
||||
if len(neighbors):
|
||||
valid_mask = (
|
||||
(neighbors[:, 0] >= 0) &
|
||||
(neighbors[:, 0] < self.width) &
|
||||
(neighbors[:, 1] >= 0) &
|
||||
(neighbors[:, 1] < self.height)
|
||||
)
|
||||
neighbors = neighbors[valid_mask]
|
||||
|
||||
for nx, ny in neighbors:
|
||||
if (nx, ny) != (x, y):
|
||||
neighbor = self.sim.particles[nx][ny]
|
||||
if neighbor:
|
||||
self._apply_neighbor_forces(particle, neighbor, forces[0], forces[1])
|
||||
|
||||
return forces[0], forces[1]
|
||||
|
||||
|
||||
def _apply_neighbor_forces(self, particle, neighbor, fx, fy):
|
||||
"""Optimized neighbor force calculation"""
|
||||
if hasattr(neighbor, 'temperature') and hasattr(particle, 'temperature'):
|
||||
temp_diff = neighbor.temperature - particle.temperature
|
||||
fy += temp_diff * 0.05
|
||||
|
||||
|
||||
def apply_gravity(self, dt):
|
||||
"""Handle only gravity and basic particle movement"""
|
||||
self.sim.spatial_grid.spatial_grid.clear()
|
||||
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
if not particle or particle.particle_type in ['wall', 'stone', 'wood']:
|
||||
continue
|
||||
|
||||
# Apply gravity
|
||||
new_y = y + 1
|
||||
new_x = x
|
||||
|
||||
# Check boundaries
|
||||
if not (0 <= new_x < self.width and 0 <= new_y < self.height):
|
||||
continue
|
||||
|
||||
# Handle granular materials (sand, dirt)
|
||||
if particle.particle_type in ['sand', 'dirt', 'snow', 'ice']:
|
||||
if self.sim.particles[x][new_y] is None:
|
||||
new_x, new_y = x, y + 1
|
||||
else:
|
||||
# Define diagonal directions first
|
||||
diagonal_dirs = [(-1, 1), (1, 1)]
|
||||
diagonal_dirs = np.array(diagonal_dirs)
|
||||
# Randomize movement directions
|
||||
diagonal_dirs = np.random.permutation(diagonal_dirs)
|
||||
|
||||
for dx, dy in diagonal_dirs:
|
||||
test_x = x + dx
|
||||
test_y = y + dy
|
||||
if (0 <= test_x < self.width and 0 <= test_y < self.height and
|
||||
self.sim.particles[test_x][test_y] is None):
|
||||
if np.random.random() < 0.8: # 80% chance to move diagonally
|
||||
new_x = test_x
|
||||
new_y = test_y
|
||||
break
|
||||
|
||||
# Handle liquid movement (water, lava)
|
||||
elif particle.liquid:
|
||||
if self.sim.particles[x][new_y] is None:
|
||||
new_x = x
|
||||
new_y = y + 1
|
||||
else:
|
||||
spread_directions = np.array([(-1, 0), (1, 0)])
|
||||
np.random.shuffle(spread_directions)
|
||||
for dx, _ in spread_directions:
|
||||
test_x = x + dx
|
||||
if (0 <= test_x < self.width and
|
||||
self.sim.particles[test_x][y] is None):
|
||||
new_x = test_x
|
||||
new_y = y
|
||||
break
|
||||
|
||||
# Move particle if destination is empty
|
||||
if self.sim.particles[new_x][new_y] is None:
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
self.sim.particles.add((new_x, new_y))
|
||||
self.sim.particles.discard((x, y))
|
||||
particle.position = (new_x, new_y)
|
||||
|
||||
|
||||
def apply_physics(self, dt, engine_settings):
|
||||
"""Handle all physics effects"""
|
||||
new_active_particles = set()
|
||||
updates = []
|
||||
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
if not particle:
|
||||
continue
|
||||
|
||||
# Skip wall physics - walls are immutable
|
||||
if particle.particle_type == 'wall':
|
||||
new_active_particles.add((x, y))
|
||||
continue
|
||||
|
||||
# Handle dissipating particles
|
||||
if particle.particle_type in ['fire', 'flame']:
|
||||
# Clear current position first
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles.discard((x, y))
|
||||
|
||||
# Handle fire movement using numpy random
|
||||
dx = np.random.uniform(-0.5, 0.5)
|
||||
dy = np.random.uniform(-1.5, -0.5) # Upward drift
|
||||
new_x = int(x + dx)
|
||||
new_y = int(y + dy)
|
||||
|
||||
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
||||
if self.sim.particles[new_x][new_y] is None:
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
new_active_particles.add((new_x, new_y))
|
||||
|
||||
# Generate smoke above with numpy random
|
||||
if np.random.random() < 0.25 and new_y > 0:
|
||||
properties = self.sim.particle_properties['smoke']
|
||||
new_smoke = Particle(
|
||||
position=(new_x, new_y-1),
|
||||
velocity=[np.random.uniform(-0.5, 0.5), -1],
|
||||
mass=properties.get('mass', 0.1),
|
||||
particle_type='smoke',
|
||||
properties=properties
|
||||
)
|
||||
if self.sim.particles[new_x][new_y-1] is None:
|
||||
self.sim.particles[new_x][new_y-1] = new_smoke
|
||||
new_active_particles.add((new_x, new_y-1))
|
||||
|
||||
# Dissipation chance using numpy random
|
||||
if np.random.random() < 0.02:
|
||||
continue
|
||||
continue
|
||||
|
||||
# Air handling
|
||||
if particle.particle_type == 'air':
|
||||
continue
|
||||
|
||||
# Handle phase transitions
|
||||
self.handle_phase_transitions(particle, x, y)
|
||||
|
||||
# Calculate forces
|
||||
fx, fy = self.calculate_forces(particle, x, y)
|
||||
|
||||
# Handle gas particles
|
||||
if particle.is_gas:
|
||||
# Gas movement with numpy random
|
||||
dx = np.random.uniform(2, -1)
|
||||
dy = np.random.uniform(-2, 0) # Bias upward
|
||||
new_x = int(x + dx)
|
||||
new_y = int(y + dy)
|
||||
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles.discard((x, y))
|
||||
|
||||
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
||||
if self.sim.particles[new_x][new_y] is None:
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
new_active_particles.add((new_x, new_y))
|
||||
self.sim.particles.discard((x, y))
|
||||
continue
|
||||
else:
|
||||
# Regular particle physics
|
||||
mass = max(particle.mass, 0.001)
|
||||
particle.velocity[0] += (fx / mass) * dt
|
||||
particle.velocity[1] += (fy / mass) * dt
|
||||
|
||||
if particle.liquid:
|
||||
# Enhanced liquid spreading with numpy random
|
||||
if np.random.random() < 0.5:
|
||||
dx = np.random.choice([-1, 1])
|
||||
if (0 <= x + dx < self.width and
|
||||
self.sim.particles[x + dx][y] is None):
|
||||
new_x = x + dx
|
||||
new_y = y
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
new_active_particles.add((new_x, new_y))
|
||||
continue
|
||||
|
||||
new_x = int(x + particle.velocity[0] * dt)
|
||||
new_y = int(y + particle.velocity[1] * dt)
|
||||
|
||||
# Update position
|
||||
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
||||
if self.sim.particles[new_x][new_y] is None:
|
||||
updates.append((x, y, new_x, new_y, particle))
|
||||
new_active_particles.add((new_x, new_y))
|
||||
self._wake_neighbors(new_x, new_y)
|
||||
else:
|
||||
new_active_particles.add((x, y))
|
||||
|
||||
# Apply updates
|
||||
for old_x, old_y, new_x, new_y, particle in updates:
|
||||
self.sim.particles[old_x][old_y] = None
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
particle.position = (new_x, new_y)
|
||||
|
||||
# Handle boundaries
|
||||
if engine_settings['outerwall']:
|
||||
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
|
||||
if self.sim.particles[x][y] is None:
|
||||
properties = self.sim.particle_properties['wall']
|
||||
wall = Particle.from_type((x, y), 'wall', properties)
|
||||
self.sim.particles[x][y] = wall
|
||||
new_active_particles.add((x, y))
|
||||
self.particle_count += 1
|
||||
continue
|
||||
else:
|
||||
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
|
||||
if self.sim.particles[x][y] is not None:
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles.discard((x, y))
|
||||
self.sim.particle_count -= 1
|
||||
continue
|
||||
|
||||
self.sim.particles = new_active_particles
|
||||
|
||||
|
||||
def add_wind_zone(self, x, y):
|
||||
# Instead of creating particles, store wind zone data
|
||||
wind_zone = {
|
||||
'x': x,
|
||||
'y': y,
|
||||
'radius': 50,
|
||||
'strength': 2.0,
|
||||
'direction': [1, 0]
|
||||
}
|
||||
self.wind_zones.append(wind_zone)
|
||||
|
||||
|
||||
def handle_gas_movement(self, particle, x, y): # this is where we handle the gas movement. this function sucks. wip
|
||||
"""Handle gas particle movement"""
|
||||
if particle.is_gas:
|
||||
dx = np.random.uniform(-1, 1)
|
||||
dy = np.random.uniform(-2, 0) # Bias upward movement
|
||||
new_x = int(x + dx)
|
||||
new_y = int(y + dy)
|
||||
|
||||
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
||||
if self.sim.particles[new_x][new_y] is None:
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.particles[new_x][new_y] = particle
|
||||
self.sim.particles.add((new_x, new_y))
|
||||
self.sim.particles.discard((x, y))
|
||||
@ -1,87 +0,0 @@
|
||||
|
||||
"""
|
||||
Spatial grid management systems for Sandpypi
|
||||
Handles particle positioning and neighbor calculations
|
||||
"""
|
||||
from physics.base import SimulationBase
|
||||
|
||||
|
||||
class SpatialGrid:
|
||||
def __init__(self, simulation):
|
||||
self.sim = simulation
|
||||
self.width = simulation.width
|
||||
self.height = simulation.height
|
||||
self.cell_size = 32
|
||||
self.spatial_grid = {}
|
||||
|
||||
|
||||
def update_spatial_grid(self):
|
||||
"""Enhanced spatial grid update"""
|
||||
if len(self.sim.active_particles) > 100:
|
||||
self.spatial_grid = {}
|
||||
cell_lists = {}
|
||||
|
||||
# Track temperature-sensitive particles
|
||||
temp_sensitive_cells = set()
|
||||
|
||||
for x, y in self.sim.active_particles:
|
||||
cell_key = (x // self.cell_size, y // self.cell_size)
|
||||
if cell_key not in cell_lists:
|
||||
cell_lists[cell_key] = []
|
||||
cell_lists[cell_key].append((x, y))
|
||||
|
||||
# Mark cells with temperature-sensitive particles
|
||||
particle = self.sim.particles[x][y]
|
||||
if hasattr(particle, 'solidify_temperature') or hasattr(particle, 'melt_temperature'):
|
||||
temp_sensitive_cells.add(cell_key)
|
||||
|
||||
self.spatial_grid = {k: set(v) for k, v in cell_lists.items()}
|
||||
return temp_sensitive_cells
|
||||
|
||||
|
||||
def get_cell_key(self, x, y): # this is where we get the cell key.
|
||||
# Convert coordinates to grid cell
|
||||
cell_x = x // self.cell_size
|
||||
cell_y = y // self.cell_size
|
||||
return (cell_x, cell_y)
|
||||
|
||||
|
||||
def _get_neighbors_from_grid(self, x, y):
|
||||
"""Get neighbors using spatial grid"""
|
||||
cell_key = self.get_cell_key(x, y)
|
||||
neighbors = []
|
||||
|
||||
# Check current and adjacent cells
|
||||
for dx in [-1, 0, 1]:
|
||||
for dy in [-1, 0, 1]:
|
||||
check_key = (cell_key[0] + dx, cell_key[1] + dy)
|
||||
if check_key in self.spatial_grid:
|
||||
neighbors.extend(self.spatial_grid[check_key])
|
||||
|
||||
return neighbors
|
||||
|
||||
|
||||
def add_to_spatial_grid(self, particle, x, y): # this is where we add to the spatial grid.
|
||||
cell_key = self.get_cell_key(x, y)
|
||||
if cell_key not in self.spatial_grid:
|
||||
self.spatial_grid[cell_key] = set()
|
||||
self.spatial_grid[cell_key].add((x, y))
|
||||
return cell_key
|
||||
|
||||
|
||||
def remove_from_spatial_grid(self, x, y): # this is where we remove from the spatial grid.
|
||||
cell_key = self.get_cell_key(x, y)
|
||||
if cell_key in self.spatial_grid:
|
||||
self.spatial_grid[cell_key].discard((x, y))
|
||||
return cell_key
|
||||
|
||||
|
||||
def _wake_neighbors(self, x, y):
|
||||
for dx in [-1, 0, 1]:
|
||||
for dy in [-1, 0, 1]:
|
||||
nx, ny = x + dx, y + dy
|
||||
key = (nx, ny)
|
||||
if key in self.dormant_particles:
|
||||
self.dormant_particles.discard(key)
|
||||
self.particle_movement_counter[key] = 0
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
|
||||
"""
|
||||
Particle interaction handling systems for Sandpypi
|
||||
Manages how different particle types interact with each other
|
||||
"""
|
||||
from config.settings import np
|
||||
from src.physics.particle import Particle
|
||||
from physics.base import SimulationBase
|
||||
|
||||
|
||||
class ParticleInteractions:
|
||||
def __init__(self, simulation):
|
||||
self.sim = simulation
|
||||
|
||||
def handle_particle_interactions(self, dt): # this is where we handle all the particle interactions.
|
||||
"""Handle interactions between different particle types"""
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
|
||||
if not particle:
|
||||
continue
|
||||
|
||||
# Handle damage for any particle with durability
|
||||
self.handle_particle_damage(particle, x, y)
|
||||
|
||||
# Check neighboring particles
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
neighbor = self.particles[nx][ny]
|
||||
if neighbor:
|
||||
self.process_interaction(particle, neighbor, x, y, nx, ny)
|
||||
|
||||
|
||||
def process_interaction(self, particle1, particle2, x1, y1, x2, y2): # this function is part 2 of handle_particle_interactions.
|
||||
"""Process specific interactions between two particles"""
|
||||
# Water + Sand = Wet Sand
|
||||
if (particle1.particle_type == 'water' and particle2.particle_type == 'sand' or
|
||||
particle2.particle_type == 'water' and particle1.particle_type == 'sand'):
|
||||
self.create_mud(x1, y1, 'wsand') # Pass wsand type
|
||||
self.particles[x2][y2] = None
|
||||
self.active_particles.discard((x2, y2))
|
||||
|
||||
# Water + Dirt = Mud
|
||||
if (particle1.particle_type == 'water' and particle2.particle_type == 'dirt' or
|
||||
particle2.particle_type == 'water' and particle1.particle_type == 'dirt'):
|
||||
self.create_mud(x1, y1, 'mud') # Pass mud type
|
||||
self.particles[x2][y2] = None
|
||||
self.active_particles.discard((x2, y2))
|
||||
|
||||
# Lava/Fire effects
|
||||
if particle1.particle_type in ['lava', 'fire', 'flame'] or particle2.particle_type in ['lava', 'fire', 'flame']:
|
||||
target = particle2 if particle1.particle_type in ['lava', 'fire', 'flame'] else particle1
|
||||
target_x, target_y = (x2, y2) if particle1.particle_type in ['lava', 'fire', 'flame'] else (x1, y1)
|
||||
|
||||
# Water to Steam
|
||||
if target.particle_type == 'water':
|
||||
self.transform_particle(target_x, target_y, 'steam')
|
||||
|
||||
# Wood to Fire
|
||||
elif target.particle_type == 'wood':
|
||||
if np.random.random() < 0.3: # 30% chance to ignite
|
||||
self.transform_particle(target_x, target_y, 'fire')
|
||||
|
||||
|
||||
def mix_liquids(self, liquid1, liquid2): # not implemented
|
||||
"""Handle liquid mixing interactions"""
|
||||
if liquid1.temperature != liquid2.temperature:
|
||||
avg_temp = (liquid1.temperature + liquid2.temperature) / 2
|
||||
liquid1.temperature = avg_temp
|
||||
liquid2.temperature = avg_temp
|
||||
liquid1.density = self.calculate_density(liquid1.temperature)
|
||||
liquid2.density = self.calculate_density(liquid2.temperature)
|
||||
liquid1.viscosity = self.calculate_viscosity(liquid1.temperature)
|
||||
liquid2.viscosity = self.calculate_viscosity(liquid2.temperature)
|
||||
liquid1.color = self.calculate_color(liquid1.temperature)
|
||||
liquid2.color = self.calculate_color(liquid2.temperature)
|
||||
|
||||
def create_mud(self, x, y, mud_type):
|
||||
"""Create either wet sand or mud based on the specified type"""
|
||||
if mud_type in self.particle_properties:
|
||||
properties = self.particle_properties[mud_type]
|
||||
new_particle = Particle.from_type((x, y), mud_type, properties)
|
||||
self.particles[x][y] = new_particle
|
||||
self.active_particles.add((x, y))
|
||||
|
||||
def handle_particle_damage(self, particle, x, y):
|
||||
"""Handle damage calculations for particles with durability"""
|
||||
if not hasattr(particle, 'durability'):
|
||||
return
|
||||
|
||||
# Pressure damage
|
||||
fx, fy = self.calculate_forces(particle, x, y)
|
||||
pressure = (fx*fx + fy*fy)**0.5 # Calculate magnitude of force vector
|
||||
|
||||
if hasattr(particle, 'pressure_threshold') and pressure > particle.pressure_threshold:
|
||||
particle.durability -= 0.1
|
||||
|
||||
# Impact damage
|
||||
neighbors = self._get_neighbors_from_grid(x, y)
|
||||
for nx, ny in neighbors:
|
||||
neighbor = self.particles[nx][ny]
|
||||
if neighbor and neighbor.velocity[1] > 5.0:
|
||||
particle.durability -= 0.2
|
||||
|
||||
# Heat damage
|
||||
if particle.temperature > 900:
|
||||
particle.durability -= 1
|
||||
|
||||
# Check if particle should break
|
||||
if particle.durability <= 0 and hasattr(particle, 'broken'):
|
||||
self.transform_particle(x, y, particle.broken)
|
||||
return
|
||||
|
||||
|
||||
def _get_quick_neighbors(self, x, y):
|
||||
"""Quick neighbor lookup without full spatial grid"""
|
||||
return [(x+dx, y+dy) for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]]
|
||||
@ -1,73 +0,0 @@
|
||||
|
||||
from physics.base import SimulationBase, time, np
|
||||
from physics.grid import SpatialGrid as sg
|
||||
from physics.forces import ForceSystem as fs
|
||||
from physics.temperature import TemperatureSystem as ts
|
||||
from physics.interactions import ParticleInteractions as pi
|
||||
from src.physics.particle import Particle, particle_properties
|
||||
from physics.sim import Simulation
|
||||
|
||||
class Simulation(SimulationBase):
|
||||
def __init__(self, width, height, x=0, y=0):
|
||||
print(f"Simulation init - before base init - particles type: {type(getattr(self, 'particles', None))}")
|
||||
|
||||
SimulationBase.__init__(self, width, height, x, y)
|
||||
|
||||
print(f"After base init - particles type: {type(self.particles)}")
|
||||
self.dormant_particles = set()
|
||||
self.particle_movement_counter = {}
|
||||
self.DORMANT_THRESHOLD = 10
|
||||
self.sim = Simulation(width, height)
|
||||
self.spatial_grid = sg(self)
|
||||
self.force_system = fs(self)
|
||||
self.temperature_system = ts(self)
|
||||
self.particle_interactions = pi(self)
|
||||
|
||||
def simulate_step(self, dt, engine_settings):
|
||||
"""Run simulation step with spatial grid updates"""
|
||||
self.spatial_grid.update_spatial_grid()
|
||||
|
||||
# Update particle positions and physics
|
||||
self.force_system.apply_gravity(dt)
|
||||
self.sim.Simulation.apply_physics(dt, engine_settings)
|
||||
|
||||
# Handle state changes and interactions
|
||||
self.temperature_system.handle_temperature(dt)
|
||||
self.particle_interactions.handle_particle_interactions(dt)
|
||||
self.temperature_system.burning()
|
||||
self.temperature_system.spread_fire()
|
||||
|
||||
|
||||
def track_tps(self):
|
||||
"""Track Ticks Per Second for simulation performance monitoring"""
|
||||
if not hasattr(self, '_tps_counter'):
|
||||
self._tps_counter = 0
|
||||
self._tps_timer = time.time()
|
||||
self._current_tps = 0
|
||||
|
||||
self._tps_counter += 1
|
||||
current_time = time.time()
|
||||
elapsed = current_time - self._tps_timer
|
||||
|
||||
# Update TPS count every second
|
||||
if elapsed >= 1.0:
|
||||
self._current_tps = self._tps_counter / elapsed
|
||||
self._tps_counter = 0
|
||||
self._tps_timer = current_time
|
||||
|
||||
return self._current_tps
|
||||
|
||||
|
||||
def reset_particle_count(self):
|
||||
"""Reset and recalculate accurate particle count"""
|
||||
active_count = np.sum([1 for x, y in self.active_particles if self.particles[x][y] is not None])
|
||||
self.particle_count = int(active_count)
|
||||
|
||||
|
||||
def get_accurate_particle_count(self):
|
||||
"""Get current accurate particle count using numpy"""
|
||||
particle_mask = np.array([[self.particles[x][y] is not None
|
||||
for y in range(self.height)]
|
||||
for x in range(self.width)])
|
||||
return np.sum(particle_mask)
|
||||
|
||||
@ -1,162 +0,0 @@
|
||||
|
||||
"""
|
||||
Temperature and state management systems for Sandpypi
|
||||
Handles temperature changes, phase transitions, and burning mechanics
|
||||
"""
|
||||
from config.settings import np
|
||||
|
||||
|
||||
|
||||
class TemperatureSystem:
|
||||
def __init__(self, simulation):
|
||||
self.sim = simulation
|
||||
|
||||
def handle_temperature(self, dt): # this is where we handle the temperature.
|
||||
"""Handle temperature changes and state transitions"""
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
if particle and particle.is_gas:
|
||||
particle.temperature -= 0.5 * dt
|
||||
if not particle:
|
||||
continue
|
||||
|
||||
|
||||
if particle.temperature > 1700:
|
||||
# Transition to gas
|
||||
particle.is_gas = True
|
||||
particle.temperature = 1700
|
||||
particle.velocity = [np.random.uniform(-1, 1), np.random.uniform(-1, 1)]
|
||||
particle.temperature < 1400
|
||||
particle.is_gas = False
|
||||
|
||||
# Temperature spread to neighbors
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
neighbor = self.sim.particles[nx][ny]
|
||||
if (neighbor and hasattr(neighbor, 'temperature')
|
||||
and neighbor.temperature is not None):
|
||||
temp_diff = particle.temperature - neighbor.temperature
|
||||
heat_transfer = temp_diff * 0.1 * dt
|
||||
particle.temperature -= heat_transfer
|
||||
neighbor.temperature += heat_transfer
|
||||
|
||||
|
||||
def handle_phase_transitions(self, particle, x, y): # this is where we handle all the phase transitions.
|
||||
"""Handle all phase transitions for a particle"""
|
||||
state_changed = False
|
||||
|
||||
if particle.particle_type == 'lava':
|
||||
if particle.temperature < particle.solidify_temperature:
|
||||
state_changed = self.transform_particle(x, y, particle.solidify)
|
||||
return state_changed
|
||||
|
||||
# Check evaporation
|
||||
if hasattr(particle, 'evaporate_temperature') and particle.evaporate_temperature is not None:
|
||||
if particle.temperature >= particle.evaporate_temperature and particle.evaporate:
|
||||
self.transform_particle(x, y, particle.evaporate)
|
||||
|
||||
# Check freezing
|
||||
if hasattr(particle, 'freeze_temperature') and particle.freeze_temperature is not None:
|
||||
if particle.temperature <= particle.freeze_temperature and particle.freeze:
|
||||
self.transform_particle(x, y, particle.freeze)
|
||||
|
||||
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
|
||||
and particle.melt_temperature is not None):
|
||||
if particle.temperature >= particle.melt_temperature:
|
||||
new_type = particle.melt
|
||||
if new_type in self.sim.particle:
|
||||
self.transform_particle(x, y, new_type)
|
||||
|
||||
# Check for solidification with proper attribute validation
|
||||
if (hasattr(particle, 'solidify') and hasattr(particle, 'solidify_temperature')
|
||||
and particle.solidify_temperature is not None):
|
||||
if particle.temperature < particle.solidify_temperature:
|
||||
new_type = particle.solidify
|
||||
if new_type in self.sim.particle_properties:
|
||||
self.transform_particle(x, y, new_type)
|
||||
return new_type
|
||||
|
||||
if particle.particle_type == 'steam':
|
||||
# Steam should condense when it cools
|
||||
if particle.temperature <= particle.solidify_temperature:
|
||||
self.transform_particle(x, y, new_type)
|
||||
return new_type
|
||||
|
||||
# Check durability property from JSON
|
||||
if (hasattr(particle, 'durability') and hasattr(particle, 'brk')
|
||||
and particle.brk is not None):
|
||||
if particle.durability <= 0:
|
||||
self.transform_particle(x, y, particle.broken)
|
||||
return new_type
|
||||
|
||||
|
||||
def burning(self): # this is where we handle the burning.
|
||||
"""Handle burning of particles."""
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
if particle and hasattr(particle, 'burning') and particle.burning:
|
||||
particle.temperature += 10
|
||||
if particle.temperature > 1000:
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.active_particles.remove((x, y))
|
||||
self.spatial_grid.pop((x, y), None)
|
||||
|
||||
|
||||
def spread_fire(self): # this is where we spread the fire.
|
||||
"""Spread fire to neighboring particles."""
|
||||
for x, y in list(self.sim.active_particles):
|
||||
particle = self.sim.particles[x][y]
|
||||
if particle and (particle.particle_type == 'fire' or getattr(particle, 'burning', False)):
|
||||
# Check all neighboring cells including diagonals
|
||||
for dx in [-1, 0, 1]:
|
||||
for dy in [-1, 0, 1]:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
neighbor = self.sim.particles[nx][ny]
|
||||
if neighbor and hasattr(neighbor, 'flamability'):
|
||||
if neighbor.particle_type == 'wood':
|
||||
# Higher chance to ignite wood
|
||||
if np.random.random() < 0.3: # 30% chance to spread
|
||||
self.ignite_particle(neighbor)
|
||||
elif neighbor.flamability > 0:
|
||||
if np.random.random() < 0.1: # 10% chance for other materials
|
||||
self.ignite_particle(neighbor)
|
||||
|
||||
|
||||
def ignite_particle(self, particle): # this is where we ignite the particle.
|
||||
"""Handle ignition and burning of flammable particles."""
|
||||
if hasattr(particle, 'flamability') and particle.flamability > 0.5:
|
||||
if hasattr(particle, 'temperature') and particle.temperature > 150:
|
||||
particle.type = 'fire'
|
||||
particle.temperature += 200
|
||||
# Add burning effect for wood
|
||||
if particle.type == 'wood':
|
||||
particle.burning = True
|
||||
particle.burn_time = 100 # burn time
|
||||
|
||||
def handle_special_particles(self, particle, x, y): # this is where we handle special particles.
|
||||
"""Handle special particle behaviors"""
|
||||
if particle.particle_type in ['fire', 'flame', 'smoke']:
|
||||
if np.random.random() < 0.6: # % chance
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.active_particles.discard((x, y))
|
||||
|
||||
if particle.particle_type in ['fire', 'flame', 'lava']:
|
||||
# Create smoke above with proper physics
|
||||
if np.random.random() < 0.65 and y > 0: # % chance for smoke
|
||||
properties = self.sim.particle_properties['smoke']
|
||||
new_smoke = self.sim.Particle.from_type((x, y-1), 'smoke', properties)
|
||||
if self.sim.particles[x][y-1] is None:
|
||||
self.sim.particles[x][y-1] = new_smoke
|
||||
self.sim.active_particles.add((x, y-1))
|
||||
|
||||
else:
|
||||
# Handle collision with water
|
||||
if particle.particle_type == 'water':
|
||||
self.sim.particles[x][y-1] = None
|
||||
self.sim.active_particles.discard((x, y-1))
|
||||
self.sim.particles[x][y] = None
|
||||
self.sim.active_particles.discard((x, y))
|
||||
self.sim.particles[x][y] = self.sim.Particle.from_type((x, y), 'water', self.sim.particle_properties['water'])
|
||||
self.sim.active_particles.add((x, y))
|
||||
0
src/rendering/py.typed
Normal file
0
src/rendering/py.typed
Normal file
@ -2,30 +2,49 @@
|
||||
#File Name: rendering.py
|
||||
Rendering class for the particle simulation.
|
||||
|
||||
This class is responsible for rendering the particles, UI elements, and debug information on the screen. It handles the setup of the display, pre-rendering of static UI elements, and the drawing of particles, buttons, and debug overlays.
|
||||
This class is responsible for rendering the particles, UI elements, and debug
|
||||
information on the screen.
|
||||
It handles the setup of the display, pre-rendering
|
||||
of static UI elements,
|
||||
and the drawing of particles, buttons, and debug overlays.
|
||||
|
||||
The `draw_particles` function is the main method for rendering the particles on the screen. It takes the particle data, active particles, particle size, and particle colors as input, and renders the particles on the `particle_surface`. The `particle_surface` is then blitted onto the main screen.
|
||||
The `draw_particles` function is the main method for rendering the particles
|
||||
on the screen. It takes the particle data, active particles, particle size,
|
||||
and particle colors as input,
|
||||
and renders the particles on the `particle_surface`. The `particle_surface` is
|
||||
then blitted onto the main screen.
|
||||
|
||||
The `draw_zoom_window` function is used to render a zoomed-in view of the particles around the mouse cursor. It creates a separate surface for the zoomed-in view and returns it.
|
||||
The `draw_zoom_window` function is used to render a zoomed-in view of the
|
||||
particles around the mouse cursor.
|
||||
It creates a separate surface for the zoomed-in view and returns it.
|
||||
|
||||
The `draw_debug_overlay` function is responsible for rendering the debug information, such as FPS, mouse position, and particle information, on the screen.
|
||||
The `draw_debug_overlay` responsible for rendering the debug information,
|
||||
such as FPS, mouse position, and particle information, on the screen.
|
||||
|
||||
The `draw_buttons` function handles the rendering of the category buttons, particle buttons, and other UI elements like the clear screen and settings buttons.
|
||||
The `draw_buttons` function handles the rendering of the category buttons,
|
||||
particle buttons, and other UI elements
|
||||
like the clear screen and settings buttons.
|
||||
|
||||
The `render_brush_cursor` function is used to draw a visual indicator for the current brush size.
|
||||
The `render_brush_cursor` draw a visual indicator for the current brush size.
|
||||
|
||||
The `draw_brush_size_slider` function renders a slider for adjusting the brush size.
|
||||
The `draw_brush_size_slider` renders a slider for adjusting the brush size.
|
||||
|
||||
The `draw_settings_menu` function creates a settings menu surface that can be displayed on the screen.
|
||||
The `draw_settings_menu` settings menu that can be displayed on the screen.
|
||||
|
||||
The `clear_screen` function is used to reset the simulation grid and clear the display surfaces.
|
||||
The `clear_screen` reset the simulation grid and clear the display surfaces.
|
||||
The `setup_gpu_rendering` placeholder for GPU-based rendering setup.
|
||||
"""
|
||||
|
||||
|
||||
from config.settings import pygame, random, particle_properties, engine_settings
|
||||
from ..config.settings import (
|
||||
engine_settings,
|
||||
particle_properties,
|
||||
pygame,
|
||||
random,
|
||||
)
|
||||
|
||||
|
||||
class Rendering:
|
||||
"""Main rendering system"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
# self.setup_gpu_rendering()
|
||||
@ -34,63 +53,85 @@ class Rendering:
|
||||
self.background.fill((0, 0, 0))
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.particle_count = 0
|
||||
self.particle_colors = {}
|
||||
self.particle_properties = particle_properties
|
||||
self.particle_surface = pygame.Surface((width, height), pygame.SRCALPHA)
|
||||
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||
self.particle_surface = pygame.Surface(
|
||||
(width, height), pygame.SRCALPHA
|
||||
)
|
||||
self.zoom_window_surface = pygame.Surface((200, 200), pygame.SRCALPHA)
|
||||
# self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||
self.particle_surface = pygame.Surface(
|
||||
(self.width, self.height), pygame.SRCALPHA
|
||||
)
|
||||
self.settings_menu_surface = pygame.Surface(
|
||||
(300, 300), pygame.SRCALPHA
|
||||
)
|
||||
self.brush_cursor_surface = pygame.Surface((10, 10), pygame.SRCALPHA)
|
||||
self._last_debug_info = None
|
||||
self.buttons = {}
|
||||
self.button_height = 30
|
||||
self.button_width = 100
|
||||
self.clear_screen_button = pygame.Rect(10, 10, 100, 30)
|
||||
self.settings_button = pygame.Rect(10, 50, 100, 30)
|
||||
self.cached_fonts = {
|
||||
'debug': pygame.font.SysFont(None, 24),
|
||||
'button': pygame.font.SysFont(None, 20),
|
||||
'slider': pygame.font.SysFont(None, 20),
|
||||
'settings': pygame.font.SysFont(None, 20),
|
||||
'zoom': pygame.font.SysFont(None, 20),
|
||||
'brush_size': pygame.font.SysFont(None, 20),
|
||||
'zoom_window': pygame.font.SysFont(None, 20),
|
||||
'zoom_window_text': pygame.font.SysFont(None, 20)
|
||||
"debug": pygame.font.SysFont(None, 24),
|
||||
"button": pygame.font.SysFont(None, 20),
|
||||
"slider": pygame.font.SysFont(None, 20),
|
||||
"settings": pygame.font.SysFont(None, 20),
|
||||
"zoom": pygame.font.SysFont(None, 20),
|
||||
"brush_size": pygame.font.SysFont(None, 20),
|
||||
"zoom_window": pygame.font.SysFont(None, 20),
|
||||
"zoom_window_text": pygame.font.SysFont(None, 20),
|
||||
}
|
||||
|
||||
# Pre-render static UI elements
|
||||
self.button_surfaces = {}
|
||||
|
||||
# Initialize categories
|
||||
for name, properties in particle_properties.items():
|
||||
if 'color' in properties:
|
||||
self.particle_colors[name.lower()] = properties['color']
|
||||
self.categories = {'Solids': [], 'Liquids': [], 'Gases': [], 'Special': []}
|
||||
if "color" in properties:
|
||||
self.particle_colors[name.lower()] = properties["color"]
|
||||
self.categories = {
|
||||
"Solids": [],
|
||||
"Liquids": [],
|
||||
"Gases": [],
|
||||
"Special": [],
|
||||
}
|
||||
for particle_name, properties in self.particle_properties.items():
|
||||
if properties.get('is_gas'):
|
||||
self.categories['Gases'].append(particle_name)
|
||||
elif properties.get('liquid'):
|
||||
self.categories['Liquids'].append(particle_name)
|
||||
elif properties.get('solid'):
|
||||
self.categories['Solids'].append(particle_name)
|
||||
if properties.get("is_gas"):
|
||||
self.categories["Gases"].append(particle_name)
|
||||
elif properties.get("liquid"):
|
||||
self.categories["Liquids"].append(particle_name)
|
||||
elif properties.get("solid"):
|
||||
self.categories["Solids"].append(particle_name)
|
||||
else:
|
||||
self.categories['Special'].append(particle_name)
|
||||
self.categories["Special"].append(particle_name)
|
||||
|
||||
self.current_category = 'Solids'
|
||||
self.current_category = "Solids"
|
||||
self.category_buttons = {}
|
||||
self.setup_category_menu()
|
||||
self.setup_static_ui()
|
||||
|
||||
|
||||
def setup_gpu_rendering(self):
|
||||
# Initialize OpenGL context
|
||||
"""Initialize OpenGL context"""
|
||||
pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 1)
|
||||
self.screen = pygame.display.set_mode((self.width, self.height),
|
||||
pygame.OPENGL | pygame.DOUBLEBUF)
|
||||
|
||||
self.screen = pygame.display.set_mode(
|
||||
(self.width, self.height), pygame.OPENGL | pygame.DOUBLEBUF
|
||||
)
|
||||
|
||||
def setup_static_ui(self):
|
||||
"""Setup static UI elements"""
|
||||
for category in self.categories:
|
||||
surf = pygame.Surface((80, 25))
|
||||
surf.fill((150, 150, 150))
|
||||
text = self.cached_fonts['button'].render(category, True, (0, 0, 0))
|
||||
text = self.cached_fonts["button"].render(
|
||||
category, True, (0, 0, 0)
|
||||
)
|
||||
surf.blit(text, (5, 5))
|
||||
self.button_surfaces[category] = surf
|
||||
|
||||
|
||||
def setup_category_menu(self):
|
||||
# Category buttons at the top
|
||||
"""Setup category menu"""
|
||||
x_offset = self.width - 350
|
||||
y_offset = 10
|
||||
for category in self.categories:
|
||||
@ -98,20 +139,25 @@ class Rendering:
|
||||
self.category_buttons[category] = button_rect
|
||||
x_offset += 90
|
||||
|
||||
|
||||
def load_buttons(self):
|
||||
"""Load buttons"""
|
||||
x_offset = 10
|
||||
y_offset = 10
|
||||
|
||||
for particle_type, properties in self.particle_properties.items():
|
||||
if 'color' in properties:
|
||||
button_rect = pygame.Rect(x_offset, y_offset, self.button_width, self.button_height)
|
||||
if "color" in properties:
|
||||
button_rect = pygame.Rect(
|
||||
x_offset, y_offset, self.button_width, self.button_height
|
||||
)
|
||||
self.buttons[particle_type.lower()] = button_rect
|
||||
x_offset += self.button_width + 10 # Add spacing between buttons
|
||||
x_offset += (
|
||||
self.button_width + 10
|
||||
) # Add spacing between buttons
|
||||
|
||||
|
||||
def draw_particles(self, particles, active_particles, particle_size, particle_colors): # this is the function that draws the particles
|
||||
#self.particle_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
|
||||
def draw_particles(
|
||||
self, particles, active_particles, particle_size, particle_colors
|
||||
):
|
||||
"""Draw particles"""
|
||||
self.particle_surface.fill((0, 0, 0, 0))
|
||||
|
||||
particle_batches = {}
|
||||
@ -124,23 +170,40 @@ class Rendering:
|
||||
if p_type not in particle_batches:
|
||||
particle_batches[p_type] = []
|
||||
|
||||
rect = pygame.Rect(x * particle_size, y * particle_size,
|
||||
particle_size, particle_size)
|
||||
rect = pygame.Rect(
|
||||
x * particle_size,
|
||||
y * particle_size,
|
||||
particle_size,
|
||||
particle_size,
|
||||
)
|
||||
particle_batches[p_type].append(rect)
|
||||
if particle.particle_type in particle_colors:
|
||||
color = particle_colors[particle.particle_type]
|
||||
else:
|
||||
color = (255, 255, 255)
|
||||
|
||||
if engine_settings['enable_glow']:
|
||||
if engine_settings["enable_glow"]:
|
||||
glow_color = (255, 255, 255)
|
||||
glow_radius = 0.5 * particle_size
|
||||
glow_surface = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
|
||||
pygame.draw.circle(glow_surface, glow_color, (glow_radius, glow_radius), glow_radius)
|
||||
glow_surface = pygame.Surface(
|
||||
(glow_radius * 2, glow_radius * 2), pygame.SRCALPHA
|
||||
)
|
||||
pygame.draw.circle(
|
||||
glow_surface,
|
||||
glow_color,
|
||||
(glow_radius, glow_radius),
|
||||
glow_radius,
|
||||
)
|
||||
glow_surface.set_alpha(85)
|
||||
self.particle_surface.blit(glow_surface, (x * particle_size - glow_radius, y * particle_size - glow_radius))
|
||||
self.particle_surface.blit(
|
||||
glow_surface,
|
||||
(
|
||||
x * particle_size - glow_radius,
|
||||
y * particle_size - glow_radius,
|
||||
),
|
||||
)
|
||||
|
||||
if engine_settings['enable_gas_effect']:
|
||||
if engine_settings["enable_gas_effect"]:
|
||||
if particle.is_gas:
|
||||
# Enhanced gas visibility
|
||||
alpha = random.randint(128, 200)
|
||||
@ -153,15 +216,22 @@ class Rendering:
|
||||
# Add subtle movement effect
|
||||
offset_x = random.randint(-1, 1)
|
||||
offset_y = random.randint(-1, 1)
|
||||
rect = (x * particle_size + offset_x, y * particle_size + offset_y,
|
||||
particle_size, particle_size)
|
||||
rect = (
|
||||
x * particle_size + offset_x,
|
||||
y * particle_size + offset_y,
|
||||
particle_size,
|
||||
particle_size,
|
||||
)
|
||||
else:
|
||||
rect = (x * particle_size, y * particle_size,
|
||||
particle_size, particle_size)
|
||||
rect = (
|
||||
x * particle_size,
|
||||
y * particle_size,
|
||||
particle_size,
|
||||
particle_size,
|
||||
)
|
||||
|
||||
pygame.draw.rect(self.particle_surface, color, rect)
|
||||
|
||||
|
||||
self.screen.blit(self.background, (0, 0))
|
||||
self.screen.blit(self.particle_surface, (0, 0))
|
||||
"""#Potentially for future
|
||||
@ -177,8 +247,16 @@ class Rendering:
|
||||
self.screen.blit(self.background, (0, 0))
|
||||
self.screen.blit(self.particle_surface, (0, 0))"""
|
||||
|
||||
def draw_zoom_window(self, particles, particle_size, particle_colors, mouse_pos, zoom_factor=4): # this is the function that draws the zoom window
|
||||
print(f"Drawing zoom window.")
|
||||
def draw_zoom_window(
|
||||
self,
|
||||
particles,
|
||||
particle_size,
|
||||
particle_colors,
|
||||
mouse_pos,
|
||||
zoom_factor=4,
|
||||
):
|
||||
"""Draw zoom window"""
|
||||
print(f"Drawing zoom window.{mouse_pos}")
|
||||
zoom_size = 100 # Size of zoom window
|
||||
zoom_surface = pygame.Surface((zoom_size, zoom_size))
|
||||
zoom_surface.fill((0, 0, 0))
|
||||
@ -195,37 +273,53 @@ class Rendering:
|
||||
if 0 <= x < len(particles) and 0 <= y < len(particles[0]):
|
||||
particle = particles[x][y]
|
||||
if particle:
|
||||
color = particle_colors.get(particle.particle_type, (255, 255, 255))
|
||||
rect = ((x - view_x) * zoom_factor,
|
||||
color = particle_colors.get(
|
||||
particle.particle_type, (255, 255, 255)
|
||||
)
|
||||
rect = (
|
||||
(x - view_x) * zoom_factor,
|
||||
(y - view_y) * zoom_factor,
|
||||
particle_size * zoom_factor,
|
||||
particle_size * zoom_factor)
|
||||
particle_size * zoom_factor,
|
||||
)
|
||||
pygame.draw.rect(zoom_surface, color, rect)
|
||||
print(f"Drawing zoom window at {mouse_pos}{zoom_surface}")
|
||||
return zoom_surface
|
||||
|
||||
|
||||
def _get_particle_info(self, particles, x, y):
|
||||
if 0 <= x < len(particles) and 0 <= y < len(particles[0]):
|
||||
particle = particles[x][y]
|
||||
if particle:
|
||||
attrs = ['temperature', 'liquid', 'is_gas', 'solid', 'mass', 'velocity', 'friction']
|
||||
attrs = [
|
||||
"temperature",
|
||||
"liquid",
|
||||
"is_gas",
|
||||
"solid",
|
||||
"mass",
|
||||
"velocity",
|
||||
"friction",
|
||||
]
|
||||
info = [f"Type: {particle.particle_type}"]
|
||||
info.extend(f"{attr}: {getattr(particle, attr)}" for attr in attrs
|
||||
if hasattr(particle, attr))
|
||||
info.extend(
|
||||
f"{attr}: {getattr(particle, attr)}"
|
||||
for attr in attrs
|
||||
if hasattr(particle, attr)
|
||||
)
|
||||
return " | ".join(info)
|
||||
return "None"
|
||||
|
||||
|
||||
"""
|
||||
def draw_wind_overlay(self, wind_zones):
|
||||
wind_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
|
||||
wind_surface = pygame.Surface(
|
||||
(self.width, self.height), pygame.SRCALPHA
|
||||
)
|
||||
|
||||
for zone in wind_zones:
|
||||
# Draw wind direction arrows
|
||||
x, y = zone['x'], zone['y']
|
||||
radius = zone['radius']
|
||||
strength = zone['strength']
|
||||
direction = zone['direction']
|
||||
x, y = zone["x"], zone["y"]
|
||||
# radius = zone["radius"]
|
||||
strength = zone["strength"]
|
||||
direction = zone["direction"]
|
||||
|
||||
# Draw wind field visualization
|
||||
arrow_color = (0, 150, 255, 100) # Light blue, semi-transparent
|
||||
@ -234,61 +328,106 @@ class Rendering:
|
||||
# Draw main direction arrow
|
||||
end_x = x + direction[0] * arrow_length
|
||||
end_y = y + direction[1] * arrow_length
|
||||
pygame.draw.line(wind_surface, arrow_color, (x, y), (end_x, end_y), 2)
|
||||
pygame.draw.line(
|
||||
wind_surface, arrow_color, (x, y), (end_x, end_y), 2
|
||||
)
|
||||
|
||||
# Draw arrow head
|
||||
pygame.draw.circle(wind_surface, arrow_color, (int(x), int(y)), 5)
|
||||
|
||||
self.screen.blit(wind_surface, (0, 0))
|
||||
self.screen.blit(wind_surface, (0, 0))"""
|
||||
|
||||
"""
|
||||
def draw_pressure_overlay(self, particles, active_particles):
|
||||
pressure_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
|
||||
pressure_surface = pygame.Surface(
|
||||
(self.width, self.height), pygame.SRCALPHA
|
||||
)
|
||||
|
||||
# Create pressure map
|
||||
for x, y in active_particles:
|
||||
particle = particles[x][y]
|
||||
if particle and hasattr(particle, 'pressure'):
|
||||
if particle and hasattr(particle, "pressure"):
|
||||
# Color gradient based on pressure
|
||||
pressure = particle.pressure
|
||||
if pressure > 0:
|
||||
color = (255, 0, 0, int(min(pressure * 50, 255))) # Red for high pressure
|
||||
color = (
|
||||
255,
|
||||
0,
|
||||
0,
|
||||
int(min(pressure * 50, 255)),
|
||||
) # Red for high pressure
|
||||
else:
|
||||
color = (0, 0, 255, int(min(-pressure * 50, 255))) # Blue for low pressure
|
||||
color = (
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
int(min(-pressure * 50, 255)),
|
||||
) # Blue for low pressure
|
||||
|
||||
pygame.draw.rect(pressure_surface, color,
|
||||
(x * self.particle_size, y * self.particle_size,
|
||||
self.particle_size, self.particle_size))
|
||||
|
||||
self.screen.blit(pressure_surface, (0, 0))
|
||||
pygame.draw.rect(
|
||||
pressure_surface,
|
||||
color,
|
||||
(
|
||||
x * self.particle_size,
|
||||
y * self.particle_size,
|
||||
self.particle_size,
|
||||
self.particle_size,
|
||||
),
|
||||
)
|
||||
|
||||
self.screen.blit(pressure_surface, (0, 0))"""
|
||||
|
||||
"""
|
||||
def draw_temperature_overlay(self, particles, active_particles):
|
||||
temperature_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
|
||||
temperature_surface = pygame.Surface(
|
||||
(self.width, self.height), pygame.SRCALPHA
|
||||
)
|
||||
|
||||
# Create temperature map
|
||||
for x, y in active_particles:
|
||||
particle = particles[x][y]
|
||||
if particle and hasattr(particle, 'temperature'):
|
||||
if particle and hasattr(particle, "temperature"):
|
||||
# Color gradient based on temperature
|
||||
temperature = particle.temperature
|
||||
if temperature > 0:
|
||||
color = (255, 0, 0, int(min(temperature * 50, 255))) # Red for high temperature
|
||||
color = (
|
||||
255,
|
||||
0,
|
||||
0,
|
||||
int(min(temperature * 50, 255)),
|
||||
) # Red for high temperature
|
||||
else:
|
||||
color = (0, 0, 255, int(min(-temperature * 50, 255))) # Blue for low temperature
|
||||
color = (
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
int(min(-temperature * 50, 255)),
|
||||
) # Blue for low temperature
|
||||
|
||||
pygame.draw.rect(temperature_surface, color,
|
||||
(x * self.particle_size, y * self.particle_size,
|
||||
self.particle_size, self.particle_size))
|
||||
|
||||
self.screen.blit(temperature_surface, (0, 0))
|
||||
pygame.draw.rect(
|
||||
temperature_surface,
|
||||
color,
|
||||
(
|
||||
x * self.particle_size,
|
||||
y * self.particle_size,
|
||||
self.particle_size,
|
||||
self.particle_size,
|
||||
),
|
||||
)
|
||||
|
||||
self.screen.blit(temperature_surface, (0, 0))"""
|
||||
|
||||
def draw_debug_overlay(self, fps, sim):
|
||||
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']:
|
||||
"""Debugger moving to debugger_system.py"""
|
||||
|
||||
if (
|
||||
not engine_settings["enable_fps"]
|
||||
and not engine_settings["enable_debug"]
|
||||
):
|
||||
return
|
||||
|
||||
# Create static debug surface if not exists
|
||||
if not hasattr(self, 'debug_surface'):
|
||||
if not hasattr(self, "debug_surface"):
|
||||
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||
|
||||
# Only update when values change significantly
|
||||
@ -300,26 +439,32 @@ class Rendering:
|
||||
int(sim.track_tps()),
|
||||
mouse_x // 10, # Reduced update frequency
|
||||
mouse_y // 10,
|
||||
sim.particle_count
|
||||
sim.particle_count,
|
||||
)
|
||||
|
||||
if not hasattr(self, '_last_debug_info') or current_info != self._last_debug_info:
|
||||
if (
|
||||
not hasattr(self, "_last_debug_info")
|
||||
or current_info != self._last_debug_info
|
||||
):
|
||||
self._last_debug_info = current_info
|
||||
self.debug_surface.fill((0, 0, 0, 0))
|
||||
|
||||
font = self.cached_fonts['debug']
|
||||
font = self.cached_fonts["debug"]
|
||||
y_offset = 10
|
||||
|
||||
if engine_settings['enable_fps']:
|
||||
fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255))
|
||||
if engine_settings["enable_fps"]:
|
||||
fps_surf = font.render(
|
||||
f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}",
|
||||
True,
|
||||
(255, 255, 255),
|
||||
)
|
||||
self.debug_surface.blit(fps_surf, (10, y_offset))
|
||||
y_offset += 25
|
||||
|
||||
if engine_settings['enable_debug']:
|
||||
if engine_settings["enable_debug"]:
|
||||
debug_lines = [
|
||||
f"Mouse: ({mouse_x}, {mouse_y})",
|
||||
f"Grid: ({grid_x}, {grid_y})",
|
||||
f"Particles: {sim.particle_count}"
|
||||
f"Particles: {sim.particle_count}",
|
||||
]
|
||||
|
||||
for line in debug_lines:
|
||||
@ -330,8 +475,8 @@ class Rendering:
|
||||
# Single blit of cached surface
|
||||
self.screen.blit(self.debug_surface, (0, 0))
|
||||
|
||||
|
||||
def draw_buttons(self): # this is the function that draws the buttons
|
||||
def draw_buttons(self):
|
||||
"""Draws the buttons on the screen."""
|
||||
self.buttons = {}
|
||||
|
||||
# Draw category buttons on right
|
||||
@ -356,13 +501,17 @@ class Rendering:
|
||||
|
||||
# Use cached button surface
|
||||
if particle_type in self.button_surfaces:
|
||||
self.screen.blit(self.button_surfaces[particle_type], button_rect)
|
||||
self.screen.blit(
|
||||
self.button_surfaces[particle_type], button_rect
|
||||
)
|
||||
else:
|
||||
# Create and cache button surface if not exists
|
||||
button_surface = pygame.Surface((80, 25))
|
||||
color = self.particle_properties[particle_type].get('color', (255, 255, 255))
|
||||
color = self.particle_properties[particle_type].get(
|
||||
"color", (255, 255, 255)
|
||||
)
|
||||
button_surface.fill(color)
|
||||
font = self.cached_fonts.get('button')
|
||||
font = self.cached_fonts.get("button")
|
||||
label = font.render(particle_type, True, (0, 0, 0))
|
||||
button_surface.blit(label, (5, 5))
|
||||
self.button_surfaces[particle_type] = button_surface
|
||||
@ -370,81 +519,99 @@ class Rendering:
|
||||
|
||||
y_offset += 30 # Stack buttons vertically
|
||||
|
||||
final_y_offset = y_offset + 10
|
||||
|
||||
# Draw clear screen button
|
||||
if 'clear' not in self.button_surfaces:
|
||||
if "clear" not in self.button_surfaces:
|
||||
clear_surface = pygame.Surface((80, 25))
|
||||
clear_surface.fill((255, 0, 0))
|
||||
font = self.cached_fonts.get('button')
|
||||
font = self.cached_fonts.get("button")
|
||||
label = font.render("Clear", True, (255, 255, 255))
|
||||
clear_surface.blit(label, (5, 5))
|
||||
self.button_surfaces['clear'] = clear_surface
|
||||
self.button_surfaces["clear"] = clear_surface
|
||||
|
||||
self.clear_screen_button = pygame.Rect(x_offset, y_offset + 10, 80, 25)
|
||||
self.screen.blit(self.button_surfaces['clear'], self.clear_screen_button)
|
||||
self.screen.blit(
|
||||
self.button_surfaces["clear"], self.clear_screen_button
|
||||
)
|
||||
|
||||
# Draw Settings menu button
|
||||
if 'settings' not in self.button_surfaces:
|
||||
if "settings" not in self.button_surfaces:
|
||||
settings_surface = pygame.Surface((80, 25))
|
||||
settings_surface.fill((255, 255, 255))
|
||||
font = self.cached_fonts.get('button')
|
||||
font = self.cached_fonts.get("button")
|
||||
label = font.render("Settings", True, (0, 0, 0))
|
||||
settings_surface.blit(label, (5, 5))
|
||||
self.button_surfaces['settings'] = settings_surface
|
||||
self.button_surfaces["settings"] = settings_surface
|
||||
|
||||
self.settings_button = pygame.Rect(x_offset, y_offset + 50, 80, 25)
|
||||
self.screen.blit(self.button_surfaces['settings'], self.settings_button)
|
||||
self.screen.blit(
|
||||
self.button_surfaces["settings"], self.settings_button
|
||||
)
|
||||
|
||||
def render_brush_cursor(self, x, y, radius):
|
||||
if engine_settings ['enable_cursor']:
|
||||
# Draw outline circle
|
||||
"""Draw outline circle"""
|
||||
if engine_settings["enable_cursor"]:
|
||||
pygame.draw.circle(self.screen, (255, 255, 255), (x, y), radius, 1)
|
||||
# Draw slightly transparent fill
|
||||
cursor_surface = pygame.Surface((radius*2, radius*2), pygame.SRCALPHA)
|
||||
pygame.draw.circle(cursor_surface, (255, 255, 255, 55), (radius, radius), radius)
|
||||
cursor_surface = pygame.Surface(
|
||||
(radius * 2, radius * 2), pygame.SRCALPHA
|
||||
)
|
||||
pygame.draw.circle(
|
||||
cursor_surface, (255, 255, 255, 55), (radius, radius), radius
|
||||
)
|
||||
self.screen.blit(cursor_surface, (x - radius, y - radius))
|
||||
|
||||
|
||||
def draw_brush_size_slider(self, brush_size): # this is the function that draws the brush size slider
|
||||
# Draw the slider for brush size
|
||||
def draw_brush_size_slider(self, brush_size):
|
||||
"""Draw the slider for brush size"""
|
||||
pygame.draw.rect(self.screen, (255, 255, 255), (500, 10, 100, 20))
|
||||
pygame.draw.rect(self.screen, (0, 0, 0), (500, 10, 100, 20), 2)
|
||||
pygame.draw.rect(self.screen, (255, 0, 0), (500 + brush_size, 10, 100 - brush_size * 2, 20))
|
||||
label = self.cached_fonts.get('brush_size').render(f"Brush Size: {brush_size}", True, (255, 255, 255))
|
||||
pygame.draw.rect(
|
||||
self.screen,
|
||||
(255, 0, 0),
|
||||
(500 + brush_size, 10, 100 - brush_size * 2, 20),
|
||||
)
|
||||
label = self.cached_fonts.get("brush_size").render(
|
||||
f"Brush Size: {brush_size}", True, (255, 255, 255)
|
||||
)
|
||||
self.screen.blit(label, (500, 10))
|
||||
|
||||
def draw_settings_menu(self):
|
||||
"""Draw the settings menu"""
|
||||
settings_surface = pygame.Surface((300, 400))
|
||||
settings_surface.fill((50, 50, 50))
|
||||
|
||||
y_offset = 10
|
||||
font = self.cached_fonts.get('settings')
|
||||
font = self.cached_fonts.get("settings")
|
||||
|
||||
for setting, value in engine_settings.items():
|
||||
# Create toggle button
|
||||
button_rect = pygame.Rect(10, y_offset, 20, 20)
|
||||
pygame.draw.rect(settings_surface, (0, 255, 0) if value else (255, 0, 0), button_rect)
|
||||
pygame.draw.rect(
|
||||
settings_surface,
|
||||
(0, 255, 0) if value else (255, 0, 0),
|
||||
button_rect,
|
||||
)
|
||||
|
||||
# Draw setting name
|
||||
label = font.render(setting.replace('_', ' ').title(), True, (255, 255, 255))
|
||||
label = font.render(
|
||||
setting.replace("_", " ").title(), True, (255, 255, 255)
|
||||
)
|
||||
settings_surface.blit(label, (40, y_offset))
|
||||
|
||||
y_offset += 30
|
||||
|
||||
return settings_surface
|
||||
|
||||
|
||||
def clear_screen(self, sim): # this is the function that clears the screen
|
||||
# Store current particle type
|
||||
def clear_screen(self, sim):
|
||||
"""Store current particle type"""
|
||||
current_type = sim.current_particle_type
|
||||
self.particle_count = 0
|
||||
# Reset simulation grid while preserving particle type
|
||||
sim.particles = [[None for _ in range(sim.height)] for _ in range(sim.width)]
|
||||
sim.particles = [
|
||||
[None for _ in range(sim.height)] for _ in range(sim.width)
|
||||
]
|
||||
sim.active_particles.clear()
|
||||
sim.current_particle_type = current_type
|
||||
sim.reset_particle_count()
|
||||
# Clear display surfaces
|
||||
self.background.fill((0, 0, 0))
|
||||
self.particle_surface.fill((0, 0, 0, 0))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user