import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Upload, Settings as SettingsIcon, Box, AlertCircle, Loader2, RefreshCw, Check } from 'lucide-react'; import { cn } from '../lib/utils'; import { useAuthStore } from '../store/authStore'; import { api } from '../lib/api'; import { editUVLayer } from '../lib/api/apiUtils'; import { Canvas } from '@react-three/fiber'; import { OrbitControls, Stage } from '@react-three/drei'; import { GLTFLoader } from 'three-stdlib'; import * as THREE from 'three'; interface ImageTo3DModalProps { isOpen: boolean; onClose: () => void; onPushToApp: (modelUrl: string) => void; } const ModelPreview = ({ url }: { url: string }) => { const [model, setModel] = useState(null); useEffect(() => { const loader = new GLTFLoader(); loader.load(url, (gltf) => { setModel(gltf.scene); }); }, [url]); if (!model) return
; return ( ); }; export const ImageTo3DModal: React.FC = ({ isOpen, onClose, onPushToApp }) => { const { profile, setProfile } = useAuthStore(); const [meshyApiKey, setMeshyApiKey] = useState(''); // Image states const [imageFile, setImageFile] = useState(null); const [originalImagePreview, setOriginalImagePreview] = useState(null); const [cleanedImageFile, setCleanedImageFile] = useState(null); const [cleanedImagePreview, setCleanedImagePreview] = useState(null); const [isCleaningImage, setIsCleaningImage] = useState(false); // Meshy specific state const [meshyModelType, setMeshyModelType] = useState('standard'); const [meshyAiModel, setMeshyAiModel] = useState('latest'); const [meshyShouldTexture, setMeshyShouldTexture] = useState(true); const [meshyEnablePbr, setMeshyEnablePbr] = useState(false); const [meshyHdTexture, setMeshyHdTexture] = useState(false); const [meshyTexturePrompt, setMeshyTexturePrompt] = useState('clear text'); const [meshyTextureImageUrl, setMeshyTextureImageUrl] = useState(''); const [meshyEnhanceImage, setMeshyEnhanceImage] = useState(true); const [meshyShouldRemesh, setMeshyShouldRemesh] = useState(false); const [meshyTopology, setMeshyTopology] = useState('triangle'); const [meshyTargetPolycount, setMeshyTargetPolycount] = useState(30000); const [isGenerating, setIsGenerating] = useState(false); const [progressMessage, setProgressMessage] = useState(''); const [generatedModelUrl, setGeneratedModelUrl] = useState(null); const [error, setError] = useState(null); const fileInputRef = useRef(null); useEffect(() => { if (isOpen) { api.getUserKeys().then(keys => { if (keys.MESHY_API_KEY) setMeshyApiKey(keys.MESHY_API_KEY); }).catch(err => console.error("Failed to load user keys", err)); } }, [isOpen]); const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setImageFile(file); const url = URL.createObjectURL(file); setOriginalImagePreview(url); setGeneratedModelUrl(null); setError(null); // Auto clean on upload handleCleanImage(file); } }; const handleCleanImage = async (fileToClean: File) => { setIsCleaningImage(true); setCleanedImageFile(null); setCleanedImagePreview(null); try { const base64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); // Scale down if too large to fit in 1MB limit safely let { width, height } = img; const maxSize = 1024; if (width > maxSize || height > maxSize) { const ratio = Math.min(maxSize / width, maxSize / height); width = Math.round(width * ratio); height = Math.round(height * ratio); } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/webp', 0.8)); }; img.onerror = reject; img.src = e.target?.result as string; }; reader.onerror = reject; reader.readAsDataURL(fileToClean); }); const token = localStorage.getItem('auth_token') || ''; const newUrl = await editUVLayer("Remove the background and make it solid transparent or white. Also carefully remove all text, labels, logos, and stickers, keeping the product as a perfectly clean, plain, unbranded object.", base64, token); const res = await fetch(newUrl); const blob = await res.blob(); const newFile = new File([blob], "cleaned-image.png", { type: "image/png" }); setCleanedImageFile(newFile); setCleanedImagePreview(newUrl); } catch (err: any) { setError("Failed to clean image: " + err.message); // Fallback to original setCleanedImageFile(fileToClean); setCleanedImagePreview(URL.createObjectURL(fileToClean)); } finally { setIsCleaningImage(false); } }; const fileToDataUrl = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); }; const generate3DModel = async () => { const targetFile = cleanedImageFile || imageFile; if (!targetFile) { setError("Please upload an image first."); return; } if (!profile || profile.api_credits <= 0) { setError("Not enough AI Credits. Please contact an administrator."); return; } if (!meshyApiKey) { setError("Meshy API Key is required. Please set it in Tools > API Settings."); return; } const deductCredit = async () => { if (profile) { try { const { api_credits } = await api.deductCredit(); setProfile({ ...profile, api_credits }); } catch (e) { console.error("Credit deduction failed", e); } } }; try { setIsGenerating(true); setError(null); const dataUrl = await fileToDataUrl(targetFile); setProgressMessage("Generating 3D Model with Meshy... This might take up to a minute."); let payload: any = { image_url: dataUrl, model_type: meshyModelType, should_texture: meshyShouldTexture, should_enhance_image: meshyEnhanceImage, target_formats: ['glb'] }; if (meshyModelType !== 'lowpoly') { payload.ai_model = meshyAiModel === 'latest' ? 'meshy-6' : meshyAiModel; payload.should_remesh = meshyShouldRemesh; if (meshyShouldRemesh) { payload.topology = meshyTopology; payload.target_polycount = meshyTargetPolycount; } } if (meshyShouldTexture) { payload.enable_pbr = meshyEnablePbr; if (meshyAiModel !== 'meshy-5') { payload.hd_texture = meshyHdTexture; } if (meshyTextureImageUrl) { payload.texture_image_url = meshyTextureImageUrl; } else if (meshyTexturePrompt) { payload.texture_prompt = meshyTexturePrompt; } } const taskRes = await fetch('https://api.meshy.ai/openapi/v1/image-to-3d', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${meshyApiKey}` }, body: JSON.stringify(payload) }); if (!taskRes.ok) { const errData = await taskRes.text(); console.error("Meshy task creation error payload:", errData); throw new Error(`Meshy API: Task creation failed. ${errData}`); } const taskData = await taskRes.json(); const taskId = taskData.result; let isDone = false; while (!isDone) { await new Promise(r => setTimeout(r, 2000)); const pollRes = await fetch(`https://api.meshy.ai/openapi/v1/image-to-3d/${taskId}`, { headers: { 'Authorization': `Bearer ${meshyApiKey}` } }); if (!pollRes.ok) throw new Error("Meshy API: Polling failed."); const pollData = await pollRes.json(); const status = (pollData.status || '').toUpperCase(); const progress = pollData.progress || 0; setProgressMessage(`Generating... ${progress}%`); if (status === 'SUCCEEDED') { const modelUrl = pollData.model_urls?.glb; if (!modelUrl) throw new Error("Meshy API: Model URL not found in result."); setGeneratedModelUrl(`/api/proxy/glb?url=${encodeURIComponent(modelUrl)}`); setProgressMessage("Done!"); isDone = true; deductCredit(); } else if (status === 'FAILED' || status === 'EXPIRED') { throw new Error(`Meshy API Task ${status}: ${pollData.task_error?.message || 'Unknown error'}`); } } } catch (err: any) { console.error(err); setError(err.message || "An error occurred during generation."); } finally { setIsGenerating(false); } }; if (!isOpen) return null; return (
{/* Header */}
Meshy 3D Generation
{/* Content Area */}
{/* Left Column: Upload & Clean */}

Input Image (Auto Cleaned)

Front View
{!originalImagePreview ? ( ) : (
{/* Original Image */} {/* Cleaned Image */}
{isCleaningImage ? ( <> Original Grayscale
AI Cleaning Image...
) : cleanedImagePreview ? ( <> Cleaned
Cleaned Result
) : ( Failed to clean. )}
)}

Meshy Config

{meshyShouldTexture && (
setMeshyTexturePrompt(e.target.value)} maxLength={600} placeholder="Texture prompt (optional)..." className="bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-300" />
)}
{meshyShouldRemesh && meshyModelType !== 'lowpoly' && (
setMeshyTargetPolycount(parseInt(e.target.value) || 30000)} className="bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-300" />
)}
{error && (

{error}

)}
{/* Right Column: 3D Preview */}

3D Preview

{isGenerating ? ( <> {cleanedImagePreview || originalImagePreview ? ( Generating Base ) : null}

{progressMessage}

) : generatedModelUrl ? ( ) : (

Your 3D model will appear here

)}
{/* Footer actions */}
Meshy Generation: 200 AI Credits
{generatedModelUrl ? ( ) : ( )}
); };