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",
|
"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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 { 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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user