Add debug logging throughout the app

Added DebugLogger utility with emoji-prefixed console output for:
- User actions (👆)
- State changes (📊)
- Processing steps (⚙️)
- Errors ()
- Image info (🖼️)

Logging added to:
- EditorViewModel: tap handling, mask operations, brush/line brush
- InpaintEngine: memory checks, Metal vs Accelerate path
- PatchMatch: texture creation and loading
- MaskingService: Vision requests
- BrushCanvasView: stroke creation and mask generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-24 12:58:27 -05:00
parent 4fbf1abb57
commit 52094862e0
7 changed files with 137 additions and 6 deletions

View File

@@ -10,7 +10,15 @@
"Bash(git commit -m \"$\\(cat <<''EOF''\nImplement core project structure and foundation components\n\nProject Setup \\(Spec 01\\):\n- Configure iOS 17.0 deployment target\n- Add photo library usage descriptions\n- Create proper folder structure \\(App, Features, Services, Models, Utilities\\)\n\nData Model \\(Spec 02\\):\n- Add EditOperation enum with mask, inpaint, adjustment cases\n- Add MaskOperation, InpaintOperation structs \\(Codable\\)\n- Add Project model with operation stack and undo/redo support\n- Add MaskData with dilation support via vImage\n\nServices \\(Specs 03-05\\):\n- Add InpaintEngine with Metal + Accelerate fallback\n- Add MaskingService wrapping Vision framework\n- Add ContourService for wire/line detection and scoring\n\nUI Components \\(Specs 06-08\\):\n- Add PhotoEditorView with photo picker integration\n- Add CanvasView with pinch-to-zoom and pan gestures\n- Add ToolbarView with tool selection and inspector panel\n\nUtilities:\n- Add ImagePipeline for preview/export rendering\n- Add EdgeRefinement for smart brush edge detection\n- Add check_build.sh for CI verification\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nImplement core project structure and foundation components\n\nProject Setup \\(Spec 01\\):\n- Configure iOS 17.0 deployment target\n- Add photo library usage descriptions\n- Create proper folder structure \\(App, Features, Services, Models, Utilities\\)\n\nData Model \\(Spec 02\\):\n- Add EditOperation enum with mask, inpaint, adjustment cases\n- Add MaskOperation, InpaintOperation structs \\(Codable\\)\n- Add Project model with operation stack and undo/redo support\n- Add MaskData with dilation support via vImage\n\nServices \\(Specs 03-05\\):\n- Add InpaintEngine with Metal + Accelerate fallback\n- Add MaskingService wrapping Vision framework\n- Add ContourService for wire/line detection and scoring\n\nUI Components \\(Specs 06-08\\):\n- Add PhotoEditorView with photo picker integration\n- Add CanvasView with pinch-to-zoom and pan gestures\n- Add ToolbarView with tool selection and inspector panel\n\nUtilities:\n- Add ImagePipeline for preview/export rendering\n- Add EdgeRefinement for smart brush edge detection\n- Add check_build.sh for CI verification\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git push)", "Bash(git push)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nIntegrate services with UI via EditorViewModel\n\n- Add EditorViewModel coordinating masking, contour detection, and inpainting\n- Connect PhotoEditorView to viewModel with mask confirmation bar\n- Add processing overlay and error toast UI\n- Update CanvasView with tap-to-mask functionality\n- Add coordinate conversion from view space to image space\n- Update ToolbarView to bind to viewModel state\n- Wire up undo/redo actions through viewModel\n\nPerson, object, and wire removal now flow:\ntap -> detect -> preview mask -> confirm -> inpaint\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nIntegrate services with UI via EditorViewModel\n\n- Add EditorViewModel coordinating masking, contour detection, and inpainting\n- Connect PhotoEditorView to viewModel with mask confirmation bar\n- Add processing overlay and error toast UI\n- Update CanvasView with tap-to-mask functionality\n- Add coordinate conversion from view space to image space\n- Update ToolbarView to bind to viewModel state\n- Wire up undo/redo actions through viewModel\n\nPerson, object, and wire removal now flow:\ntap -> detect -> preview mask -> confirm -> inpaint\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd export functionality with format options\n\n- Add ExportView with JPEG, PNG, HEIC format support\n- Include quality slider for JPEG compression\n- Show image dimensions and estimated file size\n- Implement save to Photo Library with proper permissions\n- Add share sheet for AirDrop, Messages, etc.\n- Wire export button in PhotoEditorView to show ExportView\n- Add HEIC encoding extension for UIImage\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")" "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd export functionality with format options\n\n- Add ExportView with JPEG, PNG, HEIC format support\n- Include quality slider for JPEG compression\n- Show image dimensions and estimated file size\n- Implement save to Photo Library with proper permissions\n- Add share sheet for AirDrop, Messages, etc.\n- Wire export button in PhotoEditorView to show ExportView\n- Add HEIC encoding extension for UIImage\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(bash:*)",
"Bash(xcodebuild:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd Metal-based inpainting and brush selection tools\n\nImplement GPU-accelerated inpainting using Metal compute shaders:\n- Add Shaders.metal with dilateMask, gaussianBlur, diffuseInpaint, edgeAwareBlend kernels\n- Add PatchMatchInpainter class for exemplar-based inpainting\n- Update InpaintEngine to use Metal with Accelerate fallback\n- Add BrushCanvasView for manual brush-based mask painting\n- Add LineBrushView for wire removal line drawing\n- Update CanvasView to integrate brush canvas overlay\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git push:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nFix ImagePipeline operation rendering\n\nUpdate MaskOperation to store mask dimensions for reconstruction.\nImplement proper operation rendering in ImagePipeline:\n- Apply mask+inpaint operations with proper mask reconstruction\n- Handle adjustment operations by type \\(brightness/contrast/saturation\\)\n- Scale masks when rendering previews at reduced resolution\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd person/wire removal features and accessibility support\n\n- Person removal: select all people option, mask dilation, brush refinement\n- Wire removal: line brush fallback mode with Catmull-Rom smoothing\n- Image import: Files app document picker, large image warnings\n- Accessibility: VoiceOver labels, announcements, Dynamic Type support,\n high contrast mask option, Reduce Motion, 44pt touch targets, steppers\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(grep:*)",
"Bash(git commit:*)"
] ]
} }
} }

View File

@@ -169,10 +169,18 @@ struct BrushCanvasView: View {
} }
private func applyBrushMask() async { private func applyBrushMask() async {
guard !allStrokes.isEmpty else { return } DebugLogger.action("BrushCanvasView.applyBrushMask called")
guard !allStrokes.isEmpty else {
DebugLogger.log("No strokes to apply")
return
}
DebugLogger.log("Stroke count: \(allStrokes.count), total points: \(allStrokes.reduce(0) { $0 + $1.count })")
DebugLogger.log("Image size: \(imageSize), displayed frame: \(displayedImageFrame)")
let scaleX = imageSize.width / displayedImageFrame.width let scaleX = imageSize.width / displayedImageFrame.width
let scaleY = imageSize.height / displayedImageFrame.height let scaleY = imageSize.height / displayedImageFrame.height
DebugLogger.log("Scale factors: X=\(scaleX), Y=\(scaleY)")
// Convert all strokes to image coordinates // Convert all strokes to image coordinates
var imageCoordStrokes: [[CGPoint]] = [] var imageCoordStrokes: [[CGPoint]] = []
@@ -225,11 +233,15 @@ struct BrushCanvasView: View {
} }
if let cgImage = maskImage.cgImage { if let cgImage = maskImage.cgImage {
DebugLogger.imageInfo("Created brush mask", image: cgImage)
await viewModel.applyBrushMask(cgImage) await viewModel.applyBrushMask(cgImage)
} else {
DebugLogger.error("Failed to create CGImage from brush mask")
} }
// Clear strokes after applying // Clear strokes after applying
allStrokes.removeAll() allStrokes.removeAll()
DebugLogger.log("Strokes cleared")
} }
} }

View File

@@ -74,7 +74,12 @@ final class EditorViewModel {
// MARK: - Image Loading // MARK: - Image Loading
func loadImage(_ uiImage: UIImage, localIdentifier: String? = nil) { func loadImage(_ uiImage: UIImage, localIdentifier: String? = nil) {
guard let cgImage = uiImage.cgImage else { return } DebugLogger.action("loadImage called")
guard let cgImage = uiImage.cgImage else {
DebugLogger.error("Failed to get CGImage from UIImage")
return
}
DebugLogger.imageInfo("Loading image", image: cgImage)
originalImage = cgImage originalImage = cgImage
editedImage = nil editedImage = nil
@@ -108,7 +113,12 @@ final class EditorViewModel {
// MARK: - Tap Handling // MARK: - Tap Handling
func handleTap(at point: CGPoint) async { func handleTap(at point: CGPoint) async {
guard let image = displayImage else { return } DebugLogger.action("handleTap at point: \(point), tool: \(selectedTool)")
guard let image = displayImage else {
DebugLogger.error("handleTap: No display image available")
return
}
DebugLogger.imageInfo("Current display image", image: image)
isProcessing = true isProcessing = true
errorMessage = nil errorMessage = nil
@@ -117,21 +127,25 @@ final class EditorViewModel {
switch selectedTool { switch selectedTool {
case .person: case .person:
processingMessage = "Detecting person..." processingMessage = "Detecting person..."
DebugLogger.processing("Starting person detection")
try await handlePersonTap(at: point, in: image) try await handlePersonTap(at: point, in: image)
case .object: case .object:
processingMessage = "Detecting object..." processingMessage = "Detecting object..."
DebugLogger.processing("Starting object detection")
try await handleObjectTap(at: point, in: image) try await handleObjectTap(at: point, in: image)
case .wire: case .wire:
processingMessage = "Detecting wire..." processingMessage = "Detecting wire..."
DebugLogger.processing("Starting wire detection")
try await handleWireTap(at: point, in: image) try await handleWireTap(at: point, in: image)
case .brush: case .brush:
// Brush tool handles drawing directly in CanvasView DebugLogger.log("Brush tool - tap ignored (handled by canvas)")
break break
} }
} catch { } catch {
DebugLogger.error("handleTap failed", error: error)
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
@@ -140,17 +154,23 @@ final class EditorViewModel {
} }
private func handlePersonTap(at point: CGPoint, in image: CGImage) async throws { private func handlePersonTap(at point: CGPoint, in image: CGImage) async throws {
DebugLogger.log("handlePersonTap at \(point)")
let result = try await maskingService.generatePersonMaskWithCount(at: point, in: image) let result = try await maskingService.generatePersonMaskWithCount(at: point, in: image)
detectedPeopleCount = result.instanceCount detectedPeopleCount = result.instanceCount
DebugLogger.state("Detected \(result.instanceCount) people, confidence: \(result.confidence)")
guard let mask = result.mask else { guard let mask = result.mask else {
DebugLogger.log("No person found at tap location")
errorMessage = "No person found at tap location" errorMessage = "No person found at tap location"
return return
} }
DebugLogger.imageInfo("Person mask before dilation", image: mask)
// Dilate mask by 3px to capture edge pixels // Dilate mask by 3px to capture edge pixels
let dilatedMask = dilateMask(mask, by: 3) let dilatedMask = dilateMask(mask, by: 3)
DebugLogger.imageInfo("Person mask after dilation", image: dilatedMask)
maskPreview = dilatedMask ?? mask maskPreview = dilatedMask ?? mask
showingMaskConfirmation = true showingMaskConfirmation = true
@@ -158,6 +178,7 @@ final class EditorViewModel {
// Check for low confidence and flag for user warning // Check for low confidence and flag for user warning
isLowConfidenceMask = result.confidence < 0.7 isLowConfidenceMask = result.confidence < 0.7
DebugLogger.state("Mask preview set, lowConfidence: \(isLowConfidenceMask)")
} }
func selectAllPeople() async { func selectAllPeople() async {
@@ -278,11 +299,15 @@ final class EditorViewModel {
} }
func finishLineBrush() async { func finishLineBrush() async {
DebugLogger.action("finishLineBrush called, path points: \(lineBrushPath.count)")
guard let image = displayImage, lineBrushPath.count >= 2 else { guard let image = displayImage, lineBrushPath.count >= 2 else {
DebugLogger.log("Not enough points or no image, clearing path")
lineBrushPath.removeAll() lineBrushPath.removeAll()
return return
} }
DebugLogger.imageInfo("Source image for line brush", image: image)
isProcessing = true isProcessing = true
processingMessage = "Creating line mask..." processingMessage = "Creating line mask..."
@@ -296,12 +321,14 @@ final class EditorViewModel {
lineBrushPath.removeAll() lineBrushPath.removeAll()
guard let mask = mask else { guard let mask = mask else {
DebugLogger.error("Failed to create mask from line brush")
errorMessage = "Failed to create mask from line brush" errorMessage = "Failed to create mask from line brush"
isProcessing = false isProcessing = false
processingMessage = "" processingMessage = ""
return return
} }
DebugLogger.imageInfo("Line brush mask created", image: mask)
maskPreview = mask maskPreview = mask
showingMaskConfirmation = true showingMaskConfirmation = true
isLineBrushMode = false isLineBrushMode = false
@@ -385,13 +412,22 @@ final class EditorViewModel {
// MARK: - Mask Confirmation // MARK: - Mask Confirmation
func confirmMask() async { func confirmMask() async {
guard let image = displayImage, let mask = maskPreview else { return } DebugLogger.action("confirmMask called")
guard let image = displayImage, let mask = maskPreview else {
DebugLogger.error("confirmMask: Missing image or mask")
return
}
DebugLogger.imageInfo("Source image for inpaint", image: image)
DebugLogger.imageInfo("Mask for inpaint", image: mask)
isProcessing = true isProcessing = true
processingMessage = "Removing..." processingMessage = "Removing..."
do { do {
DebugLogger.processing("Starting inpaint...")
let result = try await inpaintEngine.inpaint(image: image, mask: mask) let result = try await inpaintEngine.inpaint(image: image, mask: mask)
DebugLogger.imageInfo("Inpaint result", image: result)
editedImage = result editedImage = result
// Add operation to project // Add operation to project
@@ -481,6 +517,8 @@ final class EditorViewModel {
// MARK: - Brush Tool // MARK: - Brush Tool
func applyBrushMask(_ mask: CGImage) async { func applyBrushMask(_ mask: CGImage) async {
DebugLogger.action("applyBrushMask called")
DebugLogger.imageInfo("Brush mask", image: mask)
maskPreview = mask maskPreview = mask
await confirmMask() await confirmMask()
} }

View File

@@ -59,18 +59,26 @@ actor InpaintEngine {
} }
func inpaint(image: CGImage, mask: CGImage) async throws -> CGImage { func inpaint(image: CGImage, mask: CGImage) async throws -> CGImage {
DebugLogger.processing("InpaintEngine.inpaint called")
DebugLogger.log("Image: \(image.width)x\(image.height), Mask: \(mask.width)x\(mask.height)")
// Check memory requirements // Check memory requirements
let imageMemory = image.width * image.height * 4 let imageMemory = image.width * image.height * 4
let maskMemory = mask.width * mask.height let maskMemory = mask.width * mask.height
let estimatedTotal = imageMemory * 3 + maskMemory * 2 // rough estimate let estimatedTotal = imageMemory * 3 + maskMemory * 2 // rough estimate
DebugLogger.log("Estimated memory: \(estimatedTotal / 1_000_000)MB, max: \(maxMemoryBytes / 1_000_000)MB")
guard estimatedTotal < maxMemoryBytes else { guard estimatedTotal < maxMemoryBytes else {
DebugLogger.error("Memory pressure - image too large")
throw InpaintError.memoryPressure throw InpaintError.memoryPressure
} }
if isMetalAvailable { if isMetalAvailable {
DebugLogger.log("Using Metal for inpainting")
return try await inpaintWithMetal(image: image, mask: mask, isPreview: false) return try await inpaintWithMetal(image: image, mask: mask, isPreview: false)
} else { } else {
DebugLogger.log("Using Accelerate fallback for inpainting")
return try await inpaintWithAccelerate(image: image, mask: mask) return try await inpaintWithAccelerate(image: image, mask: mask)
} }
} }

View File

@@ -67,6 +67,10 @@ final class PatchMatchInpainter {
let width = image.width let width = image.width
let height = image.height let height = image.height
DebugLogger.processing("PatchMatchInpainter.inpaint started")
DebugLogger.log("Creating textures: \(width)x\(height), mask: \(mask.width)x\(mask.height)")
DebugLogger.log("Patch radius: \(patchRadius), diffusion iterations: \(diffusionIterations), feather: \(featherAmount)")
// Create textures // Create textures
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .rgba8Unorm, pixelFormat: .rgba8Unorm,
@@ -98,8 +102,11 @@ final class PatchMatchInpainter {
} }
// Load image data into texture // Load image data into texture
DebugLogger.log("Loading source image into texture...")
try loadCGImage(image, into: sourceTexture) try loadCGImage(image, into: sourceTexture)
DebugLogger.log("Loading mask into texture (will scale from \(mask.width)x\(mask.height) to \(width)x\(height))...")
try loadCGImageGrayscale(mask, into: maskTexture) try loadCGImageGrayscale(mask, into: maskTexture)
DebugLogger.log("Textures loaded successfully")
// Copy source to result for initial state // Copy source to result for initial state
try copyTexture(from: sourceTexture, to: resultTexture) try copyTexture(from: sourceTexture, to: resultTexture)

View File

@@ -43,13 +43,19 @@ actor MaskingService {
} }
func generatePersonMaskWithCount(at point: CGPoint, in image: CGImage) async throws -> MaskResult { func generatePersonMaskWithCount(at point: CGPoint, in image: CGImage) async throws -> MaskResult {
DebugLogger.processing("MaskingService.generatePersonMaskWithCount at \(point)")
DebugLogger.log("Image size: \(image.width)x\(image.height)")
let request = VNGenerateForegroundInstanceMaskRequest() let request = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: image, options: [:]) let handler = VNImageRequestHandler(cgImage: image, options: [:])
do { do {
DebugLogger.log("Performing Vision request...")
try handler.perform([request]) try handler.perform([request])
DebugLogger.log("Vision request completed")
} catch { } catch {
DebugLogger.error("Vision request failed", error: error)
throw MaskingError.requestFailed(error) throw MaskingError.requestFailed(error)
} }

View File

@@ -0,0 +1,52 @@
//
// DebugLogger.swift
// CheapRetouch
//
// Debug logging utility for tracking user actions and app state.
//
import Foundation
import os.log
import CoreGraphics
enum DebugLogger: Sendable {
private static let logger = Logger(subsystem: "com.cheapretouch", category: "debug")
nonisolated static func log(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
let fileName = (file as NSString).lastPathComponent
let logMessage = "[\(fileName):\(line)] \(function) - \(message)"
print("🔍 \(logMessage)")
}
nonisolated static func action(_ message: String) {
print("👆 ACTION: \(message)")
}
nonisolated static func state(_ message: String) {
print("📊 STATE: \(message)")
}
nonisolated static func error(_ message: String, error: Error? = nil) {
if let error = error {
print("❌ ERROR: \(message) - \(error.localizedDescription)")
} else {
print("❌ ERROR: \(message)")
}
}
nonisolated static func processing(_ message: String) {
print("⚙️ PROCESSING: \(message)")
}
nonisolated static func imageInfo(_ label: String, width: Int, height: Int) {
print("🖼️ \(label): \(width)x\(height) pixels")
}
nonisolated static func imageInfo(_ label: String, image: CGImage?) {
if let image = image {
print("🖼️ \(label): \(image.width)x\(image.height) pixels")
} else {
print("🖼️ \(label): nil")
}
}
}