Spaces:
Running
Running
| import { useCallback, useEffect, useRef, useState } from "react"; | |
| import OpenSeadragon from "openseadragon"; | |
| export interface Annotation { | |
| id: string; | |
| type: "rectangle" | "polygon" | "ellipse" | "brush"; | |
| points: Array<{ x: number; y: number }>; | |
| color: string; | |
| label: string; | |
| completed: boolean; | |
| } | |
| interface AnnotationCanvasProps { | |
| viewer: OpenSeadragon.Viewer | null; | |
| tool: "rectangle" | "polygon" | "ellipse" | "brush" | "select" | "none"; | |
| onAnnotationComplete: (annotation: Annotation) => void; | |
| activeLabel: string; | |
| onAnnotationSelected?: (annotationId: string | null) => void; | |
| annotations: Annotation[]; | |
| selectedAnnotationId?: string | null; | |
| showAnnotations: boolean; | |
| } | |
| export function AnnotationCanvas({ | |
| viewer, | |
| tool, | |
| onAnnotationComplete, | |
| activeLabel, | |
| onAnnotationSelected, | |
| annotations, | |
| selectedAnnotationId, | |
| showAnnotations, | |
| }: AnnotationCanvasProps) { | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| const [isDrawing, setIsDrawing] = useState(false); | |
| const [currentPoints, setCurrentPoints] = useState<Array<{ x: number; y: number }>>([]); | |
| const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null); | |
| const [drawingTool, setDrawingTool] = useState<"rectangle" | "polygon" | "ellipse" | "brush" | null>(null); | |
| const [isPanning, setIsPanning] = useState(false); | |
| const [lastPanPoint, setLastPanPoint] = useState<{ x: number; y: number } | null>(null); | |
| const labelColor = (label: string) => { | |
| switch (label) { | |
| case "Tumor": | |
| return "#EF4444"; | |
| case "Benign": | |
| return "#FACC15"; | |
| case "Stroma": | |
| return "#EC4899"; | |
| case "Necrosis": | |
| return "#22C55E"; | |
| case "DCIS": | |
| return "#3B82F6"; | |
| case "Invasive": | |
| return "#8B5CF6"; | |
| default: | |
| return "#9CA3AF"; | |
| } | |
| }; | |
| const toImagePoint = (point: { x: number; y: number }) => { | |
| if (!viewer) return point; | |
| const viewportPoint = viewer.viewport.pointFromPixel( | |
| new OpenSeadragon.Point(point.x, point.y) | |
| ); | |
| const imagePoint = viewer.viewport.viewportToImageCoordinates(viewportPoint); | |
| return { x: imagePoint.x, y: imagePoint.y }; | |
| }; | |
| const toScreenPoint = (point: { x: number; y: number }) => { | |
| if (!viewer) return point; | |
| const viewportPoint = viewer.viewport.imageToViewportCoordinates( | |
| new OpenSeadragon.Point(point.x, point.y) | |
| ); | |
| const pixelPoint = viewer.viewport.pixelFromPoint(viewportPoint, true); | |
| return { x: pixelPoint.x, y: pixelPoint.y }; | |
| }; | |
| // Setup canvas | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas || !viewer) return; | |
| const updateCanvasSize = () => { | |
| canvas.width = viewer.container.clientWidth; | |
| canvas.height = viewer.container.clientHeight; | |
| redrawAnnotations(); | |
| }; | |
| updateCanvasSize(); | |
| // Update canvas size when viewer resizes | |
| const resizeObserver = new ResizeObserver(updateCanvasSize); | |
| resizeObserver.observe(viewer.container); | |
| return () => resizeObserver.disconnect(); | |
| }, [viewer]); | |
| // Helper function to check if point is inside annotation | |
| const isPointInAnnotation = (point: { x: number; y: number }, annotation: Annotation): boolean => { | |
| const tolerance = 10; // pixels | |
| const screenPoints = annotation.points.map(toScreenPoint); | |
| if (annotation.type === "rectangle" && screenPoints.length === 2) { | |
| const [p1, p2] = screenPoints; | |
| const minX = Math.min(p1.x, p2.x); | |
| const maxX = Math.max(p1.x, p2.x); | |
| const minY = Math.min(p1.y, p2.y); | |
| const maxY = Math.max(p1.y, p2.y); | |
| return point.x >= minX - tolerance && point.x <= maxX + tolerance && | |
| point.y >= minY - tolerance && point.y <= maxY + tolerance; | |
| } else if (annotation.type === "ellipse" && screenPoints.length === 2) { | |
| const [p1, p2] = screenPoints; | |
| const cx = (p1.x + p2.x) / 2; | |
| const cy = (p1.y + p2.y) / 2; | |
| const rx = Math.max(1, Math.abs(p2.x - p1.x) / 2); | |
| const ry = Math.max(1, Math.abs(p2.y - p1.y) / 2); | |
| const dx = (point.x - cx) / rx; | |
| const dy = (point.y - cy) / ry; | |
| const dist = Math.abs(dx * dx + dy * dy - 1); | |
| const tol = tolerance / Math.max(rx, ry); | |
| return dist <= tol; | |
| } else if (annotation.type === "polygon" && screenPoints.length > 2) { | |
| // Check if point is near any line segment of the polygon | |
| const points = screenPoints; | |
| for (let i = 0; i < points.length; i++) { | |
| const p1 = points[i]; | |
| const p2 = points[(i + 1) % points.length]; | |
| const dist = distanceToLineSegment(point, p1, p2); | |
| if (dist < tolerance) return true; | |
| } | |
| // Also check if near any vertex | |
| return screenPoints.some(p => | |
| Math.hypot(p.x - point.x, p.y - point.y) < tolerance | |
| ); | |
| } else if (annotation.type === "brush" && screenPoints.length > 1) { | |
| for (let i = 0; i < screenPoints.length - 1; i++) { | |
| const dist = distanceToLineSegment(point, screenPoints[i], screenPoints[i + 1]); | |
| if (dist < tolerance) return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| // Helper function to calculate distance from point to line segment | |
| const distanceToLineSegment = ( | |
| point: { x: number; y: number }, | |
| p1: { x: number; y: number }, | |
| p2: { x: number; y: number } | |
| ): number => { | |
| const dx = p2.x - p1.x; | |
| const dy = p2.y - p1.y; | |
| const t = Math.max(0, Math.min(1, ((point.x - p1.x) * dx + (point.y - p1.y) * dy) / (dx * dx + dy * dy))); | |
| const closestX = p1.x + t * dx; | |
| const closestY = p1.y + t * dy; | |
| return Math.hypot(point.x - closestX, point.y - closestY); | |
| }; | |
| // Handle mouse events for drawing | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas || !viewer) return; | |
| const handleMouseMove = (e: MouseEvent) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const point = { x, y }; | |
| // Update hover state based on tool | |
| if (tool === "select") { | |
| const hoveredAnnotation = annotations.find(ann => isPointInAnnotation(point, ann)); | |
| setHoveredAnnotationId(hoveredAnnotation?.id || null); | |
| // Change cursor when hovering over annotation | |
| if (hoveredAnnotation) { | |
| canvas.style.cursor = "pointer"; | |
| } else { | |
| canvas.style.cursor = "default"; | |
| } | |
| } else { | |
| setHoveredAnnotationId(null); | |
| } | |
| // Handle select tool panning | |
| if (tool === "select" && isPanning && lastPanPoint && viewer) { | |
| const dx = x - lastPanPoint.x; | |
| const dy = y - lastPanPoint.y; | |
| const delta = viewer.viewport.deltaPointsFromPixels( | |
| new OpenSeadragon.Point(-dx, -dy), | |
| true | |
| ); | |
| viewer.viewport.panBy(delta); | |
| viewer.viewport.applyConstraints(); | |
| setLastPanPoint({ x, y }); | |
| return; | |
| } | |
| // Handle drawing preview for rectangle/ellipse tool | |
| if (isDrawing && (tool === "rectangle" || tool === "ellipse") && currentPoints.length > 0) { | |
| const imagePoint = toImagePoint(point); | |
| setCurrentPoints([currentPoints[0], imagePoint]); | |
| } | |
| if (isDrawing && tool === "brush" && currentPoints.length > 0) { | |
| const imagePoint = toImagePoint(point); | |
| const lastPoint = currentPoints[currentPoints.length - 1]; | |
| if (Math.hypot(imagePoint.x - lastPoint.x, imagePoint.y - lastPoint.y) > 0.5) { | |
| setCurrentPoints([...currentPoints, imagePoint]); | |
| } | |
| } | |
| }; | |
| const handleMouseDown = (e: MouseEvent) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const point = { x, y }; | |
| // Allow selection with Ctrl+Click on any annotation (works with any tool) | |
| if (e.ctrlKey || e.metaKey) { | |
| const selectedAnnotation = annotations.find(ann => isPointInAnnotation(point, ann)); | |
| if (onAnnotationSelected) { | |
| onAnnotationSelected(selectedAnnotation?.id || null); | |
| } | |
| return; | |
| } | |
| // Handle select tool - click to select annotation, drag to pan | |
| if (tool === "select") { | |
| const selectedAnnotation = annotations.find(ann => isPointInAnnotation(point, ann)); | |
| if (selectedAnnotation) { | |
| if (onAnnotationSelected) { | |
| onAnnotationSelected(selectedAnnotation.id); | |
| } | |
| return; | |
| } | |
| if (viewer) { | |
| setIsPanning(true); | |
| setLastPanPoint({ x, y }); | |
| } | |
| return; | |
| } | |
| // Handle drawing tools | |
| if (tool === "rectangle") { | |
| setIsDrawing(true); | |
| setDrawingTool("rectangle"); | |
| setCurrentPoints([toImagePoint(point)]); | |
| } else if (tool === "ellipse") { | |
| setIsDrawing(true); | |
| setDrawingTool("ellipse"); | |
| setCurrentPoints([toImagePoint(point)]); | |
| } else if (tool === "polygon") { | |
| // For polygon, add point on click | |
| const imagePoint = toImagePoint(point); | |
| const newPoints = [...currentPoints, imagePoint]; | |
| setCurrentPoints(newPoints); | |
| if (currentPoints.length === 0) { | |
| setIsDrawing(true); | |
| setDrawingTool("polygon"); | |
| } | |
| } else if (tool === "brush") { | |
| setIsDrawing(true); | |
| setDrawingTool("brush"); | |
| setCurrentPoints([toImagePoint(point)]); | |
| } | |
| }; | |
| const handleMouseUp = (e: MouseEvent) => { | |
| if (tool === "select" && isPanning) { | |
| setIsPanning(false); | |
| setLastPanPoint(null); | |
| return; | |
| } | |
| if (!isDrawing || (tool !== "rectangle" && tool !== "ellipse" && tool !== "brush")) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const imagePoint = toImagePoint({ x, y }); | |
| if (tool === "brush") { | |
| const newPoints = [...currentPoints, imagePoint]; | |
| setCurrentPoints(newPoints); | |
| completeAnnotation(undefined, newPoints); | |
| return; | |
| } | |
| setCurrentPoints([currentPoints[0], imagePoint]); | |
| completeAnnotation(); | |
| }; | |
| const handleDblClick = () => { | |
| if (tool === "polygon" && isDrawing && currentPoints.length >= 3) { | |
| completeAnnotation(); | |
| } | |
| }; | |
| const handleWheel = (e: WheelEvent) => { | |
| if (!viewer) return; | |
| e.preventDefault(); | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const zoomPoint = viewer.viewport.pointFromPixel( | |
| new OpenSeadragon.Point(x, y) | |
| ); | |
| const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; | |
| viewer.viewport.zoomBy(zoomFactor, zoomPoint); | |
| viewer.viewport.applyConstraints(); | |
| }; | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| if (e.key === "Escape" && isDrawing) { | |
| setIsDrawing(false); | |
| setCurrentPoints([]); | |
| } | |
| }; | |
| canvas.addEventListener("mousemove", handleMouseMove); | |
| canvas.addEventListener("mousedown", handleMouseDown); | |
| canvas.addEventListener("mouseup", handleMouseUp); | |
| canvas.addEventListener("dblclick", handleDblClick); | |
| canvas.addEventListener("wheel", handleWheel, { passive: false }); | |
| document.addEventListener("keydown", handleKeyDown); | |
| return () => { | |
| canvas.removeEventListener("mousemove", handleMouseMove); | |
| canvas.removeEventListener("mousedown", handleMouseDown); | |
| canvas.removeEventListener("mouseup", handleMouseUp); | |
| canvas.removeEventListener("dblclick", handleDblClick); | |
| canvas.removeEventListener("wheel", handleWheel); | |
| document.removeEventListener("keydown", handleKeyDown); | |
| }; | |
| }, [isDrawing, currentPoints, tool, viewer, annotations, selectedAnnotationId, onAnnotationSelected, hoveredAnnotationId, isPanning, lastPanPoint]); | |
| const completeAnnotation = ( | |
| forcedTool?: "rectangle" | "polygon" | "ellipse" | "brush", | |
| forcedPoints?: Array<{ x: number; y: number }> | |
| ) => { | |
| const points = forcedPoints ?? currentPoints; | |
| if (points.length < 2) { | |
| setCurrentPoints([]); | |
| setIsDrawing(false); | |
| setDrawingTool(null); | |
| return; | |
| } | |
| const finalTool = forcedTool ?? drawingTool ?? (tool as "rectangle" | "polygon" | "ellipse" | "brush"); | |
| const annotation: Annotation = { | |
| id: `annotation-${Date.now()}`, | |
| type: finalTool, | |
| points, | |
| color: labelColor(activeLabel), | |
| label: activeLabel, | |
| completed: true, | |
| }; | |
| onAnnotationComplete(annotation); | |
| setCurrentPoints([]); | |
| setIsDrawing(false); | |
| setDrawingTool(null); | |
| redrawAnnotations(); | |
| }; | |
| const redrawAnnotations = useCallback(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (!showAnnotations) return; | |
| // Draw completed annotations | |
| annotations.forEach((annotation) => { | |
| const isSelected = selectedAnnotationId === annotation.id; | |
| const isHovered = hoveredAnnotationId === annotation.id; | |
| const screenPoints = annotation.points.map(toScreenPoint); | |
| if (annotation.type === "rectangle" && screenPoints.length === 2) { | |
| const [p1, p2] = screenPoints; | |
| const x = Math.min(p1.x, p2.x); | |
| const y = Math.min(p1.y, p2.y); | |
| const width = Math.abs(p2.x - p1.x); | |
| const height = Math.abs(p2.y - p1.y); | |
| // Draw only outline, no fill | |
| if (isSelected) { | |
| ctx.strokeStyle = "#FCD34D"; | |
| ctx.lineWidth = 4; | |
| } else if (isHovered) { | |
| ctx.strokeStyle = "#60A5FA"; // Light blue for hover | |
| ctx.lineWidth = 3; | |
| } else { | |
| ctx.strokeStyle = annotation.color; | |
| ctx.lineWidth = 2; | |
| } | |
| ctx.strokeRect(x, y, width, height); | |
| // Add hover indicator | |
| if (isHovered) { | |
| // Add hover indicator | |
| ctx.fillStyle = "#60A5FA"; | |
| ctx.fillRect(x, y - 22, 65, 18); | |
| ctx.fillStyle = "#FFF"; | |
| ctx.font = "11px Arial"; | |
| ctx.textAlign = "left"; | |
| ctx.textBaseline = "top"; | |
| ctx.fillText("Click to select", x + 4, y - 18); | |
| } | |
| } else if (annotation.type === "ellipse" && screenPoints.length === 2) { | |
| const [p1, p2] = screenPoints; | |
| const cx = (p1.x + p2.x) / 2; | |
| const cy = (p1.y + p2.y) / 2; | |
| const rx = Math.abs(p2.x - p1.x) / 2; | |
| const ry = Math.abs(p2.y - p1.y) / 2; | |
| if (isSelected) { | |
| ctx.strokeStyle = "#FCD34D"; | |
| ctx.lineWidth = 4; | |
| } else if (isHovered) { | |
| ctx.strokeStyle = "#60A5FA"; | |
| ctx.lineWidth = 3; | |
| } else { | |
| ctx.strokeStyle = annotation.color; | |
| ctx.lineWidth = 2; | |
| } | |
| ctx.beginPath(); | |
| ctx.ellipse(cx, cy, Math.max(1, rx), Math.max(1, ry), 0, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| if (isHovered) { | |
| ctx.fillStyle = "#60A5FA"; | |
| ctx.fillRect(cx - 35, cy - ry - 26, 70, 18); | |
| ctx.fillStyle = "#FFF"; | |
| ctx.font = "11px Arial"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "top"; | |
| ctx.fillText("Click to select", cx, cy - ry - 22); | |
| } | |
| } else if (annotation.type === "polygon" && screenPoints.length > 1) { | |
| // Determine colors based on state | |
| let strokeColor, pointColor; | |
| let lineWidth = 2; | |
| if (isSelected) { | |
| strokeColor = "#FCD34D"; | |
| pointColor = "#FCD34D"; | |
| lineWidth = 4; | |
| } else if (isHovered) { | |
| strokeColor = "#60A5FA"; | |
| pointColor = "#60A5FA"; | |
| lineWidth = 3; | |
| } else { | |
| strokeColor = annotation.color; | |
| pointColor = annotation.color; | |
| } | |
| ctx.strokeStyle = strokeColor; | |
| ctx.lineWidth = lineWidth; | |
| // Draw main polygon | |
| ctx.strokeStyle = strokeColor; | |
| ctx.beginPath(); | |
| ctx.moveTo(screenPoints[0].x, screenPoints[0].y); | |
| screenPoints.slice(1).forEach((p) => { | |
| ctx.lineTo(p.x, p.y); | |
| }); | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| // Draw points as circles | |
| ctx.fillStyle = pointColor; | |
| screenPoints.forEach((p, index) => { | |
| ctx.beginPath(); | |
| const radius = isSelected ? 7 : isHovered ? 6 : 5; | |
| ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw point numbers | |
| ctx.fillStyle = "#FFFFFF"; | |
| ctx.font = "bold 11px Arial"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| ctx.fillText((index + 1).toString(), p.x, p.y); | |
| ctx.fillStyle = pointColor; | |
| }); | |
| // Add hover indicator | |
| if (isHovered && screenPoints.length > 0) { | |
| const firstPoint = screenPoints[0]; | |
| ctx.fillStyle = "#60A5FA"; | |
| ctx.fillRect(firstPoint.x + 10, firstPoint.y - 22, 65, 18); | |
| ctx.fillStyle = "#FFF"; | |
| ctx.font = "11px Arial"; | |
| ctx.textAlign = "left"; | |
| ctx.textBaseline = "top"; | |
| ctx.fillText("Click to select", firstPoint.x + 14, firstPoint.y - 18); | |
| } | |
| } else if (annotation.type === "brush" && screenPoints.length > 1) { | |
| if (isSelected) { | |
| ctx.strokeStyle = "#FCD34D"; | |
| ctx.lineWidth = 4; | |
| } else if (isHovered) { | |
| ctx.strokeStyle = "#60A5FA"; | |
| ctx.lineWidth = 3; | |
| } else { | |
| ctx.strokeStyle = annotation.color; | |
| ctx.lineWidth = 2; | |
| } | |
| ctx.beginPath(); | |
| ctx.moveTo(screenPoints[0].x, screenPoints[0].y); | |
| screenPoints.slice(1).forEach((p) => { | |
| ctx.lineTo(p.x, p.y); | |
| }); | |
| ctx.stroke(); | |
| if (isHovered) { | |
| const firstPoint = screenPoints[0]; | |
| ctx.fillStyle = "#60A5FA"; | |
| ctx.fillRect(firstPoint.x + 10, firstPoint.y - 22, 65, 18); | |
| ctx.fillStyle = "#FFF"; | |
| ctx.font = "11px Arial"; | |
| ctx.textAlign = "left"; | |
| ctx.textBaseline = "top"; | |
| ctx.fillText("Click to select", firstPoint.x + 14, firstPoint.y - 18); | |
| } | |
| } | |
| }); | |
| // Draw current drawing (preview) | |
| if (isDrawing && currentPoints.length > 0) { | |
| const color = tool === "rectangle" ? "#EF4444" : "#3B82F6"; | |
| const previewPoints = currentPoints.map(toScreenPoint); | |
| if ((tool === "rectangle" || tool === "ellipse") && previewPoints.length === 2) { | |
| const [p1, p2] = previewPoints; | |
| const x = Math.min(p1.x, p2.x); | |
| const y = Math.min(p1.y, p2.y); | |
| const width = Math.abs(p2.x - p1.x); | |
| const height = Math.abs(p2.y - p1.y); | |
| // Draw only outline with dashed style, no fill | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| if (tool === "rectangle") { | |
| ctx.strokeRect(x, y, width, height); | |
| } else { | |
| const cx = x + width / 2; | |
| const cy = y + height / 2; | |
| ctx.beginPath(); | |
| ctx.ellipse(cx, cy, Math.max(1, width / 2), Math.max(1, height / 2), 0, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| } | |
| ctx.setLineDash([]); | |
| } else if (tool === "polygon" && previewPoints.length > 1) { | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| ctx.beginPath(); | |
| ctx.moveTo(previewPoints[0].x, previewPoints[0].y); | |
| previewPoints.slice(1).forEach((p) => { | |
| ctx.lineTo(p.x, p.y); | |
| }); | |
| // Close the path only if we have 3+ points | |
| if (previewPoints.length >= 3) { | |
| ctx.closePath(); | |
| } | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Draw points as solid circles with numbers | |
| ctx.fillStyle = color; | |
| previewPoints.forEach((p, index) => { | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw point numbers | |
| ctx.fillStyle = "#FFFFFF"; | |
| ctx.font = "bold 11px Arial"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| ctx.fillText((index + 1).toString(), p.x, p.y); | |
| ctx.fillStyle = color; | |
| }); | |
| } else if (tool === "brush" && previewPoints.length > 1) { | |
| ctx.strokeStyle = "#EF4444"; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| ctx.beginPath(); | |
| ctx.moveTo(previewPoints[0].x, previewPoints[0].y); | |
| previewPoints.slice(1).forEach((p) => { | |
| ctx.lineTo(p.x, p.y); | |
| }); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| } | |
| }, [annotations, currentPoints, hoveredAnnotationId, isDrawing, selectedAnnotationId, showAnnotations, tool, viewer]); | |
| useEffect(() => { | |
| redrawAnnotations(); | |
| }, [redrawAnnotations]); | |
| useEffect(() => { | |
| if (!isDrawing || !drawingTool) return; | |
| if (tool === drawingTool) return; | |
| if (drawingTool === "polygon" && currentPoints.length >= 3) { | |
| completeAnnotation(drawingTool); | |
| return; | |
| } | |
| if ((drawingTool === "rectangle" || drawingTool === "ellipse") && currentPoints.length === 2) { | |
| completeAnnotation(drawingTool); | |
| return; | |
| } | |
| if (drawingTool === "brush" && currentPoints.length >= 2) { | |
| completeAnnotation(drawingTool); | |
| return; | |
| } | |
| setIsDrawing(false); | |
| setCurrentPoints([]); | |
| setDrawingTool(null); | |
| }, [tool, drawingTool, currentPoints, isDrawing]); | |
| useEffect(() => { | |
| if (!viewer) return; | |
| const handleViewportChange = () => redrawAnnotations(); | |
| viewer.addHandler("zoom", handleViewportChange); | |
| viewer.addHandler("pan", handleViewportChange); | |
| viewer.addHandler("animation", handleViewportChange); | |
| viewer.addHandler("open", handleViewportChange); | |
| return () => { | |
| viewer.removeHandler("zoom", handleViewportChange); | |
| viewer.removeHandler("pan", handleViewportChange); | |
| viewer.removeHandler("animation", handleViewportChange); | |
| viewer.removeHandler("open", handleViewportChange); | |
| }; | |
| }, [redrawAnnotations, viewer]); | |
| return ( | |
| <canvas | |
| ref={canvasRef} | |
| className={`absolute inset-0 z-40 ${ | |
| tool === "none" ? "pointer-events-none cursor-grab" : "" | |
| } ${tool === "select" ? "cursor-default" : ""} ${ | |
| tool === "rectangle" || tool === "polygon" || tool === "ellipse" || tool === "brush" | |
| ? "cursor-crosshair" | |
| : "" | |
| }`} | |
| style={{ width: "100%", height: "100%" }} | |
| /> | |
| ); | |
| } | |