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.
240 lines
8.7 KiB
TypeScript
240 lines
8.7 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { Item } from '@/lib/db';
|
||
import { X } from 'lucide-react';
|
||
|
||
interface DebugRotationPanelProps {
|
||
item: Item;
|
||
imageUrl: string;
|
||
originalCropBounds?: { x: number; y: number; width: number; height: number };
|
||
originalRotation?: number;
|
||
imageProcessing?: any;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function DebugRotationPanel({
|
||
item,
|
||
imageUrl,
|
||
originalCropBounds,
|
||
originalRotation = 0,
|
||
imageProcessing,
|
||
onClose,
|
||
}: DebugRotationPanelProps) {
|
||
const [rotation, setRotation] = useState(originalRotation);
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
const [logs, setLogs] = useState<string[]>([]);
|
||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||
const [imageLoaded, setImageLoaded] = useState(false);
|
||
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
|
||
|
||
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 },
|
||
];
|
||
|
||
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`);
|
||
|
||
// 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]);
|
||
|
||
useEffect(() => {
|
||
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
|
||
|
||
const canvas = canvasRef.current;
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const img = imageRef.current;
|
||
const { scale } = scaledDimensions;
|
||
|
||
// Set canvas size to match scaled image
|
||
canvas.width = scaledDimensions.width;
|
||
canvas.height = scaledDimensions.height;
|
||
|
||
addLog(`\n🎨 --- Drawing Preview ---`);
|
||
addLog(`Rotation: ${rotation}°`);
|
||
|
||
// Draw scaled image
|
||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||
|
||
// 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;
|
||
|
||
// Save state
|
||
ctx.save();
|
||
|
||
// Draw semi-transparent overlay (darken everything outside crop)
|
||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 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();
|
||
|
||
// 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/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-3 gap-4 p-6">
|
||
{/* Left: Controls */}
|
||
<div className="space-y-4 bg-slate-800 p-4 rounded">
|
||
{/* Rotation Slider */}
|
||
<div>
|
||
<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) => setRotation(Number(e.target.value))}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* Quick Presets */}
|
||
<div>
|
||
<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-1 px-2 rounded text-xs font-semibold transition ${
|
||
rotation === angle.value
|
||
? 'bg-yellow-600 text-white'
|
||
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
|
||
}`}
|
||
>
|
||
{angle.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Original Values */}
|
||
{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 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>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 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="max-w-full max-h-96 rounded"
|
||
/>
|
||
</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>
|
||
);
|
||
}
|