335 lines
13 KiB
GDScript
335 lines
13 KiB
GDScript
@tool
|
|
class_name StructureGenerator extends Node
|
|
|
|
# --- CONFIGURATION ---
|
|
const DENSITY_ALUMINUM = 5.0 # kg per square meter (approx for hull plating)
|
|
const COST_PER_KG = 1.0 # Currency per kg
|
|
|
|
|
|
# Run this to regenerate the entire "System 1" library
|
|
func generate_system_one():
|
|
var dir = DirAccess.open("res://data/structure/definitions/")
|
|
if not dir: DirAccess.make_dir_recursive_absolute("res://data/structure/definitions/")
|
|
|
|
print("--- Generating Design System 1: Geodesic ---")
|
|
|
|
# 1. Basic Flats (The backbone)
|
|
create_polygon_plate("s1_flat_square", 4, 0.0)
|
|
create_polygon_plate("s1_flat_triangle", 3, 0.0)
|
|
|
|
# 2. Cylinders (Corridors & Fuselage)
|
|
# 8-Sided: ~2.6m Diameter (Good for corridors)
|
|
create_cylinder_plate("s1_cyl8_wall", 8)
|
|
# 12-Sided: ~3.9m Diameter (Good for main fuselage)
|
|
create_cylinder_plate("s1_cyl12_wall", 12)
|
|
|
|
# 3. Square-Based Dome Cap
|
|
# This creates a 'Cap' square that tilts its edges down,
|
|
# and a 'Side' triangle that connects that square to a flatter ring below.
|
|
create_square_dome_set("s1_dome_sq", 20.0) # 20 degree slope
|
|
|
|
print("Generation Complete.")
|
|
|
|
# --- CORE GENERATORS ---
|
|
|
|
# Creates a regular polygon (Square, Triangle, Hexagon, etc.)
|
|
# bend_angle: Degrees to tilt the mount DOWN. 0 = Flat floor. >0 = Dome/Cylinder.
|
|
func create_polygon_plate(id: String, sides: int, bend_angle: float):
|
|
var res = _create_base_resource(id, "Plate")
|
|
res.shape = "Square" if sides == 4 else "Triangle"
|
|
|
|
# Calculate Radius for exactly 1.0m Edge Length
|
|
var radius = 0.5 / sin(PI / sides)
|
|
var angle_step = TAU / sides
|
|
var start_angle = PI / 4 if sides == 4 else -PI / 6 # Align flat edges to axes
|
|
|
|
res.vertices = [] as Array[Vector3]
|
|
for i in range(sides):
|
|
var theta = start_angle + i * angle_step
|
|
res.vertices.append(Vector3(cos(theta), sin(theta), 0) * radius)
|
|
|
|
# Generate Mounts
|
|
for i in range(sides):
|
|
var p1 = res.vertices[i]
|
|
var p2 = res.vertices[(i + 1) % sides]
|
|
_add_edge_mount(res, p1, p2, bend_angle)
|
|
|
|
_finalize_resource(res, id)
|
|
|
|
# Creates a rectangular plate that forms one segment of a N-sided cylinder
|
|
func create_cylinder_plate(id: String, total_sides: int):
|
|
var res = _create_base_resource(id, "Cylinder Wall")
|
|
res.shape = "Rect"
|
|
|
|
# Height = 1.0m (Standard grid)
|
|
# Width = 1.0m (Chord length of the cylinder)
|
|
var v0 = Vector3(-0.5, 0.5, 0)
|
|
var v1 = Vector3(0.5, 0.5, 0)
|
|
var v2 = Vector3(0.5, -0.5, 0)
|
|
var v3 = Vector3(-0.5, -0.5, 0)
|
|
res.vertices = [v0, v1, v2, v3] as Array[Vector3]
|
|
|
|
# Calculate the bend angle required to form a circle
|
|
# Interior angle = (n-2)*180/n. Bend = (180 - Interior)/2 = 360/n / 2 = 180/n
|
|
var bend = 180.0 / total_sides
|
|
|
|
# Top/Bottom: Flat (0 deg) to stack cylinders
|
|
_add_edge_mount(res, v0, v1, 0.0)
|
|
_add_edge_mount(res, v2, v3, 0.0)
|
|
|
|
# Left/Right: Bent to form the ring
|
|
_add_edge_mount(res, v1, v2, bend)
|
|
_add_edge_mount(res, v3, v0, bend)
|
|
|
|
_finalize_resource(res, id)
|
|
|
|
# Creates a Square Cap and its matching Triangle skirt
|
|
func create_square_dome_set(prefix: String, slope_angle: float):
|
|
# PART A: The Top Square
|
|
# It acts like a flat square, but all mounts are tilted down by 'slope_angle'
|
|
create_polygon_plate(prefix + "_cap", 4, slope_angle)
|
|
|
|
# PART B: The Side Triangle
|
|
# This triangle connects the Tilted Square (Top) to a Flat Ring (Bottom)
|
|
# It is an Isosceles triangle.
|
|
# Top Edge: Matches the Square (1m).
|
|
# Side Edges: Calculated to reach the flat plane.
|
|
|
|
var res = _create_base_resource(prefix + "_side", "Dome Tri")
|
|
res.shape = "Triangle"
|
|
|
|
# We generate this triangle Flat on XY, but calculate mounts to fit the 3D gap.
|
|
# Geometry:
|
|
# The gap it fills has a top width of 1m.
|
|
# The 'dihedral' angle between the Square and this Triangle is (180 - slope_angle).
|
|
# To interface with the square, this triangle's Top Mount must act like it's bent "up" by slope_angle relative to the square's normal.
|
|
|
|
# Actually, simpler logic:
|
|
# 1. Top Edge: Connects to the Square. Needs 'slope_angle' bend.
|
|
# 2. Side Edges: Connect to neighbors in the ring.
|
|
# 3. Bottom Vertex: Pointing down? No, usually a dome layer is a ring of triangles (point up) and triangles (point down).
|
|
|
|
# Let's assume a "Pyramid" style cap for simplicity first:
|
|
# 4 Triangles meeting at a point is too sharp.
|
|
# 4 Triangles connecting to a square creates a 'frustum'.
|
|
|
|
# Vertices for a standard 1m equilateral (placeholder for now, can be tweaked for specific radii)
|
|
var h = sqrt(3) * 0.5
|
|
var v0 = Vector3(-0.5, 0, 0)
|
|
var v1 = Vector3(0.5, 0, 0)
|
|
var v2 = Vector3(0, -h, 0)
|
|
res.vertices = [v0, v1, v2] as Array[Vector3]
|
|
|
|
# Top Edge (v0->v1): Connects to Square.
|
|
# The square is tilted down by 'slope'. To match it, we must tilt 'up' or 'down'?
|
|
# Normals must oppose. Square normal is Tilted Down. This normal must be Tilted Down (relative to self) to be parallel?
|
|
# Actually, both pieces tilt "in" towards the center of the sphere.
|
|
_add_edge_mount(res, v0, v1, slope_angle)
|
|
|
|
# Side Edges (v1->v2, v2->v0): Connect to other triangles in the skirt.
|
|
# These usually need a smaller bend angle, approx half the square's bend for a smooth transition.
|
|
var side_bend = slope_angle * 0.5 # Approximation
|
|
_add_edge_mount(res, v1, v2, side_bend)
|
|
_add_edge_mount(res, v2, v0, side_bend)
|
|
|
|
_finalize_resource(res, prefix + "_side")
|
|
|
|
# --- HELPERS ---
|
|
|
|
func _create_base_resource(id: String, suffix: String) -> StructureData:
|
|
var res = StructureData.new()
|
|
res.piece_name = id.capitalize()
|
|
res.type = StructureData.PieceType.PLATE
|
|
return res
|
|
|
|
func _add_edge_mount(res: StructureData, p1: Vector3, p2: Vector3, bend_deg: float):
|
|
var mid = (p1 + p2) / 2.0
|
|
var edge_vector = (p2 - p1).normalized()
|
|
# Flat normal points -Z (Back) or +Z depending on convention. Using BACK (+Z in Godot) as "Out"
|
|
var flat_normal = edge_vector.cross(Vector3.BACK).normalized()
|
|
|
|
# Rotate normal "Down" around the edge
|
|
var bend_rad = deg_to_rad(bend_deg)
|
|
var final_normal = flat_normal.rotated(edge_vector, bend_rad)
|
|
var final_up = Vector3.BACK.rotated(edge_vector, bend_rad)
|
|
|
|
res.add_mount(mid, final_normal, final_up)
|
|
|
|
func _finalize_resource(res: StructureData, filename: String):
|
|
# Calculate Area for Mass/Cost
|
|
var area = 0.0
|
|
if res.vertices.size() >= 3:
|
|
# Shoelace formula or simple triangle sum
|
|
# For convex shapes centered on 0,0:
|
|
for i in range(res.vertices.size()):
|
|
var p1 = res.vertices[i]
|
|
var p2 = res.vertices[(i + 1) % res.vertices.size()]
|
|
area += 0.5 * (p1.cross(p2).length())
|
|
|
|
res.base_mass = area * DENSITY_ALUMINUM
|
|
res.cost = {"Aluminium": res.base_mass * COST_PER_KG}
|
|
|
|
var path = "res://data/structure/definitions/%s.tres" % filename
|
|
ResourceSaver.save(res, path)
|
|
# print("Generated %s (Mass: %.1f kg)" % [filename, res.base_mass])
|
|
|
|
func generate_system_two_pentagonal():
|
|
print("--- Generating Design System 2: Pentagonal ---")
|
|
|
|
# Configuration: 2m Radius Sphere
|
|
# Icosahedron Edge Length (a) for radius (r): a = r / sin(72) * 2 approx...
|
|
# Let's standardise on the edge length = 1.0m.
|
|
# This results in a sphere radius of ~0.95m.
|
|
var edge_length = 1.0
|
|
|
|
# 1. THE TUBE (Pentagonal Antiprism)
|
|
# A tube made of 10 triangles per segment.
|
|
# To fit a regular pentagon of side 1.0m.
|
|
# Radius of pentagon = 1.0 / (2 * sin(36)) = ~0.85m
|
|
|
|
# We need a triangle that connects two points on the bottom pentagon
|
|
# to one point on the top pentagon (rotated 36 degrees).
|
|
# This forms an equilateral triangle if the height is correct (0.85m).
|
|
create_polygon_plate("s2_equilateral_tri", 3, 0.0) # Standard 1m triangle
|
|
|
|
# 2. THE SPHERE CAP (Pentagonal Pyramid)
|
|
# 5 of these triangles snap together to form a "Cap".
|
|
# The "bend" angle is the dihedral angle of an Icosahedron ~138.19 deg.
|
|
# Deviation from flat = (180 - 138.19) / 2 = ~20.9 degrees.
|
|
|
|
var bend_angle = 20.9
|
|
|
|
var res = _create_base_resource("s2_geo_tri", "Geo Plate")
|
|
res.shape = "Triangle"
|
|
res.vertices = _generate_equilateral_verts(1.0)
|
|
|
|
# Base Edge (0->1): Connects to the rest of the sphere (or extension ring)
|
|
# Side Edges (1->2, 2->0): Connect to neighbors in the 5-way cluster
|
|
|
|
# All edges in a V1 sphere have the same bend angle!
|
|
_add_edge_mount(res, res.vertices[0], res.vertices[1], bend_angle)
|
|
_add_edge_mount(res, res.vertices[1], res.vertices[2], bend_angle)
|
|
_add_edge_mount(res, res.vertices[2], res.vertices[0], bend_angle)
|
|
|
|
_finalize_resource(res, "s2_geo_tri")
|
|
|
|
print("System 2 Generated. Build tubes with 's2_equilateral_tri' and spheres with 's2_geo_tri'.")
|
|
|
|
func _generate_equilateral_verts(side: float) -> Array[Vector3]:
|
|
var h = sqrt(3) * 0.5 * side
|
|
return [
|
|
Vector3(-side/2, -h/3, 0),
|
|
Vector3(side/2, -h/3, 0),
|
|
Vector3(0, 2*h/3, 0)
|
|
]
|
|
# src/data/structure/structure_generator.gd
|
|
|
|
func generate_system_two_v2_sphere():
|
|
print("--- Generating Design System 2: V2 Geodesic (Room Size) ---")
|
|
|
|
# 1. Calculate Geometry (Normalized Radius = 1.0)
|
|
var phi = (1.0 + sqrt(5.0)) / 2.0
|
|
|
|
# Icosahedron vertices
|
|
var v0 = Vector3(0, 1, phi).normalized() # Pole
|
|
var v4 = Vector3(1, phi, 0).normalized() # Neighbor
|
|
var v8 = Vector3(phi, 0, 1).normalized() # Neighbor
|
|
|
|
# Subdivide for V2 (Midpoints projected to sphere)
|
|
var v08 = (v0 + v8).normalized()
|
|
var v84 = (v8 + v4).normalized()
|
|
var v40 = (v4 + v0).normalized()
|
|
|
|
# We now have two distinct triangles:
|
|
# Triangle A (Cap): v0 -> v08 -> v40
|
|
# Triangle B (Face): v08 -> v84 -> v40
|
|
|
|
# 2. Scale Factor
|
|
# We want the "Base" of Triangle A (edge v08-v40) to be exactly 1.0m.
|
|
# This ensures it connects perfectly to our standard 1.0m Tubes.
|
|
var unscaled_base_len = v08.distance_to(v40)
|
|
var scale = 1.0 / unscaled_base_len
|
|
|
|
print("V2 Sphere Radius: %.2fm" % scale)
|
|
|
|
# 3. Generate Triangle A (The Pentagon Cap Piece)
|
|
# This piece forms the 5-way corners.
|
|
var res_a = _create_base_resource("s2_geo_v2_a", "Geo V2 Cap")
|
|
res_a.shape = "Triangle"
|
|
# Centering: Move vertices so the average is at (0,0,0)
|
|
var center_a = (v0 + v08 + v40) / 3.0
|
|
res_a.vertices = [
|
|
(v0 - center_a) * scale, # Top (Pole)
|
|
(v08 - center_a) * scale, # Right
|
|
(v40 - center_a) * scale # Left
|
|
] as Array[Vector3]
|
|
|
|
# Calculate exact bend angles based on the sphere normals
|
|
# The mount normal should be the vertex normal (pointing out from sphere center)
|
|
# relative to the flat face normal.
|
|
_add_mount_from_sphere_geometry(res_a, v0, v08, v40, center_a)
|
|
_finalize_resource(res_a, "s2_geo_v2_a")
|
|
|
|
# 4. Generate Triangle B (The Hexagon Face Piece)
|
|
# This piece fills the gaps between caps.
|
|
var res_b = _create_base_resource("s2_geo_v2_b", "Geo V2 Face")
|
|
res_b.shape = "Triangle"
|
|
var center_b = (v08 + v84 + v40) / 3.0
|
|
res_b.vertices = [
|
|
(v08 - center_b) * scale, # Top-Left
|
|
(v84 - center_b) * scale, # Bottom
|
|
(v40 - center_b) * scale # Top-Right
|
|
] as Array[Vector3]
|
|
|
|
_add_mount_from_sphere_geometry(res_b, v08, v84, v40, center_b)
|
|
_finalize_resource(res_b, "s2_geo_v2_b")
|
|
|
|
# Helper to calculate the correct mount angle for a spherical fragment
|
|
func _add_mount_from_sphere_geometry(res: StructureData, p1_sphere: Vector3, p2_sphere: Vector3, p3_sphere: Vector3, center_sphere: Vector3):
|
|
# We reconstruct the mounts for the 3 edges
|
|
var points = [p1_sphere, p2_sphere, p3_sphere]
|
|
|
|
# Face Normal (Flat plate orientation)
|
|
var face_normal = (p2_sphere - p1_sphere).cross(p3_sphere - p1_sphere).normalized()
|
|
|
|
for i in range(3):
|
|
var a = points[i]
|
|
var b = points[(i+1)%3]
|
|
|
|
# The mount position is the midpoint of the edge (relative to piece center)
|
|
var mid_sphere = (a + b) / 2.0
|
|
var mid_local = (mid_sphere - center_sphere) # Scale is applied later in the main loop, but directions don't care about scale
|
|
|
|
# The mount normal should point OUTWARD from the edge, but follow the sphere's curvature.
|
|
# For a sphere, the perfect "Out" vector at the edge midpoint is just mid_sphere.normalized().
|
|
# However, our mount system expects the normal to be roughly perpendicular to the edge.
|
|
|
|
var edge_vec = (b - a).normalized()
|
|
# Vector perpendicular to edge, tangent to sphere surface at midpoint
|
|
var sphere_tangent_out = edge_vec.cross(mid_sphere.normalized()).normalized()
|
|
|
|
# Wait, the mount normal needs to match the *other* piece's mount normal.
|
|
# If both pieces are on the sphere, their mount normals should be parallel to the chord connecting them?
|
|
# No, standard "snap" logic opposes normals.
|
|
# If we use the Tangent, it points "along" the sphere surface.
|
|
# When two pieces snap, they will form a continuous curve.
|
|
|
|
# Let's stick to the generated tangents.
|
|
# We need to rotate this into the Local Space of the piece.
|
|
# Actually, we are defining vertices in Local Space already.
|
|
# But the normals calculated above are in Sphere Space.
|
|
|
|
# We need to rotate the calculated Sphere Normals into the Flat Face space?
|
|
# No, StructureData mounts are defined in Local Space.
|
|
# The Vertices in res.vertices are already (p - center).
|
|
|
|
# So:
|
|
# Position: (mid_sphere - center_sphere) * scale (handled in main loop, we just need direction here)
|
|
# Normal: sphere_tangent_out (It's a direction vector, translation doesn't affect it)
|
|
# Up: The sphere normal at that point? (mid_sphere.normalized())
|
|
|
|
# Let's assume 'Up' is the surface normal (Out from center of sphere)
|
|
var mount_up = mid_sphere.normalized()
|
|
|
|
res.add_mount((mid_sphere - center_sphere), sphere_tangent_out, mount_up)
|