fix annotation selection

This commit is contained in:
Ross
2025-06-03 21:39:07 +01:00
parent cdbe31df6b
commit b1db098a49
+159 -6
View File
@@ -397,6 +397,49 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
const [metaModalOpen, setMetaModalOpen] = useState(false); const [metaModalOpen, setMetaModalOpen] = useState(false);
const [metaModalContent, setMetaModalContent] = useState<any>(null); const [metaModalContent, setMetaModalContent] = useState<any>(null);
const handleSelectAnnotation = (viewportIdx: number, uid: string | null) => {
// Use the official selection API for Cornerstone3D Tools
if (window.cornerstone3dTools?.annotation?.selection?.setAnnotationSelected) {
window.cornerstone3dTools.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);
if (cornerstoneTools.annotation.state.triggerAnnotationRender) {
cornerstoneTools.annotation.state.triggerAnnotationRender();
} else if (renderingEngineRef.current) {
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;
});
if (cornerstoneTools.annotation.state.triggerAnnotationRender) {
cornerstoneTools.annotation.state.triggerAnnotationRender();
} else if (renderingEngineRef.current) {
renderingEngineRef.current.render();
}
return next;
});
};
// Add this function to gather and format metadata: // Add this function to gather and format metadata:
const handleShowMetadata = () => { const handleShowMetadata = () => {
// Find the first visible viewport with images // Find the first visible viewport with images
@@ -472,6 +515,28 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
const [altPressed, setAltPressed] = useState(false); const [altPressed, setAltPressed] = useState(false);
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
});
});
}, [MAIN_TOOL_GROUP_ID]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey) setAltPressed(true); if (e.altKey) setAltPressed(true);
@@ -1452,6 +1517,10 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
const referenceStackObj = availableStacks[referenceStackIdx]; const referenceStackObj = availableStacks[referenceStackIdx];
const referenceUID = referenceStackObj?.studyInstanceUID; const referenceUID = referenceStackObj?.studyInstanceUID;
const [selectedAnnUIDs, setSelectedAnnUIDs] = useState<(string | null)[]>(() =>
Array(MAX_VIEWPORTS).fill(null)
);
const numVisible = viewportGrid.rows * viewportGrid.cols; const numVisible = viewportGrid.rows * viewportGrid.cols;
const viewports = []; const viewports = [];
//for (let i = 0; i < MAX_VIEWPORTS; i++) { //for (let i = 0; i < MAX_VIEWPORTS; i++) {
@@ -1642,7 +1711,7 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
)} )}
</button> </button>
// Annotations buttons // Annotations buttons
{viewportModes[i] === "stack" && (() => { {viewportModes[i] === "stack" && (() => {
// Get all annotations for this stack // Get all annotations for this stack
const imageIds = viewportImageIds[i]; const imageIds = viewportImageIds[i];
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || []; const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || [];
@@ -1653,6 +1722,22 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
imageIds.includes(ann.metadata.referencedImageId) imageIds.includes(ann.metadata.referencedImageId)
); );
if (!stackAnnotations.length) return null; if (!stackAnnotations.length) return null;
// Find the annotation(s) on the current image
const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine?.getViewport(`CT_${i}`);
const currentIdx = viewport?.getCurrentImageIdIndex?.() ?? 0;
const currentImageId = imageIds[currentIdx];
const currentAnnotations = stackAnnotations.filter(
ann => ann.metadata.referencedImageId === currentImageId
);
// Use per-viewport selection state
const selectedAnnUID = selectedAnnUIDs[i];
return ( return (
<div <div
style={{ style={{
@@ -1664,8 +1749,10 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
display: "flex", display: "flex",
gap: 4, gap: 4,
pointerEvents: "auto", pointerEvents: "auto",
alignItems: "center",
}} }}
> >
{/* Prev/Next buttons ... */}
<button <button
style={{ style={{
background: "rgba(30,30,30,0.7)", background: "rgba(30,30,30,0.7)",
@@ -1686,7 +1773,7 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
}} }}
title="Previous Annotation" title="Previous Annotation"
aria-label="Previous Annotation" aria-label="Previous Annotation"
onClick={() => { onClick={() => {
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine?.getViewport(`CT_${i}`); const viewport = renderingEngine?.getViewport(`CT_${i}`);
if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return;
@@ -1695,8 +1782,14 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
if (prevIdx !== null && typeof viewport.setImageIdIndex === "function") { if (prevIdx !== null && typeof viewport.setImageIdIndex === "function") {
csUtilities.jumpToSlice(viewport.element, { imageIndex: prevIdx }); csUtilities.jumpToSlice(viewport.element, { imageIndex: prevIdx });
updateOverlay(i, viewport); 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);
} }
}} }}
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</button> </button>
@@ -1720,7 +1813,7 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
}} }}
title="Next Annotation" title="Next Annotation"
aria-label="Next Annotation" aria-label="Next Annotation"
onClick={() => { onClick={() => {
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine?.getViewport(`CT_${i}`); const viewport = renderingEngine?.getViewport(`CT_${i}`);
if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return; if (!viewport || typeof viewport.getCurrentImageIdIndex !== "function") return;
@@ -1729,14 +1822,74 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
if (nextIdx !== null && typeof viewport.setImageIdIndex === "function") { if (nextIdx !== null && typeof viewport.setImageIdIndex === "function") {
csUtilities.jumpToSlice(viewport.element, { imageIndex: nextIdx }); csUtilities.jumpToSlice(viewport.element, { imageIndex: nextIdx });
updateOverlay(i, viewport); 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);
} }
}} }}
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</button> </button>
{/* List and select/delete individual annotations */}
{currentAnnotations.length > 0 && (
<div style={{ display: "flex", gap: 2, alignItems: "center" }}>
{currentAnnotations.map(ann => (
<div
key={ann.annotationUID}
style={{
display: "flex",
alignItems: "center",
background: selectedAnnUID === ann.annotationUID ? "rgb(4, 225, 0)" : "#444",
color: selectedAnnUID === ann.annotationUID ? "#222" : "#fff",
borderRadius: "4px",
padding: "2px 6px",
marginRight: 2,
cursor: "pointer",
border: selectedAnnUID === ann.annotationUID ? "2px solid #1976d2" : "none",
fontWeight: selectedAnnUID === ann.annotationUID ? "bold" : "normal",
}}
onClick={() => handleSelectAnnotation(i, ann.annotationUID)}
>
<span style={{ marginRight: 4 }}>
{selectedAnnUID === ann.annotationUID ? "★ " : ""}
{ann.metadata.toolName || "Annotation"}
</span>
<button
style={{
background: "#b71c1c",
color: "#fff",
border: "none",
borderRadius: "50%",
width: 20,
height: 20,
fontSize: "13px",
fontWeight: "bold",
cursor: "pointer",
marginLeft: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: 0.85,
}}
title="Delete this annotation"
aria-label="Delete annotation"
onClick={e => {
e.stopPropagation();
handleDeleteAnnotation(i, ann.annotationUID, currentAnnotations);
}}
>
🗑
</button>
</div>
))}
</div>
)}
</div> </div>
); );
})()} })()}
// ...existing code... // ...existing code...
{/* Overlay info (bottom left of viewport) */} {/* Overlay info (bottom left of viewport) */}
<div <div