feat: allow reference images for AI layer edits and relax stylistic constraints to allow for realism
Build and Deploy / build-and-push (push) Successful in 2m9s
Build and Deploy / build-and-push (push) Successful in 2m9s
This commit is contained in:
@@ -46,6 +46,7 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const refImageInputRef = useRef<HTMLInputElement>(null);
|
||||
const dragRef = useRef<{ handle: Handle; startX: number; startY: number; layer: Layer } | null>(null);
|
||||
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
@@ -53,6 +54,8 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
||||
const [isEditingLayer, setIsEditingLayer] = useState(false);
|
||||
const [aiPreviewUrl, setAiPreviewUrl] = useState<string | null>(null);
|
||||
const [originalLayerUrl, setOriginalLayerUrl] = useState<string | null>(null);
|
||||
const [aiReferenceImage, setAiReferenceImage] = useState<File | null>(null);
|
||||
const [aiReferencePreview, setAiReferencePreview] = useState<string | null>(null);
|
||||
|
||||
const handleAiEdit = async (regenerate = false) => {
|
||||
if (!selectedId || !aiPrompt.trim()) return;
|
||||
@@ -77,11 +80,40 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
||||
// Use webp to preserve transparency while significantly reducing payload size
|
||||
const base64 = tempCanvas.toDataURL('image/webp', 0.8);
|
||||
|
||||
const fullPrompt = `The user has provided an image layer. Please apply the following edit exactly as requested, while strictly maintaining the EXACT same aspect ratio, resolution, and general structure of the original image.
|
||||
let referenceBase64 = undefined;
|
||||
if (aiReferenceImage) {
|
||||
const reader = new FileReader();
|
||||
referenceBase64 = await new Promise<string>((resolve, reject) => {
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const refCanvas = document.createElement('canvas');
|
||||
let rw = img.width;
|
||||
let rh = img.height;
|
||||
if (rw > 1024 || rh > 1024) {
|
||||
const rRatio = Math.min(1024 / rw, 1024 / rh);
|
||||
rw = Math.round(rw * rRatio);
|
||||
rh = Math.round(rh * rRatio);
|
||||
}
|
||||
refCanvas.width = rw;
|
||||
refCanvas.height = rh;
|
||||
const rctx = refCanvas.getContext('2d');
|
||||
rctx?.drawImage(img, 0, 0, rw, rh);
|
||||
resolve(refCanvas.toDataURL('image/webp', 0.8));
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(aiReferenceImage);
|
||||
});
|
||||
}
|
||||
|
||||
const fullPrompt = `The user has provided an image layer. Please apply the following edit exactly as requested, while strictly maintaining the EXACT same aspect ratio and resolution of the original image. You may change the style, aesthetic, or realism.
|
||||
User Prompt: "${aiPrompt}"
|
||||
Negative constraints: Do NOT change the aspect ratio. Do NOT add completely unrelated elements. Do NOT crop the image boundaries.`;
|
||||
|
||||
const newUrl = await editUVLayer(fullPrompt, base64, token || '');
|
||||
const newUrl = await editUVLayer(fullPrompt, base64, token || '', referenceBase64);
|
||||
|
||||
if (!originalLayerUrl && !regenerate) {
|
||||
setOriginalLayerUrl(layer.url);
|
||||
@@ -108,6 +140,8 @@ Negative constraints: Do NOT change the aspect ratio. Do NOT add completely unre
|
||||
setAiPreviewUrl(null);
|
||||
setOriginalLayerUrl(null);
|
||||
setAiPrompt("");
|
||||
setAiReferenceImage(null);
|
||||
setAiReferencePreview(null);
|
||||
};
|
||||
|
||||
const handleRejectEdit = () => {
|
||||
@@ -118,11 +152,23 @@ Negative constraints: Do NOT change the aspect ratio. Do NOT add completely unre
|
||||
setLayers(ls => ls.map(l => l.id === selectedId ? { ...l, url: originalLayerUrl, img } : l));
|
||||
setAiPreviewUrl(null);
|
||||
setOriginalLayerUrl(null);
|
||||
setAiReferenceImage(null);
|
||||
setAiReferencePreview(null);
|
||||
};
|
||||
img.src = originalLayerUrl;
|
||||
} else {
|
||||
setAiPreviewUrl(null);
|
||||
setOriginalLayerUrl(null);
|
||||
setAiReferenceImage(null);
|
||||
setAiReferencePreview(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setAiReferenceImage(file);
|
||||
setAiReferencePreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -526,13 +572,33 @@ Negative constraints: Do NOT change the aspect ratio. Do NOT add completely unre
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleAiEdit(false)}
|
||||
disabled={!aiPrompt.trim()}
|
||||
className="w-full py-1.5 bg-blue-500/10 hover:bg-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed rounded text-[11px] font-semibold text-blue-400 border border-blue-500/30 transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span className="text-[14px] leading-none">✨</span> Generate Edit
|
||||
</button>
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="file" ref={refImageInputRef} className="hidden" accept=".jpg,.jpeg,.png,.webp" onChange={handleRefImageUpload} />
|
||||
<button
|
||||
onClick={() => refImageInputRef.current?.click()}
|
||||
className="flex-1 py-1.5 bg-zinc-800 hover:bg-zinc-700 rounded text-[10px] font-semibold text-zinc-300 border border-zinc-700 transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<Upload size={12} /> {aiReferencePreview ? 'Change Ref' : 'Optional Reference Image'}
|
||||
</button>
|
||||
{aiReferencePreview && (
|
||||
<div className="relative w-7 h-7 rounded overflow-hidden border border-zinc-600 flex-shrink-0 group">
|
||||
<img src={aiReferencePreview} alt="Ref" className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 cursor-pointer"
|
||||
onClick={() => { setAiReferenceImage(null); setAiReferencePreview(null); }}>
|
||||
<Trash2 size={10} className="text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleAiEdit(false)}
|
||||
disabled={!aiPrompt.trim()}
|
||||
className="w-full py-1.5 bg-blue-500/10 hover:bg-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed rounded text-[11px] font-semibold text-blue-400 border border-blue-500/30 transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span className="text-[14px] leading-none">✨</span> Generate Edit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user