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 parseIFCtoThree(buffer: ArrayBuffer): Promise { 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; const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0; // One Group per IfcProduct → represents a single "element" (beam, plate…). // userData is preserved by GLTFExporter as `extras` and survives reload. const elementGroup = new THREE.Group(); elementGroup.name = `ifc_${expressID}`; elementGroup.userData = { ifcElement: true, ifcId: expressID }; 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(); 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)); 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); mesh3.userData = { ifcId: expressID }; const matrix = new THREE.Matrix4(); matrix.fromArray(placedGeometry.flatTransformation); mesh3.applyMatrix4(matrix); elementGroup.add(mesh3); ifcGeometry.delete(); } if (elementGroup.children.length > 0) scene.add(elementGroup); }); 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(); 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 scene.traverse((child) => { if (child instanceof THREE.Mesh) { child.geometry.dispose(); } }); return { blob, fileName: glbFileName, fileSize: blob.size }; }