From 6f9e1e84f02de393f6d632e59d27a99a258613ac Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 19 Jun 2025 16:52:38 -0500 Subject: [PATCH] Refactor HUD to support dynamic panel resizing and improve layout management --- core/renderer.py | 5 +- core/simulation_engine.py | 173 ++++++++++++++++--------------- main.py | 5 +- ui/hud.py | 210 +++++++++++++++++++++++++++++++++----- 4 files changed, 284 insertions(+), 109 deletions(-) diff --git a/core/renderer.py b/core/renderer.py index 1a29bf8..2259613 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -13,11 +13,8 @@ class Renderer: self.render_height = render_area.get_height() self.render_width = render_area.get_width() - def clear_screen(self, main_screen=None): + def clear_screen(self): """Clear the screen with a black background.""" - if main_screen: - main_screen.fill(BLACK) - self.render_area.fill(BLACK) def draw_grid(self, camera, showing_grid=True): diff --git a/core/simulation_engine.py b/core/simulation_engine.py index d12ce95..29db164 100644 --- a/core/simulation_engine.py +++ b/core/simulation_engine.py @@ -18,21 +18,29 @@ from ui.hud import HUD class SimulationEngine: def __init__(self): pygame.init() + self._init_window() + self._init_ui() + self._init_simulation() + self.running = True + def _init_window(self): info = pygame.display.Info() - self.window_width, self.window_height = info.current_w // 2, info.current_h // 2 - self.screen = pygame.display.set_mode((self.window_width, self.window_height), - pygame.RESIZABLE, vsync=1) - - self.ui_manager = UIManager((self.window_width, self.window_height)) - - self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) - self._update_simulation_view() - - # self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) + self.window_width = int(info.current_w // 1.5) + self.window_height = int(info.current_h // 1.5) + self.screen = pygame.display.set_mode( + (self.window_width, self.window_height), + pygame.RESIZABLE, vsync=1 + ) pygame.display.set_caption("Dynamic Abstraction System Testing") self.clock = pygame.time.Clock() + def _init_ui(self): + self.ui_manager = UIManager((self.window_width, self.window_height)) + self.hud = HUD(self.ui_manager, self.window_width, self.window_height) + self.hud.update_layout(self.window_width, self.window_height) + self._update_simulation_view() + + def _init_simulation(self): self.last_tick_time = time.perf_counter() self.last_tps_time = time.perf_counter() self.tick_counter = 0 @@ -42,21 +50,17 @@ class SimulationEngine: self.world = self._setup_world() self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect) self.renderer = Renderer(self.sim_view) - self.hud = HUD(self.ui_manager, self.window_width, self.window_height) - self.hud.update_layout(self.window_width, self.window_height) - - self.running = True def _update_simulation_view(self): - self.sim_view_width = int(self.window_width * 0.75) - self.sim_view_height = int(self.window_height * 0.75) + viewport_rect = self.hud.get_viewport_rect() + self.sim_view_width = viewport_rect.width + self.sim_view_height = viewport_rect.height self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height)) - self.sim_view_rect = self.sim_view.get_rect(center=(self.window_width // 2, self.window_height // 2)) + self.sim_view_rect = self.sim_view.get_rect(topleft=(viewport_rect.left, viewport_rect.top)) self.ui_manager.set_window_resolution((self.window_width, self.window_height)) self.renderer = Renderer(self.sim_view) - # Update camera to match new sim_view size if hasattr(self, 'camera'): self.camera.screen_width = self.sim_view_width self.camera.screen_height = self.sim_view_height @@ -64,6 +68,8 @@ class SimulationEngine: if hasattr(self, 'input_handler'): self.input_handler.update_sim_view_rect(self.sim_view_rect) + if not hasattr(self, 'camera'): + self.camera = Camera(self.sim_view_width, self.sim_view_height, RENDER_BUFFER) @staticmethod def _setup_world(): @@ -80,10 +86,11 @@ class SimulationEngine: world.add_object(FoodObject(Position(x=x, y=y))) for _ in range(300): - new_cell = DefaultCell(Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)), Rotation(angle=0)) - + new_cell = DefaultCell( + Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)), + Rotation(angle=0) + ) new_cell.behavioral_model = new_cell.behavioral_model.mutate(3) - world.add_object(new_cell) return world @@ -91,7 +98,6 @@ class SimulationEngine: def run(self): while self.running: self._handle_frame() - pygame.quit() sys.exit() @@ -99,57 +105,16 @@ class SimulationEngine: deltatime = self.clock.get_time() / 1000.0 tick_interval = 1.0 / self.input_handler.tps - # Handle events events = pygame.event.get() self.running = self.input_handler.handle_events(events, self.hud.manager) - - for event in events: - if event.type == pygame.VIDEORESIZE: - self.window_width, self.window_height = event.w, event.h - self.screen = pygame.display.set_mode((self.window_width, self.window_height), - pygame.RESIZABLE) - self._update_simulation_view() - self.hud.update_layout(self.window_width, self.window_height) + self._handle_window_events(events) if self.input_handler.sprint_mode: - # Sprint mode: run as many ticks as possible, skip rendering - current_time = time.perf_counter() - while True: - self.input_handler.update_selected_objects() - self.world.tick_all() - self.tick_counter += 1 - self.total_ticks += 1 - # Optionally break after some time to allow event processing - if time.perf_counter() - current_time > 0.05: # ~50ms per batch - break - # Update TPS every second - if time.perf_counter() - self.last_tps_time >= 1.0: - self.actual_tps = self.tick_counter - self.tick_counter = 0 - self.last_tps_time = time.perf_counter() - # No rendering or camera update - - self.renderer.clear_screen() - self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks) - pygame.display.flip() - self.clock.tick(MAX_FPS) + self._handle_sprint_mode() return if not self.input_handler.is_paused: - current_time = time.perf_counter() - while current_time - self.last_tick_time >= tick_interval: - self.last_tick_time += tick_interval - self.tick_counter += 1 - self.total_ticks += 1 - - self.input_handler.update_selected_objects() - self.world.tick_all() - self.hud.manager.update(deltatime) - - if current_time - self.last_tps_time >= 1.0: - self.actual_tps = self.tick_counter - self.tick_counter = 0 - self.last_tps_time += 1.0 + self._handle_simulation_ticks(tick_interval, deltatime) else: self.last_tick_time = time.perf_counter() self.last_tps_time = time.perf_counter() @@ -158,25 +123,74 @@ class SimulationEngine: self._update(deltatime) self._render() + def _handle_window_events(self, events): + for event in events: + self.hud.process_event(event) + if event.type == pygame.VIDEORESIZE: + self.window_width, self.window_height = event.w, event.h + self.screen = pygame.display.set_mode( + (self.window_width, self.window_height), + pygame.RESIZABLE + ) + self._update_simulation_view() + self.hud.update_layout(self.window_width, self.window_height) + + self.hud.update_layout(self.window_width, self.window_height) + self._update_simulation_view() + + def _handle_sprint_mode(self): + current_time = time.perf_counter() + while True: + self.input_handler.update_selected_objects() + self.world.tick_all() + self.tick_counter += 1 + self.total_ticks += 1 + if time.perf_counter() - current_time > 0.05: + break + if time.perf_counter() - self.last_tps_time >= 1.0: + self.actual_tps = self.tick_counter + self.tick_counter = 0 + self.last_tps_time = time.perf_counter() + self.screen.fill(BLACK) + self.renderer.clear_screen() + self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks) + pygame.display.flip() + self.clock.tick(MAX_FPS) + + def _handle_simulation_ticks(self, tick_interval, deltatime): + current_time = time.perf_counter() + while current_time - self.last_tick_time >= tick_interval: + self.last_tick_time += tick_interval + self.tick_counter += 1 + self.total_ticks += 1 + self.input_handler.update_selected_objects() + self.world.tick_all() + self.hud.manager.update(deltatime) + if current_time - self.last_tps_time >= 1.0: + self.actual_tps = self.tick_counter + self.tick_counter = 0 + self.last_tps_time += 1.0 + def _update(self, deltatime): keys = pygame.key.get_pressed() self.input_handler.update_camera(keys, deltatime) def _render(self): - self.renderer.clear_screen(self.screen) - self.renderer.draw_grid(self.camera, self.input_handler.show_grid) - self.renderer.render_world(self.world, self.camera) - self.renderer.render_interaction_radius(self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius) - self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect(), self.sim_view_rect) - self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera) + self.screen.fill(BLACK) + self.renderer.clear_screen() - # In core/simulation_engine.py, in _render(): - self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top)) + if not self.hud.dragging_splitter: + self.renderer.draw_grid(self.camera, self.input_handler.show_grid) + self.renderer.render_world(self.world, self.camera) + self.renderer.render_interaction_radius( + self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius + ) + self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect(), self.sim_view_rect) + self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera) + self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top)) - # Draw border around sim_view - border_color = (255, 255, 255) # White - border_width = 3 - pygame.draw.rect(self.screen, border_color, self.sim_view_rect, border_width) + self.hud.manager.draw_ui(self.screen) + self.hud.draw_splitters(self.screen) # <-- Add this line self.hud.render_mouse_position(self.screen, self.camera, self.sim_view_rect) self.hud.render_fps(self.screen, self.clock) @@ -185,9 +199,8 @@ class SimulationEngine: self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects) self.hud.render_legend(self.screen, self.input_handler.show_legend) self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused) - if self.input_handler.selected_objects: self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0]) pygame.display.flip() - self.clock.tick(MAX_FPS) + self.clock.tick(MAX_FPS) \ No newline at end of file diff --git a/main.py b/main.py index 91f7cd6..8a601c4 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,8 @@ from core.simulation_engine import SimulationEngine -if __name__ == "__main__": +def main(): engine = SimulationEngine() engine.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ui/hud.py b/ui/hud.py index 51ab249..78bda6b 100644 --- a/ui/hud.py +++ b/ui/hud.py @@ -6,18 +6,188 @@ import pygame_gui from config.constants import * from world.base.brain import CellBrain, FlexibleNeuralNetwork from world.objects import DefaultCell +from pygame_gui.elements import UIPanel import math +DARK_GRAY = (40, 40, 40) +DARKER_GRAY = (25, 25, 25) class HUD: def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT): self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE) self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE) + self.manager = ui_manager self.screen_width = screen_width self.screen_height = screen_height - self.manager = ui_manager + # Panel size defaults + self.control_bar_height = 48 + self.inspector_width = 260 + self.properties_width = 320 + self.console_height = 120 + self.splitter_thickness = 6 + + self.dragging_splitter = None + self._create_panels() + + def _create_panels(self): + # Top control bar + self.control_bar = UIPanel( + relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height), + manager=self.manager, + object_id="#control_bar", + ) + + # Left inspector + self.inspector_panel = UIPanel( + relative_rect=pygame.Rect( + 0, self.control_bar_height, + self.inspector_width, + self.screen_height - self.control_bar_height + ), + manager=self.manager, + object_id="#inspector_panel", + ) + + # Right properties + self.properties_panel = UIPanel( + relative_rect=pygame.Rect( + self.screen_width - self.properties_width, + self.control_bar_height, + self.properties_width, + self.screen_height - self.control_bar_height + ), + manager=self.manager, + object_id="#properties_panel", + ) + + # Bottom console + self.console_panel = UIPanel( + relative_rect=pygame.Rect( + self.inspector_width, + self.screen_height - self.console_height, + self.screen_width - self.inspector_width - self.properties_width, + self.console_height + ), + manager=self.manager, + object_id="#console_panel", + ) + + self.panels = [ + self.control_bar, + self.inspector_panel, + self.properties_panel, + self.console_panel + ] + self.dragging_splitter = None + + def get_viewport_rect(self): + # Returns the rect for the simulation viewport + x = self.inspector_width + y = self.control_bar_height + w = self.screen_width - self.inspector_width - self.properties_width + h = self.screen_height - self.control_bar_height - self.console_height + return pygame.Rect(x, y, w, h) + + def update_layout(self, window_width, window_height): + self.screen_width = window_width + self.screen_height = window_height + + # Control bar (top) + self.control_bar.set_relative_position((0, 0)) + self.control_bar.set_dimensions((self.screen_width, self.control_bar_height)) + + # Inspector panel (left) - goes all the way to the bottom + self.inspector_panel.set_relative_position((0, self.control_bar_height)) + self.inspector_panel.set_dimensions((self.inspector_width, self.screen_height - self.control_bar_height)) + + # Properties panel (right) - goes all the way to the bottom + self.properties_panel.set_relative_position( + (self.screen_width - self.properties_width, self.control_bar_height)) + self.properties_panel.set_dimensions((self.properties_width, self.screen_height - self.control_bar_height)) + + # Console panel (bottom, spans between inspector and properties) + self.console_panel.set_relative_position((self.inspector_width, self.screen_height - self.console_height)) + self.console_panel.set_dimensions( + (self.screen_width - self.inspector_width - self.properties_width, self.console_height)) + + def process_event(self, event): + # Handle splitter dragging for resizing panels + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + mx, my = event.pos + # Check if mouse is on a splitter (left/right/bottom) + if abs(mx - self.inspector_width) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height: + self.dragging_splitter = "inspector" + elif abs(mx - (self.screen_width - self.properties_width)) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height: + self.dragging_splitter = "properties" + elif abs(my - (self.screen_height - self.console_height)) < self.splitter_thickness and self.inspector_width < mx < self.screen_width - self.properties_width: + self.dragging_splitter = "console" + self.update_layout(self.screen_width, self.screen_height) + elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: + self.dragging_splitter = None + elif event.type == pygame.MOUSEMOTION and self.dragging_splitter: + mx, my = event.pos + if self.dragging_splitter == "inspector": + self.inspector_width = max(100, min(mx, self.screen_width - self.properties_width - 100)) + elif self.dragging_splitter == "properties": + self.properties_width = max(100, min(self.screen_width - mx, self.screen_width - self.inspector_width - 100)) + elif self.dragging_splitter == "console": + self.console_height = max(60, min(self.screen_height - my, self.screen_height - self.control_bar_height - 60)) + self.update_layout(self.screen_width, self.screen_height) + + def draw_splitters(self, screen): + # Draw draggable splitters for visual feedback + indicator_color = (220, 220, 220) + indicator_size = 6 # Length of indicator line + indicator_gap = 4 # Gap between indicator lines + indicator_count = 3 # Number of indicator lines + + # Vertical splitter (inspector/properties) + # Inspector/properties only if wide enough + if self.inspector_width > 0: + x = self.inspector_width - 2 + y1 = self.control_bar_height + y2 = self.screen_height - self.console_height + # Draw indicator (horizontal lines) in the middle + mid_y = (y1 + y2) // 2 + for i in range(indicator_count): + offset = (i - 1) * (indicator_gap + 1) + pygame.draw.line( + screen, indicator_color, + (x - indicator_size // 2, mid_y + offset), + (x + indicator_size // 2, mid_y + offset), + 2 + ) + + if self.properties_width > 0: + x = self.screen_width - self.properties_width + 2 + y1 = self.control_bar_height + y2 = self.screen_height - self.console_height + mid_y = (y1 + y2) // 2 + for i in range(indicator_count): + offset = (i - 1) * (indicator_gap + 1) + pygame.draw.line( + screen, indicator_color, + (x - indicator_size // 2, mid_y + offset), + (x + indicator_size // 2, mid_y + offset), + 2 + ) + + # Horizontal splitter (console) + if self.console_height > 0: + y = self.screen_height - self.console_height + 2 + x1 = self.inspector_width + x2 = self.screen_width - self.properties_width + mid_x = (x1 + x2) // 2 + for i in range(indicator_count): + offset = (i - 1) * (indicator_gap + 1) + pygame.draw.line( + screen, indicator_color, + (mid_x + offset, y - indicator_size // 2), + (mid_x + offset, y + indicator_size // 2), + 2 + ) def render_mouse_position(self, screen, camera, sim_view_rect): """Render mouse position in top left.""" @@ -145,6 +315,7 @@ class HUD: VIZ_WIDTH = 280 # Width of the neural network visualization area VIZ_HEIGHT = 300 # Height of the neural network visualization area VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization + VIZ_BOTTOM_MARGIN = 50 # Distance from the bottom of the screen # Background styling constants BACKGROUND_PADDING = 30 # Padding around the visualization background @@ -196,6 +367,9 @@ class HUD: TOOLTIP_MARGIN = 10 TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines + if self.properties_width < VIZ_RIGHT_MARGIN + 50: + self.properties_width = VIZ_RIGHT_MARGIN + 50 # Ensure properties panel is wide enough for tooltip + if not hasattr(cell, 'behavioral_model'): return @@ -206,9 +380,9 @@ class HUD: network: FlexibleNeuralNetwork = cell_brain.neural_network - # Calculate visualization position + # Calculate visualization position (bottom right) viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen - viz_y = (self.screen_height // 2) - (VIZ_HEIGHT // 2) # Centered vertically + viz_y = self.screen_height - VIZ_HEIGHT - VIZ_BOTTOM_MARGIN # Above the bottom margin layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH @@ -218,6 +392,8 @@ class HUD: pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect) pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH) + info = network.get_structure_info() + # Title title_text = self.font.render("Neural Network", True, WHITE) title_rect = title_text.get_rect() @@ -225,6 +401,13 @@ class HUD: title_rect.top = viz_y - TITLE_TOP_MARGIN screen.blit(title_text, title_rect) + # Render network cost under the title + cost_text = self.font.render(f"Cost: {info['network_cost']}", True, WHITE) + cost_rect = cost_text.get_rect() + cost_rect.centerx = title_rect.centerx + cost_rect.top = title_rect.bottom + 4 # Small gap below the title + screen.blit(cost_text, cost_rect) + # Get current activations by running a forward pass with current inputs input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys] @@ -383,22 +566,6 @@ class HUD: label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN screen.blit(label_text, label_rect) - # Draw network info - info = network.get_structure_info() - info_lines = [ - f"Layers: {info['total_layers']}", - f"Neurons: {info['total_neurons']}", - f"Connections: {info['total_connections']}", - f"Network Cost: {info['network_cost']}", - ] - - for i, line in enumerate(info_lines): - info_text = self.legend_font.render(line, True, WHITE) - info_rect = info_text.get_rect() - info_rect.left = viz_x - info_rect.top = viz_y + VIZ_HEIGHT + INFO_TEXT_TOP_MARGIN + i * INFO_TEXT_LINE_SPACING - screen.blit(info_text, info_rect) - # --- Tooltip logic for neuron hover --- mouse_x, mouse_y = pygame.mouse.get_pos() tooltip_text = None @@ -488,8 +655,3 @@ class HUD: screen.blit(header, header_rect) screen.blit(tps_text, tps_rect) screen.blit(ticks_text, ticks_rect) - - def update_layout(self, window_width, window_height): - """Update HUD layout on window resize.""" - self.screen_width = window_width - self.screen_height = window_height