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:
@@ -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 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''\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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
52
CheapRetouch/Utilities/DebugLogger.swift
Normal file
52
CheapRetouch/Utilities/DebugLogger.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user