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

161 lines
5.3 KiB
GDScript

class_name SnappingTool extends RefCounted
const SNAP_DISTANCE = 2.0 # Meters
const SNAP_ANGLE_THRESHOLD = deg_to_rad(45.0)
# Define the collision mask for mounts/structure.
const SNAP_COLLISION_MASK = 1 << 14
## Performs a shape cast to find the best candidate for snapping.
## Returns a Dictionary with { "position": Vector3, "normal": Vector3, "collider": Node } or null.
static func find_snap_target(
space_state: PhysicsDirectSpaceState3D,
ray_origin: Vector3,
ray_direction: Vector3,
reach_distance: float = 10.0,
radius: float = 0.2
) -> Dictionary:
var shape = SphereShape3D.new()
shape.radius = radius
var params = PhysicsShapeQueryParameters3D.new()
params.shape = shape
params.transform = Transform3D(Basis(), ray_origin)
params.motion = ray_direction * reach_distance
params.collision_mask = SNAP_COLLISION_MASK
params.collide_with_areas = true
params.collide_with_bodies = true
# 2. Cast the shape
var result = space_state.cast_motion(params)
# cast_motion returns [safe_fraction, unsafe_fraction]
# If safe_fraction is 1.0, we hit nothing.
if result[1] >= 1.0:
return {} # No hit
# 3. Get the collision details
# cast_motion doesn't return the collider, so we need to use get_rest_info
# at the collision point to find WHAT we hit.
# Calculate hit position
var hit_fraction = result[1]
var hit_pos = ray_origin + (ray_direction * reach_distance * hit_fraction)
params.transform.origin = hit_pos
params.motion = Vector3.ZERO
var intersection = space_state.intersect_shape(params, 1)
if intersection.is_empty():
return {}
var collider = intersection[0]["collider"]
# --- NEW: Filter for PieceMount class ---
# If we hit a PieceMount, we return it directly.
if collider is PieceMount:
return {
"position": hit_pos,
"collider": collider,
"mount": collider # Pass the strong reference
}
return {
"position": hit_pos,
"collider": collider
}
## Returns a Transform3D for the 'piece_to_place' that aligns one of its mounts
## with a compatible mount on 'target_module'.
static func get_best_snap_transform(
piece_data: StructureData,
target_module: Module,
cursor_pos: Vector3,
target_mount_node: PieceMount = null # OPTIONAL: Specific target mount
) -> Transform3D:
var best_dist = INF
var best_transform = Transform3D()
var found_snap = false
# 1. Harvest world-space mounts from the module
var world_target_mounts = []
# Optimization: If we already know the target mount node (from find_snap_target), use ONLY that one.
if target_mount_node:
world_target_mounts.append(_get_mount_data_from_node(target_mount_node))
else:
# Fallback: Search all
for child in target_module.get_structural_pieces():
if child is StructuralPiece and child.structure_data:
world_target_mounts.append_array(child.structure_data.get_mounts_transformed(child.global_transform))
# 2. Find the BEST pair of mounts
for new_mount in piece_data.mounts:
# Construct the Local Transform of the NEW mount
var local_pos = new_mount.get("position", Vector3.ZERO)
var local_norm = new_mount.get("normal", Vector3.BACK)
var local_up = new_mount.get("up", Vector3.UP)
var local_type = new_mount.get("type", 0)
var mount_local_transform = Transform3D(Basis.looking_at(local_norm, local_up), local_pos)
for target_mount in world_target_mounts:
# A. Type Compatibility Check
if target_mount.type != local_type:
continue
# B. Distance Check
# If we provided a specific target mount, distance is less relevant (we already aimed at it),
# but we still check to ensure the preview doesn't jump wildly if the mounts are far apart.
var dist_to_cursor = cursor_pos.distance_to(target_mount.position)
if dist_to_cursor > SNAP_DISTANCE: continue
if dist_to_cursor < best_dist:
# C. Construct Target World Transform
# We want the NEW mount to face OPPOSITE to the TARGET mount.
var target_basis = Basis.looking_at(-target_mount.normal, target_mount.up)
var target_world_transform = Transform3D(target_basis, target_mount.position)
# D. Calculate Final Piece Transform
# Piece_World = Target_Mount_World * Mount_Local.inverse()
best_transform = target_world_transform * mount_local_transform.affine_inverse()
best_dist = dist_to_cursor
found_snap = true
if found_snap:
return best_transform
else:
return Transform3D(Basis(), cursor_pos)
# Helper to extract data dictionary from a runtime PieceMount node
static func _get_mount_data_from_node(mount_node: PieceMount) -> Dictionary:
# We extract the transform from the node itself
var t = mount_node.global_transform
# Forward (Normal) is -Z, Up is +Y
var normal = -t.basis.z
var up = t.basis.y
return {
"position": t.origin,
"normal": normal,
"up": up,
"type": 0 # TODO: PieceMount needs a 'type' property!
}
static func _get_rotation_between_vectors(v1: Vector3, v2: Vector3) -> Quaternion:
v1 = v1.normalized(); v2 = v2.normalized()
if v1.dot(v2) > 0.999: return Quaternion.IDENTITY
if v1.dot(v2) < -0.999:
var axis = v1.cross(Vector3.UP)
if axis.length_squared() < 0.01: axis = v1.cross(Vector3.RIGHT)
return Quaternion(axis.normalized(), PI)
var cross = v1.cross(v2)
var dot = v1.dot(v2)
var w = sqrt(v1.length_squared() * v2.length_squared()) + dot
return Quaternion(cross.x, cross.y, cross.z, w).normalized()