Add procedural I-beam demo
Implements a GLB demo generator for a steel I-beam (IPE 200) and a UI button to load it as a sample model. Adds generateDemoBeamGLB utility, integrates with Index.tsx to generate and load the demo GLB, and updates UI state to handle loading. X-Lovable-Edit-ID: edt-5a8e10ef-027a-40be-9853-f8bc55818d44
This commit is contained in:
@@ -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<ArrayBuffer>((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,
|
||||||
|
};
|
||||||
|
}
|
||||||
+35
-3
@@ -1,13 +1,15 @@
|
|||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { useModelStore } from '@/stores/useModelStore';
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { generateDemoBeamGLB } from '@/lib/generateDemoBeam';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [loadingDemo, setLoadingDemo] = useState(false);
|
||||||
const { model, setModel, xrSupported, setXrSupported } = useModelStore();
|
const { model, setModel, xrSupported, setXrSupported } = useModelStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,6 +32,21 @@ const Index = () => {
|
|||||||
toast.success(`Modelo "${file.name}" carregado com sucesso!`);
|
toast.success(`Modelo "${file.name}" carregado com sucesso!`);
|
||||||
}, [setModel]);
|
}, [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 = () => {
|
const handleEnterViewer = () => {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
toast.error('Importe um modelo GLB primeiro');
|
toast.error('Importe um modelo GLB primeiro');
|
||||||
@@ -76,7 +93,22 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Model info */}
|
{/* Demo button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-12 w-full gap-3 border-muted-foreground/30 text-muted-foreground hover:border-primary hover:text-primary transition-all"
|
||||||
|
onClick={handleLoadDemo}
|
||||||
|
disabled={loadingDemo}
|
||||||
|
>
|
||||||
|
{loadingDemo ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{loadingDemo ? 'Gerando modelo…' : 'Carregar Demo — Viga IPE 200'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
{model && (
|
{model && (
|
||||||
<div className="rounded-lg border border-primary/30 bg-primary/5 p-4 glow-primary">
|
<div className="rounded-lg border border-primary/30 bg-primary/5 p-4 glow-primary">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user