Files
millimeters-of-aluminum/scenes/ship/builder/pieces/procedural_piece.gd
2025-12-05 15:50:48 +01:00

242 lines
6.9 KiB
GDScript

class_name ProceduralPiece extends StructuralPiece
@export var vertices: Array[Vector3] = []
@export var thickness: float = 0.02
const PLATE_SHADER = preload("res://scenes/ship/builder/pieces/structural_plate.gdshader")
# This is inherited from StructuralPiece, but we use the setter to update visuals
# var is_preview: bool (Inherited)
func _ready():
# If data has vertices (from the Resource), use them.
if structure_data and structure_data.vertices.size() > 0:
vertices = structure_data.vertices
# Or if configured procedurally
# elif structure_data and structure_data.procedural_params:
# _configure_from_data()
if not vertices.is_empty():
_generate_geometry()
super._ready()
# Configure this piece as a simple Strut (Beam)
func configure_strut(start: Vector3, end: Vector3):
vertices = [start, end]
# Create ephemeral data for this specific instance
structure_data = StructureData.new()
structure_data.piece_name = "Strut"
structure_data.type = StructureData.PieceType.STRUT
# Define Mounts at both ends
var dir = (end - start).normalized()
# Mount 0: At Start, facing away from End (-dir)
structure_data.add_mount(start, -dir)
# Mount 1: At End, facing away from Start (+dir)
structure_data.add_mount(end, dir)
_generate_geometry()
# Configure this piece as a Plate (Triangle/Quad)
func configure_plate(points: Array[Vector3]):
vertices = points
structure_data = StructureData.new()
structure_data.piece_name = "Plate"
structure_data.type = StructureData.PieceType.PLATE
# Add Mounts along edges (simplified: midpoints of edges)
var center = Vector3.ZERO
for p in points: center += p
center /= points.size()
for i in range(points.size()):
var p1 = points[i]
var p2 = points[(i + 1) % points.size()]
var mid = (p1 + p2) / 2.0
var edge_norm = (mid - center).normalized()
structure_data.add_mount(mid, edge_norm)
_generate_geometry()
func _generate_geometry():
for c in get_children():
if c is MeshInstance3D or c is CollisionShape3D:
c.queue_free()
if vertices.size() == 2:
_build_strut_mesh()
elif vertices.size() >= 3:
_build_plate_mesh()
func _build_strut_mesh():
# Generate a Box or Cylinder between v[0] and v[1]
var start = vertices[0]
var end = vertices[1]
var length = start.distance_to(end)
var mid = (start + end) / 2.0
var mesh_inst = MeshInstance3D.new()
var box = BoxMesh.new()
box.size = Vector3(thickness, thickness, length)
mesh_inst.mesh = box
add_child(mesh_inst)
# Orient mesh
mesh_inst.position = mid
if length > 0.001:
mesh_inst.look_at(end, Vector3.UP)
# Collision
var col = CollisionShape3D.new()
var shape = BoxShape3D.new()
shape.size = box.size
col.shape = shape
col.position = mid
col.rotation = mesh_inst.rotation
add_child(col)
func _build_plate_mesh():
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var half_thick = thickness / 2.0
var top_verts = []
var bottom_verts = []
# UV Calculation Helpers
var min_v = Vector2(INF, INF)
var max_v = Vector2(-INF, -INF)
for v in vertices:
min_v.x = min(min_v.x, v.x)
min_v.y = min(min_v.y, v.y)
max_v.x = max(max_v.x, v.x)
max_v.y = max(max_v.y, v.y)
var size_v = max_v - min_v
for v in vertices:
top_verts.append(Vector3(v.x, v.y, half_thick))
bottom_verts.append(Vector3(v.x, v.y, -half_thick))
# --- 1. Top Face (Inside - Red Channel) ---
st.set_color(Color(1, 0, 0, 1))
st.set_normal(Vector3.BACK)
# Fan Triangulation
for i in range(1, top_verts.size() - 1):
_add_vertex_with_uv(st, top_verts[0], min_v, size_v)
_add_vertex_with_uv(st, top_verts[i], min_v, size_v)
_add_vertex_with_uv(st, top_verts[i+1], min_v, size_v)
# --- 2. Bottom Face (Outside - Blue Channel) ---
st.set_color(Color(0, 0, 1, 1))
st.set_normal(Vector3.FORWARD)
for i in range(1, bottom_verts.size() - 1):
_add_vertex_with_uv(st, bottom_verts[0], min_v, size_v)
_add_vertex_with_uv(st, bottom_verts[i+1], min_v, size_v)
_add_vertex_with_uv(st, bottom_verts[i], min_v, size_v)
# --- 3. Side Faces (Sides - Black) ---
st.set_color(Color(0, 0, 0, 1))
for i in range(vertices.size()):
var next_i = (i + 1) % vertices.size()
var v1 = top_verts[i]
var v2 = top_verts[next_i]
var v3 = bottom_verts[next_i]
var v4 = bottom_verts[i]
var side_vec = (v2 - v1).normalized()
var normal = side_vec.cross(Vector3.BACK).normalized()
st.set_normal(normal)
# Sides don't need specific UVs for the margin shader
st.set_uv(Vector2.ZERO)
st.add_vertex(v1); st.add_vertex(v2); st.add_vertex(v4)
st.add_vertex(v2); st.add_vertex(v3); st.add_vertex(v4)
var mesh_inst = MeshInstance3D.new()
mesh_inst.mesh = st.commit()
_apply_material(mesh_inst)
add_child(mesh_inst)
# Generate Labels
_generate_serial_labels(half_thick)
# Collision (Convex)
# For collision, we just feed ALL the points (top and bottom) to the convex hull generator.
var col = CollisionShape3D.new()
var shape = ConvexPolygonShape3D.new()
var all_points = top_verts.duplicate()
all_points.append_array(bottom_verts)
shape.points = all_points
col.shape = shape
add_child(col)
func _add_vertex_with_uv(st: SurfaceTool, v: Vector3, min_v: Vector2, size_v: Vector2):
var uv = Vector2(
(v.x - min_v.x) / size_v.x,
(v.y - min_v.y) / size_v.y
)
st.set_uv(uv)
st.add_vertex(v)
func _apply_material(mesh_inst: MeshInstance3D):
if is_preview:
var mat = StandardMaterial3D.new()
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.albedo_color = Color(0.2, 0.6, 1.0, 0.4) # Semi-transparent Blue
mat.emission_enabled = true
mat.emission = Color(0.2, 0.6, 1.0)
mat.emission_energy_multiplier = 1.0
mat.cull_mode = BaseMaterial3D.CULL_DISABLED # See inside
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED # Glowy
# super._apply_material(mesh_inst) # Use simple transparent blue
mesh_inst.material_override = mat
else:
var mat = ShaderMaterial.new()
# Use Shader Material
mat.shader = PLATE_SHADER
mesh_inst.material_override = mat
func _generate_serial_labels(offset_z: float):
# Generate a mock serial based on instance ID or data
var serial_text = "IND-%d\nMDL-%s" % [get_instance_id() % 10000, structure_data.shape.left(3).to_upper()]
# Top Label (Inside)
var label_in = Label3D.new()
label_in.text = serial_text
label_in.font_size = 12
label_in.position = Vector3(0, 0, offset_z + 0.005) # Slight offset to avoid Z-fighting
label_in.pixel_size = 0.002
label_in.modulate = Color(0.5, 0.5, 0.5, 0.8)
label_in.rotation_degrees = Vector3(0, 0, 0)
add_child(label_in)
# Bottom Label (Outside)
var label_out = Label3D.new()
label_out.text = serial_text
label_out.font_size = 12
label_out.position = Vector3(0, 0, -offset_z - 0.005)
label_out.pixel_size = 0.002
label_out.modulate = Color(0.3, 0.3, 0.3, 0.8)
label_out.rotation_degrees = Vector3(180, 0, 0) # Face down
add_child(label_out)
# Override the parent's update function to use our specific material logic
func _update_preview_visuals():
for child in get_children():
if child is MeshInstance3D:
_apply_material(child)