Fix missing onZoomChange prop declaration in ImageCanvas

This commit is contained in:
Claude
2026-01-26 05:45:44 +00:00
parent 9cd8a5badb
commit c12e8c9508
+264 -305
View File
@@ -1,23 +1,21 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'; import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
import { fabric } from 'fabric'; import { fabric } from 'fabric';
import './ImageCanvas.css'; import './ImageCanvas.css';
const ImageCanvas = ({ const ImageCanvas = forwardRef(({
imageUrl, imageUrl,
onSelectionChange, onSelectionChange,
selectionMode, selectionMode,
advancedToolMode, activeTool,
onAdvancedToolClick, zoom = 100,
zoom = 1,
onZoomChange, onZoomChange,
externalSelection, // { polygon: [[x,y],...], bbox: {x,y,width,height} } onSmartSelect,
}) => { isProcessing
}, ref) => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const fabricCanvasRef = useRef(null); const fabricCanvasRef = useRef(null);
const [currentSelection, setCurrentSelection] = useState(null); const [currentSelection, setCurrentSelection] = useState(null);
const [isDrawing, setIsDrawing] = useState(false); const currentSelectionRef = useRef(null);
const [isTransformMode, setIsTransformMode] = useState(false);
const [currentZoom, setCurrentZoom] = useState(zoom);
const lassoPoints = useRef([]); const lassoPoints = useRef([]);
const onZoomChangeRef = useRef(onZoomChange); const onZoomChangeRef = useRef(onZoomChange);
@@ -26,17 +24,28 @@ const ImageCanvas = ({
onZoomChangeRef.current = onZoomChange; onZoomChangeRef.current = onZoomChange;
}, [onZoomChange]); }, [onZoomChange]);
// Expose methods to parent
useImperativeHandle(ref, () => ({
getCanvas: () => fabricCanvasRef.current,
clearSelection: () => clearSelection(),
}));
// Update selection ref when state changes
useEffect(() => {
currentSelectionRef.current = currentSelection;
}, [currentSelection]);
// Initialize canvas
useEffect(() => { useEffect(() => {
if (!canvasRef.current) return; if (!canvasRef.current) return;
// Initialize Fabric.js canvas
const canvas = new fabric.Canvas(canvasRef.current, { const canvas = new fabric.Canvas(canvasRef.current, {
selection: false, selection: false,
backgroundColor: '#2a2a2a', backgroundColor: 'transparent',
preserveObjectStacking: true,
}); });
fabricCanvasRef.current = canvas; fabricCanvasRef.current = canvas;
// Handle window resize
const handleResize = () => { const handleResize = () => {
const container = canvasRef.current?.parentElement; const container = canvasRef.current?.parentElement;
if (container) { if (container) {
@@ -45,19 +54,9 @@ const ImageCanvas = ({
canvas.setWidth(width); canvas.setWidth(width);
canvas.setHeight(height); canvas.setHeight(height);
// Re-center and rescale the image if it exists // Re-center image if it exists
const bgImage = canvas.backgroundImage; if (imageRef.current) {
if (bgImage) { centerImage(canvas, imageRef.current, zoom / 100);
// Allow scaling up to fill the canvas
const scale = Math.min(
(width - 40) / bgImage.width,
(height - 40) / bgImage.height
);
bgImage.scale(scale);
bgImage.set({
left: (width - bgImage.width * scale) / 2,
top: (height - bgImage.height * scale) / 2,
});
} }
canvas.renderAll(); canvas.renderAll();
} }
@@ -102,6 +101,39 @@ const ImageCanvas = ({
}; };
}, []); // Empty dependency array - only run once on mount }, []); // Empty dependency array - only run once on mount
// Center and scale image
const centerImage = (canvas, img, zoomFactor) => {
if (!img) return;
const padding = 40;
const availableWidth = canvas.width - padding;
const availableHeight = canvas.height - padding;
// Calculate base scale to fit
const fitScale = Math.min(
availableWidth / img.width,
availableHeight / img.height
);
baseScaleRef.current = fitScale;
const scale = fitScale * zoomFactor;
img.scale(scale);
img.set({
left: (canvas.width - img.width * scale) / 2,
top: (canvas.height - img.height * scale) / 2,
});
};
// Apply zoom changes
useEffect(() => {
const canvas = fabricCanvasRef.current;
if (!canvas || !imageRef.current) return;
centerImage(canvas, imageRef.current, zoom / 100);
canvas.renderAll();
}, [zoom]);
// Load image when URL changes // Load image when URL changes
useEffect(() => { useEffect(() => {
if (!fabricCanvasRef.current || !imageUrl) return; if (!fabricCanvasRef.current || !imageUrl) return;
@@ -143,186 +175,155 @@ const ImageCanvas = ({
top: ((canvas.height || 600) - img.height * scale) / 2, top: ((canvas.height || 600) - img.height * scale) / 2,
selectable: false, selectable: false,
evented: false, evented: false,
hoverCursor: 'default',
}); });
imageRef.current = img;
canvas.add(img); canvas.add(img);
canvas.sendToBack(img); canvas.sendToBack(img);
canvas.renderAll();
// Store image reference centerImage(canvas, img, zoom / 100);
canvas.backgroundImage = img; canvas.renderAll();
}, { crossOrigin: 'anonymous' }); }, { crossOrigin: 'anonymous' });
}, [imageUrl]); }, [imageUrl]);
// Handle advanced tool mode clicks (smart-select, color-select) // Handle tool/mode changes
useEffect(() => {
if (!fabricCanvasRef.current || !advancedToolMode) return;
const canvas = fabricCanvasRef.current;
const bgImage = canvas.backgroundImage;
const handleAdvancedClick = async (e) => {
if (!bgImage || !onAdvancedToolClick) return;
const pointer = canvas.getPointer(e.e);
// Convert canvas coordinates to image coordinates
const imgScale = bgImage.scaleX;
const imgLeft = bgImage.left;
const imgTop = bgImage.top;
const imgX = Math.round((pointer.x - imgLeft) / imgScale);
const imgY = Math.round((pointer.y - imgTop) / imgScale);
// Check if click is within image bounds
if (imgX < 0 || imgY < 0 || imgX > bgImage.width || imgY > bgImage.height) {
return;
}
if (advancedToolMode === 'color-select') {
// Get pixel color at click position
const ctx = canvas.getContext('2d');
const canvasX = pointer.x * canvas.getZoom();
const canvasY = pointer.y * canvas.getZoom();
// For color picking, we need to get the color from the image
// Create a temporary canvas to read pixel color
const tempCanvas = document.createElement('canvas');
tempCanvas.width = bgImage.width;
tempCanvas.height = bgImage.height;
const tempCtx = tempCanvas.getContext('2d');
// Draw the image element to temp canvas
const imgElement = bgImage.getElement();
tempCtx.drawImage(imgElement, 0, 0);
const pixelData = tempCtx.getImageData(imgX, imgY, 1, 1).data;
const color = { r: pixelData[0], g: pixelData[1], b: pixelData[2] };
onAdvancedToolClick(imgX, imgY, color);
} else {
onAdvancedToolClick(imgX, imgY, null);
}
};
canvas.on('mouse:down', handleAdvancedClick);
return () => {
canvas.off('mouse:down', handleAdvancedClick);
};
}, [advancedToolMode, onAdvancedToolClick]);
// Handle external selection (from smart-select or color-select)
useEffect(() => {
if (!fabricCanvasRef.current || !externalSelection?.polygon?.length) return;
const canvas = fabricCanvasRef.current;
const bgImage = canvas.backgroundImage;
if (!bgImage) return;
// Clear previous selection
if (currentSelection) {
canvas.remove(currentSelection);
}
// Convert image coordinates to canvas coordinates
const imgScale = bgImage.scaleX;
const imgLeft = bgImage.left;
const imgTop = bgImage.top;
const canvasPoints = externalSelection.polygon.map(([x, y]) => ({
x: x * imgScale + imgLeft,
y: y * imgScale + imgTop,
}));
// Create polygon selection
const polygon = new fabric.Polygon(canvasPoints, {
fill: 'rgba(255, 255, 255, 0.3)',
stroke: '#00ff00',
strokeWidth: 2,
selectable: true,
hasControls: true,
hasBorders: true,
lockRotation: false,
cornerColor: '#00ff00',
cornerSize: 10,
transparentCorners: false,
borderColor: '#00ff00',
borderScaleFactor: 2,
});
canvas.add(polygon);
canvas.setActiveObject(polygon);
setCurrentSelection(polygon);
lassoPoints.current = canvasPoints;
// Notify parent of selection
onSelectionChange({
type: 'polygon',
bbox: externalSelection.bbox,
selectionData: { points: externalSelection.polygon },
});
canvas.renderAll();
}, [externalSelection]);
// Handle selection mode changes
useEffect(() => { useEffect(() => {
if (!fabricCanvasRef.current) return; if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current; const canvas = fabricCanvasRef.current;
// Don't set up selection handlers if in advanced tool mode // Remove all event handlers
if (advancedToolMode) return;
// Clear previous selection when changing modes
if (currentSelection) {
canvas.remove(currentSelection);
setCurrentSelection(null);
onSelectionChange(null);
}
// Reset transform mode
setIsTransformMode(false);
// Set up event handlers based on mode
canvas.off('mouse:down'); canvas.off('mouse:down');
canvas.off('mouse:move'); canvas.off('mouse:move');
canvas.off('mouse:up'); canvas.off('mouse:up');
canvas.off('object:modified'); canvas.off('object:modified');
canvas.off('object:moving');
canvas.off('object:scaling');
// Set up handlers based on selection mode
if (selectionMode === 'rectangle') { if (selectionMode === 'rectangle') {
setupRectangleMode(canvas); setupRectangleMode(canvas);
} else if (selectionMode === 'ellipse') { } else if (selectionMode === 'ellipse') {
setupEllipseMode(canvas); setupEllipseMode(canvas);
} else if (selectionMode === 'lasso') { } else if (selectionMode === 'lasso') {
setupLassoMode(canvas); setupLassoMode(canvas);
} else if (selectionMode === 'smart') {
setupSmartSelectMode(canvas);
} else if (selectionMode === 'color') {
setupColorSelectMode(canvas);
} else if (activeTool === 'move') {
setupMoveMode(canvas);
} else if (activeTool === 'pan') {
setupPanMode(canvas);
} }
}, [selectionMode, advancedToolMode]); }, [selectionMode, activeTool, onSmartSelect]);
const setupRectangleMode = (canvas) => { const setupMoveMode = (canvas) => {
let rect, isDown, startX, startY; // In move mode, allow selecting and moving selection objects
const sel = currentSelectionRef.current;
if (sel) {
sel.set({ selectable: true, evented: true });
canvas.setActiveObject(sel);
}
canvas.on('object:modified', (e) => {
if (e.target && e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target);
}
});
};
const setupPanMode = (canvas) => {
let isPanning = false;
let lastPosX, lastPosY;
canvas.on('mouse:down', (e) => { canvas.on('mouse:down', (e) => {
// If clicking on existing selection, enable transform mode isPanning = true;
if (e.target && e.target === currentSelection) { lastPosX = e.e.clientX;
setIsTransformMode(true); lastPosY = e.e.clientY;
canvas.setCursor('grabbing');
});
canvas.on('mouse:move', (e) => {
if (!isPanning) return;
const deltaX = e.e.clientX - lastPosX;
const deltaY = e.e.clientY - lastPosY;
canvas.relativePan({ x: deltaX, y: deltaY });
lastPosX = e.e.clientX;
lastPosY = e.e.clientY;
});
canvas.on('mouse:up', () => {
isPanning = false;
canvas.setCursor('grab');
});
canvas.setCursor('grab');
};
const setupSmartSelectMode = (canvas) => {
canvas.on('mouse:down', (e) => {
if (isProcessing) return;
const pointer = canvas.getPointer(e.e);
const img = imageRef.current;
if (!img) return;
// Convert to image coordinates
const imgScale = img.scaleX;
const imgLeft = img.left;
const imgTop = img.top;
const x = Math.round((pointer.x - imgLeft) / imgScale);
const y = Math.round((pointer.y - imgTop) / imgScale);
// Check if click is within image bounds
if (x >= 0 && x < img.width && y >= 0 && y < img.height) {
onSmartSelect?.(x, y);
}
});
canvas.setCursor('crosshair');
};
const setupColorSelectMode = (canvas) => {
canvas.on('mouse:down', (e) => {
if (isProcessing) return;
// TODO: Get pixel color at click position
const pointer = canvas.getPointer(e.e);
console.log('Color select at:', pointer);
});
canvas.setCursor('crosshair');
};
const setupRectangleMode = (canvas) => {
let rect = null;
let isDown = false;
let startX, startY;
canvas.on('mouse:down', (e) => {
// Check if clicking on existing selection
const sel = currentSelectionRef.current;
if (e.target && e.target === sel) {
// Allow moving/transforming
return; return;
} }
// If in transform mode and clicking elsewhere, exit transform mode // Clear previous selection
if (isTransformMode) { if (sel) {
setIsTransformMode(false); canvas.remove(sel);
}
// Clear previous selection if exists
if (currentSelection) {
canvas.remove(currentSelection);
setCurrentSelection(null); setCurrentSelection(null);
} }
isDown = true; isDown = true;
isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e); const pointer = canvas.getPointer(e.e);
startX = pointer.x; startX = pointer.x;
startY = pointer.y; startY = pointer.y;
@@ -332,26 +333,24 @@ const ImageCanvas = ({
top: startY, top: startY,
width: 0, width: 0,
height: 0, height: 0,
fill: 'rgba(255, 255, 255, 0.3)', fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#00ff00', stroke: '#0088ff',
strokeWidth: 2, strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true, selectable: true,
hasControls: true, hasControls: true,
hasBorders: true, hasBorders: true,
lockRotation: false, cornerColor: '#0088ff',
cornerColor: '#00ff00', cornerSize: 8,
cornerSize: 10,
transparentCorners: false, transparentCorners: false,
borderColor: '#00ff00', borderColor: '#0088ff',
borderScaleFactor: 2,
}); });
canvas.add(rect); canvas.add(rect);
setCurrentSelection(rect);
}); });
canvas.on('mouse:move', (e) => { canvas.on('mouse:move', (e) => {
if (!isDown || isTransformMode) return; if (!isDown || !rect) return;
const pointer = canvas.getPointer(e.e); const pointer = canvas.getPointer(e.e);
const width = pointer.x - startX; const width = pointer.x - startX;
@@ -368,39 +367,40 @@ const ImageCanvas = ({
}); });
canvas.on('mouse:up', () => { canvas.on('mouse:up', () => {
if (isDown && !isTransformMode) { if (isDown && rect && rect.width > 5 && rect.height > 5) {
isDown = false; isDown = false;
isDrawingRef.current = false;
setCurrentSelection(rect);
canvas.setActiveObject(rect); canvas.setActiveObject(rect);
updateSelection(rect, 'rectangle'); updateSelection(rect, 'rectangle');
} else if (isDown && rect) {
// Selection too small, remove it
canvas.remove(rect);
isDown = false;
isDrawingRef.current = false;
} }
}); });
// Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => { canvas.on('object:modified', (e) => {
if (e.target && e.target === currentSelection) { if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target, 'rectangle'); updateTransformedSelection(e.target);
} }
}); });
}; };
const setupEllipseMode = (canvas) => { const setupEllipseMode = (canvas) => {
let ellipse, isDown, startX, startY; let ellipse = null;
let isDown = false;
let startX, startY;
canvas.on('mouse:down', (e) => { canvas.on('mouse:down', (e) => {
// If clicking on existing selection, enable transform mode const sel = currentSelectionRef.current;
if (e.target && e.target === currentSelection) { if (e.target && e.target === sel) {
setIsTransformMode(true);
return; return;
} }
// If in transform mode and clicking elsewhere, exit transform mode if (sel) {
if (isTransformMode) { canvas.remove(sel);
setIsTransformMode(false);
}
// Clear previous selection if exists
if (currentSelection) {
canvas.remove(currentSelection);
setCurrentSelection(null); setCurrentSelection(null);
} }
@@ -414,26 +414,24 @@ const ImageCanvas = ({
top: startY, top: startY,
rx: 0, rx: 0,
ry: 0, ry: 0,
fill: 'rgba(255, 255, 255, 0.3)', fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#00ff00', stroke: '#0088ff',
strokeWidth: 2, strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true, selectable: true,
hasControls: true, hasControls: true,
hasBorders: true, hasBorders: true,
lockRotation: false, cornerColor: '#0088ff',
cornerColor: '#00ff00', cornerSize: 8,
cornerSize: 10,
transparentCorners: false, transparentCorners: false,
borderColor: '#00ff00', borderColor: '#0088ff',
borderScaleFactor: 2,
}); });
canvas.add(ellipse); canvas.add(ellipse);
setCurrentSelection(ellipse);
}); });
canvas.on('mouse:move', (e) => { canvas.on('mouse:move', (e) => {
if (!isDown || isTransformMode) return; if (!isDown || !ellipse) return;
const pointer = canvas.getPointer(e.e); const pointer = canvas.getPointer(e.e);
const rx = Math.abs(pointer.x - startX) / 2; const rx = Math.abs(pointer.x - startX) / 2;
@@ -442,58 +440,55 @@ const ImageCanvas = ({
ellipse.set({ ellipse.set({
rx: rx, rx: rx,
ry: ry, ry: ry,
left: startX < pointer.x ? startX : pointer.x, left: Math.min(startX, pointer.x),
top: startY < pointer.y ? startY : pointer.y, top: Math.min(startY, pointer.y),
}); });
canvas.renderAll(); canvas.renderAll();
}); });
canvas.on('mouse:up', () => { canvas.on('mouse:up', () => {
if (isDown && !isTransformMode) { if (isDown && ellipse && ellipse.rx > 5 && ellipse.ry > 5) {
isDown = false; isDown = false;
setCurrentSelection(ellipse);
canvas.setActiveObject(ellipse); canvas.setActiveObject(ellipse);
updateSelection(ellipse, 'ellipse'); updateSelection(ellipse, 'ellipse');
} else if (isDown && ellipse) {
canvas.remove(ellipse);
isDown = false;
} }
}); });
// Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => { canvas.on('object:modified', (e) => {
if (e.target && e.target === currentSelection) { if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target, 'ellipse'); updateTransformedSelection(e.target);
} }
}); });
}; };
const setupLassoMode = (canvas) => { const setupLassoMode = (canvas) => {
let polygon, points = [], drawingLine; let points = [];
let drawingLine = null;
let polygon = null;
canvas.on('mouse:down', (e) => { canvas.on('mouse:down', (e) => {
// If clicking on existing selection, enable transform mode const sel = currentSelectionRef.current;
if (e.target && e.target === currentSelection) { if (e.target && e.target === sel) {
setIsTransformMode(true);
return; return;
} }
// If in transform mode and clicking elsewhere, exit transform mode if (sel) {
if (isTransformMode) { canvas.remove(sel);
setIsTransformMode(false);
}
// Clear previous selection if exists
if (currentSelection) {
canvas.remove(currentSelection);
setCurrentSelection(null); setCurrentSelection(null);
} }
setIsDrawing(true); isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e); const pointer = canvas.getPointer(e.e);
points = [{ x: pointer.x, y: pointer.y }]; points = [{ x: pointer.x, y: pointer.y }];
// Create a temporary line for visual feedback while drawing
drawingLine = new fabric.Polyline(points, { drawingLine = new fabric.Polyline(points, {
fill: 'transparent', fill: 'transparent',
stroke: '#00ff00', stroke: '#0088ff',
strokeWidth: 2, strokeWidth: 2,
selectable: false, selectable: false,
evented: false, evented: false,
@@ -503,16 +498,15 @@ const ImageCanvas = ({
}); });
canvas.on('mouse:move', (e) => { canvas.on('mouse:move', (e) => {
if (!isDrawing || isTransformMode) return; if (!isDrawingRef.current) return;
const pointer = canvas.getPointer(e.e); const pointer = canvas.getPointer(e.e);
points.push({ x: pointer.x, y: pointer.y }); points.push({ x: pointer.x, y: pointer.y });
// Remove old line and create new one with updated points
canvas.remove(drawingLine); canvas.remove(drawingLine);
drawingLine = new fabric.Polyline([...points], { drawingLine = new fabric.Polyline([...points], {
fill: 'transparent', fill: 'transparent',
stroke: '#00ff00', stroke: '#0088ff',
strokeWidth: 2, strokeWidth: 2,
selectable: false, selectable: false,
evented: false, evented: false,
@@ -522,59 +516,50 @@ const ImageCanvas = ({
}); });
canvas.on('mouse:up', () => { canvas.on('mouse:up', () => {
if (isDrawing && !isTransformMode && points.length > 2) { if (isDrawingRef.current && points.length > 5) {
setIsDrawing(false); isDrawingRef.current = false;
lassoPoints.current = [...points]; lassoPoints.current = [...points];
// Remove drawing line
canvas.remove(drawingLine); canvas.remove(drawingLine);
// Create final polygon with fill
polygon = new fabric.Polygon(points, { polygon = new fabric.Polygon(points, {
fill: 'rgba(255, 255, 255, 0.3)', fill: 'rgba(0, 136, 255, 0.2)',
stroke: '#00ff00', stroke: '#0088ff',
strokeWidth: 2, strokeWidth: 2,
strokeDashArray: [5, 5],
selectable: true, selectable: true,
hasControls: true, hasControls: true,
hasBorders: true, hasBorders: true,
lockRotation: false, cornerColor: '#0088ff',
cornerColor: '#00ff00', cornerSize: 8,
cornerSize: 10,
transparentCorners: false, transparentCorners: false,
borderColor: '#00ff00', borderColor: '#0088ff',
borderScaleFactor: 2,
}); });
canvas.add(polygon); canvas.add(polygon);
canvas.setActiveObject(polygon); canvas.setActiveObject(polygon);
setCurrentSelection(polygon); setCurrentSelection(polygon);
updateSelection(polygon, 'lasso'); updateSelection(polygon, 'lasso');
} else if (isDrawing) { } else if (isDrawingRef.current) {
setIsDrawing(false); isDrawingRef.current = false;
canvas.remove(drawingLine); canvas.remove(drawingLine);
} }
}); });
// Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => { canvas.on('object:modified', (e) => {
if (e.target && e.target === currentSelection) { if (e.target === currentSelectionRef.current) {
updateTransformedSelection(e.target, 'lasso'); updateTransformedSelection(e.target);
} }
}); });
}; };
const updateSelection = (selection, type) => { const updateSelection = (selection, type) => {
if (!selection || !fabricCanvasRef.current) return; if (!selection || !imageRef.current) return;
const canvas = fabricCanvasRef.current; const img = imageRef.current;
const bgImage = canvas.backgroundImage; const imgScale = img.scaleX;
const imgLeft = img.left;
if (!bgImage) return; const imgTop = img.top;
// Calculate bounding box in original image coordinates
const imgScale = bgImage.scaleX;
const imgLeft = bgImage.left;
const imgTop = bgImage.top;
let bbox, selectionData = null; let bbox, selectionData = null;
@@ -601,39 +586,32 @@ const ImageCanvas = ({
height: Math.round(bounds.height / imgScale), height: Math.round(bounds.height / imgScale),
}; };
// Convert lasso points to relative coordinates within bbox
const relativePoints = lassoPoints.current.map(p => [ const relativePoints = lassoPoints.current.map(p => [
Math.round((p.x - bounds.left) / imgScale), Math.round((p.x - imgLeft) / imgScale) - bbox.x,
Math.round((p.y - bounds.top) / imgScale), Math.round((p.y - imgTop) / imgScale) - bbox.y,
]); ]);
selectionData = { points: relativePoints }; selectionData = { points: relativePoints };
} }
onSelectionChange({ onSelectionChange?.({
type, type,
bbox, bbox,
selectionData, selectionData,
}); });
}; };
// Update selection after transformation (move, scale, rotate) const updateTransformedSelection = (selection) => {
const updateTransformedSelection = (selection, type) => { if (!selection || !imageRef.current) return;
if (!selection || !fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current; const img = imageRef.current;
const bgImage = canvas.backgroundImage; const imgScale = img.scaleX;
const imgLeft = img.left;
const imgTop = img.top;
if (!bgImage) return;
const imgScale = bgImage.scaleX;
const imgLeft = bgImage.left;
const imgTop = bgImage.top;
// Get the transformed bounding rect (accounts for scale and rotation)
const bounds = selection.getBoundingRect(true); const bounds = selection.getBoundingRect(true);
let bbox = { const bbox = {
x: Math.round((bounds.left - imgLeft) / imgScale), x: Math.round((bounds.left - imgLeft) / imgScale),
y: Math.round((bounds.top - imgTop) / imgScale), y: Math.round((bounds.top - imgTop) / imgScale),
width: Math.round(bounds.width / imgScale), width: Math.round(bounds.width / imgScale),
@@ -641,8 +619,8 @@ const ImageCanvas = ({
}; };
let selectionData = null; let selectionData = null;
const type = selection.type === 'polygon' ? 'lasso' : (selection.type === 'ellipse' ? 'ellipse' : 'rectangle');
// For lasso, we need to transform the points based on the object's transformation
if (type === 'lasso' && lassoPoints.current.length > 0) { if (type === 'lasso' && lassoPoints.current.length > 0) {
const matrix = selection.calcTransformMatrix(); const matrix = selection.calcTransformMatrix();
const transformedPoints = lassoPoints.current.map(p => { const transformedPoints = lassoPoints.current.map(p => {
@@ -651,14 +629,14 @@ const ImageCanvas = ({
matrix matrix
); );
return [ return [
Math.round((transformed.x - bounds.left) / imgScale), Math.round((transformed.x - imgLeft) / imgScale) - bbox.x,
Math.round((transformed.y - bounds.top) / imgScale), Math.round((transformed.y - imgTop) / imgScale) - bbox.y,
]; ];
}); });
selectionData = { points: transformedPoints }; selectionData = { points: transformedPoints };
} }
onSelectionChange({ onSelectionChange?.({
type, type,
bbox, bbox,
selectionData, selectionData,
@@ -666,10 +644,12 @@ const ImageCanvas = ({
}; };
const clearSelection = () => { const clearSelection = () => {
if (currentSelection && fabricCanvasRef.current) { const canvas = fabricCanvasRef.current;
fabricCanvasRef.current.remove(currentSelection); const sel = currentSelectionRef.current;
if (sel && canvas) {
canvas.remove(sel);
setCurrentSelection(null); setCurrentSelection(null);
onSelectionChange(null); onSelectionChange?.(null);
} }
}; };
@@ -711,36 +691,15 @@ const ImageCanvas = ({
return ( return (
<div className="canvas-container"> <div className="canvas-container">
<canvas ref={canvasRef} /> <canvas ref={canvasRef} />
{currentSelection && (
{/* Zoom controls */}
<div className="zoom-controls">
<button onClick={handleZoomOut} title="Zoom Out"></button>
<span className="zoom-level">{Math.round(currentZoom * 100)}%</span>
<button onClick={handleZoomIn} title="Zoom In">+</button>
<button onClick={handleZoomReset} title="Reset Zoom"></button>
</div>
{/* Advanced tool mode indicator */}
{advancedToolMode && (
<div className="tool-mode-indicator">
{advancedToolMode === 'smart-select' && 'Click on an object to select it'}
{advancedToolMode === 'color-select' && 'Click on a color to select similar pixels'}
{advancedToolMode === 'object-remove' && 'Click on an object to remove it'}
</div>
)}
{currentSelection && !advancedToolMode && (
<>
<div className="selection-hint">
Click selection to move/resize/rotate
</div>
<button className="clear-selection-btn" onClick={clearSelection}> <button className="clear-selection-btn" onClick={clearSelection}>
Clear Selection Clear
</button> </button>
</>
)} )}
</div> </div>
); );
}; });
ImageCanvas.displayName = 'ImageCanvas';
export default ImageCanvas; export default ImageCanvas;