176 lines
4.8 KiB
GDScript
176 lines
4.8 KiB
GDScript
@tool
|
|
class_name XRToolsSnapPath
|
|
extends XRToolsSnapZone
|
|
|
|
|
|
## An [XRToolsSnapZone] that allows [XRToolsPickable] to be placed along a
|
|
## child [Path3D] node. They can either be placed along any point in the curve
|
|
## or at discrete intervals by setting "snap_interval" above '0.0'.
|
|
##
|
|
## Note: Attached [XRToolsPickable]s will face the +Z axis.
|
|
|
|
|
|
## Real world distance between intervals in Meters.
|
|
## Enabled when not 0
|
|
@export var snap_interval := 0.0:
|
|
set(v): snap_interval = absf(v)
|
|
|
|
@onready var path : Path3D
|
|
|
|
|
|
func _ready() -> void:
|
|
super._ready()
|
|
|
|
for c in get_children():
|
|
if c is Path3D:
|
|
path = c
|
|
break
|
|
|
|
|
|
func has_snap_interval() -> bool:
|
|
return !is_equal_approx(snap_interval, 0.0)
|
|
|
|
|
|
func _get_configuration_warnings() -> PackedStringArray:
|
|
# Check for Path3D child
|
|
for c in get_children():
|
|
if c is Path3D:
|
|
path = c
|
|
return[]
|
|
return["This node requires a Path3D child node to define its shape."]
|
|
|
|
|
|
# Called when a target in our grab area is dropped
|
|
func _on_target_dropped(target: Node3D) -> void:
|
|
# Skip if invalid
|
|
if !enabled or !path or !target.can_pick_up(self) or \
|
|
!is_instance_valid(target) or \
|
|
is_instance_valid(picked_up_object):
|
|
return
|
|
|
|
# Make a zone that will destruct once its object has left
|
|
var zone = _make_temp_zone()
|
|
var offset = _find_offset(path, target.global_position)
|
|
|
|
# if snap guide
|
|
if _has_snap_guide(target):
|
|
# comply with guide
|
|
offset = _find_closest_offset_with_length(path.curve, offset, _get_snap_guide(target).length)
|
|
|
|
# too large to place on path
|
|
if is_equal_approx(offset, -1.0):
|
|
return
|
|
|
|
# if snap_interval has been set, use it
|
|
if has_snap_interval():
|
|
offset = snappedf(offset, snap_interval)
|
|
|
|
# set position
|
|
zone.position = path.curve.sample_baked(offset)
|
|
|
|
# Add zone as a child
|
|
path.add_child(zone)
|
|
zone.owner = path
|
|
|
|
# Connect self-destruct with lambda
|
|
zone.has_dropped.connect(func(): zone.queue_free(), Object.ConnectFlags.CONNECT_ONE_SHOT)
|
|
|
|
# Use Pickable's Shapes as our Shapes
|
|
for c in target.get_children():
|
|
if c is CollisionShape3D:
|
|
PhysicsServer3D.area_add_shape(zone.get_rid(), c.shape.get_rid(), c.transform)
|
|
|
|
# Force pickup
|
|
zone.pick_up_object(target)
|
|
|
|
|
|
# Make a zone that dies on dropping objects
|
|
func _make_temp_zone():
|
|
var zone = XRToolsSnapZone.new()
|
|
|
|
# connect lambda to play stash sounds when temp zone picks up
|
|
if has_node("AudioStreamPlayer3D"):
|
|
zone.has_picked_up.connect(\
|
|
func(object):\
|
|
$AudioStreamPlayer3D.stream = stash_sound;\
|
|
$AudioStreamPlayer3D.play()\
|
|
)
|
|
|
|
# XRToolsSnapZone manaul copy
|
|
zone.enabled = true
|
|
zone.stash_sound = stash_sound
|
|
zone.grab_distance = grab_distance
|
|
zone.snap_mode = snap_mode
|
|
zone.snap_require = snap_require
|
|
zone.snap_exclude = snap_exclude
|
|
zone.grab_require = grab_require
|
|
zone.grab_exclude = grab_exclude
|
|
zone.initial_object = NodePath()
|
|
|
|
# CollisionObject3D manual copy
|
|
zone.disable_mode = disable_mode
|
|
zone.collision_layer = collision_layer
|
|
zone.collision_mask = collision_mask
|
|
zone.collision_priority = collision_priority
|
|
|
|
return zone
|
|
|
|
|
|
func _has_snap_guide(target: Node3D) -> bool:
|
|
for c in target.get_children():
|
|
if c is XRToolsSnapPathGuide:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _get_snap_guide(target: Node3D) -> Node3D:
|
|
for c in target.get_children():
|
|
if c is XRToolsSnapPathGuide:
|
|
return c
|
|
return null
|
|
|
|
|
|
# Returns -1 if invalid
|
|
# _offset should be in _curve's local coordinates
|
|
func _find_closest_offset_with_length(_curve: Curve3D, _offset: float, _length: float) -> float:
|
|
# p1 and p2 are the object's start and end respectively
|
|
var p1 = _offset
|
|
var p2 = _offset - _length
|
|
|
|
# a _curve's final point is its end, aka the furthest 'forward', which is why it is p1
|
|
# path_p1 and path_p2 are the curve's start and end respectively
|
|
var path_p1 := _curve.get_closest_offset(_curve.get_point_position(_curve.point_count-1))
|
|
var path_p2 := _curve.get_closest_offset(_curve.get_point_position(0))
|
|
|
|
# if at front (or beyond)
|
|
if is_equal_approx(p1, path_p1):
|
|
# if too large
|
|
if p2 < path_p2:
|
|
return -1
|
|
# if too far back
|
|
elif p2 < path_p2:
|
|
# check if snapping will over-extend
|
|
if has_snap_interval():
|
|
# snapping p1_new may move it further back, and out-of-bounds
|
|
# larger snaps move the object further forward
|
|
var p1_new = path_p2 + _length
|
|
var ideal_snap = snappedf(p1_new, snap_interval)
|
|
var more_snap = _snappedf_up(p1_new, snap_interval)
|
|
# if ideal snap fits, take that
|
|
if ideal_snap >= p1_new:
|
|
return ideal_snap
|
|
return more_snap
|
|
return path_p2 + _length
|
|
# otherwise: within bounds
|
|
return p1
|
|
|
|
|
|
## Round 'x' upwards to the nearest 'step'
|
|
func _snappedf_up(x, step) -> float:
|
|
return step * ceilf(x / step)
|
|
|
|
|
|
func _find_offset(_path: Path3D, _global_position: Vector3) -> float:
|
|
# transform target pos to local space
|
|
var local_pos: Vector3 = _global_position * _path.global_transform
|
|
return _path.curve.get_closest_offset(local_pos)
|