Fix missing onZoomChange prop declaration in ImageCanvas
This commit is contained in:
@@ -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 */}
|
<button className="clear-selection-btn" onClick={clearSelection}>
|
||||||
<div className="zoom-controls">
|
Clear
|
||||||
<button onClick={handleZoomOut} title="Zoom Out">−</button>
|
</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}>
|
|
||||||
Clear Selection
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ImageCanvas.displayName = 'ImageCanvas';
|
||||||
|
|
||||||
export default ImageCanvas;
|
export default ImageCanvas;
|
||||||
|
|||||||
Reference in New Issue
Block a user