diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index dd1f5ad..af51442 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -459,61 +459,93 @@ function HoverDetector() { return null; } -/** Raycasting click handler for measurement mode – uses snap point when available */ +/** Pointer handler for measurement mode – uses snap point when available. + * Uses pointerdown/up with a drag gate so accidental drags don't add points. */ function MeasureClickHandler() { const { camera, scene, gl } = useThree(); const measureMode = useModelStore((s) => s.measureMode); - const addMeasurePoint = useModelStore((s) => s.addMeasurePoint); - const snapPoint = useModelStore((s) => s.snapPoint); const raycaster = useMemo(() => new THREE.Raycaster(), []); const mouse = useMemo(() => new THREE.Vector2(), []); - const handleClick = useCallback((event: MouseEvent) => { - if (!measureMode) return; - - // Use snap point if available - if (snapPoint) { - addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z }); + useEffect(() => { + if (!measureMode) { + gl.domElement.style.cursor = 'grab'; return; } + gl.domElement.style.cursor = 'crosshair'; - const rect = gl.domElement.getBoundingClientRect(); - mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + const canvas = gl.domElement; + let downX = 0, downY = 0, downBtn = 0; + let armed = false; - raycaster.setFromCamera(mouse, camera); - const intersects = raycaster.intersectObjects(scene.children, true); + const onDown = (e: PointerEvent) => { + if (e.button !== 0) { armed = false; return; } + downX = e.clientX; + downY = e.clientY; + downBtn = e.button; + armed = true; + }; - const hit = intersects.find(i => { - const obj = i.object; - if (obj instanceof THREE.GridHelper) return false; - if (obj instanceof THREE.Mesh) { + const onUp = (e: PointerEvent) => { + if (!armed || e.button !== downBtn) return; + armed = false; + const dx = e.clientX - downX; + const dy = e.clientY - downY; + if (Math.hypot(dx, dy) > 4) return; // it was a drag, not a click + + const st = useModelStore.getState(); + if (!st.measureMode) return; + + // Prefer snap + if (st.snapPoint) { + st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z }); + return; + } + + const rect = canvas.getBoundingClientRect(); + mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(scene.children, true); + const hit = intersects.find(i => { + const obj = i.object; + if (!(obj instanceof THREE.Mesh)) return false; + if (obj.userData.__edgeLine) return false; if (obj.geometry instanceof THREE.SphereGeometry) return false; if (obj.geometry instanceof THREE.RingGeometry) return false; + if (obj.geometry instanceof THREE.PlaneGeometry) return false; + return true; + }); + if (hit) { + st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); } - return true; - }); + }; - if (hit) { - addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); - } - }, [measureMode, addMeasurePoint, snapPoint, camera, scene, gl, raycaster, mouse]); + const onKey = (e: KeyboardEvent) => { + const st = useModelStore.getState(); + if (!st.measureMode) return; + if (e.key === 'Escape') { + if (st.measurePoints.length > 0) st.undoLastMeasurePoint(); + } else if ((e.key === 'z' || e.key === 'Z') && !e.ctrlKey && !e.metaKey) { + st.undoLastMeasurement(); + } + }; - useEffect(() => { - const canvas = gl.domElement; - canvas.addEventListener('click', handleClick); - return () => canvas.removeEventListener('click', handleClick); - }, [gl, handleClick]); - - // Change cursor when in measure mode - useEffect(() => { - gl.domElement.style.cursor = measureMode ? 'crosshair' : 'grab'; - return () => { gl.domElement.style.cursor = 'grab'; }; - }, [measureMode, gl]); + canvas.addEventListener('pointerdown', onDown); + canvas.addEventListener('pointerup', onUp); + window.addEventListener('keydown', onKey); + return () => { + canvas.removeEventListener('pointerdown', onDown); + canvas.removeEventListener('pointerup', onUp); + window.removeEventListener('keydown', onKey); + gl.domElement.style.cursor = 'grab'; + }; + }, [measureMode, camera, scene, gl, raycaster, mouse]); return null; } + function GridLayer() { const showGrid = useModelStore((s) => s.showGrid); const gridY = useModelStore((s) => s.gridY);