refactor: redesign debug panel with overlay visualization
Much better UX for identifying correct rotation: - Shows ORIGINAL image scaled to fit the modal - Draws GREEN bounding box showing the crop area - Draws ORANGE rotated rectangle showing rotation effect - Darkens everything outside the crop area (visual focus) - Rotation slider updates the overlay in real-time - Quick preset buttons for common angles - Detailed debug logs showing all calculations - Better styling: dark theme for readability User can now visually see what will be extracted with the current rotation angle. Report the angle that makes the label readable.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Item } from '@/lib/db';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface DebugRotationPanelProps {
|
||||
item: Item;
|
||||
@@ -9,6 +10,7 @@ interface DebugRotationPanelProps {
|
||||
originalCropBounds?: { x: number; y: number; width: number; height: number };
|
||||
originalRotation?: number;
|
||||
imageProcessing?: any;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DebugRotationPanel({
|
||||
@@ -17,13 +19,14 @@ export function DebugRotationPanel({
|
||||
originalCropBounds,
|
||||
originalRotation = 0,
|
||||
imageProcessing,
|
||||
onClose,
|
||||
}: DebugRotationPanelProps) {
|
||||
const [rotation, setRotation] = useState(originalRotation);
|
||||
const [cropBounds, setCropBounds] = useState(originalCropBounds);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
|
||||
|
||||
const commonAngles = [
|
||||
{ label: '0°', value: 0 },
|
||||
@@ -36,138 +39,140 @@ export function DebugRotationPanel({
|
||||
{ label: '±180°', value: 180 },
|
||||
];
|
||||
|
||||
const addLog = (message: string) => {
|
||||
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
imageRef.current = img;
|
||||
setImageLoaded(true);
|
||||
addLog(`Image loaded: ${img.width}×${img.height}px`);
|
||||
addLog(`📸 Image loaded: ${img.width}×${img.height}px`);
|
||||
|
||||
// Calculate scaled dimensions to fit canvas
|
||||
const maxWidth = 600;
|
||||
const maxHeight = 500;
|
||||
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
|
||||
const scaledW = img.width * scale;
|
||||
const scaledH = img.height * scale;
|
||||
setScaledDimensions({ width: scaledW, height: scaledH, scale });
|
||||
addLog(`📐 Scaled to fit: ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (scale ${scale.toFixed(2)}x)`);
|
||||
};
|
||||
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;
|
||||
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) 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`);
|
||||
const img = imageRef.current;
|
||||
const { scale } = scaledDimensions;
|
||||
|
||||
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 scaled image
|
||||
canvas.width = scaledDimensions.width;
|
||||
canvas.height = scaledDimensions.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;
|
||||
addLog(`\n🎨 --- Drawing Preview ---`);
|
||||
addLog(`Rotation: ${rotation}°`);
|
||||
|
||||
// Account for rotation expanding the canvas
|
||||
let displayWidth = finalWidth;
|
||||
let displayHeight = finalHeight;
|
||||
// Draw scaled image
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const radians = (rotation * Math.PI) / 180;
|
||||
const cos = Math.abs(Math.cos(radians));
|
||||
const sin = Math.abs(Math.sin(radians));
|
||||
// Draw crop bounding box on the ORIGINAL image
|
||||
const cropX = originalCropBounds.x * scale;
|
||||
const cropY = originalCropBounds.y * scale;
|
||||
const cropW = originalCropBounds.width * scale;
|
||||
const cropH = originalCropBounds.height * scale;
|
||||
|
||||
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
|
||||
// Save 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 semi-transparent overlay (darken everything outside crop)
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 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);
|
||||
// Clear the crop area (show full brightness)
|
||||
ctx.clearRect(cropX, cropY, cropW, cropH);
|
||||
|
||||
// Draw crop bounds rectangle
|
||||
ctx.strokeStyle = '#00FF00';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeRect(cropX, cropY, cropW, cropH);
|
||||
|
||||
// Draw rotation indicator (rotated rectangle inside crop area)
|
||||
if (Math.abs(rotation) > 0.5) {
|
||||
ctx.strokeStyle = '#FFB800';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
// Draw a rotated rectangle showing where text will be after rotation
|
||||
ctx.save();
|
||||
ctx.translate(cropX + cropW / 2, cropY + cropH / 2);
|
||||
ctx.rotate((rotation * Math.PI) / 180);
|
||||
ctx.strokeRect(-cropW / 2, -cropH / 2, cropW, cropH);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
addLog(`✅ Preview rendered`);
|
||||
addLog(`Final image: ${displayWidth}×${displayHeight}px`);
|
||||
}, [imageLoaded, rotation, cropBounds]);
|
||||
// Logs
|
||||
addLog(`Crop area: (${originalCropBounds.x}, ${originalCropBounds.y}) ${originalCropBounds.width}×${originalCropBounds.height}px`);
|
||||
addLog(`Scaled crop: (${cropX.toFixed(0)}, ${cropY.toFixed(0)}) ${cropW.toFixed(0)}×${cropH.toFixed(0)}px`);
|
||||
if (Math.abs(rotation) > 0.5) {
|
||||
addLog(`✅ Rotation overlay shown in ORANGE`);
|
||||
}
|
||||
}, [imageLoaded, rotation, originalCropBounds, scaledDimensions]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-auto">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-5xl w-full my-8">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-2xl font-bold">🔧 Debug Rotation Panel</h2>
|
||||
<p className="text-gray-600 text-sm mt-1">Item: {item.name}</p>
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl max-w-4xl w-full">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-slate-700 flex items-center justify-between bg-slate-800">
|
||||
<h2 className="text-xl font-bold text-white">🔧 Debug Rotation & Crop</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-slate-700 rounded text-gray-400 hover:text-white transition"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 p-6">
|
||||
<div className="grid grid-cols-3 gap-4 p-6">
|
||||
{/* Left: Controls */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 bg-slate-800 p-4 rounded">
|
||||
{/* Rotation Slider */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2">
|
||||
Rotation: <span className="text-blue-600 font-bold">{rotation}°</span>
|
||||
<label className="block text-sm font-semibold text-white mb-2">
|
||||
Rotation: <span className="text-yellow-400 font-bold">{rotation}°</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="-180"
|
||||
max="180"
|
||||
value={rotation}
|
||||
onChange={(e) => {
|
||||
const val = Number(e.target.value);
|
||||
setRotation(val);
|
||||
}}
|
||||
onChange={(e) => setRotation(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2">Quick Angles</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<label className="block text-sm font-semibold text-white mb-2">Quick Angles</label>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{commonAngles.map((angle) => (
|
||||
<button
|
||||
key={angle.value}
|
||||
onClick={() => setRotation(angle.value)}
|
||||
className={`py-2 px-3 rounded text-xs font-semibold transition ${
|
||||
className={`py-1 px-2 rounded text-xs font-semibold transition ${
|
||||
rotation === angle.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300'
|
||||
? 'bg-yellow-600 text-white'
|
||||
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
{angle.label}
|
||||
@@ -177,15 +182,18 @@ export function DebugRotationPanel({
|
||||
</div>
|
||||
|
||||
{/* Original Values */}
|
||||
{(originalRotation !== undefined || originalCropBounds) && (
|
||||
<div className="bg-gray-100 p-4 rounded">
|
||||
<h3 className="font-semibold text-sm mb-2">Original Values</h3>
|
||||
<div className="text-xs space-y-1 font-mono">
|
||||
{originalRotation !== undefined && (
|
||||
<div className="bg-slate-700 p-3 rounded text-xs">
|
||||
<h3 className="font-semibold text-white mb-2">Original Values</h3>
|
||||
<div className="text-gray-300 space-y-1 font-mono">
|
||||
<div>Rotation: {originalRotation}°</div>
|
||||
{originalCropBounds && (
|
||||
<div>
|
||||
Crop: ({originalCropBounds.x}, {originalCropBounds.y}) {originalCropBounds.width}×{originalCropBounds.height}
|
||||
</div>
|
||||
<>
|
||||
<div>Crop X: {originalCropBounds.x}</div>
|
||||
<div>Crop Y: {originalCropBounds.y}</div>
|
||||
<div>Width: {originalCropBounds.width}</div>
|
||||
<div>Height: {originalCropBounds.height}</div>
|
||||
</>
|
||||
)}
|
||||
{imageProcessing?.confidence && (
|
||||
<div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div>
|
||||
@@ -193,34 +201,37 @@ export function DebugRotationPanel({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<button className="w-full bg-green-600 text-white py-2 rounded font-semibold hover:bg-green-700">
|
||||
✅ Save Corrected Rotation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right: Preview + Logs */}
|
||||
<div className="space-y-4">
|
||||
{/* Canvas Preview */}
|
||||
<div className="border rounded bg-gray-50 p-4">
|
||||
<h3 className="font-semibold text-sm mb-2">Processed Preview</h3>
|
||||
{/* Center: Canvas Preview */}
|
||||
<div className="flex flex-col items-center justify-center bg-slate-800 p-4 rounded">
|
||||
<h3 className="text-sm font-semibold text-white mb-2">Original Image with Crop Box</h3>
|
||||
<div className="border border-slate-600 rounded flex items-center justify-center bg-black/50">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="border bg-white max-w-full h-auto mx-auto rounded"
|
||||
className="max-w-full max-h-96 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Logs */}
|
||||
<div className="border rounded bg-gray-50 p-4">
|
||||
<h3 className="font-semibold text-sm mb-2">Debug Logs</h3>
|
||||
<div className="bg-black text-green-400 p-3 rounded font-mono text-xs h-32 overflow-y-auto">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i}>{log}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-2 text-center">
|
||||
<div>🟢 Green = Crop area</div>
|
||||
<div>🟠 Orange = Rotation effect</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Logs */}
|
||||
<div className="bg-slate-800 p-4 rounded flex flex-col">
|
||||
<h3 className="text-sm font-semibold text-white mb-2">Debug Logs</h3>
|
||||
<div className="bg-black text-green-400 p-3 rounded font-mono text-xs flex-1 overflow-y-auto border border-slate-700">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i}>{log}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-slate-700 bg-slate-800 text-center text-sm text-gray-300">
|
||||
Find the rotation angle that makes the label text readable, then report the value back.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -201,6 +201,7 @@ export default function ItemDetailModal({
|
||||
originalCropBounds={item.labels_data ? JSON.parse(item.labels_data).image_processing?.crop_bounds : undefined}
|
||||
originalRotation={item.labels_data ? JSON.parse(item.labels_data).image_processing?.rotation_degrees : undefined}
|
||||
imageProcessing={item.labels_data ? JSON.parse(item.labels_data).image_processing : undefined}
|
||||
onClose={() => setShowDebugPanel(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user