From c30ed2f78ef29589e602d0bc95acc0a12f661728 Mon Sep 17 00:00:00 2001 From: ochafik Date: Thu, 26 Dec 2024 17:13:39 +0000 Subject: [PATCH] Fix axes rotation: get to nearest rotation before cycling, and default to diagonal --- src/components/ViewerPanel.tsx | 79 ++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/components/ViewerPanel.tsx b/src/components/ViewerPanel.tsx index 7631265..a8c083b 100644 --- a/src/components/ViewerPanel.tsx +++ b/src/components/ViewerPanel.tsx @@ -2,7 +2,6 @@ import { CSSProperties, useContext, useEffect, useRef, useState } from 'react'; import { ModelContext } from './contexts'; -import { on } from 'events'; import { Toast } from 'primereact/toast'; declare global { @@ -13,15 +12,43 @@ declare global { } } -const PREDEFINED_ORBITS: [string, number, number][] = [ +export const PREDEFINED_ORBITS: [string, number, number][] = [ + ["Diagonal", Math.PI / 4, Math.PI / 4], ["Front", 0, Math.PI / 2], ["Right", Math.PI / 2, Math.PI / 2], ["Back", Math.PI, Math.PI / 2], ["Left", -Math.PI / 2, Math.PI / 2], ["Top", 0, 0], - ["Bottom", -Math.PI, Math.PI], + ["Bottom", 0, Math.PI], ]; +function spherePoint(theta: number, phi: number): [number, number, number] { + return [ + Math.cos(theta) * Math.sin(phi), + Math.sin(theta) * Math.sin(phi), + Math.cos(phi), + ]; +} + +function euclideanDist(a: [number, number, number], b: [number, number, number]): number { + const dx = a[0] - b[0]; + const dy = a[1] - b[1]; + const dz = a[2] - b[2]; + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} +const radDist = (a: number, b: number) => Math.min(Math.abs(a - b), Math.abs(a - b + 2 * Math.PI), Math.abs(a - b - 2 * Math.PI)); + +function getClosestPredefinedOrbitIndex(theta: number, phi: number): [number, number, number] { + const point = spherePoint(theta, phi); + const points = PREDEFINED_ORBITS.map(([_, t, p]) => spherePoint(t, p)); + const distances = points.map(p => euclideanDist(point, p)); + const radDistances = PREDEFINED_ORBITS.map(([_, ptheta, pphi]) => Math.max(radDist(theta, ptheta), radDist(phi, pphi))); + const [index, dist] = distances.reduce((acc, d, i) => d < acc[1] ? [i, d] : acc, [0, Infinity]) as [number, number]; + return [index, dist, radDistances[index]]; +} + +const originalOrbit = (([name, theta, phi]) => `${theta}rad ${phi}rad auto`)(PREDEFINED_ORBITS[0]); + export default function ViewerPanel({className, style}: {className?: string, style?: CSSProperties}) { const model = useContext(ModelContext); if (!model) throw new Error('No model'); @@ -53,24 +80,46 @@ export default function ViewerPanel({className, style}: {className?: string, sty } useEffect(() => { - function onClick(e: MouseEvent) { + let mouseDownSpherePoint: [number, number, number] | undefined; + function getSpherePoint() { + const orbit = modelViewerRef.current.getCameraOrbit(); + return spherePoint(orbit.theta, orbit.phi); + } + function onMouseDown(e: MouseEvent) { + if (e.target === axesViewerRef.current) { + mouseDownSpherePoint = getSpherePoint(); + } + } + function onMouseUp(e: MouseEvent) { if (e.target === axesViewerRef.current) { + const euclEps = 0.01; + const radEps = 0.1; + + const spherePoint = getSpherePoint(); + const clickDist = mouseDownSpherePoint ? euclideanDist(spherePoint, mouseDownSpherePoint) : Infinity; + if (clickDist > euclEps) { + return; + } // Cycle through orbits - const orbit = axesViewerRef.current.getCameraOrbit(); - const eps = 0.01; - const currentIndex = PREDEFINED_ORBITS.findIndex(([_, theta, phi]) => Math.abs(orbit.theta - theta) < eps && Math.abs(orbit.phi - phi) < eps); - const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % PREDEFINED_ORBITS.length; + const orbit = modelViewerRef.current.getCameraOrbit(); + const [currentIndex, dist, radDist] = getClosestPredefinedOrbitIndex(orbit.theta, orbit.phi); + const newIndex = dist < euclEps && radDist < radEps ? (currentIndex + 1) % PREDEFINED_ORBITS.length : currentIndex; const [name, theta, phi] = PREDEFINED_ORBITS[newIndex]; - orbit.theta = theta; - orbit.phi = phi; + Object.assign(orbit, {theta, phi}); const newOrbit = modelViewerRef.current.cameraOrbit = axesViewerRef.current.cameraOrbit = orbit.toString(); toastRef.current?.show({severity: 'info', detail: `${name} view`, life: 1000,}); setInteractionPrompt('none'); } } - window.addEventListener('click', onClick); - return () => window.removeEventListener('click', onClick); - }); + window.addEventListener('mousedown', onMouseDown); + window.addEventListener('mouseup', onMouseUp); + // window.addEventListener('click', onClick); + return () => { + // window.removeEventListener('click', onClick); + window.removeEventListener('mousedown', onMouseDown); + window.removeEventListener('mouseup', onMouseUp); + }; + }, [axesViewerRef.current]); return (