import { useCallback, useEffect, useRef, useState } from 'react'; import type { AnnotationAPI, AnnotationToolId } from '@app/components/viewer/viewerTypes'; interface UseAnnotationSelectionParams { annotationApiRef: React.RefObject; deriveToolFromAnnotation: (annotation: any) => AnnotationToolId | undefined; activeToolRef: React.MutableRefObject; manualToolSwitch: React.MutableRefObject; setActiveTool: (toolId: AnnotationToolId) => void; setSelectedTextDraft: (text: string) => void; setSelectedFontSize: (size: number) => void; setInkWidth: (value: number) => void; setFreehandHighlighterWidth?: (value: number) => void; setShapeThickness: (value: number) => void; setTextColor: (value: string) => void; setTextBackgroundColor: (value: string) => void; setNoteBackgroundColor: (value: string) => void; setInkColor: (value: string) => void; setHighlightColor: (value: string) => void; setHighlightOpacity: (value: number) => void; setUnderlineColor: (value: string) => void; setUnderlineOpacity: (value: number) => void; setStrikeoutColor: (value: string) => void; setStrikeoutOpacity: (value: number) => void; setSquigglyColor: (value: string) => void; setSquigglyOpacity: (value: number) => void; setShapeStrokeColor: (value: string) => void; setShapeFillColor: (value: string) => void; setShapeOpacity: (value: number) => void; setShapeStrokeOpacity: (value: number) => void; setShapeFillOpacity: (value: number) => void; setTextAlignment: (value: 'left' | 'center' | 'right') => void; } const MARKUP_TOOL_IDS = ['highlight', 'underline', 'strikeout', 'squiggly'] as const; const DRAWING_TOOL_IDS = ['ink', 'inkHighlighter'] as const; const isTextMarkupAnnotation = (annotation: any): boolean => { const toolId = annotation?.customData?.annotationToolId || annotation?.customData?.toolId || annotation?.object?.customData?.annotationToolId || annotation?.object?.customData?.toolId; if (toolId && MARKUP_TOOL_IDS.includes(toolId)) return true; const type = annotation?.type ?? annotation?.object?.type; if (typeof type === 'number' && [9, 10, 11, 12].includes(type)) return true; const subtype = annotation?.subtype ?? annotation?.object?.subtype; if (typeof subtype === 'string') { const lower = subtype.toLowerCase(); if (MARKUP_TOOL_IDS.some((t) => lower.includes(t))) return true; } return false; }; const shouldStayOnPlacementTool = (annotation: any, derivedTool?: string | null | undefined): boolean => { const toolId = derivedTool || annotation?.customData?.annotationToolId || annotation?.customData?.toolId || annotation?.object?.customData?.annotationToolId || annotation?.object?.customData?.toolId; if (toolId && (MARKUP_TOOL_IDS.includes(toolId as any) || DRAWING_TOOL_IDS.includes(toolId as any))) { return true; } const type = annotation?.type ?? annotation?.object?.type; if (typeof type === 'number' && type === 15) return true; // ink family if (isTextMarkupAnnotation(annotation)) return true; return false; }; export function useAnnotationSelection({ annotationApiRef, deriveToolFromAnnotation, activeToolRef, manualToolSwitch, setActiveTool, setSelectedTextDraft, setSelectedFontSize, setInkWidth, setShapeThickness, setTextColor, setTextBackgroundColor, setNoteBackgroundColor, setInkColor, setHighlightColor, setHighlightOpacity, setUnderlineColor, setUnderlineOpacity, setStrikeoutColor, setStrikeoutOpacity, setSquigglyColor, setSquigglyOpacity, setShapeStrokeColor, setShapeFillColor, setShapeOpacity, setShapeStrokeOpacity, setShapeFillOpacity, setTextAlignment, setFreehandHighlighterWidth, }: UseAnnotationSelectionParams) { const [selectedAnn, setSelectedAnn] = useState(null); const [selectedAnnId, setSelectedAnnId] = useState(null); const selectedAnnIdRef = useRef(null); const applySelectionFromAnnotation = useCallback( (ann: any | null) => { const annObject = ann?.object ?? ann ?? null; const annId = annObject?.id ?? null; const type = annObject?.type; const derivedTool = annObject ? deriveToolFromAnnotation(annObject) : undefined; selectedAnnIdRef.current = annId; setSelectedAnnId(annId); // Normalize selected annotation to always expose .object for edit panels const normalizedSelection = ann?.object ? ann : annObject ? { object: annObject } : null; setSelectedAnn(normalizedSelection); if (annObject?.contents !== undefined) { setSelectedTextDraft(annObject.contents ?? ''); } if (annObject?.fontSize !== undefined) { setSelectedFontSize(annObject.fontSize ?? 14); } if (annObject?.textAlign !== undefined) { const align = annObject.textAlign; if (typeof align === 'string') { const normalized = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left'; setTextAlignment(normalized); } else if (typeof align === 'number') { const normalized = align === 1 ? 'center' : align === 2 ? 'right' : 'left'; setTextAlignment(normalized); } } if (type === 3) { const background = (annObject?.backgroundColor as string | undefined) || (annObject?.fillColor as string | undefined) || undefined; const textColor = (annObject?.textColor as string | undefined) || (annObject?.color as string | undefined); if (textColor) { setTextColor(textColor); } if (derivedTool === 'note') { setNoteBackgroundColor(background || ''); } else { setTextBackgroundColor(background || ''); } } if (type === 15) { const width = annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth ?? annObject?.thickness; if (derivedTool === 'inkHighlighter') { if (annObject?.color) setHighlightColor(annObject.color); if (annObject?.opacity !== undefined) { setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100)); } if (width !== undefined && setFreehandHighlighterWidth) { setFreehandHighlighterWidth(width); } } else { if (width !== undefined) setInkWidth(width ?? 2); if (annObject?.color) { setInkColor(annObject.color); } } } else if (type >= 4 && type <= 8) { const width = annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth; if (width !== undefined) { setShapeThickness(width ?? 1); } } if (type === 9) { if (annObject?.color) setHighlightColor(annObject.color); if (annObject?.opacity !== undefined) setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100)); } else if (type === 10) { if (annObject?.color) setUnderlineColor(annObject.color); if (annObject?.opacity !== undefined) setUnderlineOpacity(Math.round((annObject.opacity ?? 1) * 100)); } else if (type === 12) { if (annObject?.color) setStrikeoutColor(annObject.color); if (annObject?.opacity !== undefined) setStrikeoutOpacity(Math.round((annObject.opacity ?? 1) * 100)); } else if (type === 11) { if (annObject?.color) setSquigglyColor(annObject.color); if (annObject?.opacity !== undefined) setSquigglyOpacity(Math.round((annObject.opacity ?? 1) * 100)); } if ([4, 5, 6, 7, 8].includes(type)) { const stroke = (annObject?.strokeColor as string | undefined) ?? (annObject?.color as string | undefined); if (stroke) setShapeStrokeColor(stroke); if ([5, 6, 7].includes(type)) { const fill = (annObject?.color as string | undefined) ?? (annObject?.fillColor as string | undefined); if (fill) setShapeFillColor(fill); } const opacity = annObject?.opacity !== undefined ? Math.round((annObject.opacity ?? 1) * 100) : undefined; const strokeOpacityValue = annObject?.strokeOpacity !== undefined ? Math.round((annObject.strokeOpacity ?? 1) * 100) : undefined; const fillOpacityValue = annObject?.fillOpacity !== undefined ? Math.round((annObject.fillOpacity ?? 1) * 100) : undefined; if (opacity !== undefined) { setShapeOpacity(opacity); setShapeStrokeOpacity(strokeOpacityValue ?? opacity); setShapeFillOpacity(fillOpacityValue ?? opacity); } else { if (strokeOpacityValue !== undefined) setShapeStrokeOpacity(strokeOpacityValue); if (fillOpacityValue !== undefined) setShapeFillOpacity(fillOpacityValue); } } const matchingTool = derivedTool; const stayOnPlacement = shouldStayOnPlacementTool(annObject, matchingTool); if (matchingTool && activeToolRef.current !== 'select' && !stayOnPlacement) { activeToolRef.current = 'select'; setActiveTool('select'); // Immediately enable select tool to avoid re-entering placement after creation. annotationApiRef.current?.activateAnnotationTool?.('select'); } else if (activeToolRef.current === 'select') { // Keep the viewer in Select mode so clicking existing annotations does not re-enable placement. annotationApiRef.current?.activateAnnotationTool?.('select'); } }, [ activeToolRef, deriveToolFromAnnotation, manualToolSwitch, setActiveTool, setInkWidth, setNoteBackgroundColor, setSelectedFontSize, setSelectedTextDraft, setShapeThickness, setTextBackgroundColor, setTextColor, setInkColor, setHighlightColor, setHighlightOpacity, setUnderlineColor, setUnderlineOpacity, setStrikeoutColor, setStrikeoutOpacity, setSquigglyColor, setSquigglyOpacity, setShapeStrokeColor, setShapeFillColor, setShapeOpacity, setShapeStrokeOpacity, setShapeFillOpacity, setTextAlignment, setFreehandHighlighterWidth, shouldStayOnPlacementTool, ] ); useEffect(() => { const api = annotationApiRef.current as any; if (!api) return; const checkSelection = () => { let ann: any = null; if (typeof api.getSelectedAnnotation === 'function') { try { ann = api.getSelectedAnnotation(); } catch (error) { // Some builds of the annotation plugin can throw when reading // internal selection state (e.g., accessing `selectedUid` on // an undefined object). Treat this as "no current selection" // instead of crashing the annotations tool. console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error); ann = null; } } const currentId = ann?.object?.id ?? ann?.id ?? null; if (currentId !== selectedAnnIdRef.current) { applySelectionFromAnnotation(ann ?? null); } }; let interval: ReturnType | null = null; if (typeof api.onAnnotationEvent === 'function') { const handler = (event: any) => { const ann = event?.annotation ?? event?.selectedAnnotation ?? null; const eventType = event?.type; switch (eventType) { case 'create': case 'add': case 'added': case 'created': case 'annotationCreated': case 'annotationAdded': case 'complete': { const eventAnn = ann ?? api.getSelectedAnnotation?.(); applySelectionFromAnnotation(eventAnn); const currentTool = activeToolRef.current; const tool = deriveToolFromAnnotation((eventAnn as any)?.object ?? eventAnn ?? api.getSelectedAnnotation?.()) || currentTool; const stayOnPlacement = shouldStayOnPlacementTool(eventAnn, tool) || (tool ? DRAWING_TOOL_IDS.includes(tool as any) : false); if (activeToolRef.current !== 'select' && !stayOnPlacement) { activeToolRef.current = 'select'; setActiveTool('select'); annotationApiRef.current?.activateAnnotationTool?.('select'); } // Re-read selection after the viewer updates to ensure we have the full annotation object for the edit panel. setTimeout(() => { const selected = api.getSelectedAnnotation?.(); applySelectionFromAnnotation(selected ?? eventAnn ?? null); const derivedAfter = deriveToolFromAnnotation((selected as any)?.object ?? selected ?? eventAnn ?? null) || activeToolRef.current; const stayOnPlacementAfter = shouldStayOnPlacementTool(selected ?? eventAnn ?? null, derivedAfter) || (derivedAfter ? DRAWING_TOOL_IDS.includes(derivedAfter as any) : false); if (activeToolRef.current !== 'select' && !stayOnPlacementAfter) { activeToolRef.current = 'select'; setActiveTool('select'); annotationApiRef.current?.activateAnnotationTool?.('select'); } }, 50); break; } case 'select': case 'selected': case 'annotationSelected': case 'annotationClicked': case 'annotationTapped': applySelectionFromAnnotation(ann ?? api.getSelectedAnnotation?.()); break; case 'deselect': case 'clearSelection': applySelectionFromAnnotation(null); break; case 'delete': case 'remove': if (ann?.id && ann.id === selectedAnnIdRef.current) { applySelectionFromAnnotation(null); } break; case 'update': case 'change': if (selectedAnnIdRef.current) { const current = api.getSelectedAnnotation?.(); if (current) { applySelectionFromAnnotation(current); } } break; default: break; } }; const unsubscribe = api.onAnnotationEvent(handler); interval = setInterval(checkSelection, 450); return () => { if (typeof unsubscribe === 'function') { unsubscribe(); } if (interval) clearInterval(interval); }; } interval = setInterval(checkSelection, 350); return () => { if (interval) clearInterval(interval); }; }, [annotationApiRef, applySelectionFromAnnotation]); return { selectedAnn, selectedAnnId, selectedAnnIdRef, setSelectedAnn, setSelectedAnnId, applySelectionFromAnnotation, }; }