Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save joelgraff/223a4c488dffb06eb532b6eb42e1f4ab to your computer and use it in GitHub Desktop.
Save joelgraff/223a4c488dffb06eb532b6eb42e1f4ab to your computer and use it in GitHub Desktop.
import numpy as np
from shapely.geometry import Polygon, Point
from shapely.affinity import scale
import argparse
import pygame
import sys
# Set random seed for reproducibility
np.random.seed(42)
# Helper Functions for Hull Generation
def regular_polygon(n, r):
"""Generate points for a regular n-sided polygon with radius r."""
return [(r * np.cos(2 * np.pi * i / n), r * np.sin(2 * np.pi * i / n)) for i in range(n)]
def bezier(t, P0, P1, P2):
"""Compute a point on a quadratic Bezier curve."""
return (1 - t)**2 * np.array(P0) + 2 * (1 - t) * t * np.array(P1) + t**2 * np.array(P2)
def generate_hull_from_base(base_points, edge_mods, subdivisions=1, perturbation=0, directional_perturbation=False, samples_per_curve=10):
"""Generate a hull polygon from base points and edge modifications."""
points = []
n = len(base_points)
for i in range(n):
P0 = np.array(base_points[i])
P2 = np.array(base_points[(i + 1) % n])
mod = edge_mods[i]
if mod == 'straight':
for j in range(subdivisions):
t1 = j / subdivisions
t2 = (j + 1) / subdivisions
if perturbation > 0 and j < subdivisions - 1:
if directional_perturbation:
# Compute normal vector for directional perturbation
d = P2 - P0
normal = np.array([-d[1], d[0]]) / (np.linalg.norm(d) + 1e-10)
# Randomly choose inward or outward perturbation
direction = np.random.choice([-1, 1])
offset = direction * np.random.uniform(0, perturbation) * normal
P1 = (1 - t1) * P0 + t1 * P2 + offset
else:
rand_vec = np.random.uniform(-perturbation, perturbation, 2)
P1 = (1 - t1) * P0 + t1 * P2 + rand_vec
else:
P1 = (1 - t1) * P0 + t1 * P2
points.append(tuple(P1))
points.append(tuple(P2))
else: # 'curved'
control_point = np.array(mod[1])
for t in np.linspace(0, 1, samples_per_curve, endpoint=False):
B = bezier(t, P0, control_point, P2)
points.append(tuple(B))
points.append(tuple(P2))
poly = Polygon(points)
if not poly.is_valid:
print("Debug: Points causing self-intersection:", points)
raise ValueError("Invalid polygon (self-intersecting). Adjust parameters.")
return poly
def scale_polygon(poly, desired_size=50):
"""Scale the polygon to fit within a desired size (e.g., 50x50 meters)."""
minx, miny, maxx, maxy = poly.bounds
current_width = maxx - minx
current_height = maxy - miny
scale_factor = desired_size / max(current_width, current_height)
return scale(poly, xfact=scale_factor, yfact=scale_factor, origin='centroid')
# Hull Generation Functions
def generate_convex_curvilinear_hull(n=8, r=20, k=0.5):
base_points = regular_polygon(n, r)
edge_mods = []
for i in range(n):
P0 = np.array(base_points[i])
P2 = np.array(base_points[(i + 1) % n])
d = P2 - P0
normal = np.array([-d[1], d[0]]) / np.linalg.norm(d)
control_point = tuple((P0 + P2) / 2 + k * r * normal)
edge_mods.append(('curved', control_point))
poly = generate_hull_from_base(base_points, edge_mods)
return scale_polygon(poly)
def generate_concave_curvilinear_hull(n=8, r=20, k=0.5):
base_points = regular_polygon(n, r)
edge_mods = []
for i in range(n):
P0 = np.array(base_points[i])
P2 = np.array(base_points[(i + 1) % n])
d = P2 - P0
normal = np.array([-d[1], d[0]]) / np.linalg.norm(d)
if i % 2 == 0:
control_point = tuple((P0 + P2) / 2 - k * r * normal)
else:
control_point = tuple((P0 + P2) / 2 + k * r * normal)
edge_mods.append(('curved', control_point))
poly = generate_hull_from_base(base_points, edge_mods)
return scale_polygon(poly)
def generate_mixed_hull(n=8, r=20, k=0.5):
base_points = regular_polygon(n, r)
edge_mods = []
for i in range(n):
if i % 2 == 0:
edge_mods.append('straight')
else:
P0 = np.array(base_points[i])
P2 = np.array(base_points[(i + 1) % n])
d = P2 - P0
normal = np.array([-d[1], d[0]]) / np.linalg.norm(d)
control_point = tuple((P0 + P2) / 2 + k * r * normal)
edge_mods.append(('curved', control_point))
poly = generate_hull_from_base(base_points, edge_mods)
return scale_polygon(poly)
def generate_complex_linear_hull(n=16, r=20, subdivisions=3, perturbation=2.0, directional_perturbation=True, max_attempts=5):
"""Generate a complex linear hull with significant perturbations, retrying if self-intersection occurs."""
# Step 1: Perturb the base points for more significant shape variation
base_points = regular_polygon(n, r)
perturbed_base_points = []
for i in range(n):
P = np.array(base_points[i])
# Compute the normal direction (radial for a polygon centered at origin)
normal = P / (np.linalg.norm(P) + 1e-10)
# Apply a significant perturbation along the radial direction (inward or outward)
direction = np.random.choice([-1, 1])
offset = direction * np.random.uniform(0, perturbation * 2) * normal # Larger perturbation for base points
perturbed_base_points.append(tuple(P + offset))
edge_mods = ['straight'] * n
attempt = 0
current_perturbation = perturbation
while attempt < max_attempts:
try:
poly = generate_hull_from_base(
perturbed_base_points,
edge_mods,
subdivisions=subdivisions,
perturbation=current_perturbation,
directional_perturbation=directional_perturbation
)
# Simplify the polygon to remove minor self-intersections
poly = poly.simplify(0.5, preserve_topology=True)
if not poly.is_valid:
raise ValueError("Polygon still invalid after simplification.")
return scale_polygon(poly)
except ValueError as e:
print(f"Attempt {attempt + 1}: {e}")
current_perturbation *= 0.8 # Reduce perturbation on retry
attempt += 1
raise ValueError(f"Failed to generate a valid complex linear hull after {max_attempts} attempts.")
# Core Generation Functions
def generate_square_core(size=10):
return {'type': 'square', 'size': size}
def generate_elliptical_core(a=8, b=8):
return {'type': 'ellipse', 'a': a, 'b': b}
# Shape Checking Functions for Deck Generation
def is_inside_square(x, y, size):
"""Check if point (x, y) is inside a square of given size centered at (0,0)."""
return -size / 2 <= x <= size / 2 and -size / 2 <= y <= size / 2
def is_inside_ellipse(x, y, a, b):
"""Check if point (x, y) is inside an ellipse with semi-axes a and b."""
return (x / a) ** 2 + (y / b) ** 2 <= 1
def is_inside_polygon(x, y, poly):
"""Check if point (x, y) is inside a Shapely polygon."""
return poly.contains(Point(x, y))
# Deck Generation and Visualization Function
def generate_deck(hull_shape, core_shape, grid_size=1.0, scale=10, window_size=600):
"""Generate and visualize a deck layout with given hull and core shapes."""
pygame.init()
screen = pygame.display.set_mode((window_size, window_size))
pygame.display.set_caption(f"Hull: {hull_shape['type']}, Core: {core_shape['type']}")
clock = pygame.time.Clock()
# Center the deck in the window
offset_x = window_size / 2
offset_y = window_size / 2
# Determine grid bounds based on hull
if hull_shape['type'] == 'square':
hull_size = hull_shape['size']
x_min, x_max = -hull_size / 2, hull_size / 2
y_min, y_max = -hull_size / 2, hull_size / 2
elif hull_shape['type'] == 'ellipse':
a = hull_shape['a']
b = hull_shape['b']
x_min, x_max = -a, a
y_min, y_max = -b, b
else: # polygon
poly = hull_shape['poly']
minx, miny, maxx, maxy = poly.bounds
x_min, x_max = minx, maxx
y_min, y_max = miny, maxy
# Generate 1x1 meter grid
i_range = np.arange(x_min, x_max, grid_size)
j_range = np.arange(y_min, y_max, grid_size)
grid = []
for i in i_range:
row = []
for j in j_range:
center_x = i + grid_size / 2
center_y = j + grid_size / 2
# Check hull
if hull_shape['type'] == 'square':
inside_hull = is_inside_square(center_x, center_y, hull_shape['size'])
elif hull_shape['type'] == 'ellipse':
inside_hull = is_inside_ellipse(center_x, center_y, hull_shape['a'], hull_shape['b'])
else:
inside_hull = is_inside_polygon(center_x, center_y, hull_shape['poly'])
# Check core
if core_shape['type'] == 'square':
inside_core = is_inside_square(center_x, center_y, core_shape['size'])
elif core_shape['type'] == 'ellipse':
inside_core = is_inside_ellipse(center_x, center_y, core_shape['a'], core_shape['b'])
else:
inside_core = is_inside_polygon(center_x, center_y, core_shape['poly'])
status = 'usable' if inside_hull and not inside_core else 'core' if inside_core else 'void'
row.append(status)
grid.append(row)
# Visualization loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((0, 0, 0)) # Black background
# Draw grid cells
for i_idx, i in enumerate(i_range):
for j_idx, j in enumerate(j_range):
status = grid[i_idx][j_idx]
color = (200, 200, 200) if status == 'usable' else (100, 100, 100) if status == 'core' else None
if color:
screen_x = offset_x + i * scale
screen_y = offset_y - (j + grid_size) * scale # PyGame y-axis is inverted
pygame.draw.rect(screen, color, (screen_x, screen_y, grid_size * scale, grid_size * scale))
# Draw hull outline (red)
if hull_shape['type'] == 'square':
hull_rect = (-hull_shape['size'] / 2 * scale + offset_x,
-hull_shape['size'] / 2 * scale + offset_y,
hull_shape['size'] * scale, hull_shape['size'] * scale)
pygame.draw.rect(screen, (255, 0, 0), hull_rect, 2)
elif hull_shape['type'] == 'ellipse':
hull_rect = (-hull_shape['a'] * scale + offset_x,
-hull_shape['b'] * scale + offset_y,
2 * hull_shape['a'] * scale, 2 * hull_shape['b'] * scale)
pygame.draw.ellipse(screen, (255, 0, 0), hull_rect, 2)
else:
points = [(offset_x + p[0] * scale, offset_y - p[1] * scale) for p in hull_shape['poly'].exterior.coords]
pygame.draw.polygon(screen, (255, 0, 0), points, 2)
# Draw core outline (blue)
if core_shape['type'] == 'square':
core_rect = (-core_shape['size'] / 2 * scale + offset_x,
-core_shape['size'] / 2 * scale + offset_y,
core_shape['size'] * scale, core_shape['size'] * scale)
pygame.draw.rect(screen, (0, 0, 255), core_rect, 2)
elif core_shape['type'] == 'ellipse':
core_rect = (-core_shape['a'] * scale + offset_x,
-core_shape['b'] * scale + offset_y,
2 * core_shape['a'] * scale, 2 * core_shape['b'] * scale)
pygame.draw.ellipse(screen, (0, 0, 255), core_rect, 2)
else:
points = [(offset_x + p[0] * scale, offset_y - p[1] * scale) for p in core_shape['poly'].exterior.coords]
pygame.draw.polygon(screen, (0, 0, 255), points, 2)
pygame.display.flip()
clock.tick(30)
pygame.quit()
sys.exit()
# Main Function with Parameter Selection
if __name__ == "__main__":
# Command-line argument parser
parser = argparse.ArgumentParser(description="Visualize starship deck layouts.")
parser.add_argument('--hull', type=str, default='convex',
choices=['convex', 'concave', 'mixed', 'complex_linear'],
help="Type of hull to visualize (convex, concave, mixed, complex_linear)")
parser.add_argument('--core', type=str, default='square',
choices=['square', 'elliptical'],
help="Type of core to use (square, elliptical)")
args = parser.parse_args()
# Define hull types
hull_functions = {
'convex': generate_convex_curvilinear_hull,
'concave': generate_concave_curvilinear_hull,
'mixed': generate_mixed_hull,
'complex_linear': generate_complex_linear_hull
}
# Define core types
core_functions = {
'square': generate_square_core,
'elliptical': generate_elliptical_core
}
# Generate hull and core based on arguments
hull_func = hull_functions[args.hull]
hull_poly = hull_func()
hull = {'type': 'polygon', 'poly': hull_poly}
core_func = core_functions[args.core]
core = core_func()
# Visualize the deck
print(f"Visualizing {args.hull} Hull with {args.core} Core")
generate_deck(hull, core)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment