From 52094862e01a4d187a7a0b9c114c97aa6b40c7fb Mon Sep 17 00:00:00 2001 From: jared Date: Sat, 24 Jan 2026 12:58:27 -0500 Subject: [PATCH] Add debug logging throughout the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/settings.local.json | 10 +++- .../Features/Editor/BrushCanvasView.swift | 14 ++++- .../Features/Editor/EditorViewModel.swift | 46 ++++++++++++++-- .../InpaintEngine/InpaintEngine.swift | 8 +++ .../Services/InpaintEngine/PatchMatch.swift | 7 +++ CheapRetouch/Services/MaskingService.swift | 6 +++ CheapRetouch/Utilities/DebugLogger.swift | 52 +++++++++++++++++++ 7 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 CheapRetouch/Utilities/DebugLogger.swift diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3ab8411..97c1ae7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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 \nEOF\n\\)\")", "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 \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 \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 \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 \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 \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 \nEOF\n\\)\")", + "Bash(grep:*)", + "Bash(git commit:*)" ] } } diff --git a/CheapRetouch/Features/Editor/BrushCanvasView.swift b/CheapRetouch/Features/Editor/BrushCanvasView.swift index c88129b..0c8542f 100644 --- a/CheapRetouch/Features/Editor/BrushCanvasView.swift +++ b/CheapRetouch/Features/Editor/BrushCanvasView.swift @@ -169,10 +169,18 @@ struct BrushCanvasView: View { } 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 scaleY = imageSize.height / displayedImageFrame.height + DebugLogger.log("Scale factors: X=\(scaleX), Y=\(scaleY)") // Convert all strokes to image coordinates var imageCoordStrokes: [[CGPoint]] = [] @@ -225,11 +233,15 @@ struct BrushCanvasView: View { } if let cgImage = maskImage.cgImage { + DebugLogger.imageInfo("Created brush mask", image: cgImage) await viewModel.applyBrushMask(cgImage) + } else { + DebugLogger.error("Failed to create CGImage from brush mask") } // Clear strokes after applying allStrokes.removeAll() + DebugLogger.log("Strokes cleared") } } diff --git a/CheapRetouch/Features/Editor/EditorViewModel.swift b/CheapRetouch/Features/Editor/EditorViewModel.swift index 6580acb..95fb91b 100644 --- a/CheapRetouch/Features/Editor/EditorViewModel.swift +++ b/CheapRetouch/Features/Editor/EditorViewModel.swift @@ -74,7 +74,12 @@ final class EditorViewModel { // MARK: - Image Loading 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 editedImage = nil @@ -108,7 +113,12 @@ final class EditorViewModel { // MARK: - Tap Handling 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 errorMessage = nil @@ -117,21 +127,25 @@ final class EditorViewModel { switch selectedTool { case .person: processingMessage = "Detecting person..." + DebugLogger.processing("Starting person detection") try await handlePersonTap(at: point, in: image) case .object: processingMessage = "Detecting object..." + DebugLogger.processing("Starting object detection") try await handleObjectTap(at: point, in: image) case .wire: processingMessage = "Detecting wire..." + DebugLogger.processing("Starting wire detection") try await handleWireTap(at: point, in: image) case .brush: - // Brush tool handles drawing directly in CanvasView + DebugLogger.log("Brush tool - tap ignored (handled by canvas)") break } } catch { + DebugLogger.error("handleTap failed", error: error) errorMessage = error.localizedDescription } @@ -140,17 +154,23 @@ final class EditorViewModel { } 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) detectedPeopleCount = result.instanceCount + DebugLogger.state("Detected \(result.instanceCount) people, confidence: \(result.confidence)") guard let mask = result.mask else { + DebugLogger.log("No person found at tap location") errorMessage = "No person found at tap location" return } + DebugLogger.imageInfo("Person mask before dilation", image: mask) + // Dilate mask by 3px to capture edge pixels let dilatedMask = dilateMask(mask, by: 3) + DebugLogger.imageInfo("Person mask after dilation", image: dilatedMask) maskPreview = dilatedMask ?? mask showingMaskConfirmation = true @@ -158,6 +178,7 @@ final class EditorViewModel { // Check for low confidence and flag for user warning isLowConfidenceMask = result.confidence < 0.7 + DebugLogger.state("Mask preview set, lowConfidence: \(isLowConfidenceMask)") } func selectAllPeople() async { @@ -278,11 +299,15 @@ final class EditorViewModel { } func finishLineBrush() async { + DebugLogger.action("finishLineBrush called, path points: \(lineBrushPath.count)") guard let image = displayImage, lineBrushPath.count >= 2 else { + DebugLogger.log("Not enough points or no image, clearing path") lineBrushPath.removeAll() return } + DebugLogger.imageInfo("Source image for line brush", image: image) + isProcessing = true processingMessage = "Creating line mask..." @@ -296,12 +321,14 @@ final class EditorViewModel { lineBrushPath.removeAll() guard let mask = mask else { + DebugLogger.error("Failed to create mask from line brush") errorMessage = "Failed to create mask from line brush" isProcessing = false processingMessage = "" return } + DebugLogger.imageInfo("Line brush mask created", image: mask) maskPreview = mask showingMaskConfirmation = true isLineBrushMode = false @@ -385,13 +412,22 @@ final class EditorViewModel { // MARK: - Mask Confirmation 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 processingMessage = "Removing..." do { + DebugLogger.processing("Starting inpaint...") let result = try await inpaintEngine.inpaint(image: image, mask: mask) + DebugLogger.imageInfo("Inpaint result", image: result) editedImage = result // Add operation to project @@ -481,6 +517,8 @@ final class EditorViewModel { // MARK: - Brush Tool func applyBrushMask(_ mask: CGImage) async { + DebugLogger.action("applyBrushMask called") + DebugLogger.imageInfo("Brush mask", image: mask) maskPreview = mask await confirmMask() } diff --git a/CheapRetouch/Services/InpaintEngine/InpaintEngine.swift b/CheapRetouch/Services/InpaintEngine/InpaintEngine.swift index b2e2b8b..5b32a8c 100644 --- a/CheapRetouch/Services/InpaintEngine/InpaintEngine.swift +++ b/CheapRetouch/Services/InpaintEngine/InpaintEngine.swift @@ -59,18 +59,26 @@ actor InpaintEngine { } 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 let imageMemory = image.width * image.height * 4 let maskMemory = mask.width * mask.height 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 { + DebugLogger.error("Memory pressure - image too large") throw InpaintError.memoryPressure } if isMetalAvailable { + DebugLogger.log("Using Metal for inpainting") return try await inpaintWithMetal(image: image, mask: mask, isPreview: false) } else { + DebugLogger.log("Using Accelerate fallback for inpainting") return try await inpaintWithAccelerate(image: image, mask: mask) } } diff --git a/CheapRetouch/Services/InpaintEngine/PatchMatch.swift b/CheapRetouch/Services/InpaintEngine/PatchMatch.swift index 736bb90..46574db 100644 --- a/CheapRetouch/Services/InpaintEngine/PatchMatch.swift +++ b/CheapRetouch/Services/InpaintEngine/PatchMatch.swift @@ -67,6 +67,10 @@ final class PatchMatchInpainter { let width = image.width 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 let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( pixelFormat: .rgba8Unorm, @@ -98,8 +102,11 @@ final class PatchMatchInpainter { } // Load image data into texture + DebugLogger.log("Loading source image into texture...") 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) + DebugLogger.log("Textures loaded successfully") // Copy source to result for initial state try copyTexture(from: sourceTexture, to: resultTexture) diff --git a/CheapRetouch/Services/MaskingService.swift b/CheapRetouch/Services/MaskingService.swift index a4b0419..6a3a875 100644 --- a/CheapRetouch/Services/MaskingService.swift +++ b/CheapRetouch/Services/MaskingService.swift @@ -43,13 +43,19 @@ actor MaskingService { } 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 handler = VNImageRequestHandler(cgImage: image, options: [:]) do { + DebugLogger.log("Performing Vision request...") try handler.perform([request]) + DebugLogger.log("Vision request completed") } catch { + DebugLogger.error("Vision request failed", error: error) throw MaskingError.requestFailed(error) } diff --git a/CheapRetouch/Utilities/DebugLogger.swift b/CheapRetouch/Utilities/DebugLogger.swift new file mode 100644 index 0000000..d257477 --- /dev/null +++ b/CheapRetouch/Utilities/DebugLogger.swift @@ -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") + } + } +}