diff --git a/tests/geometry_demo/geometry_main.py b/tests/geometry_demo/geometry_main.py index e97d073..e0aaa4f 100644 --- a/tests/geometry_demo/geometry_main.py +++ b/tests/geometry_demo/geometry_main.py @@ -69,28 +69,34 @@ class GeometryDemoRunner: mcrfpy.createScene("geo_menu") ui = mcrfpy.sceneUI("geo_menu") + # Screen dimensions + SCREEN_WIDTH = 1024 + SCREEN_HEIGHT = 768 + # Background - bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg = mcrfpy.Frame(pos=(0, 0), size=(SCREEN_WIDTH, SCREEN_HEIGHT)) bg.fill_color = mcrfpy.Color(15, 15, 25) ui.append(bg) # Title - title = mcrfpy.Caption(text="Geometry Module Demo", pos=(400, 30)) + title = mcrfpy.Caption(text="Geometry Module Demo", pos=(SCREEN_WIDTH // 2, 40)) title.fill_color = mcrfpy.Color(255, 255, 255) title.outline = 2 title.outline_color = mcrfpy.Color(0, 0, 0) ui.append(title) - subtitle = mcrfpy.Caption(text="Pinships Orbital Mechanics", pos=(400, 70)) + subtitle = mcrfpy.Caption(text="Pinships Orbital Mechanics", pos=(SCREEN_WIDTH // 2, 80)) subtitle.fill_color = mcrfpy.Color(180, 180, 180) ui.append(subtitle) - # Menu items + # Menu items - wider buttons centered on 1024 width + btn_width = 500 + btn_x = (SCREEN_WIDTH - btn_width) // 2 for i, screen in enumerate(self.screens): - y = 130 + i * 60 + y = 140 + i * 70 # Button frame - btn = mcrfpy.Frame(pos=(200, y), size=(400, 50)) + btn = mcrfpy.Frame(pos=(btn_x, y), size=(btn_width, 60)) btn.fill_color = mcrfpy.Color(30, 40, 60) btn.outline = 2 btn.outline_color = mcrfpy.Color(80, 100, 150) @@ -102,21 +108,21 @@ class GeometryDemoRunner: btn.children.append(label) # Description - desc = mcrfpy.Caption(text=screen.description, pos=(20, 32)) + desc = mcrfpy.Caption(text=screen.description, pos=(20, 35)) desc.fill_color = mcrfpy.Color(120, 120, 150) btn.children.append(desc) # Instructions - instr1 = mcrfpy.Caption(text="Press 1-5 to view demos", pos=(300, 480)) + instr1 = mcrfpy.Caption(text="Press 1-5 to view demos", pos=(SCREEN_WIDTH // 2 - 100, 540)) instr1.fill_color = mcrfpy.Color(150, 150, 150) ui.append(instr1) - instr2 = mcrfpy.Caption(text="ESC = return to menu | Q = quit", pos=(270, 510)) + instr2 = mcrfpy.Caption(text="ESC = return to menu | Q = quit", pos=(SCREEN_WIDTH // 2 - 130, 580)) instr2.fill_color = mcrfpy.Color(100, 100, 100) ui.append(instr2) # Credits - credits = mcrfpy.Caption(text="Geometry module: src/scripts/geometry.py", pos=(250, 560)) + credits = mcrfpy.Caption(text="Geometry module: src/scripts/geometry.py", pos=(SCREEN_WIDTH // 2 - 150, 700)) credits.fill_color = mcrfpy.Color(80, 80, 100) ui.append(credits) @@ -170,12 +176,13 @@ class GeometryDemoRunner: if key in [f"Num{n}" for n in "123456789"]: idx = int(key[-1]) - 1 if idx < len(self.screens): - # Clean up previous screen's timers + # Clean up ALL screen's timers first for screen in self.screens: screen.cleanup() + # Switch to selected scene mcrfpy.setScene(self.screens[idx].scene_name) - # Re-setup the screen to restart animations - # (timers were cleaned up, need to restart) + # Restart timers for the selected screen + self.screens[idx].restart_timers() # ESC returns to menu elif key == "Escape": diff --git a/tests/geometry_demo/screens/angle_lines_demo.py b/tests/geometry_demo/screens/angle_lines_demo.py index 45e0e30..69278c0 100644 --- a/tests/geometry_demo/screens/angle_lines_demo.py +++ b/tests/geometry_demo/screens/angle_lines_demo.py @@ -1,8 +1,8 @@ """Angle calculation demonstration with Line elements.""" import mcrfpy import math -from .base import (GeometryDemoScreen, angle_between, angle_difference, - normalize_angle, point_on_circle, distance) +from .base import (GeometryDemoScreen, screen_angle_between, angle_difference, + normalize_angle, distance, SCREEN_WIDTH, SCREEN_HEIGHT) class AngleLinesDemo(GeometryDemoScreen): @@ -15,34 +15,48 @@ class AngleLinesDemo(GeometryDemoScreen): self.add_title("Angle Calculations & Line Elements") self.add_description("Computing headings, deviations, and opposite angles for pathfinding") - # Demo 1: Basic angle between two points - self._demo_basic_angle() + margin = 30 + frame_gap = 20 + top_area = 80 + bottom_margin = 30 - # Demo 2: Angle between three points (deviation) - self._demo_angle_deviation() + # Calculate frame dimensions for 2x2 layout + frame_width = (SCREEN_WIDTH - 2 * margin - frame_gap) // 2 + available_height = SCREEN_HEIGHT - top_area - bottom_margin - frame_gap + frame_height = available_height // 2 - # Demo 3: Waypoint viability visualization - self._demo_waypoint_viability() + # Demo 1: Basic angle between two points (top-left) + self._demo_basic_angle(margin, top_area, frame_width, frame_height) - # Demo 4: Orbit exit heading - self._demo_orbit_exit() + # Demo 2: Angle deviation (top-right) + self._demo_angle_deviation(margin + frame_width + frame_gap, top_area, + frame_width, frame_height) - def _demo_basic_angle(self): + # Demo 3: Multiple waypoints (bottom-left) + self._demo_waypoint_viability(margin, top_area + frame_height + frame_gap, + frame_width, frame_height) + + # Demo 4: Orbit exit heading (bottom-right) + self._demo_orbit_exit(margin + frame_width + frame_gap, top_area + frame_height + frame_gap, + frame_width, frame_height) + + def _demo_basic_angle(self, fx, fy, fw, fh): """Show angle from point A to point B.""" - bg = mcrfpy.Frame(pos=(30, 80), size=(350, 200)) + bg = mcrfpy.Frame(pos=(fx, fy), size=(fw, fh)) bg.fill_color = mcrfpy.Color(15, 15, 25) bg.outline = 1 bg.outline_color = mcrfpy.Color(60, 60, 100) self.ui.append(bg) - self.add_label("Basic Angle Calculation", 50, 85, (255, 200, 100)) + self.add_label("Basic Angle Calculation", fx + 10, fy + 5, (255, 200, 100)) - # Point A (origin) - ax, ay = 100, 180 - # Point B (target) - bx, by = 300, 120 + # Point A (origin) - lower left area of frame + ax, ay = fx + 80, fy + fh - 80 + # Point B (target) - upper right area of frame + bx, by = fx + fw - 100, fy + 100 - angle = angle_between((ax, ay), (bx, by)) + # Calculate angle using screen coordinates + angle = screen_angle_between((ax, ay), (bx, by)) dist = distance((ax, ay), (bx, by)) # Draw the line A to B (green) @@ -54,17 +68,19 @@ class AngleLinesDemo(GeometryDemoScreen): self.ui.append(line_ab) # Draw reference line (east from A) in gray + ref_length = 120 line_ref = mcrfpy.Line( - start=(ax, ay), end=(ax + 150, ay), + start=(ax, ay), end=(ax + ref_length, ay), color=mcrfpy.Color(100, 100, 100), thickness=1 ) self.ui.append(line_ref) - # Draw arc showing the angle + # Draw arc showing the angle (from reference to target line) + # Arc goes from 0 degrees (east) to the calculated angle arc = mcrfpy.Arc( - center=(ax, ay), radius=40, - start_angle=0, end_angle=-angle, # Negative because screen Y is inverted + center=(ax, ay), radius=50, + start_angle=0, end_angle=angle, color=mcrfpy.Color(255, 255, 100), thickness=2 ) @@ -79,30 +95,30 @@ class AngleLinesDemo(GeometryDemoScreen): self.ui.append(point_b) # Labels - self.add_label("A", ax - 20, ay - 5, (255, 100, 100)) - self.add_label("B", bx + 10, by - 5, (100, 255, 100)) - self.add_label(f"Angle: {angle:.1f}°", 50, 250, (255, 255, 100)) - self.add_label(f"Distance: {dist:.1f}", 180, 250, (150, 150, 150)) + self.add_label("A", ax - 20, ay + 5, (255, 100, 100)) + self.add_label("B", bx + 12, by - 5, (100, 255, 100)) + self.add_label(f"Angle: {angle:.1f} deg", fx + 10, fy + fh - 45, (255, 255, 100)) + self.add_label(f"Distance: {dist:.1f}", fx + 10, fy + fh - 25, (150, 150, 150)) - def _demo_angle_deviation(self): + def _demo_angle_deviation(self, fx, fy, fw, fh): """Show angle deviation when considering a waypoint.""" - bg = mcrfpy.Frame(pos=(400, 80), size=(380, 200)) + bg = mcrfpy.Frame(pos=(fx, fy), size=(fw, fh)) bg.fill_color = mcrfpy.Color(15, 15, 25) bg.outline = 1 bg.outline_color = mcrfpy.Color(60, 60, 100) self.ui.append(bg) - self.add_label("Waypoint Deviation", 420, 85, (255, 200, 100)) - self.add_label("Is planet C a useful waypoint from A to B?", 420, 105, (150, 150, 150)) + self.add_label("Waypoint Deviation", fx + 10, fy + 5, (255, 200, 100)) + self.add_label("Is planet C useful from A to B?", fx + 10, fy + 25, (150, 150, 150)) # Ship at A, target at B, potential waypoint C - ax, ay = 450, 230 - bx, by = 720, 180 - cx, cy = 550, 150 + ax, ay = fx + 60, fy + fh - 100 + bx, by = fx + fw - 60, fy + fh - 60 + cx, cy = fx + fw // 2, fy + 100 - # Calculate angles - angle_to_target = angle_between((ax, ay), (bx, by)) - angle_to_waypoint = angle_between((ax, ay), (cx, cy)) + # Calculate angles using screen coordinates + angle_to_target = screen_angle_between((ax, ay), (bx, by)) + angle_to_waypoint = screen_angle_between((ax, ay), (cx, cy)) deviation = abs(angle_difference(angle_to_target, angle_to_waypoint)) # Draw line A to B (direct path - green) @@ -113,21 +129,28 @@ class AngleLinesDemo(GeometryDemoScreen): ) self.ui.append(line_ab) - # Draw line A to C (waypoint path - yellow if viable, red if not) + # Draw line A to C (waypoint path) viable = deviation <= 45 - waypoint_color = mcrfpy.Color(255, 255, 100) if viable else mcrfpy.Color(255, 100, 100) + waypoint_color = (255, 255, 100) if viable else (255, 100, 100) line_ac = mcrfpy.Line( start=(ax, ay), end=(cx, cy), - color=waypoint_color, + color=mcrfpy.Color(*waypoint_color), thickness=2 ) self.ui.append(line_ac) - # Draw deviation arc + # Draw arc showing the deviation angle between the two directions + # Arc should go from angle_to_target to angle_to_waypoint + start_ang = min(angle_to_target, angle_to_waypoint) + end_ang = max(angle_to_target, angle_to_waypoint) + # If the arc would be > 180, we need to go the other way + if end_ang - start_ang > 180: + start_ang, end_ang = end_ang, start_ang + 360 + arc = mcrfpy.Arc( center=(ax, ay), radius=50, - start_angle=-angle_to_target, end_angle=-angle_to_waypoint, - color=waypoint_color, + start_angle=start_ang, end_angle=end_ang, + color=mcrfpy.Color(*waypoint_color), thickness=2 ) self.ui.append(arc) @@ -145,30 +168,31 @@ class AngleLinesDemo(GeometryDemoScreen): self.ui.append(point_b) self.ui.append(point_c) - # Labels - self.add_label("A (ship)", ax - 10, ay + 10, (255, 100, 100)) - self.add_label("B (target)", bx - 20, by + 15, (100, 255, 100)) - self.add_label("C (planet)", cx + 15, cy - 5, (150, 150, 255)) - label_color = (255, 255, 100) if viable else (255, 100, 100) - self.add_label(f"Deviation: {deviation:.1f}°", 550, 250, label_color) - status = "VIABLE (<45°)" if viable else "NOT VIABLE (>45°)" - self.add_label(status, 680, 250, label_color) + # Labels - positioned to avoid overlap + self.add_label("A (ship)", ax - 15, ay + 15, (255, 100, 100)) + self.add_label("B (target)", bx - 30, by + 15, (100, 255, 100)) + self.add_label("C (planet)", cx + 15, cy - 10, (150, 150, 255)) - def _demo_waypoint_viability(self): + # Status at bottom + self.add_label(f"Deviation: {deviation:.1f} deg", fx + 10, fy + fh - 45, waypoint_color) + status = "VIABLE (<45 deg)" if viable else "NOT VIABLE (>45 deg)" + self.add_label(status, fx + 200, fy + fh - 45, waypoint_color) + + def _demo_waypoint_viability(self, fx, fy, fw, fh): """Show multiple potential waypoints with viability indicators.""" - bg = mcrfpy.Frame(pos=(30, 300), size=(350, 280)) + bg = mcrfpy.Frame(pos=(fx, fy), size=(fw, fh)) bg.fill_color = mcrfpy.Color(15, 15, 25) bg.outline = 1 bg.outline_color = mcrfpy.Color(60, 60, 100) self.ui.append(bg) - self.add_label("Multiple Waypoint Analysis", 50, 305, (255, 200, 100)) + self.add_label("Multiple Waypoint Analysis", fx + 10, fy + 5, (255, 200, 100)) - # Ship and target - ax, ay = 80, 450 - bx, by = 320, 380 + # Ship and target positions + ax, ay = fx + 60, fy + fh - 80 + bx, by = fx + fw - 80, fy + fh // 2 - angle_to_target = angle_between((ax, ay), (bx, by)) + angle_to_target = screen_angle_between((ax, ay), (bx, by)) # Draw direct path line_ab = mcrfpy.Line( @@ -178,26 +202,25 @@ class AngleLinesDemo(GeometryDemoScreen): ) self.ui.append(line_ab) - # Potential waypoints at various angles + # Potential waypoints at various positions waypoints = [ - (150, 360, "W1"), # Ahead and left - viable - (200, 500, "W2"), # Below path - marginal - (100, 540, "W3"), # Behind - not viable - (250, 340, "W4"), # Almost on path - very viable + (fx + 180, fy + 80, "W1"), # Upper area + (fx + 280, fy + fh - 60, "W2"), # Right of path + (fx + 80, fy + fh - 150, "W3"), # Left/behind + (fx + fw - 150, fy + fh // 2 - 30, "W4"), # Near target ] threshold = 45 for wx, wy, label in waypoints: - angle_to_wp = angle_between((ax, ay), (wx, wy)) + angle_to_wp = screen_angle_between((ax, ay), (wx, wy)) deviation = abs(angle_difference(angle_to_target, angle_to_wp)) viable = deviation <= threshold # Line to waypoint color_tuple = (100, 255, 100) if viable else (255, 100, 100) - color = mcrfpy.Color(*color_tuple) line = mcrfpy.Line( start=(ax, ay), end=(wx, wy), - color=color, + color=mcrfpy.Color(*color_tuple), thickness=1 ) self.ui.append(line) @@ -206,12 +229,12 @@ class AngleLinesDemo(GeometryDemoScreen): wp_circle = mcrfpy.Circle( center=(wx, wy), radius=15, fill_color=mcrfpy.Color(80, 80, 120), - outline_color=color, + outline_color=mcrfpy.Color(*color_tuple), outline=2 ) self.ui.append(wp_circle) - self.add_label(f"{label}:{deviation:.0f}°", wx + 18, wy - 8, color_tuple) + self.add_label(f"{label}:{deviation:.0f}", wx + 18, wy - 8, color_tuple) # Ship and target markers ship = mcrfpy.Circle(center=(ax, ay), radius=8, @@ -221,33 +244,38 @@ class AngleLinesDemo(GeometryDemoScreen): self.ui.append(ship) self.ui.append(target) - self.add_label("Ship", ax - 5, ay + 12, (255, 200, 100)) - self.add_label("Target", bx - 15, by + 12, (100, 255, 100)) - self.add_label(f"Threshold: {threshold}°", 50, 555, (150, 150, 150)) + self.add_label("Ship", ax - 10, ay + 12, (255, 200, 100)) + self.add_label("Target", bx - 20, by + 12, (100, 255, 100)) + self.add_label(f"Threshold: {threshold} deg", fx + 10, fy + fh - 25, (150, 150, 150)) - def _demo_orbit_exit(self): + def _demo_orbit_exit(self, fx, fy, fw, fh): """Show optimal orbit exit heading toward target.""" - bg = mcrfpy.Frame(pos=(400, 300), size=(380, 280)) + bg = mcrfpy.Frame(pos=(fx, fy), size=(fw, fh)) bg.fill_color = mcrfpy.Color(15, 15, 25) bg.outline = 1 bg.outline_color = mcrfpy.Color(60, 60, 100) self.ui.append(bg) - self.add_label("Orbit Exit Heading", 420, 305, (255, 200, 100)) - self.add_label("Ship in orbit chooses optimal exit point", 420, 325, (150, 150, 150)) + self.add_label("Orbit Exit Heading", fx + 10, fy + 5, (255, 200, 100)) + self.add_label("Ship repositions FREE in orbit", fx + 10, fy + 25, (150, 150, 150)) # Planet center and orbit - px, py = 520, 450 - orbit_radius = 60 - surface_radius = 25 + px, py = fx + fw // 3, fy + fh // 2 + orbit_radius = 70 + surface_radius = 30 # Target position - tx, ty = 720, 380 + tx, ty = fx + fw - 80, fy + 100 - # Calculate optimal exit angle - exit_angle = angle_between((px, py), (tx, ty)) + # Calculate optimal exit angle (toward target in screen coords) + exit_angle = screen_angle_between((px, py), (tx, ty)) exit_x = px + orbit_radius * math.cos(math.radians(exit_angle)) - exit_y = py - orbit_radius * math.sin(math.radians(exit_angle)) # Flip for screen coords + exit_y = py - orbit_radius * math.sin(math.radians(exit_angle)) # Negate for screen Y + + # Ship's current position on orbit (arbitrary starting position) + ship_angle = exit_angle + 120 # 120 degrees away from exit + ship_x = px + orbit_radius * math.cos(math.radians(ship_angle)) + ship_y = py - orbit_radius * math.sin(math.radians(ship_angle)) # Draw planet surface planet = mcrfpy.Circle( @@ -267,27 +295,25 @@ class AngleLinesDemo(GeometryDemoScreen): ) self.ui.append(orbit) - # Draw ship positions around orbit (current position) - ship_angle = 200 # Current position - ship_x = px + orbit_radius * math.cos(math.radians(ship_angle)) - ship_y = py - orbit_radius * math.sin(math.radians(ship_angle)) + # Draw arc showing orbital movement from ship to exit (FREE movement) + # Arc goes from ship_angle to exit_angle + start_ang = min(ship_angle, exit_angle) + end_ang = max(ship_angle, exit_angle) + orbit_arc = mcrfpy.Arc( + center=(px, py), radius=orbit_radius, + start_angle=start_ang, end_angle=end_ang, + color=mcrfpy.Color(255, 255, 100), + thickness=4 + ) + self.ui.append(orbit_arc) + # Draw ship ship = mcrfpy.Circle( center=(ship_x, ship_y), radius=8, fill_color=mcrfpy.Color(255, 200, 100) ) self.ui.append(ship) - # Draw path: ship moves along orbit (free) to exit point - # Arc from ship position to exit position - orbit_arc = mcrfpy.Arc( - center=(px, py), radius=orbit_radius, - start_angle=-ship_angle, end_angle=-exit_angle, - color=mcrfpy.Color(255, 255, 100), - thickness=3 - ) - self.ui.append(orbit_arc) - # Draw exit point exit_point = mcrfpy.Circle( center=(exit_x, exit_y), radius=6, @@ -310,10 +336,12 @@ class AngleLinesDemo(GeometryDemoScreen): ) self.ui.append(target) - # Labels + # Labels - positioned to avoid overlap self.add_label("Planet", px - 20, py + surface_radius + 5, (100, 150, 220)) - self.add_label("Ship", ship_x - 25, ship_y - 15, (255, 200, 100)) - self.add_label("Exit", exit_x + 10, exit_y - 10, (100, 255, 100)) - self.add_label("Target", tx - 15, ty + 15, (255, 100, 100)) - self.add_label(f"Exit angle: {exit_angle:.1f}°", 420, 555, (150, 150, 150)) - self.add_label("Yellow arc = free orbital movement", 550, 555, (255, 255, 100)) + self.add_label("Ship", ship_x - 30, ship_y - 15, (255, 200, 100)) + self.add_label("Exit", exit_x + 10, exit_y - 15, (100, 255, 100)) + self.add_label("Target", tx - 20, ty + 15, (255, 100, 100)) + + # Info at bottom + self.add_label(f"Exit angle: {exit_angle:.1f} deg", fx + 10, fy + fh - 45, (150, 150, 150)) + self.add_label("Yellow = FREE orbital move", fx + 200, fy + fh - 45, (255, 255, 100)) diff --git a/tests/geometry_demo/screens/base.py b/tests/geometry_demo/screens/base.py index 3420054..ae64303 100644 --- a/tests/geometry_demo/screens/base.py +++ b/tests/geometry_demo/screens/base.py @@ -2,11 +2,30 @@ import mcrfpy import sys import os +import math # Add scripts path for geometry module sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src', 'scripts')) from geometry import * +# Screen resolution +SCREEN_WIDTH = 1024 +SCREEN_HEIGHT = 768 + + +def screen_angle_between(p1, p2): + """ + Calculate angle from p1 to p2 in screen coordinates. + In screen coords, Y increases downward, so we negate dy. + Returns angle in degrees where 0=right, 90=up, 180=left, 270=down. + """ + dx = p2[0] - p1[0] + dy = p1[1] - p2[1] # Negate because screen Y is inverted + angle = math.degrees(math.atan2(dy, dx)) + if angle < 0: + angle += 360 + return angle + class GeometryDemoScreen: """Base class for geometry demo screens.""" @@ -19,6 +38,7 @@ class GeometryDemoScreen: mcrfpy.createScene(scene_name) self.ui = mcrfpy.sceneUI(scene_name) self.timers = [] # Track timer names for cleanup + self._timer_configs = [] # Store timer configs for restart def setup(self): """Override to set up the screen content.""" @@ -32,13 +52,21 @@ class GeometryDemoScreen: except: pass + def restart_timers(self): + """Re-register timers after cleanup.""" + for name, callback, interval in self._timer_configs: + try: + mcrfpy.setTimer(name, callback, interval) + except Exception as e: + print(f"Timer restart failed: {e}") + def get_screenshot_name(self): """Return the screenshot filename for this screen.""" return f"{self.scene_name}.png" def add_title(self, text, y=10): - """Add a title caption.""" - title = mcrfpy.Caption(text=text, pos=(400, y)) + """Add a title caption centered at top.""" + title = mcrfpy.Caption(text=text, pos=(SCREEN_WIDTH // 2, y)) title.fill_color = mcrfpy.Color(255, 255, 255) title.outline = 2 title.outline_color = mcrfpy.Color(0, 0, 0) @@ -79,6 +107,10 @@ class GeometryDemoScreen: pass # Out of bounds def add_timer(self, name, callback, interval): - """Add a timer and track it for cleanup.""" + """Add a timer and track it for cleanup/restart.""" + if callback is None: + print(f"Warning: Timer '{name}' callback is None, skipping") + return mcrfpy.setTimer(name, callback, interval) self.timers.append(name) + self._timer_configs.append((name, callback, interval)) diff --git a/tests/geometry_demo/screens/bresenham_demo.py b/tests/geometry_demo/screens/bresenham_demo.py index 02c417b..94191e1 100644 --- a/tests/geometry_demo/screens/bresenham_demo.py +++ b/tests/geometry_demo/screens/bresenham_demo.py @@ -1,6 +1,7 @@ """Bresenham circle algorithm demonstration on a grid.""" import mcrfpy -from .base import GeometryDemoScreen, bresenham_circle, bresenham_line, filled_circle +import math +from .base import GeometryDemoScreen, bresenham_circle, bresenham_line, filled_circle, SCREEN_WIDTH, SCREEN_HEIGHT class BresenhamDemo(GeometryDemoScreen): @@ -13,48 +14,71 @@ class BresenhamDemo(GeometryDemoScreen): self.add_title("Bresenham Circle & Line Algorithms") self.add_description("Grid-aligned geometric primitives for orbit rings and LOS calculations") - # Create a grid for circle demo - grid_w, grid_h = 25, 18 cell_size = 16 + margin = 30 + frame_gap = 20 - # We need a texture for the grid - create a simple one - # Actually, let's use Grid's built-in cell coloring via GridPoint + # Calculate frame dimensions for 2x2 layout + # Available width: 1024 - 2*margin = 964, split into 2 with gap + frame_width = (SCREEN_WIDTH - 2 * margin - frame_gap) // 2 # ~472 each + # Available height for frames: 768 - 80 (top) - 30 (bottom margin) + top_area = 80 + bottom_margin = 30 + available_height = SCREEN_HEIGHT - top_area - bottom_margin - frame_gap + frame_height = available_height // 2 # ~314 each - # Create display area with Frame background - bg1 = mcrfpy.Frame(pos=(30, 80), size=(420, 310)) - bg1.fill_color = mcrfpy.Color(15, 15, 25) - bg1.outline = 1 - bg1.outline_color = mcrfpy.Color(60, 60, 100) - self.ui.append(bg1) + # Top-left: Bresenham Circle + self._draw_circle_demo(margin, top_area, frame_width, frame_height, cell_size) - self.add_label("Bresenham Circle (radius=8)", 50, 85, (255, 200, 100)) - self.add_label("Center: (12, 9)", 50, 105, (150, 150, 150)) + # Top-right: Bresenham Lines + self._draw_lines_demo(margin + frame_width + frame_gap, top_area, frame_width, frame_height, cell_size) + + # Bottom-left: Filled Circle + self._draw_filled_demo(margin, top_area + frame_height + frame_gap, frame_width, frame_height, cell_size) + + # Bottom-right: Planet + Orbit Ring + self._draw_combined_demo(margin + frame_width + frame_gap, top_area + frame_height + frame_gap, + frame_width, frame_height, cell_size) + + def _draw_circle_demo(self, x, y, w, h, cell_size): + """Draw Bresenham circle demonstration.""" + bg = mcrfpy.Frame(pos=(x, y), size=(w, h)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) + + self.add_label("Bresenham Circle (radius=8)", x + 10, y + 5, (255, 200, 100)) + self.add_label("Center: (12, 9)", x + 10, y + 25, (150, 150, 150)) + + # Grid origin for this demo + grid_x = x + 20 + grid_y = y + 50 - # Draw circle using UICircle primitives to show the cells center = (12, 9) radius = 8 circle_cells = bresenham_circle(center, radius) - # Draw each cell as a small rectangle - for x, y in circle_cells: - px = 40 + x * cell_size - py = 120 + y * cell_size + # Draw each cell + for cx, cy in circle_cells: + px = grid_x + cx * cell_size + py = grid_y + cy * cell_size cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) cell_rect.fill_color = mcrfpy.Color(100, 200, 255) cell_rect.outline = 0 self.ui.append(cell_rect) # Draw center point - cx_px = 40 + center[0] * cell_size - cy_px = 120 + center[1] * cell_size + cx_px = grid_x + center[0] * cell_size + cy_px = grid_y + center[1] * cell_size center_rect = mcrfpy.Frame(pos=(cx_px, cy_px), size=(cell_size - 1, cell_size - 1)) center_rect.fill_color = mcrfpy.Color(255, 100, 100) self.ui.append(center_rect) - # Draw the actual circle outline for comparison + # Draw actual circle outline for comparison (centered on cells) actual_circle = mcrfpy.Circle( - center=(40 + center[0] * cell_size + cell_size // 2, - 120 + center[1] * cell_size + cell_size // 2), + center=(grid_x + center[0] * cell_size + cell_size // 2, + grid_y + center[1] * cell_size + cell_size // 2), radius=radius * cell_size, fill_color=mcrfpy.Color(0, 0, 0, 0), outline_color=mcrfpy.Color(255, 255, 100, 128), @@ -62,14 +86,18 @@ class BresenhamDemo(GeometryDemoScreen): ) self.ui.append(actual_circle) - # Second demo: Bresenham line - bg2 = mcrfpy.Frame(pos=(470, 80), size=(310, 310)) - bg2.fill_color = mcrfpy.Color(15, 15, 25) - bg2.outline = 1 - bg2.outline_color = mcrfpy.Color(60, 60, 100) - self.ui.append(bg2) + def _draw_lines_demo(self, x, y, w, h, cell_size): + """Draw Bresenham lines demonstration.""" + bg = mcrfpy.Frame(pos=(x, y), size=(w, h)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) - self.add_label("Bresenham Lines", 490, 85, (255, 200, 100)) + self.add_label("Bresenham Lines", x + 10, y + 5, (255, 200, 100)) + + grid_x = x + 20 + grid_y = y + 40 # Draw multiple lines at different angles lines_data = [ @@ -80,91 +108,100 @@ class BresenhamDemo(GeometryDemoScreen): for start, end, color in lines_data: line_cells = bresenham_line(start, end) - for x, y in line_cells: - px = 480 + x * cell_size - py = 110 + y * cell_size + for cx, cy in line_cells: + px = grid_x + cx * cell_size + py = grid_y + cy * cell_size cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) cell_rect.fill_color = mcrfpy.Color(*color) self.ui.append(cell_rect) - # Draw the actual line for comparison + # Draw the actual line for comparison (through cell centers) line = mcrfpy.Line( - start=(480 + start[0] * cell_size + cell_size // 2, - 110 + start[1] * cell_size + cell_size // 2), - end=(480 + end[0] * cell_size + cell_size // 2, - 110 + end[1] * cell_size + cell_size // 2), + start=(grid_x + start[0] * cell_size + cell_size // 2, + grid_y + start[1] * cell_size + cell_size // 2), + end=(grid_x + end[0] * cell_size + cell_size // 2, + grid_y + end[1] * cell_size + cell_size // 2), color=mcrfpy.Color(255, 255, 255, 128), thickness=1 ) self.ui.append(line) - # Third demo: Filled circle (planet surface) - bg3 = mcrfpy.Frame(pos=(30, 410), size=(200, 170)) - bg3.fill_color = mcrfpy.Color(15, 15, 25) - bg3.outline = 1 - bg3.outline_color = mcrfpy.Color(60, 60, 100) - self.ui.append(bg3) + def _draw_filled_demo(self, x, y, w, h, cell_size): + """Draw filled circle demonstration.""" + bg = mcrfpy.Frame(pos=(x, y), size=(w, h)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) - self.add_label("Filled Circle (radius=4)", 50, 415, (255, 200, 100)) - self.add_label("Planet surface representation", 50, 435, (150, 150, 150)) + self.add_label("Filled Circle (radius=5)", x + 10, y + 5, (255, 200, 100)) + self.add_label("Planet surface representation", x + 10, y + 25, (150, 150, 150)) - fill_center = (6, 5) - fill_radius = 4 + grid_x = x + 50 + grid_y = y + 60 + + fill_center = (8, 8) + fill_radius = 5 filled_cells = filled_circle(fill_center, fill_radius) - for x, y in filled_cells: - px = 40 + x * cell_size - py = 460 + y * cell_size - cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + for cx, cy in filled_cells: + px = grid_x + cx * cell_size + py = grid_y + cy * cell_size # Gradient based on distance from center - dist = ((x - fill_center[0])**2 + (y - fill_center[1])**2) ** 0.5 + dist = math.sqrt((cx - fill_center[0])**2 + (cy - fill_center[1])**2) intensity = int(255 * (1 - dist / (fill_radius + 1))) + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) cell_rect.fill_color = mcrfpy.Color(intensity, intensity // 2, 50) self.ui.append(cell_rect) - # Fourth demo: Combined - planet with orbit ring - bg4 = mcrfpy.Frame(pos=(250, 410), size=(530, 170)) - bg4.fill_color = mcrfpy.Color(15, 15, 25) - bg4.outline = 1 - bg4.outline_color = mcrfpy.Color(60, 60, 100) - self.ui.append(bg4) + def _draw_combined_demo(self, x, y, w, h, cell_size): + """Draw planet + orbit ring demonstration.""" + bg = mcrfpy.Frame(pos=(x, y), size=(w, h)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + bg.outline = 1 + bg.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(bg) - self.add_label("Planet + Orbit Ring", 270, 415, (255, 200, 100)) - self.add_label("Surface (r=3) + Orbit (r=7)", 270, 435, (150, 150, 150)) + self.add_label("Planet + Orbit Ring", x + 10, y + 5, (255, 200, 100)) + self.add_label("Surface (r=3) + Orbit (r=8)", x + 10, y + 25, (150, 150, 150)) - planet_center = (16, 5) + grid_x = x + 60 + grid_y = y + 50 + + planet_center = (12, 10) surface_radius = 3 - orbit_radius = 7 + orbit_radius = 8 # Draw orbit ring (behind planet) orbit_cells = bresenham_circle(planet_center, orbit_radius) - for x, y in orbit_cells: - px = 260 + x * cell_size - py = 460 + y * cell_size + for cx, cy in orbit_cells: + px = grid_x + cx * cell_size + py = grid_y + cy * cell_size cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) cell_rect.fill_color = mcrfpy.Color(50, 150, 50, 180) self.ui.append(cell_rect) # Draw planet surface (on top) surface_cells = filled_circle(planet_center, surface_radius) - for x, y in surface_cells: - px = 260 + x * cell_size - py = 460 + y * cell_size - cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) - dist = ((x - planet_center[0])**2 + (y - planet_center[1])**2) ** 0.5 + for cx, cy in surface_cells: + px = grid_x + cx * cell_size + py = grid_y + cy * cell_size + dist = math.sqrt((cx - planet_center[0])**2 + (cy - planet_center[1])**2) intensity = int(200 * (1 - dist / (surface_radius + 1))) + cell_rect = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) cell_rect.fill_color = mcrfpy.Color(50 + intensity, 100 + intensity // 2, 200) self.ui.append(cell_rect) - # Legend - self.add_label("Legend:", 600, 455, (200, 200, 200)) + # Legend in bottom-left of frame + leg_x = x + 10 + leg_y = y + h - 50 - leg1 = mcrfpy.Frame(pos=(600, 475), size=(12, 12)) + leg1 = mcrfpy.Frame(pos=(leg_x, leg_y), size=(12, 12)) leg1.fill_color = mcrfpy.Color(100, 150, 200) self.ui.append(leg1) - self.add_label("Planet surface", 620, 473, (150, 150, 150)) + self.add_label("Planet", leg_x + 18, leg_y - 2, (150, 150, 150)) - leg2 = mcrfpy.Frame(pos=(600, 495), size=(12, 12)) + leg2 = mcrfpy.Frame(pos=(leg_x, leg_y + 20), size=(12, 12)) leg2.fill_color = mcrfpy.Color(50, 150, 50) self.ui.append(leg2) - self.add_label("Orbit ring (ship positions)", 620, 493, (150, 150, 150)) + self.add_label("Orbit ring", leg_x + 18, leg_y + 18, (150, 150, 150)) diff --git a/tests/geometry_demo/screens/pathfinding_animated_demo.py b/tests/geometry_demo/screens/pathfinding_animated_demo.py index 2d8c2d2..a1fc7b7 100644 --- a/tests/geometry_demo/screens/pathfinding_animated_demo.py +++ b/tests/geometry_demo/screens/pathfinding_animated_demo.py @@ -4,7 +4,8 @@ import math from .base import (GeometryDemoScreen, OrbitalBody, create_solar_system, create_planet, point_on_circle, distance, angle_between, normalize_angle, is_viable_waypoint, nearest_orbit_entry, - optimal_exit_heading) + optimal_exit_heading, screen_angle_between, + SCREEN_WIDTH, SCREEN_HEIGHT) class PathfindingAnimatedDemo(GeometryDemoScreen): @@ -17,18 +18,29 @@ class PathfindingAnimatedDemo(GeometryDemoScreen): self.add_title("Pathfinding Through Moving Planets") self.add_description("Ship anticipates planetary motion to use orbital slingshots") - # Screen layout - self.center_x = 400 - self.center_y = 320 - self.scale = 2.0 + margin = 30 + top_area = 80 + bottom_panel = 60 + + # Screen layout - full width for 1024x768 + frame_width = SCREEN_WIDTH - 2 * margin + frame_height = SCREEN_HEIGHT - top_area - bottom_panel - margin + + # Center of display area + self.center_x = margin + frame_width // 2 + self.center_y = top_area + frame_height // 2 + self.scale = 2.5 # Larger scale for better visibility # Background - bg = mcrfpy.Frame(pos=(50, 80), size=(700, 460)) + bg = mcrfpy.Frame(pos=(margin, top_area), size=(frame_width, frame_height)) bg.fill_color = mcrfpy.Color(5, 5, 15) bg.outline = 1 bg.outline_color = mcrfpy.Color(40, 40, 80) self.ui.append(bg) + # Store frame boundaries + self.frame_bottom = top_area + frame_height + # Create solar system self.star = create_solar_system( grid_width=200, grid_height=200, @@ -39,7 +51,7 @@ class PathfindingAnimatedDemo(GeometryDemoScreen): self.planet = create_planet( name="Waypoint", star=self.star, - orbital_radius=80, + orbital_radius=60, # Smaller orbit to not clip edges surface_radius=8, orbit_ring_radius=15, angular_velocity=5, # Moves 5 degrees per turn @@ -47,9 +59,10 @@ class PathfindingAnimatedDemo(GeometryDemoScreen): ) # Ship state - self.ship_speed = 10 # Grid units per turn - self.ship_pos = [30, 100] # Start position (grid coords, relative to star) - self.ship_target = [100, -80] # Target position + self.ship_speed = 8 # Grid units per turn + # Position ship further from sun to avoid line clipping through it + self.ship_pos = [-80, 60] # Start position (grid coords, relative to star) - lower left + self.ship_target = [80, -60] # Target position - upper right self.ship_state = "approach" # approach, orbiting, exiting, traveling self.ship_orbit_angle = 0 self.current_time = 0 @@ -234,24 +247,25 @@ class PathfindingAnimatedDemo(GeometryDemoScreen): def _draw_info_panel(self): """Draw information panel.""" - panel = mcrfpy.Frame(pos=(50, 545), size=(700, 45)) + panel_y = self.frame_bottom + 10 + panel = mcrfpy.Frame(pos=(30, panel_y), size=(SCREEN_WIDTH - 60, 45)) panel.fill_color = mcrfpy.Color(20, 20, 35) panel.outline = 1 panel.outline_color = mcrfpy.Color(60, 60, 100) self.ui.append(panel) # Time display - self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(60, 555)) + self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(40, panel_y + 12)) self.time_label.fill_color = mcrfpy.Color(255, 255, 255) self.ui.append(self.time_label) # Status display - self.status_label = mcrfpy.Caption(text="Status: Approaching planet", pos=(180, 555)) + self.status_label = mcrfpy.Caption(text="Status: Approaching planet", pos=(180, panel_y + 12)) self.status_label.fill_color = mcrfpy.Color(100, 200, 255) self.ui.append(self.status_label) # Distance display - self.dist_label = mcrfpy.Caption(text="Distance to target: ---", pos=(450, 555)) + self.dist_label = mcrfpy.Caption(text="Distance to target: ---", pos=(550, panel_y + 12)) self.dist_label.fill_color = mcrfpy.Color(150, 150, 150) self.ui.append(self.dist_label) diff --git a/tests/geometry_demo/screens/pathfinding_static_demo.py b/tests/geometry_demo/screens/pathfinding_static_demo.py index ca7c66f..2692d7a 100644 --- a/tests/geometry_demo/screens/pathfinding_static_demo.py +++ b/tests/geometry_demo/screens/pathfinding_static_demo.py @@ -1,9 +1,8 @@ """Static pathfinding demonstration with planets and orbit rings.""" import mcrfpy import math -from .base import (GeometryDemoScreen, OrbitalBody, bresenham_circle, filled_circle, - angle_between, distance, point_on_circle, is_viable_waypoint, - nearest_orbit_entry, optimal_exit_heading) +from .base import (GeometryDemoScreen, bresenham_circle, filled_circle, + screen_angle_between, distance, SCREEN_WIDTH, SCREEN_HEIGHT) class PathfindingStaticDemo(GeometryDemoScreen): @@ -16,16 +15,20 @@ class PathfindingStaticDemo(GeometryDemoScreen): self.add_title("Pathfinding Through Orbital Bodies") self.add_description("Using free orbital movement to optimize travel paths") - # Create a scenario with multiple planets - # Ship needs to go from bottom-left to top-right - # Optimal path uses planetary orbits as "free repositioning stations" + margin = 30 + top_area = 80 + legend_height = 70 + + # Main display area - use most of screen + frame_width = SCREEN_WIDTH - 2 * margin + frame_height = SCREEN_HEIGHT - top_area - margin - legend_height self.cell_size = 8 - self.offset_x = 50 - self.offset_y = 100 + self.grid_x = margin + 20 + self.grid_y = top_area + 20 # Background - bg = mcrfpy.Frame(pos=(30, 80), size=(740, 480)) + bg = mcrfpy.Frame(pos=(margin, top_area), size=(frame_width, frame_height)) bg.fill_color = mcrfpy.Color(5, 5, 15) bg.outline = 1 bg.outline_color = mcrfpy.Color(40, 40, 80) @@ -33,55 +36,60 @@ class PathfindingStaticDemo(GeometryDemoScreen): # Define planets (center_x, center_y, surface_radius, orbit_radius, name) self.planets = [ - (20, 45, 8, 14, "Alpha"), - (55, 25, 5, 10, "Beta"), - (70, 50, 6, 12, "Gamma"), + (25, 50, 6, 12, "Alpha"), + (60, 25, 4, 9, "Beta"), + (85, 55, 5, 11, "Gamma"), ] # Ship start and end - self.ship_start = (5, 55) - self.ship_end = (85, 10) + self.ship_start = (8, 65) + self.ship_end = (105, 15) - # Draw grid reference (faint) + # Draw grid reference self._draw_grid_reference() - # Draw planets with surfaces and orbit rings + # Draw planets for px, py, sr, orbit_r, name in self.planets: self._draw_planet(px, py, sr, orbit_r, name) - # Calculate and draw optimal path + # Draw optimal path self._draw_optimal_path() # Draw ship and target self._draw_ship_and_target() - # Legend - self._draw_legend() + # Legend at bottom + self._draw_legend(margin, top_area + frame_height + 10) def _to_screen(self, gx, gy): - """Convert grid coords to screen coords.""" - return (self.offset_x + gx * self.cell_size, - self.offset_y + gy * self.cell_size) + """Convert grid coords to screen coords (center of cell).""" + return (self.grid_x + gx * self.cell_size + self.cell_size // 2, + self.grid_y + gy * self.cell_size + self.cell_size // 2) + + def _to_screen_corner(self, gx, gy): + """Convert grid coords to screen coords (corner of cell).""" + return (self.grid_x + gx * self.cell_size, + self.grid_y + gy * self.cell_size) def _draw_grid_reference(self): """Draw faint grid lines for reference.""" - for i in range(0, 91, 10): - # Vertical lines - x = self.offset_x + i * self.cell_size + max_x = 115 + max_y = 75 + for i in range(0, max_x + 1, 10): + x = self.grid_x + i * self.cell_size line = mcrfpy.Line( - start=(x, self.offset_y), - end=(x, self.offset_y + 60 * self.cell_size), + start=(x, self.grid_y), + end=(x, self.grid_y + max_y * self.cell_size), color=mcrfpy.Color(30, 30, 50), thickness=1 ) self.ui.append(line) - for i in range(0, 61, 10): - # Horizontal lines - y = self.offset_y + i * self.cell_size + for i in range(0, max_y + 1, 10): + y = self.grid_y + i * self.cell_size line = mcrfpy.Line( - start=(self.offset_x, y), - end=(self.offset_x + 90 * self.cell_size, y), + start=(self.grid_x, y), + end=(self.grid_x + max_x * self.cell_size, y), color=mcrfpy.Color(30, 30, 50), thickness=1 ) @@ -91,7 +99,7 @@ class PathfindingStaticDemo(GeometryDemoScreen): """Draw a planet with surface and orbit ring.""" sx, sy = self._to_screen(cx, cy) - # Orbit ring (using mcrfpy.Circle for smooth rendering) + # Orbit ring (smooth circle) orbit = mcrfpy.Circle( center=(sx, sy), radius=orbit_r * self.cell_size, @@ -101,10 +109,10 @@ class PathfindingStaticDemo(GeometryDemoScreen): ) self.ui.append(orbit) - # Also draw Bresenham orbit cells for accuracy demo + # Bresenham orbit cells orbit_cells = bresenham_circle((cx, cy), orbit_r) for gx, gy in orbit_cells: - px, py = self._to_screen(gx, gy) + px, py = self._to_screen_corner(gx, gy) cell = mcrfpy.Frame( pos=(px, py), size=(self.cell_size - 1, self.cell_size - 1) @@ -112,10 +120,10 @@ class PathfindingStaticDemo(GeometryDemoScreen): cell.fill_color = mcrfpy.Color(40, 100, 40, 100) self.ui.append(cell) - # Planet surface (filled circle) + # Planet surface surface_cells = filled_circle((cx, cy), surface_r) for gx, gy in surface_cells: - px, py = self._to_screen(gx, gy) + px, py = self._to_screen_corner(gx, gy) dist = math.sqrt((gx - cx)**2 + (gy - cy)**2) intensity = int(180 * (1 - dist / (surface_r + 1))) cell = mcrfpy.Frame( @@ -125,75 +133,90 @@ class PathfindingStaticDemo(GeometryDemoScreen): cell.fill_color = mcrfpy.Color(60 + intensity, 80 + intensity//2, 150) self.ui.append(cell) - # Planet label - self.add_label(name, sx - 15, sy - surface_r * self.cell_size - 15, (150, 150, 200)) + # Planet label (below planet to avoid overlap) + self.add_label(name, sx - 15, sy + (surface_r + 2) * self.cell_size, (150, 150, 200)) def _draw_optimal_path(self): - """Calculate and draw the optimal path using orbital waypoints.""" - # The optimal path: - # 1. Ship starts at (5, 55) - # 2. Direct line to Alpha's orbit entry - # 3. Free arc around Alpha to optimal exit - # 4. Direct line to Gamma's orbit entry - # 5. Free arc around Gamma to optimal exit - # 6. Direct line to target (85, 10) + """Draw the optimal path using orbital waypoints.""" + # The path: + # 1. Ship at (8, 65) -> Alpha orbit entry + # 2. Arc around Alpha -> exit toward Gamma + # 3. Straight to Gamma orbit entry + # 4. Arc around Gamma -> exit toward target + # 5. Straight to target (105, 15) - path_segments = [] + # Alpha: center (25, 50), orbit_r=12 + alpha_center = (25, 50) + alpha_orbit = 12 - # Current position - current = self.ship_start + # Gamma: center (85, 55), orbit_r=11 + gamma_center = (85, 55) + gamma_orbit = 11 - # For this demo, manually define the path through Alpha and Gamma - # (In a real implementation, this would be computed by the pathfinder) + ship_screen = self._to_screen(*self.ship_start) + target_screen = self._to_screen(*self.ship_end) - # Planet Alpha (20, 45, orbit_r=14) - alpha_center = (20, 45) - alpha_orbit = 14 + # --- Segment 1: Ship to Alpha orbit entry --- + # Entry angle: direction from Alpha to ship + entry_angle_alpha = screen_angle_between( + self._to_screen(*alpha_center), + ship_screen + ) + entry_alpha = ( + alpha_center[0] + alpha_orbit * math.cos(math.radians(entry_angle_alpha)), + alpha_center[1] - alpha_orbit * math.sin(math.radians(entry_angle_alpha)) # Screen Y inverted + ) + entry_alpha_screen = self._to_screen(*entry_alpha) - # Entry to Alpha - entry_angle_alpha = angle_between(alpha_center, current) - entry_alpha = point_on_circle(alpha_center, alpha_orbit, entry_angle_alpha) + self._draw_path_line(ship_screen, entry_alpha_screen, (100, 200, 255)) - # Draw line: start -> Alpha entry - self._draw_path_line(current, entry_alpha, (100, 200, 255)) - current = entry_alpha + # --- Segment 2: Arc around Alpha toward Gamma --- + exit_angle_alpha = screen_angle_between( + self._to_screen(*alpha_center), + self._to_screen(*gamma_center) + ) + exit_alpha = ( + alpha_center[0] + alpha_orbit * math.cos(math.radians(exit_angle_alpha)), + alpha_center[1] - alpha_orbit * math.sin(math.radians(exit_angle_alpha)) + ) + exit_alpha_screen = self._to_screen(*exit_alpha) - # Exit from Alpha toward Gamma - gamma_center = (70, 50) - exit_angle_alpha = angle_between(alpha_center, gamma_center) - exit_alpha = point_on_circle(alpha_center, alpha_orbit, exit_angle_alpha) + self._draw_orbit_arc(self._to_screen(*alpha_center), alpha_orbit * self.cell_size, + entry_angle_alpha, exit_angle_alpha) - # Draw arc on Alpha's orbit - self._draw_orbit_arc(alpha_center, alpha_orbit, entry_angle_alpha, exit_angle_alpha) - current = exit_alpha + # --- Segment 3: Alpha exit to Gamma entry --- + entry_angle_gamma = screen_angle_between( + self._to_screen(*gamma_center), + exit_alpha_screen + ) + entry_gamma = ( + gamma_center[0] + gamma_orbit * math.cos(math.radians(entry_angle_gamma)), + gamma_center[1] - gamma_orbit * math.sin(math.radians(entry_angle_gamma)) + ) + entry_gamma_screen = self._to_screen(*entry_gamma) - # Planet Gamma (70, 50, orbit_r=12) - gamma_orbit = 12 + self._draw_path_line(exit_alpha_screen, entry_gamma_screen, (100, 200, 255)) - # Entry to Gamma - entry_angle_gamma = angle_between(gamma_center, current) - entry_gamma = point_on_circle(gamma_center, gamma_orbit, entry_angle_gamma) + # --- Segment 4: Arc around Gamma toward target --- + exit_angle_gamma = screen_angle_between( + self._to_screen(*gamma_center), + target_screen + ) + exit_gamma = ( + gamma_center[0] + gamma_orbit * math.cos(math.radians(exit_angle_gamma)), + gamma_center[1] - gamma_orbit * math.sin(math.radians(exit_angle_gamma)) + ) + exit_gamma_screen = self._to_screen(*exit_gamma) - # Draw line: Alpha exit -> Gamma entry - self._draw_path_line(current, entry_gamma, (100, 200, 255)) - current = entry_gamma + self._draw_orbit_arc(self._to_screen(*gamma_center), gamma_orbit * self.cell_size, + entry_angle_gamma, exit_angle_gamma) - # Exit from Gamma toward target - exit_angle_gamma = angle_between(gamma_center, self.ship_end) - exit_gamma = point_on_circle(gamma_center, gamma_orbit, exit_angle_gamma) + # --- Segment 5: Gamma exit to target --- + self._draw_path_line(exit_gamma_screen, target_screen, (100, 200, 255)) - # Draw arc on Gamma's orbit - self._draw_orbit_arc(gamma_center, gamma_orbit, entry_angle_gamma, exit_angle_gamma) - current = exit_gamma - - # Final segment to target - self._draw_path_line(current, self.ship_end, (100, 200, 255)) - - # For comparison, draw direct path (inefficient) - direct_start = self._to_screen(*self.ship_start) - direct_end = self._to_screen(*self.ship_end) + # Draw direct path for comparison direct_line = mcrfpy.Line( - start=direct_start, end=direct_end, + start=ship_screen, end=target_screen, color=mcrfpy.Color(255, 100, 100, 80), thickness=1 ) @@ -201,10 +224,8 @@ class PathfindingStaticDemo(GeometryDemoScreen): def _draw_path_line(self, p1, p2, color): """Draw a path segment line.""" - s1 = self._to_screen(p1[0], p1[1]) - s2 = self._to_screen(p2[0], p2[1]) line = mcrfpy.Line( - start=s1, end=s2, + start=p1, end=p2, color=mcrfpy.Color(*color), thickness=3 ) @@ -212,81 +233,82 @@ class PathfindingStaticDemo(GeometryDemoScreen): def _draw_orbit_arc(self, center, radius, start_angle, end_angle): """Draw an arc showing orbital movement (free movement).""" - sx, sy = self._to_screen(center[0], center[1]) + # Ensure we draw the shorter arc + diff = end_angle - start_angle + if diff > 180: + start_angle, end_angle = end_angle, start_angle + elif diff < -180: + start_angle, end_angle = end_angle, start_angle - # Normalize angles for drawing - # Screen coordinates have Y inverted, so negate angles arc = mcrfpy.Arc( - center=(sx, sy), - radius=radius * self.cell_size, - start_angle=-start_angle, - end_angle=-end_angle, + center=center, + radius=radius, + start_angle=min(start_angle, end_angle), + end_angle=max(start_angle, end_angle), color=mcrfpy.Color(255, 255, 100), thickness=4 ) self.ui.append(arc) def _draw_ship_and_target(self): - """Draw ship start and target end positions.""" + """Draw ship and target markers.""" + ship_screen = self._to_screen(*self.ship_start) + target_screen = self._to_screen(*self.ship_end) + # Ship - ship_x, ship_y = self._to_screen(*self.ship_start) ship = mcrfpy.Circle( - center=(ship_x + self.cell_size//2, ship_y + self.cell_size//2), + center=ship_screen, radius=10, fill_color=mcrfpy.Color(255, 200, 100), outline_color=mcrfpy.Color(255, 255, 200), outline=2 ) self.ui.append(ship) - self.add_label("SHIP", ship_x - 10, ship_y + 20, (255, 200, 100)) + self.add_label("SHIP", ship_screen[0] - 15, ship_screen[1] + 15, (255, 200, 100)) # Target - target_x, target_y = self._to_screen(*self.ship_end) target = mcrfpy.Circle( - center=(target_x + self.cell_size//2, target_y + self.cell_size//2), + center=target_screen, radius=10, fill_color=mcrfpy.Color(255, 100, 100), outline_color=mcrfpy.Color(255, 200, 200), outline=2 ) self.ui.append(target) - self.add_label("TARGET", target_x - 15, target_y + 20, (255, 100, 100)) - - def _draw_legend(self): - """Draw legend explaining the visualization.""" - leg_x = 50 - leg_y = 520 + self.add_label("TARGET", target_screen[0] - 25, target_screen[1] + 15, (255, 100, 100)) + def _draw_legend(self, x, y): + """Draw legend.""" # Blue line = movement cost line1 = mcrfpy.Line( - start=(leg_x, leg_y + 10), end=(leg_x + 30, leg_y + 10), + start=(x, y + 15), end=(x + 40, y + 15), color=mcrfpy.Color(100, 200, 255), thickness=3 ) self.ui.append(line1) - self.add_label("Impulse movement (costs energy)", leg_x + 40, leg_y + 3, (150, 150, 150)) + self.add_label("Impulse movement (costs energy)", x + 50, y + 8, (150, 150, 150)) # Yellow arc = free movement arc1 = mcrfpy.Arc( - center=(leg_x + 15, leg_y + 45), radius=15, + center=(x + 20, y + 50), radius=18, start_angle=0, end_angle=180, color=mcrfpy.Color(255, 255, 100), thickness=3 ) self.ui.append(arc1) - self.add_label("Orbital movement (FREE)", leg_x + 40, leg_y + 35, (255, 255, 100)) + self.add_label("Orbital movement (FREE)", x + 50, y + 40, (255, 255, 100)) - # Red line = direct (inefficient) + # Red line = direct line2 = mcrfpy.Line( - start=(leg_x + 300, leg_y + 10), end=(leg_x + 330, leg_y + 10), + start=(x + 400, y + 15), end=(x + 440, y + 15), color=mcrfpy.Color(255, 100, 100, 80), thickness=1 ) self.ui.append(line2) - self.add_label("Direct path (for comparison)", leg_x + 340, leg_y + 3, (150, 150, 150)) + self.add_label("Direct path (comparison)", x + 450, y + 8, (150, 150, 150)) # Green cells = orbit ring - cell1 = mcrfpy.Frame(pos=(leg_x + 300, leg_y + 35), size=(15, 15)) + cell1 = mcrfpy.Frame(pos=(x + 400, y + 40), size=(15, 15)) cell1.fill_color = mcrfpy.Color(40, 100, 40) self.ui.append(cell1) - self.add_label("Orbit ring cells (valid ship positions)", leg_x + 320, leg_y + 35, (150, 150, 150)) + self.add_label("Orbit ring (ship positions)", x + 420, y + 40, (150, 150, 150)) diff --git a/tests/geometry_demo/screens/solar_system_demo.py b/tests/geometry_demo/screens/solar_system_demo.py index 8210851..bd6eead 100644 --- a/tests/geometry_demo/screens/solar_system_demo.py +++ b/tests/geometry_demo/screens/solar_system_demo.py @@ -2,7 +2,8 @@ import mcrfpy import math from .base import (GeometryDemoScreen, OrbitalBody, create_solar_system, - create_planet, create_moon, point_on_circle) + create_planet, create_moon, point_on_circle, + SCREEN_WIDTH, SCREEN_HEIGHT) class SolarSystemDemo(GeometryDemoScreen): @@ -15,18 +16,29 @@ class SolarSystemDemo(GeometryDemoScreen): self.add_title("Animated Solar System") self.add_description("Planets snap to grid positions as time advances (1 tick = 1 turn)") - # Screen layout - self.center_x = 400 - self.center_y = 320 - self.scale = 1.5 # Pixels per grid unit + margin = 30 + top_area = 80 + bottom_panel = 60 + + # Screen layout - centered, with room for Earth's moon orbit + frame_width = SCREEN_WIDTH - 2 * margin + frame_height = SCREEN_HEIGHT - top_area - bottom_panel - margin + + # Center of display area, shifted down a bit to give room for moon orbit at top + self.center_x = margin + frame_width // 2 + self.center_y = top_area + frame_height // 2 + 30 # Shifted down + self.scale = 2.0 # Pixels per grid unit (larger for 1024x768) # Background - bg = mcrfpy.Frame(pos=(50, 80), size=(700, 480)) + bg = mcrfpy.Frame(pos=(margin, top_area), size=(frame_width, frame_height)) bg.fill_color = mcrfpy.Color(5, 5, 15) bg.outline = 1 bg.outline_color = mcrfpy.Color(40, 40, 80) self.ui.append(bg) + # Store frame boundaries for info panel + self.frame_bottom = top_area + frame_height + # Create the solar system using geometry module self.star = create_solar_system( grid_width=200, grid_height=200, @@ -92,13 +104,21 @@ class SolarSystemDemo(GeometryDemoScreen): # Draw initial planet positions self._draw_planets() + # Info panel below the main frame + panel_y = self.frame_bottom + 10 + panel = mcrfpy.Frame(pos=(30, panel_y), size=(SCREEN_WIDTH - 60, 45)) + panel.fill_color = mcrfpy.Color(20, 20, 35) + panel.outline = 1 + panel.outline_color = mcrfpy.Color(60, 60, 100) + self.ui.append(panel) + # Time display - self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(60, 530)) + self.time_label = mcrfpy.Caption(text="Turn: 0", pos=(40, panel_y + 12)) self.time_label.fill_color = mcrfpy.Color(255, 255, 255) self.ui.append(self.time_label) # Instructions - self.add_label("Time advances automatically every second", 200, 530, (150, 150, 150)) + self.add_label("Time advances automatically every second", 200, panel_y + 12, (150, 150, 150)) # Start the animation timer self.add_timer("solar_tick", self._tick, 1000) # 1 second per turn diff --git a/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png b/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png index 64b43e5..37716e9 100644 Binary files a/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png and b/tests/geometry_demo/screenshots/geo_00_bresenham_algorithms.png differ diff --git a/tests/geometry_demo/screenshots/geo_01_angle_calculations.png b/tests/geometry_demo/screenshots/geo_01_angle_calculations.png index 5d1550d..b7efa92 100644 Binary files a/tests/geometry_demo/screenshots/geo_01_angle_calculations.png and b/tests/geometry_demo/screenshots/geo_01_angle_calculations.png differ diff --git a/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png b/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png index 833bef1..55d8aee 100644 Binary files a/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png and b/tests/geometry_demo/screenshots/geo_02_static_pathfinding.png differ diff --git a/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png b/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png index fb74ab4..c31c08c 100644 Binary files a/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png and b/tests/geometry_demo/screenshots/geo_03_solar_system_animation.png differ diff --git a/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png b/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png index 7346ebb..698e521 100644 Binary files a/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png and b/tests/geometry_demo/screenshots/geo_04_animated_pathfinding.png differ