Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu 9f65d427a0 optimize: make debug panel more compact to fit screen
Layout changes:
- Reduced padding and margins throughout
- Changed from 3-column to 2-column layout
- Canvas + logs on right side (side-by-side)
- Smaller fonts and spacing
- Sticky header that stays in view
- Max-height with scrolling for modal
- Removed footer text to save space
- More efficient use of screen real estate

Now fits on most screen sizes even with large rotation angles.
2026-04-22 11:08:49 +03:00

238 lines
8.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';
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-2">
<div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl max-w-5xl w-full max-h-[95vh] overflow-y-auto">
{/* Header */}
<div className="p-2 border-b border-slate-700 flex items-center justify-between bg-slate-800 sticky top-0">
<h2 className="text-base 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={18} />
</button>
</div>
<div className="grid grid-cols-2 gap-2 p-3 auto-rows-max">
{/* Left: Controls */}
<div className="space-y-2 bg-slate-800 p-2 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 + Right: Logs */}
<div className="col-span-2 flex gap-2">
{/* Canvas */}
<div className="flex-1 flex flex-col bg-slate-800 p-2 rounded">
<h3 className="text-xs font-semibold text-white mb-1">Image Preview</h3>
<div className="border border-slate-600 rounded flex items-center justify-center bg-black/50 flex-1 min-h-72">
<canvas
ref={canvasRef}
className="max-w-full max-h-full rounded"
/>
</div>
<div className="text-xs text-gray-400 mt-1 text-center">
<span>🟢 Crop </span>
<span>🟠 Rotate</span>
</div>
</div>
{/* Logs */}
<div className="w-64 bg-slate-800 p-2 rounded flex flex-col">
<h3 className="text-xs font-semibold text-white mb-1">Logs</h3>
<div className="bg-black text-green-400 p-2 rounded font-mono text-xs flex-1 overflow-y-auto border border-slate-700">
{logs.map((log, i) => (
<div key={i} className="whitespace-pre-wrap">{log}</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}