| import React, { useState } from "react"; | |
| import { AnnotationPath } from "../types"; | |
| import { useAppStore } from "../store"; | |
| export const AnnotationNode = ({ ann }: { ann: AnnotationPath }) => { | |
| const { | |
| zoom, | |
| selectedNodeIds, | |
| setSelectedNodeIds, | |
| updateSelectedNodes, | |
| isAnnotationMode, | |
| isClickThrough, | |
| annotations, | |
| images, | |
| textNotes, | |
| } = useAppStore(); | |
| const [isDragging, setIsDragging] = useState(false); | |
| const isSelected = selectedNodeIds.includes(ann.id); | |
| const handlePointerDown = (e: React.PointerEvent) => { | |
| if (isAnnotationMode || isClickThrough) return; | |
| if (!isSelected) { | |
| let idsToSelect = [ann.id]; | |
| if (ann.groupId) { | |
| const groupAnnotations = annotations | |
| .filter((a) => a.groupId === ann.groupId) | |
| .map((a) => a.id); | |
| const groupImages = images | |
| .filter((i) => i.groupId === ann.groupId) | |
| .map((i) => i.id); | |
| const groupNotes = textNotes | |
| .filter((n) => n.groupId === ann.groupId) | |
| .map((n) => n.id); | |
| idsToSelect = [...groupAnnotations, ...groupImages, ...groupNotes]; | |
| } | |
| if (e.shiftKey) { | |
| setSelectedNodeIds( | |
| Array.from(new Set([...selectedNodeIds, ...idsToSelect])), | |
| ); | |
| } else { | |
| setSelectedNodeIds(idsToSelect); | |
| } | |
| } | |
| setIsDragging(true); | |
| e.currentTarget.setPointerCapture(e.pointerId); | |
| }; | |
| const handlePointerMove = (e: React.PointerEvent) => { | |
| if (isDragging) { | |
| e.stopPropagation(); | |
| updateSelectedNodes(e.movementX / zoom, e.movementY / zoom, ann.id); | |
| } | |
| }; | |
| const handlePointerUp = (e: React.PointerEvent) => { | |
| if (isDragging) { | |
| setIsDragging(false); | |
| e.currentTarget.releasePointerCapture(e.pointerId); | |
| e.stopPropagation(); | |
| } | |
| }; | |
| return ( | |
| <polyline | |
| points={ann.points.map((p) => `${p.x},${p.y}`).join(" ")} | |
| fill="none" | |
| stroke={isSelected ? "#0A84FF" : ann.color} | |
| strokeWidth={ | |
| ann.strokeWidth * zoom * (ann.isHighlighter ? 3 : 1) + | |
| (isSelected ? 2 : 0) | |
| } | |
| strokeLinecap={ann.isHighlighter ? "square" : "round"} | |
| strokeLinejoin={ann.isHighlighter ? "miter" : "round"} | |
| opacity={ann.isHighlighter ? 0.4 : 1} | |
| style={{ | |
| ...(ann.isHighlighter ? { mixBlendMode: "screen" } : {}), | |
| pointerEvents: isAnnotationMode || isClickThrough ? "none" : "stroke", | |
| cursor: "default", | |
| }} | |
| onPointerDown={handlePointerDown} | |
| onPointerMove={handlePointerMove} | |
| onPointerUp={handlePointerUp} | |
| onDoubleClick={(e) => { | |
| e.stopPropagation(); | |
| }} | |
| /> | |
| ); | |
| }; | |