From 1ddc9a810a9fc9c5121b5d62680b8be02ce057ef Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Jun 2026 09:50:55 +0100 Subject: [PATCH] update --- dicom-viewer/src/App.tsx | 111 ++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 50fe66a..8d2394f 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -7,7 +7,7 @@ import { } from "@cornerstonejs/core" import { init as csRenderInit } from "@cornerstonejs/core" import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool, utilities as csToolsUtilities } from "@cornerstonejs/tools" -import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools" +import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools" import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader" import * as cornerstoneTools from '@cornerstonejs/tools'; import * as cornerstone3d from '@cornerstonejs/core'; @@ -23,6 +23,26 @@ import { getLoggerConfig, logger, parseLogFormat, parseLogLevel, setLoggerConfig import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool"; import { render } from "@cornerstonejs/tools/tools/displayTools/Labelmap/labelmapDisplay" +const windowLevelToolPrototype = WindowLevelTool.prototype as any; + +if (!windowLevelToolPrototype._mouseSensitivityPatched) { + const originalGetNewRange = windowLevelToolPrototype.getNewRange; + windowLevelToolPrototype.getNewRange = function ({ deltaPointsCanvas, ...rest }: any) { + const sensitivity = typeof this?.configuration?.mouseSensitivity === "number" + ? this.configuration.mouseSensitivity + : 1; + const scaledDeltaPointsCanvas = Array.isArray(deltaPointsCanvas) + ? [deltaPointsCanvas[0] * sensitivity, deltaPointsCanvas[1] * sensitivity] + : deltaPointsCanvas; + + return originalGetNewRange.call(this, { + ...rest, + deltaPointsCanvas: scaledDeltaPointsCanvas, + }); + }; + windowLevelToolPrototype._mouseSensitivityPatched = true; +} + // Extend the Window interface to include cornerstoneTools @@ -152,7 +172,6 @@ const TOOL_OPTIONS = [ { label: "Rectangle ROI", value: RectangleROITool.toolName, icon: "▭" }, { label: "Ellipse ROI", value: EllipticalROITool.toolName, icon: "◯" }, { label: "Freehand ROI", value: PlanarFreehandROITool.toolName, icon: "✏️" }, - { label: "Freehand Contour Segmentation", value: PlanarFreehandContourSegmentationTool.toolName, icon: "🖊️" }, { label: "Sculptor", value: SculptorTool.toolName, icon: "🔧" }, { label: "Reference Cursors", value: ReferenceCursors.toolName, icon: "⦿" }, ]; @@ -433,6 +452,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat const MOUSE_SETTINGS_KEY = "dv3d_mouseToolBindings"; const CTRL_MOUSE_SETTINGS_KEY = "dv3d_ctrlMouseToolBindings"; + const WINDOW_LEVEL_SENSITIVITY_KEY = "dv3d_windowLevelMouseSensitivity"; const initialLogMinLevel = (initialLoggerConfig?.minLevel as LogLevel | undefined) ?? (initialLoggerConfig?.debug !== undefined ? (initialLoggerConfig.debug ? "debug" : "warn") : undefined) ?? getLoggerConfig().minLevel; @@ -2122,6 +2142,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat Auxiliary: ProbeTool.toolName, Secondary: NoLabelArrowAnnotateTool.toolName, }) + const [windowLevelMouseSensitivity, setWindowLevelMouseSensitivity] = useState(1) // Track if Ctrl is pressed const [ctrlPressed, setCtrlPressed] = useState(false) @@ -2302,6 +2323,13 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY); if (mouse) setMouseToolBindings(JSON.parse(mouse)); if (ctrl) setCtrlMouseToolBindings(JSON.parse(ctrl)); + const rawWindowLevelSensitivity = localStorage.getItem(WINDOW_LEVEL_SENSITIVITY_KEY); + if (rawWindowLevelSensitivity) { + const parsedSensitivity = JSON.parse(rawWindowLevelSensitivity); + if (typeof parsedSensitivity?.mouseSensitivity === "number") { + setWindowLevelMouseSensitivity(parsedSensitivity.mouseSensitivity); + } + } const rawLogging = localStorage.getItem(LOG_SETTINGS_KEY); if (rawLogging) { @@ -2351,6 +2379,15 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat } catch (e) { } }, [ctrlMouseToolBindings]); + useEffect(() => { + try { + localStorage.setItem( + WINDOW_LEVEL_SENSITIVITY_KEY, + JSON.stringify({ mouseSensitivity: windowLevelMouseSensitivity }) + ); + } catch (e) { } + }, [windowLevelMouseSensitivity]); + useEffect(() => { setLoggerConfig({ minLevel: loggingMinLevel, @@ -2479,7 +2516,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat 'RectangleROI', 'EllipticalROI', 'PlanarFreehandROI', - 'PlanarFreehandContourSegmentation', + 'Sculptor', // Add more if needed ].forEach(toolName => { mainToolGroup.setToolConfiguration(toolName, { @@ -2604,6 +2641,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat toolGroup.setToolActive(tool, { bindings }); }); + if (typeof toolGroup.setToolConfiguration === "function") { + toolGroup.setToolConfiguration(WindowLevelTool.toolName, { + mouseSensitivity: windowLevelMouseSensitivity, + }); + } + // Add touch bindings so tools are usable on phones/tablets. const primaryTool = mouseToolBindings.Primary; if (primaryTool) { @@ -2652,7 +2695,11 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat } }); - }, [mouseToolBindings, ctrlMouseToolBindings]); + }, [mouseToolBindings, ctrlMouseToolBindings, windowLevelMouseSensitivity]); + + useEffect(() => { + applyMouseToolBindings(); + }, [windowLevelMouseSensitivity, applyMouseToolBindings]); const [sideMenuOpen, setSideMenuOpen] = useState(false); const [folderLoadMode, setFolderLoadMode] = useState<'add' | 'replace'>('add'); @@ -3872,22 +3919,6 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat cancelAnimationFrame(raf); }; }, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds]); - - - // Handler to reset the view (camera and window/level) - const handleDoubleClick = useCallback((viewportIdx: number) => { - const viewportId = `CT_${viewportIdx}`; - const renderingEngine = renderingEngineRef.current; - const viewport = renderingEngine.getViewport(viewportId); - if (viewport && typeof viewport.resetCamera === "function") { - viewport.resetCamera(); - - appLogger.debug("Restoring viewport properties for viewport", viewportIdx); - viewport.resetProperties(); // Resets window/level and other properties - viewport.render(); - } - }, []) - // Handler to apply a preset for a specific viewport const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => { const viewportId = `CT_${viewportIdx}`; @@ -4060,6 +4091,20 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat }); }, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]); + // Handler to reset the view (camera and window/level) + const handleDoubleClick = useCallback((viewportIdx: number) => { + const viewportId = `CT_${viewportIdx}`; + const renderingEngine = renderingEngineRef.current; + const viewport = renderingEngine.getViewport(viewportId); + if (viewport && typeof viewport.resetCamera === "function") { + viewport.resetCamera(); + + appLogger.debug("Restoring default window/level for viewport", viewportIdx); + applyDefaultWindowLevel(viewportIdx); + viewport.render(); + } + }, [applyDefaultWindowLevel]) + const applyPresetOption = useCallback((viewportIdx: number, preset: PresetOption, persistSelection = true, resetCameraOnReset = true) => { if (persistSelection) { activePresetByViewportRef.current[viewportIdx] = preset; @@ -5058,7 +5103,6 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat RectangleROITool.toolName, EllipticalROITool.toolName, PlanarFreehandROITool.toolName, - PlanarFreehandContourSegmentationTool.toolName, ProbeTool.toolName, SculptorTool.toolName, ]; @@ -5407,6 +5451,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat || !mobileInteractionActive || !mobileSingleViewportMode || mobileFocusedViewport === i; + const hasCineableStack = viewportModes[i] === "stack" && ((imageInfos[i]?.total || 0) > 1 || (imageInfos[i]?.totalFrames || 0) > 1); const currentPreviewZone = dropPreview?.viewportIdx === i ? dropPreview.zone : null; const resolvedPreviewZone = currentPreviewZone ? resolveDropZone(currentPreviewZone) : null; @@ -5764,6 +5809,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat )} + {hasCineableStack && ( + )} {viewportModes[i] === "stack" && annotationNavEnabled && (() => { // Get all annotations for this stack const imageIds = viewportImageIds[i]; @@ -6166,7 +6213,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat )} - {viewportModes[i] === "stack" && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1 || (imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames)) && ( + {hasCineableStack && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1 || (imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames)) && (
e.stopPropagation()} > - {cineControlsOpenByViewport[i] && imageInfos[i]?.total > 1 && ( + {cineControlsOpenByViewport[i] && hasCineableStack && ( <>
)} +
Window/Level Mouse Sensitivity
+ {/* Ctrl + Mouse Button Tool Bindings */}
Ctrl + Mouse Button Tool Bindings
{MOUSE_BUTTONS.map((btn) => ( @@ -8088,7 +8149,6 @@ function setupTools(MAIN_TOOL_GROUP_ID) { cornerstoneTools.addTool(RectangleROITool) cornerstoneTools.addTool(EllipticalROITool) cornerstoneTools.addTool(PlanarFreehandROITool) - cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool) cornerstoneTools.addTool(SculptorTool) cornerstoneTools.addTool(ReferenceCursors) cornerstoneTools.addTool(CrosshairsTool) @@ -8113,7 +8173,6 @@ function setupTools(MAIN_TOOL_GROUP_ID) { mainToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' }); mainToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' }); mainToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false }); - mainToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {}); mainToolGroup.addTool(SculptorTool.toolName, {}); //mainToolGroup.addTool(CrosshairsTool.toolName); mainToolGroup.addTool(ReferenceLinesTool.toolName);