import React, { useRef, useState, useEffect } from 'react'; import * as AnnotationService from '../services/AnnotationService'; import DetectionContainer from './DetectionContainer'; function CanvasEditor({ width, height, detections, initialCurrentDetection = null, selectedDetectionIndices, onDetectionsChange, onSelectionChange, children, detectionClass }) { const containerRef = useRef(null); const [currentDetection, setCurrentDetection] = useState(initialCurrentDetection); const [mouseDownPos, setMouseDownPos] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [resizeData, setResizeData] = useState(null); const [localDetections, setLocalDetections] = useState(detections || []); const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []); const [dimensions, setDimensions] = useState({ width: width || 640, height: height || 480 }); // Track if we're in a dragging operation const [isDragging, setIsDragging] = useState(false); useEffect(() => { if (width && height) { setDimensions({ width, height }); } }, [width, height]); useEffect(() => { setLocalDetections(detections || []); }, [detections]); useEffect(() => { setLocalSelectedIndices(selectedDetectionIndices || []); }, [selectedDetectionIndices]); const handleMouseDown = (e) => { e.preventDefault(); if (!containerRef.current) return; const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef); setMouseDownPos({ mouseX, mouseY }); let detectionFound = false; for (let i = localDetections.length - 1; i >= 0; i--) { if (AnnotationService.isMouseOverDetection(e.clientX, e.clientY, localDetections[i], containerRef)) { if (e.ctrlKey) { const newSelectedIndices = localSelectedIndices.includes(i) ? localSelectedIndices.filter(index => index !== i) : [...localSelectedIndices, i]; setLocalSelectedIndices(newSelectedIndices); if (onSelectionChange) { onSelectionChange(newSelectedIndices); } } else { const newSelectedIndices = [i]; setLocalSelectedIndices(newSelectedIndices); if (onSelectionChange) { onSelectionChange(newSelectedIndices); } } setDragOffset({ x: mouseX - localDetections[i].x1, y: mouseY - localDetections[i].y1, }); setIsDragging(true); detectionFound = true; break; } } if (!detectionFound) { if (!e.ctrlKey) { setLocalSelectedIndices([]); if (onSelectionChange) { onSelectionChange([]); } } if (detectionClass) { setCurrentDetection({ x1: mouseX, y1: mouseY, x2: mouseX, y2: mouseY, class: detectionClass }); } } }; const handleMouseMove = (e) => { if (!containerRef.current) return; const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef); if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) { // Dragging logic setIsDragging(true); const newDetections = [...localDetections]; const firstSelectedIndex = localSelectedIndices[0]; if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return; const firstSelectedDetection = newDetections[firstSelectedIndex]; const { newX1, newY1, newX2, newY2 } = AnnotationService.calculateNewPosition(mouseX, mouseY, dragOffset, firstSelectedDetection, containerRef); const deltaX = newX1 - firstSelectedDetection.x1; const deltaY = newY1 - firstSelectedDetection.y1; localSelectedIndices.forEach(index => { if (newDetections[index] === undefined) return; const detection = newDetections[index]; let updatedX1 = detection.x1 + deltaX; let updatedY1 = detection.y1 + deltaY; let updatedX2 = detection.x2 + deltaX; let updatedY2 = detection.y2 + deltaY; const bounds = AnnotationService.calculateNewPosition(updatedX1 + dragOffset.x, updatedY1 + dragOffset.y, dragOffset, { ...detection, x1: updatedX1, y1: updatedY1, x2: updatedX2, y2: updatedY2 }, containerRef); detection.x1 = bounds.newX1; detection.y1 = bounds.newY1; detection.x2 = bounds.newX2; detection.y2 = bounds.newY2; }); setLocalDetections(newDetections); if (onDetectionsChange) { onDetectionsChange(newDetections); } } else if (currentDetection && !resizeData) { setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY })); } else if (resizeData) { setIsDragging(true); const { index, position } = resizeData; if (localDetections[index] === undefined) return; const newDetections = [...localDetections]; const detection = { ...newDetections[index] }; const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef); newDetections[index] = updatedDetection; setLocalDetections(newDetections); if (onDetectionsChange) { onDetectionsChange(newDetections); } } }; const handleMouseUp = (e) => { // If we're dragging (or resizing), stop propagation to prevent other elements from reacting if (isDragging || resizeData) { e.stopPropagation(); } if (currentDetection && mouseDownPos) { const dx = Math.abs(currentDetection.x2 - currentDetection.x1); const dy = Math.abs(currentDetection.y2 - currentDetection.y1); if (dx > 5 && dy > 5) { // Normalize coordinates so x1,y1 is always top-left and x2,y2 is bottom-right const normalizedDetection = { ...currentDetection, x1: Math.min(currentDetection.x1, currentDetection.x2), y1: Math.min(currentDetection.y1, currentDetection.y2), x2: Math.max(currentDetection.x1, currentDetection.x2), y2: Math.max(currentDetection.y1, currentDetection.y2), }; const newDetections = [...localDetections, normalizedDetection]; setLocalDetections(newDetections); if (onDetectionsChange) { onDetectionsChange(newDetections); } } } setCurrentDetection(null); setMouseDownPos(null); setDragOffset({ x: 0, y: 0 }); setResizeData(null); setIsDragging(false); }; const handleDetectionMouseDown = (e, index) => { e.stopPropagation(); if (!localSelectedIndices.includes(index)) { if (!e.ctrlKey) { const newSelectedIndices = [index]; setLocalSelectedIndices(newSelectedIndices); onSelectionChange && onSelectionChange(newSelectedIndices); } else { const newSelectedIndices = [...localSelectedIndices, index]; setLocalSelectedIndices(newSelectedIndices); onSelectionChange && onSelectionChange(newSelectedIndices); } } const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef); setDragOffset({ x: mouseX - localDetections[index].x1, y: mouseY - localDetections[index].y1, }); setMouseDownPos({x: mouseX, y: mouseY}); setIsDragging(true); }; const handleResize = (e, index, position) => { e.stopPropagation(); setResizeData({ index, position }); if (!localSelectedIndices.includes(index)) { if (!e.ctrlKey) { setLocalSelectedIndices([index]); onSelectionChange && onSelectionChange([index]); } else{ const newSelectedIndices = [...localSelectedIndices, index]; setLocalSelectedIndices(newSelectedIndices); onSelectionChange && onSelectionChange(newSelectedIndices); } } setIsDragging(true); }; // Add a document-level mouse move and up handler for dragging outside container useEffect(() => { if (isDragging || resizeData) { const handleDocumentMouseMove = (e) => { handleMouseMove(e); }; const handleDocumentMouseUp = (e) => { handleMouseUp(e); }; document.addEventListener('mousemove', handleDocumentMouseMove); document.addEventListener('mouseup', handleDocumentMouseUp); return () => { document.removeEventListener('mousemove', handleDocumentMouseMove); document.removeEventListener('mouseup', handleDocumentMouseUp); }; } }, [isDragging, resizeData, mouseDownPos]); return (
{children}
); } export default CanvasEditor;