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

View File

@@ -0,0 +1,31 @@
# Create Specification
Create a new specification file in `specs/` based on the description provided.
## Arguments
$ARGUMENTS - Description of the feature to specify
## Output
Create a file `specs/[feature-name].md` with:
```markdown
# [Feature Name]
## Description
[What this feature does]
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] ...
## Technical Notes
[Implementation guidance based on project-specifications.txt]
## Edge Cases
[How to handle failure scenarios]
```
Ensure acceptance criteria are specific, testable, and aligned with the project constitution.

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(git ls-remote:*)",
"Bash(git add:*)"
]
}
}

View File

@@ -0,0 +1,31 @@
# Create Specification
Create a new specification file in `specs/` based on the description provided.
## Arguments
$ARGUMENTS - Description of the feature to specify
## Output
Create a file `specs/[feature-name].md` with:
```markdown
# [Feature Name]
## Description
[What this feature does]
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] ...
## Technical Notes
[Implementation guidance based on project-specifications.txt]
## Edge Cases
[How to handle failure scenarios]
```
Ensure acceptance criteria are specific, testable, and aligned with the project constitution.

56
.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# Xcode
build/
DerivedData/
*.xcodeproj/project.xcworkspace/
*.xcodeproj/xcuserdata/
*.xcworkspace/xcuserdata/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata/
# Swift Package Manager
.build/
.swiftpm/
Package.resolved
# CocoaPods (if ever used)
Pods/
# Fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
# Thumbnails
Thumbs.db
# IDE
*.swp
*.swo
*~
.idea/
*.iml
# Logs
logs/*.log
# Ralph Wiggum
history/
logs/
# Temporary files
*.tmp
*.temp

View File

@@ -0,0 +1,72 @@
# CheapRetouch Constitution
**Version**: ralph-wiggum@81231ca4e7466d84e3908841e9ed3d08e8c0803e
**Created**: 2026-01-23
## Project Vision
On-device iOS photo editor for removing unwanted elements using only Apple's public frameworks and classical image processing. No custom ML models.
**Platform**: iOS 17.0+
**Core Capabilities**:
- Person removal (Vision handles this well)
- Foreground object removal (user-initiated, Vision-assisted)
- Wire/line removal (geometric contour detection)
## Core Principles
1. **Privacy-first**: All processing on-device, no network calls for core functionality, no analytics or telemetry
2. **Graceful fallbacks**: Always provide manual alternatives (brush, line brush) when auto-detection fails - the user is never stuck
3. **Performance matters**: Fast previews (<300ms), responsive UI, efficient memory management (1.5GB budget)
## Technical Stack
| Layer | Framework | Purpose |
|-------|-----------|---------|
| UI | SwiftUI + UIKit interop | Canvas, tools, state management |
| Masking | Vision | `VNGenerateForegroundInstanceMaskRequest`, `VNDetectContoursRequest` |
| Subject Interaction | VisionKit | `ImageAnalyzer`, `ImageAnalysis`, `ImageAnalysisInteraction` |
| Inpainting | Metal (custom) | Patch-based synthesis, mask feathering, blending |
| Compositing | Core Image | Color adjustments, preview pipeline |
| Fallback Processing | Accelerate/vImage | Simulator, older devices without Metal |
## Autonomy Settings
- **YOLO Mode**: ENABLED - Make implementation decisions autonomously
- **Git Autonomy**: ENABLED - Commit and push completed work automatically
## Operational Guidelines
### When Implementing Features
1. Read the spec completely before starting
2. Follow the project structure defined in the spec
3. Implement the primary path first, then fallbacks
4. Ensure all edge cases from the spec are handled
5. Write tests as specified (unit, snapshot, UI, performance, memory)
### Code Quality
- Non-destructive editing: original image never modified
- Full undo/redo support via operation stack
- All operations must be Codable for persistence
- Memory management: release intermediate textures aggressively
- Tile-based processing for images > 12MP
### User Experience
- Clear feedback states (processing, no detection, success)
- Contextual inspector panel based on active tool
- Accessibility: VoiceOver labels, Dynamic Type, Reduce Motion support
### What NOT To Do
- Do not add ML models or custom neural networks
- Do not add features explicitly marked as out of scope
- Do not make network calls for core functionality
- Do not over-engineer beyond what the spec requires
## Completion Signal
Output `<promise>DONE</promise>` only when a specification is 100% complete with all acceptance criteria met. Never output this prematurely.

18
AGENTS.md Normal file
View File

@@ -0,0 +1,18 @@
# Agent Instructions
Read and follow the constitution at `.specify/memory/constitution.md` for all project guidelines, principles, and technical decisions.
## Quick Reference
- **Project**: CheapRetouch - iOS photo editor for removing unwanted elements
- **Platform**: iOS 17.0+
- **Stack**: SwiftUI, Vision, VisionKit, Metal, Core Image
- **YOLO Mode**: Enabled
- **Git Autonomy**: Enabled
## Workflow
1. Check `specs/` for incomplete specifications
2. Implement each spec fully before moving to the next
3. Commit and push when spec is complete
4. Output `<promise>DONE</promise>` only when 100% complete

18
CLAUDE.md Normal file
View File

@@ -0,0 +1,18 @@
# Claude Instructions
Read and follow the constitution at `.specify/memory/constitution.md` for all project guidelines, principles, and technical decisions.
## Quick Reference
- **Project**: CheapRetouch - iOS photo editor for removing unwanted elements
- **Platform**: iOS 17.0+
- **Stack**: SwiftUI, Vision, VisionKit, Metal, Core Image
- **YOLO Mode**: Enabled
- **Git Autonomy**: Enabled
## Workflow
1. Check `specs/` for incomplete specifications
2. Implement each spec fully before moving to the next
3. Commit and push when spec is complete
4. Output `<promise>DONE</promise>` only when 100% complete

28
PROMPT_build.md Normal file
View File

@@ -0,0 +1,28 @@
# Build Mode Instructions
You are implementing CheapRetouch, an iOS photo editor for removing unwanted elements.
## Your Task
1. Read `.specify/memory/constitution.md` for project guidelines
2. Find the next incomplete spec in `specs/`
3. Implement it fully, following all acceptance criteria
4. Run tests to verify completion
5. Commit and push (Git Autonomy is enabled)
6. Output `<promise>DONE</promise>` when the spec is 100% complete
## Implementation Guidelines
- Follow the project structure in `project-specifications.txt`
- Implement primary detection path first, then fallbacks
- Handle all edge cases specified
- Write tests as required (unit, snapshot, UI, performance)
- Keep memory under 1.5GB budget
- Ensure preview renders < 300ms on A14 baseline
## Do Not
- Add ML models or neural networks
- Add features marked as out of scope
- Make network calls for core functionality
- Output `<promise>DONE</promise>` until truly complete

331
project-specifications.txt Normal file
View File

@@ -0,0 +1,331 @@
## CheapRetouch — Revised Specification
### Project Overview
**Platform**: iOS 17.0+
**Objective**: On-device photo editor for removing unwanted elements using only Apple's public frameworks and classical image processing. No custom ML models.
**Core Capabilities** (achievable without ML):
- Person removal (Vision handles this well)
- Foreground object removal (user-initiated, Vision-assisted)
- Wire/line removal (geometric contour detection)
**Removed from Scope**:
- Automatic fence/mesh detection (requires semantic understanding)
- Automatic identification of object types (trash cans, stop signs, etc.)
---
### Technical Stack
| Layer | Framework | Purpose |
|-------|-----------|---------|
| UI | SwiftUI + UIKit interop | Canvas, tools, state management |
| Masking | Vision | `VNGenerateForegroundInstanceMaskRequest`, `VNDetectContoursRequest` |
| Subject Interaction | VisionKit | `ImageAnalyzer`, `ImageAnalysis`, `ImageAnalysisInteraction` |
| Inpainting | Metal (custom) | Patch-based synthesis, mask feathering, blending |
| Compositing | Core Image | Color adjustments, preview pipeline |
| Fallback Processing | Accelerate/vImage | Simulator, older devices without Metal |
---
### Features
#### 1. Person Removal
**How it works**:
1. User taps a person in the photo
2. `VNGenerateForegroundInstanceMaskRequest` generates a precise mask
3. Mask is dilated and feathered
4. Custom Metal inpainting fills the region from surrounding context
**Why this works**: Vision's person segmentation is robust and well-documented for iOS 17+.
**User flow**:
```
Tap person → Mask preview shown → Confirm → Inpaint → Done
```
**Edge cases**:
- Multiple people: user taps each individually or uses "select all people" option
- Partial occlusion: Vision still provides usable mask; user can refine with brush
- No person detected: show "No person found at tap location" feedback
---
#### 2. Foreground Object Removal
**How it works**:
1. User taps an object
2. `VNGenerateForegroundInstanceMaskRequest` attempts to isolate it
3. If successful, mask is used for inpainting
4. If Vision returns no mask (object not salient), fall back to smart brush
**Smart brush fallback**:
- User paints rough selection over object
- App refines selection to nearest strong edges using gradient magnitude analysis
- User confirms refined mask
**Why this works**: Vision detects visually distinct foreground regions. It doesn't know *what* the object is, but it can separate it from the background if there's sufficient contrast.
**Limitations** (be explicit with users):
- Works best on objects that stand out from their background
- Low-contrast objects require manual brush selection
- App cannot identify object types — it sees shapes, not meanings
**User flow**:
```
Tap object → Vision attempts mask
├─ Success → Mask preview → Confirm → Inpaint
└─ Failure → "Use brush to select" prompt → User paints → Edge refinement → Confirm → Inpaint
```
---
#### 3. Wire & Line Removal
**How it works**:
1. User taps near a wire or line
2. `VNDetectContoursRequest` returns all detected contours
3. App scores contours by:
- Proximity to tap point
- Aspect ratio (thin and elongated)
- Straightness / low curvature
- Length (longer scores higher)
4. Best-scoring contour becomes mask
5. Mask is expanded to configurable width (default 6px, range 220px)
6. Inpaint along the mask
**Line brush fallback**:
When contour detection fails (low contrast, busy background):
- User switches to "Line brush" tool
- User draws along the wire
- App maintains consistent stroke width
- Stroke becomes mask for inpainting
**Why this works**: Power lines against sky have strong edges that `VNDetectContoursRequest` captures reliably. The scoring heuristics select the most "wire-like" contour.
**Limitations**:
- High-contrast lines (sky background): works well
- Low-contrast lines (against buildings, trees): requires manual line brush
- Curved wires: contour detection still works; scoring allows moderate curvature
**User flow**:
```
Tap near wire → Contour analysis
├─ Match found → Highlight line → Confirm → Inpaint
└─ No match → "Use line brush" prompt → User draws → Inpaint
```
---
### Inpainting Engine (Metal)
Since there's no public Apple API for content-aware fill, you must implement this yourself.
**Algorithm**: Exemplar-based inpainting (Criminisi-style)
**Why this approach**:
- Deterministic (same input → same output)
- Handles textures reasonably well
- No ML required
- Well-documented in academic literature
**Pipeline**:
```
1. Input: source image + binary mask
2. Dilate mask by 24px (capture edge pixels)
3. Feather mask edges (gaussian blur on alpha)
4. Build image pyramid (for preview vs export)
5. For each pixel on mask boundary (priority order):
a. Find best matching patch from known region
b. Copy patch into unknown region
c. Update boundary
6. Final edge-aware blend to reduce seams
```
**Performance targets**:
| Resolution | Target Time | Device Baseline |
|------------|-------------|-----------------|
| Preview (2048px) | < 300ms | iPhone 12 / A14 |
| Export (12MP) | < 4 seconds | iPhone 12 / A14 |
| Export (48MP) | < 12 seconds | iPhone 15 Pro / A17 |
**Memory management**:
- Tile-based processing for images > 12MP
- Peak memory budget: 1.5GB
- Release intermediate textures aggressively
---
### Data Model (Non-Destructive Editing)
**Principles**:
- Original image is never modified
- All edits stored as an operation stack
- Full undo/redo support
**Operation types**:
```swift
enum EditOperation: Codable {
case mask(MaskOperation)
case inpaint(InpaintOperation)
case adjustment(AdjustmentOperation)
}
struct MaskOperation: Codable {
let id: UUID
let toolType: ToolType // .person, .object, .wire, .brush
let maskData: Data // compressed R8 texture
let timestamp: Date
}
struct InpaintOperation: Codable {
let id: UUID
let maskOperationId: UUID
let patchRadius: Int
let featherAmount: Float
let timestamp: Date
}
```
**Persistence**:
- Project saved as JSON (operation stack) + original image reference
- Store PHAsset local identifier when sourced from Photos
- Store embedded image data when imported from Files
- Cached previews marked `isExcludedFromBackup = true`
---
### UI Specification
**Main Canvas**:
- Pinch to zoom, pan to navigate
- Mask overlay toggle (red tint / marching ants / hidden)
- Before/after comparison (long press or toggle)
**Toolbar**:
| Tool | Icon | Behavior |
|------|------|----------|
| Person | 👤 | Tap to select/remove people |
| Object | ⬭ | Tap to select foreground objects |
| Wire | ⚡ | Tap to select lines/wires |
| Brush | 🖌 | Manual selection for fallback |
| Undo | ↩ | Step back in operation stack |
| Redo | ↪ | Step forward in operation stack |
**Inspector Panel** (contextual):
- Brush size slider (when brush active)
- Feather amount slider
- Mask expansion slider (for wire tool)
- "Refine edges" toggle
**Feedback states**:
- Processing: show spinner on affected region
- No detection: toast message with fallback suggestion
- Success: brief checkmark animation
---
### Error Handling
| Scenario | Response |
|----------|----------|
| Vision returns no mask | "Couldn't detect object. Try the brush tool to select manually." |
| Vision returns low-confidence mask | Show mask preview with "Does this look right?" confirmation |
| Contour detection finds no lines | "No lines detected. Use the line brush to draw along the wire." |
| Inpaint produces visible seams | Offer "Refine" button that expands mask and re-runs |
| Memory pressure during export | "Image too large to process. Try cropping first." |
| Metal unavailable | Fall back to Accelerate with "Processing may be slower" warning |
---
### Privacy & Permissions
- All processing on-device
- No network calls for core functionality
- Photo library access via `PHPickerViewController` (limited access supported)
- Request write permission only when user saves
- No analytics or telemetry in core features
---
### Accessibility
- All tools labeled for VoiceOver
- Brush size adjustable via stepper (not just slider)
- High contrast mask visualization option
- Reduce Motion: disable transition animations
- Dynamic Type support in all UI text
---
### Testing Requirements
| Test Type | Coverage |
|-----------|----------|
| Unit | Edit stack operations, mask combination logic, contour scoring |
| Snapshot | Inpaint engine (reference images with known outputs) |
| UI | Full flow: import → edit → export |
| Performance | Render times on A14, A15, A17 devices |
| Memory | Peak usage during 48MP export |
---
### Project Structure
```
CheapRetouch/
├── App/
│ └── CheapRetouchApp.swift
├── Features/
│ ├── Editor/
│ │ ├── PhotoEditorView.swift
│ │ ├── CanvasView.swift
│ │ └── ToolbarView.swift
│ └── Export/
│ └── ExportView.swift
├── Services/
│ ├── MaskingService.swift // Vision/VisionKit wrappers
│ ├── ContourService.swift // Line detection + scoring
│ └── InpaintEngine/
│ ├── InpaintEngine.swift // Public interface
│ ├── Shaders.metal // Metal kernels
│ └── PatchMatch.swift // Algorithm implementation
├── Models/
│ ├── EditOperation.swift
│ ├── Project.swift
│ └── MaskData.swift
├── Utilities/
│ ├── ImagePipeline.swift // Preview/export rendering
│ └── EdgeRefinement.swift // Smart brush edge detection
└── Resources/
└── Assets.xcassets
```
---
### What's Explicitly Out of Scope
| Feature | Reason |
|---------|--------|
| Automatic fence/mesh detection | Requires semantic understanding (ML) |
| Object type identification | Requires classification (ML) |
| "Find all X in photo" | Requires semantic search (ML) |
| Blemish/skin retouching | Removed to keep scope focused; could add later |
| Background replacement | Different feature set; out of scope for v1 |
---
### Summary
This spec delivers three solid features using only public APIs:
1. **Person removal** — Vision handles the hard part
2. **Object removal** — Vision-assisted with brush fallback
3. **Wire removal** — Contour detection with line brush fallback
Each feature has a clear primary path and a fallback for when detection fails. The user is never stuck.

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