Files
SteelXR/src/lib/convertIFC.ts
T

124 lines
4.1 KiB
TypeScript

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<THREE.Scene> {
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<number, THREE.MeshStandardMaterial> = 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<ArrayBuffer>((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 };
}