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.
238 lines
8.6 KiB
TypeScript
238 lines
8.6 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-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>
|
||
);
|
||
}
|