Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu 82948ada92 fix: improve debug panel layout and log display
- Clear logs between rotation updates (no more expanding text)
- Increase canvas size from 600x450 to 900x600 for better visibility
- Reorganize layout: left controls, right canvas + single-line log
- Make crop box much larger and easier to see
2026-04-22 11:20:43 +03:00

228 lines
8.2 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;
originalPhotoPath?: string;
originalCropBounds?: { x: number; y: number; width: number; height: number };
originalRotation?: number;
imageProcessing?: any;
onClose: () => void;
}
export function DebugRotationPanel({
item,
imageUrl,
originalPhotoPath,
originalCropBounds,
originalRotation = 0,
imageProcessing,
onClose,
}: DebugRotationPanelProps) {
const [rotation, setRotation] = useState(originalRotation);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [log, setLog] = useState('');
const imageRef = useRef<HTMLImageElement | null>(null);
const [imageLoaded, setImageLoaded] = useState(false);
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
const containerRef = useRef<HTMLDivElement>(null);
// Use original photo path if available, otherwise fall back to processed image URL
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
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 updateLog = (message: string) => {
setLog(message);
};
useEffect(() => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
imageRef.current = img;
setImageLoaded(true);
// Calculate scaled dimensions to fit available space (much larger)
const maxWidth = 900;
const maxHeight = 600;
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 });
updateLog(`📸 ${img.width}×${img.height}px → scaled ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (${scale.toFixed(2)}x)`);
};
img.onerror = () => updateLog('❌ Failed to load image');
img.src = displayImageUrl;
}, [displayImageUrl]);
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;
// Draw scaled image
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Calculate crop box in scaled coordinates
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();
// Update log with current state
const logText = `Rotation: ${rotation}° | Crop: (${cropX.toFixed(0)}, ${cropY.toFixed(0)}) ${cropW.toFixed(0)}×${cropH.toFixed(0)}px`;
updateLog(logText);
}, [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-6xl w-full max-h-[95vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-3 border-b border-slate-700 flex items-center justify-between bg-slate-800 flex-shrink-0">
<h2 className="text-lg font-normal text-white">Debug Rotation & Crop</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-700 rounded text-gray-400 hover:text-white transition"
>
<X size={20} />
</button>
</div>
<div className="flex gap-4 p-4 flex-1 overflow-hidden">
{/* Left: Controls */}
<div className="w-48 bg-slate-800 p-3 rounded space-y-4 flex-shrink-0 overflow-y-auto">
{/* Rotation Slider */}
<div>
<label className="block text-sm font-normal text-white mb-2">
Rotation: <span className="text-yellow-400">{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-normal text-white mb-2">Quick Angles</label>
<div className="grid grid-cols-2 gap-1">
{commonAngles.map((angle) => (
<button
key={angle.value}
onClick={() => setRotation(angle.value)}
className={`py-1 px-2 rounded text-xs font-normal 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 space-y-1">
<h3 className="font-normal text-white">Original AI Values</h3>
<div className="text-gray-300 font-mono text-xs">
<div>Rotation: {originalRotation}°</div>
{originalCropBounds && (
<>
<div>Crop ({originalCropBounds.x}, {originalCropBounds.y})</div>
<div>Size {originalCropBounds.width}×{originalCropBounds.height}</div>
</>
)}
{imageProcessing?.confidence && (
<div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div>
)}
</div>
</div>
)}
</div>
{/* Right: Canvas + Log */}
<div className="flex-1 flex flex-col gap-3 min-w-0 overflow-hidden">
{/* Canvas - takes most space */}
<div className="flex-1 flex flex-col bg-slate-800 p-3 rounded border border-slate-700 overflow-hidden">
<h3 className="text-xs font-normal text-white mb-2">Preview (Green = Crop, Orange = Rotation)</h3>
<div className="flex-1 flex items-center justify-center bg-black/70 rounded border border-slate-600 overflow-auto">
<canvas
ref={canvasRef}
className="rounded"
/>
</div>
</div>
{/* Log - single line */}
<div className="bg-slate-800 p-2 rounded border border-slate-700 flex-shrink-0">
<div className="bg-black text-green-400 p-2 rounded font-mono text-xs">
{log}
</div>
</div>
</div>
</div>
</div>
</div>
);
}