diff --git a/src/lib/generateDemoBeam.ts b/src/lib/generateDemoBeam.ts new file mode 100644 index 0000000..3b916b8 --- /dev/null +++ b/src/lib/generateDemoBeam.ts @@ -0,0 +1,63 @@ +import * as THREE from 'three'; +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; + +/** + * Generates a steel I-beam (IPE 200) as a GLB blob. + * Dimensions in meters (model is 1:1 scale, original units mm). + * IPE 200: h=200mm, b=100mm, tw=5.6mm, tf=8.5mm, length=1000mm + */ +export async function generateDemoBeamGLB(): Promise<{ blob: Blob; fileName: string; fileSize: number }> { + const scene = new THREE.Scene(); + + // IPE 200 dimensions in meters + const h = 0.200; // height + const b = 0.100; // flange width + const tw = 0.0056; // web thickness + const tf = 0.0085; // flange thickness + const L = 1.0; // length (1000mm) + + const material = new THREE.MeshStandardMaterial({ + color: 0x8899aa, + metalness: 0.85, + roughness: 0.35, + }); + + // Web (vertical plate in the center) + const webHeight = h - 2 * tf; + const webGeo = new THREE.BoxGeometry(L, webHeight, tw); + const web = new THREE.Mesh(webGeo, material); + web.position.set(0, 0, 0); + + // Top flange + const topFlangeGeo = new THREE.BoxGeometry(L, tf, b); + const topFlange = new THREE.Mesh(topFlangeGeo, material); + topFlange.position.set(0, (webHeight + tf) / 2, 0); + + // Bottom flange + const bottomFlangeGeo = new THREE.BoxGeometry(L, tf, b); + const bottomFlange = new THREE.Mesh(bottomFlangeGeo, material); + bottomFlange.position.set(0, -(webHeight + tf) / 2, 0); + + const group = new THREE.Group(); + group.name = 'IPE200_Steel_Beam'; + group.add(web, topFlange, bottomFlange); + scene.add(group); + + // Export as GLB + const exporter = new GLTFExporter(); + const glb = await new Promise((resolve, reject) => { + exporter.parse( + scene, + (result) => resolve(result as ArrayBuffer), + (error) => reject(error), + { binary: true } + ); + }); + + const blob = new Blob([glb], { type: 'model/gltf-binary' }); + return { + blob, + fileName: 'Demo_IPE200_1000mm.glb', + fileSize: blob.size, + }; +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 8151106..85b9ece 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,13 +1,15 @@ -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box } from 'lucide-react'; +import { Upload, Glasses, CheckCircle, XCircle, Loader2, Box, Package } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; +import { generateDemoBeamGLB } from '@/lib/generateDemoBeam'; const Index = () => { const navigate = useNavigate(); const fileInputRef = useRef(null); + const [loadingDemo, setLoadingDemo] = useState(false); const { model, setModel, xrSupported, setXrSupported } = useModelStore(); useEffect(() => { @@ -30,6 +32,21 @@ const Index = () => { toast.success(`Modelo "${file.name}" carregado com sucesso!`); }, [setModel]); + const handleLoadDemo = useCallback(async () => { + setLoadingDemo(true); + try { + const { blob, fileName, fileSize } = await generateDemoBeamGLB(); + const url = URL.createObjectURL(blob); + setModel({ fileName, fileSize, url }); + toast.success('Modelo demo "IPE 200 — 1000mm" carregado!'); + } catch (err) { + console.error(err); + toast.error('Falha ao gerar modelo demo'); + } finally { + setLoadingDemo(false); + } + }, [setModel]); + const handleEnterViewer = () => { if (!model) { toast.error('Importe um modelo GLB primeiro'); @@ -76,7 +93,22 @@ const Index = () => { - {/* Model info */} + {/* Demo button */} + + + {model && (