fix annotation selection
This commit is contained in:
+246
-93
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user