import React, { useState, useEffect, useRef, useCallback } from 'react'; import { UploadedImage } from '../types'; import { segmentSubject } from '../services/geminiService'; import { ArrowLeftIcon } from './icons/ArrowLeftIcon'; import { ArrowRightIcon } from './icons/ArrowRightIcon'; import { BrushIcon } from './icons/BrushIcon'; import { EraserIcon } from './icons/EraserIcon'; import { useLogger } from '../contexts/LoggingContext'; interface ImageSegmenterProps { images: UploadedImage[]; onComplete: (images: UploadedImage[]) => void; onBack: () => void; } type EditorMode = 'brush' | 'eraser'; const ImageSegmenter: React.FC = ({ images, onComplete, onBack }) => { const [internalImages, setInternalImages] = useState(images); const [loadingStates, setLoadingStates] = useState>({}); const [errorStates, setErrorStates] = useState>({}); const [activeIndex, setActiveIndex] = useState(0); const [mode, setMode] = useState('brush'); const [brushSize, setBrushSize] = useState(20); const canvasRef = useRef(null); const imageRef = useRef(null); const maskRef = useRef(null); const isDrawing = useRef(false); const { log } = useLogger(); const generateMask = useCallback(async (index: number) => { setLoadingStates(prev => ({ ...prev, [index]: true })); setErrorStates(prev => ({ ...prev, [index]: null })); log('info', `Requesting segmentation mask for image ${index + 1} ("${internalImages[index].subjectDescription}").`); try { const maskBase64 = await segmentSubject(internalImages[index].file, internalImages[index].subjectDescription); setInternalImages(prev => { const updated = [...prev]; updated[index].maskDataUrl = `data:image/png;base64,${maskBase64}`; return updated; }); log('success', `Successfully received segmentation mask for image ${index + 1}.`); } catch (e) { const errorMessage = e instanceof Error ? e.message : 'Mask generation failed'; let displayError = "Failed"; if (errorMessage.includes("The AI returned a message")) { displayError = "AI Response Error"; } else if (errorMessage.includes("No segmentation mask")) { displayError = "No Mask Found"; } setErrorStates(prev => ({ ...prev, [index]: displayError })); log('error', `Failed to generate mask for image ${index + 1}: ${errorMessage}`); } finally { setLoadingStates(prev => ({ ...prev, [index]: false })); } }, [internalImages, log]); useEffect(() => { internalImages.forEach((image, index) => { if (!image.maskDataUrl && !loadingStates[index] && !errorStates[index]) { generateMask(index); } }); }, [internalImages, generateMask, loadingStates, errorStates]); const draw = useCallback(() => { const canvas = canvasRef.current; const originalImage = imageRef.current; const maskImage = maskRef.current; if (!canvas || !originalImage || !maskImage) return; const ctx = canvas.getContext('2d'); if (!ctx) return; if (originalImage.naturalWidth === 0 || maskImage.naturalWidth === 0 || !originalImage.complete) { return; } const { naturalWidth: w, naturalHeight: h } = originalImage; if(canvas.width !== w) canvas.width = w; if(canvas.height !== h) canvas.height = h; ctx.clearRect(0, 0, w, h); // Use a temporary canvas for the overlay so we don't mess up the main canvas's state const overlayCanvas = document.createElement('canvas'); overlayCanvas.width = w; overlayCanvas.height = h; const overlayCtx = overlayCanvas.getContext('2d'); if (!overlayCtx) return; // Fill the overlay with a semi-transparent black overlayCtx.fillStyle = 'rgba(0, 0, 0, 0.6)'; overlayCtx.fillRect(0, 0, w, h); // Use 'destination-out' to punch a hole in the overlay where the mask is white overlayCtx.globalCompositeOperation = 'destination-out'; overlayCtx.drawImage(maskImage, 0, 0, w, h); // Draw the original image on the main canvas ctx.drawImage(originalImage, 0, 0, w, h); // Draw the overlay (with the hole punched out) on top ctx.drawImage(overlayCanvas, 0, 0); }, []); useEffect(() => { const activeImage = internalImages[activeIndex]; if (activeImage?.previewUrl && activeImage?.maskDataUrl) { const originalImage = new Image(); const maskImage = new Image(); imageRef.current = originalImage; maskRef.current = maskImage; originalImage.src = activeImage.previewUrl; maskImage.src = activeImage.maskDataUrl; const loadImages = Promise.all([ new Promise((res, rej) => { originalImage.onload = res; originalImage.onerror = rej; }), new Promise((res, rej) => { maskImage.onload = res; maskImage.onerror = rej; }) ]); loadImages.then(() => { draw(); }).catch(err => { console.error("Error loading images for canvas: ", err); log('error', `Canvas Error: Failed to load images for editor view. ${err}`); }); } }, [activeIndex, internalImages, draw, log]); const handleCanvasInteraction = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing.current && e.type !== 'mousedown' && e.type !== 'touchstart') return; const canvas = canvasRef.current; if (!canvas || !maskRef.current) return; const tempCanvas = document.createElement('canvas'); tempCanvas.width = maskRef.current.naturalWidth; tempCanvas.height = maskRef.current.naturalHeight; const tempCtx = tempCanvas.getContext('2d'); if (!tempCtx) return; tempCtx.drawImage(maskRef.current, 0, 0); const rect = canvas.getBoundingClientRect(); const scaleX = tempCanvas.width / rect.width; const scaleY = tempCanvas.height / rect.height; const getCoords = (evt: any) => { if (evt.touches) { return { x: evt.touches[0].clientX - rect.left, y: evt.touches[0].clientY - rect.top }; } return { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; } const {x, y} = getCoords(e.nativeEvent); tempCtx.fillStyle = mode === 'brush' ? '#FFFFFF' : '#000000'; tempCtx.beginPath(); tempCtx.arc(x * scaleX, y * scaleY, (brushSize/2) * scaleX, 0, 2 * Math.PI); tempCtx.fill(); const newMaskUrl = tempCanvas.toDataURL(); maskRef.current.src = newMaskUrl; maskRef.current.onload = () => { draw(); // Update state debounced or on mouse up for performance if (e.type === 'mouseup' || e.type === 'touchend') { setInternalImages(prev => { const updated = [...prev]; updated[activeIndex].maskDataUrl = newMaskUrl; return updated; }); } }; }; const startDrawing = () => { isDrawing.current = true; }; const stopDrawing = (e: any) => { isDrawing.current = false; handleCanvasInteraction(e); }; const canProceed = internalImages.every(img => img.maskDataUrl); return (

Review & Refine Subjects

The AI has extracted the subjects. Use the tools to refine the selection if needed.

{/* Thumbnails */}
{internalImages.map((image, index) => ( ))}
{/* Editor */}
setBrushSize(Number(e.target.value))} className="w-32 cursor-pointer"/>
); }; export default ImageSegmenter;