From a6b31c30c9f2da9ac465305938899179497bf461 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 12 Jun 2026 11:43:58 -0600 Subject: [PATCH] implemented touch controls for the camera --- .../CameraControls/OrbitCameraControls.tsx | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/3dModels/CameraControls/OrbitCameraControls.tsx b/src/3dModels/CameraControls/OrbitCameraControls.tsx index d8e323b..7e098d6 100644 --- a/src/3dModels/CameraControls/OrbitCameraControls.tsx +++ b/src/3dModels/CameraControls/OrbitCameraControls.tsx @@ -73,6 +73,7 @@ export default function OrbitCameraControls(props: Props) { const isPanning = useRef(false); const lastPos = useRef({ x: 0, y: 0 }); const panCameraPosition = useRef(new Vector3()); + const lastPinchDist = useRef(0); // Spherical coords const spherical = useRef({ @@ -222,16 +223,105 @@ export default function OrbitCameraControls(props: Props) { updateCamera(); }; + const handleTouchStart = (e: TouchEvent) => { + if (e.target !== canvas) return; + e.preventDefault(); + + if (e.touches.length === 1) { + isDragging.current = true; + isPanning.current = false; + lastPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + } else if (e.touches.length === 2) { + isDragging.current = false; + isPanning.current = true; + lastPos.current = { + x: (e.touches[0].clientX + e.touches[1].clientX) / 2, + y: (e.touches[0].clientY + e.touches[1].clientY) / 2, + }; + // Store initial pinch distance for zoom + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + lastPinchDist.current = Math.sqrt(dx * dx + dy * dy); + } + }; + + const handleTouchMove = (e: TouchEvent) => { + e.preventDefault(); + + if (e.touches.length === 1 && isDragging.current) { + const deltaX = e.touches[0].clientX - lastPos.current.x; + const deltaY = e.touches[0].clientY - lastPos.current.y; + spherical.current.theta -= deltaX * 0.005; + spherical.current.phi -= deltaY * 0.005; + if (clampVerticalRotation) { + spherical.current.phi = Math.max(minPhi, Math.min(maxPhi, spherical.current.phi)); + } + lastPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + updateCamera(); + + } else if (e.touches.length === 2) { + // Pinch zoom + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const dist = Math.sqrt(dx * dx + dy * dy); + const pinchDelta = lastPinchDist.current - dist; + spherical.current.radius += pinchDelta * 0.05; + spherical.current.radius = Math.max(3, Math.min(maxRadius ?? 30, spherical.current.radius)); + lastPinchDist.current = dist; + + // Two-finger pan + const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; + const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; + const panDeltaX = midX - lastPos.current.x; + const panDeltaY = midY - lastPos.current.y; + + const distance = spherical.current.radius; + const panSpeed = 0.002 * distance; + const forward = new Vector3(); + camera.getWorldDirection(forward); + const right = new Vector3().crossVectors(forward, new Vector3(0, 1, 0)).normalize(); + if (right.lengthSq() < 0.001) right.crossVectors(forward, camera.up).normalize(); + const screenUp = new Vector3().crossVectors(right, forward).normalize(); + right.multiplyScalar(-panDeltaX * panSpeed); + screenUp.multiplyScalar(panDeltaY * panSpeed); + const pan = new Vector3().addVectors(right, screenUp); + targetRef.current.x += pan.x; + targetRef.current.y += pan.y; + targetRef.current.z += pan.z; + + lastPos.current = { x: midX, y: midY }; + updateCamera(); + } + }; + + const handleTouchEnd = (e: TouchEvent) => { + if (e.touches.length === 0) { + isDragging.current = false; + isPanning.current = false; + } else if (e.touches.length === 1) { + // Transitioned from pinch back to single finger + isDragging.current = true; + isPanning.current = false; + lastPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + } + }; + window.addEventListener("mousedown", handleMouseDown); window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); canvas.addEventListener("wheel", handleWheel, { passive: false }); + canvas.addEventListener("touchstart", handleTouchStart, { passive: false }); + canvas.addEventListener("touchmove", handleTouchMove, { passive: false }); + canvas.addEventListener("touchend", handleTouchEnd); return () => { window.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); canvas.removeEventListener("wheel", handleWheel); + canvas.removeEventListener("touchstart", handleTouchStart); + canvas.removeEventListener("touchmove", handleTouchMove); + canvas.removeEventListener("touchend", handleTouchEnd); }; }, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius]);