diff --git a/package-lock.json b/package-lock.json index d9fa773..9cc8305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "tailwindcss-animate": "^1.0.7", "three": "^0.160.1", "vaul": "^0.9.9", + "web-ifc": "^0.0.57", "zod": "^3.25.76", "zustand": "^4.5.7" }, @@ -8977,6 +8978,11 @@ "node": ">=14" } }, + "node_modules/web-ifc": { + "version": "0.0.57", + "resolved": "https://registry.npmjs.org/web-ifc/-/web-ifc-0.0.57.tgz", + "integrity": "sha512-ozzfxiCNT4+VqIhm93QdTozOmkMSPRZzf1RoRMMQIQLqoidYdExnAmY1OLoq1emnbvHPqFNQJ5HnQoqxvYZeFA==" + }, "node_modules/webgl-constants": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", diff --git a/package.json b/package.json index 4e6e6ef..067e7f7 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "tailwindcss-animate": "^1.0.7", "three": "^0.160.1", "vaul": "^0.9.9", + "web-ifc": "^0.0.57", "zod": "^3.25.76", "zustand": "^4.5.7" }, diff --git a/src/lib/convertIFC.ts b/src/lib/convertIFC.ts new file mode 100644 index 0000000..75377c4 --- /dev/null +++ b/src/lib/convertIFC.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; +import * as WebIFC from 'web-ifc'; +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; + +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 }> { + const ifcApi = new WebIFC.IfcAPI(); + ifcApi.SetWasmPath(WASM_PATH, true); + await ifcApi.Init(); + + const data = new Uint8Array(buffer); + const modelID = ifcApi.OpenModel(data); + + const scene = new THREE.Scene(); + const materials: Map = new Map(); + + ifcApi.StreamAllMeshes(modelID, (mesh: WebIFC.FlatMesh) => { + const placedGeometries = mesh.geometries; + + for (let i = 0; i < placedGeometries.size(); i++) { + const placedGeometry = placedGeometries.get(i); + const ifcGeometry = ifcApi.GetGeometry(modelID, placedGeometry.geometryExpressID); + + const verts = ifcApi.GetVertexArray( + ifcGeometry.GetVertexData(), + ifcGeometry.GetVertexDataSize() + ); + const indices = ifcApi.GetIndexArray( + ifcGeometry.GetIndexData(), + ifcGeometry.GetIndexDataSize() + ); + + const geometry = new THREE.BufferGeometry(); + + // web-ifc vertex format: x, y, z, nx, ny, nz (6 floats per vertex) + const positionArray = new Float32Array(verts.length / 2); + const normalArray = new Float32Array(verts.length / 2); + + for (let j = 0; j < verts.length; j += 6) { + const idx = j / 6; + positionArray[idx * 3] = verts[j]; + positionArray[idx * 3 + 1] = verts[j + 1]; + positionArray[idx * 3 + 2] = verts[j + 2]; + normalArray[idx * 3] = verts[j + 3]; + normalArray[idx * 3 + 1] = verts[j + 4]; + normalArray[idx * 3 + 2] = verts[j + 5]; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3)); + geometry.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3)); + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + + // Get or create material based on color + const color = placedGeometry.color; + const colorKey = (color.x * 255) << 16 | (color.y * 255) << 8 | (color.z * 255); + + let material = materials.get(colorKey); + if (!material) { + material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(color.x, color.y, color.z), + metalness: 0.2, + roughness: 0.8, + transparent: color.w < 1, + opacity: color.w, + side: THREE.DoubleSide, + }); + materials.set(colorKey, material); + } + + const mesh3 = new THREE.Mesh(geometry, material); + + // Apply placement transform + const matrix = new THREE.Matrix4(); + matrix.fromArray(placedGeometry.flatTransformation); + mesh3.applyMatrix4(matrix); + + scene.add(mesh3); + + ifcGeometry.delete(); + } + }); + + ifcApi.CloseModel(modelID); + + // Export to GLB + const exporter = new GLTFExporter(); + const glb = await new Promise((resolve, reject) => { + exporter.parse( + scene, + (result) => resolve(result as ArrayBuffer), + (error) => reject(error), + { binary: true } + ); + }); + + const glbFileName = fileName.replace(/\.ifc$/i, '.glb'); + 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(); + } + }); + + return { blob, fileName: glbFileName, fileSize: blob.size }; +} diff --git a/src/lib/convertToGLB.ts b/src/lib/convertToGLB.ts index 8fd63ae..0af6522 100644 --- a/src/lib/convertToGLB.ts +++ b/src/lib/convertToGLB.ts @@ -3,15 +3,15 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; -type SupportedExt = 'glb' | 'obj' | 'stl'; +type SupportedExt = 'glb' | 'obj' | 'stl' | 'ifc'; export function getSupportedExtension(fileName: string): SupportedExt | null { const ext = fileName.split('.').pop()?.toLowerCase(); - if (ext === 'glb' || ext === 'obj' || ext === 'stl') return ext; + if (ext === 'glb' || ext === 'obj' || ext === 'stl' || ext === 'ifc') return ext; return null; } -export const ACCEPTED_EXTENSIONS = '.glb,.obj,.stl'; +export const ACCEPTED_EXTENSIONS = '.glb,.obj,.stl,.ifc'; /** * Convert an OBJ or STL file (ArrayBuffer) into a GLB Blob. diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 68d05b1..ea5919d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,6 +6,7 @@ import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; import { generateDemoBeamGLB } from '@/lib/generateDemoBeam'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; +import { convertIFCtoGLB } from '@/lib/convertIFC'; const Index = () => { const navigate = useNavigate(); @@ -28,7 +29,7 @@ const Index = () => { const ext = getSupportedExtension(file.name); if (!ext) { - toast.error('Formato inválido. Selecione um arquivo .GLB, .OBJ ou .STL'); + toast.error('Formato inválido. Selecione um arquivo .GLB, .OBJ, .STL ou .IFC'); return; } @@ -39,13 +40,18 @@ const Index = () => { return; } - // Convert OBJ/STL to GLB + // Convert OBJ/STL/IFC to GLB setConverting(true); try { const buffer = await file.arrayBuffer(); - const { blob, fileName, fileSize } = await convertToGLB(buffer, ext, file.name); - const url = URL.createObjectURL(blob); - setModel({ fileName, fileSize, url }); + let result; + if (ext === 'ifc') { + result = await convertIFCtoGLB(buffer, file.name); + } else { + result = await convertToGLB(buffer, ext, file.name); + } + const url = URL.createObjectURL(result.blob); + setModel({ fileName: result.fileName, fileSize: result.fileSize, url }); toast.success(`"${file.name}" convertido para GLB e carregado!`); } catch (err) { console.error(err); @@ -113,7 +119,7 @@ const Index = () => { {converting ? : }

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

-

GLB · OBJ · STL — Escala 1:1

+

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