get legacy annotation import working

This commit is contained in:
Ross
2025-06-03 15:45:11 +01:00
parent 2518da4413
commit 8c218d42f4
3 changed files with 518 additions and 341 deletions
+504 -338
View File
@@ -21,9 +21,28 @@ import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool";
// Extend the Window interface to include cornerstoneTools
declare global {
interface Window {
cornerstoneTools: typeof cornerstoneTools;
cornerstone3dTools: typeof cornerstoneTools;
}
}
function pixelToWorld(imageId: string, point: { x: number, y: number }) {
const plane = metaData.get("imagePlaneModule", imageId);
if (!plane) return undefined;
const { rowCosines, columnCosines, imagePositionPatient, rowPixelSpacing, columnPixelSpacing } = plane;
if (!rowCosines || !columnCosines || !imagePositionPatient) return undefined;
// Default to 1 if spacing is missing
const dx = columnPixelSpacing || 1;
const dy = rowPixelSpacing || 1;
// Convert cosines to vectors
const rc = rowCosines;
const cc = columnCosines;
const ipp = imagePositionPatient;
// World = IPP + x*col*cos*spacing + y*row*cos*spacing
return [
ipp[0] + point.x * cc[0] * dx + point.y * rc[0] * dy,
ipp[1] + point.x * cc[1] * dx + point.y * rc[1] * dy,
ipp[2] + point.x * cc[2] * dx + point.y * rc[2] * dy,
];
}
// Add to global types:
declare global {
@@ -32,6 +51,8 @@ declare global {
importViewerState?: (json: string) => void;
exportAnnotations?: () => string;
importAnnotations?: (json: string) => void;
importLegacyAnnotations?: (json: string) => void;
importLegacyViewport?: (json: string) => void;
}
}
@@ -50,7 +71,7 @@ declare global {
}
}
window.cornerstoneTools = cornerstoneTools;
window.cornerstone3dTools = cornerstoneTools;
type ViewportDiv = HTMLDivElement & {
@@ -116,11 +137,12 @@ const ALL_MOUSE_BINDINGS = [
type AppProps = {
container_id?: string;
imageStacks: () => Promise<string[][]>;
autoCacheStack?: boolean; // <-- new prop
// ...other props if needed
};
// App definintion
function App({ container_id, imageStacks, ...props }: AppProps) {
function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps) {
const elementRef = useRef<HTMLDivElement>(null)
const running = useRef(false)
@@ -175,7 +197,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
);
setAvailableStacks(stackEntries);
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
setLoadedImageIdsAndCacheMeta(stacks[0]);
await setLoadedImageIdsAndCacheMeta(stacks[0]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
});
}, []);
@@ -214,6 +236,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
const [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false);
const sortViewportImageIdsBySliceLocation = () => {
console.log("Sorting viewport imageIds by slice location");
setViewportImageIds(prev => {
const next = prev.map((imageIds, idx) => {
if (!imageIds || imageIds.length === 0) return imageIds;
@@ -255,38 +278,44 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
// Wrap setLoadedImageIds to also cache DICOM metadata
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
console.log("Setting loaded imageIds:", imageIds);
console.log("autoCacheStack:", autoCacheStack);
const loadPromises: Promise<any>[] = [];
for (const imageId of imageIds) {
if (imageId.startsWith("wadouri:")) {
try {
const url = imageId.slice(8);
if (url.startsWith("blob:") || url.startsWith("/")) {
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
// If load returns a promise, collect it
// @ts-ignore
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(
url,
);
if (result && typeof result.then === "function") {
loadPromises.push(result);
if (autoCacheStack) {
for (const imageId of imageIds) {
console.log("Caching metadata for imageId:", imageId);
if (imageId.startsWith("wadouri:")) {
try {
const url = imageId.slice(8);
if (url.startsWith("blob:") || url.startsWith("/")) {
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
// If load returns a promise, collect it
// @ts-ignore
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(
url,
);
if (result && typeof result.then === "function") {
loadPromises.push(result);
}
} else {
console.warn("MetaDataManager not found. Unable to cache metadata.");
}
} else {
console.warn("MetaDataManager not found. Unable to cache metadata.");
}
} catch (e) {
console.warn(`Failed to cache metadata for ${imageId}:`, e);
}
} catch (e) {
console.warn(`Failed to cache metadata for ${imageId}:`, e);
}
}
}
// Wait for all metadata loads to finish
await Promise.all(loadPromises);
// Wait for all metadata loads to finish
await Promise.all(loadPromises);
// Trigger sort if enabled
if (orderBySliceLocation) {
sortViewportImageIdsBySliceLocation();
// Trigger sort if enabled
if (orderBySliceLocation) {
console.log("Sorting viewport imageIds by slice location");
sortViewportImageIdsBySliceLocation();
}
}
};
@@ -667,7 +696,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
// --- In your setup function, use the correct tool group per viewport ---
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
if (!mainToolGroup) {
mainToolGroup = setupTools(MAIN_TOOL_GROUP_ID).mainToolGroup;
mainToolGroup = setupTools(MAIN_TOOL_GROUP_ID)
}
@@ -1249,12 +1278,149 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
}
};
window[`importLegacyAnnotations_${apiKey}`] = (json: string) => {
let legacyData: any;
try {
legacyData = typeof json === "string" ? JSON.parse(json) : json;
} catch (e) {
alert("Invalid legacy annotation JSON");
return;
}
// Remove all existing annotations
if (cornerstoneTools.annotation.state.removeAllAnnotations) {
cornerstoneTools.annotation.state.removeAllAnnotations();
}
const toolNameMap: Record<string, string> = {
ArrowAnnotate: "ArrowAnnotate",
// Add more mappings if needed
};
Object.entries(legacyData).forEach(([imageId, tools]) => {
Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => {
const cs3Tool = toolNameMap[legacyTool] || legacyTool;
if (toolData.data && Array.isArray(toolData.data)) {
toolData.data.forEach((ann: any) => {
// Convert pixel handles to world coordinates
const startWorld = pixelToWorld(imageId, { x: ann.handles.start.y, y: ann.handles.start.x });
const endWorld = pixelToWorld(imageId, { x: ann.handles.end.y, y: ann.handles.end.x });
console.log("Importing legacy annotation:")
console.log(" Image ID:", imageId);
console.log(" Start World:", startWorld);
console.log(" End World:", endWorld);
// Get spatial metadata
const plane = metaData.get("imagePlaneModule", imageId);
console.log(metaData);
let FrameOfReferenceUID = metaData.get("frameOfReferenceUID", imageId);
if (!FrameOfReferenceUID && plane && plane.frameOfReferenceUID) {
FrameOfReferenceUID = plane.frameOfReferenceUID;
}
const viewPlaneNormal = plane?.rowCosines && plane?.columnCosines
? [
plane.rowCosines[1] * plane.columnCosines[2] - plane.rowCosines[2] * plane.columnCosines[1],
plane.rowCosines[2] * plane.columnCosines[0] - plane.rowCosines[0] * plane.columnCosines[2],
plane.rowCosines[0] * plane.columnCosines[1] - plane.rowCosines[1] * plane.columnCosines[0],
]
: undefined;
const annotation = {
annotationUID: ann.uuid || cornerstoneTools.utilities.uuidv4(),
metadata: {
toolName: cs3Tool,
referencedImageId: imageId,
FrameOfReferenceUID,
viewPlaneNormal,
// Optionally add viewUp, cameraFocalPoint, etc.
},
data: {
handles: {
points: [startWorld, endWorld],
textBox: ann.handles.textBox,
},
text: ann.text || "",
// Add more fields as needed
},
visibility: ann.visible !== false,
active: ann.active || false,
};
cornerstoneTools.annotation.state.addAnnotation(annotation, annotation.annotationUID);
});
}
});
});
if (renderingEngineRef.current) {
renderingEngineRef.current.render();
}
};
// Import and apply a legacy viewport state to the active viewport
window[`importLegacyViewport_${apiKey}`] = (json: string) => {
let legacy: any;
try {
legacy = typeof json === "string" ? JSON.parse(json) : json;
} catch (e) {
alert("Invalid legacy viewport JSON");
return;
}
const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) return;
const viewportId = `CT_${activeViewport ?? 0}`;
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) return;
// Map legacy fields to Cornerstone3D properties
const props: any = {};
if (legacy.voi) {
props.voiRange = {
lower: legacy.voi.windowCenter - legacy.voi.windowWidth / 2,
upper: legacy.voi.windowCenter + legacy.voi.windowWidth / 2,
};
}
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;
if (typeof viewport.setProperties === "function") {
viewport.setProperties(props);
}
// Apply translation and scale (pan/zoom)
if (legacy.translation || legacy.scale) {
const camera = viewport.getCamera ? viewport.getCamera() : {};
if (legacy.translation) {
camera.translation = { ...camera.translation, ...legacy.translation };
}
if (legacy.scale) {
camera.parallelScale = 1 / legacy.scale;
}
if (typeof viewport.setCamera === "function") {
viewport.setCamera(camera);
}
}
viewport.render();
updateOverlay(activeViewport ?? 0, viewport);
};
// Cleanup
return () => {
window[`exportViewerState_${apiKey}`] = undefined;
window[`importViewerState_${apiKey}`] = undefined;
window[`exportAnnotations_${apiKey}`] = undefined;
window[`importAnnotations_${apiKey}`] = undefined;
window[`importLegacyAnnotations_${apiKey}`] = undefined;
window[`importLegacyViewport_${apiKey}`] = undefined;
};
}, [
viewportGrid,
@@ -1279,7 +1445,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
const numVisible = viewportGrid.rows * viewportGrid.cols;
const viewports = [];
//for (let i = 0; i < MAX_VIEWPORTS; i++) {
for (let i = 0; i < numVisible; i++) {
for (let i = 0; i < numVisible; i++) {
// Determine if this viewport should be visible in the current grid
const isVisible = i < viewportGrid.rows * viewportGrid.cols;
@@ -1651,8 +1817,8 @@ for (let i = 0; i < numVisible; i++) {
idx < viewportImageIds[i].length - 1
? "1px solid #222"
: "none",
cursor: loaded ? "pointer" : "default",
pointerEvents: loaded ? "auto" : "none",
pointerEvents: "auto",
cursor: "pointer",
}}
title={
isCurrent
@@ -1683,164 +1849,164 @@ for (let i = 0; i < numVisible; i++) {
</div>
</div>
)}
{isVisible && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 8,
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "center", // Center horizontally
pointerEvents: "auto",
width: "100%",
// Remove background/gap if you want only the selector to be visible
background: "none",
borderRadius: "0",
padding: 0,
}}
>
{/* Custom Stack Dropdown */}
<div style={{ position: "relative" }}>
<button
style={{
background: "#222",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "2px 8px",
fontSize: "13px",
marginRight: "6px",
minWidth: 120,
textAlign: "left",
cursor: "pointer",
}}
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
//onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
aria-haspopup="listbox"
aria-expanded={openDropdownIdx === i}
>
{/* Show current stack with indicator */}
{(() => {
const stackObj = availableStacks[selectedStackIdx[i] || 0];
const thisUID = stackObj?.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
return (
<span>
<span style={{
color: dotColor,
fontWeight: "bold",
marginRight: 6,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {selectedStackIdx[i] + 1} ({stackObj?.imageIds.length || 0} images)
</span>
);
})()}
<span style={{ float: "right", fontSize: "12px" }}></span>
</button>
{openDropdownIdx === i && (
<div
ref={el => {
if (el) el.focus();
}}
tabIndex={0}
{isVisible && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 8,
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "center", // Center horizontally
pointerEvents: "auto",
width: "100%",
// Remove background/gap if you want only the selector to be visible
background: "none",
borderRadius: "0",
padding: 0,
}}
>
{/* Custom Stack Dropdown */}
<div style={{ position: "relative" }}>
<button
style={{
position: "absolute",
left: 0,
bottom: "110%",
background: "#222",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
zIndex: 100,
minWidth: 180,
maxHeight: 220,
overflowY: "auto",
overscrollBehavior: "contain",
scrollbarWidth: "thin",
}}
role="listbox"
onMouseDown={e => e.stopPropagation()}
onWheel={e => {
// Trap scroll inside the dropdown
const target = e.currentTarget as HTMLDivElement;
const { scrollTop, scrollHeight, clientHeight } = target;
const atTop = scrollTop === 0;
const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
if (
(e.deltaY < 0 && atTop) ||
(e.deltaY > 0 && atBottom)
) {
// Let the event bubble to parent (viewport)
return;
}
// Otherwise, prevent scrolling the viewport
e.stopPropagation();
e.preventDefault();
// Manually scroll the menu
target.scrollTop += e.deltaY;
}}
onBlur={e => {
const related = e.relatedTarget as Node;
const parent = e.currentTarget.parentElement;
if (!parent?.contains(related)) {
setTimeout(() => setOpenDropdownIdx(null), 100);
}
padding: "2px 8px",
fontSize: "13px",
marginRight: "6px",
minWidth: 120,
textAlign: "left",
cursor: "pointer",
}}
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
//onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
aria-haspopup="listbox"
aria-expanded={openDropdownIdx === i}
>
{availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID;
{/* Show current stack with indicator */}
{(() => {
const stackObj = availableStacks[selectedStackIdx[i] || 0];
const thisUID = stackObj?.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
const isEmpty = stackObj.imageIds.length === 0;
return (
<div
key={idx}
role="option"
aria-selected={selectedStackIdx[i] === idx}
style={{
padding: "6px 12px",
background: selectedStackIdx[i] === idx ? "#444" : "#222",
color: isEmpty ? "#aaa" : "#fff",
cursor: isEmpty ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
borderBottom: "1px solid #333",
userSelect: "none",
opacity: isEmpty ? 0.6 : 1,
}}
tabIndex={-1}
onMouseDown={e => {
e.preventDefault();
if (!isEmpty) {
handleLoadStack(i, idx);
setOpenDropdownIdx(null);
}
}}
>
<span>
<span style={{
color: dotColor,
fontWeight: "bold",
marginRight: 8,
marginRight: 6,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {idx + 1} ({stackObj.imageIds.length} images
{isEmpty && <span style={{ color: "#f44336", marginLeft: 6 }}>(empty)</span>})
</div>
Stack {selectedStackIdx[i] + 1} ({stackObj?.imageIds.length || 0} images)
</span>
);
})}
</div>
)}
})()}
<span style={{ float: "right", fontSize: "12px" }}></span>
</button>
{openDropdownIdx === i && (
<div
ref={el => {
if (el) el.focus();
}}
tabIndex={0}
style={{
position: "absolute",
left: 0,
bottom: "110%",
background: "#222",
border: "1px solid #444",
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
zIndex: 100,
minWidth: 180,
maxHeight: 220,
overflowY: "auto",
overscrollBehavior: "contain",
scrollbarWidth: "thin",
}}
role="listbox"
onMouseDown={e => e.stopPropagation()}
onWheel={e => {
// Trap scroll inside the dropdown
const target = e.currentTarget as HTMLDivElement;
const { scrollTop, scrollHeight, clientHeight } = target;
const atTop = scrollTop === 0;
const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
if (
(e.deltaY < 0 && atTop) ||
(e.deltaY > 0 && atBottom)
) {
// Let the event bubble to parent (viewport)
return;
}
// Otherwise, prevent scrolling the viewport
e.stopPropagation();
e.preventDefault();
// Manually scroll the menu
target.scrollTop += e.deltaY;
}}
onBlur={e => {
const related = e.relatedTarget as Node;
const parent = e.currentTarget.parentElement;
if (!parent?.contains(related)) {
setTimeout(() => setOpenDropdownIdx(null), 100);
}
}}
>
{availableStacks.map((stackObj, idx) => {
const thisUID = stackObj.studyInstanceUID;
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
const dotColor = sameStudy ? "#4caf50" : "#888";
const isEmpty = stackObj.imageIds.length === 0;
return (
<div
key={idx}
role="option"
aria-selected={selectedStackIdx[i] === idx}
style={{
padding: "6px 12px",
background: selectedStackIdx[i] === idx ? "#444" : "#222",
color: isEmpty ? "#aaa" : "#fff",
cursor: isEmpty ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
borderBottom: "1px solid #333",
userSelect: "none",
opacity: isEmpty ? 0.6 : 1,
}}
tabIndex={-1}
onMouseDown={e => {
e.preventDefault();
if (!isEmpty) {
handleLoadStack(i, idx);
setOpenDropdownIdx(null);
}
}}
>
<span style={{
color: dotColor,
fontWeight: "bold",
marginRight: 8,
fontSize: "16px",
verticalAlign: "middle",
}}></span>
Stack {idx + 1} ({stackObj.imageIds.length} images
{isEmpty && <span style={{ color: "#f44336", marginLeft: 6 }}>(empty)</span>})
</div>
);
})}
</div>
)}
</div>
</div>
</div>
)}
)}
</div>
);
}
@@ -1850,16 +2016,16 @@ for (let i = 0; i < numVisible; i++) {
return (
// App root
<div style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
boxSizing: "border-box",
background: "#222",
padding: "12px",
overflow: "hidden",
}}>
<div style={{
position: "relative",
inset: 0,
width: "100%",
height: "100%",
boxSizing: "border-box",
background: "#222",
padding: "12px",
overflow: "hidden",
}}>
<div>
{/* Side menu toggle button */}
<button
@@ -1888,175 +2054,175 @@ for (let i = 0; i < numVisible; i++) {
</button>
{/* Side menu drawer */}
{sideMenuOpen && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: 220,
height: "100%",
background: "#222",
color: "#fff",
zIndex: 199,
boxShadow: "2px 0 12px rgba(0,0,0,0.3)",
flexDirection: "column",
padding: "24px 16px 16px 16px",
display: "flex",
overflow: "hidden",
}}
>
{sideMenuOpen && (
<div
style={{
flex: 1,
minHeight: 0,
overflowY: "auto", // <-- enables scrolling for menu content
display: "flex",
position: "absolute",
top: 0,
left: 0,
width: 220,
height: "100%",
background: "#222",
color: "#fff",
zIndex: 199,
boxShadow: "2px 0 12px rgba(0,0,0,0.3)",
flexDirection: "column",
padding: "24px 16px 16px 16px",
display: "flex",
overflow: "hidden",
}}
>
<div style={{ fontWeight: "bold", fontSize: 18, marginBottom: 24 }}>
Menu
<button
style={{
float: "right",
background: "transparent",
color: "#fff",
border: "none",
fontSize: "22px",
cursor: "pointer",
}}
onClick={() => setSideMenuOpen(false)}
aria-label="Close menu"
>
×
</button>
</div>
<button
style={{
padding: "10px 18px",
background: "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => fileInputRef.current?.click()}
>
Load DICOM Folder
</button>
<input
ref={fileInputRef}
type="file"
style={{ display: "none" }}
multiple
onChange={handleFilesSelected}
accept=".dcm,application/dicom"
/>
<div
style={{
marginBottom: "18px",
flex: 1,
minHeight: 0,
overflowY: "auto", // <-- enables scrolling for menu content
display: "flex",
alignItems: "center",
gap: "8px",
flexDirection: "column",
}}
>
<div style={{ fontWeight: "bold", fontSize: 18, marginBottom: 24 }}>
Menu
<button
style={{
float: "right",
background: "transparent",
color: "#fff",
border: "none",
fontSize: "22px",
cursor: "pointer",
}}
onClick={() => setSideMenuOpen(false)}
aria-label="Close menu"
>
×
</button>
</div>
<button
style={{
padding: "10px 18px",
background: "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => fileInputRef.current?.click()}
>
Load DICOM Folder
</button>
<input
type="checkbox"
id="orderBySliceLocation"
checked={orderBySliceLocation}
onChange={e => setOrderBySliceLocation(e.target.checked)}
style={{ accentColor: "#666" }}
ref={fileInputRef}
type="file"
style={{ display: "none" }}
multiple
onChange={handleFilesSelected}
accept=".dcm,application/dicom"
/>
<label htmlFor="orderBySliceLocation" style={{ fontSize: "15px", cursor: "pointer" }}>
Order by Slice Location
</label>
</div>
<button
style={{
padding: "10px 18px",
background: "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={handleShowMetadata}
>
Show DICOM Metadata
</button>
<button
style={{
padding: "10px 18px",
background: crossHairsEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setCrossHairsEnabled((prev) => !prev)}
>
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
</button>
<button
style={{
padding: "10px 18px",
background: referenceLinesEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setReferenceLinesEnabled((prev) => !prev)}
>
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
</button>
<button
onClick={sortViewportImageIdsBySliceLocation}
style={{ margin: 8, padding: 8 }}
>
Sort All Viewports by Slice Location
</button>
<button
style={{
padding: "10px 18px",
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setAltEnablesReferenceCursors((prev) => !prev)}
>
{altEnablesReferenceCursors
? "Disable Alt Key for Reference Cursors"
: "Enable Alt Key for Reference Cursors"}
</button>
{/* Add more menu items here if needed */}
<div
style={{
marginBottom: "18px",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<input
type="checkbox"
id="orderBySliceLocation"
checked={orderBySliceLocation}
onChange={e => setOrderBySliceLocation(e.target.checked)}
style={{ accentColor: "#666" }}
/>
<label htmlFor="orderBySliceLocation" style={{ fontSize: "15px", cursor: "pointer" }}>
Order by Slice Location
</label>
</div>
<button
style={{
padding: "10px 18px",
background: "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={handleShowMetadata}
>
Show DICOM Metadata
</button>
<button
style={{
padding: "10px 18px",
background: crossHairsEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setCrossHairsEnabled((prev) => !prev)}
>
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
</button>
<button
style={{
padding: "10px 18px",
background: referenceLinesEnabled ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setReferenceLinesEnabled((prev) => !prev)}
>
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
</button>
<button
onClick={sortViewportImageIdsBySliceLocation}
style={{ margin: 8, padding: 8 }}
>
Sort All Viewports by Slice Location
</button>
<button
style={{
padding: "10px 18px",
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
fontWeight: "bold",
cursor: "pointer",
fontSize: "15px",
marginBottom: "18px",
width: "100%",
}}
onClick={() => setAltEnablesReferenceCursors((prev) => !prev)}
>
{altEnablesReferenceCursors
? "Disable Alt Key for Reference Cursors"
: "Enable Alt Key for Reference Cursors"}
</button>
{/* Add more menu items here if needed */}
</div>
</div>
</div>
)}
)}
</div>
{/* Viewport grid menu at top center */}
@@ -2515,5 +2681,5 @@ function setupTools(MAIN_TOOL_GROUP_ID) {
mainToolGroup.addTool(ReferenceCursors.toolName);
}
return { mainToolGroup };
return mainToolGroup;
}