Compare commits
No commits in common. "69296cf4b7c61a33e1cfdf23649d551d9b139365" and "b65474ed2ac6f0478b366bb6143e2709ec4e62fe" have entirely different histories.
69296cf4b7
...
b65474ed2a
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,4 +3,3 @@ uv.lock
|
|||||||
.idea/
|
.idea/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
simulation_output/
|
|
||||||
@ -42,22 +42,6 @@ HUD_MARGIN = 10
|
|||||||
LINE_HEIGHT = 20
|
LINE_HEIGHT = 20
|
||||||
SELECTION_THRESHOLD = 3 # pixels
|
SELECTION_THRESHOLD = 3 # pixels
|
||||||
|
|
||||||
# Unified Panel Styling System (based on tree widget design)
|
|
||||||
PANEL_BACKGROUND_COLOR = (30, 30, 40) # Dark blue-gray background
|
|
||||||
PANEL_SELECTED_COLOR = (50, 100, 150) # Blue highlight for selected elements
|
|
||||||
PANEL_HOVER_COLOR = (60, 60, 80) # Dark blue highlight for interactive elements
|
|
||||||
PANEL_TEXT_COLOR = (200, 200, 200) # Light gray text
|
|
||||||
PANEL_ICON_COLOR = (150, 150, 150) # Medium gray icons
|
|
||||||
PANEL_BORDER_COLOR = (220, 220, 220) # Light gray borders/dividers
|
|
||||||
|
|
||||||
# Panel spacing and dimensions
|
|
||||||
PANEL_DIVIDER_WIDTH = 0 # No divider lines between panels
|
|
||||||
PANEL_BORDER_WIDTH = 2 # Border width for emphasis elements
|
|
||||||
PANEL_INTERNAL_PADDING = 8 # Standard padding inside panels
|
|
||||||
PANEL_TIGHT_SPACING = 4 # Tight spacing between components
|
|
||||||
PANEL_NODE_HEIGHT = 20 # Height for list/grid items
|
|
||||||
PANEL_INDENTATION = 20 # Indentation per hierarchy level
|
|
||||||
|
|
||||||
# Simulation settings
|
# Simulation settings
|
||||||
FOOD_SPAWNING = True
|
FOOD_SPAWNING = True
|
||||||
FOOD_OBJECTS_COUNT = 500
|
FOOD_OBJECTS_COUNT = 500
|
||||||
|
|||||||
@ -24,9 +24,9 @@ class OutputConfig:
|
|||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
directory: str = "simulation_output"
|
directory: str = "simulation_output"
|
||||||
formats: List[str] = field(default_factory=lambda: ['json'])
|
formats: List[str] = field(default_factory=lambda: ['json'])
|
||||||
collect_metrics: bool = False
|
collect_metrics: bool = True
|
||||||
collect_entities: bool = False
|
collect_entities: bool = True
|
||||||
collect_evolution: bool = False
|
collect_evolution: bool = True
|
||||||
metrics_interval: int = 100
|
metrics_interval: int = 100
|
||||||
entities_interval: int = 1000
|
entities_interval: int = 1000
|
||||||
evolution_interval: int = 1000
|
evolution_interval: int = 1000
|
||||||
|
|||||||
@ -127,16 +127,14 @@ class InputHandler:
|
|||||||
# Zoom in viewport
|
# Zoom in viewport
|
||||||
self.camera.handle_zoom(event.y)
|
self.camera.handle_zoom(event.y)
|
||||||
elif inspector_rect and inspector_rect.collidepoint(mouse_x, mouse_y) and self.hud.tree_widget:
|
elif inspector_rect and inspector_rect.collidepoint(mouse_x, mouse_y) and self.hud.tree_widget:
|
||||||
# Scroll tree widget in inspector
|
# Scroll tree widget in inspector - convert to local coordinates if needed
|
||||||
if not viewport_rect.collidepoint(mouse_x, mouse_y):
|
if not hasattr(event, 'pos'):
|
||||||
# Convert to local coordinates if needed
|
event.pos = (mouse_x - inspector_rect.x, mouse_y - inspector_rect.y)
|
||||||
if not hasattr(event, 'pos'):
|
else:
|
||||||
event.pos = (mouse_x - inspector_rect.x, mouse_y - inspector_rect.y)
|
local_x = mouse_x - inspector_rect.x
|
||||||
else:
|
local_y = mouse_y - inspector_rect.y
|
||||||
local_x = mouse_x - inspector_rect.x
|
event.pos = (local_x, local_y)
|
||||||
local_y = mouse_y - inspector_rect.y
|
self.hud.tree_widget.handle_event(event)
|
||||||
event.pos = (local_x, local_y)
|
|
||||||
self.hud.tree_widget.handle_event(event)
|
|
||||||
else:
|
else:
|
||||||
# Fallback: always zoom if no HUD reference
|
# Fallback: always zoom if no HUD reference
|
||||||
self.camera.handle_zoom(event.y)
|
self.camera.handle_zoom(event.y)
|
||||||
|
|||||||
@ -269,11 +269,9 @@ class SimulationEngine:
|
|||||||
# Update tree widget
|
# Update tree widget
|
||||||
self.hud.update_tree_widget(deltatime)
|
self.hud.update_tree_widget(deltatime)
|
||||||
|
|
||||||
# Draw panel backgrounds first (before pygame_gui UI)
|
|
||||||
self.hud.render_panel_backgrounds(self.screen)
|
|
||||||
|
|
||||||
# Draw UI elements
|
# Draw UI elements
|
||||||
self.hud.manager.draw_ui(self.screen)
|
self.hud.manager.draw_ui(self.screen)
|
||||||
|
self.hud.draw_splitters(self.screen)
|
||||||
|
|
||||||
# Render tree widget
|
# Render tree widget
|
||||||
self.hud.render_tree_widget(self.screen)
|
self.hud.render_tree_widget(self.screen)
|
||||||
|
|||||||
@ -12,11 +12,6 @@ from output import MetricsCollector, EntityCollector, EvolutionCollector
|
|||||||
from output.formatters.json_formatter import JSONFormatter
|
from output.formatters.json_formatter import JSONFormatter
|
||||||
from output.formatters.csv_formatter import CSVFormatter
|
from output.formatters.csv_formatter import CSVFormatter
|
||||||
from output.writers.file_writer import FileWriter
|
from output.writers.file_writer import FileWriter
|
||||||
try:
|
|
||||||
from tqdm import tqdm
|
|
||||||
TQDM_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
TQDM_AVAILABLE = False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -61,12 +56,6 @@ class HeadlessSimulationEngine:
|
|||||||
'evolution': []
|
'evolution': []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Progress tracking
|
|
||||||
self.files_written = 0
|
|
||||||
self.last_progress_update = 0
|
|
||||||
self.progress_update_interval = 1.0 # Update progress every second
|
|
||||||
self.progress_bar = None
|
|
||||||
|
|
||||||
# Setup signal handlers for graceful shutdown
|
# Setup signal handlers for graceful shutdown
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
signal.signal(signal.SIGINT, self._signal_handler)
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||||
@ -99,120 +88,6 @@ class HeadlessSimulationEngine:
|
|||||||
|
|
||||||
return collectors
|
return collectors
|
||||||
|
|
||||||
def _init_progress_bar(self):
|
|
||||||
"""Initialize progress bar for simulation."""
|
|
||||||
if not TQDM_AVAILABLE:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine progress total based on configuration
|
|
||||||
if self.config.max_ticks:
|
|
||||||
total = self.config.max_ticks
|
|
||||||
unit = 'ticks'
|
|
||||||
elif self.config.max_duration:
|
|
||||||
total = int(self.config.max_duration)
|
|
||||||
unit = 'sec'
|
|
||||||
else:
|
|
||||||
# No clear total - create indeterminate progress bar
|
|
||||||
total = None
|
|
||||||
unit = 'ticks'
|
|
||||||
|
|
||||||
if total:
|
|
||||||
self.progress_bar = tqdm(
|
|
||||||
total=total,
|
|
||||||
unit=unit,
|
|
||||||
desc="Simulation",
|
|
||||||
leave=True, # Keep the bar when done
|
|
||||||
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.progress_bar = tqdm(
|
|
||||||
unit='ticks',
|
|
||||||
desc="Simulation",
|
|
||||||
leave=True,
|
|
||||||
bar_format='{l_bar}{bar}| {n_fmt} [{elapsed}, {rate_fmt}]'
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_progress_bar(self):
|
|
||||||
"""Update progress bar with current status."""
|
|
||||||
current_time = time.time()
|
|
||||||
if current_time - self.last_progress_update < self.progress_update_interval:
|
|
||||||
return
|
|
||||||
|
|
||||||
current_tick = self.simulation_core.state.total_ticks
|
|
||||||
tps = self.simulation_core.state.actual_tps
|
|
||||||
elapsed = current_time - self.start_time
|
|
||||||
|
|
||||||
if TQDM_AVAILABLE and self.progress_bar:
|
|
||||||
# Use tqdm progress bar
|
|
||||||
if self.config.max_ticks:
|
|
||||||
# Update based on tick progress
|
|
||||||
progress = min(current_tick, self.config.max_ticks)
|
|
||||||
self.progress_bar.n = progress
|
|
||||||
self.progress_bar.set_postfix({
|
|
||||||
'TPS': f'{tps:.1f}',
|
|
||||||
'Files': self.files_written
|
|
||||||
})
|
|
||||||
elif self.config.max_duration:
|
|
||||||
# Update based on elapsed time
|
|
||||||
progress = min(elapsed, self.config.max_duration)
|
|
||||||
self.progress_bar.n = int(progress)
|
|
||||||
self.progress_bar.set_postfix({
|
|
||||||
'TPS': f'{tps:.1f}',
|
|
||||||
'Files': self.files_written,
|
|
||||||
'Tick': current_tick
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
# Indeterminate progress
|
|
||||||
self.progress_bar.n = current_tick
|
|
||||||
self.progress_bar.set_postfix({
|
|
||||||
'TPS': f'{tps:.1f}',
|
|
||||||
'Files': self.files_written
|
|
||||||
})
|
|
||||||
|
|
||||||
self.progress_bar.refresh()
|
|
||||||
else:
|
|
||||||
# Simple text-based progress
|
|
||||||
eta_text = ""
|
|
||||||
if self.config.max_ticks and current_tick > 0:
|
|
||||||
tick_rate = current_tick / elapsed if elapsed > 0 else 0
|
|
||||||
remaining_ticks = self.config.max_ticks - current_tick
|
|
||||||
eta_seconds = remaining_ticks / tick_rate if tick_rate > 0 else 0
|
|
||||||
eta_minutes, eta_seconds = divmod(eta_seconds, 60)
|
|
||||||
eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s"
|
|
||||||
elif self.config.max_duration:
|
|
||||||
remaining_seconds = self.config.max_duration - elapsed
|
|
||||||
eta_minutes, eta_seconds = divmod(remaining_seconds, 60)
|
|
||||||
eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s"
|
|
||||||
|
|
||||||
# Calculate progress percentage if we have a limit
|
|
||||||
progress_pct = ""
|
|
||||||
if self.config.max_ticks:
|
|
||||||
pct = (current_tick / self.config.max_ticks) * 100
|
|
||||||
progress_pct = f"{pct:.1f}%"
|
|
||||||
elif self.config.max_duration:
|
|
||||||
pct = (elapsed / self.config.max_duration) * 100
|
|
||||||
progress_pct = f"{pct:.1f}%"
|
|
||||||
|
|
||||||
progress_line = f"[{current_time - self.start_time:.1f}s] "
|
|
||||||
if progress_pct:
|
|
||||||
progress_line += f"Progress: {progress_pct} "
|
|
||||||
progress_line += f"Tick: {current_tick} TPS: {tps:.1f} Files: {self.files_written}"
|
|
||||||
if eta_text:
|
|
||||||
progress_line += f" {eta_text}"
|
|
||||||
|
|
||||||
# Overwrite the previous line (using carriage return)
|
|
||||||
print(f"\r{progress_line}", end="", flush=True)
|
|
||||||
|
|
||||||
self.last_progress_update = current_time
|
|
||||||
|
|
||||||
def _close_progress_bar(self):
|
|
||||||
"""Close the progress bar."""
|
|
||||||
if not TQDM_AVAILABLE and self.running:
|
|
||||||
# Print a newline to clear the text progress line
|
|
||||||
print()
|
|
||||||
elif TQDM_AVAILABLE and self.progress_bar:
|
|
||||||
self.progress_bar.close()
|
|
||||||
|
|
||||||
def run(self) -> Dict[str, Any]:
|
def run(self) -> Dict[str, Any]:
|
||||||
"""Run the headless simulation."""
|
"""Run the headless simulation."""
|
||||||
# Determine if we should run at max speed
|
# Determine if we should run at max speed
|
||||||
@ -232,12 +107,10 @@ class HeadlessSimulationEngine:
|
|||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.simulation_core.start()
|
self.simulation_core.start()
|
||||||
|
|
||||||
# Initialize progress bar
|
|
||||||
self._init_progress_bar()
|
|
||||||
|
|
||||||
# Enable sprint mode for maximum speed if not real-time mode
|
# Enable sprint mode for maximum speed if not real-time mode
|
||||||
if max_speed_mode:
|
if max_speed_mode:
|
||||||
self.simulation_core.timing.set_sprint_mode(True)
|
self.simulation_core.timing.set_sprint_mode(True)
|
||||||
|
print("Running at maximum speed (sprint mode enabled)")
|
||||||
|
|
||||||
last_batch_time = time.time()
|
last_batch_time = time.time()
|
||||||
batch_interval = 5.0
|
batch_interval = 5.0
|
||||||
@ -264,9 +137,6 @@ class HeadlessSimulationEngine:
|
|||||||
self._write_batch_data()
|
self._write_batch_data()
|
||||||
last_batch_time = time.time()
|
last_batch_time = time.time()
|
||||||
|
|
||||||
# Update progress bar
|
|
||||||
self._update_progress_bar()
|
|
||||||
|
|
||||||
# Real-time delay if needed
|
# Real-time delay if needed
|
||||||
if self.config.real_time:
|
if self.config.real_time:
|
||||||
time.sleep(0.016) # ~60 FPS
|
time.sleep(0.016) # ~60 FPS
|
||||||
@ -336,17 +206,14 @@ class HeadlessSimulationEngine:
|
|||||||
}
|
}
|
||||||
formatted_data = formatter.format(combined_data)
|
formatted_data = formatter.format(combined_data)
|
||||||
self.file_writer.write(formatted_data, filename)
|
self.file_writer.write(formatted_data, filename)
|
||||||
self.files_written += 1
|
|
||||||
|
|
||||||
# Clear written data
|
# Clear written data
|
||||||
data_list.clear()
|
data_list.clear()
|
||||||
|
|
||||||
|
print(f"Wrote batch data at tick {self.simulation_core.state.total_ticks}")
|
||||||
|
|
||||||
def _finalize(self):
|
def _finalize(self):
|
||||||
"""Finalize simulation and write remaining data."""
|
"""Finalize simulation and write remaining data."""
|
||||||
|
|
||||||
# Close progress bar
|
|
||||||
self._close_progress_bar()
|
|
||||||
|
|
||||||
print("Finalizing simulation...")
|
print("Finalizing simulation...")
|
||||||
|
|
||||||
# Write any remaining data
|
# Write any remaining data
|
||||||
@ -357,14 +224,12 @@ class HeadlessSimulationEngine:
|
|||||||
if 'json' in self.formatters:
|
if 'json' in self.formatters:
|
||||||
summary_data = self.formatters['json'].format(summary)
|
summary_data = self.formatters['json'].format(summary)
|
||||||
self.file_writer.write(summary_data, "simulation_summary.json")
|
self.file_writer.write(summary_data, "simulation_summary.json")
|
||||||
self.files_written += 1
|
|
||||||
|
|
||||||
# Stop simulation
|
# Stop simulation
|
||||||
self.simulation_core.stop()
|
self.simulation_core.stop()
|
||||||
self.file_writer.close()
|
self.file_writer.close()
|
||||||
|
|
||||||
print("Simulation completed")
|
print("Simulation completed")
|
||||||
print(f"Total files written: {self.files_written}")
|
|
||||||
|
|
||||||
def _get_summary(self) -> Dict[str, Any]:
|
def _get_summary(self) -> Dict[str, Any]:
|
||||||
"""Get simulation summary."""
|
"""Get simulation summary."""
|
||||||
|
|||||||
@ -51,26 +51,6 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Run in real-time mode (instead of as fast as possible)"
|
help="Run in real-time mode (instead of as fast as possible)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--collect-metrics",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable metrics data collection"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--collect-entities",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable entity data collection"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--collect-evolution",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable evolution data collection"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--collect-all",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable all data collection (metrics, entities, evolution)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--collect-every-tick",
|
"--collect-every-tick",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -110,35 +90,11 @@ def main():
|
|||||||
headless_config.output.directory = args.output_dir
|
headless_config.output.directory = args.output_dir
|
||||||
if args.real_time:
|
if args.real_time:
|
||||||
headless_config.output.real_time = True
|
headless_config.output.real_time = True
|
||||||
# Handle data collection arguments - only enable if explicitly requested
|
|
||||||
# Start with all disabled (default)
|
|
||||||
headless_config.output.collect_metrics = False
|
|
||||||
headless_config.output.collect_entities = False
|
|
||||||
headless_config.output.collect_evolution = False
|
|
||||||
|
|
||||||
if args.collect_all:
|
|
||||||
# Enable all collection
|
|
||||||
headless_config.output.collect_metrics = True
|
|
||||||
headless_config.output.collect_entities = True
|
|
||||||
headless_config.output.collect_evolution = True
|
|
||||||
else:
|
|
||||||
# Enable specific collection types
|
|
||||||
if args.collect_metrics:
|
|
||||||
headless_config.output.collect_metrics = True
|
|
||||||
if args.collect_entities:
|
|
||||||
headless_config.output.collect_entities = True
|
|
||||||
if args.collect_evolution:
|
|
||||||
headless_config.output.collect_evolution = True
|
|
||||||
|
|
||||||
if args.collect_every_tick:
|
if args.collect_every_tick:
|
||||||
# Set all collection intervals to 1 for every-tick collection
|
# Set all collection intervals to 1 for every-tick collection
|
||||||
headless_config.output.metrics_interval = 1
|
headless_config.output.metrics_interval = 1
|
||||||
headless_config.output.entities_interval = 1
|
headless_config.output.entities_interval = 1
|
||||||
headless_config.output.evolution_interval = 1
|
headless_config.output.evolution_interval = 1
|
||||||
# Also enable all collection if using --collect-every-tick (backward compatibility)
|
|
||||||
headless_config.output.collect_metrics = True
|
|
||||||
headless_config.output.collect_entities = True
|
|
||||||
headless_config.output.collect_evolution = True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading configuration: {e}")
|
print(f"Error loading configuration: {e}")
|
||||||
|
|||||||
@ -12,7 +12,6 @@ dependencies = [
|
|||||||
"pygame>=2.6.1",
|
"pygame>=2.6.1",
|
||||||
"pygame-gui>=0.6.14",
|
"pygame-gui>=0.6.14",
|
||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"tqdm>=4.67.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
120
ui/hud.py
120
ui/hud.py
@ -10,26 +10,9 @@ from pygame_gui.elements import UIPanel, UIButton, UITextEntryLine, UILabel
|
|||||||
from ui.tree_widget import TreeWidget
|
from ui.tree_widget import TreeWidget
|
||||||
import math
|
import math
|
||||||
|
|
||||||
# Custom HUD colors (preserving existing functionality)
|
|
||||||
DARK_GRAY = (40, 40, 40)
|
DARK_GRAY = (40, 40, 40)
|
||||||
DARKER_GRAY = (25, 25, 25)
|
DARKER_GRAY = (25, 25, 25)
|
||||||
|
|
||||||
|
|
||||||
def create_panel_style(manager: pygame_gui.UIManager) -> dict:
|
|
||||||
"""Create unified styling dictionary for panels."""
|
|
||||||
return {
|
|
||||||
'panel_background': PANEL_BACKGROUND_COLOR,
|
|
||||||
'border_color': PANEL_BORDER_COLOR,
|
|
||||||
'text_color': PANEL_TEXT_COLOR,
|
|
||||||
'internal_padding': PANEL_INTERNAL_PADDING,
|
|
||||||
'border_width': PANEL_BORDER_WIDTH
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def render_panel_divider(surface: pygame.Surface, rect: pygame.Rect):
|
|
||||||
"""Render a thin divider line between panels."""
|
|
||||||
pygame.draw.rect(surface, PANEL_BORDER_COLOR, rect, PANEL_DIVIDER_WIDTH)
|
|
||||||
|
|
||||||
# Panel visibility constants
|
# Panel visibility constants
|
||||||
SHOW_CONTROL_BAR = True
|
SHOW_CONTROL_BAR = True
|
||||||
SHOW_INSPECTOR_PANEL = True
|
SHOW_INSPECTOR_PANEL = True
|
||||||
@ -67,16 +50,13 @@ class HUD:
|
|||||||
self.world = None # Will be set when world is available
|
self.world = None # Will be set when world is available
|
||||||
self._last_tree_selection = None # Track last selection to avoid unnecessary updates
|
self._last_tree_selection = None # Track last selection to avoid unnecessary updates
|
||||||
|
|
||||||
# Initialize unified panel styling
|
|
||||||
self.panel_style = create_panel_style(self.manager)
|
|
||||||
|
|
||||||
self._create_panels()
|
self._create_panels()
|
||||||
self._create_simulation_controls()
|
self._create_simulation_controls()
|
||||||
|
|
||||||
def _create_panels(self):
|
def _create_panels(self):
|
||||||
self.panels = []
|
self.panels = []
|
||||||
|
|
||||||
# Top control bar - full width, positioned at top
|
# Top control bar
|
||||||
if SHOW_CONTROL_BAR:
|
if SHOW_CONTROL_BAR:
|
||||||
self.control_bar = UIPanel(
|
self.control_bar = UIPanel(
|
||||||
relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height),
|
relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height),
|
||||||
@ -87,35 +67,34 @@ class HUD:
|
|||||||
else:
|
else:
|
||||||
self.control_bar = None
|
self.control_bar = None
|
||||||
|
|
||||||
# Calculate vertical position for side panels (edge-to-edge with control bar)
|
# Left inspector with tree widget
|
||||||
side_panel_top = self.control_bar_height if SHOW_CONTROL_BAR else 0
|
|
||||||
side_panel_height = self.screen_height - side_panel_top
|
|
||||||
|
|
||||||
# Left inspector panel - edge-to-edge with control bar, no gap
|
|
||||||
if SHOW_INSPECTOR_PANEL:
|
if SHOW_INSPECTOR_PANEL:
|
||||||
|
# Create a container panel for the inspector
|
||||||
self.inspector_panel = UIPanel(
|
self.inspector_panel = UIPanel(
|
||||||
relative_rect=pygame.Rect(
|
relative_rect=pygame.Rect(
|
||||||
0, side_panel_top, # Start right at control bar edge
|
0, self.control_bar_height if SHOW_CONTROL_BAR else 0,
|
||||||
self.inspector_width,
|
self.inspector_width,
|
||||||
side_panel_height # Extend to bottom edge
|
self.screen_height - (self.control_bar_height if SHOW_CONTROL_BAR else 0)
|
||||||
),
|
),
|
||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
object_id="#inspector_panel",
|
object_id="#inspector_panel",
|
||||||
)
|
)
|
||||||
self.panels.append(self.inspector_panel)
|
self.panels.append(self.inspector_panel)
|
||||||
|
|
||||||
|
# Tree widget will be created when world is available
|
||||||
self.tree_widget = None
|
self.tree_widget = None
|
||||||
else:
|
else:
|
||||||
self.inspector_panel = None
|
self.inspector_panel = None
|
||||||
self.tree_widget = None
|
self.tree_widget = None
|
||||||
|
|
||||||
# Right properties panel - edge-to-edge with control bar, no gap
|
# Right properties
|
||||||
if SHOW_PROPERTIES_PANEL:
|
if SHOW_PROPERTIES_PANEL:
|
||||||
self.properties_panel = UIPanel(
|
self.properties_panel = UIPanel(
|
||||||
relative_rect=pygame.Rect(
|
relative_rect=pygame.Rect(
|
||||||
self.screen_width - self.properties_width, # Precisely at right edge
|
self.screen_width - self.properties_width,
|
||||||
side_panel_top, # Align with control bar
|
self.control_bar_height if SHOW_CONTROL_BAR else 0,
|
||||||
self.properties_width,
|
self.properties_width,
|
||||||
side_panel_height # Extend to bottom edge
|
self.screen_height - (self.control_bar_height if SHOW_CONTROL_BAR else 0)
|
||||||
),
|
),
|
||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
object_id="#properties_panel",
|
object_id="#properties_panel",
|
||||||
@ -124,16 +103,13 @@ class HUD:
|
|||||||
else:
|
else:
|
||||||
self.properties_panel = None
|
self.properties_panel = None
|
||||||
|
|
||||||
# Bottom console panel - edge-to-edge with side panels, no gap
|
# Bottom console
|
||||||
if SHOW_CONSOLE_PANEL:
|
if SHOW_CONSOLE_PANEL:
|
||||||
console_left = self.inspector_width if SHOW_INSPECTOR_PANEL else 0
|
|
||||||
console_width = self.screen_width - console_left - (self.properties_width if SHOW_PROPERTIES_PANEL else 0)
|
|
||||||
|
|
||||||
self.console_panel = UIPanel(
|
self.console_panel = UIPanel(
|
||||||
relative_rect=pygame.Rect(
|
relative_rect=pygame.Rect(
|
||||||
console_left, # Start right at inspector edge
|
self.inspector_width if SHOW_INSPECTOR_PANEL else 0,
|
||||||
self.screen_height - self.console_height, # Exactly at bottom edge
|
self.screen_height - self.console_height,
|
||||||
console_width, # Fill space between side panels
|
self.screen_width - (self.inspector_width if SHOW_INSPECTOR_PANEL else 0) - (self.properties_width if SHOW_PROPERTIES_PANEL else 0),
|
||||||
self.console_height
|
self.console_height
|
||||||
),
|
),
|
||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
@ -163,55 +139,17 @@ class HUD:
|
|||||||
self.tree_widget.select_entities(selected_objects)
|
self.tree_widget.select_entities(selected_objects)
|
||||||
self._last_tree_selection = current_selection
|
self._last_tree_selection = current_selection
|
||||||
|
|
||||||
def render_panel_backgrounds(self, screen: pygame.Surface):
|
|
||||||
"""Render panel backgrounds with consistent colors before UI elements."""
|
|
||||||
|
|
||||||
# Render control bar background to match inspector panel
|
|
||||||
if SHOW_CONTROL_BAR and self.control_bar:
|
|
||||||
control_bg_rect = pygame.Rect(
|
|
||||||
0, 0,
|
|
||||||
self.screen_width,
|
|
||||||
self.control_bar_height
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, control_bg_rect)
|
|
||||||
|
|
||||||
# Render console panel background to match inspector panel
|
|
||||||
if SHOW_CONSOLE_PANEL and self.console_panel:
|
|
||||||
console_bg_rect = pygame.Rect(
|
|
||||||
self.console_panel.rect.x,
|
|
||||||
self.console_panel.rect.y,
|
|
||||||
self.console_panel.rect.width,
|
|
||||||
self.console_panel.rect.height
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, console_bg_rect)
|
|
||||||
|
|
||||||
# Render properties panel background to match inspector panel
|
|
||||||
if SHOW_PROPERTIES_PANEL and self.properties_panel:
|
|
||||||
properties_bg_rect = pygame.Rect(
|
|
||||||
self.properties_panel.rect.x,
|
|
||||||
self.properties_panel.rect.y,
|
|
||||||
self.properties_panel.rect.width,
|
|
||||||
self.properties_panel.rect.height
|
|
||||||
)
|
|
||||||
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, properties_bg_rect)
|
|
||||||
|
|
||||||
def render_panel_dividers(self, screen: pygame.Surface):
|
|
||||||
"""Render consistent panel sliders with proper rendering order."""
|
|
||||||
|
|
||||||
# Render consistent panel sliders
|
|
||||||
self.draw_splitters(screen)
|
|
||||||
|
|
||||||
def _create_simulation_controls(self):
|
def _create_simulation_controls(self):
|
||||||
"""Create simulation control buttons in the control bar."""
|
"""Create simulation control buttons in the control bar."""
|
||||||
if not self.control_bar:
|
if not self.control_bar:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Button layout constants (using standardized spacing)
|
# Button layout constants
|
||||||
button_width = 40
|
button_width = 40
|
||||||
button_height = 32
|
button_height = 32
|
||||||
button_spacing = PANEL_TIGHT_SPACING # Using standardized tight spacing
|
button_spacing = 8
|
||||||
start_x = PANEL_INTERNAL_PADDING # Using standardized internal padding
|
start_x = 20
|
||||||
start_y = PANEL_TIGHT_SPACING # Using standardized tight spacing
|
start_y = 8
|
||||||
|
|
||||||
# Play/Pause button
|
# Play/Pause button
|
||||||
self.play_pause_button = UIButton(
|
self.play_pause_button = UIButton(
|
||||||
@ -430,19 +368,14 @@ class HUD:
|
|||||||
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
|
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
|
||||||
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
|
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
|
||||||
event.pos = tree_local_pos
|
event.pos = tree_local_pos
|
||||||
# This is the handle_event call that is being run even when the mouse is not over the tree widget
|
|
||||||
if self.tree_widget.handle_event(event):
|
if self.tree_widget.handle_event(event):
|
||||||
selected_entities = self.tree_widget.get_selected_entities()
|
selected_entities = self.tree_widget.get_selected_entities()
|
||||||
return 'tree_selection_changed', selected_entities
|
return 'tree_selection_changed', selected_entities
|
||||||
else:
|
|
||||||
# Mouse left the tree widget area, clear hover
|
|
||||||
self.tree_widget.clear_hover()
|
|
||||||
else:
|
else:
|
||||||
# Handle specific mouse events in tree widget (but not wheel - handled by InputHandler)
|
# For non-mouse events, try handling them normally
|
||||||
if event.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP):
|
if self.tree_widget.handle_event(event):
|
||||||
if self.tree_widget.handle_event(event):
|
selected_entities = self.tree_widget.get_selected_entities()
|
||||||
selected_entities = self.tree_widget.get_selected_entities()
|
return 'tree_selection_changed', selected_entities
|
||||||
return 'tree_selection_changed', selected_entities
|
|
||||||
|
|
||||||
# Handle simulation control button events using ID matching
|
# Handle simulation control button events using ID matching
|
||||||
if event.type == pygame_gui.UI_BUTTON_START_PRESS:
|
if event.type == pygame_gui.UI_BUTTON_START_PRESS:
|
||||||
@ -506,8 +439,8 @@ class HUD:
|
|||||||
self.update_layout(self.screen_width, self.screen_height)
|
self.update_layout(self.screen_width, self.screen_height)
|
||||||
|
|
||||||
def draw_splitters(self, screen):
|
def draw_splitters(self, screen):
|
||||||
# Draw draggable splitters for visual feedback with consistent styling
|
# Draw draggable splitters for visual feedback
|
||||||
indicator_color = PANEL_ICON_COLOR # Use standardized icon color for consistency
|
indicator_color = (220, 220, 220)
|
||||||
indicator_size = 6 # Length of indicator line
|
indicator_size = 6 # Length of indicator line
|
||||||
indicator_gap = 4 # Gap between indicator lines
|
indicator_gap = 4 # Gap between indicator lines
|
||||||
indicator_count = 3 # Number of indicator lines
|
indicator_count = 3 # Number of indicator lines
|
||||||
@ -1047,6 +980,3 @@ class HUD:
|
|||||||
# Create a surface for the tree widget area
|
# Create a surface for the tree widget area
|
||||||
tree_surface = screen.subsurface(self.inspector_panel.rect)
|
tree_surface = screen.subsurface(self.inspector_panel.rect)
|
||||||
self.tree_widget.draw(tree_surface)
|
self.tree_widget.draw(tree_surface)
|
||||||
|
|
||||||
# Render panel dividers for visual consistency
|
|
||||||
self.render_panel_dividers(screen)
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ Provides extensible tree structure for entity inspection.
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
import pygame
|
import pygame
|
||||||
from config.constants import PANEL_NODE_HEIGHT, PANEL_INDENTATION
|
|
||||||
|
|
||||||
|
|
||||||
class TreeNode(ABC):
|
class TreeNode(ABC):
|
||||||
@ -19,7 +18,7 @@ class TreeNode(ABC):
|
|||||||
self.is_expanded = False
|
self.is_expanded = False
|
||||||
self.is_selected = False
|
self.is_selected = False
|
||||||
self.depth = 0 if parent is None else parent.depth + 1
|
self.depth = 0 if parent is None else parent.depth + 1
|
||||||
self.rect = pygame.Rect(0, 0, 0, PANEL_NODE_HEIGHT) # Will be updated during layout
|
self.rect = pygame.Rect(0, 0, 0, 20) # Will be updated during layout
|
||||||
|
|
||||||
def add_child(self, child: 'TreeNode') -> None:
|
def add_child(self, child: 'TreeNode') -> None:
|
||||||
"""Add a child node to this node."""
|
"""Add a child node to this node."""
|
||||||
@ -78,7 +77,7 @@ class TreeNode(ABC):
|
|||||||
|
|
||||||
def get_indent(self) -> int:
|
def get_indent(self) -> int:
|
||||||
"""Get the indentation width for this node."""
|
"""Get the indentation width for this node."""
|
||||||
return self.depth * PANEL_INDENTATION
|
return self.depth * 20
|
||||||
|
|
||||||
|
|
||||||
class SimulationNode(TreeNode):
|
class SimulationNode(TreeNode):
|
||||||
@ -124,8 +123,6 @@ class SimulationNode(TreeNode):
|
|||||||
# Create new type node
|
# Create new type node
|
||||||
type_node = EntityTypeNode(type_name, entities)
|
type_node = EntityTypeNode(type_name, entities)
|
||||||
self.add_child(type_node)
|
self.add_child(type_node)
|
||||||
# Now update children after this node has correct depth
|
|
||||||
type_node._update_children()
|
|
||||||
|
|
||||||
|
|
||||||
class EntityTypeNode(TreeNode):
|
class EntityTypeNode(TreeNode):
|
||||||
@ -134,7 +131,7 @@ class EntityTypeNode(TreeNode):
|
|||||||
def __init__(self, entity_type: str, entities: List[Any]):
|
def __init__(self, entity_type: str, entities: List[Any]):
|
||||||
super().__init__(entity_type)
|
super().__init__(entity_type)
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
# Don't call _update_children() here - parent will call after adding this node
|
self._update_children()
|
||||||
|
|
||||||
def _update_children(self) -> None:
|
def _update_children(self) -> None:
|
||||||
"""Update child entity nodes to match current entities."""
|
"""Update child entity nodes to match current entities."""
|
||||||
|
|||||||
@ -8,10 +8,6 @@ import pygame_gui
|
|||||||
from pygame_gui.core import UIElement
|
from pygame_gui.core import UIElement
|
||||||
from typing import List, Optional, Tuple, Any
|
from typing import List, Optional, Tuple, Any
|
||||||
from ui.inspector_tree import TreeNode, SimulationNode, TreeSelectionManager
|
from ui.inspector_tree import TreeNode, SimulationNode, TreeSelectionManager
|
||||||
from config.constants import (
|
|
||||||
PANEL_BACKGROUND_COLOR, PANEL_SELECTED_COLOR, PANEL_HOVER_COLOR,
|
|
||||||
PANEL_TEXT_COLOR, PANEL_ICON_COLOR, PANEL_NODE_HEIGHT, PANEL_INDENTATION
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TreeWidget(UIElement):
|
class TreeWidget(UIElement):
|
||||||
@ -33,20 +29,19 @@ class TreeWidget(UIElement):
|
|||||||
# Selection management
|
# Selection management
|
||||||
self.selection_manager = TreeSelectionManager()
|
self.selection_manager = TreeSelectionManager()
|
||||||
|
|
||||||
# Visual properties (using standardized panel styling)
|
# Visual properties
|
||||||
self.node_height = PANEL_NODE_HEIGHT
|
self.node_height = 20
|
||||||
self.expand_collapse_width = PANEL_INDENTATION
|
self.expand_collapse_width = 20
|
||||||
self.icon_size = 8
|
self.icon_size = 8
|
||||||
self.text_color = PANEL_TEXT_COLOR
|
self.text_color = (200, 200, 200)
|
||||||
self.selected_color = PANEL_SELECTED_COLOR
|
self.selected_color = (50, 100, 150)
|
||||||
self.hover_color = PANEL_HOVER_COLOR
|
self.hover_color = (60, 60, 80)
|
||||||
self.expand_icon_color = PANEL_ICON_COLOR
|
self.expand_icon_color = (150, 150, 150)
|
||||||
|
|
||||||
# Interaction state
|
# Interaction state
|
||||||
self.hovered_node: Optional[TreeNode] = None
|
self.hovered_node: Optional[TreeNode] = None
|
||||||
self.drag_start_node: Optional[TreeNode] = None
|
self.drag_start_node: Optional[TreeNode] = None
|
||||||
self.is_dragging = False
|
self.is_dragging = False
|
||||||
self.last_mouse_pos: Optional[Tuple[int, int]] = None
|
|
||||||
|
|
||||||
# Scrolling
|
# Scrolling
|
||||||
self.scroll_offset = 0
|
self.scroll_offset = 0
|
||||||
@ -138,7 +133,6 @@ class TreeWidget(UIElement):
|
|||||||
elif node == self.hovered_node:
|
elif node == self.hovered_node:
|
||||||
pygame.draw.rect(surface, self.hover_color, node.rect)
|
pygame.draw.rect(surface, self.hover_color, node.rect)
|
||||||
|
|
||||||
|
|
||||||
# Expand/collapse icon
|
# Expand/collapse icon
|
||||||
if node.can_expand():
|
if node.can_expand():
|
||||||
icon_rect = self._get_expand_collapse_rect(node)
|
icon_rect = self._get_expand_collapse_rect(node)
|
||||||
@ -188,7 +182,6 @@ class TreeWidget(UIElement):
|
|||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
if event.button == 1: # Left click
|
if event.button == 1: # Left click
|
||||||
# Event position is already converted to local coordinates by HUD
|
# Event position is already converted to local coordinates by HUD
|
||||||
self.last_mouse_pos = event.pos
|
|
||||||
node = self._get_node_at_position_local(event.pos)
|
node = self._get_node_at_position_local(event.pos)
|
||||||
if node:
|
if node:
|
||||||
# Check for expand/collapse click
|
# Check for expand/collapse click
|
||||||
@ -203,7 +196,6 @@ class TreeWidget(UIElement):
|
|||||||
|
|
||||||
elif event.type == pygame.MOUSEMOTION:
|
elif event.type == pygame.MOUSEMOTION:
|
||||||
# Event position is already converted to local coordinates by HUD
|
# Event position is already converted to local coordinates by HUD
|
||||||
self.last_mouse_pos = event.pos
|
|
||||||
self.hovered_node = self._get_node_at_position_local(event.pos)
|
self.hovered_node = self._get_node_at_position_local(event.pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -225,9 +217,6 @@ class TreeWidget(UIElement):
|
|||||||
if self._should_update_tree():
|
if self._should_update_tree():
|
||||||
self._update_tree_structure()
|
self._update_tree_structure()
|
||||||
self._update_visible_nodes()
|
self._update_visible_nodes()
|
||||||
# Recalculate hover state based on last mouse position after structure changes
|
|
||||||
if self.last_mouse_pos:
|
|
||||||
self.hovered_node = self._get_node_at_position_local(self.last_mouse_pos)
|
|
||||||
|
|
||||||
def _should_update_tree(self) -> bool:
|
def _should_update_tree(self) -> bool:
|
||||||
"""Determine if the tree structure needs updating."""
|
"""Determine if the tree structure needs updating."""
|
||||||
@ -257,7 +246,7 @@ class TreeWidget(UIElement):
|
|||||||
"""Draw the tree widget."""
|
"""Draw the tree widget."""
|
||||||
# Create a clipping surface for the tree area
|
# Create a clipping surface for the tree area
|
||||||
tree_surface = pygame.Surface((self.rect.width, self.rect.height))
|
tree_surface = pygame.Surface((self.rect.width, self.rect.height))
|
||||||
tree_surface.fill(PANEL_BACKGROUND_COLOR) # Using standardized background color
|
tree_surface.fill((30, 30, 40)) # Background color
|
||||||
|
|
||||||
# Set up clipping rect to prevent rendering outside bounds
|
# Set up clipping rect to prevent rendering outside bounds
|
||||||
clip_rect = pygame.Rect(0, 0, self.rect.width, self.rect.height)
|
clip_rect = pygame.Rect(0, 0, self.rect.width, self.rect.height)
|
||||||
@ -328,8 +317,3 @@ class TreeWidget(UIElement):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._update_node_layout()
|
self._update_node_layout()
|
||||||
|
|
||||||
def clear_hover(self) -> None:
|
|
||||||
"""Clear the hover state (called when mouse leaves tree widget)."""
|
|
||||||
self.hovered_node = None
|
|
||||||
self.last_mouse_pos = None
|
|
||||||
14
uv.lock
generated
14
uv.lock
generated
@ -146,7 +146,6 @@ dependencies = [
|
|||||||
{ name = "pygame" },
|
{ name = "pygame" },
|
||||||
{ name = "pygame-gui" },
|
{ name = "pygame-gui" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "tqdm" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@ -166,7 +165,6 @@ requires-dist = [
|
|||||||
{ name = "pygame", specifier = ">=2.6.1" },
|
{ name = "pygame", specifier = ">=2.6.1" },
|
||||||
{ name = "pygame-gui", specifier = ">=0.6.14" },
|
{ name = "pygame-gui", specifier = ">=0.6.14" },
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@ -989,18 +987,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tqdm"
|
|
||||||
version = "4.67.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.14.0"
|
version = "4.14.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user