get legacy annotation import working
This commit is contained in:
@@ -7,10 +7,16 @@
|
|||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" class="dicom-viewer-test-root" style="max-height:500px; height:500px; overflow:auto;"></div>
|
<!-- <div id="root" class="dicom-viewer-test-root" style="max-height:500px; height:500px; overflow:auto;"></div> -->
|
||||||
<div id="root2" class="dicom-viewer-root" style="max-height:500px; height:500px; overflow:auto;"
|
<div id="root" class="dicom-viewer-root" style="max-height:500px; height:500px; overflow:auto;"
|
||||||
data-images="["http://localhost:8000/media/atlas/dicom/4596ea7654b7a5074d2c3f40039c556b11fb01ae.dcm", "http://localhost:8000/media/atlas/dicom/b2fa301bc731d15be3e000402c9a79c114d17aa5.dcm", "http://localhost:8000/media/atlas/dicom/66310ef801fbecb37c5ced160c450dfd5ecc6d2d.dcm", "http://localhost:8000/media/atlas/dicom/63d2d117bf256a76a61190066f91eda6c4d67c8f.dcm", "http://localhost:8000/media/atlas/dicom/181a3575ea7fb8bf8f6449ff61e6e369a3247454.dcm", "http://localhost:8000/media/atlas/dicom/7c5c0538339266ac8897fa21c481b2f61172068e.dcm", "http://localhost:8000/media/atlas/dicom/1681410681308af7111af6644fed598fde304b96.dcm", "http://localhost:8000/media/atlas/dicom/4adcd3d7a64a9c59b530f730ce0d991bf7f27813.dcm", "http://localhost:8000/media/atlas/dicom/671e6004c7fb402a627a2282bbf304d7b4dcc12b.dcm", "http://localhost:8000/media/atlas/dicom/8d18ac56914b9de72b8aecced09654ec4b2c9bb7.dcm", "http://localhost:8000/media/atlas/dicom/74ce708a90dff0636fc4bc914fe47e8b6d9320bd.dcm", "http://localhost:8000/media/atlas/dicom/a96b6c9900570e076edf849a0ae3b52734ed2650.dcm", "http://localhost:8000/media/atlas/dicom/0183718d9336458478ec42130395d78e01492bd6.dcm", "http://localhost:8000/media/atlas/dicom/52eee968bbce5590274346eed43f1837f9daba11.dcm", "http://localhost:8000/media/atlas/dicom/961449f88197566c4643fcfd20f48ea4e810490b.dcm", "http://localhost:8000/media/atlas/dicom/1a16ad5b25e11739eadbcb8ea461072b100113a0.dcm", "http://localhost:8000/media/atlas/dicom/b89ddcc3f499059d2dedb0028a2bb1ce4abf5574.dcm", "http://localhost:8000/media/atlas/dicom/c1379c03a4e449c2fa88784d601cb766af907739.dcm", "http://localhost:8000/media/atlas/dicom/4fb532d5003c59a73f7e4248f365b1a8660ea710.dcm", "http://localhost:8000/media/atlas/dicom/e861b0de042a1f213cbccc995679aacd29c7ef53.dcm", "http://localhost:8000/media/atlas/dicom/1a854282afe264c3afff6a6e287cd646b2193a0c.dcm", "http://localhost:8000/media/atlas/dicom/f59d6d9bc33160abad3af8e2b246179b3574d884.dcm", "http://localhost:8000/media/atlas/dicom/04fadab0ed8b3f1925e601c4f9cfb792c95626c2.dcm", "http://localhost:8000/media/atlas/dicom/d45d0b12fb650e65daeeee4e5fa3b21c988597f6.dcm", "http://localhost:8000/media/atlas/dicom/e19c47b7787ac45dbc225319dfa143dc2c1fdf1f.dcm", "http://localhost:8000/media/atlas/dicom/dccabf3103a5be254a45457ce2d8d9ce9cb19a6a.dcm"]"
|
data-images="["http://localhost:8000/media/atlas/dicom/0fb1283bab62d691bdaf7c7e47b8c674b600cddb.dcm", "http://localhost:8000/media/atlas/dicom/c98950f460ed153c14bd1d0d32e712397ace4564.dcm", "http://localhost:8000/media/atlas/dicom/512bfc7f9d07d54ae8f92af1ab98be390174d618.dcm", "http://localhost:8000/media/atlas/dicom/5f0a6b97da797b7639361732599bfcf607c17dfc.dcm", "http://localhost:8000/media/atlas/dicom/c5f854a8c2ac36da60c8a5a2bc215639d5302d20.dcm", "http://localhost:8000/media/atlas/dicom/0cec8d98bd80818c38c657b9c5934b30316920a2.dcm", "http://localhost:8000/media/atlas/dicom/613166d33e9e35898238f0bd334969c500822bbf.dcm", "http://localhost:8000/media/atlas/dicom/48801a3aa1fea5d943cc934b74e9bc7273c1e944.dcm", "http://localhost:8000/media/atlas/dicom/db8ddb477387a41b023efae98e7e40be890886ef.dcm", "http://localhost:8000/media/atlas/dicom/8720dc9b0b3b2c596f1d87acc951818307b7ad6a.dcm", "http://localhost:8000/media/atlas/dicom/24d1e5397fa97dd0c904568a20e4649fea13d1cf.dcm", "http://localhost:8000/media/atlas/dicom/bb586d17474988a07b1c90afce456abd7f69a753.dcm", "http://localhost:8000/media/atlas/dicom/24fa49c0e482bb885c92b71f74451f558940cb4b.dcm", "http://localhost:8000/media/atlas/dicom/6d9b09614f74b21a0b2778a5129d22b3cbafd3da.dcm", "http://localhost:8000/media/atlas/dicom/39963ce191beafe943a9cf980f7320adad74c1d5.dcm", "http://localhost:8000/media/atlas/dicom/09adc694456f1e51a5ab9c1cf7eaf9d748c938db.dcm", "http://localhost:8000/media/atlas/dicom/d4f6521a9e9a26975e1218ba3b7f48a78ac46d8f.dcm", "http://localhost:8000/media/atlas/dicom/0e1490ed17cdf2ceb1bcf069400f8f47a4437561.dcm", "http://localhost:8000/media/atlas/dicom/2cb43b50760d9335df9a5f2d6bd645daf2a0c917.dcm", "http://localhost:8000/media/atlas/dicom/9b02fae29bf6e775c5ba94b0b7158e10d4697ff6.dcm", "http://localhost:8000/media/atlas/dicom/9628d1d2fb457bc6f03efa8c980ef3165092230f.dcm", "http://localhost:8000/media/atlas/dicom/b879d6488a79d38ba374366f5543aab49fa8cb86.dcm"]"
|
||||||
|
|
||||||
|
data-auto-cache-stack="true"
|
||||||
></div>
|
></div>
|
||||||
|
<!-- <div id="root2" class="dicom-viewer-root" style="max-height:500px; height:500px; overflow:auto;"
|
||||||
|
data-images="["http://localhost:8000/media/atlas/dicom/4596ea7654b7a5074d2c3f40039c556b11fb01ae.dcm", "http://localhost:8000/media/atlas/dicom/b2fa301bc731d15be3e000402c9a79c114d17aa5.dcm", "http://localhost:8000/media/atlas/dicom/66310ef801fbecb37c5ced160c450dfd5ecc6d2d.dcm", "http://localhost:8000/media/atlas/dicom/63d2d117bf256a76a61190066f91eda6c4d67c8f.dcm", "http://localhost:8000/media/atlas/dicom/181a3575ea7fb8bf8f6449ff61e6e369a3247454.dcm", "http://localhost:8000/media/atlas/dicom/7c5c0538339266ac8897fa21c481b2f61172068e.dcm", "http://localhost:8000/media/atlas/dicom/1681410681308af7111af6644fed598fde304b96.dcm", "http://localhost:8000/media/atlas/dicom/4adcd3d7a64a9c59b530f730ce0d991bf7f27813.dcm", "http://localhost:8000/media/atlas/dicom/671e6004c7fb402a627a2282bbf304d7b4dcc12b.dcm", "http://localhost:8000/media/atlas/dicom/8d18ac56914b9de72b8aecced09654ec4b2c9bb7.dcm", "http://localhost:8000/media/atlas/dicom/74ce708a90dff0636fc4bc914fe47e8b6d9320bd.dcm", "http://localhost:8000/media/atlas/dicom/a96b6c9900570e076edf849a0ae3b52734ed2650.dcm", "http://localhost:8000/media/atlas/dicom/0183718d9336458478ec42130395d78e01492bd6.dcm", "http://localhost:8000/media/atlas/dicom/52eee968bbce5590274346eed43f1837f9daba11.dcm", "http://localhost:8000/media/atlas/dicom/961449f88197566c4643fcfd20f48ea4e810490b.dcm", "http://localhost:8000/media/atlas/dicom/1a16ad5b25e11739eadbcb8ea461072b100113a0.dcm", "http://localhost:8000/media/atlas/dicom/b89ddcc3f499059d2dedb0028a2bb1ce4abf5574.dcm", "http://localhost:8000/media/atlas/dicom/c1379c03a4e449c2fa88784d601cb766af907739.dcm", "http://localhost:8000/media/atlas/dicom/4fb532d5003c59a73f7e4248f365b1a8660ea710.dcm", "http://localhost:8000/media/atlas/dicom/e861b0de042a1f213cbccc995679aacd29c7ef53.dcm", "http://localhost:8000/media/atlas/dicom/1a854282afe264c3afff6a6e287cd646b2193a0c.dcm", "http://localhost:8000/media/atlas/dicom/f59d6d9bc33160abad3af8e2b246179b3574d884.dcm", "http://localhost:8000/media/atlas/dicom/04fadab0ed8b3f1925e601c4f9cfb792c95626c2.dcm", "http://localhost:8000/media/atlas/dicom/d45d0b12fb650e65daeeee4e5fa3b21c988597f6.dcm", "http://localhost:8000/media/atlas/dicom/e19c47b7787ac45dbc225319dfa143dc2c1fdf1f.dcm", "http://localhost:8000/media/atlas/dicom/dccabf3103a5be254a45457ce2d8d9ce9cb19a6a.dcm"]"
|
||||||
|
data-auto-cache-stack="true"
|
||||||
|
></div> -->
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
<input type="file" id="dicomInput" multiple />
|
<input type="file" id="dicomInput" multiple />
|
||||||
|
|||||||
+504
-338
@@ -21,9 +21,28 @@ import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool";
|
|||||||
// Extend the Window interface to include cornerstoneTools
|
// Extend the Window interface to include cornerstoneTools
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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:
|
// Add to global types:
|
||||||
declare global {
|
declare global {
|
||||||
@@ -32,6 +51,8 @@ declare global {
|
|||||||
importViewerState?: (json: string) => void;
|
importViewerState?: (json: string) => void;
|
||||||
exportAnnotations?: () => string;
|
exportAnnotations?: () => string;
|
||||||
importAnnotations?: (json: string) => void;
|
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 & {
|
type ViewportDiv = HTMLDivElement & {
|
||||||
@@ -116,11 +137,12 @@ const ALL_MOUSE_BINDINGS = [
|
|||||||
type AppProps = {
|
type AppProps = {
|
||||||
container_id?: string;
|
container_id?: string;
|
||||||
imageStacks: () => Promise<string[][]>;
|
imageStacks: () => Promise<string[][]>;
|
||||||
|
autoCacheStack?: boolean; // <-- new prop
|
||||||
// ...other props if needed
|
// ...other props if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
// App definintion
|
// App definintion
|
||||||
function App({ container_id, imageStacks, ...props }: AppProps) {
|
function App({ container_id, imageStacks, autoCacheStack, ...props }: AppProps) {
|
||||||
const elementRef = useRef<HTMLDivElement>(null)
|
const elementRef = useRef<HTMLDivElement>(null)
|
||||||
const running = useRef(false)
|
const running = useRef(false)
|
||||||
|
|
||||||
@@ -175,7 +197,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
|
|||||||
);
|
);
|
||||||
setAvailableStacks(stackEntries);
|
setAvailableStacks(stackEntries);
|
||||||
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
|
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
|
||||||
setLoadedImageIdsAndCacheMeta(stacks[0]);
|
await setLoadedImageIdsAndCacheMeta(stacks[0]);
|
||||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(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 [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false);
|
||||||
|
|
||||||
const sortViewportImageIdsBySliceLocation = () => {
|
const sortViewportImageIdsBySliceLocation = () => {
|
||||||
|
console.log("Sorting viewport imageIds by slice location");
|
||||||
setViewportImageIds(prev => {
|
setViewportImageIds(prev => {
|
||||||
const next = prev.map((imageIds, idx) => {
|
const next = prev.map((imageIds, idx) => {
|
||||||
if (!imageIds || imageIds.length === 0) return imageIds;
|
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
|
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||||||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
||||||
|
console.log("Setting loaded imageIds:", imageIds);
|
||||||
|
console.log("autoCacheStack:", autoCacheStack);
|
||||||
const loadPromises: Promise<any>[] = [];
|
const loadPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const imageId of imageIds) {
|
if (autoCacheStack) {
|
||||||
if (imageId.startsWith("wadouri:")) {
|
for (const imageId of imageIds) {
|
||||||
try {
|
console.log("Caching metadata for imageId:", imageId);
|
||||||
const url = imageId.slice(8);
|
if (imageId.startsWith("wadouri:")) {
|
||||||
if (url.startsWith("blob:") || url.startsWith("/")) {
|
try {
|
||||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
const url = imageId.slice(8);
|
||||||
// If load returns a promise, collect it
|
if (url.startsWith("blob:") || url.startsWith("/")) {
|
||||||
// @ts-ignore
|
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(
|
// If load returns a promise, collect it
|
||||||
url,
|
// @ts-ignore
|
||||||
);
|
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(
|
||||||
if (result && typeof result.then === "function") {
|
url,
|
||||||
loadPromises.push(result);
|
);
|
||||||
|
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
|
// Wait for all metadata loads to finish
|
||||||
await Promise.all(loadPromises);
|
await Promise.all(loadPromises);
|
||||||
|
|
||||||
// Trigger sort if enabled
|
// Trigger sort if enabled
|
||||||
if (orderBySliceLocation) {
|
if (orderBySliceLocation) {
|
||||||
sortViewportImageIdsBySliceLocation();
|
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 ---
|
// --- In your setup function, use the correct tool group per viewport ---
|
||||||
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||||||
if (!mainToolGroup) {
|
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
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
window[`exportViewerState_${apiKey}`] = undefined;
|
window[`exportViewerState_${apiKey}`] = undefined;
|
||||||
window[`importViewerState_${apiKey}`] = undefined;
|
window[`importViewerState_${apiKey}`] = undefined;
|
||||||
window[`exportAnnotations_${apiKey}`] = undefined;
|
window[`exportAnnotations_${apiKey}`] = undefined;
|
||||||
window[`importAnnotations_${apiKey}`] = undefined;
|
window[`importAnnotations_${apiKey}`] = undefined;
|
||||||
|
window[`importLegacyAnnotations_${apiKey}`] = undefined;
|
||||||
|
window[`importLegacyViewport_${apiKey}`] = undefined;
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
viewportGrid,
|
viewportGrid,
|
||||||
@@ -1279,7 +1445,7 @@ function App({ container_id, imageStacks, ...props }: AppProps) {
|
|||||||
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++) {
|
||||||
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
|
// Determine if this viewport should be visible in the current grid
|
||||||
const isVisible = i < viewportGrid.rows * viewportGrid.cols;
|
const isVisible = i < viewportGrid.rows * viewportGrid.cols;
|
||||||
|
|
||||||
@@ -1651,8 +1817,8 @@ for (let i = 0; i < numVisible; i++) {
|
|||||||
idx < viewportImageIds[i].length - 1
|
idx < viewportImageIds[i].length - 1
|
||||||
? "1px solid #222"
|
? "1px solid #222"
|
||||||
: "none",
|
: "none",
|
||||||
cursor: loaded ? "pointer" : "default",
|
pointerEvents: "auto",
|
||||||
pointerEvents: loaded ? "auto" : "none",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
isCurrent
|
isCurrent
|
||||||
@@ -1683,164 +1849,164 @@ for (let i = 0; i < numVisible; i++) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center", // Center horizontally
|
justifyContent: "center", // Center horizontally
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
// Remove background/gap if you want only the selector to be visible
|
// Remove background/gap if you want only the selector to be visible
|
||||||
background: "none",
|
background: "none",
|
||||||
borderRadius: "0",
|
borderRadius: "0",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Custom Stack Dropdown */}
|
{/* Custom Stack Dropdown */}
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<button
|
<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}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
bottom: "110%",
|
|
||||||
background: "#222",
|
background: "#222",
|
||||||
|
color: "#fff",
|
||||||
border: "1px solid #444",
|
border: "1px solid #444",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
padding: "2px 8px",
|
||||||
zIndex: 100,
|
fontSize: "13px",
|
||||||
minWidth: 180,
|
marginRight: "6px",
|
||||||
maxHeight: 220,
|
minWidth: 120,
|
||||||
overflowY: "auto",
|
textAlign: "left",
|
||||||
overscrollBehavior: "contain",
|
cursor: "pointer",
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
|
||||||
|
//onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={openDropdownIdx === i}
|
||||||
>
|
>
|
||||||
{availableStacks.map((stackObj, idx) => {
|
{/* Show current stack with indicator */}
|
||||||
const thisUID = stackObj.studyInstanceUID;
|
{(() => {
|
||||||
|
const stackObj = availableStacks[selectedStackIdx[i] || 0];
|
||||||
|
const thisUID = stackObj?.studyInstanceUID;
|
||||||
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
|
const sameStudy = referenceUID && thisUID && referenceUID === thisUID;
|
||||||
const dotColor = sameStudy ? "#4caf50" : "#888";
|
const dotColor = sameStudy ? "#4caf50" : "#888";
|
||||||
const isEmpty = stackObj.imageIds.length === 0;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<span>
|
||||||
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={{
|
<span style={{
|
||||||
color: dotColor,
|
color: dotColor,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
marginRight: 8,
|
marginRight: 6,
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
verticalAlign: "middle",
|
verticalAlign: "middle",
|
||||||
}}>●</span>
|
}}>●</span>
|
||||||
Stack {idx + 1} ({stackObj.imageIds.length} images
|
Stack {selectedStackIdx[i] + 1} ({stackObj?.imageIds.length || 0} images)
|
||||||
{isEmpty && <span style={{ color: "#f44336", marginLeft: 6 }}>(empty)</span>})
|
</span>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})()}
|
||||||
</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>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1850,16 +2016,16 @@ for (let i = 0; i < numVisible; i++) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// App root
|
// App root
|
||||||
<div style={{
|
<div style={{
|
||||||
position: "absolute",
|
position: "relative",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
background: "#222",
|
background: "#222",
|
||||||
padding: "12px",
|
padding: "12px",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
{/* Side menu toggle button */}
|
{/* Side menu toggle button */}
|
||||||
<button
|
<button
|
||||||
@@ -1888,175 +2054,175 @@ for (let i = 0; i < numVisible; i++) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Side menu drawer */}
|
{/* Side menu drawer */}
|
||||||
{sideMenuOpen && (
|
{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",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
position: "absolute",
|
||||||
minHeight: 0,
|
top: 0,
|
||||||
overflowY: "auto", // <-- enables scrolling for menu content
|
left: 0,
|
||||||
display: "flex",
|
width: 220,
|
||||||
|
height: "100%",
|
||||||
|
background: "#222",
|
||||||
|
color: "#fff",
|
||||||
|
zIndex: 199,
|
||||||
|
boxShadow: "2px 0 12px rgba(0,0,0,0.3)",
|
||||||
flexDirection: "column",
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "18px",
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
overflowY: "auto", // <-- enables scrolling for menu content
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
gap: "8px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<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
|
<input
|
||||||
type="checkbox"
|
ref={fileInputRef}
|
||||||
id="orderBySliceLocation"
|
type="file"
|
||||||
checked={orderBySliceLocation}
|
style={{ display: "none" }}
|
||||||
onChange={e => setOrderBySliceLocation(e.target.checked)}
|
multiple
|
||||||
style={{ accentColor: "#666" }}
|
onChange={handleFilesSelected}
|
||||||
|
accept=".dcm,application/dicom"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="orderBySliceLocation" style={{ fontSize: "15px", cursor: "pointer" }}>
|
<div
|
||||||
Order by Slice Location
|
style={{
|
||||||
</label>
|
marginBottom: "18px",
|
||||||
</div>
|
display: "flex",
|
||||||
<button
|
alignItems: "center",
|
||||||
style={{
|
gap: "8px",
|
||||||
padding: "10px 18px",
|
}}
|
||||||
background: "#333",
|
>
|
||||||
color: "#fff",
|
<input
|
||||||
border: "none",
|
type="checkbox"
|
||||||
borderRadius: "4px",
|
id="orderBySliceLocation"
|
||||||
fontWeight: "bold",
|
checked={orderBySliceLocation}
|
||||||
cursor: "pointer",
|
onChange={e => setOrderBySliceLocation(e.target.checked)}
|
||||||
fontSize: "15px",
|
style={{ accentColor: "#666" }}
|
||||||
marginBottom: "18px",
|
/>
|
||||||
width: "100%",
|
<label htmlFor="orderBySliceLocation" style={{ fontSize: "15px", cursor: "pointer" }}>
|
||||||
}}
|
Order by Slice Location
|
||||||
onClick={handleShowMetadata}
|
</label>
|
||||||
>
|
</div>
|
||||||
Show DICOM Metadata
|
<button
|
||||||
</button>
|
style={{
|
||||||
<button
|
padding: "10px 18px",
|
||||||
style={{
|
background: "#333",
|
||||||
padding: "10px 18px",
|
color: "#fff",
|
||||||
background: crossHairsEnabled ? "#1976d2" : "#333",
|
border: "none",
|
||||||
color: "#fff",
|
borderRadius: "4px",
|
||||||
border: "none",
|
fontWeight: "bold",
|
||||||
borderRadius: "4px",
|
cursor: "pointer",
|
||||||
fontWeight: "bold",
|
fontSize: "15px",
|
||||||
cursor: "pointer",
|
marginBottom: "18px",
|
||||||
fontSize: "15px",
|
width: "100%",
|
||||||
marginBottom: "18px",
|
}}
|
||||||
width: "100%",
|
onClick={handleShowMetadata}
|
||||||
}}
|
>
|
||||||
onClick={() => setCrossHairsEnabled((prev) => !prev)}
|
Show DICOM Metadata
|
||||||
>
|
</button>
|
||||||
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
|
<button
|
||||||
</button>
|
style={{
|
||||||
<button
|
padding: "10px 18px",
|
||||||
style={{
|
background: crossHairsEnabled ? "#1976d2" : "#333",
|
||||||
padding: "10px 18px",
|
color: "#fff",
|
||||||
background: referenceLinesEnabled ? "#1976d2" : "#333",
|
border: "none",
|
||||||
color: "#fff",
|
borderRadius: "4px",
|
||||||
border: "none",
|
fontWeight: "bold",
|
||||||
borderRadius: "4px",
|
cursor: "pointer",
|
||||||
fontWeight: "bold",
|
fontSize: "15px",
|
||||||
cursor: "pointer",
|
marginBottom: "18px",
|
||||||
fontSize: "15px",
|
width: "100%",
|
||||||
marginBottom: "18px",
|
}}
|
||||||
width: "100%",
|
onClick={() => setCrossHairsEnabled((prev) => !prev)}
|
||||||
}}
|
>
|
||||||
onClick={() => setReferenceLinesEnabled((prev) => !prev)}
|
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
|
||||||
>
|
</button>
|
||||||
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
|
<button
|
||||||
</button>
|
style={{
|
||||||
<button
|
padding: "10px 18px",
|
||||||
onClick={sortViewportImageIdsBySliceLocation}
|
background: referenceLinesEnabled ? "#1976d2" : "#333",
|
||||||
style={{ margin: 8, padding: 8 }}
|
color: "#fff",
|
||||||
>
|
border: "none",
|
||||||
Sort All Viewports by Slice Location
|
borderRadius: "4px",
|
||||||
</button>
|
fontWeight: "bold",
|
||||||
<button
|
cursor: "pointer",
|
||||||
style={{
|
fontSize: "15px",
|
||||||
padding: "10px 18px",
|
marginBottom: "18px",
|
||||||
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
|
width: "100%",
|
||||||
color: "#fff",
|
}}
|
||||||
border: "none",
|
onClick={() => setReferenceLinesEnabled((prev) => !prev)}
|
||||||
borderRadius: "4px",
|
>
|
||||||
fontWeight: "bold",
|
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
|
||||||
cursor: "pointer",
|
</button>
|
||||||
fontSize: "15px",
|
<button
|
||||||
marginBottom: "18px",
|
onClick={sortViewportImageIdsBySliceLocation}
|
||||||
width: "100%",
|
style={{ margin: 8, padding: 8 }}
|
||||||
}}
|
>
|
||||||
onClick={() => setAltEnablesReferenceCursors((prev) => !prev)}
|
Sort All Viewports by Slice Location
|
||||||
>
|
</button>
|
||||||
{altEnablesReferenceCursors
|
<button
|
||||||
? "Disable Alt Key for Reference Cursors"
|
style={{
|
||||||
: "Enable Alt Key for Reference Cursors"}
|
padding: "10px 18px",
|
||||||
</button>
|
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
|
||||||
{/* Add more menu items here if needed */}
|
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>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* Viewport grid menu at top center */}
|
{/* Viewport grid menu at top center */}
|
||||||
|
|
||||||
@@ -2515,5 +2681,5 @@ function setupTools(MAIN_TOOL_GROUP_ID) {
|
|||||||
mainToolGroup.addTool(ReferenceCursors.toolName);
|
mainToolGroup.addTool(ReferenceCursors.toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mainToolGroup };
|
return mainToolGroup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,15 @@ containers.forEach((container) => {
|
|||||||
imageStacks = () => Promise.resolve([[]]);
|
imageStacks = () => Promise.resolve([[]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read autoCacheStack from data-auto-cache-stack attribute
|
||||||
|
const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack');
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user