mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 06:56:33 +00:00
278 lines
11 KiB
JavaScript
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; |