mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(erase-object): add freeform lasso selection mode (#503)
Adds a Brush | Lasso toggle to the object eraser. Lasso lets the user drag a freeform loop that auto-closes and fills into the mask, so they select around a subject instead of painting every pixel. Frontend-only; the mask contract is unchanged. Also un-skips the erase-object e2e suite via a shared mockAiFeaturesInstalled helper (7 tests now run; 2 multi-file tests fixme'd for a pre-existing tool-page remount bug). Closes #492.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Download, Redo, Trash2 } from "lucide-react";
|
||||
import { Download, Lasso, Paintbrush, Redo, Trash2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
@@ -117,6 +117,8 @@ interface EraseObjectSettingsProps {
|
||||
hasStrokes: boolean;
|
||||
brushSize: number;
|
||||
onBrushSizeChange: (size: number) => void;
|
||||
mode: "brush" | "lasso";
|
||||
onModeChange: (mode: "brush" | "lasso") => void;
|
||||
onMaskCenter?: (centerPct: number) => void;
|
||||
maskedFileCount: number;
|
||||
}
|
||||
@@ -126,6 +128,8 @@ export function EraseObjectSettings({
|
||||
hasStrokes,
|
||||
brushSize,
|
||||
onBrushSizeChange: setBrushSize,
|
||||
mode,
|
||||
onModeChange,
|
||||
onMaskCenter,
|
||||
maskedFileCount,
|
||||
}: EraseObjectSettingsProps) {
|
||||
@@ -439,29 +443,63 @@ export function EraseObjectSettings({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Brush size */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<label htmlFor="eraser-brush-size" className="text-xs text-muted-foreground">
|
||||
{t.toolSettings["erase-object"].brushSize}
|
||||
</label>
|
||||
<span className="text-xs font-mono text-foreground">{brushSize}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="eraser-brush-size"
|
||||
type="range"
|
||||
min={5}
|
||||
max={100}
|
||||
value={brushSize}
|
||||
onChange={(e) => setBrushSize(Number(e.target.value))}
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-muted-foreground mt-0.5">
|
||||
<span>{t.toolSettings["erase-object"].fine}</span>
|
||||
<span>{t.toolSettings["erase-object"].wide}</span>
|
||||
</div>
|
||||
{/* Mode: brush vs lasso */}
|
||||
<div className="flex gap-1 rounded-lg bg-muted p-1">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="eraser-mode-brush"
|
||||
aria-pressed={mode === "brush"}
|
||||
onClick={() => onModeChange("brush")}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||
mode === "brush"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Paintbrush className="h-3.5 w-3.5" />
|
||||
{t.toolSettings["erase-object"].brushMode}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="eraser-mode-lasso"
|
||||
aria-pressed={mode === "lasso"}
|
||||
onClick={() => onModeChange("lasso")}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||
mode === "lasso"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Lasso className="h-3.5 w-3.5" />
|
||||
{t.toolSettings["erase-object"].lassoMode}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Brush size (brush mode only) */}
|
||||
{mode === "brush" && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<label htmlFor="eraser-brush-size" className="text-xs text-muted-foreground">
|
||||
{t.toolSettings["erase-object"].brushSize}
|
||||
</label>
|
||||
<span className="text-xs font-mono text-foreground">{brushSize}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="eraser-brush-size"
|
||||
type="range"
|
||||
min={5}
|
||||
max={100}
|
||||
value={brushSize}
|
||||
onChange={(e) => setBrushSize(Number(e.target.value))}
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-muted-foreground mt-0.5">
|
||||
<span>{t.toolSettings["erase-object"].fine}</span>
|
||||
<span>{t.toolSettings["erase-object"].wide}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Clear / Undo */}
|
||||
{hasStrokes && (
|
||||
<div className="flex gap-2">
|
||||
@@ -528,7 +566,9 @@ export function EraseObjectSettings({
|
||||
{/* Hint */}
|
||||
{hasFile && !hasStrokes && (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Paint over the objects you want to remove. Use Ctrl+Z to undo.
|
||||
{mode === "lasso"
|
||||
? t.toolSettings["erase-object"].lassoHint
|
||||
: t.toolSettings["erase-object"].paintHint}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user