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'; import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment } from './SmartMeasure'; 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 wireframeColor = useModelStore((s) => s.wireframeColor); const wireframeThickness = useModelStore((s) => s.wireframeThickness); const edgeThresholdAngle = useModelStore((s) => s.edgeThresholdAngle); 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]); // Store original colors so we can restore them const originalColors = useRef>(new Map()); 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(); } // Clean up previous edge lines const toRemove: THREE.Object3D[] = []; child.children.forEach(c => { if (c.userData.__edgeLine) toRemove.push(c); }); toRemove.forEach(c => { if (c instanceof THREE.LineSegments) { c.geometry.dispose(); (c.material as THREE.Material).dispose(); } child.remove(c); }); const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach((mat: THREE.Material) => { if (mat instanceof THREE.MeshStandardMaterial) { // Store original color on first encounter if (!originalColors.current.has(mat)) { originalColors.current.set(mat, mat.color.clone()); } if (renderMode === 'edges') { mat.visible = false; } else { mat.visible = true; mat.transparent = opacity < 1; mat.opacity = opacity; mat.wireframe = renderMode === 'wireframe'; if (renderMode === 'wireframe') { mat.wireframeLinewidth = wireframeThickness; mat.color.set(wireframeColor); } else 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); } } mat.needsUpdate = true; } }); // Create edge lines for edges mode if (renderMode === 'edges' && child.geometry) { const edgesGeo = new THREE.EdgesGeometry(child.geometry, edgeThresholdAngle); const lineMat = new THREE.LineBasicMaterial({ color: wireframeColor, linewidth: wireframeThickness, }); const lineSegments = new THREE.LineSegments(edgesGeo, lineMat); lineSegments.userData.__edgeLine = true; child.add(lineSegments); } } }); }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]); const rotXRad = (fineTuning.rotX * Math.PI) / 180; const rotYRad = (fineTuning.rotY * Math.PI) / 180; const rotZRad = (fineTuning.rotZ * Math.PI) / 180; const s = fineTuning.scale ?? 1; const scaleRatio = useModelStore((st) => st.scaleRatio); const renderFactor = scaleRatio?.factor ?? 1; return ( ); } /** Sphere marker at a 3D point */ function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) { return ( ); } /** Snap ring indicator */ function SnapRing({ position }: { position: [number, number, number] }) { return ( ); } /** Renders all measurements, snap point, and hover info */ function MeasurementOverlay() { const measurements = useModelStore((s) => s.measurements); const measurePoints = useModelStore((s) => s.measurePoints); const snapPoint = useModelStore((s) => s.snapPoint); const hoverInfo = useModelStore((s) => s.hoverInfo); const measureMode = useModelStore((s) => s.measureMode); return ( <> {/* Snap indicator */} {measureMode && snapPoint && ( )} {/* Hover auto-detect tooltip */} {hoverInfo && (
{hoverInfo.type === 'hole' ? '⌀ ' : ''} {hoverInfo.value.toFixed(1)} mm
)} {/* 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
); })} ); } /** Smart vertex snap on pointer move */ function SmartSnapHandler() { const { camera, scene, gl } = useThree(); const measureMode = useModelStore((s) => s.measureMode); const setSnapPoint = useModelStore((s) => s.setSnapPoint); const raycaster = useMemo(() => new THREE.Raycaster(), []); const mouse = useMemo(() => new THREE.Vector2(), []); const mouseRef = useRef({ x: 0, y: 0 }); useEffect(() => { const onMove = (e: MouseEvent) => { const rect = gl.domElement.getBoundingClientRect(); mouseRef.current.x = e.clientX - rect.left; mouseRef.current.y = e.clientY - rect.top; mouse.x = (mouseRef.current.x / rect.width) * 2 - 1; mouse.y = -(mouseRef.current.y / rect.height) * 2 + 1; }; gl.domElement.addEventListener('pointermove', onMove); return () => gl.domElement.removeEventListener('pointermove', onMove); }, [gl, mouse]); useFrame(() => { if (!measureMode) { setSnapPoint(null); return; } raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); const hit = intersects.find(i => { const obj = i.object; if (obj instanceof THREE.GridHelper) return false; if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false; if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false; if (obj.userData.__edgeLine) return false; return obj instanceof THREE.Mesh; }); if (hit && hit.object instanceof THREE.Mesh) { const canvas = gl.domElement; const snap = findNearestVertex( hit.object, hit.point, camera, { width: canvas.clientWidth, height: canvas.clientHeight }, 10 ); if (snap) { setSnapPoint({ x: snap.x, y: snap.y, z: snap.z }); } else { setSnapPoint(null); } } else { setSnapPoint(null); } }); return null; } /** Hover detector for auto-detect hole diameter and edge length */ 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 lastHitKey = useRef(''); useEffect(() => { if (measureMode) { setHoverInfo(null); if (hoverTimer.current) clearTimeout(hoverTimer.current); return; } const onMove = (e: MouseEvent) => { const rect = gl.domElement.getBoundingClientRect(); mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); const hit = intersects.find(i => { const obj = i.object; if (obj instanceof THREE.GridHelper) return false; if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false; if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false; if (obj.userData.__edgeLine) return false; return obj instanceof THREE.Mesh; }); if (!hit || !(hit.object instanceof THREE.Mesh)) { lastHitKey.current = ''; setHoverInfo(null); if (hoverTimer.current) clearTimeout(hoverTimer.current); return; } // Stability check – same approximate position for debounce 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); 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) { const hole = detectHoleAtFace(hit.object, hit.faceIndex); if (hole) { setHoverInfo({ type: 'hole', position: [hole.center.x, hole.center.y, hole.center.z], value: hole.diameterMM, }); 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; } setHoverInfo(null); }, 1000); }; const onLeave = () => { lastHitKey.current = ''; setHoverInfo(null); if (hoverTimer.current) clearTimeout(hoverTimer.current); }; gl.domElement.addEventListener('pointermove', onMove); gl.domElement.addEventListener('pointerleave', onLeave); return () => { gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointerleave', onLeave); if (hoverTimer.current) clearTimeout(hoverTimer.current); }; }, [measureMode, camera, scene, gl, raycaster, mouse, setHoverInfo]); return null; } /** Raycasting click handler for measurement mode – uses snap point when available */ function MeasureClickHandler() { const { camera, scene, gl } = useThree(); const measureMode = useModelStore((s) => s.measureMode); const addMeasurePoint = useModelStore((s) => s.addMeasurePoint); const snapPoint = useModelStore((s) => s.snapPoint); const raycaster = useMemo(() => new THREE.Raycaster(), []); const mouse = useMemo(() => new THREE.Vector2(), []); const handleClick = useCallback((event: MouseEvent) => { if (!measureMode) return; // Use snap point if available if (snapPoint) { addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z }); 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); 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; if (obj.geometry instanceof THREE.RingGeometry) return false; } return true; }); if (hit) { addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z }); } }, [measureMode, addMeasurePoint, snapPoint, 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; } function GridLayer() { const showGrid = useModelStore((s) => s.showGrid); if (!showGrid) return null; return ( ); } export function ModelViewerCanvas({ url }: ModelViewerProps) { return ( { gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); const loop = () => { invalidate(); requestAnimationFrame(loop); }; loop(); }} > }> ); }