Skip to content

Commit 6bae590

Browse files
authored
Merge pull request #3 from MujahidAbbas/refine-edges
feat: Add Refine Edges modal for precision mask editing
2 parents 010d63b + 1a9ca2b commit 6bae590

File tree

8 files changed

+1037
-14
lines changed

8 files changed

+1037
-14
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ pnpm-debug.log*
2727
.claude/
2828
docs/
2929
.planning/
30+
.cursor/

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ Clearcut is a privacy-first background removal and image editing tool that runs
2424
- **Undo/Redo** - Up to 20 history entries for brush edits.
2525
- **Before/After Comparison** - Interactive slider to compare original and edited images.
2626

27+
#### Edge Refinement
28+
- **Refine Edges Modal** - Dedicated overlay for precision mask editing with zoom/pan support.
29+
- **Eraser Tool** - Remove leftover background artifacts the AI missed.
30+
- **Restore Tool** - Bring back accidentally removed details like hair edges, fingers, etc.
31+
- **Zoom Controls** - 1.0x to 4.0x zoom for precision work on fine details.
32+
- **Pan Navigation** - Hold Space + drag (desktop) or two-finger drag (mobile) to navigate zoomed canvas.
33+
- **Touch Gestures** - Pinch-to-zoom and two-finger pan for mobile devices.
34+
- **Reset All Edits** - Revert all refinement strokes back to the original state.
35+
2736
#### Image Editing Modal
2837
- **Full-Screen Editor** - Immersive editing experience with real-time preview.
2938
- **Background Selection** - Choose from 12 preset colors, custom color picker, or upload your own background image.
@@ -110,7 +119,10 @@ clearcut/
110119
│ │ │ ├── ExportModal.tsx # Export flow with Ko-fi
111120
│ │ │ ├── BackgroundSelector.tsx # Preset/custom backgrounds
112121
│ │ │ ├── FilterControls.tsx # BG filter sliders
113-
│ │ │ └── TransformControls.tsx # Zoom/rotate/flip/crop
122+
│ │ │ ├── TransformControls.tsx # Zoom/rotate/flip/crop
123+
│ │ │ ├── RefineEdgesModal.tsx # Edge refinement overlay
124+
│ │ │ ├── RefineEdgesCanvas.tsx # Zoomable canvas with brush
125+
│ │ │ └── RefineEdgesControls.tsx # Refinement tool controls
114126
│ │ └── ui/ # Reusable UI components
115127
│ ├── lib/
116128
│ │ ├── segmentation.ts # AI model integration
@@ -139,9 +151,10 @@ clearcut/
139151
1. **Model Loading** - On first visit, the RMBG-1.4 model (~45MB) is downloaded and cached in your browser.
140152
2. **Image Processing** - When you upload an image, it's processed entirely client-side using WebGPU (if available) or WASM.
141153
3. **Mask Generation** - The AI generates a segmentation mask identifying foreground vs background.
142-
4. **Image Editing** - Open the full-screen editor to apply background replacements, filters (brightness, contrast, saturation, blur), transforms (zoom, rotate, flip), and crop to specific aspect ratios.
143-
5. **Compositing** - The mask is applied to create a transparent background, with optional color/image replacement and filters.
144-
6. **Export** - Click the download button to trigger the export modal. The final result is rendered to a canvas and exported as PNG/JPG with the original filename + "-nobg" suffix.
154+
4. **Edge Refinement** - Use the Refine Edges tool to clean up any imperfections. Zoom in (up to 4x) for precision work, use Eraser to remove leftover background, or Restore to bring back accidentally removed details.
155+
5. **Image Editing** - Open the full-screen editor to apply background replacements, filters (brightness, contrast, saturation, blur), transforms (zoom, rotate, flip), and crop to specific aspect ratios.
156+
6. **Compositing** - The mask is applied to create a transparent background, with optional color/image replacement and filters.
157+
7. **Export** - Click the download button to trigger the export modal. The final result is rendered to a canvas and exported as PNG/JPG with the original filename + "-nobg" suffix.
145158

146159
**No data ever leaves your browser.**
147160

src/components/remove/BackgroundRemover.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export default function BackgroundRemover() {
190190
}
191191
}
192192

193+
// Increment preview version to force ResultsView/ComparisonSlider to update
194+
// This is needed because maskCanvas reference stays the same even when contents change
195+
setPreviewVersion(v => v + 1);
196+
193197
// Reset edit state and close modal
194198
resetEditState();
195199
closeEditModal();

src/components/remove/EditModal.tsx

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { useEffect, useRef, useState } from 'react';
1+
import { useEffect, useRef, useState, useCallback } from 'react';
22
import { useAppStore } from '@/stores/appStore';
3+
import { saveMaskSnapshot } from '@/lib/brushTool';
34
import Button from '@/components/ui/Button';
45
import EditPreview from './EditPreview';
56
import BackgroundSelector from './BackgroundSelector';
67
import FilterControls from './FilterControls';
78
import TransformControls from './TransformControls';
9+
import RefineEdgesModal from './RefineEdgesModal';
810

911
interface EditModalProps {
1012
originalImage: HTMLImageElement;
@@ -24,9 +26,43 @@ export default function EditModal({
2426
const hasUnsavedEdits = useAppStore((state) => state.hasUnsavedEdits);
2527
const setHasUnsavedEdits = useAppStore((state) => state.setHasUnsavedEdits);
2628
const resetEditState = useAppStore((state) => state.resetEditState);
29+
30+
// Refine Edges modal state
31+
const isRefineModalOpen = useAppStore((state) => state.isRefineModalOpen);
32+
const openRefineModal = useAppStore((state) => state.openRefineModal);
2733

2834
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
2935
const [showBefore, setShowBefore] = useState(false);
36+
37+
// Preview version counter - incremented to force EditPreview re-render after mask changes
38+
const [previewVersion, setPreviewVersion] = useState(0);
39+
40+
// Handler to open Refine Edges modal
41+
const handleOpenRefineEdges = useCallback(() => {
42+
// Save current mask state before opening refine modal
43+
const snapshot = saveMaskSnapshot(maskCanvas);
44+
openRefineModal(snapshot);
45+
}, [maskCanvas, openRefineModal]);
46+
47+
// Handler when Refine Edges modal finishes (returns to edit modal)
48+
const handleRefineFinish = useCallback(() => {
49+
// Mark as having unsaved edits since mask may have changed
50+
setHasUnsavedEdits(true);
51+
// Increment preview version to force EditPreview to re-render with updated mask
52+
setPreviewVersion(v => v + 1);
53+
}, [setHasUnsavedEdits]);
54+
55+
// Handler when "Apply & Done" is clicked from Refine Edges modal
56+
const handleRefineApplyAndDone = useCallback(() => {
57+
setHasUnsavedEdits(false);
58+
onApply();
59+
}, [setHasUnsavedEdits, onApply]);
60+
61+
// Handler when "Exit Editor" is clicked from Refine Edges modal
62+
const handleRefineExitEditor = useCallback(() => {
63+
resetEditState();
64+
onClose();
65+
}, [resetEditState, onClose]);
3066

3167
// Focus trap
3268
useEffect(() => {
@@ -137,9 +173,10 @@ export default function EditModal({
137173
</button>
138174
</div>
139175

140-
{/* Canvas Preview */}
176+
{/* Canvas Preview - key prop forces re-render when mask changes in Refine Edges */}
141177
<div className="flex-1 overflow-hidden">
142178
<EditPreview
179+
key={previewVersion}
143180
originalImage={originalImage}
144181
maskCanvas={maskCanvas}
145182
showBefore={showBefore}
@@ -155,20 +192,20 @@ export default function EditModal({
155192
<FilterControls />
156193
<TransformControls />
157194

158-
{/* Refine Edges Section - Placeholder for Phase 3 */}
195+
{/* Refine Edges Section */}
159196
<div className="space-y-3">
160197
<h3 className="text-sm font-semibold text-gray-700">Edges</h3>
161198
<button
162-
disabled
163-
className="w-full py-2 px-4 rounded-lg border border-gray-200 text-sm text-gray-400
164-
bg-gray-50 cursor-not-allowed flex items-center justify-center gap-2"
165-
title="Coming soon - Fine-tune background removal edges"
199+
onClick={handleOpenRefineEdges}
200+
className="w-full py-2.5 px-4 rounded-lg border-2 border-primary text-sm text-primary font-medium
201+
bg-primary-light hover:bg-primary hover:text-white transition-colors flex items-center justify-center gap-2"
202+
title="Fine-tune background removal edges"
166203
>
167204
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
168-
<path d="M12 3v18M3 12h18" strokeLinecap="round" />
205+
<path d="M12 20h9" strokeLinecap="round" strokeLinejoin="round" />
206+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" strokeLinecap="round" strokeLinejoin="round" />
169207
</svg>
170208
Refine Edges
171-
<span className="text-xs bg-gray-200 px-1.5 py-0.5 rounded">Soon</span>
172209
</button>
173210
<p className="text-xs text-gray-400">
174211
Fine-tune the edges of your cutout for cleaner results.
@@ -225,6 +262,17 @@ export default function EditModal({
225262
</div>
226263
</div>
227264
)}
265+
266+
{/* Refine Edges Modal */}
267+
{isRefineModalOpen && (
268+
<RefineEdgesModal
269+
originalImage={originalImage}
270+
maskCanvas={maskCanvas}
271+
onFinishRefining={handleRefineFinish}
272+
onApplyAndDone={handleRefineApplyAndDone}
273+
onExitEditor={handleRefineExitEditor}
274+
/>
275+
)}
228276
</div>
229277
);
230278
}

0 commit comments

Comments
 (0)