fix: integrate crop tool with canvas component

The crop tool was previously not connected to the canvas component, preventing it
from receiving pointer events and displaying its overlay. Changes:

- Added cropOverlayNeedsUpdate state to trigger re-renders when crop changes
- Added crop tool overlay rendering in main canvas render effect
- Added crop tool pointer handlers in handlePointerDown, handlePointerMove, and
  handlePointerUp
- Crop tool now creates temporary canvas contexts for state management while
  drawing overlay on display canvas
- Crop overlay is properly transformed with zoom and pan transformations

The crop tool now works correctly: it displays the crop rectangle with handles,
responds to dragging to define/move/resize the crop area, and updates in real-time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 21:03:28 +01:00
parent c3ce440d48
commit 1999aa5252

View File

@@ -74,6 +74,7 @@ export function CanvasWithTools({ onCursorMove }: CanvasWithToolsProps = {}) {
prevY: 0, prevY: 0,
pressure: 1, pressure: 1,
}); });
const [cropOverlayNeedsUpdate, setCropOverlayNeedsUpdate] = useState(0);
// Touch gesture support for mobile // Touch gesture support for mobile
useTouchGestures(containerRef, { useTouchGestures(containerRef, {
@@ -216,7 +217,30 @@ export function CanvasWithTools({ onCursorMove }: CanvasWithToolsProps = {}) {
// Restore context state // Restore context state
ctx.restore(); ctx.restore();
}, [layers, width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, pointer, activeSelection, isMarching, marchingOffset, textObjects, editingTextId]);
// Draw crop tool overlay if crop tool is active
if (activeTool === 'crop') {
const cropTool = toolsCache.current['crop'];
if (cropTool && typeof (cropTool as any).drawCropOverlay === 'function') {
ctx.save();
ctx.translate(offsetX + canvas.width / 2, offsetY + canvas.height / 2);
ctx.scale(zoom, zoom);
ctx.translate(-width / 2, -height / 2);
// Create a temporary canvas context that matches the full canvas size
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
(cropTool as any).drawCropOverlay(tempCtx);
ctx.drawImage(tempCanvas, 0, 0);
}
ctx.restore();
}
}
}, [layers, width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, pointer, activeSelection, isMarching, marchingOffset, textObjects, editingTextId, activeTool, cropOverlayNeedsUpdate]);
// Marching ants animation // Marching ants animation
useEffect(() => { useEffect(() => {
@@ -283,6 +307,38 @@ export function CanvasWithTools({ onCursorMove }: CanvasWithToolsProps = {}) {
return; return;
} }
// Crop tool
if (e.button === 0 && !e.shiftKey && activeTool === 'crop') {
const cropTool = toolsCache.current['crop'];
if (!cropTool) return; // Tool not loaded yet
const newPointer: PointerState = {
isDown: true,
x: canvasPos.x,
y: canvasPos.y,
prevX: canvasPos.x,
prevY: canvasPos.y,
pressure: e.pressure || 1,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey,
};
setPointer(newPointer);
// Create a temporary canvas for the crop tool
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
cropTool.onPointerDown(newPointer, tempCtx, settings);
setCropOverlayNeedsUpdate(prev => prev + 1);
}
return;
}
// Text tool - only handle if editor is not already active // Text tool - only handle if editor is not already active
if (activeTool === 'text') { if (activeTool === 'text') {
// If editor is active, let it handle its own events (selection, dragging, click-outside) // If editor is active, let it handle its own events (selection, dragging, click-outside)
@@ -394,6 +450,35 @@ export function CanvasWithTools({ onCursorMove }: CanvasWithToolsProps = {}) {
return; return;
} }
// Crop tool
if (pointer.isDown && activeTool === 'crop') {
const cropTool = toolsCache.current['crop'];
if (!cropTool) return;
const newPointer: PointerState = {
...pointer,
x: canvasPos.x,
y: canvasPos.y,
pressure: e.pressure || 1,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey,
};
setPointer(newPointer);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
cropTool.onPointerMove(newPointer, tempCtx, settings);
setCropOverlayNeedsUpdate(prev => prev + 1);
}
return;
}
// Drawing // Drawing
if (pointer.isDown && ['pencil', 'brush', 'eraser', 'eyedropper', 'shape', 'clone', 'smudge', 'dodge'].includes(activeTool)) { if (pointer.isDown && ['pencil', 'brush', 'eraser', 'eyedropper', 'shape', 'clone', 'smudge', 'dodge'].includes(activeTool)) {
const activeLayer = getActiveLayer(); const activeLayer = getActiveLayer();
@@ -429,6 +514,23 @@ export function CanvasWithTools({ onCursorMove }: CanvasWithToolsProps = {}) {
return; return;
} }
// Crop tool
if (pointer.isDown && activeTool === 'crop') {
const cropTool = toolsCache.current['crop'];
if (cropTool) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
cropTool.onPointerUp(pointer, tempCtx, settings);
setCropOverlayNeedsUpdate(prev => prev + 1);
}
}
setPointer({ ...pointer, isDown: false });
return;
}
if (pointer.isDown && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper', 'shape'].includes(activeTool)) { if (pointer.isDown && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper', 'shape'].includes(activeTool)) {
const activeLayer = getActiveLayer(); const activeLayer = getActiveLayer();
if (!activeLayer || !activeLayer.canvas) return; if (!activeLayer || !activeLayer.canvas) return;