diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index dc0b8df..2ff0ced 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1035,38 +1035,41 @@ function GridLayer() { ); } -/** Computes min Y of all visible models and updates store gridY when auto-follow is on. */ +/** Continuously aligns the grid Y to the bottom of all visible models so the + * piece always looks like it is sitting on the grid — including after + * calibration, fine-tuning, repositioning or scale changes. */ function GridAutoFollower() { - const models = useModelStore((s) => s.models); const { scene } = useThree(); - useEffect(() => { + const lastY = useRef(Number.NaN); + const acc = useRef(0); + useFrame((_, dt) => { const auto = useModelStore.getState().gridAutoFollow; if (!auto) return; - // Defer to next tick so models have rendered/positioned - const id = setTimeout(() => { - if (!useModelStore.getState().gridAutoFollow) return; - const box = new THREE.Box3(); - let has = false; - scene.traverse((obj) => { - if (obj instanceof THREE.Mesh && obj.geometry) { - // Skip helpers (markers, rings) - if (obj.geometry instanceof THREE.SphereGeometry) return; - if (obj.geometry instanceof THREE.RingGeometry) return; - if (obj.userData.__edgeLine) return; - obj.updateWorldMatrix(true, false); - const b = new THREE.Box3().setFromObject(obj); - if (isFinite(b.min.y)) { - if (!has) { box.copy(b); has = true; } - else box.union(b); - } + // Throttle to ~10 Hz to keep this cheap. + acc.current += dt; + if (acc.current < 0.1) return; + acc.current = 0; + const box = new THREE.Box3(); + let has = false; + scene.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.geometry) { + if (obj.geometry instanceof THREE.SphereGeometry) return; + if (obj.geometry instanceof THREE.RingGeometry) return; + if (obj.userData.__edgeLine) return; + const b = new THREE.Box3().setFromObject(obj); + if (isFinite(b.min.y)) { + if (!has) { box.copy(b); has = true; } + else box.union(b); } - }); - if (has) { - useModelStore.setState({ gridY: box.min.y - 0.005 }); } - }, 100); - return () => clearTimeout(id); - }, [models, scene]); + }); + if (!has) return; + const target = box.min.y - 0.005; + if (!Number.isFinite(lastY.current) || Math.abs(target - lastY.current) > 1e-4) { + lastY.current = target; + useModelStore.setState({ gridY: target }); + } + }); return null; }