diff --git a/frontend/src/app/components/viewer3d.tsx b/frontend/src/app/components/viewer3d.tsx index 6c730ed9..67189bc1 100644 --- a/frontend/src/app/components/viewer3d.tsx +++ b/frontend/src/app/components/viewer3d.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { Suspense, useMemo } from 'react'; +import React, { Suspense, useEffect, useMemo } from 'react'; import { Canvas } from '@react-three/fiber'; import { Environment, OrbitControls, useGLTF } from '@react-three/drei'; @@ -8,6 +8,12 @@ type Viewer3DProps = { modelUrl: string; }; +declare global { + interface Window { + __lod0VisibleAt?: number; + } +} + function Model({ modelUrl }: { modelUrl: string }) { // Resolve absolute URL to avoid base path issues const src = useMemo(() => { @@ -19,6 +25,33 @@ function Model({ modelUrl }: { modelUrl: string }) { // Load GLB (no extra params to avoid signature mismatches) const gltf = useGLTF(src); + + useEffect(() => { + if (typeof window === 'undefined') { + return undefined; + } + + if (!gltf?.scene) { + return undefined; + } + + let cancelled = false; + const rafId = window.requestAnimationFrame(() => { + if (cancelled) { + return; + } + + const ts = performance.now(); + window.__lod0VisibleAt = ts; + window.dispatchEvent(new CustomEvent('lod0:visible', { detail: { ts } })); + }); + + return () => { + cancelled = true; + window.cancelAnimationFrame(rafId); + }; + }, [gltf?.scene, src]); + return ; }