Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-21 13:44:59 +00:00
parent f7ad9e0f21
commit c63aa72d99
+43 -21
View File
@@ -393,22 +393,24 @@ function SmartSnapHandler() {
return null; 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() { function HoverDetector() {
const { camera, scene, gl } = useThree(); const { camera, scene, gl } = useThree();
const measureMode = useModelStore((s) => s.measureMode);
const setHoverInfo = useModelStore((s) => s.setHoverInfo); const setHoverInfo = useModelStore((s) => s.setHoverInfo);
const raycaster = useMemo(() => new THREE.Raycaster(), []); const raycaster = useMemo(() => new THREE.Raycaster(), []);
const mouse = useMemo(() => new THREE.Vector2(), []); const mouse = useMemo(() => new THREE.Vector2(), []);
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastHitKey = useRef(''); const lastHitKey = useRef('');
useEffect(() => { useEffect(() => {
if (measureMode) { const clearTimers = () => {
setHoverInfo(null); if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; }
if (hoverTimer.current) clearTimeout(hoverTimer.current); if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null; }
return; };
}
const onMove = (e: MouseEvent) => { const onMove = (e: MouseEvent) => {
const rect = gl.domElement.getBoundingClientRect(); const rect = gl.domElement.getBoundingClientRect();
@@ -429,24 +431,31 @@ function HoverDetector() {
if (!hit || !(hit.object instanceof THREE.Mesh)) { if (!hit || !(hit.object instanceof THREE.Mesh)) {
lastHitKey.current = ''; lastHitKey.current = '';
setHoverInfo(null); setHoverInfo(null);
if (hoverTimer.current) clearTimeout(hoverTimer.current); clearTimers();
return; 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)}`; 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 if (key === lastHitKey.current) return; // timer already running
lastHitKey.current = key; lastHitKey.current = key;
setHoverInfo(null); setHoverInfo(null);
clearTimers();
if (hoverTimer.current) clearTimeout(hoverTimer.current);
hoverTimer.current = setTimeout(() => { hoverTimer.current = setTimeout(() => {
if (!(hit.object instanceof THREE.Mesh)) return; if (!(hit.object instanceof THREE.Mesh)) return;
const canvas = gl.domElement; const canvas = gl.domElement;
const canvasSize = { width: canvas.clientWidth, height: canvas.clientHeight }; const canvasSize = { width: canvas.clientWidth, height: canvas.clientHeight };
// Try hole detection first // 1) Try circle fit from edge vertices around the hit (catches holes on flat faces)
if (hit.faceIndex !== undefined) { 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); const hole = detectHoleAtFace(hit.object, hit.faceIndex);
if (hole) { if (hole) {
setHoverInfo({ setHoverInfo({
@@ -454,29 +463,42 @@ function HoverDetector() {
position: [hole.center.x, hole.center.y, hole.center.z], position: [hole.center.x, hole.center.y, hole.center.z],
value: hole.diameterMM, value: hole.diameterMM,
}); });
return; return true;
} }
} return false;
})()) {
// Try edge detection // handled inside IIFE
} else {
// 3) Edge length
const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15); const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15);
if (edge) { if (edge) {
setHoverInfo({ setHoverInfo({
type: 'edge', type: 'edge',
position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z], position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z],
value: edge.lengthMM, 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; return;
} }
}
setHoverInfo(null); // Outside measure mode, auto-hide after 4 s
}, 1000); const inMeasure = useModelStore.getState().measureMode;
if (!inMeasure) {
hideTimer.current = setTimeout(() => setHoverInfo(null), 4000);
}
}, 2000);
}; };
const onLeave = () => { const onLeave = () => {
lastHitKey.current = ''; lastHitKey.current = '';
setHoverInfo(null); setHoverInfo(null);
if (hoverTimer.current) clearTimeout(hoverTimer.current); clearTimers();
}; };
gl.domElement.addEventListener('pointermove', onMove); gl.domElement.addEventListener('pointermove', onMove);
@@ -484,9 +506,9 @@ function HoverDetector() {
return () => { return () => {
gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointermove', onMove);
gl.domElement.removeEventListener('pointerleave', onLeave); 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; return null;
} }