feat: Add Clean Image (AI) button to pre-process images before Meshy 3D generation
Build and Deploy / build-and-push (push) Successful in 1m11s
Build and Deploy / build-and-push (push) Successful in 1m11s
This commit is contained in:
@@ -5,6 +5,7 @@ import { fal } from '@fal-ai/client';
|
|||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
import { editUVLayer } from '../lib/api/apiUtils';
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { OrbitControls, Stage } from '@react-three/drei';
|
import { OrbitControls, Stage } from '@react-three/drei';
|
||||||
import { GLTFLoader } from 'three-stdlib';
|
import { GLTFLoader } from 'three-stdlib';
|
||||||
@@ -46,7 +47,7 @@ const ModelPreview = ({ url }: { url: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose, onPushToApp }) => {
|
export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose, onPushToApp }) => {
|
||||||
const { profile, setProfile } = useAuthStore();
|
const { profile, setProfile, token } = useAuthStore();
|
||||||
const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal');
|
const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal');
|
||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
const [tripoApiKey, setTripoApiKey] = useState('');
|
const [tripoApiKey, setTripoApiKey] = useState('');
|
||||||
@@ -72,6 +73,7 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
|
|||||||
const [progressMessage, setProgressMessage] = useState('');
|
const [progressMessage, setProgressMessage] = useState('');
|
||||||
const [generatedModelUrl, setGeneratedModelUrl] = useState<string | null>(null);
|
const [generatedModelUrl, setGeneratedModelUrl] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isCleaningImage, setIsCleaningImage] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isValidatingKey, setIsValidatingKey] = useState(false);
|
const [isValidatingKey, setIsValidatingKey] = useState(false);
|
||||||
const [keyValidationStatus, setKeyValidationStatus] = useState<'idle' | 'valid' | 'invalid' | 'error'>('idle');
|
const [keyValidationStatus, setKeyValidationStatus] = useState<'idle' | 'valid' | 'invalid' | 'error'>('idle');
|
||||||
@@ -137,6 +139,29 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCleanImage = async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!imageFile || !token) return;
|
||||||
|
|
||||||
|
setIsCleaningImage(true);
|
||||||
|
try {
|
||||||
|
const base64 = await fileToDataUrl(imageFile);
|
||||||
|
const newUrl = await editUVLayer("Remove all text, labels, and logos from this object, leaving a clean plain surface of the exact same shape.", base64, token);
|
||||||
|
|
||||||
|
const res = await fetch(newUrl);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const newFile = new File([blob], "cleaned-image.png", { type: "image/png" });
|
||||||
|
|
||||||
|
setImageFile(newFile);
|
||||||
|
setImagePreview(newUrl);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert("Failed to clean image: " + err.message);
|
||||||
|
} finally {
|
||||||
|
setIsCleaningImage(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Utility to convert file to data URL for Fal API if needed
|
// Utility to convert file to data URL for Fal API if needed
|
||||||
const fileToDataUrl = (file: File): Promise<string> => {
|
const fileToDataUrl = (file: File): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -453,8 +478,16 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
|
|||||||
{imagePreview ? (
|
{imagePreview ? (
|
||||||
<>
|
<>
|
||||||
<img src={imagePreview} alt="Preview" className="absolute inset-0 w-full h-full object-contain p-2" />
|
<img src={imagePreview} alt="Preview" className="absolute inset-0 w-full h-full object-contain p-2" />
|
||||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex flex-col items-center justify-center gap-3 transition-opacity">
|
||||||
<span className="text-white text-xs font-medium flex items-center gap-2"><Upload size={14}/> Change Image</span>
|
<span className="text-white text-xs font-medium flex items-center gap-2"><Upload size={14}/> Change Image</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCleanImage}
|
||||||
|
disabled={isCleaningImage}
|
||||||
|
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded text-xs font-bold flex items-center gap-2 transition-colors shadow-lg shadow-black/50"
|
||||||
|
>
|
||||||
|
{isCleaningImage ? <Loader2 size={12} className="animate-spin" /> : null}
|
||||||
|
Clean Image (AI)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user