Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu 1a774088a1 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
2026-04-22 10:56:27 +03:00

229 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<HTMLCanvasElement>(null);
const [logs, setLogs] = useState<string[]>([]);
const [imageLoaded, setImageLoaded] = useState(false);
const imageRef = useRef<HTMLImageElement>(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 (
<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>
<div className="grid grid-cols-2 gap-6 p-6">
{/* Left: Controls */}
<div className="space-y-6">
{/* Rotation Slider */}
<div>
<label className="block text-sm font-semibold mb-2">
Rotation: <span className="text-blue-600 font-bold">{rotation}°</span>
</label>
<input
type="range"
min="-180"
max="180"
value={rotation}
onChange={(e) => {
const val = Number(e.target.value);
setRotation(val);
}}
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">
{commonAngles.map((angle) => (
<button
key={angle.value}
onClick={() => setRotation(angle.value)}
className={`py-2 px-3 rounded text-xs font-semibold transition ${
rotation === angle.value
? 'bg-blue-600 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}`}
>
{angle.label}
</button>
))}
</div>
</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">
<div>Rotation: {originalRotation}°</div>
{originalCropBounds && (
<div>
Crop: ({originalCropBounds.x}, {originalCropBounds.y}) {originalCropBounds.width}×{originalCropBounds.height}
</div>
)}
{imageProcessing?.confidence && (
<div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div>
)}
</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>
<canvas
ref={canvasRef}
className="border bg-white max-w-full h-auto mx-auto 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>
</div>
</div>
</div>
</div>
);
}