fix annotation selection

This commit is contained in:
Ross
2025-06-03 21:39:07 +01:00
parent cdbe31df6b
commit b1db098a49
+246 -93
View File
@@ -397,6 +397,49 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
const [metaModalOpen, setMetaModalOpen] = useState(false);
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:
const handleShowMetadata = () => {
// Find the first visible viewport with images
@@ -472,6 +515,28 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
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(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey) setAltPressed(true);
@@ -1452,6 +1517,10 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
const referenceStackObj = availableStacks[referenceStackIdx];
const referenceUID = referenceStackObj?.studyInstanceUID;
const [selectedAnnUIDs, setSelectedAnnUIDs] = useState<(string | null)[]>(() =>
Array(MAX_VIEWPORTS).fill(null)
);
const numVisible = viewportGrid.rows * viewportGrid.cols;
const viewports = [];
//for (let i = 0; i < MAX_VIEWPORTS; i++) {
@@ -1642,101 +1711,185 @@ function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps)
)}
</button>
// Annotations buttons
{viewportModes[i] === "stack" && (() => {
// Get all annotations for this stack
const imageIds = viewportImageIds[i];
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || [];
const stackAnnotations = allAnnotations.filter(
ann =>
ann.metadata &&
ann.metadata.referencedImageId &&
imageIds.includes(ann.metadata.referencedImageId)
);
if (!stackAnnotations.length) return null;
return (
<div
style={{
position: "absolute",
left: "50%",
bottom: 60,
transform: "translateX(-50%)",
zIndex: 30,
display: "flex",
gap: 4,
pointerEvents: "auto",
}}
>
<button
style={{
background: "rgba(30,30,30,0.7)",
color: "#fff",
border: "none",
borderRadius: "50%",
width: 28,
height: 28,
fontSize: "16px",
fontWeight: "bold",
cursor: "pointer",
opacity: 0.7,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
transition: "opacity 0.2s",
}}
title="Previous Annotation"
aria-label="Previous Annotation"
onClick={() => {
{viewportModes[i] === "stack" && (() => {
// Get all annotations for this stack
const imageIds = viewportImageIds[i];
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations() || [];
const stackAnnotations = allAnnotations.filter(
ann =>
ann.metadata &&
ann.metadata.referencedImageId &&
imageIds.includes(ann.metadata.referencedImageId)
);
if (!stackAnnotations.length) return null;
// Find the annotation(s) on the current image
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);
}
}}
>
<span aria-hidden="true"></span>
</button>
<button
style={{
background: "rgba(30,30,30,0.7)",
color: "#fff",
border: "none",
borderRadius: "50%",
width: 28,
height: 28,
fontSize: "16px",
fontWeight: "bold",
cursor: "pointer",
opacity: 0.7,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
transition: "opacity 0.2s",
}}
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);
}
}}
>
<span aria-hidden="true"></span>
</button>
</div>
);
})()}
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 (
<div
style={{
position: "absolute",
left: "50%",
bottom: 60,
transform: "translateX(-50%)",
zIndex: 30,
display: "flex",
gap: 4,
pointerEvents: "auto",
alignItems: "center",
}}
>
{/* Prev/Next buttons ... */}
<button
style={{
background: "rgba(30,30,30,0.7)",
color: "#fff",
border: "none",
borderRadius: "50%",
width: 28,
height: 28,
fontSize: "16px",
fontWeight: "bold",
cursor: "pointer",
opacity: 0.7,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
transition: "opacity 0.2s",
}}
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);
}
}}
>
<span aria-hidden="true"></span>
</button>
<button
style={{
background: "rgba(30,30,30,0.7)",
color: "#fff",
border: "none",
borderRadius: "50%",
width: 28,
height: 28,
fontSize: "16px",
fontWeight: "bold",
cursor: "pointer",
opacity: 0.7,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
transition: "opacity 0.2s",
}}
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);
}
}}
>
<span aria-hidden="true"></span>
</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>
);
})()}
// ...existing code...
{/* Overlay info (bottom left of viewport) */}
<div