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:
31
specs/01-project-setup.md
Normal file
31
specs/01-project-setup.md
Normal 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
31
specs/02-data-model.md
Normal 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
|
||||
43
specs/03-inpaint-engine.md
Normal file
43
specs/03-inpaint-engine.md
Normal 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
|
||||
28
specs/04-masking-service.md
Normal file
28
specs/04-masking-service.md
Normal 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
|
||||
30
specs/05-contour-service.md
Normal file
30
specs/05-contour-service.md
Normal 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
34
specs/06-canvas-view.md
Normal 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
37
specs/07-toolbar-view.md
Normal 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
|
||||
37
specs/08-photo-editor-view.md
Normal file
37
specs/08-photo-editor-view.md
Normal 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
|
||||
33
specs/09-person-removal.md
Normal file
33
specs/09-person-removal.md
Normal 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
|
||||
34
specs/10-object-removal.md
Normal file
34
specs/10-object-removal.md
Normal 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
36
specs/11-wire-removal.md
Normal 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
29
specs/12-image-import.md
Normal 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
33
specs/13-export-view.md
Normal 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
32
specs/14-brush-tool.md
Normal 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
29
specs/15-accessibility.md
Normal 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
|
||||
Reference in New Issue
Block a user