Created
January 1, 2025 08:26
-
-
Save brandonrobertz/3043a4b507b5c4e0465c9ec6d4c43b2f to your computer and use it in GitHub Desktop.
A triangle making program for hexagonal lattices
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math | |
| import time | |
| import pygame | |
| def generate_hexagonal_lattice(width, height, scale): | |
| lattice = [] | |
| dx = 1 | |
| dy = math.sqrt(3) / 2 | |
| cols = int(width / (dx * scale)) + 2 | |
| rows = int(height / (dy * scale)) + 2 | |
| for row in range(rows): | |
| for col in range(cols): | |
| x = col * dx + (0.5 * dx if row % 2 != 0 else 0) | |
| y = row * dy | |
| lattice.append((x, y)) | |
| return lattice | |
| def render_hexagonal_lattice(lattice, screen, scale=50, highlight_point=None, point_size=5): | |
| for point in lattice: | |
| x, y = point | |
| screen_x = int(x * scale) | |
| screen_y = int(y * scale) | |
| color = (0, 255, 0) if highlight_point == point else (55, 55, 55) | |
| pygame.draw.circle(screen, color, (screen_x, screen_y), point_size) | |
| def find_nearest_point(mouse_pos, lattice, scale=50): | |
| mouse_x, mouse_y = mouse_pos | |
| min_dist = float('inf') | |
| nearest_point = None | |
| for point in lattice: | |
| x, y = point | |
| screen_x = int(x * scale) | |
| screen_y = int(y * scale) | |
| dist = math.sqrt((screen_x - mouse_x)**2 + (screen_y - mouse_y)**2) | |
| if dist < min_dist: | |
| min_dist = dist | |
| nearest_point = point | |
| return nearest_point | |
| def side_lengths(tri): | |
| (x1, y1), (x2, y2), (x3, y3) = tri | |
| a = math.sqrt((x2 - x3)**2 + (y2 - y3)**2) | |
| b = math.sqrt((x1 - x3)**2 + (y1 - y3)**2) | |
| c = math.sqrt((x1 - x2)**2 + (y1 - y2)**2) | |
| return a, b, c | |
| def calculate_incenter(tri): | |
| (x1, y1), (x2, y2), (x3, y3) = tri | |
| a, b, c = side_lengths(tri) | |
| px = (a * x1 + b * x2 + c * x3) / (a + b + c) | |
| py = (a * y1 + b * y2 + c * y3) / (a + b + c) | |
| return (px, py) | |
| def calculate_incircle_radius(tri): | |
| """ | |
| The radius of the largest inscribed circle of a triangle, also known as | |
| the inradius 𝑟, is determined using the formula: | |
| 𝑟 = 𝐴 / 𝑠 | |
| where: | |
| 𝐴: the area of the triangle. | |
| 𝑠: the semi-perimeter of the triangle, calculated as: | |
| a+b+c / 2 | |
| """ | |
| a, b, c = side_lengths(tri) | |
| s = (a + b + c) / 2 # semi-perimeter | |
| area = math.sqrt(s * (s - a) * (s - b) * (s - c)) | |
| return area / s if s != 0 else 0 | |
| def calculate_lattice_distance(p1, p2): | |
| dx = abs(p1[0] - p2[0]) | |
| dy = abs(p1[1] - p2[1]) | |
| return math.sqrt(dx**2 + dy**2) | |
| def find_nearest_lattice_point(point, lattice): | |
| min_dist = float('inf') | |
| nearest_point = None | |
| for lattice_point in lattice: | |
| dist = calculate_lattice_distance(point, lattice_point) | |
| if dist < min_dist: | |
| min_dist = dist | |
| nearest_point = lattice_point | |
| return nearest_point, min_dist | |
| def calculate_angle(p1, p2, p3): | |
| a = calculate_lattice_distance(p2, p3) | |
| b = calculate_lattice_distance(p1, p3) | |
| c = calculate_lattice_distance(p1, p2) | |
| try: | |
| angle = math.acos((c**2 + a**2 - b**2) / (2 * c * a)) | |
| except (ZeroDivisionError, ValueError): | |
| return 0 | |
| return math.degrees(angle) | |
| def is_leftmost_point(p, tri): | |
| return p[0] == min([t[0] for t in tri]) | |
| def render_angles(screen, tri, scale): | |
| font = pygame.font.Font(None, 24) | |
| text_color = (255, 255, 255) # White for angle labels | |
| for i, p in enumerate(tri): | |
| p1 = tri[i - 2] | |
| p2 = tri[i - 1] | |
| p3 = p | |
| angle = calculate_angle(p1, p3, p2) | |
| screen_x = int(p[0] * scale) | |
| screen_y = int(p[1] * scale) | |
| # Adjust the position to better align with the vertex | |
| if is_leftmost_point(p, tri): | |
| offset_x = -20 | |
| offset_y = -15 | |
| else: | |
| offset_x = 15 if screen_x > 0 else -15 | |
| offset_y = -15 if screen_y > 0 else 15 | |
| text_surface = font.render(f"{angle:.0f}┬░", True, text_color) | |
| screen.blit(text_surface, (screen_x + offset_x, screen_y + offset_y)) | |
| def angle_between_points(p1, p2, radians=False): | |
| x2, y2 = p2[0] - p1[0], p2[1] - p1[1] # Vector from p1 to p2 | |
| angle_rads = math.atan2(y2, x2) | |
| if radians: | |
| return angle_rads | |
| angle = math.degrees(angle_rads) # Angle in degrees | |
| return angle % 360 # Normalize to [0, 360) | |
| def render_side_lengths(screen, tri, scale): | |
| font = pygame.font.Font(None, 24) | |
| text_color = (200, 200, 200) | |
| incenter = calculate_incenter(tri) | |
| incenter_x, incenter_y = incenter | |
| def draw_label(p1, p2): | |
| mid_x = (p1[0] + p2[0]) / 2 | |
| mid_y = (p1[1] + p2[1]) / 2 | |
| length = calculate_lattice_distance(p1, p2) | |
| text_surface = font.render(f"{length:.0f}", True, text_color) | |
| angle = angle_between_points((mid_x, mid_y), incenter) | |
| offset_x = 0 | |
| offset_y = 0 | |
| if angle <= 90: | |
| offset_x = -20 | |
| offset_y = -20 | |
| elif angle <= 180: | |
| offset_x = 10 | |
| offset_y = -20 | |
| elif angle <= 270: | |
| offset_x = -10 | |
| offset_y = 10 | |
| else: | |
| offset_x = 20 | |
| offset_y = 5 | |
| screen.blit(text_surface, ( | |
| int(mid_x * scale) + offset_x, | |
| int(mid_y * scale) + offset_y | |
| )) | |
| draw_label(tri[0], tri[1]) | |
| draw_label(tri[1], tri[2]) | |
| draw_label(tri[2], tri[0]) | |
| def render_triangles(screen, tri, scale, lattice): | |
| tri_color = (0, 0, 255, 128) | |
| s = pygame.Surface(screen.get_size(), pygame.SRCALPHA) | |
| font = pygame.font.Font(None, 24) | |
| for tri in triangles: | |
| points = [(int(x * scale), int(y * scale)) for x, y in tri] | |
| pygame.draw.polygon(s, tri_color, points, 4) | |
| # Draw incenter circle | |
| incenter = calculate_incenter(tri) | |
| radius = calculate_incircle_radius(tri) | |
| screen_x, screen_y = int(incenter[0] * scale), int(incenter[1] * scale) | |
| pygame.draw.circle(s, (0, 255, 255), (screen_x, screen_y), int(radius * scale), 1) | |
| max_y = max([t[1] for t in tri]) | |
| # Draw incenter diff from lattice | |
| # screen_x = incenter[0] | |
| # screen_y = max([t[0] for t in tri]) + 10 | |
| # screen_x, screen_y = int(incenter[0] * scale), int(incenter[1] * scale) | |
| if incenter in lattice: | |
| pygame.draw.circle(screen, (0, 255, 255), (screen_x, screen_y), 7) | |
| else: | |
| pygame.draw.circle(screen, (255, 0, 255), (screen_x, screen_y), 7) | |
| nearest_point, dist = find_nearest_lattice_point(incenter, lattice) | |
| distance_text = font.render(f"d={dist:.2f}", True, (255, 0, 255)) | |
| # screen.blit(distance_text, (screen_x, screen_y)) | |
| screen.blit(distance_text, (screen_x + 20, (max_y*scale)+30)) | |
| # Display radius | |
| radius_text = font.render(f"r={radius:.2f}", True, (0, 255, 255)) | |
| screen.blit( | |
| radius_text, | |
| ( | |
| screen_x-30, | |
| (max_y*scale) + 30 | |
| ) | |
| ) | |
| render_side_lengths(screen, tri, scale) | |
| render_angles(screen, tri, scale) | |
| screen.blit(s, (0, 0)) | |
| def render_temporary_triangle(screen, points, scale, lattice): | |
| if len(points) > 2: | |
| tri_color = (255, 255, 0, 128) | |
| scaled_points = [(int(x * scale), int(y * scale)) for x, y in points] | |
| s = pygame.Surface(screen.get_size(), pygame.SRCALPHA) | |
| pygame.draw.polygon(s, tri_color, scaled_points, 0) | |
| screen.blit(s, (0, 0)) | |
| incenter = calculate_incenter(points) | |
| screen_x, screen_y = int(incenter[0] * scale), int(incenter[1] * scale) | |
| if incenter in lattice: | |
| pygame.draw.circle(screen, (0, 255, 255), (screen_x, screen_y), 7) | |
| else: | |
| pygame.draw.circle(screen, (255, 0, 255), (screen_x, screen_y), 7) | |
| nearest_point, dist = find_nearest_lattice_point(incenter, lattice) | |
| font = pygame.font.Font(None, 24) | |
| distance_text = font.render(f"d={dist:.2e}", True, (255, 255, 0)) | |
| screen.blit(distance_text, (screen_x + 10, screen_y - 20)) | |
| render_angles(screen, points, scale) | |
| pygame.init() | |
| # width, height = 800, 600 | |
| width, height = 1024, 768 | |
| screen = pygame.display.set_mode((width, height), pygame.RESIZABLE) | |
| pygame.display.set_caption("Hexagonal Lattice") | |
| background_color = (0, 0, 0) | |
| scale = 40 | |
| point_size = 3 | |
| hex_lattice = generate_hexagonal_lattice(width, height, scale) | |
| running = True | |
| highlight_point = None | |
| triangles = [] | |
| temp_points = [] | |
| while running: | |
| time.sleep(0.1) | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: | |
| running = False | |
| elif event.type == pygame.VIDEORESIZE: | |
| width, height = event.w, event.h | |
| screen = pygame.display.set_mode((width, height), pygame.RESIZABLE) | |
| hex_lattice = generate_hexagonal_lattice(width, height, scale) | |
| elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: | |
| if len(temp_points): | |
| temp_points = [] | |
| else: | |
| triangles = triangles[:-1] | |
| elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: | |
| point = find_nearest_point(pygame.mouse.get_pos(), hex_lattice, scale) | |
| if point and point not in temp_points: | |
| temp_points.append(point) | |
| if len(temp_points) == 3: | |
| triangles.append(tuple(temp_points)) | |
| temp_points = [] | |
| mouse_pos = pygame.mouse.get_pos() | |
| highlight_point = find_nearest_point(mouse_pos, hex_lattice, scale) | |
| screen.fill(background_color) | |
| render_hexagonal_lattice( | |
| hex_lattice, screen, scale, highlight_point, | |
| point_size=point_size | |
| ) | |
| render_triangles(screen, triangles, scale, hex_lattice) | |
| if len(temp_points) > 0: | |
| render_temporary_triangle(screen, temp_points + [highlight_point], scale, hex_lattice) | |
| pygame.display.flip() | |
| pygame.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment