Working straight angle placement of ship pieces
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
.vscode/
|
||||
/android/
|
||||
|
||||
*.tmp
|
||||
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"godotTools.editorPath.godot4": "./godot_engine/bin/godot.windows.editor.double.x86_64.console.exe",
|
||||
"search.exclude": {
|
||||
"/godot_engine": true
|
||||
}
|
||||
}
|
||||
61
README.md
61
README.md
@ -29,7 +29,6 @@ Godot requires a C++ compiler to build from source.
|
||||
* Install **Xcode** from the App Store.
|
||||
* Run `xcode-select --install` in the terminal.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Setup & Compilation
|
||||
|
||||
@ -93,4 +92,62 @@ Import and open the `project.godot` file located in the root `ProjectMillimeters
|
||||
### Troubleshooting
|
||||
"No valid compilers found" (Windows): Ensure you installed the C++ Desktop Development workload in the Visual Studio Installer. Just the editor is not enough.
|
||||
|
||||
Jittering Objects: If objects jitter at large distances, ensure you are running the double precision binary and not a standard build.
|
||||
Jittering Objects: If objects jitter at large distances, ensure you are running the double precision binary and not a standard build.
|
||||
|
||||
## 🛠 Compiling Custom Export Templates
|
||||
|
||||
This project uses features from the latest development branch of Godot (`master` branch). As a result, standard export templates downloaded from the Godot website may not be compatible. To export the project, you must compile custom export templates from the same source version used to build your editor.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have a C++ build environment set up (SCons, Python, Visual Studio/GCC/MinGW). See the official Godot documentation on compiling for platform-specific instructions.
|
||||
|
||||
### 1. Build the Editor (Optional)
|
||||
|
||||
See above section.
|
||||
|
||||
### 2. Build the Export Templates
|
||||
|
||||
You need to build two templates: one for debug (used during development/testing) and one for release (optimized for final distribution).
|
||||
|
||||
**Windows:**
|
||||
```PowerShell
|
||||
# Debug Template (console enabled, debug tools)
|
||||
scons platform=windows target=template_debug
|
||||
|
||||
# Release Template (optimized, no console)
|
||||
scons platform=windows target=template_release
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
# Debug Template
|
||||
scons platform=linuxbsd target=template_debug
|
||||
|
||||
# Release Template
|
||||
scons platform=linuxbsd target=template_release
|
||||
```
|
||||
|
||||
### 3. Locate Output Files
|
||||
|
||||
After compilation, the binaries will be located in the bin/ directory of your Godot source folder.
|
||||
|
||||
- Debug: godot.windows.template_debug.x86_64.exe (or similar)
|
||||
|
||||
- Release: godot.windows.template_release.x86_64.exe (or similar)
|
||||
|
||||
### 4. Configure Export Presets
|
||||
|
||||
1. Open the project in Godot.
|
||||
|
||||
2. Go to Project > Export.
|
||||
|
||||
3. Select your export preset (e.g., Windows Desktop).
|
||||
|
||||
4. Under the Options tab, find the Custom Template section.
|
||||
|
||||
5. Set Debug to the path of your compiled template_debug binary.
|
||||
|
||||
6. Set Release to the path of your compiled template_release binary.
|
||||
|
||||
You can now export the project using your custom engine build!
|
||||
1
godot_engine
Submodule
1
godot_engine
Submodule
Submodule godot_engine added at 8a2b782c42
13
reinit_submodules.sh
Normal file
13
reinit_submodules.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
git config -f .gitmodules --get-regexp '^submodule\..*\.path$' |
|
||||
while read path_key local_path
|
||||
do
|
||||
url_key=$(echo $path_key | sed 's/\.path/.url/')
|
||||
url=$(git config -f .gitmodules --get "$url_key")
|
||||
git submodule add $url $local_path
|
||||
done
|
||||
|
||||
# https://stackoverflow.com/questions/11258737/restore-git-submodules-from-gitmodules
|
||||
@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@ -12,6 +12,7 @@ var _mouse_motion_input: Vector2 = Vector2.ZERO
|
||||
var active_preview_piece: ProceduralPiece = null
|
||||
var active_structure_data: StructureData = null
|
||||
var build_mode_enabled: bool = false
|
||||
var is_snap_valid: bool = false # Track if we are currently snapped
|
||||
|
||||
# Resources
|
||||
const SQUARE_RES = preload("res://data/structure/definitions/1m_square_plate.tres")
|
||||
@ -138,44 +139,63 @@ func _select_piece(index: int):
|
||||
|
||||
func _update_preview_transform():
|
||||
if not is_instance_valid(possessed_pawn): return
|
||||
|
||||
is_snap_valid = false # Reset snap state
|
||||
|
||||
# 1. Cast Ray from Camera
|
||||
var cam = possessed_pawn.camera
|
||||
var space_state = possessed_pawn.get_world_3d().direct_space_state
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
var from = cam.project_ray_origin(mouse_pos)
|
||||
var to = from + cam.project_ray_normal(mouse_pos) * 10.0 # 10m reach
|
||||
|
||||
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 1 << 14 # Layer 15 (Weld/Structure)
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
|
||||
if result:
|
||||
var collider = result.collider
|
||||
# Try to find the Module or Piece
|
||||
var dir = cam.project_ray_normal(mouse_pos)
|
||||
|
||||
# --- NEW: Use SnappingTool to perform the physics sweep ---
|
||||
# This replaces the simple raycast with a thick cylinder cast
|
||||
var hit_result = SnappingTool.find_snap_target(space_state, from, dir, 10.0, 0.2)
|
||||
|
||||
if not hit_result.is_empty():
|
||||
var collider = hit_result["collider"]
|
||||
var hit_pos = hit_result["position"]
|
||||
|
||||
var target_module: Module = null
|
||||
if collider is StructuralPiece:
|
||||
target_module = collider.get_root_module()
|
||||
elif collider is Module:
|
||||
target_module = collider
|
||||
if collider is PieceMount:
|
||||
# If we hit a mount, get its piece and its module, get its module
|
||||
print(collider.get_parent())
|
||||
if collider.get_parent() is StructuralPiece:
|
||||
target_module = collider.get_parent().get_parent()
|
||||
if collider.owner is Module: target_module = collider.owner
|
||||
elif collider.get_parent() is Module: target_module = collider.get_parent()
|
||||
|
||||
if target_module:
|
||||
# Call Snapping Tool
|
||||
# Attempt Snap using the hit position
|
||||
var snap_transform = SnappingTool.get_best_snap_transform(
|
||||
active_structure_data,
|
||||
target_module,
|
||||
result.position, # Ray hit position
|
||||
(to - from).normalized()
|
||||
hit_pos, # Ray hit position
|
||||
)
|
||||
active_preview_piece.global_transform = snap_transform
|
||||
return
|
||||
print(snap_transform)
|
||||
# If the transform has changed significantly from the hit pos, it means a snap occurred.
|
||||
# (Simple heuristic: check distance from hit to new origin)
|
||||
if snap_transform.origin.distance_to(hit_pos) < SnappingTool.SNAP_DISTANCE:
|
||||
active_preview_piece.global_transform = snap_transform
|
||||
is_snap_valid = true
|
||||
_update_preview_color(Color.GREEN)
|
||||
return
|
||||
|
||||
# Fallback: Float in front of player
|
||||
var float_pos = from + cam.project_ray_normal(mouse_pos) * 3.0
|
||||
var float_pos = from + dir * 3.0
|
||||
active_preview_piece.global_position = float_pos
|
||||
# Orient to face camera roughly
|
||||
active_preview_piece.look_at(cam.global_position, Vector3.UP)
|
||||
_update_preview_color(Color.CYAN) # Cyan = Floating
|
||||
|
||||
func _update_preview_color(color: Color):
|
||||
if not is_instance_valid(active_preview_piece): return
|
||||
|
||||
var mesh_inst = active_preview_piece.find_child("MeshInstance3D")
|
||||
if mesh_inst and mesh_inst.material_override:
|
||||
var mat = mesh_inst.material_override as StandardMaterial3D
|
||||
mat.albedo_color = Color(color.r, color.g, color.b, 0.4)
|
||||
mat.emission = color
|
||||
|
||||
func _place_piece():
|
||||
if not active_preview_piece or not active_structure_data: return
|
||||
@ -206,7 +226,7 @@ func server_request_place_piece(resource_path: String, transform: Transform3D):
|
||||
# Logic to create a new module could go here
|
||||
print("No module nearby to attach piece.")
|
||||
var new_module = Module.new()
|
||||
new_module.name = "Module_%d" % get_process_delta_time() # Unique name
|
||||
new_module.name = "Module_%s" % get_process_delta_time() # Unique name
|
||||
possessed_pawn.get_parent().add_child(new_module)
|
||||
new_module.global_position = query_pos
|
||||
module = new_module
|
||||
@ -224,12 +244,33 @@ func server_request_place_piece(resource_path: String, transform: Transform3D):
|
||||
|
||||
# Helper to find modules on server side (uses global overlap check)
|
||||
func _find_module_near_server(pos: Vector3) -> Module:
|
||||
# Simple distance check against all modules in the system data
|
||||
# A better way uses a Physics Shape Query
|
||||
if GameManager.current_star_system and GameManager.current_star_system.system_data:
|
||||
for ship in GameManager.current_star_system.system_data.ships:
|
||||
if ship is Module and ship.global_position.distance_to(pos) < 10.0: # Arbitrary range
|
||||
return ship
|
||||
# Create a sphere query parameters object
|
||||
var space_state = possessed_pawn.get_world_3d().direct_space_state
|
||||
var params = PhysicsShapeQueryParameters3D.new()
|
||||
var shape = SphereShape3D.new()
|
||||
shape.radius = 5.0 # Search radius
|
||||
params.shape = shape
|
||||
params.transform = Transform3D(Basis(), pos)
|
||||
params.collide_with_bodies = true
|
||||
params.collision_mask = 0xFFFFFFFF # Check all layers, or specific module layer
|
||||
|
||||
var results = space_state.intersect_shape(params)
|
||||
|
||||
for result in results:
|
||||
var collider = result["collider"]
|
||||
|
||||
# Case 1: We hit the Module directly (RigidBody3D)
|
||||
if collider is Module:
|
||||
return collider
|
||||
|
||||
# Case 2: We hit a StructuralPiece attached to a Module
|
||||
if collider is StructuralPiece:
|
||||
# StructuralPiece should have a way to get its root module
|
||||
if collider.get_parent() is Module:
|
||||
return collider.get_parent()
|
||||
# Or if you use the helper:
|
||||
# return collider.get_root_module()
|
||||
|
||||
return null
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
|
||||
12
src/scenes/ship/builder/pieces/piece_mount.gd
Normal file
12
src/scenes/ship/builder/pieces/piece_mount.gd
Normal file
@ -0,0 +1,12 @@
|
||||
class_name PieceMount extends Area3D
|
||||
|
||||
# Define compatibility types (0=Universal, 1=Strut, 2=Plate, etc.)
|
||||
@export var mount_type: int = 0
|
||||
|
||||
# You could add other metadata here (e.g., is_occupied)
|
||||
var is_occupied: bool = false
|
||||
|
||||
func _ready():
|
||||
# Ensure this is on the correct layer for snapping queries
|
||||
collision_layer = 1 << 14
|
||||
collision_mask = 0 # Mounts don't need to scan for things, things scan for them
|
||||
1
src/scenes/ship/builder/pieces/piece_mount.gd.uid
Normal file
1
src/scenes/ship/builder/pieces/piece_mount.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dfnsf5gg805k2
|
||||
@ -1,22 +1,11 @@
|
||||
class_name StructuralPiece extends ShipPiece
|
||||
|
||||
# The definition of this piece.
|
||||
@export var structure_data: StructureData = null:
|
||||
set(value):
|
||||
structure_data = value
|
||||
print("StructuralPiece assigned StructureData: %s" % structure_data.piece_name if structure_data else "null")
|
||||
print(structure_data)
|
||||
if structure_data:
|
||||
on_structure_data_update()
|
||||
@export var structure_data: StructureData
|
||||
|
||||
func on_structure_data_update():
|
||||
# Placeholder: In a real implementation, this would create the MeshInstance3D
|
||||
# and CollisionShape3D based on 'structure_data'.
|
||||
pass
|
||||
|
||||
# Track who we are welde d to so we don't double-weld
|
||||
# Track who we are welded to so we don't double-weld
|
||||
var connected_neighbors: Array[StructuralPiece] = []
|
||||
var mount_areas: Array[Area3D] = []
|
||||
var mount_areas: Array[PieceMount] = []
|
||||
|
||||
# Flag to distinguish between a preview piece (no physics) and a placed piece
|
||||
var is_preview: bool = false:
|
||||
@ -45,14 +34,15 @@ func _ready():
|
||||
_generate_mount_detectors()
|
||||
|
||||
# Attempt to weld to anything we are already touching
|
||||
# _initial_weld_scan()
|
||||
_initial_weld_scan()
|
||||
|
||||
func _generate_mount_detectors():
|
||||
for i in range(structure_data.mounts.size()):
|
||||
var mount_def = structure_data.mounts[i]
|
||||
|
||||
var area = Area3D.new()
|
||||
var area = PieceMount.new() # Uses the class_name
|
||||
area.name = "Mount_%d" % i
|
||||
area.mount_type = mount_def.get("type", 0) # Set the type!
|
||||
add_child(area)
|
||||
|
||||
# Position
|
||||
@ -62,7 +52,6 @@ func _generate_mount_detectors():
|
||||
var normal = mount_def.get("normal", Vector3.BACK)
|
||||
var up = mount_def.get("up", Vector3.UP)
|
||||
|
||||
# Safety check for parallel vectors
|
||||
if normal.cross(up).is_zero_approx():
|
||||
up = Vector3.RIGHT if abs(normal.y) > 0.9 else Vector3.UP
|
||||
|
||||
@ -72,11 +61,11 @@ func _generate_mount_detectors():
|
||||
# Shape
|
||||
var shape = CollisionShape3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = 0.15 # Small tolerance radius
|
||||
sphere.radius = 0.15
|
||||
shape.shape = sphere
|
||||
area.add_child(shape)
|
||||
|
||||
# Collision Layers (Use Layer 15 "Weld" = bit 14)
|
||||
# Layer setup is handled in PieceMount._ready() now, or explicit here:
|
||||
area.collision_layer = 1 << 14
|
||||
area.collision_mask = 1 << 14
|
||||
area.monitoring = true
|
||||
@ -103,24 +92,16 @@ func _scan_and_weld_neighbors():
|
||||
_create_weld_to(other_piece)
|
||||
|
||||
func _create_weld_to(neighbor: StructuralPiece):
|
||||
# 1. Create the Joint
|
||||
var joint = Generic6DOFJoint3D.new()
|
||||
|
||||
# 2. Configure as a rigid lock
|
||||
_lock_joint_axis(joint)
|
||||
# 3. Add to scene (Joints should generally be peers or children of the system root)
|
||||
# Adding it to 'self' is fine; if 'self' dies, the joint breaks, which is correct.
|
||||
add_child(joint)
|
||||
|
||||
# 4. Position joint at the midpoint (visual/physics anchor)
|
||||
# (Optional, but good for stability)
|
||||
# Position joint halfway between pieces
|
||||
joint.global_position = (self.global_position + neighbor.global_position) / 2.0
|
||||
|
||||
# 5. Connect Bodies
|
||||
joint.node_a = self.get_path()
|
||||
joint.node_b = neighbor.get_path()
|
||||
|
||||
# 6. Record connection on BOTH sides
|
||||
|
||||
connected_neighbors.append(neighbor)
|
||||
neighbor.connected_neighbors.append(self)
|
||||
|
||||
@ -129,7 +110,6 @@ func _lock_joint_axis(joint: Generic6DOFJoint3D):
|
||||
joint.set_param_x(axis, 0.0)
|
||||
joint.set_param_y(axis, 0.0)
|
||||
joint.set_param_z(axis, 0.0)
|
||||
|
||||
for axis in [Generic6DOFJoint3D.PARAM_ANGULAR_LOWER_LIMIT, Generic6DOFJoint3D.PARAM_ANGULAR_UPPER_LIMIT]:
|
||||
joint.set_param_x(axis, 0.0)
|
||||
joint.set_param_y(axis, 0.0)
|
||||
@ -146,4 +126,4 @@ func _update_preview_visuals():
|
||||
func _exit_tree():
|
||||
for neighbor in connected_neighbors:
|
||||
if is_instance_valid(neighbor):
|
||||
neighbor.connected_neighbors.erase(self)
|
||||
neighbor.connected_neighbors.erase(self)
|
||||
@ -3,91 +3,158 @@ 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,
|
||||
ray_origin: Vector3,
|
||||
ray_direction: Vector3
|
||||
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 all world-space mounts from the module's existing pieces.
|
||||
# Optimization: The Module could cache this list.
|
||||
# 1. Harvest world-space mounts from the module
|
||||
var world_target_mounts = []
|
||||
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. Determine where the player is pointing (Project ray out)
|
||||
var cursor_pos = ray_origin + ray_direction * 4.0 # Default 4m reach if no snap
|
||||
# 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))
|
||||
|
||||
# 3. Find the BEST pair of mounts (New Mount <-> Target Mount)
|
||||
# 2. Find the BEST pair of mounts
|
||||
for new_mount in piece_data.mounts:
|
||||
var local_mount_pos = new_mount.get("position", Vector3.ZERO)
|
||||
var local_mount_norm = new_mount.get("normal", Vector3.BACK)
|
||||
|
||||
# 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. Check Distance to cursor
|
||||
# We check distance between target mount and cursor to see if user is "aiming" at it
|
||||
var dist_to_cursor = cursor_pos.distance_to(target_mount.position)
|
||||
|
||||
# Optimization: If user is too far from this target mount, skip detailed math
|
||||
# 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
|
||||
|
||||
# B. Check if this is the closest snap so far
|
||||
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)
|
||||
|
||||
# C. Calculate Orientation
|
||||
# We want the New Piece's Mount Normal to oppose the Target Mount Normal
|
||||
# Target Normal points OUT of the ship. New Normal points OUT of the piece.
|
||||
# When connected, they point at each other (180 degrees apart).
|
||||
# D. Calculate Final Piece Transform
|
||||
# Piece_World = Target_Mount_World * Mount_Local.inverse()
|
||||
|
||||
var desired_normal = -target_mount.normal # The direction we want our mount to face
|
||||
best_transform = target_world_transform * mount_local_transform.affine_inverse()
|
||||
|
||||
# Calculate rotation to align 'local_mount_norm' to 'desired_normal'
|
||||
var rot_quat = _get_rotation_between_vectors(local_mount_norm, desired_normal)
|
||||
var final_basis = Basis(rot_quat)
|
||||
|
||||
# D. Calculate Position
|
||||
# We know where the target mount is (Target_Pos).
|
||||
# We need to place the Piece Origin such that:
|
||||
# Piece_Origin + (Rotated_Mount_Offset) = Target_Pos
|
||||
# Therefore: Piece_Origin = Target_Pos - (Rotated_Mount_Offset)
|
||||
var rotated_offset = final_basis * local_mount_pos
|
||||
var final_pos = target_mount.position - rotated_offset
|
||||
|
||||
best_transform = Transform3D(final_basis, final_pos)
|
||||
best_dist = dist_to_cursor
|
||||
found_snap = true
|
||||
|
||||
if found_snap:
|
||||
return best_transform
|
||||
else:
|
||||
# Fallback: Just place floating at cursor
|
||||
return Transform3D(Basis(), cursor_pos)
|
||||
|
||||
# Helper: Calculate quaternion to rotate vector A to vector B
|
||||
static func _get_rotation_between_vectors(v1: Vector3, v2: Vector3) -> Quaternion:
|
||||
v1 = v1.normalized()
|
||||
v2 = v2.normalized()
|
||||
# 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
|
||||
|
||||
# If vectors are parallel (same direction)
|
||||
if v1.dot(v2) > 0.999:
|
||||
return Quaternion.IDENTITY
|
||||
|
||||
# If vectors are opposite (180 degrees)
|
||||
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:
|
||||
# We need any axis perpendicular to v1 to rotate 180 degrees around
|
||||
var axis = v1.cross(Vector3.UP)
|
||||
if axis.length_squared() < 0.01:
|
||||
axis = v1.cross(Vector3.RIGHT)
|
||||
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
|
||||
|
||||
@ -98,7 +98,7 @@ shape = SubResource("CylinderShape3D_nvgim")
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D8"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.016562464, 0.05784607, -0.0040130615)
|
||||
transform = Transform3D(0.9999999999999997, 0, 0, 0, 0.9999999999999997, 0, 0, 0, 1, 0.09530011764134061, 0.10799497165389675, -0.025631436817796976)
|
||||
mesh = SubResource("CylinderMesh_nvgim")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D8"]
|
||||
@ -164,4 +164,4 @@ shape = SubResource("CylinderShape3D_nvgim")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.309784, 0, -12.84836)
|
||||
|
||||
[node name="CharacterPawn3D" parent="." instance=ExtResource("3_7yxt7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.12362689122544, -0.49999999999999645, -11.643245313710281)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.12362689122544, -0.4999999999999965, -11.64324531371028)
|
||||
|
||||
Reference in New Issue
Block a user