From c63aa72d9955cb97f15b78227381ae759edcebcf Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 13:44:59 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 76 ++++++++++++++++++---------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 3098428..a71406a 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -393,22 +393,24 @@ function SmartSnapHandler() { return null; } -/** Hover detector for auto-detect hole diameter and edge length */ +/** Hover detector for auto-detect hole diameter and edge length. + * Runs in BOTH view mode and measure mode. Outside measure mode the label + * auto-hides after 4s; in measure mode it persists until the user clicks + * (registering it as a saved measurement) or moves the mouse significantly. */ function HoverDetector() { const { camera, scene, gl } = useThree(); - const measureMode = useModelStore((s) => s.measureMode); const setHoverInfo = useModelStore((s) => s.setHoverInfo); const raycaster = useMemo(() => new THREE.Raycaster(), []); const mouse = useMemo(() => new THREE.Vector2(), []); const hoverTimer = useRef | null>(null); + const hideTimer = useRef | null>(null); const lastHitKey = useRef(''); useEffect(() => { - if (measureMode) { - setHoverInfo(null); - if (hoverTimer.current) clearTimeout(hoverTimer.current); - return; - } + const clearTimers = () => { + if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; } + if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null; } + }; const onMove = (e: MouseEvent) => { const rect = gl.domElement.getBoundingClientRect(); @@ -429,24 +431,31 @@ function HoverDetector() { if (!hit || !(hit.object instanceof THREE.Mesh)) { lastHitKey.current = ''; setHoverInfo(null); - if (hoverTimer.current) clearTimeout(hoverTimer.current); + clearTimers(); return; } - // Stability check – same approximate position for debounce + // Stability check – same approximate position for debounce (~3 mm) const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`; if (key === lastHitKey.current) return; // timer already running lastHitKey.current = key; setHoverInfo(null); + clearTimers(); - if (hoverTimer.current) clearTimeout(hoverTimer.current); hoverTimer.current = setTimeout(() => { if (!(hit.object instanceof THREE.Mesh)) return; const canvas = gl.domElement; const canvasSize = { width: canvas.clientWidth, height: canvas.clientHeight }; - // Try hole detection first - if (hit.faceIndex !== undefined) { + // 1) Try circle fit from edge vertices around the hit (catches holes on flat faces) + const circ = detectCircularEdgeAtPoint(hit.object, hit.point, camera, canvasSize, 50); + if (circ) { + setHoverInfo({ + type: 'hole', + position: [circ.center.x, circ.center.y, circ.center.z], + value: circ.diameterMM, + }); + } else if (hit.faceIndex !== undefined && (() => { const hole = detectHoleAtFace(hit.object, hit.faceIndex); if (hole) { setHoverInfo({ @@ -454,29 +463,42 @@ function HoverDetector() { position: [hole.center.x, hole.center.y, hole.center.z], value: hole.diameterMM, }); + return true; + } + return false; + })()) { + // handled inside IIFE + } else { + // 3) Edge length + const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15); + if (edge) { + setHoverInfo({ + type: 'edge', + position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z], + value: edge.lengthMM, + endpoints: { + a: { x: edge.a.x, y: edge.a.y, z: edge.a.z }, + b: { x: edge.b.x, y: edge.b.y, z: edge.b.z }, + }, + }); + } else { + setHoverInfo(null); return; } } - // Try edge detection - const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15); - if (edge) { - setHoverInfo({ - type: 'edge', - position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z], - value: edge.lengthMM, - }); - return; + // Outside measure mode, auto-hide after 4 s + const inMeasure = useModelStore.getState().measureMode; + if (!inMeasure) { + hideTimer.current = setTimeout(() => setHoverInfo(null), 4000); } - - setHoverInfo(null); - }, 1000); + }, 2000); }; const onLeave = () => { lastHitKey.current = ''; setHoverInfo(null); - if (hoverTimer.current) clearTimeout(hoverTimer.current); + clearTimers(); }; gl.domElement.addEventListener('pointermove', onMove); @@ -484,9 +506,9 @@ function HoverDetector() { return () => { gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointerleave', onLeave); - if (hoverTimer.current) clearTimeout(hoverTimer.current); + clearTimers(); }; - }, [measureMode, camera, scene, gl, raycaster, mouse, setHoverInfo]); + }, [camera, scene, gl, raycaster, mouse, setHoverInfo]); return null; }