diff --git a/package.json b/package.json index 9bd8c87..1cb9a53 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite_react_shadcn_ts", "private": true, - "version": "1.0.2", + "version": "1.0.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index e8ea0c2..a6fa571 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1,4 +1,4 @@ -import { Suspense, useRef, useMemo, useEffect, type ReactNode } from 'react'; +import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } 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'; @@ -6,6 +6,8 @@ import * as THREE from 'three'; import { useModelStore, type SceneModel } from '@/stores/useModelStore'; import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure'; import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms'; +import { parseIFCtoThree } from '@/lib/convertIFC'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; interface ModelViewerProps { url?: string; // legacy, ignored — uses store.models @@ -73,9 +75,36 @@ function LoadingFallback() { } function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: boolean }) { - const { scene: rawScene } = useGLTF(sceneModel.url); - // Clone scene per instance so multiple GLBModels with same url don't share materials/transforms - const scene = useMemo(() => rawScene.clone(true), [rawScene]); + const [rawScene, setRawScene] = useState(null); + const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]); + + useEffect(() => { + let active = true; + const isIfc = sceneModel.fileName.toLowerCase().endsWith('.ifc'); + if (isIfc) { + fetch(sceneModel.url) + .then((res) => res.arrayBuffer()) + .then((buf) => parseIFCtoThree(buf)) + .then((threeScene) => { + if (active) setRawScene(threeScene); + }) + .catch((err) => console.error('[GLBModel] IFC parsing error', err)); + } else { + const loader = new GLTFLoader(); + loader.load( + sceneModel.url, + (gltf) => { + if (active) setRawScene(gltf.scene); + }, + undefined, + (err) => console.error('[GLBModel] GLTF loading error', err) + ); + } + return () => { + active = false; + }; + }, [sceneModel.url, sceneModel.fileName]); + const ref = useRef(null); const opacity = useModelStore((s) => s.opacity); const renderMode = useModelStore((s) => s.renderMode); @@ -88,6 +117,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: const fineTuning = sceneModel.fineTuning; const modelInfo = useMemo(() => { + if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() }; const box = new THREE.Box3().setFromObject(scene); const size = new THREE.Vector3(); const center = new THREE.Vector3(); @@ -99,6 +129,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: const originalColors = useRef>(new Map()); useEffect(() => { + if (!scene) return; const hasRejected = checklist.some(i => i.status === 'rejected'); const allApproved = checklist.every(i => i.status === 'approved'); @@ -174,6 +205,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: const renderFactor = scaleRatio?.factor ?? 1; if (!sceneModel.visible) return null; + if (!scene) return null; // Determine dominant local axis of the model (longest bbox side) for axial roll const dominantAxis: 'x' | 'y' | 'z' = diff --git a/src/lib/convertIFC.ts b/src/lib/convertIFC.ts index d175758..6f4ebf1 100644 --- a/src/lib/convertIFC.ts +++ b/src/lib/convertIFC.ts @@ -7,10 +7,7 @@ const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/'; /** * Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter. */ -export async function convertIFCtoGLB( - buffer: ArrayBuffer, - fileName: string -): Promise<{ blob: Blob; fileName: string; fileSize: number }> { +export async function parseIFCtoThree(buffer: ArrayBuffer): Promise { const ifcApi = new WebIFC.IfcAPI(); ifcApi.SetWasmPath(WASM_PATH, true); await ifcApi.Init(); @@ -92,6 +89,14 @@ export async function convertIFCtoGLB( }); ifcApi.CloseModel(modelID); + return scene; +} + +export async function convertIFCtoGLB( + buffer: ArrayBuffer, + fileName: string +): Promise<{ blob: Blob; fileName: string; fileSize: number }> { + const scene = await parseIFCtoThree(buffer); // Export to GLB const exporter = new GLTFExporter(); @@ -108,7 +113,6 @@ export async function convertIFCtoGLB( const blob = new Blob([glb], { type: 'model/gltf-binary' }); // Cleanup - materials.forEach((m) => m.dispose()); scene.traverse((child) => { if (child instanceof THREE.Mesh) { child.geometry.dispose(); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index b7c1ffd..d462d2a 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -11,6 +11,8 @@ import { CloudLoader } from '@/components/CloudLoader'; import { SceneModelList } from '@/components/SceneModelList'; import { RecentFilesList } from '@/components/RecentFilesList'; import { addRecentFile } from '@/lib/recentFiles'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; const Index = () => { const navigate = useNavigate(); @@ -21,6 +23,16 @@ const Index = () => { const [converting, setConverting] = useState(false); const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); + const [ifcConvert, setIfcConvert] = useState(() => { + const saved = localStorage.getItem('tsxr_ifc_convert'); + return saved !== 'false'; + }); + + const handleToggleIfcConvert = (checked: boolean) => { + setIfcConvert(checked); + localStorage.setItem('tsxr_ifc_convert', checked ? 'true' : 'false'); + }; + useEffect(() => { if (navigator.xr) { navigator.xr.isSessionSupported('immersive-ar').then(setXrSupported).catch(() => setXrSupported(false)); @@ -74,8 +86,19 @@ const Index = () => { if (!sigCheck.ok) { toast.error(sigCheck.reason!); if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); + setConverting(false); return; } + + if (ext === 'ifc' && !ifcConvert) { + const url = URL.createObjectURL(file); + addModel({ fileName: file.name, fileSize: file.size, url }); + addRecentFile(file.name, file).catch(() => {}); + toast.success(`Modelo "${file.name}" carregado de forma nativa!`); + setConverting(false); + return; + } + let result; if (ext === 'ifc') { result = await convertIFCtoGLB(buffer, file.name); @@ -93,7 +116,7 @@ const Index = () => { } finally { setConverting(false); } - }, [addModel]); + }, [addModel, ifcConvert]); // Carregar da Nuvem agora é encapsulado em @@ -102,6 +125,14 @@ const Index = () => { try { const response = await fetch('/models/demo250hp.ifc'); const buffer = await response.arrayBuffer(); + if (!ifcConvert) { + const blob = new Blob([buffer], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + addModel({ fileName: 'demo250hp.ifc', fileSize: blob.size, url }); + addRecentFile('demo250hp.ifc', blob).catch(() => {}); + toast.success('Modelo demo "250 HP" (IFC nativo) carregado!'); + return; + } const result = await convertIFCtoGLB(buffer, 'demo250hp.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); @@ -109,17 +140,25 @@ const Index = () => { toast.success('Modelo demo "250 HP" (IFC) carregado!'); } catch (err) { console.error(err); - toast.error('Falha ao converter modelo demo IFC'); + toast.error('Falha ao processar modelo demo IFC'); } finally { setLoadingDemoIFC(false); } - }, [addModel]); + }, [addModel, ifcConvert]); const handleLoadDemoCotovelo = useCallback(async () => { setLoadingDemoCotovelo(true); try { const response = await fetch('/models/B126-COTOVELO.ifc'); const buffer = await response.arrayBuffer(); + if (!ifcConvert) { + const blob = new Blob([buffer], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + addModel({ fileName: 'B126-COTOVELO.ifc', fileSize: blob.size, url }); + addRecentFile('B126-COTOVELO.ifc', blob).catch(() => {}); + toast.success('Modelo demo "B126 Cotovelo" (IFC nativo) carregado!'); + return; + } const result = await convertIFCtoGLB(buffer, 'B126-COTOVELO.ifc'); const url = URL.createObjectURL(result.blob); addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); @@ -127,15 +166,15 @@ const Index = () => { toast.success('Modelo demo "B126 Cotovelo" (IFC) carregado!'); } catch (err) { console.error(err); - toast.error('Falha ao converter modelo demo Cotovelo'); + toast.error('Falha ao processar modelo demo Cotovelo'); } finally { setLoadingDemoCotovelo(false); } - }, [addModel]); + }, [addModel, ifcConvert]); const handleEnterViewer = () => { if (!model) { - toast.error('Importe um modelo GLB primeiro'); + toast.error('Importe um modelo 3D primeiro'); return; } navigate('/viewer'); @@ -175,11 +214,28 @@ const Index = () => { {converting ? : }
-

{converting ? 'Convertendo modelo…' : 'Importar Modelo 3D'}

+

{converting ? 'Carregando modelo…' : 'Importar Modelo 3D'}

GLB · OBJ · STL · IFC — Escala 1:1

+ {/* Toggle IFC Conversion */} +
+
+ +

+ {ifcConvert ? 'Otimizado para AR (conversão automática)' : 'Modo nativo (carregamento bruto IFC)'} +

+
+ +
+ {/* Demo buttons */}
@@ -254,7 +310,7 @@ const Index = () => { {/* Footer */}

- TrackSteelXR v1.02 — Q.C. Inspection + TrackSteelXR v1.03 — Q.C. Inspection

); diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index e650183..5e7da1b 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -23,6 +23,8 @@ import { useDevKit } from '@/devkit/useDevKit'; import { DevPanel } from '@/devkit/DevPanel'; import { FakeControllers } from '@/devkit/FakeControllers'; import { VisibilityApplier } from '@/components/three/ModelViewer'; +import { parseIFCtoThree } from '@/lib/convertIFC'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // --- Diagnóstico XR --- console.log('[XR] Inicializando store...'); @@ -66,8 +68,36 @@ const store = createXRStore({ // ─── XRModel ─────────────────────────────────────────── function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').SceneModel }) { - const { scene: rawScene } = useGLTF(sceneModel.url); - const scene = useMemo(() => rawScene.clone(true), [rawScene]); + const [rawScene, setRawScene] = useState(null); + const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]); + + useEffect(() => { + let active = true; + const isIfc = sceneModel.fileName.toLowerCase().endsWith('.ifc'); + if (isIfc) { + fetch(sceneModel.url) + .then((res) => res.arrayBuffer()) + .then((buf) => parseIFCtoThree(buf)) + .then((threeScene) => { + if (active) setRawScene(threeScene); + }) + .catch((err) => console.error('[XRModel] IFC parsing error', err)); + } else { + const loader = new GLTFLoader(); + loader.load( + sceneModel.url, + (gltf) => { + if (active) setRawScene(gltf.scene); + }, + undefined, + (err) => console.error('[XRModel] GLTF loading error', err) + ); + } + return () => { + active = false; + }; + }, [sceneModel.url, sceneModel.fileName]); + const ref = useRef(null); const fineTuning = sceneModel.fineTuning; const opacity = useModelStore((s) => s.opacity); @@ -78,6 +108,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). const checklist = useModelStore((s) => s.checklist); const modelInfo = useMemo(() => { + if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() }; const box = new THREE.Box3().setFromObject(scene); const size = new THREE.Vector3(); const center = new THREE.Vector3(); @@ -87,6 +118,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). }, [scene]); useEffect(() => { + if (!scene) return; const hasRejected = checklist.some(i => i.status === 'rejected'); const allApproved = checklist.every(i => i.status === 'approved'); @@ -154,6 +186,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). const renderFactor = scaleRatio?.factor ?? 1; if (!sceneModel.visible) return null; + if (!scene) return null; return (