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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user