diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index ef69a6a..514ed09 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -398,42 +398,42 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps) const [metaModalOpen, setMetaModalOpen] = useState(false); const [metaModalContent, setMetaModalContent] = useState(null); -const handleSelectAnnotation = (viewportIdx: number, uid: string | null) => { - // Use the official selection API for Cornerstone3D Tools - if (cornerstoneTools?.annotation?.selection?.setAnnotationSelected) { - cornerstoneTools.annotation.selection.setAnnotationSelected(uid); - } + const handleSelectAnnotation = (viewportIdx: number, uid: string | null) => { + // Use the official selection API for Cornerstone3D Tools + if (cornerstoneTools?.annotation?.selection?.setAnnotationSelected) { + cornerstoneTools.annotation.selection.setAnnotationSelected(uid); + } - // Optionally, update your own UI state - setSelectedAnnUIDs(prev => { - const next = [...prev]; - next[viewportIdx] = uid; - return next; - }); -}; - -const handleDeleteAnnotation = (viewportIdx: number, uid: string, currentAnnotations: any[]) => { - const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || []; - allAnnotations.forEach(ann => { - if (ann.annotationUID === uid) ann.isSelected = false; - }); - cornerstoneTools.annotation.state.removeAnnotation(uid); - renderingEngineRef.current.render(); - //} - // Auto-select another annotation if available - const remaining = currentAnnotations.filter(ann => ann.annotationUID !== uid); - setSelectedAnnUIDs(prev => { - const next = [...prev]; - const newSelected = remaining.length > 0 ? remaining[0].annotationUID : null; - next[viewportIdx] = newSelected; - allAnnotations.forEach(ann => { - ann.isSelected = ann.annotationUID === newSelected; + // Optionally, update your own UI state + setSelectedAnnUIDs(prev => { + const next = [...prev]; + next[viewportIdx] = uid; + return next; }); - cornerstoneTools.annotation.selection.setAnnotationSelected(newSelected); + }; + + const handleDeleteAnnotation = (viewportIdx: number, uid: string, currentAnnotations: any[]) => { + const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || []; + allAnnotations.forEach(ann => { + if (ann.annotationUID === uid) ann.isSelected = false; + }); + cornerstoneTools.annotation.state.removeAnnotation(uid); renderingEngineRef.current.render(); - return next; - }); -}; + //} + // Auto-select another annotation if available + const remaining = currentAnnotations.filter(ann => ann.annotationUID !== uid); + setSelectedAnnUIDs(prev => { + const next = [...prev]; + const newSelected = remaining.length > 0 ? remaining[0].annotationUID : null; + next[viewportIdx] = newSelected; + allAnnotations.forEach(ann => { + ann.isSelected = ann.annotationUID === newSelected; + }); + cornerstoneTools.annotation.selection.setAnnotationSelected(newSelected); + renderingEngineRef.current.render(); + return next; + }); + }; // Add this function to gather and format metadata: const handleShowMetadata = () => { @@ -510,27 +510,27 @@ const handleDeleteAnnotation = (viewportIdx: number, uid: string, currentAnnotat const [altPressed, setAltPressed] = useState(false); -useEffect(() => { - const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); - if (!mainToolGroup) return; + useEffect(() => { + const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); + if (!mainToolGroup) return; - // List all annotation tools you use - [ - 'ArrowAnnotate', - 'NoLabelArrowAnnotate', - 'Length', - 'RectangleROI', - 'EllipticalROI', - 'PlanarFreehandROI', - 'PlanarFreehandContourSegmentation', - // Add more if needed - ].forEach(toolName => { - mainToolGroup.setToolConfiguration(toolName, { - color: '#00e676', // normal color - selectedColor: '#ffeb3b', // yellow for selected + // List all annotation tools you use + [ + 'ArrowAnnotate', + 'NoLabelArrowAnnotate', + 'Length', + 'RectangleROI', + 'EllipticalROI', + 'PlanarFreehandROI', + 'PlanarFreehandContourSegmentation', + // Add more if needed + ].forEach(toolName => { + mainToolGroup.setToolConfiguration(toolName, { + color: '#00e676', // normal color + selectedColor: '#ffeb3b', // yellow for selected + }); }); - }); -}, [MAIN_TOOL_GROUP_ID]); + }, [MAIN_TOOL_GROUP_ID]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -1161,14 +1161,17 @@ useEffect(() => { const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); if (!viewport || typeof viewport.getImageIds !== "function") return; const imageIds = viewport.getImageIds(); - const idx = imageIds.indexOf(imageId); + // Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison + const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, "")); + const normTarget = imageId.replace(/^wadouri:/, ""); + const idx = normImageIds.indexOf(normTarget); if (idx !== -1 && typeof viewport.setImageIdIndex === "function") { csUtilities.jumpToSlice(viewport.element, { imageIndex: idx }); updateOverlay(viewportIdx, viewport); } } - // Attach to window with a unique name - window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId; + // Attach to window with a unique name + window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId; // Export the current viewer state as JSON @@ -1351,6 +1354,11 @@ useEffect(() => { }; window[`importLegacyAnnotations_${apiKey}`] = (json: string) => { + // Remove all existing annotations + if (cornerstoneTools.annotation.state.removeAllAnnotations) { + cornerstoneTools.annotation.state.removeAllAnnotations(); + } + let legacyData: any; try { legacyData = typeof json === "string" ? JSON.parse(json) : json; @@ -1359,17 +1367,13 @@ useEffect(() => { return; } - // Remove all existing annotations - if (cornerstoneTools.annotation.state.removeAllAnnotations) { - cornerstoneTools.annotation.state.removeAllAnnotations(); - } - const toolNameMap: Record = { ArrowAnnotate: "ArrowAnnotate", // Add more mappings if needed }; Object.entries(legacyData).forEach(([imageId, tools]) => { + console.log("Importing legacy annotations for imageId:", imageId); Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => { const cs3Tool = toolNameMap[legacyTool] || legacyTool; if (toolData.data && Array.isArray(toolData.data)) { @@ -1410,8 +1414,11 @@ useEffect(() => { } - - + // --- Add rotation if present in legacy annotation --- + // Does this actually exist? + if (typeof ann.rotation === "number") { + metadata.rotation = ann.rotation; + } const annotation = { annotationUID: ann.uuid, // || cornerstoneTools.utilities.uuidv4(), @@ -1471,7 +1478,6 @@ useEffect(() => { } if (typeof legacy.invert === "boolean") props.invert = legacy.invert; if (typeof legacy.pixelReplication === "boolean") props.interpolationType = legacy.pixelReplication ? 0 : 1; - if (typeof legacy.rotation === "number") props.rotation = legacy.rotation; if (typeof legacy.hflip === "boolean") props.hflip = legacy.hflip; if (typeof legacy.vflip === "boolean") props.vflip = legacy.vflip; @@ -1493,6 +1499,10 @@ useEffect(() => { viewport.setCamera(camera); } } + // --- Fix: Import rotation from legacy viewport --- + if (typeof legacy.rotation === "number") { + viewport.setRotation(legacy.rotation); + } viewport.render(); updateOverlay(activeViewport ?? 0, viewport); @@ -1732,7 +1742,8 @@ useEffect(() => { ann => ann.metadata && ann.metadata.referencedImageId && - imageIds.includes(ann.metadata.referencedImageId) + imageIds.includes(ann.metadata.referencedImageId) && + ann.metadata.toolName !== ReferenceCursors.toolName // Exclude ReferenceCursors ); if (!stackAnnotations.length) return null; // Find the annotation(s) on the current image @@ -1786,23 +1797,23 @@ useEffect(() => { }} title="Previous Annotation" aria-label="Previous Annotation" -onClick={() => { - const renderingEngine = renderingEngineRef.current; - const viewport = renderingEngine?.getViewport(`CT_${i}`); - if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; - const currentIdx = viewport.getCurrentImageIdIndex(); - const prevIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, -1); - if (prevIdx !== null && typeof viewport.setImageIdIndex === "function") { - csUtilities.jumpToSlice(viewport.element, { imageIndex: prevIdx }); - updateOverlay(i, viewport); - // Select the first annotation on the new image - const newImageId = imageIds[prevIdx]; - const annsOnImage = stackAnnotations.filter( - ann => ann.metadata.referencedImageId === newImageId - ); - handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); - } -}} + onClick={() => { + const renderingEngine = renderingEngineRef.current; + const viewport = renderingEngine?.getViewport(`CT_${i}`); + if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; + const currentIdx = viewport.getCurrentImageIdIndex(); + const prevIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, -1); + if (prevIdx !== null && typeof viewport.setImageIdIndex === "function") { + csUtilities.jumpToSlice(viewport.element, { imageIndex: prevIdx }); + updateOverlay(i, viewport); + // Select the first annotation on the new image + const newImageId = imageIds[prevIdx]; + const annsOnImage = stackAnnotations.filter( + ann => ann.metadata.referencedImageId === newImageId + ); + handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); + } + }} > @@ -1826,23 +1837,23 @@ onClick={() => { }} title="Next Annotation" aria-label="Next Annotation" -onClick={() => { - const renderingEngine = renderingEngineRef.current; - const viewport = renderingEngine?.getViewport(`CT_${i}`); - if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; - const currentIdx = viewport.getCurrentImageIdIndex(); - const nextIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, 1); - if (nextIdx !== null && typeof viewport.setImageIdIndex === "function") { - csUtilities.jumpToSlice(viewport.element, { imageIndex: nextIdx }); - updateOverlay(i, viewport); - // Select the first annotation on the new image - const newImageId = imageIds[nextIdx]; - const annsOnImage = stackAnnotations.filter( - ann => ann.metadata.referencedImageId === newImageId - ); - handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); - } -}} + onClick={() => { + const renderingEngine = renderingEngineRef.current; + const viewport = renderingEngine?.getViewport(`CT_${i}`); + if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; + const currentIdx = viewport.getCurrentImageIdIndex(); + const nextIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, 1); + if (nextIdx !== null && typeof viewport.setImageIdIndex === "function") { + csUtilities.jumpToSlice(viewport.element, { imageIndex: nextIdx }); + updateOverlay(i, viewport); + // Select the first annotation on the new image + const newImageId = imageIds[nextIdx]; + const annsOnImage = stackAnnotations.filter( + ann => ann.metadata.referencedImageId === newImageId + ); + handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); + } + }} > @@ -1850,26 +1861,26 @@ onClick={() => { {currentAnnotations.length > 0 && (
{currentAnnotations.map(ann => ( -
handleSelectAnnotation(i, ann.annotationUID)} -> - - {selectedAnnUID === ann.annotationUID ? "★ " : ""} - {ann.metadata.toolName || "Annotation"} - +
handleSelectAnnotation(i, ann.annotationUID)} + > + + {selectedAnnUID === ann.annotationUID ? "★ " : ""} + {ann.metadata.toolName || "Annotation"} +
)} - {isVisible && ( + {isVisible && availableStacks.length > 1 && (
{ - const id = container.id || ''; - const imageStacks = () => availableImageIds(); - createRoot(container).render( - - ); -}); +// Helper to convert URLs to wadouri +const toWadouri = (arr: string[]) => + arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`); -const containers = document.querySelectorAll(".dicom-viewer-root"); -containers.forEach((container) => { - const id = container.id || ''; - let imageStacks; +export function mountDicomViewers() { + // Test containers + const test_containers = document.querySelectorAll(".dicom-viewer-test-root"); + test_containers.forEach((container) => { + const id = container.id || ''; + const imageStacks = () => availableImageIds(); + createRoot(container).render( + + ); + }); - // Helper to convert URLs to wadouri - const toWadouri = (arr: string[]) => - arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`); + // Main containers + const containers = document.querySelectorAll(".dicom-viewer-root"); + containers.forEach((container) => { + const id = container.id || ''; + let imageStacks; - // Check for data-images attribute - const dataImages = container.getAttribute('data-images'); - if (dataImages) { - let parsed; - try { - parsed = JSON.parse(dataImages); - } catch (e) { - console.error("Invalid data-images JSON:", e); - parsed = []; - } - // If it's a flat list, wrap in an array to make it a stack list and convert to wadouri - if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { - imageStacks = () => Promise.resolve([toWadouri(parsed)]); - } else if (Array.isArray(parsed)) { - imageStacks = () => Promise.resolve(parsed.map( - stack => Array.isArray(stack) ? toWadouri(stack) : [] - )); + const dataImages = container.getAttribute('data-images'); + if (dataImages) { + let parsed; + try { + parsed = JSON.parse(dataImages); + } catch (e) { + console.error("Invalid data-images JSON:", e); + parsed = []; + } + if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { + imageStacks = () => Promise.resolve([toWadouri(parsed)]); + } else if (Array.isArray(parsed)) { + imageStacks = () => Promise.resolve(parsed.map( + stack => Array.isArray(stack) ? toWadouri(stack) : [] + )); + } else { + imageStacks = () => Promise.resolve([[]]); + } } else { imageStacks = () => Promise.resolve([[]]); } - } else { - imageStacks = () => Promise.resolve([[]]); - } - // Read autoCacheStack from data-auto-cache-stack attribute - const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack'); - const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1"; + const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack'); + const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1"; - createRoot(container).render( - - ); -}); \ No newline at end of file + createRoot(container).render( + + ); + }); +} + +// Optionally, call it immediately for static containers: +mountDicomViewers(); + +// Optionally, expose globally for dynamic use: +(window as any).mountDicomViewers = mountDicomViewers; \ No newline at end of file