Created
May 20, 2025 13:57
-
-
Save joelgraff/223a4c488dffb06eb532b6eb42e1f4ab to your computer and use it in GitHub Desktop.
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 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