Add IFC to GLB conversion
Implemented client-side IFC support: - Added convertIFC.ts to parse IFC files via web-ifc and export as GLB using GLTFExporter - Extended conversion utility to handle IFC and produce GLB blob - Updated file import flow to accept .ifc, convert to GLB, and load - Updated UI text and accepted extensions to include IFC - Wired IFC path into existing index page logic for seamless conversion and loading X-Lovable-Edit-ID: edt-acd42c51-1cf4-4cbc-8e7b-c5c79ddb3d49
This commit is contained in:
Generated
+6
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<number, THREE.MeshStandardMaterial> = 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<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
|
||||
materials.forEach((m) => m.dispose());
|
||||
scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return { blob, fileName: glbFileName, fileSize: blob.size };
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
+12
-6
@@ -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 ? <Loader2 className="h-8 w-8 animate-spin" /> : <Upload className="h-8 w-8" />}
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold">{converting ? 'Convertendo modelo…' : 'Importar Modelo 3D'}</p>
|
||||
<p className="text-xs text-muted-foreground">GLB · OBJ · STL — Escala 1:1</p>
|
||||
<p className="text-xs text-muted-foreground">GLB · OBJ · STL · IFC — Escala 1:1</p>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user