126 lines
4.7 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
}
|