From dba47aa656e2597fe80054895987ecab54b5da99 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Wed, 22 Apr 2026 11:47:13 +0300 Subject: [PATCH] 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 --- frontend/components/DebugRotationPanel.tsx | 140 +++++++++++++++++++-- 1 file changed, 132 insertions(+), 8 deletions(-) diff --git a/frontend/components/DebugRotationPanel.tsx b/frontend/components/DebugRotationPanel.tsx index 69df51f5..ba05f9ca 100644 --- a/frontend/components/DebugRotationPanel.tsx +++ b/frontend/components/DebugRotationPanel.tsx @@ -31,6 +31,10 @@ export function DebugRotationPanel({ const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 }); const containerRef = useRef(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) => { + 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) => { + 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 (
@@ -203,6 +314,15 @@ export function DebugRotationPanel({
+ {/* Reset Button */} + + {/* Original Values */} {originalRotation !== undefined && (
@@ -231,7 +351,11 @@ export function DebugRotationPanel({