Skip to content

Instantly share code, notes, and snippets.

@brandonrobertz
Created January 1, 2025 08:26
Show Gist options
  • Select an option

  • Save brandonrobertz/3043a4b507b5c4e0465c9ec6d4c43b2f to your computer and use it in GitHub Desktop.

Select an option

Save brandonrobertz/3043a4b507b5c4e0465c9ec6d4c43b2f to your computer and use it in GitHub Desktop.
A triangle making program for hexagonal lattices
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