From 972b38057a0572bd0dab9591b2117c78c4fbf5af Mon Sep 17 00:00:00 2001 From: balaji Date: Tue, 16 Jun 2026 16:19:50 +0530 Subject: [PATCH] Commit recent project changes --- src/App.tsx | 111 ++++++++- src/components/ApiSettingsModal.tsx | 173 ++++++++++++++ src/components/ImageTo3DModal.tsx | 182 ++++----------- src/components/VizAiModal.tsx | 334 ++++++++++++++++++++++++++++ 4 files changed, 661 insertions(+), 139 deletions(-) create mode 100644 src/components/ApiSettingsModal.tsx create mode 100644 src/components/VizAiModal.tsx diff --git a/src/App.tsx b/src/App.tsx index b5a9edd..04da2dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,15 +40,19 @@ import { Clipboard, RotateCw, Save, - FolderOpen + FolderOpen, + Sparkles } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { ModelViewer } from "./components/ModelViewer"; import { JuiceBox } from "./components/JuiceBox"; import { RenderModal, RenderSettings } from "./components/RenderModal"; +import { UnifiedMappingMaterial } from './components/UnifiedMappingMaterial'; import { ImageTo3DModal } from "./components/ImageTo3DModal"; import { UVEditor } from "./components/UVEditor"; +import { VizAiModal } from "./components/VizAiModal"; +import { ApiSettingsModal } from "./components/ApiSettingsModal"; import { EffectComposer, DepthOfField, Bloom, Vignette, BrightnessContrast, HueSaturation } from "@react-three/postprocessing"; import { cn } from "./lib/utils"; import * as THREE from "three"; @@ -412,6 +416,9 @@ export default function App() { const [backgroundType, setBackgroundType] = useState<"color" | "hdri" | "image">("color"); const [showGrid, setShowGrid] = useState(true); const [showRenderModal, setShowRenderModal] = useState(false); + const [showVizAi, setShowVizAi] = useState(false); + const [showApiSettings, setShowApiSettings] = useState(false); + const [vizAiBaseImage, setVizAiBaseImage] = useState(null); const [renderRequest, setRenderRequest] = useState(null); const [exportRequest, setExportRequest] = useState(false); const [exportFormat, setExportFormat] = useState<'glb' | 'usdz'>('glb'); @@ -1716,7 +1723,28 @@ export default function App() { Camera Image Render - Tools +
+ setOpenMenu(openMenu === 'tools' ? null : 'tools')} + > + Tools + + {openMenu === 'tools' && ( +
+ +
+ )} +
setRenderRequest(settings)} /> + setShowVizAi(false)} + baseImage={vizAiBaseImage} + /> + + setShowApiSettings(false)} + /> + {/* Export / Save Dialog */} {exportDialog && (
Render +
@@ -4749,6 +4801,61 @@ export default function App() { } }); + // Save model data for scene persistence + const modelBlob = await fetch(modelUrl).then(r => r.blob()); + const modelBase64 = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsDataURL(modelBlob); + }); + setModelData({ name: "Generated_Model.glb", data: modelBase64, extension: 'glb' }); + + const boundingBox = new THREE.Box3().setFromObject(object); + const center = new THREE.Vector3(); + boundingBox.getCenter(center); + const size = new THREE.Vector3(); + boundingBox.getSize(size); + const maxDim = Math.max(size.x || 0, size.y || 0, size.z || 0) || 1; + const radius = maxDim / 2; + const minY = boundingBox.min.y; + + setModelBounds({ center, size, maxDim, minY, radius }); + + if (directionalLightRef.current) { + const light = directionalLightRef.current; + light.shadow.camera.left = -maxDim; + light.shadow.camera.right = maxDim; + light.shadow.camera.top = maxDim; + light.shadow.camera.bottom = -maxDim; + light.shadow.camera.near = 0.1; + light.shadow.camera.far = maxDim * 5; + light.shadow.camera.updateProjectionMatrix(); + light.shadow.mapSize.width = 2048; + light.shadow.mapSize.height = 2048; + } + + let cameraZDistance = 0; + if (cameraRef.current) { + const camera = cameraRef.current; + const fovRadians = camera.fov * (Math.PI / 180); + cameraZDistance = radius / Math.sin(fovRadians / 2); + cameraZDistance *= 1.5; + + camera.position.set(center.x, center.y + (maxDim / 4), center.z + cameraZDistance); + camera.near = radius / 100; + camera.far = radius * 100; + camera.updateProjectionMatrix(); + camera.lookAt(center); + } + + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + if (center) controls.target.copy(center); + controls.minDistance = radius / 2; + controls.maxDistance = (cameraZDistance || radius * 10) * 5; + controls.update(); + } + setLoadedModel(object); setBlobURLs(prev => [...prev, modelUrl]); setSceneObjects(prev => [ diff --git a/src/components/ApiSettingsModal.tsx b/src/components/ApiSettingsModal.tsx new file mode 100644 index 0000000..a417b4d --- /dev/null +++ b/src/components/ApiSettingsModal.tsx @@ -0,0 +1,173 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Settings, Key, Sparkles, Image as ImageIcon, Lightbulb, Box } from 'lucide-react'; +import { cn } from '../lib/utils'; + +interface ApiSettingsModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const ApiSettingsModal: React.FC = ({ isOpen, onClose }) => { + const [falApiKey, setFalApiKey] = useState(''); + const [tripoApiKey, setTripoApiKey] = useState(''); + const [meshyApiKey, setMeshyApiKey] = useState(''); + const [stabilityApiKey, setStabilityApiKey] = useState(''); + const [openAiKey, setOpenAiKey] = useState(''); + + useEffect(() => { + if (isOpen) { + setFalApiKey(localStorage.getItem('FAL_API_KEY') || ''); + setTripoApiKey(localStorage.getItem('TRIPO_API_KEY') || ''); + setMeshyApiKey(localStorage.getItem('MESHY_API_KEY') || ''); + setStabilityApiKey(localStorage.getItem('stability_api_key') || ''); + setOpenAiKey(localStorage.getItem('openai_api_key') || ''); + } + }, [isOpen]); + + const saveKeys = () => { + localStorage.setItem('FAL_API_KEY', falApiKey); + localStorage.setItem('TRIPO_API_KEY', tripoApiKey); + localStorage.setItem('MESHY_API_KEY', meshyApiKey); + localStorage.setItem('stability_api_key', stabilityApiKey); + localStorage.setItem('openai_api_key', openAiKey); + onClose(); + }; + + if (!isOpen) return null; + + return ( + +
+ + {/* Header */} +
+
+
+ +
+
+

Global API Settings

+

Configure your API keys for AI generation features

+
+
+ + +
+ + {/* Content */} +
+ + {/* 3D Generation */} +
+

+ Image-to-3D Providers +

+ +
+
+
+ + For Hunyuan3D +
+ setFalApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-zinc-500" + /> +
+ +
+
+ + For Native Tripo3D +
+ setTripoApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-zinc-500" + /> +
+ +
+
+ + For Native Meshy +
+ setMeshyApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-zinc-500" + /> +
+
+
+ +
+ + {/* Viz AI (Image-to-Image & LLM) */} +
+

+ Viz AI & Mockup Generation +

+ +
+
+
+ + For Scene rendering (Image-to-Image) +
+ setStabilityApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-blue-500/50" + /> +
+ +
+
+ + For AI Idea Prompt Generation +
+ setOpenAiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-green-500/50" + /> +
+
+
+ +
+ +
+ +
+ +
+ + ); +}; diff --git a/src/components/ImageTo3DModal.tsx b/src/components/ImageTo3DModal.tsx index a4894a0..bfb407e 100644 --- a/src/components/ImageTo3DModal.tsx +++ b/src/components/ImageTo3DModal.tsx @@ -44,7 +44,6 @@ const ModelPreview = ({ url }: { url: string }) => { }; export const ImageTo3DModal: React.FC = ({ isOpen, onClose, onPushToApp }) => { - const [activeTab, setActiveTab] = useState<'generator' | 'settings'>('generator'); const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal'); const [apiKey, setApiKey] = useState(''); const [tripoApiKey, setTripoApiKey] = useState(''); @@ -59,8 +58,9 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, const [meshyShouldTexture, setMeshyShouldTexture] = useState(true); const [meshyEnablePbr, setMeshyEnablePbr] = useState(false); const [meshyHdTexture, setMeshyHdTexture] = useState(false); - const [meshyTexturePrompt, setMeshyTexturePrompt] = useState(''); + 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); @@ -120,16 +120,6 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, if (savedModel) setSelectedModel(savedModel); }, []); - const handleSaveSettings = () => { - localStorage.setItem('API_PROVIDER', apiProvider); - localStorage.setItem('FAL_API_KEY', apiKey); - localStorage.setItem('TRIPO_API_KEY', tripoApiKey); - localStorage.setItem('MESHY_API_KEY', meshyApiKey); - localStorage.setItem('FAL_MODEL', selectedModel); - if (apiProvider === 'fal') fal.config({ credentials: apiKey }); - setActiveTab('generator'); - }; - const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -157,18 +147,15 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, return; } if (apiProvider === 'fal' && !apiKey) { - setError("Fal API Key is required. Please check settings."); - setActiveTab('settings'); + setError("Fal API Key is required. Please set it in Tools > API Settings."); return; } if (apiProvider === 'tripo' && !tripoApiKey) { - setError("Tripo API Key is required. Please check settings."); - setActiveTab('settings'); + setError("Tripo API Key is required. Please set it in Tools > API Settings."); return; } if (apiProvider === 'meshy' && !meshyApiKey) { - setError("Meshy API Key is required. Please check settings."); - setActiveTab('settings'); + setError("Meshy API Key is required. Please set it in Tools > API Settings."); return; } @@ -293,7 +280,8 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, let payload: any = { image_url: dataUrl, model_type: meshyModelType, - should_texture: meshyShouldTexture + should_texture: meshyShouldTexture, + should_enhance_image: meshyEnhanceImage }; if (meshyModelType !== 'lowpoly') { @@ -387,127 +375,44 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose,
- {/* Sidebar Tabs */} -
- - -
- {/* Content Area */}
- {activeTab === 'settings' ? ( -
-
-

API Configuration

-

Configure your API credentials to access AI models.

-
- -
- - -
- - {apiProvider === 'fal' ? ( - <> -
- - setApiKey(e.target.value)} - placeholder="Enter your fal.ai API key" - className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" - /> -
- -
- - -
- - ) : apiProvider === 'tripo' ? ( -
- - setTripoApiKey(e.target.value)} - placeholder="Enter your Tripo3D API key (starts with tsk_...)" - className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" - /> -
- ) : ( -
- - setMeshyApiKey(e.target.value)} - placeholder="Enter your Meshy API key (starts with msy_...)" - className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" - /> -
- )} - -
- - {keyValidationStatus === 'valid' && Valid Key} - {keyValidationStatus === 'invalid' && Invalid Key} - {keyValidationStatus === 'error' && Network Error} -
- - -
- ) : (
{/* Left Column: Upload */}
+
+ +
+ + {apiProvider === 'fal' && ( + + )} +
+
+

Input Image

Front View @@ -600,6 +505,10 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, setMeshyShouldRemesh(e.target.checked)} disabled={meshyModelType === 'lowpoly'} className="rounded border-zinc-700 bg-zinc-900 text-blue-500 cursor-pointer" /> Remesh +
{meshyShouldRemesh && meshyModelType !== 'lowpoly' && ( @@ -678,8 +587,7 @@ export const ImageTo3DModal: React.FC = ({ isOpen, onClose, )}
- )} -
+
diff --git a/src/components/VizAiModal.tsx b/src/components/VizAiModal.tsx new file mode 100644 index 0000000..e5627ed --- /dev/null +++ b/src/components/VizAiModal.tsx @@ -0,0 +1,334 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Sparkles, Image as ImageIcon, Loader2, Download, Settings, Wand2, Lightbulb } from 'lucide-react'; +import { cn } from '../lib/utils'; + +interface VizAiModalProps { + isOpen: boolean; + onClose: () => void; + baseImage: string | null; +} + +const dataURLtoBlob = (dataurl: string) => { + const arr = dataurl.split(','); + const mime = arr[0].match(/:(.*?);/)?.[1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while(n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], {type:mime}); +}; + +export const VizAiModal: React.FC = ({ isOpen, onClose, baseImage }) => { + // API Keys + const [stabilityApiKey, setStabilityApiKey] = useState(''); + const [openAiKey, setOpenAiKey] = useState(''); + + // Prompt Generation State (AI Idea) + const [productName, setProductName] = useState(''); + const [ingredients, setIngredients] = useState(''); + const [imageTone, setImageTone] = useState(''); + const [packDesign, setPackDesign] = useState(''); + const [isGeneratingPrompt, setIsGeneratingPrompt] = useState(false); + + // Image Generation State + const [prompt, setPrompt] = useState(''); + const [strength, setStrength] = useState(0.85); + const [isGeneratingImage, setIsGeneratingImage] = useState(false); + const [generatedImage, setGeneratedImage] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen) { + setStabilityApiKey(localStorage.getItem('stability_api_key') || ''); + setOpenAiKey(localStorage.getItem('openai_api_key') || ''); + } + }, [isOpen]); + + const generateAiIdea = async () => { + if (!openAiKey) { + setError("Please set your OpenAI API key in Tools > API Settings first."); + return; + } + + setIsGeneratingPrompt(true); + setError(null); + try { + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${openAiKey}` + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [{ + role: 'system', + content: 'You are an expert prompt engineer for Stable Diffusion. Your goal is to take product specifications and write a single, highly descriptive prompt for generating a beautiful, professional product mockup scene. Describe the lighting, the background, and the aesthetic vividly.' + }, { + role: 'user', + content: `Create a single, highly descriptive image generation prompt based on these details: + Product Name: ${productName || 'Unspecified Product'} + Ingredients/Elements: ${ingredients || 'None specified'} + Image Tone/Mood: ${imageTone || 'Professional, studio lighting'} + Pack Design/Shape: ${packDesign || 'Standard'} + + Return ONLY the prompt string (under 100 words) without any conversational text or quotes.` + }] + }) + }); + + const data = await response.json(); + if (data.error) throw new Error(data.error.message); + + const generated = data.choices?.[0]?.message?.content?.trim() || ""; + // Strip quotes if they were included + setPrompt(generated.replace(/^"|"$/g, '')); + } catch (err: any) { + console.error(err); + setError(err.message || "Failed to generate AI Idea."); + } finally { + setIsGeneratingPrompt(false); + } + }; + + const generateImage = async () => { + if (!stabilityApiKey) { + setError("Please set your Stability AI API key in Tools > API Settings first."); + return; + } + if (!baseImage) { + setError("No base image captured from the scene."); + return; + } + if (!prompt) { + setError("Please enter a prompt or use AI Idea."); + return; + } + + setIsGeneratingImage(true); + setError(null); + + try { + const formData = new FormData(); + formData.append('image', dataURLtoBlob(baseImage), 'base.png'); + formData.append('prompt', prompt); + formData.append('strength', strength.toString()); + formData.append('mode', 'image-to-image'); + formData.append('model', 'sd3.5-large'); + formData.append('output_format', 'png'); + + const response = await fetch('https://api.stability.ai/v2beta/stable-image/generate/sd3', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${stabilityApiKey}`, + 'Accept': 'image/*' + }, + body: formData + }); + + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error(errData.message || `Stability API error: ${response.status}`); + } + + const blob = await response.blob(); + const imageUrl = URL.createObjectURL(blob); + setGeneratedImage(imageUrl); + } catch (err: any) { + console.error(err); + setError(err.message || "Failed to generate image."); + } finally { + setIsGeneratingImage(false); + } + }; + + if (!isOpen) return null; + + return ( + +
+ + {/* Header */} +
+
+
+ +
+
+

VIZ AI Generator

+

Generate stunning product mockups directly from your 3D scene

+
+
+ +
+ +
+
+ +
+
+ {/* Left Panel: Inputs */} +
+ {/* AI Idea Box */} +
+
+ +

AI Idea Generator

+
+ +
+ setProductName(e.target.value)} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-3 py-2 text-xs text-white placeholder-zinc-600 focus:border-yellow-500/50 outline-none" + /> + setIngredients(e.target.value)} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-3 py-2 text-xs text-white placeholder-zinc-600 focus:border-yellow-500/50 outline-none" + /> + setImageTone(e.target.value)} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-3 py-2 text-xs text-white placeholder-zinc-600 focus:border-yellow-500/50 outline-none" + /> + setPackDesign(e.target.value)} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-3 py-2 text-xs text-white placeholder-zinc-600 focus:border-yellow-500/50 outline-none" + /> + +
+
+ + {/* Main Generation Settings */} +
+
+ +