mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(erase-object): auto-center compare slider at erased region
Track the painted mask centroid and initialise the before/after slider at that position so the result is immediately visible without manual dragging.
This commit is contained in:
@@ -9,6 +9,8 @@ interface BeforeAfterSliderProps {
|
|||||||
beforeSize?: number;
|
beforeSize?: number;
|
||||||
/** Processed file size in bytes. */
|
/** Processed file size in bytes. */
|
||||||
afterSize?: number;
|
afterSize?: number;
|
||||||
|
/** Initial divider position as a percentage (0–100). Defaults to 50. */
|
||||||
|
initialPosition?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(bytes: number): string {
|
function formatSize(bytes: number): string {
|
||||||
@@ -29,9 +31,10 @@ export function BeforeAfterSlider({
|
|||||||
afterSrc,
|
afterSrc,
|
||||||
beforeSize,
|
beforeSize,
|
||||||
afterSize,
|
afterSize,
|
||||||
|
initialPosition = 50,
|
||||||
}: BeforeAfterSliderProps) {
|
}: BeforeAfterSliderProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [position, setPosition] = useState(50); // percentage 0-100
|
const [position, setPosition] = useState(initialPosition); // percentage 0-100
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const updatePosition = useCallback((clientX: number) => {
|
const updatePosition = useCallback((clientX: number) => {
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export function CompareSettings() {
|
|||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
setSimilarity(result.similarity);
|
setSimilarity(result.similarity);
|
||||||
setDownloadUrl(result.downloadUrl);
|
setDownloadUrl(result.downloadUrl);
|
||||||
setProcessedUrl(result.downloadUrl);
|
// Show the second image in the slider (not the diff) so the user can
|
||||||
|
// visually compare their two originals. The diff is still downloadable.
|
||||||
|
setProcessedUrl(URL.createObjectURL(secondFile));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Comparison failed");
|
setError(err instanceof Error ? err.message : "Comparison failed");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface EraseObjectSettingsProps {
|
|||||||
hasStrokes: boolean;
|
hasStrokes: boolean;
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
onBrushSizeChange: (size: number) => void;
|
onBrushSizeChange: (size: number) => void;
|
||||||
|
onMaskCenter?: (centerPct: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EraseObjectSettings({
|
export function EraseObjectSettings({
|
||||||
@@ -21,6 +22,7 @@ export function EraseObjectSettings({
|
|||||||
hasStrokes,
|
hasStrokes,
|
||||||
brushSize,
|
brushSize,
|
||||||
onBrushSizeChange: setBrushSize,
|
onBrushSizeChange: setBrushSize,
|
||||||
|
onMaskCenter,
|
||||||
}: EraseObjectSettingsProps) {
|
}: EraseObjectSettingsProps) {
|
||||||
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes } =
|
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes } =
|
||||||
useFileStore();
|
useFileStore();
|
||||||
@@ -41,6 +43,12 @@ export function EraseObjectSettings({
|
|||||||
const maskBlob = await eraserRef.current.exportMask();
|
const maskBlob = await eraserRef.current.exportMask();
|
||||||
if (!maskBlob) return;
|
if (!maskBlob) return;
|
||||||
|
|
||||||
|
// Record where the user painted so the comparison slider starts at that location
|
||||||
|
const maskCenter = eraserRef.current.getMaskCenter();
|
||||||
|
if (maskCenter !== null && onMaskCenter) {
|
||||||
|
onMaskCenter(maskCenter);
|
||||||
|
}
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setDownloadUrl(null);
|
setDownloadUrl(null);
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ type Stroke = { points: Point[]; size: number };
|
|||||||
|
|
||||||
export interface EraserCanvasRef {
|
export interface EraserCanvasRef {
|
||||||
exportMask: () => Promise<Blob | null>;
|
exportMask: () => Promise<Blob | null>;
|
||||||
|
getMaskCenter: () => number | null;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
}
|
}
|
||||||
@@ -238,6 +239,20 @@ export const EraserCanvas = forwardRef<EraserCanvasRef, EraserCanvasProps>(funct
|
|||||||
mask.toBlob((b) => resolve(b), "image/png");
|
mask.toBlob((b) => resolve(b), "image/png");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getMaskCenter: () => {
|
||||||
|
if (strokesRef.current.length === 0 || !canvasSize) return null;
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
for (const stroke of strokesRef.current) {
|
||||||
|
for (const pt of stroke.points) {
|
||||||
|
minX = Math.min(minX, pt.x - stroke.size / 2);
|
||||||
|
maxX = Math.max(maxX, pt.x + stroke.size / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minX === Infinity) return null;
|
||||||
|
const centerX = (minX + maxX) / 2;
|
||||||
|
return Math.max(0, Math.min(100, (centerX / canvasSize.w) * 100));
|
||||||
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
strokesRef.current = [];
|
strokesRef.current = [];
|
||||||
currentPointsRef.current = [];
|
currentPointsRef.current = [];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface EraserProps {
|
|||||||
hasStrokes: boolean;
|
hasStrokes: boolean;
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
onBrushSizeChange: (size: number) => void;
|
onBrushSizeChange: (size: number) => void;
|
||||||
|
onMaskCenter?: (centerPct: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Registry entry ─────────────────────────────────────────────────
|
// ── Registry entry ─────────────────────────────────────────────────
|
||||||
@@ -433,7 +434,7 @@ export const toolRegistry = new Map<string, ToolRegistryEntry>([
|
|||||||
[
|
[
|
||||||
"image-enhancement",
|
"image-enhancement",
|
||||||
{
|
{
|
||||||
displayMode: "live-preview" as DisplayMode,
|
displayMode: "before-after" as DisplayMode,
|
||||||
livePreview: true,
|
livePreview: true,
|
||||||
Settings: ImageEnhancementSettings as never,
|
Settings: ImageEnhancementSettings as never,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ export function ToolPage() {
|
|||||||
const eraserRef = useRef<EraserCanvasRef | null>(null);
|
const eraserRef = useRef<EraserCanvasRef | null>(null);
|
||||||
const [eraserHasStrokes, setEraserHasStrokes] = useState(false);
|
const [eraserHasStrokes, setEraserHasStrokes] = useState(false);
|
||||||
const [eraserBrushSize, setEraserBrushSize] = useState(30);
|
const [eraserBrushSize, setEraserBrushSize] = useState(30);
|
||||||
|
// Center of the painted mask as a 0-100 percentage — used to init the slider at the right spot
|
||||||
|
const [eraserSliderInitPos, setEraserSliderInitPos] = useState<number | null>(null);
|
||||||
|
|
||||||
// Reset crop state when the image changes
|
// Reset crop state when the image changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -188,6 +190,7 @@ export function ToolPage() {
|
|||||||
|
|
||||||
const handleFiles = useCallback(
|
const handleFiles = useCallback(
|
||||||
(newFiles: File[]) => {
|
(newFiles: File[]) => {
|
||||||
|
setEraserSliderInitPos(null);
|
||||||
reset();
|
reset();
|
||||||
setFiles(newFiles);
|
setFiles(newFiles);
|
||||||
},
|
},
|
||||||
@@ -196,6 +199,7 @@ export function ToolPage() {
|
|||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
undoProcessing();
|
undoProcessing();
|
||||||
|
setEraserSliderInitPos(null);
|
||||||
}, [undoProcessing]);
|
}, [undoProcessing]);
|
||||||
|
|
||||||
const handleAddMore = useCallback(() => {
|
const handleAddMore = useCallback(() => {
|
||||||
@@ -276,6 +280,7 @@ export function ToolPage() {
|
|||||||
hasStrokes: eraserHasStrokes,
|
hasStrokes: eraserHasStrokes,
|
||||||
brushSize: eraserBrushSize,
|
brushSize: eraserBrushSize,
|
||||||
onBrushSizeChange: setEraserBrushSize,
|
onBrushSizeChange: setEraserBrushSize,
|
||||||
|
onMaskCenter: setEraserSliderInitPos,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@@ -349,6 +354,21 @@ export function ToolPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After erasing: compare clean original vs inpainted result.
|
||||||
|
// Initialise the divider at the center of the painted area so the comparison
|
||||||
|
// lands right where the object was removed.
|
||||||
|
if (displayMode === "interactive-eraser" && hasProcessed && originalBlobUrl) {
|
||||||
|
return (
|
||||||
|
<BeforeAfterSlider
|
||||||
|
beforeSrc={originalBlobUrl}
|
||||||
|
afterSrc={displayUrl}
|
||||||
|
beforeSize={originalSize ?? undefined}
|
||||||
|
afterSize={processedSize ?? undefined}
|
||||||
|
initialPosition={eraserSliderInitPos ?? 50}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (displayMode === "interactive-split" && hasFile && originalBlobUrl) {
|
if (displayMode === "interactive-split" && hasFile && originalBlobUrl) {
|
||||||
if (registryEntry?.ResultsPanel) {
|
if (registryEntry?.ResultsPanel) {
|
||||||
const Panel = registryEntry.ResultsPanel;
|
const Panel = registryEntry.ResultsPanel;
|
||||||
|
|||||||
Reference in New Issue
Block a user