diff --git a/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/addManipulationBindings.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/addManipulationBindings.ts new file mode 100644 index 0000000..92ff237 --- /dev/null +++ b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/addManipulationBindings.ts @@ -0,0 +1,185 @@ +import * as cornerstoneTools from '@cornerstonejs/tools'; +import type { Types } from '@cornerstonejs/tools'; + +const { + LengthTool, + StackScrollMouseWheelTool, + StackScrollTool, + PanTool, + ZoomTool, + TrackballRotateTool, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { MouseBindings, KeyboardBindings } = csToolsEnums; + +let registered = false; + +export type ToolBinding = { + // A base tool to register. Should only be defined once per tool + tool?: any; + // The tool name to base this on + baseTool?: string; + // The configuration to register with + configuration?: Record; + // Sets to passive initially + passive?: boolean; + // Initial bindings + bindings?: Types.IToolBinding[]; +}; + +/** + * Adds navigation bindings to the given tool group. Registers the basic + * tool with CS Tools if register is true. + * + * Adds: + * * Pan on Right or Primary+Ctrl + * * Zoom on Middle, Primary+Shift + * * Stack Scroll on Mouse Wheel, Primary+Alt + * * Length Tool on fourth button + * + * Also allows registering other tools by having them in the options.toolMap with configuration values. + */ +export default function addManipulationBindings( + toolGroup, + options: { + enableShiftClickZoom?: boolean; + is3DViewport?: boolean; + toolMap?: Map; + } = {} +) { + const zoomBindings: Types.IToolBinding[] = [ + { + mouseButton: MouseBindings.Secondary, + }, + ]; + + const { + is3DViewport = false, + enableShiftClickZoom = false, + toolMap = new Map(), + } = options; + + if (enableShiftClickZoom === true) { + zoomBindings.push({ + mouseButton: MouseBindings.Primary, // Shift Left Click + modifierKey: KeyboardBindings.Shift, + }); + } + + if (!registered) { + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(TrackballRotateTool); + cornerstoneTools.addTool(LengthTool); + cornerstoneTools.addTool(StackScrollTool); + for (const [, config] of toolMap) { + if (config.tool) { + cornerstoneTools.addTool(config.tool); + } + } + } + + registered = true; + + toolGroup.addTool(PanTool.toolName); + // Allow significant zooming to occur + toolGroup.addTool(ZoomTool.toolName, { + minZoomScale: 0.001, + maxZoomScale: 4000, + }); + if (is3DViewport) { + toolGroup.addTool(TrackballRotateTool.toolName); + } else { + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + } + toolGroup.addTool(LengthTool.toolName); + toolGroup.addTool(StackScrollTool.toolName); + + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + { + numTouchPoints: 1, + modifierKey: KeyboardBindings.Ctrl, + }, + ], + }); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: zoomBindings, + }); + // Need a binding to navigate without a wheel mouse + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.Alt, + }, + { + numTouchPoints: 1, + modifierKey: KeyboardBindings.Alt, + }, + ], + }); + // Add a length tool binding to allow testing annotations on examples targetting + // other use cases. Use a primary button with shift+ctrl as that is relatively + // unlikely to be otherwise used. + toolGroup.setToolActive(LengthTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.ShiftCtrl, + }, + { + numTouchPoints: 1, + modifierKey: KeyboardBindings.ShiftCtrl, + }, + ], + }); + + if (is3DViewport) { + toolGroup.setToolActive(TrackballRotateTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + }, + ], + }); + } else { + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + } + + // Add extra tools from the toolMap + for (const [toolName, config] of toolMap) { + if (config.baseTool) { + if (!toolGroup.hasTool(config.baseTool)) { + toolGroup.addTool( + config.baseTool, + toolMap.get(config.baseTool)?.configuration + ); + } + toolGroup.addToolInstance( + toolName, + config.baseTool, + config.configuration + ); + } else if (!toolGroup.hasTool(toolName)) { + toolGroup.addTool(toolName, config.configuration); + } + if (config.passive) { + // This can be applied during add/remove contours + toolGroup.setToolPassive(toolName); + } + if (config.bindings || config.selected) { + toolGroup.setToolActive( + toolName, + (config.bindings && config) || { + bindings: [{ mouseButton: MouseBindings.Primary }], + } + ); + } + } +} diff --git a/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts index e3c5510..1bc43e0 100644 --- a/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts +++ b/apps/desktop/src/pages/Viewer/MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor.ts @@ -1,5 +1,11 @@ +type VOIRange = { + /** upper value for display */ + upper: number; + /** lower value for display */ + lower: number; +}; -let ctVoiRange; +let ctVoiRange: VOIRange export interface CtTransferFunction { volumeActor: any diff --git a/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx index 87d9a80..2b82ee3 100644 --- a/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx +++ b/apps/desktop/src/pages/Viewer/MprViewer/Crosshair.tsx @@ -6,6 +6,7 @@ import { RenderingEngine, setVolumesForViewports, Enums as CoreEnums, + volumeLoader, } from "@cornerstonejs/core"; import { viewportColors, @@ -18,6 +19,8 @@ import { ViewportId, } from "./Crosshair.config"; +const viewportIds = [viewportId1, viewportId2, viewportId3]; + const { ToolGroupManager, CrosshairsTool, @@ -55,7 +58,7 @@ interface CrosshairMprProps { windowCenter: number; windowWidth: number; }; - volumeId: string; + imageIds: string[]; } export const CrosshairMpr = (props: CrosshairMprProps) => { @@ -64,156 +67,129 @@ export const CrosshairMpr = (props: CrosshairMprProps) => { const viewportRef_SAGITTAL = useRef(null); const viewportRef_CORONAL = useRef(null); const renderingEngine = useRef(); - const toolGroupRef = useRef( - undefined - ); - const ts = "-" + Date.now(); - const toolGroupId = "mprToolGroup" + ts; - const renderingEngineId = "mprRenderingEngine" + ts; + const volumeId = "volumeId"; + const toolGroupId = "mprToolGroup"; + const renderingEngineId = "mprRenderingEngine"; useEffect(() => { - cornerstoneTools.addTool(StackScrollMouseWheelTool); - cornerstoneTools.addTool(CrosshairsTool); - cornerstoneTools.addTool(WindowLevelTool); - cornerstoneTools.addTool(ZoomTool); - toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId); const run = async () => { if ( - !viewportRef_AXIAL.current || - !viewportRef_SAGITTAL.current || - !viewportRef_CORONAL.current - ) - return; + viewportRef_AXIAL.current && + viewportRef_SAGITTAL.current && + viewportRef_CORONAL.current + ) { + console.log("mpr rendering"); - renderingEngine.current = new RenderingEngine(renderingEngineId); + renderingEngine.current = new RenderingEngine(renderingEngineId); - // Create the viewports - const viewportInputArray: PublicViewportInput[] = [ - { - viewportId: viewportId1, - type: ViewportType.ORTHOGRAPHIC, - element: viewportRef_AXIAL.current, - defaultOptions: { - orientation: CoreEnums.OrientationAxis.AXIAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId2, - type: ViewportType.ORTHOGRAPHIC, - element: viewportRef_SAGITTAL.current, - defaultOptions: { - orientation: CoreEnums.OrientationAxis.SAGITTAL, - background: [0, 0, 0], - }, - }, - { - viewportId: viewportId3, - type: ViewportType.ORTHOGRAPHIC, - element: viewportRef_CORONAL.current, - defaultOptions: { - orientation: CoreEnums.OrientationAxis.CORONAL, - background: [0, 0, 0], - }, - }, - ]; - - renderingEngine.current.setViewports(viewportInputArray); - - // Set volumes on the viewports - await setVolumesForViewports( - renderingEngine.current, - [ + const viewportInputArray: PublicViewportInput[] = [ { - volumeId: props.volumeId, - callback: ({ volumeActor }) => - setCtTransferFunctionForVolumeActor({ - volumeActor, - defaultWindowCenter: props.wwwl.windowCenter, - defaultWindowWidth: props.wwwl.windowWidth, - }), + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: viewportRef_AXIAL.current, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.AXIAL, + background: [0, 0, 0], + }, }, - ], - [viewportId1, viewportId2, viewportId3] - ); - - if (toolGroupRef.current) { - // For the crosshairs to operate, the viewports must currently be - // added ahead of setting the tool active. This will be improved in the future. - toolGroupRef.current.addViewport(viewportId1, renderingEngineId); - toolGroupRef.current.addViewport(viewportId2, renderingEngineId); - toolGroupRef.current.addViewport(viewportId3, renderingEngineId); + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: viewportRef_SAGITTAL.current, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.SAGITTAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: viewportRef_CORONAL.current, + defaultOptions: { + orientation: CoreEnums.OrientationAxis.CORONAL, + background: [0, 0, 0], + }, + }, + ]; + renderingEngine.current.setViewports(viewportInputArray); /** - * zoom影像 + * bug here: WebGL: INVALID_OPERATION: bindTexture: object does not belong to this context */ - toolGroupRef.current.addTool(ZoomTool.toolName); - toolGroupRef.current.setToolActive(ZoomTool.toolName, { - bindings: [ + await setVolumesForViewports( + renderingEngine.current, + [ { - mouseButton: MouseBindings.Secondary, // 鼠标中键 + volumeId: volumeId, + callback: ({ volumeActor }) => + setCtTransferFunctionForVolumeActor({ + volumeActor, + defaultWindowCenter: props.wwwl.windowCenter, + defaultWindowWidth: props.wwwl.windowWidth, + }), }, ], - }); + viewportIds + ); - // Manipulation Tools - toolGroupRef.current.addTool(StackScrollMouseWheelTool.toolName); - // Add Crosshairs tool and configure it to link the three viewports - // These viewports could use different tool groups. See the PET-CT example - // for a more complicated used case. + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); - toolGroupRef.current.addTool(CrosshairsTool.toolName, { - getReferenceLineColor, - getReferenceLineControllable, - getReferenceLineDraggableRotatable, - getReferenceLineSlabThicknessControlsOn, - }); + if (toolGroup) { + // 为使十字准线正常工作,目前必须在将工具设置为活动之前添加视口 + toolGroup.addViewport(viewportId1, renderingEngineId); + toolGroup.addViewport(viewportId2, renderingEngineId); + toolGroup.addViewport(viewportId3, renderingEngineId); - toolGroupRef.current.setToolActive(CrosshairsTool.toolName, { - bindings: [{ mouseButton: 1 }], - }); - // As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` - // hook instead of mouse buttons, it does not need to assign any mouse button. - toolGroupRef.current.setToolActive(StackScrollMouseWheelTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + // 添加十字准线工具并将其配置为连接三个视口 + toolGroup.addTool(CrosshairsTool.toolName, { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }); + toolGroup.addTool(WindowLevelTool.toolName); - toolGroupRef.current.addTool(WindowLevelTool.toolName); - toolGroupRef.current.setToolActive(WindowLevelTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Auxiliary, - }, - ], - }); + toolGroup.setToolActive(CrosshairsTool.toolName, { + bindings: [{ mouseButton: 1 }], + }); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // 鼠标中键 + }, + ], + }); + // using the `mouseWheelCallback` + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + ], + }); + } + + renderingEngine.current.renderViewports(viewportIds); } - - renderingEngine.current.renderViewports([ - viewportId1, - viewportId2, - viewportId3, - ]); }; run(); return () => { - // 禁用视口 renderingEngine.current?.disableElement(viewportId1); renderingEngine.current?.disableElement(viewportId2); renderingEngine.current?.disableElement(viewportId3); - // 销毁渲染引擎 - renderingEngine.current?.destroy(); - // 从 ToolGroupManager 中移除工具组 ToolGroupManager.destroyToolGroup(toolGroupId); - // 移出工具注册 - cornerstoneTools.removeTool(StackScrollMouseWheelTool); - cornerstoneTools.removeTool(CrosshairsTool); - cornerstoneTools.removeTool(WindowLevelTool); - cornerstoneTools.removeTool(ZoomTool); + // 销毁渲染引擎 + renderingEngine.current?.destroy(); }; }, [props, renderingEngineId, toolGroupId]); @@ -238,8 +214,14 @@ export const CrosshairMpr = (props: CrosshairMprProps) => { return (
-
-
+
+
); diff --git a/apps/desktop/src/pages/Viewer/StackViewer/index.tsx b/apps/desktop/src/pages/Viewer/StackViewer/index.tsx index 1b41b5d..2759f62 100644 --- a/apps/desktop/src/pages/Viewer/StackViewer/index.tsx +++ b/apps/desktop/src/pages/Viewer/StackViewer/index.tsx @@ -1,22 +1,44 @@ import { useEffect, useRef, useState } from "react"; +import * as cornerstoneTools from "@cornerstonejs/tools"; import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js"; import { Enums as CoreEnums, RenderingEngine } from "@cornerstonejs/core"; import { IStackViewport } from "@cornerstonejs/core/dist/types/types"; import { Slider } from "@/components/ui/slider"; +import { ctVoiRange } from "../MprViewer/CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor"; +const { + ToolGroupManager, + WindowLevelTool, + ZoomTool, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; export interface StackViewerProps { imageIds: string[]; + wwwl: { + windowCenter: number; + windowWidth: number; + }; } export const StackViewer = (props: StackViewerProps) => { const viewportStackRef = useRef(null); const renderingEngineRef = useRef(); + /** + * 当前的index + */ + const [index, setIndex] = useState(Math.floor(props.imageIds.length / 2)); const containerRef = useRef(null); const viewportId = "stackViewport"; const renderingEngineId = "stackRenderingEngine"; + const toolGroupId = "stackToolGroup"; useEffect(() => { if (!viewportStackRef.current) return; + + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + renderingEngineRef.current = new RenderingEngine(renderingEngineId); const viewportInput: PublicViewportInput = { viewportId, @@ -32,13 +54,54 @@ export const StackViewer = (props: StackViewerProps) => { ) as IStackViewport; viewport.setStack(props.imageIds); + // viewport.setProperties({ + // voiRange: ctVoiRange, + // }); + + if (toolGroup) { + toolGroup.addViewport(viewportId, renderingEngineId); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // 鼠标中键 + }, + ], + }); + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, + }, + ], + }); + } return () => { + ToolGroupManager.destroyToolGroup(toolGroupId); renderingEngineRef.current?.disableElement(viewportId); renderingEngineRef.current?.destroy(); }; }, [props.imageIds, renderingEngineId, viewportId]); + /** + * 滚轮换图逻辑 + */ + useEffect(() => { + const handleWheel = (event: WheelEvent) => { + const delta = event.deltaY > 0 ? -1 : 1; + if (delta === -1 && index === 0) return; + if (delta === 1 && index === props.imageIds.length - 1) return; + setIndex((p) => p + delta); + }; + const stackElement = viewportStackRef.current; + stackElement?.addEventListener("wheel", handleWheel); + return () => { + stackElement?.removeEventListener("wheel", handleWheel); + }; + }, [index, props.imageIds.length]); + useEffect(() => { const container = containerRef.current; if (!container) return; @@ -58,22 +121,38 @@ export const StackViewer = (props: StackViewerProps) => { }; }, []); + /** + * 拖拽滚动条 + */ const onChangeIndex = (value: number[]) => { - if (renderingEngineRef.current) { - const viewport = renderingEngineRef.current.getViewport(viewportId); - (viewport as IStackViewport).setImageIdIndex(value[0]); - } + setIndex(value[0]); }; + useEffect(() => { + if (renderingEngineRef.current) { + const viewport = renderingEngineRef.current.getViewport(viewportId); + (viewport as IStackViewport).setImageIdIndex(index); + } + }, [index]); + return ( -
+
- +
+
+ {index + 1}/{props.imageIds.length} +
+ +
); }; diff --git a/apps/desktop/src/pages/Viewer/index.tsx b/apps/desktop/src/pages/Viewer/index.tsx index 3a81196..d28808a 100644 --- a/apps/desktop/src/pages/Viewer/index.tsx +++ b/apps/desktop/src/pages/Viewer/index.tsx @@ -9,35 +9,42 @@ import { } from "@/components/ui/resizable"; import { StackViewer } from "./StackViewer"; import { createImageIdsAndCacheMetaData } from "./MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData"; + +import * as cornerstoneTools from "@cornerstonejs/tools"; import { volumeLoader } from "@cornerstonejs/core"; const wadoRsRoot = "http://localhost:8042/dicom-web"; +const { StackScrollMouseWheelTool, WindowLevelTool, ZoomTool, CrosshairsTool } = + cornerstoneTools; + export const Viewer = () => { const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const SeriesInstanceUID = queryParams.get("SeriesInstanceUID"); const StudyInstanceUID = queryParams.get("StudyInstanceUID"); - const imageIdsRef = useRef(); - const volumeId = "volume"; + const imageIdsSorted = useRef(); const wwwl = { windowCenter: 50, windowWidth: 850 }; + const volumeId = "volumeId"; useEffect(() => { const setImageOrderCache = async () => { if (!StudyInstanceUID || !SeriesInstanceUID) return; // imageIds此时由于流式加载for mpr,是错乱的图片顺序 - const imageIds = await createImageIdsAndCacheMetaData({ + const unSortedImageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID, SeriesInstanceUID, wadoRsRoot, }); + // 这一步会对imageIds进行排序,如果不排序imageIds会错误乱,stackViewport顺序会错误 const volume = await volumeLoader.createAndCacheVolume(volumeId, { - imageIds, + imageIds: unSortedImageIds, }); volume.load(); - imageIdsRef.current = volume.imageIds; + + imageIdsSorted.current = volume.imageIds; // 默认windowWidtth const { windowCenter, windowWidth } = volume.cornerstoneImageMetaData; @@ -48,8 +55,20 @@ export const Viewer = () => { initCornerstone(() => { setImageOrderCache(); + + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(CrosshairsTool); }); + return () => { + // 移出工具注册 + cornerstoneTools.removeTool(CrosshairsTool); + cornerstoneTools.removeTool(StackScrollMouseWheelTool); + cornerstoneTools.removeTool(WindowLevelTool); + cornerstoneTools.removeTool(ZoomTool); + }; }, [SeriesInstanceUID, StudyInstanceUID]); return ( @@ -67,8 +86,11 @@ export const Viewer = () => { >
- {cornerstoneLoaded && imageIdsRef.current && ( - + {cornerstoneLoaded && imageIdsSorted.current && ( + )}
@@ -81,8 +103,8 @@ export const Viewer = () => { - {cornerstoneLoaded && imageIdsRef.current && ( - + {cornerstoneLoaded && imageIdsSorted.current && ( + )}