Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu 11f0634721 fix: add rotation direction indicators and fix CORS for dev origins
- Add TOP/BTM labels to rotated box showing which edge is which after rotation
- Allows user to visually see rotation direction without calculating degrees
- Fix allowedDevOrigins to include common private IP ranges (192.168.*, 10.*, 172.16.*)
- Resolves CORS warning when accessing from local network IPs
2026-04-22 11:38:32 +03:00

251 lines
8.9 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 orientation indicators at corners
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.font = 'bold 14px monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('⬆ UP', 10, 5);
ctx.fillText('⬅ LEFT', 5, 20);
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText('RIGHT ➡', canvas.width - 10, 20);
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText('⬇ DOWN', 10, canvas.height - 5);
// Draw crop bounds rectangle (bright green)
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 4;
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 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 = 3;
ctx.setLineDash([5, 5]);
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 2;
// 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);
// Add direction markers to rotated box
ctx.setLineDash([]);
ctx.fillStyle = '#FFB800';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('↑ TOP', 0, -cropH / 2 - 10);
ctx.fillText('↓ BTM', 0, cropH / 2 + 10);
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>
);
}