From d5d44c5d14f71f0c97ff28cfa55c5f33a90fc04d Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 19 Jun 2025 14:45:15 -0500 Subject: [PATCH] Refactor HUD and renderer to support dynamic screen resizing and update input handling for simulation view --- core/input_handler.py | 28 ++++++++++++---- core/renderer.py | 70 ++++++++++++++++++++++----------------- core/simulation_engine.py | 49 ++++++++++++++++++++++----- ui/hud.py | 50 +++++++++++++++++----------- 4 files changed, 132 insertions(+), 65 deletions(-) diff --git a/core/input_handler.py b/core/input_handler.py index 7c227b4..97e3586 100644 --- a/core/input_handler.py +++ b/core/input_handler.py @@ -6,7 +6,7 @@ from config.constants import * class InputHandler: - def __init__(self, camera, world): + def __init__(self, camera, world, sim_view_rect): self.camera = camera self.world = world @@ -27,6 +27,13 @@ class InputHandler: self.default_tps = DEFAULT_TPS self.sprint_mode = False + # sim-view rect for mouse position calculations + self.sim_view_rect = sim_view_rect + + def update_sim_view_rect(self, sim_view_rect): + """Update the sim_view rectangle.""" + self.sim_view_rect = sim_view_rect + def handle_events(self, events, ui_manager): """Process all pygame events and return game state.""" running = True @@ -115,20 +122,27 @@ class InputHandler: """Process object selection logic.""" self.selecting = False - # Convert screen to world coordinates - x1, y1 = self.camera.get_real_coordinates(*self.select_start) - x2, y2 = self.camera.get_real_coordinates(*self.select_end) + # Map screen to sim_view coordinates + sx1 = self.select_start[0] - self.sim_view_rect.left + sy1 = self.select_start[1] - self.sim_view_rect.top + sx2 = self.select_end[0] - self.sim_view_rect.left + sy2 = self.select_end[1] - self.sim_view_rect.top + + # Convert sim_view to world coordinates + x1, y1 = self.camera.get_real_coordinates(sx1, sy1) + x2, y2 = self.camera.get_real_coordinates(sx2, sy2) # Check if selection is a click or drag - if (abs(self.select_start[0] - self.select_end[0]) < SELECTION_THRESHOLD and - abs(self.select_start[1] - self.select_end[1]) < SELECTION_THRESHOLD): + if (abs(sx1 - sx2) < SELECTION_THRESHOLD and + abs(sy1 - sy2) < SELECTION_THRESHOLD): self._handle_click_selection() else: self._handle_drag_selection(x1, y1, x2, y2) def _handle_click_selection(self): """Handle single click selection.""" - mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(*self.select_start) + sx, sy = self.select_start[0] - self.sim_view_rect.left, self.select_start[1] - self.sim_view_rect.top + mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(sx, sy) obj = self.world.query_closest_object(mouse_world_x, mouse_world_y) self.selected_objects = [] diff --git a/core/renderer.py b/core/renderer.py index 309c680..1a29bf8 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -8,12 +8,17 @@ from world.base.brain import CellBrain class Renderer: - def __init__(self, screen): - self.screen = screen + def __init__(self, render_area): + self.render_area = render_area + self.render_height = render_area.get_height() + self.render_width = render_area.get_width() - def clear_screen(self): + def clear_screen(self, main_screen=None): """Clear the screen with a black background.""" - self.screen.fill(BLACK) + if main_screen: + main_screen.fill(BLACK) + + self.render_area.fill(BLACK) def draw_grid(self, camera, showing_grid=True): """Draw the reference grid.""" @@ -28,8 +33,8 @@ class Renderer: grid_world_height = GRID_HEIGHT * effective_cell_size # Calculate grid position relative to camera (with grid centered at 0,0) - grid_center_x = SCREEN_WIDTH // 2 - camera.x * camera.zoom - grid_center_y = SCREEN_HEIGHT // 2 - camera.y * camera.zoom + grid_center_x = self.render_width // 2 - camera.x * camera.zoom + grid_center_y = self.render_height // 2 - camera.y * camera.zoom grid_left = grid_center_x - grid_world_width // 2 grid_top = grid_center_y - grid_world_height // 2 @@ -37,20 +42,20 @@ class Renderer: grid_bottom = grid_top + grid_world_height # Check if grid is visible on screen - if (grid_right < 0 or grid_left > SCREEN_WIDTH or - grid_bottom < 0 or grid_top > SCREEN_HEIGHT): + if (grid_right < 0 or grid_left > self.render_width or + grid_bottom < 0 or grid_top > self.render_height): return # Fill the grid area with dark gray background grid_rect = pygame.Rect( max(0, grid_left), max(0, grid_top), - min(SCREEN_WIDTH, grid_right) - max(0, grid_left), - min(SCREEN_HEIGHT, grid_bottom) - max(0, grid_top), + min(self.render_width, grid_right) - max(0, grid_left), + min(self.render_height, grid_bottom) - max(0, grid_top), ) if grid_rect.width > 0 and grid_rect.height > 0: - pygame.draw.rect(self.screen, DARK_GRAY, grid_rect) + pygame.draw.rect(self.render_area, DARK_GRAY, grid_rect) # Draw grid lines only if zoom is high enough if effective_cell_size > 4: @@ -65,30 +70,30 @@ class Renderer: # Vertical lines if i <= GRID_WIDTH: line_x = grid_left + i * effective_cell_size - if 0 <= line_x <= SCREEN_WIDTH: + if 0 <= line_x <= self.render_width: start_y = max(0, grid_top) - end_y = min(SCREEN_HEIGHT, grid_bottom) + end_y = min(self.render_height, grid_bottom) if start_y < end_y: vertical_lines.append(((line_x, start_y), (line_x, end_y))) # Horizontal lines if i <= GRID_HEIGHT: line_y = grid_top + i * effective_cell_size - if 0 <= line_y <= SCREEN_HEIGHT: + if 0 <= line_y <= self.render_height: start_x = max(0, grid_left) - end_x = min(SCREEN_WIDTH, grid_right) + end_x = min(self.render_width, grid_right) if start_x < end_x: horizontal_lines.append(((start_x, line_y), (end_x, line_y))) # Draw all lines for start, end in vertical_lines: - pygame.draw.line(self.screen, GRAY, start, end) + pygame.draw.line(self.render_area, GRAY, start, end) for start, end in horizontal_lines: - pygame.draw.line(self.screen, GRAY, start, end) + pygame.draw.line(self.render_area, GRAY, start, end) def render_world(self, world, camera): """Render all world objects.""" - world.render_all(camera, self.screen) + world.render_all(camera, self.render_area) def render_interaction_radius(self, world, camera, selected_objects, show_radius=False): """Render interaction radius and debug vectors for objects.""" @@ -108,7 +113,7 @@ class Renderer: if screen_radius > 0: # Draw interaction radius circle - pygame.draw.circle(self.screen, RED, (screen_x, screen_y), screen_radius, 1) + pygame.draw.circle(self.render_area, RED, (screen_x, screen_y), screen_radius, 1) # Draw direction arrow self._draw_direction_arrow(obj, screen_x, screen_y, camera) @@ -125,7 +130,7 @@ class Renderer: end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) # Draw arrow line - pygame.draw.line(self.screen, WHITE, (screen_x, screen_y), (end_x, end_y), 2) + pygame.draw.line(self.render_area, WHITE, (screen_x, screen_y), (end_x, end_y), 2) # Draw arrowhead tip_size = DIRECTION_TIP_SIZE * camera.zoom @@ -135,7 +140,7 @@ class Renderer: right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) pygame.draw.polygon( - self.screen, WHITE, + self.render_area, WHITE, [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] ) @@ -167,7 +172,7 @@ class Renderer: angular_acc_end_x = end_x + angular_accel_magnitude * math.cos(math.radians(angular_direction)) angular_acc_end_y = end_y + angular_accel_magnitude * math.sin(math.radians(angular_direction)) - pygame.draw.line(self.screen, LIGHT_BLUE, (end_x, end_y), (angular_acc_end_x, angular_acc_end_y), 2) + pygame.draw.line(self.render_area, LIGHT_BLUE, (end_x, end_y), (angular_acc_end_x, angular_acc_end_y), 2) # Draw arrowhead self._draw_arrowhead(angular_acc_end_x, angular_acc_end_y, angular_direction, @@ -184,7 +189,7 @@ class Renderer: acc_end_x = screen_x + acc_vector_length * math.cos(math.radians(acc_direction)) acc_end_y = screen_y + acc_vector_length * math.sin(math.radians(acc_direction)) - pygame.draw.line(self.screen, RED, (screen_x, screen_y), (acc_end_x, acc_end_y), 2) + pygame.draw.line(self.render_area, RED, (screen_x, screen_y), (acc_end_x, acc_end_y), 2) self._draw_arrowhead(acc_end_x, acc_end_y, acc_direction, ARROW_TIP_SIZE * camera.zoom, RED) @@ -199,7 +204,7 @@ class Renderer: vel_end_x = screen_x + vel_vector_length * math.cos(math.radians(vel_direction)) vel_end_y = screen_y + vel_vector_length * math.sin(math.radians(vel_direction)) - pygame.draw.line(self.screen, BLUE, (screen_x, screen_y), (vel_end_x, vel_end_y), 2) + pygame.draw.line(self.render_area, BLUE, (screen_x, screen_y), (vel_end_x, vel_end_y), 2) self._draw_arrowhead(vel_end_x, vel_end_y, vel_direction, ARROW_TIP_SIZE * camera.zoom, BLUE) @@ -211,24 +216,29 @@ class Renderer: right_tip_y = end_y - tip_size * math.sin(math.radians(direction - 150 + 180)) pygame.draw.polygon( - self.screen, color, + self.render_area, color, [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] ) - def render_selection_rectangle(self, selection_rect): - """Render the selection rectangle.""" + def render_selection_rectangle(self, selection_rect, sim_view_rect=None): + """Render the selection rectangle, offset for sim_view if sim_view_rect is provided.""" if not selection_rect: return left, top, width, height = selection_rect + # Offset for sim_view if sim_view_rect is given + if sim_view_rect is not None: + left -= sim_view_rect.left + top -= sim_view_rect.top + # Draw semi-transparent fill s = pygame.Surface((width, height), pygame.SRCALPHA) s.fill(SELECTION_GRAY) - self.screen.blit(s, (left, top)) + self.render_area.blit(s, (left, top)) # Draw border - pygame.draw.rect(self.screen, SELECTION_BORDER, + pygame.draw.rect(self.render_area, SELECTION_BORDER, pygame.Rect(left, top, width, height), 1) def render_selected_objects_outline(self, selected_objects, camera): @@ -239,4 +249,4 @@ class Renderer: screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) size = camera.get_relative_size(width) rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) - pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1) + pygame.draw.rect(self.render_area, SELECTION_BLUE, rect, 1) diff --git a/core/simulation_engine.py b/core/simulation_engine.py index a086664..d12ce95 100644 --- a/core/simulation_engine.py +++ b/core/simulation_engine.py @@ -20,16 +20,18 @@ class SimulationEngine: pygame.init() info = pygame.display.Info() - self.window_width, self.window_height = info.current_w, info.current_h + 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 | pygame.FULLSCREEN) + 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) pygame.display.set_caption("Dynamic Abstraction System Testing") self.clock = pygame.time.Clock() - self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) self.last_tick_time = time.perf_counter() self.last_tps_time = time.perf_counter() @@ -38,9 +40,10 @@ class SimulationEngine: self.total_ticks = 0 self.world = self._setup_world() - self.input_handler = InputHandler(self.camera, self.world) - self.renderer = Renderer(self.screen) - self.hud = HUD() + 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 @@ -50,6 +53,18 @@ class SimulationEngine: 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.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 + + if hasattr(self, 'input_handler'): + self.input_handler.update_sim_view_rect(self.sim_view_rect) + + @staticmethod def _setup_world(): world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) @@ -88,6 +103,14 @@ class SimulationEngine: 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) + if self.input_handler.sprint_mode: # Sprint mode: run as many ticks as possible, skip rendering current_time = time.perf_counter() @@ -140,14 +163,22 @@ class SimulationEngine: self.input_handler.update_camera(keys, deltatime) def _render(self): - self.renderer.clear_screen() + 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.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.hud.render_mouse_position(self.screen, self.camera) + # In core/simulation_engine.py, in _render(): + 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.render_mouse_position(self.screen, self.camera, self.sim_view_rect) self.hud.render_fps(self.screen, self.clock) self.hud.render_tps(self.screen, self.actual_tps) self.hud.render_tick_count(self.screen, self.total_ticks) diff --git a/ui/hud.py b/ui/hud.py index c6fbfcb..51ab249 100644 --- a/ui/hud.py +++ b/ui/hud.py @@ -10,15 +10,22 @@ import math class HUD: - def __init__(self, ui_manager): + 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.screen_width = screen_width + self.screen_height = screen_height + self.manager = ui_manager - def render_mouse_position(self, screen, camera): + def render_mouse_position(self, screen, camera, sim_view_rect): """Render mouse position in top left.""" - mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) + mouse_x, mouse_y = pygame.mouse.get_pos() + sim_view_x = mouse_x - sim_view_rect.left + sim_view_y = mouse_y - sim_view_rect.top + world_x, world_y = camera.get_real_coordinates(sim_view_x, sim_view_y) + mouse_text = self.font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) text_rect = mouse_text.get_rect() text_rect.topleft = (HUD_MARGIN, HUD_MARGIN) @@ -28,21 +35,21 @@ class HUD: """Render FPS in top right.""" fps_text = self.font.render(f"FPS: {int(clock.get_fps())}", True, WHITE) fps_rect = fps_text.get_rect() - fps_rect.topright = (SCREEN_WIDTH - HUD_MARGIN, HUD_MARGIN) + fps_rect.topright = (self.screen_width - HUD_MARGIN, HUD_MARGIN) screen.blit(fps_text, fps_rect) def render_tps(self, screen, actual_tps): """Render TPS in bottom right.""" tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE) tps_rect = tps_text.get_rect() - tps_rect.bottomright = (SCREEN_WIDTH - HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) + tps_rect.bottomright = (self.screen_width - HUD_MARGIN, self.screen_height - HUD_MARGIN) screen.blit(tps_text, tps_rect) def render_tick_count(self, screen, total_ticks): """Render total tick count in bottom left.""" tick_text = self.font.render(f"Ticks: {total_ticks}", True, WHITE) tick_rect = tick_text.get_rect() - tick_rect.bottomleft = (HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) + tick_rect.bottomleft = (HUD_MARGIN, self.screen_height - HUD_MARGIN) screen.blit(tick_text, tick_rect) def render_pause_indicator(self, screen, is_paused): @@ -50,7 +57,7 @@ class HUD: if is_paused: pause_text = self.font.render("Press 'Space' to unpause", True, WHITE) pause_rect = pause_text.get_rect() - pause_rect.center = (SCREEN_WIDTH // 2, 20) + pause_rect.center = (self.screen_width // 2, 20) screen.blit(pause_text, pause_rect) def render_selected_objects_info(self, screen, selected_objects): @@ -58,7 +65,7 @@ class HUD: if len(selected_objects) < 1: return - max_width = SCREEN_WIDTH - 20 + max_width = self.screen_width - 20 i = 0 for obj in selected_objects: @@ -94,7 +101,7 @@ class HUD: if not showing_legend: legend_text = self.legend_font.render("Press 'L' to show controls", True, WHITE) legend_rect = legend_text.get_rect() - legend_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 20) + legend_rect.center = (self.screen_width // 2, self.screen_height - 20) screen.blit(legend_text, legend_rect) return @@ -112,8 +119,8 @@ class HUD: legend_width = left_width + right_width + column_gap legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10 - legend_x = (SCREEN_WIDTH - legend_width) // 2 - legend_y = SCREEN_HEIGHT - legend_height - 10 + legend_x = (self.screen_width - legend_width) // 2 + legend_y = self.screen_height - legend_height - 10 # Draw left column for i, (key, desc) in enumerate(left_col): @@ -200,8 +207,8 @@ class HUD: network: FlexibleNeuralNetwork = cell_brain.neural_network # Calculate visualization position - viz_x = SCREEN_WIDTH - VIZ_RIGHT_MARGIN # Right side of screen - viz_y = (SCREEN_HEIGHT // 2) - (VIZ_HEIGHT // 2) # Centered vertically + viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen + viz_y = (self.screen_height // 2) - (VIZ_HEIGHT // 2) # Centered vertically layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH @@ -453,10 +460,10 @@ class HUD: tooltip_y = mouse_y + TOOLTIP_Y_OFFSET # Adjust if off right edge - if tooltip_x + width > SCREEN_WIDTH: + if tooltip_x + width > self.screen_width: tooltip_x = mouse_x - width - TOOLTIP_X_OFFSET # Adjust if off bottom edge - if tooltip_y + height > SCREEN_HEIGHT: + if tooltip_y + height > self.screen_height: tooltip_y = mouse_y - height - TOOLTIP_Y_OFFSET tooltip_rect = pygame.Rect(tooltip_x, tooltip_y, width, height) @@ -473,11 +480,16 @@ class HUD: tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255)) ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255)) - y = SCREEN_HEIGHT // 2 - 40 - header_rect = header.get_rect(center=(SCREEN_WIDTH // 2, y)) - tps_rect = tps_text.get_rect(center=(SCREEN_WIDTH // 2, y + 40)) - ticks_rect = ticks_text.get_rect(center=(SCREEN_WIDTH // 2, y + 80)) + y = self.screen_height // 2 - 40 + header_rect = header.get_rect(center=(self.screen_width // 2, y)) + tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40)) + ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80)) 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