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 [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
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
|
// Use original photo path if available, otherwise fall back to processed image URL
|
||||||
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
|
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
|
||||||
|
|
||||||
@@ -49,6 +53,97 @@ export function DebugRotationPanel({
|
|||||||
setLog(message);
|
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(() => {
|
useEffect(() => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
@@ -70,7 +165,7 @@ export function DebugRotationPanel({
|
|||||||
}, [displayImageUrl]);
|
}, [displayImageUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
|
if (!imageLoaded || !imageRef.current || !canvasRef.current || !displayCropBounds) return;
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
@@ -87,10 +182,10 @@ export function DebugRotationPanel({
|
|||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Calculate crop box in scaled coordinates
|
// Calculate crop box in scaled coordinates
|
||||||
const cropX = originalCropBounds.x * scale;
|
const cropX = displayCropBounds.x * scale;
|
||||||
const cropY = originalCropBounds.y * scale;
|
const cropY = displayCropBounds.y * scale;
|
||||||
const cropW = originalCropBounds.width * scale;
|
const cropW = displayCropBounds.width * scale;
|
||||||
const cropH = originalCropBounds.height * scale;
|
const cropH = displayCropBounds.height * scale;
|
||||||
|
|
||||||
// Save state
|
// Save state
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -118,6 +213,17 @@ export function DebugRotationPanel({
|
|||||||
ctx.shadowBlur = 3;
|
ctx.shadowBlur = 3;
|
||||||
ctx.strokeRect(cropX, cropY, cropW, cropH);
|
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)
|
// Draw rotation indicator (rotated rectangle inside crop area)
|
||||||
if (Math.abs(rotation) > 0.5) {
|
if (Math.abs(rotation) > 0.5) {
|
||||||
ctx.strokeStyle = '#FFB800';
|
ctx.strokeStyle = '#FFB800';
|
||||||
@@ -147,9 +253,14 @@ export function DebugRotationPanel({
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// Update log with current state
|
// 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);
|
updateLog(logText);
|
||||||
}, [imageLoaded, rotation, originalCropBounds, scaledDimensions]);
|
}, [imageLoaded, rotation, displayCropBounds, scaledDimensions, adjustedCropBounds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
<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>
|
||||||
</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 */}
|
{/* Original Values */}
|
||||||
{originalRotation !== undefined && (
|
{originalRotation !== undefined && (
|
||||||
<div className="bg-slate-700 p-3 rounded text-xs space-y-1">
|
<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">
|
<div className="flex-1 flex items-center justify-center bg-black/70 rounded border border-slate-600 overflow-auto">
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="rounded"
|
className="rounded cursor-move"
|
||||||
|
onMouseDown={handleCanvasMouseDown}
|
||||||
|
onMouseMove={handleCanvasMouseMove}
|
||||||
|
onMouseUp={handleCanvasMouseUp}
|
||||||
|
onMouseLeave={handleCanvasMouseUp}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user