import { useRef } from "react"; import * as THREE from "three"; import { ThreeEvent, useThree } from "@react-three/fiber"; type OnUpdateCallback = (object: THREE.Object3D) => void; export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { const { camera, gl, controls, scene } = useThree(); const activeObjRef = useRef(null); const planeRef = useRef( new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) ); const offsetRef = useRef(new THREE.Vector3()); const initialPositionRef = useRef(new THREE.Vector3()); const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); const handlePointerDown = (e: ThreeEvent) => { e.stopPropagation(); let obj: THREE.Object3D | null = e.object; // Traverse up until we find modelUuid in userData while (obj && !obj.userData?.modelUuid) { obj = obj.parent; } if (!obj) return; // Disable orbit controls while dragging if (controls) (controls as any).enabled = false; activeObjRef.current = obj; initialPositionRef.current.copy(obj.position); // Get world position const objectWorldPos = new THREE.Vector3(); obj.getWorldPosition(objectWorldPos); // Set plane at the object's Y level planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y); // Convert pointer to NDC const rect = gl.domElement.getBoundingClientRect(); pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; // Raycast to intersection raycaster.setFromCamera(pointer, camera); const intersection = new THREE.Vector3(); raycaster.ray.intersectPlane(planeRef.current, intersection); // Calculate offset offsetRef.current.copy(objectWorldPos).sub(intersection); // Start listening for drag gl.domElement.addEventListener("pointermove", handlePointerMove); gl.domElement.addEventListener("pointerup", handlePointerUp); }; const handlePointerMove = (e: PointerEvent) => { if (!activeObjRef.current) return; // Check if Shift key is pressed const isShiftKeyPressed = e.shiftKey; // Get the mouse position relative to the canvas const rect = gl.domElement.getBoundingClientRect(); pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; // Update raycaster to point to the mouse position raycaster.setFromCamera(pointer, camera); // Create a vector to store intersection point const intersection = new THREE.Vector3(); const intersects = raycaster.ray.intersectPlane(planeRef.current, intersection); if (!intersects) return; // Add offset for dragging intersection.add(offsetRef.current); console.log('intersection: ', intersection); // Get the parent's world matrix if exists const parent = activeObjRef.current.parent; const targetPosition = new THREE.Vector3(); if (isShiftKeyPressed) { console.log('isShiftKeyPressed: ', isShiftKeyPressed); // For Y-axis only movement, maintain original X and Z console.log('initialPositionRef: ', initialPositionRef); console.log('intersection.y: ', intersection); targetPosition.set( initialPositionRef.current.x, intersection.y, initialPositionRef.current.z ); } else { // For free movement targetPosition.copy(intersection); } // Convert world position to local if object is nested inside a parent if (parent) { parent.worldToLocal(targetPosition); } // Update object position activeObjRef.current.position.copy(targetPosition); }; const handlePointerUp = () => { if (controls) (controls as any).enabled = true; if (activeObjRef.current) { // Pass the updated position to the onUpdate callback to persist it onUpdate(activeObjRef.current); } gl.domElement.removeEventListener("pointermove", handlePointerMove); gl.domElement.removeEventListener("pointerup", handlePointerUp); activeObjRef.current = null; }; return { handlePointerDown }; }