From 1a774088a1e70f3d10678ff13645b9b621823f7e Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Wed, 22 Apr 2026 10:56:27 +0300 Subject: [PATCH] feat: add debug rotation panel for real-time testing New DebugRotationPanel component allows testing different rotation angles with live preview and detailed debug logs showing: - Current rotation angle (slider + quick presets) - Crop bounds and calculations - Image dimensions at each step - Final processed image preview - Confidence score Integrated into ItemDetailModal via wrench icon in header (only visible when item has a photo). Helps identify correct rotation values for images at any angle. Debug panel shows: - Original image info - Crop bounds and resulting size - Rotation angle and final canvas size - Step-by-step processing logs - Live canvas preview of result --- frontend/components/DebugRotationPanel.tsx | 228 +++++++++++++++++++++ frontend/components/ItemDetailModal.tsx | 41 +++- 2 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 frontend/components/DebugRotationPanel.tsx diff --git a/frontend/components/DebugRotationPanel.tsx b/frontend/components/DebugRotationPanel.tsx new file mode 100644 index 00000000..2c4c44b6 --- /dev/null +++ b/frontend/components/DebugRotationPanel.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { Item } from '@/lib/db'; + +interface DebugRotationPanelProps { + item: Item; + imageUrl: string; + originalCropBounds?: { x: number; y: number; width: number; height: number }; + originalRotation?: number; + imageProcessing?: any; +} + +export function DebugRotationPanel({ + item, + imageUrl, + originalCropBounds, + originalRotation = 0, + imageProcessing, +}: DebugRotationPanelProps) { + const [rotation, setRotation] = useState(originalRotation); + const [cropBounds, setCropBounds] = useState(originalCropBounds); + const canvasRef = useRef(null); + const [logs, setLogs] = useState([]); + const [imageLoaded, setImageLoaded] = useState(false); + const imageRef = useRef(null); + + const commonAngles = [ + { label: '0°', value: 0 }, + { label: '+22°', value: 22 }, + { label: '-22°', value: -22 }, + { label: '+45°', value: 45 }, + { label: '-45°', value: -45 }, + { label: '+90°', value: 90 }, + { label: '-90°', value: -90 }, + { label: '±180°', value: 180 }, + ]; + + useEffect(() => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => { + imageRef.current = img; + setImageLoaded(true); + addLog(`Image loaded: ${img.width}×${img.height}px`); + }; + img.onerror = () => addLog('❌ Failed to load image'); + img.src = imageUrl; + }, [imageUrl]); + + const addLog = (message: string) => { + setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]); + }; + + useEffect(() => { + if (!imageLoaded || !imageRef.current || !canvasRef.current) return; + + const img = imageRef.current; + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + addLog(`\n--- Processing Step ---`); + addLog(`Original image: ${img.width}×${img.height}px`); + + if (cropBounds) { + addLog(`Crop bounds: x=${cropBounds.x}, y=${cropBounds.y}, w=${cropBounds.width}, h=${cropBounds.height}`); + addLog(`Crop rect: (${cropBounds.x}, ${cropBounds.y}, ${cropBounds.x + cropBounds.width}, ${cropBounds.y + cropBounds.height})`); + } + + // Set canvas size to match cropped result (or full image if no crop) + const finalWidth = cropBounds ? cropBounds.width : img.width; + const finalHeight = cropBounds ? cropBounds.height : img.height; + + // Account for rotation expanding the canvas + let displayWidth = finalWidth; + let displayHeight = finalHeight; + + const radians = (rotation * Math.PI) / 180; + const cos = Math.abs(Math.cos(radians)); + const sin = Math.abs(Math.sin(radians)); + + if (Math.abs(rotation % 90) !== 0) { + displayWidth = Math.ceil(finalWidth * cos + finalHeight * sin); + displayHeight = Math.ceil(finalWidth * sin + finalHeight * cos); + } + + canvas.width = displayWidth; + canvas.height = displayHeight; + + addLog(`After crop: ${finalWidth}×${finalHeight}px`); + addLog(`Applying rotation: ${rotation}°`); + addLog(`Canvas with expansion: ${displayWidth}×${displayHeight}px`); + + // Draw: Crop → Rotate + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Save context state + ctx.save(); + + // Move to center, rotate, move back + ctx.translate(displayWidth / 2, displayHeight / 2); + ctx.rotate((rotation * Math.PI) / 180); + ctx.translate(-finalWidth / 2, -finalHeight / 2); + + // Draw cropped portion of image + if (cropBounds) { + ctx.drawImage( + img, + cropBounds.x, + cropBounds.y, + cropBounds.width, + cropBounds.height, + 0, + 0, + cropBounds.width, + cropBounds.height + ); + } else { + ctx.drawImage(img, 0, 0); + } + + ctx.restore(); + + addLog(`✅ Preview rendered`); + addLog(`Final image: ${displayWidth}×${displayHeight}px`); + }, [imageLoaded, rotation, cropBounds]); + + return ( +
+
+
+

🔧 Debug Rotation Panel

+

Item: {item.name}

+
+ +
+ {/* Left: Controls */} +
+ {/* Rotation Slider */} +
+ + { + const val = Number(e.target.value); + setRotation(val); + }} + className="w-full" + /> +
+ + {/* Quick Presets */} +
+ +
+ {commonAngles.map((angle) => ( + + ))} +
+
+ + {/* Original Values */} + {(originalRotation !== undefined || originalCropBounds) && ( +
+

Original Values

+
+
Rotation: {originalRotation}°
+ {originalCropBounds && ( +
+ Crop: ({originalCropBounds.x}, {originalCropBounds.y}) {originalCropBounds.width}×{originalCropBounds.height} +
+ )} + {imageProcessing?.confidence && ( +
Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%
+ )} +
+
+ )} + + {/* Save Button */} + +
+ + {/* Right: Preview + Logs */} +
+ {/* Canvas Preview */} +
+

Processed Preview

+ +
+ + {/* Logs */} +
+

Debug Logs

+
+ {logs.map((log, i) => ( +
{log}
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/frontend/components/ItemDetailModal.tsx b/frontend/components/ItemDetailModal.tsx index 1841901e..8ee821c7 100644 --- a/frontend/components/ItemDetailModal.tsx +++ b/frontend/components/ItemDetailModal.tsx @@ -4,7 +4,8 @@ import React, { useState, useRef } from 'react'; import { Item } from '@/lib/db'; import { inventoryApi, buildPhotoUrl } from '@/lib/api'; import ItemPhotoUpload from '@/components/ItemPhotoUpload'; -import { X, Camera, Trash2 } from 'lucide-react'; +import { DebugRotationPanel } from '@/components/DebugRotationPanel'; +import { X, Camera, Trash2, Wrench } from 'lucide-react'; import { toast } from 'react-hot-toast'; interface ItemDetailModalProps { @@ -23,6 +24,7 @@ export default function ItemDetailModal({ backendUrl = '', }: ItemDetailModalProps) { const [showPhotoUpload, setShowPhotoUpload] = useState(false); + const [showDebugPanel, setShowDebugPanel] = useState(false); const [currentPhoto, setCurrentPhoto] = useState<{ thumbnail_url: string; full_url: string } | null>( item.photo_path && item.photo_thumbnail_path ? { thumbnail_url: item.photo_thumbnail_path, full_url: item.photo_path } @@ -71,13 +73,25 @@ export default function ItemDetailModal({ {/* Header */}

{item.name}

- +
+ {currentPhoto && ( + + )} + +
{/* Content */} @@ -178,6 +192,17 @@ export default function ItemDetailModal({ + + {/* Debug Rotation Panel */} + {showDebugPanel && currentPhoto && ( + + )} ); }