an orbital camera and some basic shapes are used to build the 3d bin shell and the flat inventory when using manual or lidar are built
119 lines
No EOL
3.4 KiB
TypeScript
119 lines
No EOL
3.4 KiB
TypeScript
import { useThree } from "@react-three/fiber";
|
|
import { useEffect, useRef } from "react";
|
|
|
|
interface CameraTarget {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
}
|
|
|
|
interface Props {
|
|
target?: CameraTarget;
|
|
clampVerticalRotation?: boolean;
|
|
/**
|
|
* The minimum vertical rotation in radians
|
|
* @default 0.01
|
|
*/
|
|
minPhi?: number;
|
|
/**
|
|
* The maximum vertical rotation in radians
|
|
* @default Math.PI - 0.01
|
|
*/
|
|
maxPhi?: number;
|
|
}
|
|
|
|
export default function OrbitCameraControls(props: Props) {
|
|
const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props;
|
|
const { camera, gl } = useThree();
|
|
|
|
const isDragging = useRef(false);
|
|
const lastPos = useRef({ x: 0, y: 0 });
|
|
|
|
// Spherical coords
|
|
const spherical = useRef({
|
|
radius: 10,
|
|
theta: 0, // horizontal
|
|
phi: Math.PI / 2, // vertical
|
|
});
|
|
|
|
useEffect(() => {
|
|
const canvas = gl.domElement;
|
|
|
|
const updateCamera = () => {
|
|
const { radius, theta, phi } = spherical.current;
|
|
|
|
const x = radius * Math.sin(phi) * Math.sin(theta);
|
|
const y = radius * Math.cos(phi);
|
|
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);
|
|
};
|
|
|
|
updateCamera();
|
|
|
|
const handleMouseDown = (e: MouseEvent) => {
|
|
if (e.target !== canvas) return;
|
|
isDragging.current = true;
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
};
|
|
|
|
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)
|
|
);
|
|
}
|
|
|
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
|
|
updateCamera();
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
isDragging.current = false;
|
|
};
|
|
|
|
const handleWheel = (e: WheelEvent) => {
|
|
if (e.target !== canvas) return;
|
|
|
|
e.preventDefault();
|
|
|
|
spherical.current.radius += e.deltaY * 0.01;
|
|
|
|
// Clamp zoom
|
|
spherical.current.radius = Math.max(
|
|
3,
|
|
Math.min(30, spherical.current.radius)
|
|
);
|
|
|
|
updateCamera();
|
|
};
|
|
|
|
window.addEventListener("mousedown", handleMouseDown);
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
window.addEventListener("mouseup", handleMouseUp);
|
|
|
|
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
|
|
|
return () => {
|
|
window.removeEventListener("mousedown", handleMouseDown);
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
window.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
canvas.removeEventListener("wheel", handleWheel);
|
|
};
|
|
}, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi]);
|
|
|
|
return null;
|
|
} |