Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd3020f0c | ||
|
|
313c7a9e9a | ||
|
|
40be13b51a | ||
|
|
4875b95ab2 | ||
|
|
c7728bf1a2 | ||
|
|
c90c280255 |
+53
-1
@@ -7,7 +7,59 @@
|
|||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root" style="max-height:500px; height:500px; overflow:auto;"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
|
<input type="file" id="dicomInput" multiple />
|
||||||
|
<button onclick="sendToViewer()">Send to Viewer</button>
|
||||||
|
<button onclick="testLoadWadouriStack()">Test Load wadouri Stack</button>
|
||||||
|
<script>
|
||||||
|
function sendToViewer() {
|
||||||
|
const files = document.getElementById('dicomInput').files;
|
||||||
|
if (window.loadDicomStackFromFiles) {
|
||||||
|
window.loadDicomStackFromFiles(files);
|
||||||
|
} else {
|
||||||
|
alert("Viewer API not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testLoadWadouriStack() {
|
||||||
|
const urls = [
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_757_XQQyovo.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_766_8fWI0Qc.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_774_c8IpxUu.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_758_6WKI7ws.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_767_TduVOb8.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_775_7zWneQ0.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_759_BtKzgS5.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_768_RdvtM8K.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_776_LhVY3Ty.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_760_a8CiR4J.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_769_YHyJ1l6.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_777_8IFYJmL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_761_vyvO1EN.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_770_vQwi4LW.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_778_BwGAMOR.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_762_DhSgQsm.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_771_xifitCl.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_779_LyA1zYn.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_763_aVjff3H.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_772_cDQZtxQ.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_780_qaZqsqg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_764_dDoeldL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_773_93Hvedg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_781_bEx9Owg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_765_qmd5mYJ.dcm"
|
||||||
|
];
|
||||||
|
const wadouriList = urls.map(url => "wadouri:" + url);
|
||||||
|
if (window.loadDicomStackFromWadouriList) {
|
||||||
|
window.loadDicomStackFromWadouriList(wadouriList);
|
||||||
|
} else {
|
||||||
|
alert("Viewer API not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+564
-54
@@ -9,7 +9,6 @@ import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums,
|
|||||||
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools"
|
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools"
|
||||||
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
|
|
||||||
import { segmentation, Enums as csEnums } from '@cornerstonejs/tools';
|
import { segmentation, Enums as csEnums } from '@cornerstonejs/tools';
|
||||||
import { voi } from "@cornerstonejs/tools/utilities"
|
import { voi } from "@cornerstonejs/tools/utilities"
|
||||||
import { readDicomElementExplicit } from "dicom-parser"
|
import { readDicomElementExplicit } from "dicom-parser"
|
||||||
@@ -18,9 +17,48 @@ import { metaData } from '@cornerstonejs/core';
|
|||||||
import * as dicomParser from "dicom-parser";
|
import * as dicomParser from "dicom-parser";
|
||||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
||||||
|
|
||||||
|
// Extend the Window interface to include cornerstoneTools
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
cornerstoneTools: typeof cornerstoneTools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to global types:
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
exportViewerState?: () => string;
|
||||||
|
importViewerState?: (json: string) => void;
|
||||||
|
exportAnnotations?: () => string;
|
||||||
|
importAnnotations?: (json: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StackEntry = { imageIds: string[], studyInstanceUID?: string };
|
||||||
|
|
||||||
|
// Add this type for clarity
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
loadDicomStackFromFiles?: (files: FileList | File[]) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
loadDicomStackFromWadouriList?: (wadouriImageIds: string[]) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.cornerstoneTools = cornerstoneTools;
|
window.cornerstoneTools = cornerstoneTools;
|
||||||
|
|
||||||
|
|
||||||
|
type ViewportDiv = HTMLDivElement & {
|
||||||
|
_cornerstoneEnabled?: boolean;
|
||||||
|
_lastViewportType?: any;
|
||||||
|
_resizeObserver?: ResizeObserver;
|
||||||
|
_overlayListener?: EventListener;
|
||||||
|
};
|
||||||
|
|
||||||
const { MouseBindings, KeyboardBindings } = csToolsEnums;
|
const { MouseBindings, KeyboardBindings } = csToolsEnums;
|
||||||
|
|
||||||
const { IMAGE_RENDERED } = Enums.Events;
|
const { IMAGE_RENDERED } = Enums.Events;
|
||||||
@@ -88,6 +126,8 @@ function App() {
|
|||||||
|
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [fullscreenHoverIdx, setFullscreenHoverIdx] = useState<number | null>(null);
|
||||||
|
const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
|
||||||
// State for overlay info
|
// State for overlay info
|
||||||
const [imageInfo, setImageInfo] = useState({
|
const [imageInfo, setImageInfo] = useState({
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -100,7 +140,10 @@ function App() {
|
|||||||
const renderingEngineId = "mainRenderingEngine";
|
const renderingEngineId = "mainRenderingEngine";
|
||||||
const renderingEngineRef = useRef<any>(null);
|
const renderingEngineRef = useRef<any>(null);
|
||||||
|
|
||||||
const [availableStacks, setAvailableStacks] = useState<string[][]>([]);
|
const restoringStateRef = useRef(false);
|
||||||
|
|
||||||
|
|
||||||
|
const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
|
||||||
const [activeViewport, setActiveViewport] = useState<number | null>(0);
|
const [activeViewport, setActiveViewport] = useState<number | null>(0);
|
||||||
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
|
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
|
||||||
() => Array(MAX_VIEWPORTS).fill([])
|
() => Array(MAX_VIEWPORTS).fill([])
|
||||||
@@ -109,20 +152,49 @@ function App() {
|
|||||||
() => Array(MAX_VIEWPORTS).fill(0)
|
() => Array(MAX_VIEWPORTS).fill(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Load available stacks on mount
|
// Load available stacks on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
availableImageIds().then(stacks => {
|
availableImageIds().then(async stacks => {
|
||||||
setAvailableStacks(stacks);
|
// Try to extract StudyInstanceUID for each stack
|
||||||
// Optionally initialize each viewport with the first stack
|
const stackEntries: StackEntry[] = await Promise.all(
|
||||||
|
stacks.map(async imageIds => {
|
||||||
|
let studyInstanceUID: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
// Try to fetch the first image as a blob and parse it
|
||||||
|
const firstId = imageIds[0];
|
||||||
|
if (firstId && firstId.startsWith("wadouri:")) {
|
||||||
|
const url = firstId.slice(8);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
|
studyInstanceUID = dataSet.string('x0020000d');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors, fallback to undefined
|
||||||
|
}
|
||||||
|
return { imageIds, studyInstanceUID };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setAvailableStacks(stackEntries);
|
||||||
|
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
|
||||||
setLoadedImageIdsAndCacheMeta(stacks[0]);
|
setLoadedImageIdsAndCacheMeta(stacks[0]);
|
||||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
|
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||||
const stack = availableStacks[stackIdx];
|
const stackObj = availableStacks[stackIdx];
|
||||||
|
if (!stackObj) return;
|
||||||
|
const stack = stackObj.imageIds;
|
||||||
|
|
||||||
|
// Always revert to stack mode when loading a new stack
|
||||||
|
setViewportModes(prev => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[viewportIdx] = 'stack';
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
if (!stack) return;
|
|
||||||
setViewportImageIds(prev => {
|
setViewportImageIds(prev => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
next[viewportIdx] = stack;
|
next[viewportIdx] = stack;
|
||||||
@@ -142,6 +214,7 @@ function App() {
|
|||||||
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
|
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
|
||||||
|
|
||||||
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
|
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
|
||||||
|
const [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false);
|
||||||
|
|
||||||
const sortViewportImageIdsBySliceLocation = () => {
|
const sortViewportImageIdsBySliceLocation = () => {
|
||||||
setViewportImageIds(prev => {
|
setViewportImageIds(prev => {
|
||||||
@@ -194,7 +267,10 @@ function App() {
|
|||||||
if (url.startsWith("blob:") || url.startsWith("/")) {
|
if (url.startsWith("blob:") || url.startsWith("/")) {
|
||||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||||
// If load returns a promise, collect it
|
// If load returns a promise, collect it
|
||||||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
// @ts-ignore
|
||||||
|
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(
|
||||||
|
url,
|
||||||
|
);
|
||||||
if (result && typeof result.then === "function") {
|
if (result && typeof result.then === "function") {
|
||||||
loadPromises.push(result);
|
loadPromises.push(result);
|
||||||
}
|
}
|
||||||
@@ -222,7 +298,7 @@ function App() {
|
|||||||
const [viewportMenuOpen, setViewportMenuOpen] = useState(false)
|
const [viewportMenuOpen, setViewportMenuOpen] = useState(false)
|
||||||
|
|
||||||
// Store refs for each viewport
|
// Store refs for each viewport
|
||||||
const viewportRefs = useRef<Array<HTMLDivElement | null>>([])
|
const viewportRefs = useRef<Array<ViewportDiv | null>>([]);
|
||||||
|
|
||||||
// Store the properties and scroll position of existing viewports before grid change
|
// Store the properties and scroll position of existing viewports before grid change
|
||||||
const prevViewportProps = useRef<Record<number, any>>({});
|
const prevViewportProps = useRef<Record<number, any>>({});
|
||||||
@@ -358,6 +434,21 @@ function App() {
|
|||||||
setMetaModalOpen(true);
|
setMetaModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Inside your App component:
|
||||||
|
useEffect(() => {
|
||||||
|
window.loadDicomStackFromFiles = (files: FileList | File[]) => {
|
||||||
|
// Convert FileList to array if needed
|
||||||
|
const fileArray = Array.from(files);
|
||||||
|
// Your logic to handle the files and load them as a stack
|
||||||
|
// For example, call your existing handler:
|
||||||
|
handleFilesSelected({ target: { files: fileArray } } as any);
|
||||||
|
};
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
window.loadDicomStackFromFiles = undefined;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [altPressed, setAltPressed] = useState(false);
|
const [altPressed, setAltPressed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -463,23 +554,38 @@ function App() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// Handler for file input change
|
// Handler for file input change
|
||||||
const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement> | { target: { files: File[] | FileList } }) => {
|
||||||
const files = event.target.files;
|
const files = event.target.files;
|
||||||
if (!files || files.length === 0) return;
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
// Reset order by slice location when new files are loaded
|
|
||||||
setOrderBySliceLocation(false);
|
setOrderBySliceLocation(false);
|
||||||
|
|
||||||
// Convert FileList to Array and sort by name (optional, for series order)
|
|
||||||
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
|
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
let studyInstanceUID: string | undefined = undefined;
|
||||||
|
const arrayBuffer = await fileArray[0].arrayBuffer();
|
||||||
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
|
studyInstanceUID = dataSet.string('x0020000d');
|
||||||
|
console.log("StudyInstanceUID:", studyInstanceUID);
|
||||||
|
|
||||||
// Create imageIds for each file using the cornerstone-wado-image-loader file API
|
|
||||||
// (Assumes cornerstone-wado-image-loader is configured for file loading)
|
|
||||||
const imageIds = fileArray.map(
|
const imageIds = fileArray.map(
|
||||||
file => `wadouri:${URL.createObjectURL(file)}`
|
file => `wadouri:${URL.createObjectURL(file)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setAvailableStacks(prev => {
|
||||||
|
const next = [...prev, { imageIds, studyInstanceUID }];
|
||||||
|
setViewportImageIds(vpPrev => {
|
||||||
|
const vpNext = [...vpPrev];
|
||||||
|
vpNext[0] = imageIds;
|
||||||
|
return vpNext;
|
||||||
|
});
|
||||||
|
setSelectedStackIdx(selPrev => {
|
||||||
|
const selNext = [...selPrev];
|
||||||
|
selNext[0] = next.length - 1;
|
||||||
|
return selNext;
|
||||||
|
});
|
||||||
setLoadedImageIdsAndCacheMeta(imageIds);
|
setLoadedImageIdsAndCacheMeta(imageIds);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -541,6 +647,8 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
|
console.log("setup")
|
||||||
|
|
||||||
// 1. Initialize Cornerstone3D and tools ONCE
|
// 1. Initialize Cornerstone3D and tools ONCE
|
||||||
if (!running.current) {
|
if (!running.current) {
|
||||||
running.current = true;
|
running.current = true;
|
||||||
@@ -631,7 +739,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewportModes[i] === 'stack') {
|
if (viewportModes[i] === 'stack') {
|
||||||
if (imageIds) {
|
if (imageIds && !restoringStateRef.current) {
|
||||||
viewport.setStack(imageIds);
|
viewport.setStack(imageIds);
|
||||||
}
|
}
|
||||||
// CrosshairsTool is not applicable for stack viewports
|
// CrosshairsTool is not applicable for stack viewports
|
||||||
@@ -666,6 +774,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
viewport.render();
|
viewport.render();
|
||||||
|
|
||||||
|
if (!restoringStateRef.current) {
|
||||||
// Restore previous scroll position for existing viewports only
|
// Restore previous scroll position for existing viewports only
|
||||||
if (
|
if (
|
||||||
prevScrollIndices.current[i] !== undefined &&
|
prevScrollIndices.current[i] !== undefined &&
|
||||||
@@ -694,21 +803,23 @@ function App() {
|
|||||||
propsToRestore.colormap = colormap;
|
propsToRestore.colormap = colormap;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log("Restoring properties (timeout) for viewport", i, propsToRestore);
|
||||||
viewport.setProperties(propsToRestore);
|
viewport.setProperties(propsToRestore);
|
||||||
viewport.render()
|
viewport.render()
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Always (re-)attach overlay update for each viewport
|
// Always (re-)attach overlay update for each viewport
|
||||||
if (element._overlayListener) {
|
if ((element)._overlayListener) {
|
||||||
// Remove previous listener if present
|
// Remove previous listener if present
|
||||||
viewport.element.removeEventListener(IMAGE_RENDERED, element._overlayListener);
|
viewport.element.removeEventListener(IMAGE_RENDERED, (element)._overlayListener);
|
||||||
}
|
}
|
||||||
const overlayListener = () => updateOverlay(i, viewport);
|
const overlayListener = () => updateOverlay(i, viewport);
|
||||||
viewport.element.addEventListener(IMAGE_RENDERED, overlayListener);
|
viewport.element.addEventListener(IMAGE_RENDERED, overlayListener);
|
||||||
element._overlayListener = overlayListener;
|
(element)._overlayListener = overlayListener;
|
||||||
updateOverlay(i, viewport);
|
updateOverlay(i, viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,9 +874,9 @@ function App() {
|
|||||||
// Cleanup: disconnect ResizeObservers
|
// Cleanup: disconnect ResizeObservers
|
||||||
return () => {
|
return () => {
|
||||||
viewportRefs.current.forEach(el => {
|
viewportRefs.current.forEach(el => {
|
||||||
if (el && el._resizeObserver) {
|
if (el && (el)._resizeObserver) {
|
||||||
el._resizeObserver.disconnect();
|
(el)._resizeObserver.disconnect();
|
||||||
delete el._resizeObserver;
|
delete (el)._resizeObserver;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
@@ -780,6 +891,8 @@ function App() {
|
|||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (viewport && typeof viewport.resetCamera === "function") {
|
if (viewport && typeof viewport.resetCamera === "function") {
|
||||||
viewport.resetCamera();
|
viewport.resetCamera();
|
||||||
|
|
||||||
|
console.log("Restoring viewport properties for viewport", viewportIdx);
|
||||||
viewport.resetProperties(); // Resets window/level and other properties
|
viewport.resetProperties(); // Resets window/level and other properties
|
||||||
viewport.render();
|
viewport.render();
|
||||||
}
|
}
|
||||||
@@ -793,6 +906,7 @@ function App() {
|
|||||||
if (renderingEngine) {
|
if (renderingEngine) {
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (viewport && typeof viewport.setProperties === "function") {
|
if (viewport && typeof viewport.setProperties === "function") {
|
||||||
|
console.log(`Applying preset to viewport ${viewportIdx}: center=${center}, width=${width}`);
|
||||||
viewport.setProperties({
|
viewport.setProperties({
|
||||||
voiRange: {
|
voiRange: {
|
||||||
lower: center - width / 2,
|
lower: center - width / 2,
|
||||||
@@ -853,6 +967,57 @@ function App() {
|
|||||||
updateActiveReferenceLineViewport(mainToolGroup)
|
updateActiveReferenceLineViewport(mainToolGroup)
|
||||||
}, [activeViewport, referenceLinesEnabled]);
|
}, [activeViewport, referenceLinesEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.setAttribute("webkitdirectory", "true");
|
||||||
|
fileInputRef.current.setAttribute("directory", "true");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// API: Load a stack from a list of wadouri imageIds (strings)
|
||||||
|
window.loadDicomStackFromWadouriList = async (wadouriImageIds: string[]) => {
|
||||||
|
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
||||||
|
|
||||||
|
// Try to extract StudyInstanceUID from the first image if possible
|
||||||
|
let studyInstanceUID: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
const firstId = wadouriImageIds[0];
|
||||||
|
if (firstId && firstId.startsWith("wadouri:")) {
|
||||||
|
const url = firstId.slice(8);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
|
studyInstanceUID = dataSet.string('x0020000d');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors, fallback to undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvailableStacks(prev => {
|
||||||
|
const next = [...prev, { imageIds: wadouriImageIds, studyInstanceUID }];
|
||||||
|
setViewportImageIds(vpPrev => {
|
||||||
|
const vpNext = [...vpPrev];
|
||||||
|
vpNext[0] = wadouriImageIds;
|
||||||
|
return vpNext;
|
||||||
|
});
|
||||||
|
setSelectedStackIdx(selPrev => {
|
||||||
|
const selNext = [...selPrev];
|
||||||
|
selNext[0] = next.length - 1;
|
||||||
|
return selNext;
|
||||||
|
});
|
||||||
|
setLoadedImageIdsAndCacheMeta(wadouriImageIds);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.loadDicomStackFromWadouriList = undefined;
|
||||||
|
};
|
||||||
|
}, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderBySliceLocation) return;
|
if (!orderBySliceLocation) return;
|
||||||
|
|
||||||
@@ -906,6 +1071,180 @@ function App() {
|
|||||||
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
|
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
// Export the current viewer state as JSON
|
||||||
|
window.exportViewerState = () => {
|
||||||
|
const numActive = viewportGrid.rows * viewportGrid.cols;
|
||||||
|
const modes = viewportModes.slice(0, numActive);
|
||||||
|
const stackIdx = selectedStackIdx.slice(0, numActive);
|
||||||
|
const stacks = availableStacks.map(s => ({
|
||||||
|
i: s.imageIds, // imageIds
|
||||||
|
u: s.studyInstanceUID // studyInstanceUID
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Only keep minimal properties
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
const props: any[] = [];
|
||||||
|
const stackPos: number[] = [];
|
||||||
|
const cam: any[] = [];
|
||||||
|
for (let i = 0; i < numActive; i++) {
|
||||||
|
let p = null, s = null, c = null;
|
||||||
|
if (renderingEngine) {
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
if (viewport) {
|
||||||
|
if (typeof viewport.getProperties === "function") {
|
||||||
|
const { voiRange, invert, interpolationType, VOILUTFunction, colormap } = viewport.getProperties();
|
||||||
|
p = { voiRange, invert, interpolationType, VOILUTFunction, colormap };
|
||||||
|
}
|
||||||
|
if (typeof viewport.getCurrentImageIdIndex === "function") {
|
||||||
|
s = viewport.getCurrentImageIdIndex();
|
||||||
|
}
|
||||||
|
if (typeof viewport.getCamera === "function") {
|
||||||
|
c = viewport.getCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.push(p);
|
||||||
|
stackPos.push(s);
|
||||||
|
cam.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use short keys for compactness
|
||||||
|
const state = {
|
||||||
|
grid: viewportGrid,
|
||||||
|
modes,
|
||||||
|
stacks,
|
||||||
|
stackIdx,
|
||||||
|
stackPos,
|
||||||
|
props,
|
||||||
|
cam,
|
||||||
|
};
|
||||||
|
return JSON.stringify(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import and restore a viewer state from JSON (using stack indices)
|
||||||
|
window.importViewerState = (json: string) => {
|
||||||
|
try {
|
||||||
|
restoringStateRef.current = true;
|
||||||
|
const state = typeof json === "string" ? JSON.parse(json) : json;
|
||||||
|
if (state.grid) setViewportGrid(state.grid);
|
||||||
|
if (state.modes) setViewportModes(state.modes);
|
||||||
|
if (state.stacks) setAvailableStacks(state.stacks.map((s: any) => ({
|
||||||
|
imageIds: s.i,
|
||||||
|
studyInstanceUID: s.u,
|
||||||
|
})));
|
||||||
|
if (state.stackIdx && state.stacks) {
|
||||||
|
const reconstructed = state.stackIdx.map(
|
||||||
|
(idx: number) => state.stacks[idx]?.i || []
|
||||||
|
);
|
||||||
|
setViewportImageIds(reconstructed);
|
||||||
|
setSelectedStackIdx(state.stackIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
if (!renderingEngine) {
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const numActive = state.grid?.rows * state.grid?.cols || 1;
|
||||||
|
for (let i = 0; i < numActive; i++) {
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
if (!viewport) continue;
|
||||||
|
|
||||||
|
// Restore stack position
|
||||||
|
if (
|
||||||
|
state.stackPos &&
|
||||||
|
typeof state.stackPos[i] === "number" &&
|
||||||
|
typeof viewport.setImageIdIndex === "function"
|
||||||
|
) {
|
||||||
|
if (csUtilities && csUtilities.jumpToSlice) {
|
||||||
|
csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPos[i] });
|
||||||
|
} else {
|
||||||
|
viewport.setImageIdIndex(state.stackPos[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore properties
|
||||||
|
if (
|
||||||
|
state.props &&
|
||||||
|
state.props[i] &&
|
||||||
|
typeof viewport.setProperties === "function"
|
||||||
|
) {
|
||||||
|
viewport.setProperties(state.props[i]);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore camera
|
||||||
|
if (
|
||||||
|
state.cam &&
|
||||||
|
state.cam[i] &&
|
||||||
|
typeof viewport.setCamera === "function"
|
||||||
|
) {
|
||||||
|
viewport.setCamera(state.cam[i]);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
}, 300);
|
||||||
|
} catch (e) {
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
alert("Failed to import viewer state: " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export all annotations using cornerstoneTools.annotation.state.getAllAnnotations()
|
||||||
|
window.exportAnnotations = () => {
|
||||||
|
try {
|
||||||
|
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
|
||||||
|
return JSON.stringify(allAnnotations, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to export annotations: " + e);
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import and restore all annotations using cornerstoneTools.annotation.state.restoreAnnotations()
|
||||||
|
window.importAnnotations = (json: string) => {
|
||||||
|
try {
|
||||||
|
const annotationData = typeof json === "string" ? JSON.parse(json) : json;
|
||||||
|
cornerstoneTools.annotation.state.restoreAnnotations(annotationData);
|
||||||
|
if (renderingEngineRef.current) {
|
||||||
|
renderingEngineRef.current.render();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to import annotations: " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.exportViewerState = undefined;
|
||||||
|
window.importViewerState = undefined;
|
||||||
|
window.exportAnnotations = undefined;
|
||||||
|
window.importAnnotations = undefined;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
viewportGrid,
|
||||||
|
viewportModes,
|
||||||
|
selectedStackIdx,
|
||||||
|
availableStacks,
|
||||||
|
viewportImageIds,
|
||||||
|
setViewportGrid,
|
||||||
|
setViewportModes,
|
||||||
|
setSelectedStackIdx,
|
||||||
|
setAvailableStacks,
|
||||||
|
setViewportImageIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Reference stack and UID for study comparison (use active viewport)
|
||||||
|
const referenceStackIdx = activeViewport !== null ? selectedStackIdx[activeViewport] : 0;
|
||||||
|
const referenceStackObj = availableStacks[referenceStackIdx];
|
||||||
|
const referenceUID = referenceStackObj?.studyInstanceUID;
|
||||||
|
|
||||||
const viewports = [];
|
const viewports = [];
|
||||||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||||||
@@ -1047,6 +1386,52 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Fullscreen button below the Volume/Stack toggle */}
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 40,
|
||||||
|
left: 8,
|
||||||
|
zIndex: 20,
|
||||||
|
background: "#111",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "13px",
|
||||||
|
opacity: 0.85,
|
||||||
|
marginTop: "4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
}}
|
||||||
|
title="Toggle Fullscreen"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const el = viewportRefs.current[i];
|
||||||
|
if (document.fullscreenElement === el) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (el?.requestFullscreen) {
|
||||||
|
el.requestFullscreen();
|
||||||
|
} else if ((el as any)?.webkitRequestFullscreen) {
|
||||||
|
(el as any).webkitRequestFullscreen();
|
||||||
|
} else if ((el as any)?.msRequestFullscreen) {
|
||||||
|
(el as any).msRequestFullscreen();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setFullscreenHoverIdx(i)}
|
||||||
|
onMouseLeave={() => setFullscreenHoverIdx(null)}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: "18px", lineHeight: 1 }}>⛶</span>
|
||||||
|
{fullscreenHoverIdx === i && (
|
||||||
|
<span style={{ marginLeft: 6 }}>
|
||||||
|
{document.fullscreenElement === viewportRefs.current[i]
|
||||||
|
? "Exit Fullscreen"
|
||||||
|
: "Fullscreen"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
{/* Overlay info (bottom left of viewport) */}
|
{/* Overlay info (bottom left of viewport) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -1282,9 +1667,9 @@ function App() {
|
|||||||
padding: "2px 8px",
|
padding: "2px 8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<select
|
{/* Custom Stack Dropdown */}
|
||||||
value={selectedStackIdx[i] || 0}
|
<div style={{ position: "relative" }}>
|
||||||
onChange={e => handleLoadStack(i, Number(e.target.value))}
|
<button
|
||||||
style={{
|
style={{
|
||||||
background: "#222",
|
background: "#222",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
@@ -1293,29 +1678,127 @@ function App() {
|
|||||||
padding: "2px 8px",
|
padding: "2px 8px",
|
||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
marginRight: "6px",
|
marginRight: "6px",
|
||||||
}}
|
minWidth: 120,
|
||||||
>
|
textAlign: "left",
|
||||||
{availableStacks.map((stack, idx) => (
|
|
||||||
<option key={idx} value={idx}>
|
|
||||||
Stack {idx + 1} ({stack.length} images)
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
background: "#1976d2",
|
|
||||||
color: "#fff",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "3px",
|
|
||||||
padding: "2px 12px",
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontSize: "13px",
|
|
||||||
}}
|
}}
|
||||||
onClick={() => handleLoadStack(i, selectedStackIdx[i] || 0)}
|
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
|
||||||
disabled={!availableStacks.length}
|
//onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={openDropdownIdx === i}
|
||||||
>
|
>
|
||||||
Load Stack
|
{/* 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>
|
</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";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
role="option"
|
||||||
|
aria-selected={selectedStackIdx[i] === idx}
|
||||||
|
style={{
|
||||||
|
padding: "6px 12px",
|
||||||
|
background: selectedStackIdx[i] === idx ? "#444" : "#222",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
|
||||||
|
borderBottom: "1px solid #333",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
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)
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1328,8 +1811,8 @@ function App() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: "99vw",
|
width: "99vw",
|
||||||
height: "99vh",
|
height: "100%", // <-- changed from 500px
|
||||||
background: "#111",
|
maxHeight: "100%", // <-- ensures it doesn't overflow parent
|
||||||
padding: "12px", // <-- Add this line
|
padding: "12px", // <-- Add this line
|
||||||
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
|
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
|
||||||
background: "#222",
|
background: "#222",
|
||||||
@@ -1342,7 +1825,7 @@ function App() {
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
transform: "translateY(-50%)",
|
transform: "translateY(-50%)",
|
||||||
left: 20,
|
left: sideMenuOpen ? 250 : -20, // Slide right when open
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -1354,7 +1837,7 @@ function App() {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontSize: "22px",
|
fontSize: "22px",
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
||||||
transition: "background 0.2s",
|
transition: "left 0.25s cubic-bezier(.4,2,.6,1), background 0.2s",
|
||||||
}}
|
}}
|
||||||
onClick={() => setSideMenuOpen(open => !open)}
|
onClick={() => setSideMenuOpen(open => !open)}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
@@ -1367,7 +1850,7 @@ function App() {
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: sideMenuOpen ? 0 : -220,
|
left: sideMenuOpen ? 0 : -260,
|
||||||
width: 220,
|
width: 220,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
background: "#222",
|
background: "#222",
|
||||||
@@ -1377,7 +1860,18 @@ function App() {
|
|||||||
transition: "left 0.25s cubic-bezier(.4,2,.6,1)",
|
transition: "left 0.25s cubic-bezier(.4,2,.6,1)",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
padding: "24px 16px 16px 16px",
|
padding: "24px 16px 16px 16px",
|
||||||
display: sideMenuOpen ? "flex" : "none",
|
display: "flex",
|
||||||
|
overflow: "hidden", // <-- add this
|
||||||
|
pointerEvents: sideMenuOpen ? "auto" : "none", // <-- add this
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
overflowY: "auto", // <-- enables scrolling for menu content
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: "bold", fontSize: 18, marginBottom: 24 }}>
|
<div style={{ fontWeight: "bold", fontSize: 18, marginBottom: 24 }}>
|
||||||
@@ -1419,8 +1913,6 @@ function App() {
|
|||||||
type="file"
|
type="file"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
multiple
|
multiple
|
||||||
webkitdirectory="true"
|
|
||||||
directory="true"
|
|
||||||
onChange={handleFilesSelected}
|
onChange={handleFilesSelected}
|
||||||
accept=".dcm,application/dicom"
|
accept=".dcm,application/dicom"
|
||||||
/>
|
/>
|
||||||
@@ -1523,6 +2015,7 @@ function App() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Viewport grid menu at top center */}
|
{/* Viewport grid menu at top center */}
|
||||||
|
|
||||||
|
|
||||||
@@ -1620,7 +2113,7 @@ function App() {
|
|||||||
fontSize: "15px",
|
fontSize: "15px",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
saveVisibleViewportState(viewportGrid, renderingEngineRef.current);
|
saveVisibleViewportState();
|
||||||
setViewportGrid({ rows: opt.rows, cols: opt.cols })
|
setViewportGrid({ rows: opt.rows, cols: opt.cols })
|
||||||
setViewportMenuOpen(false)
|
setViewportMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
@@ -1804,8 +2297,24 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Expandable advanced ctrl+mouse bindings */}
|
{/* Separate button for advanced Ctrl+Mouse bindings */}
|
||||||
{showAdvancedMouseBindings && (
|
<div style={{ margin: "12px 0" }}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
background: "#333",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "4px 12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "14px",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}
|
||||||
|
onClick={() => setShowAdvancedCtrlMouseBindings(v => !v)}
|
||||||
|
>
|
||||||
|
{showAdvancedCtrlMouseBindings ? "Hide" : "Show"} Advanced Ctrl + Mouse Bindings
|
||||||
|
</button>
|
||||||
|
{showAdvancedCtrlMouseBindings && (
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
{ALL_MOUSE_BINDINGS.slice(3).map((btn) => (
|
{ALL_MOUSE_BINDINGS.slice(3).map((btn) => (
|
||||||
<div key={btn.value + "_ctrl"} style={{ marginBottom: 10 }}>
|
<div key={btn.value + "_ctrl"} style={{ marginBottom: 10 }}>
|
||||||
@@ -1832,6 +2341,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { api } from "dicomweb-client"
|
import { api } from "dicomweb-client"
|
||||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
||||||
|
|
||||||
import { readFile } from "dicom-parser"
|
|
||||||
/**
|
/**
|
||||||
/**
|
/**
|
||||||
* Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone,
|
* Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { api } from "dicomweb-client"
|
import { api } from "dicomweb-client"
|
||||||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
||||||
|
|
||||||
import { readFile } from "dicom-parser"
|
|
||||||
/**
|
/**
|
||||||
/**
|
/**
|
||||||
* Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone,
|
* Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone,
|
||||||
|
|||||||
Reference in New Issue
Block a user