a lot of fixes

This commit is contained in:
Ross
2025-06-09 13:31:56 +01:00
parent 7d8caebf8c
commit 28569a1c94
2 changed files with 187 additions and 169 deletions
+131 -120
View File
@@ -398,42 +398,42 @@ 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) => { const handleSelectAnnotation = (viewportIdx: number, uid: string | null) => {
// Use the official selection API for Cornerstone3D Tools // Use the official selection API for Cornerstone3D Tools
if (cornerstoneTools?.annotation?.selection?.setAnnotationSelected) { if (cornerstoneTools?.annotation?.selection?.setAnnotationSelected) {
cornerstoneTools.annotation.selection.setAnnotationSelected(uid); cornerstoneTools.annotation.selection.setAnnotationSelected(uid);
} }
// Optionally, update your own UI state // Optionally, update your own UI state
setSelectedAnnUIDs(prev => { setSelectedAnnUIDs(prev => {
const next = [...prev]; const next = [...prev];
next[viewportIdx] = uid; next[viewportIdx] = uid;
return next; 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;
}); });
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(); 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: // Add this function to gather and format metadata:
const handleShowMetadata = () => { const handleShowMetadata = () => {
@@ -510,27 +510,27 @@ const handleDeleteAnnotation = (viewportIdx: number, uid: string, currentAnnotat
const [altPressed, setAltPressed] = useState(false); const [altPressed, setAltPressed] = useState(false);
useEffect(() => { useEffect(() => {
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID); const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) return; if (!mainToolGroup) return;
// List all annotation tools you use // List all annotation tools you use
[ [
'ArrowAnnotate', 'ArrowAnnotate',
'NoLabelArrowAnnotate', 'NoLabelArrowAnnotate',
'Length', 'Length',
'RectangleROI', 'RectangleROI',
'EllipticalROI', 'EllipticalROI',
'PlanarFreehandROI', 'PlanarFreehandROI',
'PlanarFreehandContourSegmentation', 'PlanarFreehandContourSegmentation',
// Add more if needed // Add more if needed
].forEach(toolName => { ].forEach(toolName => {
mainToolGroup.setToolConfiguration(toolName, { mainToolGroup.setToolConfiguration(toolName, {
color: '#00e676', // normal color color: '#00e676', // normal color
selectedColor: '#ffeb3b', // yellow for selected selectedColor: '#ffeb3b', // yellow for selected
});
}); });
}); }, [MAIN_TOOL_GROUP_ID]);
}, [MAIN_TOOL_GROUP_ID]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -1161,14 +1161,17 @@ useEffect(() => {
const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`); const viewport = renderingEngine.getViewport(`CT_${viewportIdx}`);
if (!viewport || typeof viewport.getImageIds !== "function") return; if (!viewport || typeof viewport.getImageIds !== "function") return;
const imageIds = viewport.getImageIds(); 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") { if (idx !== -1 && typeof viewport.setImageIdIndex === "function") {
csUtilities.jumpToSlice(viewport.element, { imageIndex: idx }); csUtilities.jumpToSlice(viewport.element, { imageIndex: idx });
updateOverlay(viewportIdx, viewport); updateOverlay(viewportIdx, viewport);
} }
} }
// Attach to window with a unique name // Attach to window with a unique name
window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId; window[`jumpToSliceByImageId_${apiKey}`] = jumpToSliceByImageId;
// Export the current viewer state as JSON // Export the current viewer state as JSON
@@ -1351,6 +1354,11 @@ useEffect(() => {
}; };
window[`importLegacyAnnotations_${apiKey}`] = (json: string) => { window[`importLegacyAnnotations_${apiKey}`] = (json: string) => {
// Remove all existing annotations
if (cornerstoneTools.annotation.state.removeAllAnnotations) {
cornerstoneTools.annotation.state.removeAllAnnotations();
}
let legacyData: any; let legacyData: any;
try { try {
legacyData = typeof json === "string" ? JSON.parse(json) : json; legacyData = typeof json === "string" ? JSON.parse(json) : json;
@@ -1359,17 +1367,13 @@ useEffect(() => {
return; return;
} }
// Remove all existing annotations
if (cornerstoneTools.annotation.state.removeAllAnnotations) {
cornerstoneTools.annotation.state.removeAllAnnotations();
}
const toolNameMap: Record<string, string> = { const toolNameMap: Record<string, string> = {
ArrowAnnotate: "ArrowAnnotate", ArrowAnnotate: "ArrowAnnotate",
// Add more mappings if needed // Add more mappings if needed
}; };
Object.entries(legacyData).forEach(([imageId, tools]) => { Object.entries(legacyData).forEach(([imageId, tools]) => {
console.log("Importing legacy annotations for imageId:", imageId);
Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => { Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => {
const cs3Tool = toolNameMap[legacyTool] || legacyTool; const cs3Tool = toolNameMap[legacyTool] || legacyTool;
if (toolData.data && Array.isArray(toolData.data)) { 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 = { const annotation = {
annotationUID: ann.uuid, // || cornerstoneTools.utilities.uuidv4(), annotationUID: ann.uuid, // || cornerstoneTools.utilities.uuidv4(),
@@ -1471,7 +1478,6 @@ useEffect(() => {
} }
if (typeof legacy.invert === "boolean") props.invert = legacy.invert; if (typeof legacy.invert === "boolean") props.invert = legacy.invert;
if (typeof legacy.pixelReplication === "boolean") props.interpolationType = legacy.pixelReplication ? 0 : 1; 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.hflip === "boolean") props.hflip = legacy.hflip;
if (typeof legacy.vflip === "boolean") props.vflip = legacy.vflip; if (typeof legacy.vflip === "boolean") props.vflip = legacy.vflip;
@@ -1493,6 +1499,10 @@ useEffect(() => {
viewport.setCamera(camera); viewport.setCamera(camera);
} }
} }
// --- Fix: Import rotation from legacy viewport ---
if (typeof legacy.rotation === "number") {
viewport.setRotation(legacy.rotation);
}
viewport.render(); viewport.render();
updateOverlay(activeViewport ?? 0, viewport); updateOverlay(activeViewport ?? 0, viewport);
@@ -1732,7 +1742,8 @@ useEffect(() => {
ann => ann =>
ann.metadata && ann.metadata &&
ann.metadata.referencedImageId && 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; if (!stackAnnotations.length) return null;
// Find the annotation(s) on the current image // Find the annotation(s) on the current image
@@ -1786,23 +1797,23 @@ useEffect(() => {
}} }}
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;
const currentIdx = viewport.getCurrentImageIdIndex(); const currentIdx = viewport.getCurrentImageIdIndex();
const prevIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, -1); const prevIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, -1);
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 // Select the first annotation on the new image
const newImageId = imageIds[prevIdx]; const newImageId = imageIds[prevIdx];
const annsOnImage = stackAnnotations.filter( const annsOnImage = stackAnnotations.filter(
ann => ann.metadata.referencedImageId === newImageId ann => ann.metadata.referencedImageId === newImageId
); );
handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null);
} }
}} }}
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</button> </button>
@@ -1826,23 +1837,23 @@ onClick={() => {
}} }}
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;
const currentIdx = viewport.getCurrentImageIdIndex(); const currentIdx = viewport.getCurrentImageIdIndex();
const nextIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, 1); const nextIdx = findNextAnnotationIdx(currentIdx, imageIds, stackAnnotations, 1);
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 // Select the first annotation on the new image
const newImageId = imageIds[nextIdx]; const newImageId = imageIds[nextIdx];
const annsOnImage = stackAnnotations.filter( const annsOnImage = stackAnnotations.filter(
ann => ann.metadata.referencedImageId === newImageId ann => ann.metadata.referencedImageId === newImageId
); );
handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null); handleSelectAnnotation(i, annsOnImage.length > 0 ? annsOnImage[0].annotationUID : null);
} }
}} }}
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</button> </button>
@@ -1850,26 +1861,26 @@ onClick={() => {
{currentAnnotations.length > 0 && ( {currentAnnotations.length > 0 && (
<div style={{ display: "flex", gap: 2, alignItems: "center" }}> <div style={{ display: "flex", gap: 2, alignItems: "center" }}>
{currentAnnotations.map(ann => ( {currentAnnotations.map(ann => (
<div <div
key={ann.annotationUID} key={ann.annotationUID}
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
background: selectedAnnUID === ann.annotationUID ? "rgb(4, 225, 0)" : "#444", background: selectedAnnUID === ann.annotationUID ? "rgb(4, 225, 0)" : "#444",
color: selectedAnnUID === ann.annotationUID ? "#222" : "#fff", color: selectedAnnUID === ann.annotationUID ? "#222" : "#fff",
borderRadius: "4px", borderRadius: "4px",
padding: "2px 6px", padding: "2px 6px",
marginRight: 2, marginRight: 2,
cursor: "pointer", cursor: "pointer",
border: selectedAnnUID === ann.annotationUID ? "2px solid #1976d2" : "none", border: selectedAnnUID === ann.annotationUID ? "2px solid #1976d2" : "none",
fontWeight: selectedAnnUID === ann.annotationUID ? "bold" : "normal", fontWeight: selectedAnnUID === ann.annotationUID ? "bold" : "normal",
}} }}
onClick={() => handleSelectAnnotation(i, ann.annotationUID)} onClick={() => handleSelectAnnotation(i, ann.annotationUID)}
> >
<span style={{ marginRight: 4 }}> <span style={{ marginRight: 4 }}>
{selectedAnnUID === ann.annotationUID ? "★ " : ""} {selectedAnnUID === ann.annotationUID ? "★ " : ""}
{ann.metadata.toolName || "Annotation"} {ann.metadata.toolName || "Annotation"}
</span> </span>
<button <button
style={{ style={{
background: "#b71c1c", background: "#b71c1c",
@@ -1891,7 +1902,7 @@ onClick={() => {
aria-label="Delete annotation" aria-label="Delete annotation"
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleDeleteAnnotation(i, ann.annotationUID, currentAnnotations); handleDeleteAnnotation(i, ann.annotationUID, currentAnnotations);
}} }}
> >
🗑 🗑
@@ -2123,7 +2134,7 @@ onClick={() => {
</div> </div>
</div> </div>
)} )}
{isVisible && ( {isVisible && availableStacks.length > 1 && (
<div <div
style={{ style={{
position: "absolute", position: "absolute",
+56 -49
View File
@@ -6,60 +6,67 @@ import Nifti from './Nifti.tsx'
import './index.css' import './index.css'
import { createImageIds, availableImageIds } from "./lib/createImageIds.ts" import { createImageIds, availableImageIds } from "./lib/createImageIds.ts"
const test_containers = document.querySelectorAll(".dicom-viewer-test-root"); // Helper to convert URLs to wadouri
test_containers.forEach((container) => { const toWadouri = (arr: string[]) =>
const id = container.id || ''; arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`);
const imageStacks = () => availableImageIds();
createRoot(container).render(
<App
container_id={id}
imageStacks={imageStacks}
/>
);
});
const containers = document.querySelectorAll(".dicom-viewer-root"); export function mountDicomViewers() {
containers.forEach((container) => { // Test containers
const id = container.id || ''; const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
let imageStacks; test_containers.forEach((container) => {
const id = container.id || '';
const imageStacks = () => availableImageIds();
createRoot(container).render(
<App
container_id={id}
imageStacks={imageStacks}
/>
);
});
// Helper to convert URLs to wadouri // Main containers
const toWadouri = (arr: string[]) => const containers = document.querySelectorAll(".dicom-viewer-root");
arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`); containers.forEach((container) => {
const id = container.id || '';
let imageStacks;
// Check for data-images attribute const dataImages = container.getAttribute('data-images');
const dataImages = container.getAttribute('data-images'); if (dataImages) {
if (dataImages) { let parsed;
let parsed; try {
try { parsed = JSON.parse(dataImages);
parsed = JSON.parse(dataImages); } catch (e) {
} catch (e) { console.error("Invalid data-images JSON:", e);
console.error("Invalid data-images JSON:", e); parsed = [];
parsed = []; }
} if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
// If it's a flat list, wrap in an array to make it a stack list and convert to wadouri imageStacks = () => Promise.resolve([toWadouri(parsed)]);
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { } else if (Array.isArray(parsed)) {
imageStacks = () => Promise.resolve([toWadouri(parsed)]); imageStacks = () => Promise.resolve(parsed.map(
} else if (Array.isArray(parsed)) { stack => Array.isArray(stack) ? toWadouri(stack) : []
imageStacks = () => Promise.resolve(parsed.map( ));
stack => Array.isArray(stack) ? toWadouri(stack) : [] } else {
)); imageStacks = () => Promise.resolve([[]]);
}
} else { } else {
imageStacks = () => Promise.resolve([[]]); imageStacks = () => Promise.resolve([[]]);
} }
} else {
imageStacks = () => Promise.resolve([[]]);
}
// Read autoCacheStack from data-auto-cache-stack attribute const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack');
const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack'); const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1";
const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1";
createRoot(container).render( createRoot(container).render(
<App <App
container_id={id} container_id={id}
imageStacks={imageStacks} imageStacks={imageStacks}
autoCacheStack={autoCacheStack} autoCacheStack={autoCacheStack}
/> />
); );
}); });
}
// Optionally, call it immediately for static containers:
mountDicomViewers();
// Optionally, expose globally for dynamic use:
(window as any).mountDicomViewers = mountDicomViewers;