This commit is contained in:
gpt-engineer-app[bot]
2026-02-27 02:36:49 +00:00
parent 09d6b381a4
commit 11f16eff5a
5 changed files with 137 additions and 9 deletions
+115
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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>