extends Sprite3D const SLIDE_DURATION := 0.25 const UP_DURATION := 0.1 const HOLD_DURATION := 0.05 const DOWN_DURATION := 0.18 var _open_button: Node3D var _close_button: Node3D var _target_button: Node3D var _close_rest_x: float var _open_rest_x: float var _rest_y: float var _slide_tween: Tween var _press_tween: Tween func _ready() -> void: if not get_parent().is_node_ready(): await get_parent().ready await get_tree().process_frame _open_button = get_parent().get_node("DoorOpen") _close_button = get_parent().get_node("DoorClose") _close_rest_x = _project_to_hand_plane(_close_button) _open_rest_x = _project_to_hand_plane(_open_button) _rest_y = position.y _target_button = _close_button position = Vector3(_close_rest_x, _rest_y, position.z) print("[VirtuaHand] close_x=", _close_rest_x, " open_x=", _open_rest_x, " hand_pos=", position) EventBus.button_pressed.connect(_animate_press) EventBus.active_button_changed.connect(_on_active_button_changed) func _project_to_hand_plane(button: Node3D) -> float: var cam := get_viewport().get_camera_3d() if not cam: return button.position.x var screen_pos := cam.unproject_position(button.global_position) var ray_origin := cam.project_ray_origin(screen_pos) var ray_dir := cam.project_ray_normal(screen_pos) if absf(ray_dir.z) < 0.0001: return button.position.x var t := (global_position.z - ray_origin.z) / ray_dir.z var hit := ray_origin + ray_dir * t return get_parent().to_local(hit).x func _on_active_button_changed(target: String) -> void: var new_button: Node3D = _open_button if target == "open" else _close_button if new_button == _target_button: return _target_button = new_button var target_x: float = _open_rest_x if target == "open" else _close_rest_x print("[VirtuaHand] slide target=", target, " target_x=", target_x, " current_pos=", position) if _press_tween and _press_tween.is_valid(): _press_tween.kill() if _slide_tween and _slide_tween.is_valid(): _slide_tween.kill() _slide_tween = create_tween() _slide_tween.tween_property(self, "position:x", target_x, SLIDE_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) func _animate_press() -> void: if not is_instance_valid(_target_button): return if _press_tween and _press_tween.is_valid(): _press_tween.kill() var fingertip_offset := texture.get_height() * 0.5 * pixel_size * scale.y var press_y := _target_button.position.y - fingertip_offset _press_tween = create_tween() _press_tween.tween_property(self, "position:y", press_y, UP_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) _press_tween.tween_interval(HOLD_DURATION) _press_tween.tween_property(self, "position:y", _rest_y, DOWN_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)