From e7ab6a2d76f356178f56cbaf05ed9640977a0f91 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 13 Apr 2026 14:46:39 -0600 Subject: [PATCH] working on panning, currently kinda glitchy --- .../CameraControls/OrbitCameraControls.tsx | 119 +++++++++++++++--- 1 file changed, 100 insertions(+), 19 deletions(-) diff --git a/src/3dModels/CameraControls/OrbitCameraControls.tsx b/src/3dModels/CameraControls/OrbitCameraControls.tsx index 20768b4..5d707c3 100644 --- a/src/3dModels/CameraControls/OrbitCameraControls.tsx +++ b/src/3dModels/CameraControls/OrbitCameraControls.tsx @@ -1,5 +1,6 @@ import { useThree } from "@react-three/fiber"; import { useEffect, useRef } from "react"; +import { Plane, Raycaster, Vector2, Vector3 } from "three"; interface CameraTarget { x: number; @@ -25,8 +26,18 @@ interface Props { export default function OrbitCameraControls(props: Props) { const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props; const { camera, gl } = useThree(); + const panPlane = useRef(new Plane()); + const panStartPoint = useRef(new Vector3()); + const raycaster = useRef(new Raycaster()); + const mouse = useRef(new Vector2()); + const targetRef = useRef({ + x: target?.x ?? 0, + y: target?.y ?? 0, + z: target?.z ?? 0, + }); const isDragging = useRef(false); + const isPanning = useRef(false); const lastPos = useRef({ x: 0, y: 0 }); // Spherical coords @@ -38,7 +49,7 @@ export default function OrbitCameraControls(props: Props) { useEffect(() => { const canvas = gl.domElement; - + canvas.addEventListener("contextmenu", e => e.preventDefault()); const updateCamera = () => { const { radius, theta, phi } = spherical.current; @@ -47,41 +58,111 @@ export default function OrbitCameraControls(props: Props) { const z = radius * Math.sin(phi) * Math.cos(theta); camera.position.set(x, y, z); - camera.lookAt(target?.x ?? 0, target?.y ?? 0, target?.z ?? 0); + camera.lookAt( + targetRef.current.x, + targetRef.current.y, + targetRef.current.z + ); }; updateCamera(); const handleMouseDown = (e: MouseEvent) => { if (e.target !== canvas) return; - isDragging.current = true; + lastPos.current = { x: e.clientX, y: e.clientY }; - }; + + // Left click = rotate + if (e.button === 0) { + isDragging.current = true; + isPanning.current = false; + } + + // Right click = pan + if (e.button === 2) { + isPanning.current = true; + isDragging.current = false; - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging.current) return; - - const deltaX = e.clientX - lastPos.current.x; - const deltaY = e.clientY - lastPos.current.y; - - spherical.current.theta -= deltaX * 0.005; - spherical.current.phi -= deltaY * 0.005; - - // Clamp vertical rotation (prevents flipping) - if (clampVerticalRotation) { - spherical.current.phi = Math.max( - minPhi, - Math.min(maxPhi, spherical.current.phi) + // plane facing camera through target + const camDir = new Vector3(); + camera.getWorldDirection(camDir); + + panPlane.current.setFromNormalAndCoplanarPoint( + camDir, + new Vector3( + targetRef.current.x, + targetRef.current.y, + targetRef.current.z + ) + ); + + // starting intersection point + raycaster.current.setFromCamera( + new Vector2( + (e.clientX / window.innerWidth) * 2 - 1, + -(e.clientY / window.innerHeight) * 2 + 1 + ), + camera + ); + + raycaster.current.ray.intersectPlane( + panPlane.current, + panStartPoint.current ); } + }; + + + const handleMouseMove = (e: MouseEvent) => { + const deltaX = e.clientX - lastPos.current.x; + const deltaY = e.clientY - lastPos.current.y; + + // ROTATE + if (isDragging.current) { + 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) + ); + } + } + + // PAN + if (isPanning.current) { + const raycaster = new Raycaster(); + + mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse.current, camera); + + const currentPoint = new Vector3(); + + raycaster.ray.intersectPlane(panPlane.current, currentPoint); + + const delta = new Vector3().subVectors( + panStartPoint.current, + currentPoint + ); + + targetRef.current.x += delta.x; + targetRef.current.y += delta.y; + targetRef.current.z += delta.z; + + panStartPoint.current.copy(currentPoint); + } + lastPos.current = { x: e.clientX, y: e.clientY }; - updateCamera(); }; const handleMouseUp = () => { isDragging.current = false; + isPanning.current = false; }; const handleWheel = (e: WheelEvent) => {