Files
ui/src/components/CanvasEditor.js
T
Alex Bezdieniezhnykh e18157648c annotation save fixed
2025-03-20 13:37:07 +02:00

278 lines
11 KiB
JavaScript

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 (
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
pointerEvents: 'auto',
}}
>
<div ref={containerRef}
style={{
position: 'absolute',
width: '100%',
height: '100%',
pointerEvents: 'auto',
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{children}
<DetectionContainer
detections={localDetections}
selectedDetectionIndices={localSelectedIndices}
onDetectionMouseDown={handleDetectionMouseDown}
currentDetection={currentDetection}
onResize={handleResize}
/>
</div>
</div>
);
}
export default CanvasEditor;