import { Suspense, useRef, useMemo, useEffect, useCallback } from 'react'; import { Canvas, useThree, useFrame } from '@react-three/fiber'; import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei'; import { Loader2 } from 'lucide-react'; import * as THREE from 'three'; import { useModelStore } from '@/stores/useModelStore'; interface ModelViewerProps { url: string; } function LoadingFallback() { return (
Carregando modelo…
); } function GLBModel({ url }: { url: string }) { const { scene } = useGLTF(url); const ref = useRef(null); const opacity = useModelStore((s) => s.opacity); const renderMode = useModelStore((s) => s.renderMode); const checklist = useModelStore((s) => s.checklist); const fineTuning = useModelStore((s) => s.fineTuning); const modelInfo = useMemo(() => { const box = new THREE.Box3().setFromObject(scene); const size = new THREE.Vector3(); const center = new THREE.Vector3(); box.getSize(size); box.getCenter(center); return { size, center }; }, [scene]); useEffect(() => { const hasRejected = checklist.some(i => i.status === 'rejected'); const allApproved = checklist.every(i => i.status === 'approved'); scene.traverse((child) => { if (child instanceof THREE.Mesh) { if (Array.isArray(child.material)) { child.material = child.material.map(m => m.clone()); } else { child.material = child.material.clone(); } const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach((mat: THREE.Material) => { if (mat instanceof THREE.MeshStandardMaterial) { mat.transparent = opacity < 1; mat.opacity = opacity; mat.wireframe = renderMode === 'wireframe'; mat.needsUpdate = true; if (hasRejected) { mat.color.setHSL(0, 0.7, 0.5); } else if (allApproved) { mat.color.setHSL(0.38, 0.7, 0.45); } else { mat.color.set(0x8899aa); } } }); } }); }, [scene, opacity, renderMode, checklist]); const rotYRad = (fineTuning.rotY * Math.PI) / 180; return ( ); } /** Sphere marker at a 3D point */ function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) { return ( ); } /** Renders all measurements and pending points */ function MeasurementOverlay() { const measurements = useModelStore((s) => s.measurements); const measurePoints = useModelStore((s) => s.measurePoints); return ( <> {/* Pending first point */} {measurePoints.length === 1 && ( )} {/* Completed measurements */} {measurements.map((m) => { const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z]; const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z]; const mid: [number, number, number] = [ (a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, ]; return (
{m.distanceMM.toFixed(1)} mm
); })} ); } /** Raycasting click handler for measurement mode */ function MeasureClickHandler() { const { camera, scene, gl } = useThree(); const measureMode = useModelStore((s) => s.measureMode); const addMeasurePoint = useModelStore((s) => s.addMeasurePoint); const raycaster = useMemo(() => new THREE.Raycaster(), []); const mouse = useMemo(() => new THREE.Vector2(), []); const handleClick = useCallback((event: MouseEvent) => { if (!measureMode) return; 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; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); // Filter out measurement markers and grid const hit = intersects.find(i => { const obj = i.object; if (obj instanceof THREE.GridHelper) return false; if (obj instanceof THREE.Mesh) { if (obj.geometry instanceof THREE.SphereGeometry) return false; } return true; }); if (hit) { addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); } }, [measureMode, addMeasurePoint, camera, scene, gl, raycaster, mouse]); 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]); return null; } export function ModelViewerCanvas({ url }: ModelViewerProps) { return ( { gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); const loop = () => { invalidate(); requestAnimationFrame(loop); }; loop(); }} > }> ); }