feat: make crop box interactive in debug panel

- Add drag-to-move functionality for entire crop box
- Add resize handles at corners (drag to resize)
- Show adjusted bounds separately from AI bounds in log
- Add Reset button to revert to original crop bounds
- Allows visual verification of AI crop detection accuracy
This commit is contained in:
2026-04-22 11:47:13 +03:00
parent 11f0634721
commit dba47aa656

View File

@@ -31,6 +31,10 @@ export function DebugRotationPanel({
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
const containerRef = useRef<HTMLDivElement>(null);
// Adjustable crop bounds (in original image coordinates)
const [adjustedCropBounds, setAdjustedCropBounds] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
const [draggingState, setDraggingState] = useState<{ type: string; startX: number; startY: number; startBounds: any } | null>(null);
// Use original photo path if available, otherwise fall back to processed image URL
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
@@ -49,6 +53,97 @@ export function DebugRotationPanel({
setLog(message);
};
// Get the crop bounds to display (adjusted if user modified, otherwise original)
const displayCropBounds = adjustedCropBounds || originalCropBounds;
// Handle mouse down on canvas for dragging/resizing
const handleCanvasMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!canvasRef.current || !displayCropBounds) return;
const rect = canvasRef.current.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) / (rect.width / scaledDimensions.width);
const mouseY = (e.clientY - rect.top) / (rect.height / scaledDimensions.height);
// Convert screen coords to original image coords
const scale = scaledDimensions.scale;
const origX = mouseX / scale;
const origY = mouseY / scale;
const cropX = displayCropBounds.x;
const cropY = displayCropBounds.y;
const cropW = displayCropBounds.width;
const cropH = displayCropBounds.height;
const margin = 15; // pixels in original coords for resize handles
let dragType = '';
if (origX >= cropX - margin && origX <= cropX + margin && origY >= cropY - margin && origY <= cropY + margin) {
dragType = 'nw'; // NW corner
} else if (origX >= cropX + cropW - margin && origX <= cropX + cropW + margin && origY >= cropY - margin && origY <= cropY + margin) {
dragType = 'ne'; // NE corner
} else if (origX >= cropX - margin && origX <= cropX + margin && origY >= cropY + cropH - margin && origY <= cropY + cropH + margin) {
dragType = 'sw'; // SW corner
} else if (origX >= cropX + cropW - margin && origX <= cropX + cropW + margin && origY >= cropY + cropH - margin && origY <= cropY + cropH + margin) {
dragType = 'se'; // SE corner
} else if (origX >= cropX && origX <= cropX + cropW && origY >= cropY && origY <= cropY + cropH) {
dragType = 'move'; // Inside the box
}
if (dragType) {
setDraggingState({
type: dragType,
startX: origX,
startY: origY,
startBounds: { ...displayCropBounds },
});
}
};
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!draggingState || !canvasRef.current || !displayCropBounds) return;
const rect = canvasRef.current.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) / (rect.width / scaledDimensions.width);
const mouseY = (e.clientY - rect.top) / (rect.height / scaledDimensions.height);
const scale = scaledDimensions.scale;
const origX = mouseX / scale;
const origY = mouseY / scale;
const dx = origX - draggingState.startX;
const dy = origY - draggingState.startY;
const startBounds = draggingState.startBounds;
let newBounds = { ...startBounds };
if (draggingState.type === 'move') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.y = Math.max(0, startBounds.y + dy);
} else if (draggingState.type === 'nw') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.y = Math.max(0, startBounds.y + dy);
newBounds.width = Math.max(50, startBounds.width - dx);
newBounds.height = Math.max(50, startBounds.height - dy);
} else if (draggingState.type === 'ne') {
newBounds.y = Math.max(0, startBounds.y + dy);
newBounds.width = Math.max(50, startBounds.width + dx);
newBounds.height = Math.max(50, startBounds.height - dy);
} else if (draggingState.type === 'sw') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.width = Math.max(50, startBounds.width - dx);
newBounds.height = Math.max(50, startBounds.height + dy);
} else if (draggingState.type === 'se') {
newBounds.width = Math.max(50, startBounds.width + dx);
newBounds.height = Math.max(50, startBounds.height + dy);
}
setAdjustedCropBounds(newBounds);
};
const handleCanvasMouseUp = () => {
setDraggingState(null);
};
useEffect(() => {
const img = new Image();
img.crossOrigin = 'anonymous';
@@ -70,7 +165,7 @@ export function DebugRotationPanel({
}, [displayImageUrl]);
useEffect(() => {
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
if (!imageLoaded || !imageRef.current || !canvasRef.current || !displayCropBounds) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
@@ -87,10 +182,10 @@ export function DebugRotationPanel({
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;
const cropX = displayCropBounds.x * scale;
const cropY = displayCropBounds.y * scale;
const cropW = displayCropBounds.width * scale;
const cropH = displayCropBounds.height * scale;
// Save state
ctx.save();
@@ -118,6 +213,17 @@ export function DebugRotationPanel({
ctx.shadowBlur = 3;
ctx.strokeRect(cropX, cropY, cropW, cropH);
// Draw resize handles at corners
const handleSize = 8;
ctx.fillStyle = '#FFB800';
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 2;
// NW, NE, SW, SE corners
ctx.fillRect(cropX - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize);
// Draw rotation indicator (rotated rectangle inside crop area)
if (Math.abs(rotation) > 0.5) {
ctx.strokeStyle = '#FFB800';
@@ -147,9 +253,14 @@ export function DebugRotationPanel({
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`;
let logText = `Rotation: ${rotation}°`;
if (adjustedCropBounds) {
logText += ` | Adjusted: (${adjustedCropBounds.x}, ${adjustedCropBounds.y}) ${adjustedCropBounds.width}×${adjustedCropBounds.height}px`;
} else {
logText += ` | AI Crop: (${displayCropBounds.x}, ${displayCropBounds.y}) ${displayCropBounds.width}×${displayCropBounds.height}px`;
}
updateLog(logText);
}, [imageLoaded, rotation, originalCropBounds, scaledDimensions]);
}, [imageLoaded, rotation, displayCropBounds, scaledDimensions, adjustedCropBounds]);
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
@@ -203,6 +314,15 @@ export function DebugRotationPanel({
</div>
</div>
{/* Reset Button */}
<button
onClick={() => setAdjustedCropBounds(null)}
disabled={!adjustedCropBounds}
className="w-full py-2 px-3 rounded text-sm font-normal bg-slate-700 text-gray-300 hover:bg-slate-600 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
Reset Crop Box
</button>
{/* Original Values */}
{originalRotation !== undefined && (
<div className="bg-slate-700 p-3 rounded text-xs space-y-1">
@@ -231,7 +351,11 @@ export function DebugRotationPanel({
<div className="flex-1 flex items-center justify-center bg-black/70 rounded border border-slate-600 overflow-auto">
<canvas
ref={canvasRef}
className="rounded"
className="rounded cursor-move"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
onMouseLeave={handleCanvasMouseUp}
/>
</div>
</div>