Add Ralph Wiggum agent setup and project specifications

- Add project constitution with vision, principles, and autonomy settings
- Add 15 feature specifications covering full app scope
- Configure agent entry points (AGENTS.md, CLAUDE.md)
- Add build prompt and speckit command for spec creation
- Include comprehensive .gitignore for iOS development

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 23:19:41 -05:00
parent c64aa4b8ac
commit 1049057d7d
24 changed files with 1092 additions and 0 deletions

31
specs/01-project-setup.md Normal file
View File

@@ -0,0 +1,31 @@
# Project Setup
## Description
Initialize the Xcode project with the correct structure, targets, and dependencies for CheapRetouch.
## Acceptance Criteria
- [ ] Xcode project created with iOS 17.0 deployment target
- [ ] Project structure matches specification:
```
CheapRetouch/
├── App/CheapRetouchApp.swift
├── Features/Editor/
├── Features/Export/
├── Services/
├── Services/InpaintEngine/
├── Models/
├── Utilities/
└── Resources/Assets.xcassets
```
- [ ] SwiftUI app lifecycle configured
- [ ] Metal capability added to project
- [ ] Photo library usage description added to Info.plist
- [ ] App builds and runs on iOS 17 simulator
## Technical Notes
- Use SwiftUI App lifecycle (`@main` struct)
- No external dependencies - Apple frameworks only
- Ensure Metal framework is linked
## Edge Cases
- None for setup phase

31
specs/02-data-model.md Normal file
View File

@@ -0,0 +1,31 @@
# Data Model & Edit Operations
## Description
Implement the non-destructive editing data model with operation stack for full undo/redo support.
## Acceptance Criteria
- [ ] `EditOperation` enum implemented with cases: `.mask`, `.inpaint`, `.adjustment`
- [ ] `MaskOperation` struct with: id, toolType, maskData (compressed R8), timestamp
- [ ] `InpaintOperation` struct with: id, maskOperationId, patchRadius, featherAmount, timestamp
- [ ] `ToolType` enum with cases: `.person`, `.object`, `.wire`, `.brush`
- [ ] `Project` model that holds:
- Original image reference (PHAsset identifier or embedded Data)
- Operation stack (array of EditOperation)
- Current stack position for undo/redo
- [ ] All models conform to `Codable`
- [ ] Undo operation decrements stack position
- [ ] Redo operation increments stack position
- [ ] Project can be serialized to/from JSON
- [ ] Unit tests for operation stack logic
## Technical Notes
- Original image is NEVER modified
- Mask data should be compressed (R8 texture format)
- Store PHAsset `localIdentifier` for Photos-sourced images
- Store embedded image data for Files-imported images
- Cached previews should set `isExcludedFromBackup = true`
## Edge Cases
- Undo at beginning of stack: no-op, return false
- Redo at end of stack: no-op, return false
- Empty operation stack: valid state, shows original image

View File

@@ -0,0 +1,43 @@
# Inpainting Engine (Metal)
## Description
Implement exemplar-based inpainting (Criminisi-style) using Metal for content-aware fill of masked regions.
## Acceptance Criteria
- [ ] `InpaintEngine` class with public interface:
- `func inpaint(image: CGImage, mask: CGImage) async throws -> CGImage`
- `func inpaintPreview(image: CGImage, mask: CGImage) async throws -> CGImage`
- [ ] Metal shaders in `Shaders.metal` for:
- Mask dilation (2-4px configurable)
- Mask feathering (gaussian blur on alpha)
- Patch matching (find best match from known region)
- Patch copying (fill unknown region)
- Edge-aware blending (reduce seams)
- [ ] `PatchMatch.swift` implementing the algorithm:
- Build image pyramid for preview vs export
- Priority-based boundary pixel processing
- Best-matching patch search
- Boundary update after each patch copy
- [ ] Performance targets met:
- Preview (2048px): < 300ms on A14
- Export (12MP): < 4 seconds on A14
- Export (48MP): < 12 seconds on A17 Pro
- [ ] Memory management:
- Tile-based processing for images > 12MP
- Peak memory < 1.5GB
- Intermediate textures released aggressively
- [ ] Accelerate/vImage fallback when Metal unavailable
- [ ] Snapshot tests with reference images verifying output quality
## Technical Notes
- Criminisi algorithm paper: "Region Filling and Object Removal by Exemplar-Based Inpainting"
- Patch size typically 9x9 or 11x11 pixels
- Priority = confidence × data term (edge strength)
- Search region can be limited for performance
- Use MTLHeap for efficient texture allocation
## Edge Cases
- Metal unavailable: fall back to Accelerate with warning toast
- Memory pressure during export: throw error with "Image too large" message
- Very large mask (>50% of image): may produce poor results, warn user
- Mask touches image edge: handle boundary conditions in patch search

View File

@@ -0,0 +1,28 @@
# Masking Service
## Description
Wrapper around Vision framework for generating masks from user taps and contour detection.
## Acceptance Criteria
- [ ] `MaskingService` class with methods:
- `func generatePersonMask(at point: CGPoint, in image: CGImage) async throws -> CGImage?`
- `func generateForegroundMask(at point: CGPoint, in image: CGImage) async throws -> CGImage?`
- `func detectContours(in image: CGImage) async throws -> [VNContour]`
- [ ] Uses `VNGenerateForegroundInstanceMaskRequest` for person/object masks
- [ ] Uses `VNDetectContoursRequest` for wire/line detection
- [ ] Mask dilation method: `func dilate(mask: CGImage, by pixels: Int) -> CGImage`
- [ ] Mask feathering method: `func feather(mask: CGImage, amount: Float) -> CGImage`
- [ ] Returns `nil` when no mask detected at tap location (not an error)
- [ ] Unit tests for mask operations
## Technical Notes
- `VNGenerateForegroundInstanceMaskRequest` requires iOS 17+
- Point coordinates must be normalized (0-1) for Vision requests
- Instance masks can identify multiple separate foreground objects
- Use `indexesOfInstancesContainingPoint` to find which instance was tapped
## Edge Cases
- No person/object at tap location: return nil, caller shows fallback UI
- Multiple overlapping instances: return the one containing the tap point
- Vision request fails: throw descriptive error
- Image orientation: ensure coordinates are transformed correctly

View File

@@ -0,0 +1,30 @@
# Contour Service
## Description
Service for detecting and scoring wire/line contours from Vision results.
## Acceptance Criteria
- [ ] `ContourService` class with methods:
- `func findBestWireContour(at point: CGPoint, from contours: [VNContour]) -> VNContour?`
- `func scoreContour(_ contour: VNContour, relativeTo point: CGPoint) -> Float`
- `func contourToMask(_ contour: VNContour, width: Int, imageSize: CGSize) -> CGImage`
- [ ] Contour scoring considers:
- Proximity to tap point (closer = higher score)
- Aspect ratio (thin and elongated = higher score)
- Straightness / low curvature (straighter = higher score)
- Length (longer = higher score)
- [ ] Configurable mask width: default 6px, range 2-20px
- [ ] Returns `nil` when no wire-like contour found near tap
- [ ] Unit tests for contour scoring logic
## Technical Notes
- VNContour provides normalized path points
- Calculate curvature by analyzing angle changes along path
- Aspect ratio = length / average width
- Weight scoring factors: proximity (0.3), aspect (0.3), straightness (0.2), length (0.2)
## Edge Cases
- No contours detected: return nil
- All contours score below threshold: return nil
- Curved wires: allow moderate curvature, don't require perfectly straight
- Contour very close to tap but not wire-like: score should be low

34
specs/06-canvas-view.md Normal file
View File

@@ -0,0 +1,34 @@
# Canvas View
## Description
Main editing canvas with pinch-to-zoom, pan, mask overlay, and before/after comparison.
## Acceptance Criteria
- [ ] `CanvasView` SwiftUI view displaying the current edited image
- [ ] Pinch-to-zoom gesture with smooth animation
- [ ] Pan gesture for navigation when zoomed
- [ ] Zoom limits: 1x to 10x
- [ ] Mask overlay modes:
- Red tint (50% opacity red on masked areas)
- Marching ants (animated dashed border)
- Hidden (no overlay)
- [ ] Toggle between overlay modes via UI control
- [ ] Before/after comparison:
- Long press shows original image
- Release returns to edited version
- Optional toggle button for sticky comparison
- [ ] Renders at appropriate resolution for current zoom level
- [ ] Smooth 60fps interaction on A14 devices
- [ ] UI tests for gesture interactions
## Technical Notes
- Use `MagnificationGesture` and `DragGesture` simultaneously
- Consider using UIKit interop (`UIViewRepresentable`) for smoother gestures if needed
- Mask overlay should be composited efficiently (don't re-render full image)
- Use `drawingGroup()` or Metal for overlay rendering if performance issues
## Edge Cases
- Zoom at image boundary: clamp pan to keep image visible
- Very large image: use tiled rendering or lower resolution preview
- No edits yet: before/after shows same image (no-op)
- Rapid gesture changes: debounce if needed to prevent jank

37
specs/07-toolbar-view.md Normal file
View File

@@ -0,0 +1,37 @@
# Toolbar View
## Description
Tool selection toolbar with contextual inspector panel.
## Acceptance Criteria
- [ ] `ToolbarView` SwiftUI view with tool buttons:
- Person tool (👤 icon or SF Symbol)
- Object tool (circle/square icon)
- Wire tool (line/bolt icon)
- Brush tool (paintbrush icon)
- Undo button (arrow.uturn.backward)
- Redo button (arrow.uturn.forward)
- [ ] Selected tool highlighted visually
- [ ] Undo/redo buttons disabled when not available
- [ ] Contextual inspector panel appears based on active tool:
- Brush: size slider (1-100px)
- All tools: feather amount slider (0-20px)
- Wire tool: mask expansion slider (2-20px, default 6)
- Optional "Refine edges" toggle
- [ ] Inspector animates in/out smoothly
- [ ] All tools labeled for VoiceOver
- [ ] Brush size adjustable via stepper (accessibility)
- [ ] Dynamic Type support for any text labels
- [ ] UI tests for tool selection and inspector
## Technical Notes
- Use SF Symbols for icons where possible
- Store selected tool in shared state (environment or binding)
- Inspector can be sheet, popover, or inline panel based on device
- Consider compact layout for smaller devices
## Edge Cases
- No image loaded: tools disabled
- Processing in progress: tools disabled, show activity indicator
- Undo stack empty: undo button disabled
- Redo stack empty: redo button disabled

View File

@@ -0,0 +1,37 @@
# Photo Editor View
## Description
Main editor screen composing canvas, toolbar, and coordinating edit operations.
## Acceptance Criteria
- [ ] `PhotoEditorView` SwiftUI view containing:
- `CanvasView` for image display and interaction
- `ToolbarView` for tool selection
- Status/feedback area for messages
- [ ] Tap handling routed to appropriate service based on selected tool:
- Person tool → MaskingService.generatePersonMask
- Object tool → MaskingService.generateForegroundMask
- Wire tool → ContourService.findBestWireContour
- Brush tool → direct drawing on mask layer
- [ ] Mask preview shown after detection, before inpainting
- [ ] Confirm/cancel buttons for mask preview
- [ ] On confirm: InpaintEngine processes, result added to operation stack
- [ ] Feedback states implemented:
- Processing: spinner overlay on affected region
- No detection: toast with fallback suggestion
- Success: brief checkmark animation
- [ ] Undo/redo triggers re-render from operation stack
- [ ] State persisted when app backgrounds
- [ ] Full flow UI test: import → edit → confirm
## Technical Notes
- Use `@StateObject` or `@ObservedObject` for editor state
- Coordinate space conversion between view and image coordinates
- Show mask preview as overlay before committing
- Processing should be async to keep UI responsive
## Edge Cases
- Tap during processing: ignore or queue
- App backgrounded during processing: complete in background if possible
- Memory warning during processing: cancel gracefully, show error
- User cancels mask preview: discard mask, return to ready state

View File

@@ -0,0 +1,33 @@
# Person Removal Feature
## Description
Tap-to-remove people from photos using Vision's person segmentation.
## Acceptance Criteria
- [ ] User taps person in photo with Person tool selected
- [ ] `VNGenerateForegroundInstanceMaskRequest` generates mask for tapped person
- [ ] Mask preview shown with red tint overlay
- [ ] Mask automatically dilated by 2-4px to capture edge pixels
- [ ] User can adjust feather amount before confirming
- [ ] On confirm: mask feathered and passed to InpaintEngine
- [ ] Inpainted result displayed, operation added to stack
- [ ] "Select all people" option available when multiple people detected
- [ ] Multiple people can be removed one at a time
- [ ] Partial occlusion handled (Vision provides usable mask)
- [ ] User can refine mask with brush tool if needed
- [ ] Error handling:
- No person at tap: "No person found at tap location" toast
- Low confidence mask: "Does this look right?" confirmation with preview
- [ ] Performance: mask generation < 500ms, inpaint per spec targets
## Technical Notes
- Vision's person segmentation is robust on iOS 17+
- Use `indexesOfInstancesContainingPoint` to identify which person was tapped
- For "select all", combine masks from all detected instances
- Allow brush refinement by switching to brush tool with existing mask loaded
## Edge Cases
- Person partially out of frame: mask what's visible
- Person behind object: Vision may include occluding object, allow brush refinement
- Very small person in photo: may not be detected, suggest zoom or brush
- Multiple overlapping people: tap selects frontmost, allow sequential removal

View File

@@ -0,0 +1,34 @@
# Foreground Object Removal Feature
## Description
Remove foreground objects via tap detection with smart brush fallback.
## Acceptance Criteria
- [ ] User taps object in photo with Object tool selected
- [ ] `VNGenerateForegroundInstanceMaskRequest` attempts to isolate object
- [ ] If mask found: preview shown, user confirms, inpaint executes
- [ ] If no mask found: "Use brush to select" prompt displayed
- [ ] Smart brush fallback:
- User paints rough selection over object
- App refines selection to nearest strong edges
- Edge refinement uses gradient magnitude analysis
- Refined mask preview shown
- User confirms refined mask
- [ ] Brush tool settings: size slider (1-100px)
- [ ] Edge refinement toggle available
- [ ] Clear messaging about limitations:
- "Works best on objects that stand out from background"
- "Low-contrast objects may require manual selection"
- [ ] Performance: detection < 500ms, edge refinement < 200ms
## Technical Notes
- Vision detects visually distinct foreground regions
- It separates by contrast, not semantic understanding
- Edge refinement: compute gradient magnitude, snap brush stroke to nearby edges
- `EdgeRefinement.swift` utility for gradient-based snapping
## Edge Cases
- Object blends with background: Vision returns no mask, prompt brush
- Very large object: may affect inpaint quality, warn if >30% of image
- Object at image edge: handle boundary in mask and inpaint
- User brush stroke misses object: edge refinement helps, but may need retry

36
specs/11-wire-removal.md Normal file
View File

@@ -0,0 +1,36 @@
# Wire & Line Removal Feature
## Description
Remove power lines and wires using contour detection with line brush fallback.
## Acceptance Criteria
- [ ] User taps near wire with Wire tool selected
- [ ] `VNDetectContoursRequest` returns all detected contours
- [ ] ContourService scores contours and selects best wire-like match:
- Proximity to tap point
- Thin/elongated aspect ratio
- Straightness (low curvature)
- Length
- [ ] Best contour highlighted for preview
- [ ] Mask width configurable: default 6px, range 2-20px via slider
- [ ] User confirms, mask expanded to configured width, inpaint executes
- [ ] Line brush fallback when no contour detected:
- User switches to "Line brush" mode
- User draws along wire
- Consistent stroke width maintained
- Stroke becomes mask for inpainting
- [ ] Multiple wires can be removed sequentially
- [ ] Performance: contour detection < 300ms
## Technical Notes
- Power lines against sky have strong edges - ideal case
- Scoring weights: proximity 0.3, aspect 0.3, straightness 0.2, length 0.2
- Line brush should use touch velocity for smooth strokes
- Consider Catmull-Rom spline for smooth line brush paths
## Edge Cases
- No wire-like contours found: "No lines detected. Use line brush" prompt
- Wire against busy background (buildings, trees): likely needs line brush
- Curved wires (drooping power lines): scoring allows moderate curvature
- Wire crosses entire image: may need to process in segments
- Multiple parallel wires: each tap selects one, remove sequentially

29
specs/12-image-import.md Normal file
View File

@@ -0,0 +1,29 @@
# Image Import
## Description
Import photos from Photo Library or Files app.
## Acceptance Criteria
- [ ] Import button opens `PHPickerViewController`
- [ ] Limited photo library access supported (no full access required)
- [ ] Selected photo loaded into editor
- [ ] PHAsset `localIdentifier` stored for Photos-sourced images
- [ ] Files app import supported via document picker
- [ ] File-imported images embedded in project data
- [ ] Supported formats: JPEG, PNG, HEIC
- [ ] Large images (>12MP) handled with appropriate warnings
- [ ] Loading indicator shown during import
- [ ] Import errors handled gracefully with user feedback
- [ ] Privacy: no photo library write permission requested at import time
## Technical Notes
- Use `PHPickerViewController` (not `UIImagePickerController`) for modern API
- Configure picker: `filter: .images`, `selectionLimit: 1`
- For Files: use `UIDocumentPickerViewController` with `UTType.image`
- Store original at full resolution, generate preview separately
## Edge Cases
- User cancels picker: return to previous state, no error
- Corrupted/unreadable image: show "Unable to load image" error
- Very large image (48MP+): warn about potential memory issues
- Permission denied: show guidance to enable in Settings

33
specs/13-export-view.md Normal file
View File

@@ -0,0 +1,33 @@
# Export View
## Description
Export edited photos at full resolution to Photo Library or Files.
## Acceptance Criteria
- [ ] Export button in editor triggers export flow
- [ ] Full resolution render from operation stack
- [ ] Progress indicator during export rendering
- [ ] Export options:
- Save to Photo Library (requests write permission)
- Share sheet (AirDrop, Messages, etc.)
- Save to Files
- [ ] Export formats: JPEG (quality slider 0.7-1.0), PNG
- [ ] HEIC output supported on compatible devices
- [ ] Metadata preserved from original where possible
- [ ] Success confirmation after save
- [ ] Export time within spec targets:
- 12MP: < 4 seconds
- 48MP: < 12 seconds
- [ ] Memory management: tile-based for large images
## Technical Notes
- Request `PHPhotoLibrary.authorizationStatus(for: .addOnly)` for write
- Use `PHAssetChangeRequest.creationRequestForAsset` to save
- For share sheet, create temporary file, clean up after
- Consider background processing for very large exports
## Edge Cases
- Export during low memory: show "Image too large, try cropping" error
- Permission denied: show guidance to enable in Settings
- Export cancelled mid-process: clean up partial work
- Disk space insufficient: detect and show appropriate error

32
specs/14-brush-tool.md Normal file
View File

@@ -0,0 +1,32 @@
# Brush Tool
## Description
Manual brush selection for fallback when automatic detection fails.
## Acceptance Criteria
- [ ] Brush tool available in toolbar
- [ ] Brush size adjustable: 1-100px via slider
- [ ] Brush size also adjustable via stepper (accessibility)
- [ ] Touch draws on mask layer in real-time
- [ ] Smooth stroke rendering (interpolate between touch points)
- [ ] Brush preview circle follows finger
- [ ] Erase mode toggle to remove from mask
- [ ] Clear mask button to start over
- [ ] Edge refinement optional: snap brush strokes to nearby edges
- [ ] Mask preview shown in real-time as user paints
- [ ] When done painting, user taps "Done" to proceed to inpaint
- [ ] Pinch-to-zoom still works while brush active
- [ ] Brush works with Apple Pencil (pressure sensitivity optional)
- [ ] Performance: 60fps stroke rendering
## Technical Notes
- Use Core Graphics or Metal for stroke rendering
- Interpolate touch points with quadratic curves for smoothness
- Edge refinement uses gradient magnitude from `EdgeRefinement.swift`
- Consider separate gesture recognizer for drawing vs zoom/pan
## Edge Cases
- Very fast strokes: ensure no gaps between points
- Stroke at image edge: clamp to image bounds
- Accidental touch: undo single stroke or clear all
- Zoom while drawing: complete current stroke, then zoom

29
specs/15-accessibility.md Normal file
View File

@@ -0,0 +1,29 @@
# Accessibility
## Description
Ensure the app is fully accessible per Apple guidelines.
## Acceptance Criteria
- [ ] All tools labeled for VoiceOver with descriptive labels
- [ ] Tool actions announced: "Person tool selected", "Undo complete"
- [ ] Brush size adjustable via stepper (not just slider)
- [ ] High contrast mask visualization option in settings
- [ ] Mask overlay uses accessible colors (not red-green dependent)
- [ ] Reduce Motion support: disable transition animations when enabled
- [ ] Dynamic Type support in all UI text
- [ ] Minimum touch target size: 44x44 points
- [ ] Focus order logical for VoiceOver navigation
- [ ] Processing states announced: "Processing", "Complete"
- [ ] Error messages announced clearly
- [ ] Accessibility audit passes with no critical issues
## Technical Notes
- Use `.accessibilityLabel()` and `.accessibilityHint()` modifiers
- Check `UIAccessibility.isReduceMotionEnabled` for animations
- Use `@ScaledMetric` for Dynamic Type-responsive sizing
- Test with VoiceOver, Voice Control, and Switch Control
## Edge Cases
- Complex canvas gestures: provide alternative VoiceOver actions
- Image descriptions: consider describing detected content for blind users
- Color-only indicators: always pair with shape or text