CAD/client/src/helpers/hooks/useInteration.ts

126 lines
4.7 KiB
TypeScript

import { useEffect, type RefObject } from "react";
import * as THREE from "three";
import { normalizeScreenPosition } from "../normalizeScreenPosition";
const CLICK_THRESHOLD = 2; // px
export type InteractionMouseEventArgs = {
screenPosition: THREE.Vector2Like,
position: THREE.Vector2Like,
screenSize: THREE.Vector2Like,
pixelSize: THREE.Vector2Like,
};
export type UseInteractionOptions = {
onMouseClick?: (e: InteractionMouseEventArgs) => void,
onMouseMove?: (e: InteractionMouseEventArgs) => void,
onCameraChange?: () => void,
}
export function useInteraction(
targetRef: RefObject<HTMLCanvasElement | null>,
cameraRef: RefObject<THREE.PerspectiveCamera | null>,
options: UseInteractionOptions,
): void {
useEffect(
() => {
const target = targetRef.current!;
const camera = cameraRef.current!;
const onWheel = (e: WheelEvent) => {
radius = Math.max(2, Math.min(20, radius + e.deltaY * 0.02));
updateCamera();
e.preventDefault();
};
// --- Orbit Controls (manual implementation, no OrbitControls import needed) ---
let isDragging = false;
let isRightDrag = false;
let startX = 0, startY = 0;
let lastX = 0, lastY = 0;
let azimuth = -0.8, elevation = 0.45, radius = 10;
let targetPoint = new THREE.Vector3(0.5, 0.5, 0);
let rotationSpeed = 0.005;
// prevent flip at poles
const epsilon = 0.0001;
function updateCamera() {
const cosEl = Math.cos(elevation);
const x = targetPoint.x + radius * cosEl * Math.cos(azimuth);
const y = targetPoint.y + radius * cosEl * Math.sin(azimuth);
const z = targetPoint.z + radius * Math.sin(elevation);
camera.position.set(x, y, z)
camera.lookAt(targetPoint);
options.onCameraChange?.();
}
updateCamera();
const onMouseDown = (e: MouseEvent) => {
isDragging = true;
isRightDrag = e.button === 2;
startX = e.clientX;
startY = e.clientY;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
};
const onMouseUp = (e: MouseEvent) => {
isDragging = false;
if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD)) {
const screenPosition = { x: e.clientX, y: e.clientY };
options.onMouseClick?.({
...normalizeScreenPosition(screenPosition, target),
screenPosition,
});
}
};
const onMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
if (isRightDrag) {
targetPoint.x -= dx * 0.01;
targetPoint.y += dy * 0.01;
} else {
azimuth = azimuth - dx * rotationSpeed;
elevation = elevation + dy * rotationSpeed;
elevation = Math.max(-Math.PI / 2 + epsilon, Math.min(Math.PI / 2 - epsilon, elevation + dy * rotationSpeed));
}
updateCamera();
};
const onHover = (e: MouseEvent) => {
const screenPosition = { x: e.clientX, y: e.clientY };
options.onMouseMove?.({
...normalizeScreenPosition(screenPosition, target),
screenPosition,
});
};
const onContextMenu = (e: Event) => e.preventDefault();
target.addEventListener("mousedown", onMouseDown);
target.addEventListener("contextmenu", onContextMenu);
target.addEventListener("wheel", onWheel, { passive: false });
target.addEventListener("mousemove", onHover);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
return () => {
target.removeEventListener("mousedown", onMouseDown);
target.removeEventListener("contextmenu", onContextMenu);
target.removeEventListener("wheel", onWheel);
target.removeEventListener("mousemove", onHover);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
}
});
}