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
+6
View File
@@ -61,6 +61,7 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.160.1", "three": "^0.160.1",
"vaul": "^0.9.9", "vaul": "^0.9.9",
"web-ifc": "^0.0.57",
"zod": "^3.25.76", "zod": "^3.25.76",
"zustand": "^4.5.7" "zustand": "^4.5.7"
}, },
@@ -8977,6 +8978,11 @@
"node": ">=14" "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": { "node_modules/webgl-constants": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+1
View File
@@ -66,6 +66,7 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.160.1", "three": "^0.160.1",
"vaul": "^0.9.9", "vaul": "^0.9.9",
"web-ifc": "^0.0.57",
"zod": "^3.25.76", "zod": "^3.25.76",
"zustand": "^4.5.7" "zustand": "^4.5.7"
}, },
+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 { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.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 { export function getSupportedExtension(fileName: string): SupportedExt | null {
const ext = fileName.split('.').pop()?.toLowerCase(); 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; 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. * 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 { toast } from 'sonner';
import { generateDemoBeamGLB } from '@/lib/generateDemoBeam'; import { generateDemoBeamGLB } from '@/lib/generateDemoBeam';
import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB';
import { convertIFCtoGLB } from '@/lib/convertIFC';
const Index = () => { const Index = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -28,7 +29,7 @@ const Index = () => {
const ext = getSupportedExtension(file.name); const ext = getSupportedExtension(file.name);
if (!ext) { 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; return;
} }
@@ -39,13 +40,18 @@ const Index = () => {
return; return;
} }
// Convert OBJ/STL to GLB // Convert OBJ/STL/IFC to GLB
setConverting(true); setConverting(true);
try { try {
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const { blob, fileName, fileSize } = await convertToGLB(buffer, ext, file.name); let result;
const url = URL.createObjectURL(blob); if (ext === 'ifc') {
setModel({ fileName, fileSize, url }); 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!`); toast.success(`"${file.name}" convertido para GLB e carregado!`);
} catch (err) { } catch (err) {
console.error(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" />} {converting ? <Loader2 className="h-8 w-8 animate-spin" /> : <Upload className="h-8 w-8" />}
<div className="text-center"> <div className="text-center">
<p className="text-sm font-semibold">{converting ? 'Convertendo modelo…' : 'Importar Modelo 3D'}</p> <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> </div>
</Button> </Button>