From cd4e6408bc2964e5ee27427cdc328751eaf4d088 Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 5 May 2025 16:35:05 -0400 Subject: [PATCH] feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007) --- .vscode/settings.json | 4 +- bun.lock | 278 +++++----- .../cornerstone-dicom-pmap/package.json | 4 +- .../src/getSopClassHandlerModule.ts | 13 +- .../src/getCommandsModule.ts | 2 +- .../src/getSopClassHandlerModule.ts | 54 +- .../viewports/OHIFCornerstoneRTViewport.tsx | 2 +- extensions/cornerstone-dicom-seg/package.json | 4 +- .../src/getSopClassHandlerModule.ts | 9 +- .../viewports/OHIFCornerstoneSEGViewport.tsx | 4 +- extensions/cornerstone-dicom-sr/package.json | 6 +- .../OHIFCornerstoneSRMeasurementViewport.tsx | 2 +- .../src/getSopClassHandlerModule.ts | 7 +- .../cornerstone-dynamic-volume/package.json | 4 +- extensions/cornerstone/package.json | 14 +- .../src/Viewport/OHIFCornerstoneViewport.tsx | 66 +-- .../Overlays/ViewportImageScrollbar.tsx | 6 +- extensions/cornerstone/src/commandsModule.ts | 60 ++- .../components/OHIFViewportActionCorners.tsx | 9 +- .../ViewportDataOverlayMenu.tsx | 478 ++++++++++++++++++ .../ViewportDataOverlayMenuWrapper.tsx | 62 +++ .../ViewportSegmentationMenu.tsx | 136 ----- .../ViewportSegmentationMenuWrapper.tsx | 83 --- .../ViewportDataOverlaySettingMenu/index.tsx | 4 +- .../ViewportDataOverlaySettingMenu/utils.ts | 156 ++++++ .../ViewportOrientationMenu.tsx | 140 +++++ .../ViewportOrientationMenuWrapper.tsx | 27 + .../ViewportOrientationMenu/index.tsx | 14 + .../WindowLevelActionMenu/Colorbar.tsx | 38 +- .../WindowLevelActionMenu/Colormap.tsx | 10 +- .../WindowLevelActionMenu/VolumeLighting.tsx | 9 +- .../VolumeRenderingOptions.tsx | 16 +- .../VolumeRenderingPresets.tsx | 5 +- .../VolumeRenderingQuality.tsx | 4 +- .../WindowLevelActionMenu/VolumeShade.tsx | 4 +- .../WindowLevelActionMenu/VolumeShift.tsx | 8 +- .../WindowLevelActionMenu/WindowLevel.tsx | 10 +- .../WindowLevelActionMenu.tsx | 23 +- .../getWindowLevelActionMenu.tsx | 24 +- extensions/cornerstone/src/constants/index.ts | 3 + .../modalityColorMapCustomization.ts | 23 + .../viewportActionMenuCustomizations.ts | 63 ++- .../src/getCustomizationModule.tsx | 2 + .../src/getSopClassHandlerModule.js | 2 + .../cornerstone/src/getToolbarModule.tsx | 2 +- extensions/cornerstone/src/hooks/index.ts | 5 + .../cornerstone/src/hooks/useSegmentations.ts | 2 +- .../src/hooks/useViewportActionCorners.ts | 113 +++++ .../src/hooks/useViewportDisplaySets.ts | 247 +++++++++ extensions/cornerstone/src/index.tsx | 3 + extensions/cornerstone/src/init.tsx | 52 +- .../ColorbarService/ColorbarService.ts | 67 ++- .../CornerstoneCacheService.ts | 3 +- .../RTSTRUCT/mapROIContoursToRTStructData.ts | 39 +- .../SegmentationService.ts | 6 +- .../ViewportActionCornersService.ts | 21 + .../CornerstoneViewportService.ts | 75 ++- .../src/services/ViewportService/Viewport.ts | 4 + .../cornerstone/src/types/ViewportPresets.ts | 15 - extensions/cornerstone/src/types/index.ts | 5 + extensions/cornerstone/src/utils/colormaps.js | 9 + .../presentations/getViewportPresentations.ts | 10 +- .../src/Components/MoreDropdownMenu.tsx | 19 +- extensions/default/src/commandsModule.ts | 139 +++++ .../studyBrowserCustomization.ts | 20 + .../default/src/getSopClassHandlerModule.js | 7 + extensions/default/src/getViewportModule.tsx | 11 +- extensions/default/src/init.ts | 37 +- .../src/utils/layerConfigurationUtils.ts | 157 ++++++ .../dicom-pdf/src/getSopClassHandlerModule.js | 10 +- .../src/getSopClassHandlerModule.js | 6 +- extensions/measurement-tracking/package.json | 4 +- .../TrackedMeasurementsContext.tsx | 2 +- .../viewports/TrackedCornerstoneViewport.tsx | 2 +- platform/app/package.json | 2 +- platform/app/src/components/ViewportGrid.tsx | 2 +- platform/core/package.json | 4 +- .../MeasurementService/MeasurementService.ts | 2 +- .../services/ToolBarService/ToolbarService.ts | 5 +- platform/core/src/types/DisplaySet.ts | 3 + .../core/src/utils/combineFrameInstance.ts | 56 +- .../core/src/utils/isLowPriorityModality.ts | 12 +- platform/docs/docs/faq/add-viewport-icon.md | 178 +++++++ platform/docs/docs/faq/technical.md | 1 + .../migration-guide/3p10-to-3p11/index.md | 22 + .../3p10-to-3p11/viewport-action-menu.md | 102 ++++ .../docs/migration-guide/3p9-to-3p10/index.md | 2 +- .../sampleCustomizations.tsx | 6 +- .../src/components/DataRow/DataRow.tsx | 23 +- .../components/DoubleSlider/DoubleSlider.tsx | 261 +++++++++- .../ui-next/src/components/Icons/Icons.tsx | 8 + .../Icons/Sources/LayerBackground.tsx | 28 + .../Icons/Sources/LayerForeground.tsx | 34 ++ .../Icons/Sources/LayerSegmentation.tsx | 20 + .../Icons/Sources/OrientationSwitch.tsx | 41 ++ .../src/components/Numeric/Numeric.tsx | 7 +- .../SegmentationSegments.tsx | 3 +- .../contexts/SegmentationTableContext.tsx | 1 + .../ui-next/src/components/Select/Select.tsx | 2 +- .../Viewport/ViewportActionCorners.tsx | 2 +- .../contextProviders/ViewportGridProvider.tsx | 2 + .../src/types/ViewportActionCornersTypes.ts | 7 + playwright.config.ts | 8 +- tests/JumpToMeasurementMPR.spec.ts | 21 +- yarn.lock | 56 +- 105 files changed, 3117 insertions(+), 788 deletions(-) create mode 100644 extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenu.tsx create mode 100644 extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx delete mode 100644 extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx delete mode 100644 extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx create mode 100644 extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/utils.ts create mode 100644 extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx create mode 100644 extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx create mode 100644 extensions/cornerstone/src/components/ViewportOrientationMenu/index.tsx create mode 100644 extensions/cornerstone/src/constants/index.ts create mode 100644 extensions/cornerstone/src/customizations/modalityColorMapCustomization.ts create mode 100644 extensions/cornerstone/src/hooks/index.ts create mode 100644 extensions/cornerstone/src/hooks/useViewportActionCorners.ts create mode 100644 extensions/cornerstone/src/hooks/useViewportDisplaySets.ts create mode 100644 extensions/default/src/utils/layerConfigurationUtils.ts create mode 100644 platform/docs/docs/faq/add-viewport-icon.md create mode 100644 platform/docs/docs/migration-guide/3p10-to-3p11/index.md create mode 100644 platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md create mode 100644 platform/ui-next/src/components/Icons/Sources/LayerBackground.tsx create mode 100644 platform/ui-next/src/components/Icons/Sources/LayerForeground.tsx create mode 100644 platform/ui-next/src/components/Icons/Sources/LayerSegmentation.tsx create mode 100644 platform/ui-next/src/components/Icons/Sources/OrientationSwitch.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bd5e24c0..7f147e3a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -109,6 +109,8 @@ "wado", "wadors", "wadouri", - "workerpool" + "workerpool", + "Colorbar", + "Colorbars" ] } diff --git a/bun.lock b/bun.lock index 5eba5072e..febadd02f 100644 --- a/bun.lock +++ b/bun.lock @@ -40,7 +40,7 @@ }, "addOns/externals/devDependencies": { "name": "@externals/devDependencies", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "@kitware/vtk.js": "32.12.0", @@ -127,22 +127,22 @@ }, "addOns/externals/dicom-microscopy-viewer": { "name": "@externals/dicom-microscopy-viewer", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "dicom-microscopy-viewer": "^0.46.1", }, }, "extensions/cornerstone": { "name": "@ohif/extension-cornerstone", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/ai": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/labelmap-interpolation": "^3.9.5", - "@cornerstonejs/polymorphic-segmentation": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/ai": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/labelmap-interpolation": "^3.11.6", + "@cornerstonejs/polymorphic-segmentation": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "^1.4.1", @@ -159,9 +159,9 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", - "@ohif/core": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@cornerstonejs/dicom-image-loader": "^3.11.6", + "@ohif/core": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -173,19 +173,19 @@ }, "extensions/cornerstone-dicom-pmap": { "name": "@ohif/extension-cornerstone-dicom-pmap", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -196,16 +196,16 @@ }, "extensions/cornerstone-dicom-rt": { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -216,19 +216,19 @@ }, "extensions/cornerstone-dicom-seg": { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -239,19 +239,19 @@ }, "extensions/cornerstone-dicom-sr": { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-measurement-tracking": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-measurement-tracking": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -261,19 +261,19 @@ }, "extensions/cornerstone-dynamic-volume": { "name": "@ohif/extension-cornerstone-dynamic-volume", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -283,7 +283,7 @@ }, "extensions/default": { "name": "@ohif/extension-default", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/calculate-suv": "^1.1.0", @@ -291,8 +291,8 @@ "lodash.uniqby": "^4.7.0", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", "dcmjs": "*", "dicomweb-client": "^0.10.4", "prop-types": "^15.6.2", @@ -306,7 +306,7 @@ }, "extensions/dicom-microscopy": { "name": "@ohif/extension-dicom-microscopy", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", @@ -317,10 +317,10 @@ "mathjs": "^12.4.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -331,14 +331,14 @@ }, "extensions/dicom-pdf": { "name": "@ohif/extension-dicom-pdf", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -348,14 +348,14 @@ }, "extensions/dicom-video": { "name": "@ohif/extension-dicom-video", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -365,20 +365,20 @@ }, "extensions/measurement-tracking": { "name": "@ohif/extension-measurement-tracking", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/ui": "3.11.0-beta.23", "@xstate/react": "^3.2.2", "xstate": "^4.10.0", }, "peerDependencies": { - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "classnames": "^2.3.2", "dcmjs": "*", "lodash.debounce": "^4.0.8", @@ -391,14 +391,14 @@ }, "extensions/test-extension": { "name": "@ohif/extension-test", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", "dcmjs": "0.38.0", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -408,7 +408,7 @@ }, "extensions/tmtv": { "name": "@ohif/extension-tmtv", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -425,7 +425,7 @@ }, "modes/basic-dev-mode": { "name": "@ohif/mode-basic-dev-mode", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -435,17 +435,17 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/extension-dicom-pdf": "3.10.0-beta.158", - "@ohif/extension-dicom-video": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/extension-dicom-pdf": "3.11.0-beta.23", + "@ohif/extension-dicom-video": "3.11.0-beta.23", }, }, "modes/basic-test-mode": { "name": "@ohif/mode-test", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -455,19 +455,19 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/extension-dicom-pdf": "3.10.0-beta.158", - "@ohif/extension-dicom-video": "3.10.0-beta.158", - "@ohif/extension-measurement-tracking": "3.10.0-beta.158", - "@ohif/extension-test": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/extension-dicom-pdf": "3.11.0-beta.23", + "@ohif/extension-dicom-video": "3.11.0-beta.23", + "@ohif/extension-measurement-tracking": "3.11.0-beta.23", + "@ohif/extension-test": "3.11.0-beta.23", }, }, "modes/longitudinal": { "name": "@ohif/mode-longitudinal", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -477,32 +477,32 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/extension-dicom-pdf": "3.10.0-beta.158", - "@ohif/extension-dicom-video": "3.10.0-beta.158", - "@ohif/extension-measurement-tracking": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/extension-dicom-pdf": "3.11.0-beta.23", + "@ohif/extension-dicom-video": "3.11.0-beta.23", + "@ohif/extension-measurement-tracking": "3.11.0-beta.23", }, }, "modes/microscopy": { "name": "@ohif/mode-microscopy", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-dicom-microscopy": "3.11.0-beta.23", }, }, "modes/preclinical-4d": { "name": "@ohif/mode-preclinical-4d", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", }, @@ -521,7 +521,7 @@ }, "modes/segmentation": { "name": "@ohif/mode-segmentation", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -565,7 +565,7 @@ }, "modes/tmtv": { "name": "@ohif/mode-tmtv", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -586,32 +586,32 @@ }, "platform/app": { "name": "@ohif/app", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", + "@cornerstonejs/dicom-image-loader": "^3.11.6", "@emotion/serialize": "^1.1.3", - "@ohif/core": "3.10.0-beta.158", - "@ohif/extension-cornerstone": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.158", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158", - "@ohif/extension-default": "3.10.0-beta.158", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.158", - "@ohif/extension-dicom-pdf": "3.10.0-beta.158", - "@ohif/extension-dicom-video": "3.10.0-beta.158", - "@ohif/extension-test": "3.10.0-beta.158", - "@ohif/i18n": "3.10.0-beta.158", - "@ohif/mode-basic-dev-mode": "3.10.0-beta.158", - "@ohif/mode-longitudinal": "3.10.0-beta.158", - "@ohif/mode-microscopy": "3.10.0-beta.158", - "@ohif/mode-test": "3.10.0-beta.158", - "@ohif/ui": "3.10.0-beta.158", - "@ohif/ui-next": "3.10.0-beta.158", + "@ohif/core": "3.11.0-beta.23", + "@ohif/extension-cornerstone": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.23", + "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", + "@ohif/extension-default": "3.11.0-beta.23", + "@ohif/extension-dicom-microscopy": "3.11.0-beta.23", + "@ohif/extension-dicom-pdf": "3.11.0-beta.23", + "@ohif/extension-dicom-video": "3.11.0-beta.23", + "@ohif/extension-test": "3.11.0-beta.23", + "@ohif/i18n": "3.11.0-beta.23", + "@ohif/mode-basic-dev-mode": "3.11.0-beta.23", + "@ohif/mode-longitudinal": "3.11.0-beta.23", + "@ohif/mode-microscopy": "3.11.0-beta.23", + "@ohif/mode-test": "3.11.0-beta.23", + "@ohif/ui": "3.11.0-beta.23", + "@ohif/ui-next": "3.11.0-beta.23", "@svgr/webpack": "^8.1.0", "@types/react": "^18.3.3", "classnames": "^2.3.2", @@ -652,7 +652,7 @@ }, "platform/cli": { "name": "@ohif/cli", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "bin": { "ohif-cli": "src/index.js", }, @@ -676,7 +676,7 @@ }, "platform/core": { "name": "@ohif/core", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "dcmjs": "*", @@ -701,16 +701,16 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", - "@ohif/ui": "3.10.0-beta.158", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/dicom-image-loader": "^3.11.6", + "@ohif/ui": "3.11.0-beta.23", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21", }, }, "platform/i18n": { "name": "@ohif/i18n", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@babel/runtime": "^7.20.13", "i18next-locize-backend": "^2.0.0", @@ -735,7 +735,7 @@ }, "platform/ui": { "name": "@ohif/ui", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@testing-library/react": "^13.1.0", "browser-detect": "^0.2.28", @@ -795,7 +795,7 @@ }, "platform/ui-next": { "name": "@ohif/ui-next", - "version": "3.11.0-beta.5", + "version": "3.11.0-beta.23", "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", @@ -1106,9 +1106,9 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.9.5", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5" } }, "sha512-I8nPFmwNBqBwrH26w06gZfsJaHyZPN4nZC8GO7IerkVSkyYXgFEFC5iRdVuU+Kr94nWm8zPKyLqHx7hAeNudgw=="], + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.11.6", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6" } }, "sha512-EYLkBXfXF6UcmcZPitIkDNe02HdKykiFDnG9CtXPJkTSqFfCZnkF4Mqe4WN/husc9wia6BUMlESQRBYEgBithA=="], - "@cornerstonejs/ai": ["@cornerstonejs/ai@3.9.5", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5" } }, "sha512-AgVPLVoYJeefPcp6tf5UNfS8qoEJc8VxrqZab6f+LybhSa2se52h+57rCB5Ihd7SziNvWyIdux7v1Iy3GiZm+g=="], + "@cornerstonejs/ai": ["@cornerstonejs/ai@3.11.6", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6" } }, "sha512-F17IDzbn1PC2Zt1S2RS4OaLqO5ccC71Mmk2/9tU3wQS7g7qmyh8JzyB3B6t3iHkSlv20p3HNh8MqSug7zHOcrg=="], "@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="], @@ -1120,15 +1120,15 @@ "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], - "@cornerstonejs/core": ["@cornerstonejs/core@3.9.5", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-EwXeTNSlL7UnXmj8o1DigNZOsVbhtNDKT2Bbi66jhnzZQm+rTPyYeTva+FnnqvpxBb/6QqwDEJ8gwlXwV0Rk5w=="], + "@cornerstonejs/core": ["@cornerstonejs/core@3.11.6", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-8IlnDj7MZV/pfbtdF5gdpL53hLkriBgqcTY4hBIY4iW1A1plmwADsbVXzgyXl3Bi63aovmJhiQZ1VtQx0TzZGQ=="], - "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.9.5", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5" } }, "sha512-g0GZNkZ6PBRCl/dvi6NoQlmpmZhuLLeI4cdiFwV0Ug81HPkHdn8FQaqgy1XPOMknLSL6sJGZsJN2l2nW5COQSA=="], + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.11.6", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6" } }, "sha512-SJZ/LAw2gIyxzyQTEFWqBtyZELakqjcHOJnFxhkEmmpuJdpX4tgtV4k+6fnbKTg9OhybyP3prouMVPj5myjCZA=="], - "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.9.5", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5", "@kitware/vtk.js": "32.12.1" } }, "sha512-U7XnaqrrEHGvH3kKdTwGiwLYaydxp5GVo0aqVvZaRDXMXfP3tbOpCfMQcqD+bv4Sh+Rr0719O5aDJ0+ENpA/MA=="], + "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.11.6", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6", "@kitware/vtk.js": "32.12.1" } }, "sha512-N55MxFxlRNXHVKpsJgeO8Z2aEURINxdmyPvGODGgYjZSOXZ+iR12Aly/iZHW9d/crq4GU440d5B6EmEZ6iuW8w=="], - "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.9.5", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5", "@kitware/vtk.js": "32.12.1" } }, "sha512-6ucJkb0X5CBBx00g3CzJGuLd6x4T99bPyKXa+y9Bek3jC+RyKI4vySS5RRKtQ4N59yuealZkBvV7dT0FseE8og=="], + "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.11.6", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6", "@kitware/vtk.js": "32.12.1" } }, "sha512-dk+G12MqqmKSd2Rz8gHo1piEDAeijBcz1EAdF+50UQlUEds1fczHwFkMrh9mlHc9treyHi5+QEIr2MKRmwaJIw=="], - "@cornerstonejs/tools": ["@cornerstonejs/tools@3.9.5", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-vdPsm4szolXmyBdDNTB1gyRCP5EEicsf+TOsTU5P6QYWR7cB+nyio7GeGK5zFQrUi0hb2a2qkVjjLQLQH9inAw=="], + "@cornerstonejs/tools": ["@cornerstonejs/tools@3.11.6", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-Sp1QkEaA952GgZikTNzyj125FzpYtVAYbeqCOn0E3WS7K1yw2XPuRbSCTRkRcV/J66ZoeKl4KgZVrhDlxJGAnw=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index 9e790f979..b68fc2941 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts index dc21ba3bf..124c10d30 100644 --- a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts @@ -1,10 +1,9 @@ -import { utils } from '@ohif/core'; +import { utils, Types as OhifTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core'; import { adaptersPMAP } from '@cornerstonejs/adapters'; import { SOPClassHandlerId } from './id'; -import { dicomLoaderService } from '@ohif/extension-cornerstone'; - -const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; +import { dicomLoaderService, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone'; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30']; function _getDisplaySetsFromSeries( @@ -57,7 +56,8 @@ function _getDisplaySetsFromSeries( wadoRoot, wadoUriRoot, wadoUri, - isOverlayDisplaySet: true, + supportsWindowLevel: true, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`, }; const referencedSeriesSequence = instance.ReferencedSeriesSequence; @@ -227,7 +227,8 @@ async function _loadParametricMap({ displaySet, headers }: withAppTypes) { return derivedVolume; } -function getSopClassHandlerModule({ servicesManager, extensionManager }) { +function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { + const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; diff --git a/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts index d2737c555..b51a886df 100644 --- a/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts +++ b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts @@ -27,7 +27,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { // it will be in the correct position zoom and pan commandsManager.runCommand('updateStoredPositionPresentation', { viewportId, - displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], }); viewportGridService.setDisplaySetsForViewport({ diff --git a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts index 505819d43..d0c490e4b 100644 --- a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts @@ -1,4 +1,5 @@ -import { utils } from '@ohif/core'; +import { utils, Types as OhifTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; import { SOPClassHandlerId } from './id'; import loadRTStruct from './loadRTStruct'; @@ -53,6 +54,7 @@ function _getDisplaySetsFromSeries( wadoUriRoot, wadoUri, isOverlayDisplaySet: true, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`, }; let referencedSeriesSequence = instance.ReferencedSeriesSequence; @@ -94,12 +96,19 @@ function _getDisplaySetsFromSeries( displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; } - displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers); + displaySet.load = ({ headers, createSegmentation = true }) => + _load(displaySet, servicesManager, extensionManager, headers, createSegmentation); return [displaySet]; } -function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) { +function _load( + rtDisplaySet, + servicesManager: AppTypes.ServicesManager, + extensionManager, + headers, + createSegmentation = true +) { const { SOPInstanceUID } = rtDisplaySet; const { segmentationService } = servicesManager.services; if ( @@ -121,16 +130,21 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio rtDisplaySet.structureSet = structureSet; } - segmentationService - .createSegmentationForRTDisplaySet(rtDisplaySet) - .then(() => { - rtDisplaySet.loading = false; - resolve(); - }) - .catch(error => { - rtDisplaySet.loading = false; - reject(error); - }); + if (createSegmentation) { + segmentationService + .createSegmentationForRTDisplaySet(rtDisplaySet) + .then(() => { + rtDisplaySet.loading = false; + resolve(); + }) + .catch(error => { + rtDisplaySet.loading = false; + reject(error); + }); + } else { + rtDisplaySet.loading = false; + resolve(); + } }); return loadPromises[SOPInstanceUID]; @@ -171,20 +185,14 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence( return ReferencedSeriesSequence; } -function _segmentationExistsInCache( - rtDisplaySet, - segmentationService: AppTypes.SegmentationService -) { +function _segmentationExistsInCache() { // Todo: fix this return false; - // This should be abstracted with the CornerstoneCacheService - const rtContourId = rtDisplaySet.displaySetInstanceUID; - const contour = segmentationService.getContour(rtContourId); - - return contour !== undefined; } -function getSopClassHandlerModule({ servicesManager, extensionManager }) { +function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { + const { servicesManager, extensionManager } = params; + return [ { name: 'dicom-rt', diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 19cf891d0..980afb4d7 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -323,7 +323,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { onStatusClick, }), indexPriority: -100, - location: viewportActionCornersService.LOCATIONS.topLeft, + location: viewportActionCornersService.LOCATIONS.topRight, }, { viewportId, diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 41f6072cc..acfa235af 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts index 162a548b3..cadd77b7e 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -1,5 +1,6 @@ -import { utils } from '@ohif/core'; -import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core'; +import { utils, Types as OhifTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; +import { metaData, eventTarget } from '@cornerstonejs/core'; import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; @@ -57,6 +58,7 @@ function _getDisplaySetsFromSeries( wadoUriRoot, wadoUri, isOverlayDisplaySet: true, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SEG')}`, }; const referencedSeriesSequence = instance.ReferencedSeriesSequence; @@ -232,7 +234,8 @@ function _segmentationExists(segDisplaySet) { return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID); } -function getSopClassHandlerModule({ servicesManager, extensionManager }) { +function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { + const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 2d7ae9105..4c7ceffc4 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -157,7 +157,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { // it will be in the correct position zoom and pan commandsManager.runCommand('updateStoredPositionPresentation', { viewportId, - displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], }); commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { @@ -304,7 +304,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { onStatusClick, }), indexPriority: -100, - location: viewportActionCornersService.LOCATIONS.topLeft, + location: viewportActionCornersService.LOCATIONS.topRight, }, { viewportId, diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index a5b9508e3..1031e3d61 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx index 11ae7bcc9..50ea47557 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -273,7 +273,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { servicesManager, }), indexPriority: -100, - location: viewportActionCornersService.LOCATIONS.topLeft, + location: viewportActionCornersService.LOCATIONS.topRight, }, { viewportId, diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index c8da45531..775ef20bb 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -1,4 +1,5 @@ -import { utils, classes, DisplaySetService, Types } from '@ohif/core'; +import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import { adaptersSR } from '@cornerstonejs/adapters'; @@ -122,6 +123,7 @@ function _getDisplaySetsFromSeries( sopClassUids, instance, addInstances, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`, }; displaySet.load = () => _load(displaySet, servicesManager, extensionManager); @@ -354,7 +356,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame * @param {Object} options.extensionManager - The extension manager. * @returns {Array} An array containing the SOP class handler module. */ -function getSopClassHandlerModule({ servicesManager, extensionManager }) { +function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { + const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 3a46996a4..a28a6b7d2 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index ceb830ef1..4f812adfb 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,7 +38,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", + "@cornerstonejs/dicom-image-loader": "^3.11.6", "@ohif/core": "3.11.0-beta.23", "@ohif/ui": "3.11.0-beta.23", "dcmjs": "*", @@ -51,12 +51,12 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.9.5", - "@cornerstonejs/ai": "^3.9.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/labelmap-interpolation": "^3.9.5", - "@cornerstonejs/polymorphic-segmentation": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/adapters": "^3.11.6", + "@cornerstonejs/ai": "^3.11.6", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/labelmap-interpolation": "^3.11.6", + "@cornerstonejs/polymorphic-segmentation": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "^1.4.1", diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index d28bda89d..73bb70375 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useRef, useCallback, useState } from 'react'; import * as cs3DTools from '@cornerstonejs/tools'; import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core'; import { MeasurementService } from '@ohif/core'; -import { AllInOneMenu } from '@ohif/ui-next'; import { useViewportDialog } from '@ohif/ui-next'; import type { Types as csTypes } from '@cornerstonejs/core'; @@ -14,12 +13,11 @@ import CinePlayer from '../components/CinePlayer'; import type { Types } from '@ohif/core'; import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners'; -import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu'; -import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu'; import { getViewportPresentations } from '../utils/presentations/getViewportPresentations'; import { useSynchronizersStore } from '../stores/useSynchronizersStore'; import ActiveViewportBehavior from '../utils/ActiveViewportBehavior'; import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService'; +import { useViewportActionCorners } from '../hooks'; const STACK = 'stack'; @@ -321,49 +319,15 @@ const OHIFCornerstoneViewport = React.memo( }; }, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]); - // Set up the window level action menu in the viewport action corners. - useEffect(() => { - const windowLevelActionMenu = customizationService.getCustomization( - 'viewportActionMenu.windowLevelActionMenu' - ); - const segmentationOverlay = customizationService.getCustomization( - 'viewportActionMenu.segmentationOverlay' - ); - - if (windowLevelActionMenu?.enabled) { - viewportActionCornersService.addComponent({ - viewportId, - id: 'windowLevelActionMenu', - component: getWindowLevelActionMenu({ - viewportId, - element: elementRef.current, - displaySets, - servicesManager, - commandsManager, - location: windowLevelActionMenu.location, - verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom, - horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft, - }), - location: windowLevelActionMenu.location, - }); - } - - if (segmentationOverlay?.enabled) { - viewportActionCornersService.addComponent({ - viewportId, - id: 'segmentation', - component: getViewportDataOverlaySettingsMenu({ - viewportId, - element: elementRef.current, - displaySets, - servicesManager, - commandsManager, - location: segmentationOverlay.location, - }), - location: segmentationOverlay.location, - }); - } - }, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]); + // Set up the window level action menu in the viewport action corners using the custom hook + useViewportActionCorners({ + viewportId, + elementRef, + displaySets, + viewportActionCornersService, + customizationService, + commandsManager, + }); const Notification = customizationService.getCustomization('ui.notificationComponent'); @@ -524,22 +488,18 @@ OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport'; function areEqual(prevProps, nextProps) { if (nextProps.needsRerendering) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering'); return false; } if (prevProps.displaySets.length !== nextProps.displaySets.length) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change'); return false; } if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change'); return false; } if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change'); return false; } @@ -547,12 +507,10 @@ function areEqual(prevProps, nextProps) { nextProps.viewportOptions.viewportType && prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType ) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change'); return false; } if (nextProps.viewportOptions.needsRerendering) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering'); return false; } @@ -560,7 +518,6 @@ function areEqual(prevProps, nextProps) { const nextDisplaySets = nextProps.displaySets; if (prevDisplaySets.length !== nextDisplaySets.length) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch'); return false; } @@ -573,13 +530,11 @@ function areEqual(prevProps, nextProps) { ); if (!foundDisplaySet) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found'); return false; } // check they contain the same image if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch'); return false; } @@ -587,7 +542,6 @@ function areEqual(prevProps, nextProps) { if (foundDisplaySet.images?.length) { for (let j = 0; j < foundDisplaySet.images.length; j++) { if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) { - console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch'); return false; } } diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx index 2c7f02903..edb0b22b1 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -63,7 +63,7 @@ function CornerstoneImageScrollbar({ } const { viewportType } = viewportData; const eventId = - (viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) || + (viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) || (viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) || Enums.Events.IMAGE_RENDERED; @@ -72,11 +72,11 @@ function CornerstoneImageScrollbar({ if (!viewport || viewport instanceof VolumeViewport3D) { return; } - const { imageIndex, newImageIdIndex = imageIndex } = event.detail; + const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail; const numberOfSlices = viewport.getNumberOfSlices(); // find the index of imageId in the imageIds setImageSliceData({ - imageIndex: newImageIdIndex, + imageIndex: newImageIdIndex ?? imageIdIndex, numberOfSlices, }); }; diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 237e78df0..e461bb106 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -3,6 +3,7 @@ import { StackViewport, VolumeViewport, utilities as csUtils, + Enums as CoreEnums, Types as CoreTypes, BaseVolumeViewport, } from '@cornerstonejs/core'; @@ -248,7 +249,7 @@ function commandsModule({ }, updateStoredPositionPresentation: ({ viewportId, - displaySetInstanceUID, + displaySetInstanceUIDs, referencedImageId, options, }) => { @@ -256,11 +257,24 @@ function commandsModule({ const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } = usePositionPresentationStore.getState(); - // Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID + // Look inside positionPresentationStore and find the key that includes ALL the displaySetInstanceUIDs // and the value has viewportId as activeViewportId. - const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find( - ([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId - )?.[0]; + let previousReferencedDisplaySetStoreKey; + + if ( + displaySetInstanceUIDs && + Array.isArray(displaySetInstanceUIDs) && + displaySetInstanceUIDs.length > 0 + ) { + previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find( + ([key, value]) => { + return ( + displaySetInstanceUIDs.every(uid => key.includes(uid)) && + value.viewportId === viewportId + ); + } + )?.[0]; + } // Create presentation data with referencedImageId and options if provided const presentationData = referencedImageId @@ -282,7 +296,7 @@ function commandsModule({ // so we need to grab the positionPresentationId directly from the store, // Todo: this is really hacky, we should have a better way for this const positionPresentationId = getPositionPresentationId({ - displaySetInstanceUIDs: [displaySetInstanceUID], + displaySetInstanceUIDs, viewportId, }); @@ -719,7 +733,7 @@ function commandsModule({ return; } - if (!toolGroup.hasTool(toolName)) { + if (!toolGroup?.hasTool(toolName)) { return; } @@ -1019,7 +1033,7 @@ function commandsModule({ const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); - if (!toolGroup.hasTool(toolName)) { + if (!toolGroup?.hasTool(toolName)) { return; } @@ -1189,6 +1203,9 @@ function commandsModule({ }, /** * Creates a labelmap for the active viewport + * + * The created labelmap will be registered as a display set and also added + * as a segmentation representation to the viewport. */ createLabelmapForViewport: async ({ viewportId, options = {} }) => { const { viewportGridService, displaySetService, segmentationService } = @@ -1209,6 +1226,7 @@ function commandsModule({ const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + // This will create the segmentation and register it as a display set const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet( displaySet, { @@ -1225,6 +1243,7 @@ function commandsModule({ } ); + // Also add the segmentation representation to the viewport await segmentationService.addSegmentationRepresentation(viewportId, { segmentationId, type: Enums.SegmentationRepresentations.Labelmap, @@ -1741,6 +1760,30 @@ function commandsModule({ }); }); }, + setViewportOrientation: ({ viewportId, orientation }) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) { + console.warn('Orientation can only be set on volume viewports'); + return; + } + + // Get display sets for this viewport to verify at least one is reconstructable + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + const displaySets = displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)); + + if (!displaySets.some(ds => ds.isReconstructable)) { + console.warn('Cannot change orientation: No reconstructable display sets in viewport'); + return; + } + + viewport.setOrientation(orientation); + viewport.render(); + + // update the orientation in the viewport info + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + viewportInfo.setOrientation(orientation); + }, }; const definitions = { @@ -2021,6 +2064,7 @@ function commandsModule({ decreaseBrushSize: actions.decreaseBrushSize, addNewSegment: actions.addNewSegment, loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport, + setViewportOrientation: actions.setViewportOrientation, }; return { diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx index 041dfa591..ab60510c0 100644 --- a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx +++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider'; import { useSystem } from '@ohif/core'; +import { useViewportGrid } from '@ohif/ui-next'; export type OHIFViewportActionCornersProps = { viewportId: string; @@ -9,8 +10,13 @@ export type OHIFViewportActionCornersProps = { function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) { const { servicesManager } = useSystem(); const [viewportActionCornersState] = useViewportActionCornersContext(); + + const [viewportGrid] = useViewportGrid(); + const isActiveViewport = viewportGrid.activeViewportId === viewportId; + const ViewportActionCorners = servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner'); + if (!viewportActionCornersState[viewportId]) { return null; } @@ -18,7 +24,8 @@ function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProp return ( + isActiveViewport={isActiveViewport} + /> ); } diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenu.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenu.tsx new file mode 100644 index 000000000..b8d032ccf --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenu.tsx @@ -0,0 +1,478 @@ +import React, { useState } from 'react'; +import { + Button, + Icons, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Switch, +} from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; + +import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets'; + +function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) { + const { commandsManager, servicesManager } = useSystem(); + const [pendingForegrounds, setPendingForegrounds] = useState([]); + const [pendingSegmentations, setPendingSegmentations] = useState([]); + const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false); + + const { hangingProtocolService } = servicesManager.services; + + const { + backgroundDisplaySet, + potentialOverlayDisplaySets, + potentialForegroundDisplaySets, + potentialBackgroundDisplaySets, + overlayDisplaySets, + foregroundDisplaySets, + } = useViewportDisplaySets(viewportId); + + /** + * Change the background display set + */ + const handleBackgroundSelection = (newBackgroundDisplaySet: AppTypes.DisplaySet) => { + const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + newBackgroundDisplaySet.displaySetInstanceUID + ); + + commandsManager.run('setDisplaySetsForViewports', { + viewportsToUpdate: updatedViewports, + }); + }; + + /** + * Handle threshold and opacity toggle + */ + const handleThresholdOpacityToggle = (checked: boolean) => { + setThresholdOpacityEnabled(checked); + + if (foregroundDisplaySets.length > 0) { + // Example implementation of threshold/opacity adjustment + commandsManager.runCommand('setForegroundThresholdOpacity', { + viewportId, + enabled: checked, + }); + } + }; + + /** + * Replace a display set layer with a new one + */ + const handleReplaceDisplaySetLayer = ( + currentDisplaySetInstanceUID: string, + newDisplaySetInstanceUID: string + ) => { + // Remove current display set + commandsManager.runCommand('removeDisplaySetLayer', { + viewportId, + displaySetInstanceUID: currentDisplaySetInstanceUID, + }); + + setTimeout(() => { + commandsManager.runCommand('addDisplaySetAsLayer', { + viewportId, + displaySetInstanceUID: newDisplaySetInstanceUID, + }); + }, 0); + }; + + /** + * Remove a display set layer + */ + const handleRemoveDisplaySetLayer = (displaySetInstanceUID: string) => { + commandsManager.runCommand('removeDisplaySetLayer', { + viewportId, + displaySetInstanceUID, + }); + }; + + /** + * Add a display set as a layer + */ + const handleAddDisplaySetAsLayer = (displaySetInstanceUID: string) => { + commandsManager.runCommand('addDisplaySetAsLayer', { + viewportId, + displaySetInstanceUID, + }); + }; + + /** + * Handle overlay display set selection change + */ + const handleOverlaySelectionChange = ( + currentDisplaySet: AppTypes.DisplaySet, + newDisplaySetInstanceUID: string + ) => { + if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) { + return; + } + + // Find the selected display set + const selectedDisplaySet = potentialOverlayDisplaySets.find( + ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID + ); + + if (selectedDisplaySet) { + handleReplaceDisplaySetLayer( + currentDisplaySet.displaySetInstanceUID, + selectedDisplaySet.displaySetInstanceUID + ); + } + }; + + /** + * Handle foreground display set selection change + */ + const handleForegroundSelectionChange = ( + currentDisplaySet: AppTypes.DisplaySet, + newDisplaySetInstanceUID: string + ) => { + if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) { + return; + } + + // Find the selected display set + const selectedDisplaySet = potentialForegroundDisplaySets.find( + ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID + ); + + if (selectedDisplaySet) { + handleReplaceDisplaySetLayer( + currentDisplaySet.displaySetInstanceUID, + selectedDisplaySet.displaySetInstanceUID + ); + } + }; + + /** + * Handle pending segmentation selection + */ + const handlePendingSegmentationSelection = (pendingId: string, displaySetInstanceUID: string) => { + const selectedDisplaySet = potentialOverlayDisplaySets.find( + ds => ds.displaySetInstanceUID === displaySetInstanceUID + ); + + if (selectedDisplaySet) { + handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID); + // Remove this pending segmentation from the list + setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId)); + } + }; + + /** + * Handle pending foreground selection + */ + const handlePendingForegroundSelection = (pendingId: string, displaySetInstanceUID: string) => { + const selectedDisplaySet = potentialForegroundDisplaySets.find( + ds => ds.displaySetInstanceUID === displaySetInstanceUID + ); + + if (selectedDisplaySet) { + handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID); + // Remove this pending foreground from the list + setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId)); + } + }; + + return ( +
+ {/* Top buttons row */} +
+ + +
+ +
+ {/* Overlays Segmentation section */} +
+ {overlayDisplaySets.map((displaySet, index) => ( +
+ + + + + + + + handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)} + > + Remove + + + +
+ ))} + + {pendingSegmentations.map(pendingId => ( +
+ + + + + + + + { + // Remove this pending segmentation + setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId)); + }} + > + Cancel + + + +
+ ))} +
+ {/* Foregrounds section */} +
+ {foregroundDisplaySets.map((displaySet, index) => ( +
+ + + + + + + + handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)} + > + Remove + + + +
+ ))} + + {pendingForegrounds.map(pendingId => ( +
+ + + + + + + + { + // Remove this pending foreground + setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId)); + }} + > + Cancel + + + +
+ ))} +
+ {/* Background section */} +
+ + +
+
+ {/* {foregroundDisplaySets.length > 0 && ( +
+
+ + +
+
+ )} */} +
+ ); +} + +export default ViewportDataOverlayMenu; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx new file mode 100644 index 000000000..efa155203 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx @@ -0,0 +1,62 @@ +import React, { ReactNode } from 'react'; +import { + Button, + cn, + Icons, + Popover, + PopoverContent, + PopoverTrigger, + useViewportGrid, +} from '@ohif/ui-next'; +import ViewportDataOverlayMenu from './ViewportDataOverlayMenu'; +import classNames from 'classnames'; +import { useSystem } from '@ohif/core'; + +export function ViewportDataOverlayMenuWrapper({ + viewportId, + displaySets, + location, +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; +}>): ReactNode { + const { servicesManager } = useSystem(); + const { viewportActionCornersService } = servicesManager.services; + const [viewportGrid] = useViewportGrid(); + + const { align, side } = viewportActionCornersService.getAlignAndSide(location); + + return ( + + + + + + + + + ); +} diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx deleted file mode 100644 index b247dbc96..000000000 --- a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Icons, Separator } from '@ohif/ui-next'; -import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; - -function ViewportSegmentationMenu({ - viewportId, - servicesManager, -}: withAppTypes<{ viewportId: string }>) { - const { segmentationService } = servicesManager.services; - const [activeSegmentations, setActiveSegmentations] = useState([]); - const [availableSegmentations, setAvailableSegmentations] = useState([]); - - useEffect(() => { - const updateSegmentations = () => { - const active = segmentationService.getSegmentationRepresentations(viewportId); - setActiveSegmentations(active); - - const all = segmentationService.getSegmentations(); - const available = all.filter( - seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId) - ); - setAvailableSegmentations(available); - }; - - updateSegmentations(); - - const subscriptions = [ - segmentationService.EVENTS.SEGMENTATION_MODIFIED, - segmentationService.EVENTS.SEGMENTATION_REMOVED, - segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, - ].map(event => segmentationService.subscribe(event, updateSegmentations)); - - return () => { - subscriptions.forEach(subscription => subscription.unsubscribe()); - }; - }, [segmentationService, viewportId]); - - const toggleSegmentationRepresentationVisibility = ( - segmentationId, - type = SegmentationRepresentations.Labelmap - ) => { - segmentationService.toggleSegmentationRepresentationVisibility(viewportId, { - segmentationId, - type, - }); - }; - - const addSegmentationToViewport = segmentationId => { - segmentationService.addSegmentationRepresentation(viewportId, { segmentationId }); - }; - - const removeSegmentationFromViewport = segmentationId => { - segmentationService.removeSegmentationRepresentations(viewportId, { - segmentationId, - }); - }; - - return ( -
- Current Viewport -
    - {activeSegmentations.map(segmentation => ( -
  • - - {segmentation.label} - {segmentation.visible ? ( - - ) : ( - - )} -
  • - ))} -
- {availableSegmentations.length > 0 && ( - <> - - Available -
    - {availableSegmentations.map(({ segmentationId, label }) => ( -
  • - - {label} -
  • - ))} -
- - )} -
- ); -} - -export default ViewportSegmentationMenu; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx deleted file mode 100644 index 76fb4e35b..000000000 --- a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { ReactNode, useEffect, useState } from 'react'; -import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next'; -import ViewportSegmentationMenu from './ViewportSegmentationMenu'; -import classNames from 'classnames'; -import { useSegmentations } from '../../hooks/useSegmentations'; - -export function ViewportSegmentationMenuWrapper({ - viewportId, - displaySets, - servicesManager, - commandsManager, - location, -}: withAppTypes<{ - viewportId: string; - element: HTMLElement; -}>): ReactNode { - const { viewportActionCornersService, viewportGridService } = servicesManager.services; - - const segmentations = useSegmentations({ servicesManager }); - - const activeViewportId = viewportGridService.getActiveViewportId(); - const isActiveViewport = viewportId === activeViewportId; - - const { align, side } = getAlignAndSide(viewportActionCornersService, location); - - if (!segmentations?.length) { - return null; - } - - return ( - - - - - - - - - ); -} - -const getAlignAndSide = (viewportActionCornersService, location) => { - const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS; - - switch (location) { - case ViewportActionCornersLocations.topLeft: - return { align: 'start', side: 'bottom' }; - case ViewportActionCornersLocations.topRight: - return { align: 'end', side: 'bottom' }; - case ViewportActionCornersLocations.bottomLeft: - return { align: 'start', side: 'top' }; - case ViewportActionCornersLocations.bottomRight: - return { align: 'end', side: 'top' }; - default: - console.debug('Unknown location, defaulting to bottom-start'); - return { align: 'start', side: 'bottom' }; - } -}; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx index 5933a82cd..729288ec8 100644 --- a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from 'react'; -import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper'; +import { ViewportDataOverlayMenuWrapper } from './ViewportDataOverlayMenuWrapper'; export function getViewportDataOverlaySettingsMenu( props: withAppTypes<{ @@ -7,5 +7,5 @@ export function getViewportDataOverlaySettingsMenu( element: HTMLElement; }> ): ReactNode { - return ; + return ; } diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/utils.ts b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/utils.ts new file mode 100644 index 000000000..17d1a9a33 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/utils.ts @@ -0,0 +1,156 @@ +import { utilities as csUtils } from '@cornerstonejs/core'; + +export const DEFAULT_COLORMAP = 'hsv'; +export const DEFAULT_OPACITY = 0.5; +export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100; +export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT']; + +/** + * Get modality-specific color and opacity settings from the customization service + */ +export function getModalityOverlayColormap(customizationService, modality) { + const modalityOverlayDefaultColorMaps = customizationService?.getCustomization( + 'cornerstone.modalityOverlayDefaultColorMaps' + ) || { defaultSettings: {} }; + + return ( + modalityOverlayDefaultColorMaps.defaultSettings[modality] || { + colormap: DEFAULT_COLORMAP, + opacity: DEFAULT_OPACITY, + } + ); +} + +/** + * Identifies display sets that can be used as overlays for a specific viewport. + * + * "Enhanced" display sets are those that: + * 1. Are not already in the viewport + * 2. Are evaluated for their ability to be overlaid onto the background display set + * 3. Have an "isOverlayable" flag indicating if they're compatible with the viewport + * + * A display set is considered overlayable when: + * - The background display set is reconstructable + * - The display set is not unsupported + * - The Frame of Reference matches the background display set + * - For non-derived modalities: background can be a volume and display set is either multiframe or valid volume + * + * @returns {Object} Object containing: + * - viewportDisplaySets: Display sets already in the viewport + * - enhancedDisplaySets: Other display sets with overlayability assessment + */ +export function getEnhancedDisplaySets({ viewportId, services }) { + const { displaySetService, viewportGridService } = services; + const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + const allDisplaySets = displaySetService.getActiveDisplaySets(); + + const otherDisplaySets = allDisplaySets.filter( + displaySet => !displaySetsUIDs.includes(displaySet.displaySetInstanceUID) + ); + + const viewportDisplaySets = displaySetsUIDs.map(displaySetUID => + displaySetService.getDisplaySetByUID(displaySetUID) + ); + + const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds); + const backgroundDisplaySet = viewportDisplaySets[0]; + + const enhancedDisplaySets = otherDisplaySets.map(displaySet => { + if (!backgroundDisplaySet.isReconstructable) { + return { + ...displaySet, + isOverlayable: false, + }; + } + + if (displaySet.unsupported) { + return { + ...displaySet, + isOverlayable: false, + }; + } + + // Check if Frame of Reference matches + if ( + displaySet.FrameOfReferenceUID && + displaySet.FrameOfReferenceUID !== backgroundDisplaySet.FrameOfReferenceUID + ) { + return { + ...displaySet, + isOverlayable: false, + }; + } + + // Special handling for derived modalities + if (!DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality)) { + if (!backgroundCanBeVolume) { + return { + ...displaySet, + isOverlayable: false, + }; + } + + const imageIds = displaySet.imageIds || displaySet.images?.map(image => image.imageId); + const isMultiframe = displaySet.isMultiFrame; + + if (!isMultiframe && imageIds?.length > 0 && !csUtils.isValidVolume(imageIds)) { + return { + ...displaySet, + isOverlayable: false, + }; + } + } + + return { + ...displaySet, + isOverlayable: true, + }; + }); + + return { + viewportDisplaySets, + enhancedDisplaySets, + }; +} + +/** + * Sort function: puts disabled items (isOverlayable: false) at the end + */ +export const sortByOverlayable = (a, b) => { + if (a.isOverlayable === b.isOverlayable) { + return 0; + } + return a.isOverlayable ? -1 : 1; +}; + +/** + * Create display set options based on modality and opacity settings + */ +export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) { + if (displaySet.Modality === 'SEG') { + return {}; + } + + const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality); + return { + colormap: { + name: modalitySettings.colormap || DEFAULT_COLORMAP, + opacity: opacity / 100, // Convert from percentage to 0-1 range + }, + }; +} + +/** + * Get segmentations that can be added as overlays to the viewport + * + * Note: This function is deprecated as we now use display sets for segmentations + */ +export function getAvailableSegmentations(segmentationService) { + const segmentations = segmentationService.getSegmentations() || []; + return segmentations.map(segmentation => ({ + segmentationId: segmentation.segmentationId, + label: segmentation.label || 'Segmentation', + segments: segmentation.segments, + frameOfReferenceUID: segmentation.frameOfReferenceUID, + })); +} diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx new file mode 100644 index 000000000..0ea4160f7 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Icons, useViewportGrid } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; +import { Enums } from '@cornerstonejs/core'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + Button, +} from '@ohif/ui-next'; + +function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) { + const { servicesManager, commandsManager } = useSystem(); + const [viewportGridState, viewportGridService] = useViewportGrid(); + const { cornerstoneViewportService, displaySetService } = servicesManager.services; + + const viewportId = viewportGridState.activeViewportId; + + const handleOrientationChange = (orientation: string) => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const currentViewportType = viewportInfo?.getViewportType(); + + // Get the displaySets in this viewport + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + const displaySets = displaySetUIDs + .map(uid => displaySetService.getDisplaySetByUID(uid)) + .filter(Boolean); + + if (!displaySets.length) { + return; + } + + // Check if at least one displaySet is reconstructable + const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable); + + if (!hasReconstructableDisplaySet) { + console.warn('Cannot change orientation: No reconstructable display sets in viewport'); + return; + } + + // Set the orientation enum based on the selected orientation + let orientationEnum; + switch (orientation.toLowerCase()) { + case 'axial': + orientationEnum = Enums.OrientationAxis.AXIAL; + break; + case 'sagittal': + orientationEnum = Enums.OrientationAxis.SAGITTAL; + break; + case 'coronal': + orientationEnum = Enums.OrientationAxis.CORONAL; + break; + default: + orientationEnum = Enums.OrientationAxis.ACQUISITION; + } + + // If viewport is not already a volume type, we need to convert it + if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) { + // Configure the viewport to be a volume viewport with current display sets + const updatedViewport = { + viewportId, + displaySetInstanceUIDs: displaySetUIDs, + viewportOptions: { + viewportType: Enums.ViewportType.ORTHOGRAPHIC, + orientation: orientationEnum, + }, + displaySetOptions: displaySetUIDs.map(() => ({})), + }; + + // Update the viewport to be a volume viewport + commandsManager.run('setDisplaySetsForViewports', { + viewportsToUpdate: [updatedViewport], + }); + } else { + // Set the viewport orientation + commandsManager.runCommand('setViewportOrientation', { + viewportId, + orientation: orientationEnum, + }); + } + }; + const { viewportActionCornersService } = servicesManager.services; + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + const displaySets = displaySetUIDs + .map(uid => displaySetService.getDisplaySetByUID(uid)) + .filter(Boolean); + + if (!displaySets.length) { + return null; + } + + const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable); + + // Get proper alignment and side based on the location + let align = 'center'; + let side = 'bottom'; + + if (location !== undefined) { + const positioning = viewportActionCornersService.getAlignAndSide(location); + align = positioning.align; + side = positioning.side; + } + + return ( + + + + + + Orientation + handleOrientationChange('axial')}>Axial + handleOrientationChange('sagittal')}> + Sagittal + + handleOrientationChange('coronal')}> + Coronal + + handleOrientationChange('acquisition')}> + Acquisition + + + + ); +} + +export default ViewportOrientationMenu; diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx new file mode 100644 index 000000000..efd85b3cb --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from 'react'; +import { useViewportGrid } from '@ohif/ui-next'; +import { cn } from '@ohif/ui-next'; +import ViewportOrientationMenu from './ViewportOrientationMenu'; + +export function ViewportOrientationMenuWrapper({ + viewportId, + location, +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; + location: string; +}>): ReactNode { + const [viewportGrid] = useViewportGrid(); + const isActiveViewport = viewportId === viewportGrid.activeViewportId; + + return ( +
+ +
+ ); +} diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/index.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/index.tsx new file mode 100644 index 000000000..2e0c23e93 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/index.tsx @@ -0,0 +1,14 @@ +import React, { ReactNode } from 'react'; +import { ViewportOrientationMenuWrapper } from './ViewportOrientationMenuWrapper'; + +export function getViewportOrientationMenu( + props: withAppTypes<{ + viewportId: string; + element: HTMLElement; + location: string; + }> +): ReactNode { + return ; +} + +export * from './ViewportOrientationMenu'; diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx index 503f17412..7b740f8d0 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx @@ -1,18 +1,18 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import { Switch } from '@ohif/ui-next'; -import { StackViewport, VolumeViewport } from '@cornerstonejs/core'; +import { StackViewport } from '@cornerstonejs/core'; import { ColorbarProps } from '../../types/Colorbar'; import { utilities } from '@cornerstonejs/core'; +import { useSystem } from '@ohif/core'; export function setViewportColorbar( viewportId, - displaySets, commandsManager, servicesManager: AppTypes.ServicesManager, colorbarOptions ) { - const { cornerstoneViewportService } = servicesManager.services; - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { cornerstoneViewportService, viewportGridService } = servicesManager.services; + const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); const backgroundColor = viewportInfo.getViewportOptions().background; @@ -32,36 +32,18 @@ export function setViewportColorbar( }; } - const displaySetInstanceUIDs = []; - - if (viewport instanceof StackViewport) { - displaySetInstanceUIDs.push(viewportId); - } - - if (viewport instanceof VolumeViewport) { - displaySets.forEach(ds => { - displaySetInstanceUIDs.push(ds.displaySetInstanceUID); - }); - } - - commandsManager.run({ - commandName: 'toggleViewportColorbar', - commandOptions: { - viewportId, - options: colorbarOptions, - displaySetInstanceUIDs, - }, - context: 'CORNERSTONE', + commandsManager.run('toggleViewportColorbar', { + viewportId, + options: colorbarOptions, + displaySetInstanceUIDs, }); } export function Colorbar({ viewportId, - displaySets, - commandsManager, - servicesManager, colorbarProperties, }: withAppTypes): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { colorbarService } = servicesManager.services; const { width: colorbarWidth, @@ -73,7 +55,7 @@ export function Colorbar({ const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId)); const onSetColorbar = useCallback(() => { - setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, { + setViewportColorbar(viewportId, commandsManager, servicesManager, { viewportId, colormaps, ticks: { diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx index 6bf57c324..6b15d85d9 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx @@ -3,14 +3,10 @@ import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next import { StackViewport, Types } from '@cornerstonejs/core'; import { ColormapProps } from '../../types/Colormap'; +import { useSystem } from '@ohif/core'; -export function Colormap({ - colormaps, - viewportId, - displaySets, - commandsManager, - servicesManager, -}: ColormapProps): ReactElement { +export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { cornerstoneViewportService } = servicesManager.services; const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]); diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx index f602f9294..5883a5f98 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx @@ -1,13 +1,10 @@ import React, { ReactElement, useState, useEffect, useCallback } from 'react'; import { VolumeLightingProps } from '../../types/ViewportPresets'; import { Numeric } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; -export function VolumeLighting({ - servicesManager, - commandsManager, - viewportId, - hasShade, -}: VolumeLightingProps): ReactElement { +export function VolumeLighting({ viewportId, hasShade }: VolumeLightingProps): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { cornerstoneViewportService } = servicesManager.services; const [lightingValues, setLightingValues] = useState({ ambient: null, diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx index d87fa1f13..f55428240 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx @@ -5,43 +5,31 @@ import { VolumeRenderingQuality } from './VolumeRenderingQuality'; import { VolumeShift } from './VolumeShift'; import { VolumeLighting } from './VolumeLighting'; import { VolumeShade } from './VolumeShade'; + export function VolumeRenderingOptions({ viewportId, - commandsManager, volumeRenderingQualityRange, - servicesManager, }: VolumeRenderingOptionsProps): ReactElement { const [hasShade, setShade] = useState(false); return ( - - +
Lighting
diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx index a492a0741..195f57b67 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx @@ -3,13 +3,13 @@ import { Icons } from '@ohif/ui-next'; import React, { ReactElement } from 'react'; import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets'; import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent'; +import { useSystem } from '@ohif/core'; export function VolumeRenderingPresets({ viewportId, - servicesManager, - commandsManager, volumeRenderingPresets, }: VolumeRenderingPresetsProps): ReactElement { + const { servicesManager } = useSystem(); const { uiDialogService } = servicesManager.services; const onClickPresets = () => { @@ -21,7 +21,6 @@ export function VolumeRenderingPresets({ contentProps: { presets: volumeRenderingPresets, viewportId, - commandsManager, }, }); }; diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx index 3b02f5d26..1536eafed 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx @@ -1,13 +1,13 @@ import React, { ReactElement, useCallback, useState, useEffect } from 'react'; import { VolumeRenderingQualityProps } from '../../types/ViewportPresets'; import { Numeric } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; export function VolumeRenderingQuality({ volumeRenderingQualityRange, - commandsManager, - servicesManager, viewportId, }: VolumeRenderingQualityProps): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { cornerstoneViewportService } = servicesManager.services; const { min, max, step } = volumeRenderingQualityRange; const [quality, setQuality] = useState(null); diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx index 5be08d3d4..c374956b7 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx @@ -1,13 +1,13 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import { Switch } from '@ohif/ui-next'; import { VolumeShadeProps } from '../../types/ViewportPresets'; +import { useSystem } from '@ohif/core'; export function VolumeShade({ - commandsManager, viewportId, - servicesManager, onClickShade = bool => {}, }: VolumeShadeProps): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { cornerstoneViewportService } = servicesManager.services; const [shade, setShade] = useState(true); const [key, setKey] = useState(0); diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx index c6cb4c6f2..ac67feaea 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx @@ -1,12 +1,10 @@ import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react'; import { VolumeShiftProps } from '../../types/ViewportPresets'; import { Numeric } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; -export function VolumeShift({ - viewportId, - commandsManager, - servicesManager, -}: VolumeShiftProps): ReactElement { +export function VolumeShift({ viewportId }: VolumeShiftProps): ReactElement { + const { servicesManager, commandsManager } = useSystem(); const { cornerstoneViewportService } = servicesManager.services; const [minShift, setMinShift] = useState(null); const [maxShift, setMaxShift] = useState(null); diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx index 49152cd69..ada20bac0 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx @@ -1,20 +1,16 @@ import React, { ReactElement, useCallback } from 'react'; import { AllInOneMenu } from '@ohif/ui-next'; import { WindowLevelPreset } from '../../types/WindowLevel'; -import { CommandsManager } from '@ohif/core'; +import { useSystem } from '@ohif/core'; import { useTranslation } from 'react-i18next'; export type WindowLevelProps = { viewportId: string; presets: Array>>; - commandsManager: CommandsManager; }; -export function WindowLevel({ - viewportId, - commandsManager, - presets, -}: WindowLevelProps): ReactElement { +export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement { + const { commandsManager } = useSystem(); const { t } = useTranslation('WindowLevelActionMenu'); const onSetWindowLevel = useCallback( diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx index d9e10663a..add141fa4 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx @@ -15,8 +15,7 @@ import { VolumeRenderingOptions } from './VolumeRenderingOptions'; import { ViewportPreset } from '../../types/ViewportPresets'; import { VolumeViewport3D } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/core'; - -export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; +import { useSystem } from '@ohif/core'; export type WindowLevelActionMenuProps = { viewportId: string; @@ -34,13 +33,12 @@ export function WindowLevelActionMenu({ presets, verticalDirection, horizontalDirection, - commandsManager, - servicesManager, colorbarProperties, displaySets, volumeRenderingPresets, volumeRenderingQualityRange, }: withAppTypes): ReactElement { + const { commandsManager, servicesManager } = useSystem(); const { colormaps, colorbarContainerPosition, @@ -48,6 +46,7 @@ export function WindowLevelActionMenu({ colorbarTickPosition, width: colorbarWidth, } = colorbarProperties; + const { colorbarService, cornerstoneViewportService } = servicesManager.services; const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); @@ -64,7 +63,7 @@ export function WindowLevelActionMenu({ const [is3DVolume, setIs3DVolume] = useState(false); const onSetColorbar = useCallback(() => { - setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, { + setViewportColorbar(viewportId, commandsManager, servicesManager, { colormaps, ticks: { position: colorbarTickPosition, @@ -117,7 +116,6 @@ export function WindowLevelActionMenu({ verticalDirection={verticalDirection} horizontalDirection={horizontalDirection} iconClassName={classNames( - // Visible on hover and for the active viewport activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible', 'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight', isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30' @@ -132,9 +130,7 @@ export function WindowLevelActionMenu({ {!is3DVolume && ( !nonWLModalities.includes(ds.Modality))} - commandsManager={commandsManager} - servicesManager={servicesManager} + displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)} colorbarProperties={colorbarProperties} /> )} @@ -150,9 +146,7 @@ export function WindowLevelActionMenu({ className="flex h-full w-full flex-col" colormaps={colormaps} viewportId={viewportId} - displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))} - commandsManager={commandsManager} - servicesManager={servicesManager} + displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)} /> )} @@ -165,7 +159,6 @@ export function WindowLevelActionMenu({ > @@ -173,9 +166,7 @@ export function WindowLevelActionMenu({ {volumeRenderingPresets && is3DVolume && ( )} @@ -184,9 +175,7 @@ export function WindowLevelActionMenu({ )} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx index f00e045ef..0bb74feea 100644 --- a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx @@ -1,12 +1,12 @@ import React, { ReactNode } from 'react'; -import { nonWLModalities } from './WindowLevelActionMenu'; +import { WindowLevelActionMenuProps } from './WindowLevelActionMenu'; +import { useSystem } from '@ohif/core'; +import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu'; -export function getWindowLevelActionMenu({ +function WindowLevelActionMenu({ viewportId, element, displaySets, - servicesManager, - commandsManager, verticalDirection, horizontalDirection, }: withAppTypes<{ @@ -14,31 +14,25 @@ export function getWindowLevelActionMenu({ element: HTMLElement; displaySets: AppTypes.DisplaySet[]; }>): ReactNode { + const { servicesManager } = useSystem(); const { customizationService } = servicesManager.services; const presets = customizationService.getCustomization('cornerstone.windowLevelPresets'); const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar'); const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.getCustomization('cornerstone.3dVolumeRendering'); - const WindowLevelActionMenu = customizationService.getCustomization( - 'viewportActionMenu.windowLevelActionMenu' - ); const displaySetPresets = displaySets .filter(displaySet => presets[displaySet.Modality]) .map(displaySet => { return { [displaySet.Modality]: presets[displaySet.Modality] }; }); - const modalities = displaySets - .map(displaySet => displaySet.Modality) - .filter(modality => !nonWLModalities.includes(modality)); + const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel); if (modalities.length === 0) { return null; } - const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component; - return ( ); } + +export function getWindowLevelActionMenu(props: WindowLevelActionMenuProps) { + return ; +} diff --git a/extensions/cornerstone/src/constants/index.ts b/extensions/cornerstone/src/constants/index.ts new file mode 100644 index 000000000..8469adf7b --- /dev/null +++ b/extensions/cornerstone/src/constants/index.ts @@ -0,0 +1,3 @@ +// Volume loader schemes +export const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; +export const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; \ No newline at end of file diff --git a/extensions/cornerstone/src/customizations/modalityColorMapCustomization.ts b/extensions/cornerstone/src/customizations/modalityColorMapCustomization.ts new file mode 100644 index 000000000..22c16b854 --- /dev/null +++ b/extensions/cornerstone/src/customizations/modalityColorMapCustomization.ts @@ -0,0 +1,23 @@ +export default { + 'cornerstone.modalityOverlayDefaultColorMaps': { + defaultSettings: { + PT: { + colormap: 'hsv', + // Note: Right now, there is a nonlinear relationship between the opacity value + // below and how it will get applied to the image. The limitation is in rendering. + // We are working on this and will remove this note when it's fixed. + // But don't expect 0.5 to be 50% opacity, but rather close to that. + opacity: 0.5, + }, + RTDOSE: { + colormap: 'Isodose', + // Note: Right now, there is a nonlinear relationship between the opacity value + // below and how it will get applied to the image. The limitation is in rendering. + // We are working on this and will remove this note when it's fixed. + // But don't expect 0.5 to be 50% opacity, but rather close to that. + + opacity: 0.5, + }, + }, + }, +}; diff --git a/extensions/cornerstone/src/customizations/viewportActionMenuCustomizations.ts b/extensions/cornerstone/src/customizations/viewportActionMenuCustomizations.ts index 8bff4eea4..7d91d47fd 100644 --- a/extensions/cornerstone/src/customizations/viewportActionMenuCustomizations.ts +++ b/extensions/cornerstone/src/customizations/viewportActionMenuCustomizations.ts @@ -1,14 +1,55 @@ -import viewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService'; -import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu'; +import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu'; +import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu'; +import { getViewportOrientationMenu } from '../components/ViewportOrientationMenu'; +import { AllInOneMenu } from '@ohif/ui-next'; + +// Generate component renderer functions for each component type +const createOrientationMenu = ({ viewportId, element, location }) => { + return getViewportOrientationMenu({ + viewportId, + element, + location, + }); +}; + +const createDataOverlay = ({ viewportId, element, displaySets, location }) => { + return getViewportDataOverlaySettingsMenu({ + viewportId, + element, + displaySets, + location, + }); +}; + +const createWindowLevelMenu = ({ viewportId, element, displaySets }) => { + return getWindowLevelActionMenu({ + viewportId, + element, + displaySets, + verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom, + horizontalDirection: AllInOneMenu.HorizontalDirection.LeftToRight, + }); +}; export default { - 'viewportActionMenu.windowLevelActionMenu': { - enabled: true, - location: viewportActionCornersService.LOCATIONS.topRight, - component: WindowLevelActionMenu, - }, - 'viewportActionMenu.segmentationOverlay': { - enabled: true, - location: viewportActionCornersService.LOCATIONS.topRight, - }, + 'viewportActionMenu.topLeft': [ + { + id: 'orientationMenu', + enabled: true, + component: createOrientationMenu, + }, + { + id: 'dataOverlay', + enabled: true, + component: createDataOverlay, + }, + { + id: 'windowLevelActionMenu', + enabled: true, + component: createWindowLevelMenu, + }, + ], + 'viewportActionMenu.topRight': [], + 'viewportActionMenu.bottomLeft': [], + 'viewportActionMenu.bottomRight': [], }; diff --git a/extensions/cornerstone/src/getCustomizationModule.tsx b/extensions/cornerstone/src/getCustomizationModule.tsx index 8a17c0fe5..0f2f2f6c1 100644 --- a/extensions/cornerstone/src/getCustomizationModule.tsx +++ b/extensions/cornerstone/src/getCustomizationModule.tsx @@ -6,6 +6,7 @@ import viewportClickCommandsCustomization from './customizations/viewportClickCo import measurementsCustomization from './customizations/measurementsCustomization'; import volumeRenderingCustomization from './customizations/volumeRenderingCustomization'; import colorbarCustomization from './customizations/colorbarCustomization'; +import modalityColorMapCustomization from './customizations/modalityColorMapCustomization'; import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization'; import miscCustomization from './customizations/miscCustomization'; import captureViewportModalCustomization from './customizations/captureViewportModalCustomization'; @@ -25,6 +26,7 @@ function getCustomizationModule({ commandsManager, servicesManager }) { ...measurementsCustomization, ...volumeRenderingCustomization, ...colorbarCustomization, + ...modalityColorMapCustomization, ...windowLevelPresetsCustomization, ...miscCustomization, ...captureViewportModalCustomization, diff --git a/extensions/cornerstone/src/getSopClassHandlerModule.js b/extensions/cornerstone/src/getSopClassHandlerModule.js index f88dc66ae..a8accad0c 100644 --- a/extensions/cornerstone/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone/src/getSopClassHandlerModule.js @@ -1,4 +1,5 @@ import OHIF from '@ohif/core'; +import i18n from '@ohif/i18n'; import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core'; import dcmjs from 'dcmjs'; import { dicomWebUtils } from '@ohif/extension-default'; @@ -112,6 +113,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) instances, othersFrameOfReferenceUID, imageIds: instances.map(instance => instance.imageId), + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SM')}`, }; // The microscopy viewer directly accesses the metadata already loaded, and // uses the DICOMweb client library directly for loading, so it has to be diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index 7279bbfee..7e5cbc36f 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -233,7 +233,7 @@ function _evaluateToggle({ } const toolName = toolbarService.getToolNameForButton(button); - if (!toolGroup.hasTool(toolName)) { + if (!toolGroup?.hasTool(toolName)) { return getDisabledState(disabledText); } diff --git a/extensions/cornerstone/src/hooks/index.ts b/extensions/cornerstone/src/hooks/index.ts new file mode 100644 index 000000000..033962d35 --- /dev/null +++ b/extensions/cornerstone/src/hooks/index.ts @@ -0,0 +1,5 @@ +export { default as useViewportActionCorners } from './useViewportActionCorners'; +export * from './useViewportActionCorners'; +export * from './useActiveViewportSegmentationRepresentations'; +export * from './useMeasurements'; +export * from './useSegmentations'; \ No newline at end of file diff --git a/extensions/cornerstone/src/hooks/useSegmentations.ts b/extensions/cornerstone/src/hooks/useSegmentations.ts index 3c258654e..66434f7c2 100644 --- a/extensions/cornerstone/src/hooks/useSegmentations.ts +++ b/extensions/cornerstone/src/hooks/useSegmentations.ts @@ -84,7 +84,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) { * @param options.debounceTime - Debounce time in milliseconds for updates. * @returns An array of segmentation data. */ -export function useSegmentations(options: { +export function useSegmentations(options?: { subscribeToDataModified?: boolean; debounceTime?: number; }): SegmentationData[] { diff --git a/extensions/cornerstone/src/hooks/useViewportActionCorners.ts b/extensions/cornerstone/src/hooks/useViewportActionCorners.ts new file mode 100644 index 000000000..765053e73 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useViewportActionCorners.ts @@ -0,0 +1,113 @@ +import { useEffect } from 'react'; +import React from 'react'; +import { ViewportActionCornersLocations } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core/src'; + +/** + * Viewport action corner location type + */ +export interface ViewportActionCornerLocation { + type: 'VIEWPORT'; + id: string; + corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +} + +/** + * Viewport action corner service interface + */ +export interface ViewportActionCornerService { + addComponent: (params: { + viewportId: string; + id: string; + component: React.ReactNode; + location: ViewportActionCornersLocations | string; + indexPriority?: number; + }) => void; + clear: (viewportId: string) => void; + getAlignAndSide: (location: ViewportActionCornersLocations | string) => { + align: 'start' | 'center' | 'end'; + side: 'top' | 'right' | 'bottom' | 'left'; + }; +} + +/** + * Customization service interface + */ +export interface CustomizationService { + getCustomization: (name: string) => any; +} + +/** + * Props for the useViewportActionCorners hook + */ +interface UseViewportActionCornersProps { + viewportId: string; + elementRef: React.MutableRefObject; + displaySets: AppTypes.DisplaySet[]; +} + +// Map of customization keys to their corresponding enum values +const locationMap = { + 'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft, + 'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight, + 'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft, + 'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight, +}; + +/** + * Hook to manage viewport action corners for a Cornerstone viewport + */ +export function useViewportActionCorners({ + viewportId, + elementRef, + displaySets, +}: UseViewportActionCornersProps): void { + const { servicesManager } = useSystem(); + const { viewportActionCornersService, customizationService } = servicesManager.services; + + useEffect(() => { + // Process each location + Object.entries(locationMap).forEach(([locationKey, locationValue]) => { + const items = customizationService.getCustomization(locationKey); + + if (!items || !items.length) { + return; + } + + items.forEach(item => { + if (!item.enabled) { + return; + } + + const componentId = item.id; + + if (item.component) { + // Use the component renderer provided directly in the item + const component = item.component({ + viewportId, + element: elementRef.current, + displaySets, + location: locationValue, + }); + + viewportActionCornersService.addComponent({ + viewportId, + id: componentId, + component, + location: locationValue, + }); + } else if (item.component) { + // Handle static components + viewportActionCornersService.addComponent({ + viewportId, + id: item.id, + component: item.component, + location: locationValue, + }); + } + }); + }); + }, [displaySets, viewportId, viewportActionCornersService, elementRef]); +} + +export default useViewportActionCorners; diff --git a/extensions/cornerstone/src/hooks/useViewportDisplaySets.ts b/extensions/cornerstone/src/hooks/useViewportDisplaySets.ts new file mode 100644 index 000000000..cca79a109 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useViewportDisplaySets.ts @@ -0,0 +1,247 @@ +import { useMemo } from 'react'; +import { useSystem, utils } from '@ohif/core'; +import { useViewportGrid } from '@ohif/ui-next'; +import { + getEnhancedDisplaySets, + sortByOverlayable, + DERIVED_OVERLAY_MODALITIES, +} from '../components/ViewportDataOverlaySettingMenu/utils'; + +const sortByPriority = (a, b) => { + if (utils.isLowPriorityModality(a.Modality)) { + return 1; + } + return -1; +}; + +/** + * Options for the useViewportDisplaySets hook + */ +export type UseViewportDisplaySetsOptions = { + /** + * Whether to include background display set + */ + includeBackground?: boolean; + /** + * Whether to include foreground display sets + */ + includeForeground?: boolean; + /** + * Whether to include overlay display sets + */ + includeOverlay?: boolean; + /** + * Whether to include potential overlay display sets + */ + includePotentialOverlay?: boolean; + /** + * Whether to include potential foreground display sets + */ + includePotentialForeground?: boolean; + /** + * Whether to include potential background display sets + */ + includePotentialBackground?: boolean; +}; + +/** + * Return type for useViewportDisplaySets + */ +export type ViewportDisplaySets = { + /** + * The primary display set for the viewport (base image) + */ + backgroundDisplaySet?: any; + /** + * Display sets currently shown with background (non-overlay layers) + */ + foregroundDisplaySets?: any[]; + /** + * Segmentation display sets currently applied as overlays + */ + overlayDisplaySets?: any[]; + /** + * Display sets that could be toggled on as overlays (derived modalities) + */ + potentialOverlayDisplaySets?: any[]; + /** + * Display sets that could be added as foreground layers + */ + potentialForegroundDisplaySets?: any[]; + /** + * Display sets that could replace the current background + */ + potentialBackgroundDisplaySets?: any[]; +}; + +/** + * Hook to provide display sets and overlay information for a viewport based on options. + * + * @param viewportId - The viewport ID to get display sets for + * @param options - Options to control which display sets to compute + * @returns Object containing requested display set collections based on options + */ +export function useViewportDisplaySets( + viewportId: string, + options?: UseViewportDisplaySetsOptions +): ViewportDisplaySets { + const { servicesManager } = useSystem(); + const { displaySetService, segmentationService } = servicesManager.services; + + // Note: this is very important we should use the useViewportGrid hook here, + // since if the viewport displaySet is changed we should re-run this hook + // to get the latest displaySets + const [_, viewportGridService] = useViewportGrid(); + + // Apply defaults - include everything if no options specified + const { + includeBackground = true, + includeForeground = true, + includeOverlay = true, + includePotentialOverlay = true, + includePotentialForeground = true, + includePotentialBackground = true, + } = options || {}; + + // Get all available display sets (only if needed) + const needsAllDisplaySets = includePotentialBackground; + const allDisplaySets = useMemo( + () => (needsAllDisplaySets ? displaySetService.getActiveDisplaySets() : []), + [displaySetService, needsAllDisplaySets] + ); + + // Get all available segmentations (only if needed) + const needsSegmentations = includeOverlay; + const segmentationRepresentations = useMemo( + () => + needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportId) : [], + [segmentationService, viewportId, needsSegmentations] + ); + + const overlayDisplaySets = useMemo(() => { + if (!includeOverlay) { + return []; + } + return segmentationRepresentations.map(repr => { + const displaySet = displaySetService.getDisplaySetByUID(repr.segmentationId); + return displaySet; + }); + }, [includeOverlay, segmentationRepresentations, displaySetService]); + + const overlayDisplaySetUIDs = useMemo( + () => overlayDisplaySets.map(ds => ds.displaySetInstanceUID), + [overlayDisplaySets] + ); + + // Get enhanced display sets (only if needed) + const needsEnhancedDisplaySets = + includeBackground || includeForeground || includePotentialOverlay || includePotentialForeground; + + const { viewportDisplaySets, enhancedDisplaySets } = useMemo(() => { + if (!needsEnhancedDisplaySets) { + return { viewportDisplaySets: [], enhancedDisplaySets: [] }; + } + return getEnhancedDisplaySets({ + viewportId, + services: { displaySetService, viewportGridService }, + }); + }, [viewportId, displaySetService, viewportGridService, needsEnhancedDisplaySets]); + + const backgroundDisplaySet = useMemo( + () => (includeBackground ? viewportDisplaySets[0] : undefined), + [includeBackground, viewportDisplaySets] + ); + + const foregroundDisplaySets = useMemo(() => { + if (!includeForeground || !backgroundDisplaySet) { + return []; + } + return viewportDisplaySets.filter( + ds => + !DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) && + ds.displaySetInstanceUID !== backgroundDisplaySet.displaySetInstanceUID + ); + }, [includeForeground, viewportDisplaySets, backgroundDisplaySet]); + + const foregroundDisplaySetUIDs = useMemo( + () => foregroundDisplaySets.map(ds => ds.displaySetInstanceUID), + [foregroundDisplaySets] + ); + + const potentialOverlayDisplaySets = useMemo(() => { + if (!includePotentialOverlay) { + return []; + } + return enhancedDisplaySets + .filter( + ds => + DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) && + !overlayDisplaySetUIDs.includes(ds.displaySetInstanceUID) && + ds.isOverlayable + ) + .sort(sortByOverlayable); + }, [includePotentialOverlay, enhancedDisplaySets, overlayDisplaySetUIDs]); + + const potentialForegroundDisplaySets = useMemo(() => { + if (!includePotentialForeground) { + return []; + } + return enhancedDisplaySets + .filter( + ds => + !DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) && + !foregroundDisplaySetUIDs.includes(ds.displaySetInstanceUID) && + ds.isOverlayable + ) + .sort(sortByPriority); + }, [includePotentialForeground, enhancedDisplaySets, foregroundDisplaySetUIDs]); + + const potentialBackgroundDisplaySets = useMemo(() => { + if (!includePotentialBackground || !backgroundDisplaySet) { + return []; + } + return allDisplaySets + .filter( + ds => + !DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) && + ds.displaySetInstanceUID !== backgroundDisplaySet.displaySetInstanceUID && + !overlayDisplaySetUIDs.includes(ds.displaySetInstanceUID) && + !foregroundDisplaySetUIDs.includes(ds.displaySetInstanceUID) + ) + .sort(sortByPriority); + }, [ + includePotentialBackground, + allDisplaySets, + backgroundDisplaySet, + overlayDisplaySetUIDs, + foregroundDisplaySetUIDs, + ]); + + const result: ViewportDisplaySets = {}; + + if (includeBackground) { + result.backgroundDisplaySet = backgroundDisplaySet; + } + + if (includeForeground) { + result.foregroundDisplaySets = foregroundDisplaySets; + } + + if (includeOverlay) { + result.overlayDisplaySets = overlayDisplaySets; + } + + if (includePotentialOverlay) { + result.potentialOverlayDisplaySets = potentialOverlayDisplaySets; + } + + if (includePotentialForeground) { + result.potentialForegroundDisplaySets = potentialForegroundDisplaySets; + } + + if (includePotentialBackground) { + result.potentialBackgroundDisplaySets = potentialBackgroundDisplaySets; + } + + return result; +} diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index fb29254af..9deebc1e4 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -269,4 +269,7 @@ export { CornerstoneViewportDownloadForm, utils, }; + +// Export constants +export { VOLUME_LOADER_SCHEME, DYNAMIC_VOLUME_LOADER_SCHEME } from './constants'; export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index a7ce82140..562443797 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -96,6 +96,8 @@ export default async function init({ viewportGridService, segmentationService, measurementService, + colorbarService, + displaySetService, } = servicesManager.services; window.services = servicesManager.services; @@ -255,8 +257,17 @@ export default async function init({ eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler); eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler); + const getDisplaySetFromVolumeId = (volumeId: string) => { + const allDisplaySets = displaySetService.getActiveDisplaySets(); + const volume = cornerstone.cache.getVolume(volumeId); + const imageIds = volume.imageIds; + return allDisplaySets.find(ds => ds.imageIds?.some(id => imageIds.includes(id))); + }; + function elementEnabledHandler(evt) { const { element } = evt.detail; + const { viewport } = getEnabledElement(element); + initViewTiming({ element }); element.addEventListener(EVENTS.CAMERA_RESET, evt => { const { element } = evt.detail; @@ -268,7 +279,10 @@ export default async function init({ commandsManager.runCommand('resetCrosshairs', { viewportId }); }); - initViewTiming({ element }); + // limitation: currently supporting only volume viewports with fusion + if (viewport.type !== cornerstone.Enums.ViewportType.ORTHOGRAPHIC) { + return; + } } eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); @@ -294,12 +308,48 @@ export default async function init({ 100 ); + // Subscribe to actor events to dynamically update colorbars + // Call this function when initializing initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService); + const { unsubscribe: unsubscribeSegmentationCreated } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_ADDED, + evt => { + const { segmentationId } = evt; + const displaySet = displaySetService.getDisplaySetByUID(segmentationId); + if (displaySet) { + return; + } + + const segmentation = segmentationService.getSegmentation(segmentationId); + const label = segmentation.cachedStats.info; + const imageIds = segmentation.representationData.Labelmap.imageIds; + + // Create a display set for the segmentation + const segmentationDisplaySet = { + displaySetInstanceUID: segmentationId, + SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4', + SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + SeriesDescription: label, + Modality: 'SEG', + numImageFrames: imageIds.length, + imageIds, + isOverlayDisplaySet: true, + label, + madeInClient: true, + segmentationId: segmentationId, + isDerived: true, + }; + + displaySetService.addDisplaySets(segmentationDisplaySet); + } + ); + const unsubscriptions = [ unsubscribeSegmentationDataModifiedHandler, unsubscribeSegmentationModifiedHandler, + unsubscribeSegmentationCreated, ]; return { unsubscriptions }; diff --git a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts index 9c62ff492..3514104b3 100644 --- a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts +++ b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts @@ -38,14 +38,16 @@ export default class ColorbarService extends PubSubService { public static REGISTRATION = { name: 'colorbarService', - create: () => { - return new ColorbarService(); + create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => { + return new ColorbarService(servicesManager); }, }; colorbars = {}; + servicesManager: AppTypes.ServicesManager; - constructor() { + constructor(servicesManager: AppTypes.ServicesManager) { super(ColorbarService.EVENTS); + this.servicesManager = servicesManager; } /** @@ -68,6 +70,7 @@ export default class ColorbarService extends PubSubService { * @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width. */ public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) { + const { displaySetService } = this.servicesManager.services; const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); const viewport = renderingEngine.getViewport(viewportId); @@ -95,6 +98,12 @@ export default class ColorbarService extends PubSubService { ); displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => { + // don't show colorbar for overlay display sets (e.g. segmentation) + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (displaySet.isOverlayDisplaySet) { + return; + } + const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID); const properties = viewport?.getProperties(volumeId); const colormap = properties?.colormap; @@ -123,9 +132,19 @@ export default class ColorbarService extends PubSubService { volumeId: viewport instanceof VolumeViewport ? volumeId : undefined, }); if (this.colorbars[viewportId]) { - this.colorbars[viewportId].push({ colorbar, container: colorbarContainer }); + this.colorbars[viewportId].push({ + colorbar, + container: colorbarContainer, + displaySetInstanceUID, + }); } else { - this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }]; + this.colorbars[viewportId] = [ + { + colorbar, + container: colorbarContainer, + displaySetInstanceUID, + }, + ]; } }); @@ -136,24 +155,50 @@ export default class ColorbarService extends PubSubService { } /** - * Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references. + * Removes a colorbar from a specific viewport. If displaySetInstanceUID is provided, + * only the colorbar associated with that specific displaySetInstanceUID will be removed. + * Otherwise, all colorbars for the given viewport will be removed. * * @param viewportId The identifier for the viewport from which the colorbar will be removed. + * @param displaySetInstanceUID Optional. The specific display set instance UID associated with the colorbar to remove. */ - public removeColorbar(viewportId) { + public removeColorbar(viewportId, displaySetInstanceUID?: string) { const colorbarInfo = this.colorbars[viewportId]; if (!colorbarInfo) { return; } - colorbarInfo.forEach(({ colorbar, container }) => { - container.parentNode.removeChild(container); - }); + if (displaySetInstanceUID) { + // Find the index of the colorbar with the matching displaySetInstanceUID + const index = colorbarInfo.findIndex( + info => info.displaySetInstanceUID === displaySetInstanceUID + ); - delete this.colorbars[viewportId]; + if (index !== -1) { + // Remove only the specific colorbar container + const { container } = colorbarInfo[index]; + container.parentNode.removeChild(container); + + // Remove the colorbar from the array + colorbarInfo.splice(index, 1); + + // If there are no more colorbars for this viewport, remove the entry + if (colorbarInfo.length === 0) { + delete this.colorbars[viewportId]; + } + } + } else { + // Remove all colorbars for the viewport (original behavior) + colorbarInfo.forEach(({ container }) => { + container.parentNode.removeChild(container); + }); + + delete this.colorbars[viewportId]; + } this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, { viewportId, + displaySetInstanceUID, changeType: ChangeTypes.Removed, }); } diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index b185f6cdd..ccec38040 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -3,8 +3,7 @@ import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; - -const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; +import { VOLUME_LOADER_SCHEME } from '../../constants'; class CornerstoneCacheService { static REGISTRATION = { diff --git a/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts b/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts index b61307412..b0396bf7a 100644 --- a/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts +++ b/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts @@ -8,26 +8,29 @@ * and geometry Id */ export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) { - return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => { - const data = contourPoints.map(({ points, ...rest }) => { - const newPoints = points.map(({ x, y, z }) => { - return [x, y, z]; + return structureSet.ROIContours.map( + ({ contourPoints, ROINumber, ROIName, colorArray, ROIGroup }) => { + const data = contourPoints.map(({ points, ...rest }) => { + const newPoints = points.map(({ x, y, z }) => { + return [x, y, z]; + }); + + return { + ...rest, + points: newPoints, + }; }); + const id = ROIName || ROINumber; + return { - ...rest, - points: newPoints, + data, + id, + segmentIndex: ROINumber, + color: colorArray, + group: ROIGroup, + geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`, }; - }); - - const id = ROIName || ROINumber; - - return { - data, - id, - segmentIndex: ROINumber, - color: colorArray, - geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`, - }; - }); + } + ); } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index e19f0ecc8..f3368bfb0 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -29,6 +29,7 @@ import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/ import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents'; import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap'; import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation'; +import { VOLUME_LOADER_SCHEME } from '../../constants'; const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap; const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour; @@ -80,8 +81,6 @@ const EVENTS = { const VALUE_TYPES = {}; -const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; - class SegmentationService extends PubSubService { static REGISTRATION = { name: 'segmentationService', @@ -615,7 +614,7 @@ class SegmentationService extends PubSubService { // Process each segment similarly to the SEG function for (const rtStructData of allRTStructData) { - const { data, id, color, segmentIndex, geometryId } = rtStructData; + const { data, id, color, segmentIndex, geometryId, group } = rtStructData; // Add the color to the colorLUT array colorLUT.push(color); @@ -646,6 +645,7 @@ class SegmentationService extends PubSubService { cachedStats: segmentsCachedStats, locked: false, active: false, + group, }; // Broadcast segment loading progress diff --git a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts index 6dc79954f..0c95194c9 100644 --- a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts +++ b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts @@ -10,6 +10,11 @@ export type ActionComponentInfo = { indexPriority?: number; }; +export type AlignAndSide = { + align: 'start' | 'end' | 'center'; + side: 'top' | 'bottom' | 'left' | 'right'; +}; + class ViewportActionCornersService extends PubSubService { public static readonly EVENTS = {}; public static readonly LOCATIONS = ViewportActionCornersLocations; @@ -66,6 +71,22 @@ class ViewportActionCornersService extends PubSubService { public clear(viewportId: string) { this.serviceImplementation._clear(viewportId); } + + public getAlignAndSide(location: ViewportActionCornersLocations): AlignAndSide { + switch (location) { + case ViewportActionCornersLocations.topLeft: + return { align: 'start', side: 'bottom' }; + case ViewportActionCornersLocations.topRight: + return { align: 'end', side: 'bottom' }; + case ViewportActionCornersLocations.bottomLeft: + return { align: 'start', side: 'top' }; + case ViewportActionCornersLocations.bottomRight: + return { align: 'end', side: 'top' }; + default: + console.debug('Unknown location, defaulting to bottom-start'); + return { align: 'start', side: 'bottom' }; + } + } } export default ViewportActionCornersService; diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index e05ce06f5..b454502c3 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -32,6 +32,7 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore'; import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; import { useSynchronizersStore } from '../../stores/useSynchronizersStore'; import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; +import { VOLUME_LOADER_SCHEME } from '../../constants'; const EVENTS = { VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', @@ -407,11 +408,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi // and we would lose the presentation. this.storePresentation({ viewportId: viewportInfo.getViewportId() }); - // Todo: i don't like this here, move it - this.servicesManager.services.segmentationService.clearSegmentationRepresentations( - viewportInfo.getViewportId() - ); - if (!viewportInfo) { throw new Error('element is not enabled for the given viewportId'); } @@ -513,6 +509,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return this.viewportsById.get(viewportId); } + public getOrientation(viewportId: string): string { + const viewportInfo = this.getViewportInfo(viewportId); + return viewportInfo.getOrientation(); + } + /** * Looks through the viewports to see if the specified measurement can be * displayed in one of the viewports. This function tries to get a "best fit" @@ -761,8 +762,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const { imageIds, displaySetInstanceUID } = data; let volume = data.volume; - if (!volume) { - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (!volume && displaySet.images) { volume = csToolsUtils.getOrCreateImageVolume(displaySet.images.map(image => image.imageId)); } @@ -780,6 +781,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi volumeInputArray.push({ imageIds, volumeId, + modality: displaySet.Modality, + displaySetInstanceUID, blendMode: displaySetOptions.blendMode, slabThickness: this._getSlabThickness(displaySetOptions, volumeId), }); @@ -822,8 +825,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id); const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]); const displaySetModality = displaySet?.Modality; + + // filter overlay display sets (e.g. segmentation) since they will get handled below via the segmentation service + const filteredVolumeInputArray = volumeInputArray.filter(volumeInput => { + const displaySet = displaySetService.getDisplaySetByUID(volumeInput.displaySetInstanceUID); + return !displaySet?.isOverlayDisplaySet; + }); + // Todo: use presentations states - const volumesProperties = volumeInputArray.map((volumeInput, index) => { + const volumesProperties = filteredVolumeInputArray.map((volumeInput, index) => { const { volumeId } = volumeInput; const displaySetOption = displaySetOptions[index]; const { voi, voiInverted, colormap, displayPreset } = displaySetOption; @@ -853,9 +863,29 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); // For SEG and RT viewports - const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {}; + const { addOverlayFn, imageIds } = this._processExtraDisplaySetsForViewport(viewport) || {}; - await viewport.setVolumes(volumeInputArray); + if (!filteredVolumeInputArray.length && addOverlayFn) { + // if there is no volume input array, and there is an addOverlayFn, means we need to take + // care of the background overlay display set first then the addOverlayFn will add the + // SEG displaySet + const sampleImageId = imageIds[0]; + const backgroundDisplaySet = displaySetService.getDisplaySetsBy( + displaySet => + !displaySet.isOverlayDisplaySet && + displaySet.images.some(image => image.imageId === sampleImageId) + ); + + if (backgroundDisplaySet.length !== 1) { + throw new Error('Background display set not found'); + } + + await viewport.setVolumes([ + { volumeId: `${VOLUME_LOADER_SCHEME}:${backgroundDisplaySet[0].displaySetInstanceUID}` }, + ]); + } else { + await viewport.setVolumes(filteredVolumeInputArray); + } if (addOverlayFn) { addOverlayFn(); @@ -896,31 +926,30 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi // load any secondary displaySets const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); - // Can be SEG or RTSTRUCT for now but not PMAP - const segOrRTSOverlayDisplaySet = displaySetInstanceUIDs + // Find overlay display sets (e.g. SEG, RTSTRUCT) + const overlayDisplaySet = displaySetInstanceUIDs .map(displaySetService.getDisplaySetByUID) - .find( - displaySet => - displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality) - ); + .find(displaySet => displaySet?.isOverlayDisplaySet); // if it is only the overlay displaySet, then we need to get the reference // displaySet imageIds and set them as the imageIds for the viewport, // here we can do some logic if the reference is missing // then find the most similar match of displaySet instead - if (!segOrRTSOverlayDisplaySet) { + if (!overlayDisplaySet) { return; } - const referenceDisplaySet = displaySetService.getDisplaySetByUID( - segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID - ); - const imageIds = referenceDisplaySet.images.map(image => image.imageId); + let imageIds; + if (overlayDisplaySet.referencedDisplaySetInstanceUID) { + const referenceDisplaySet = displaySetService.getDisplaySetByUID( + overlayDisplaySet.referencedDisplaySetInstanceUID + ); + imageIds = referenceDisplaySet.images.map(image => image.imageId); + } return { imageIds, - addOverlayFn: () => - this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport), + addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport), }; } diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index 091c7f79d..ae557934c 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -316,6 +316,10 @@ class ViewportInfo { return this.viewportOptions.orientation; } + public setOrientation(orientation: Enums.OrientationAxis): void { + this.viewportOptions.orientation = orientation; + } + public getDisplayArea(): Types.DisplayArea { return this.viewportOptions.displayArea; } diff --git a/extensions/cornerstone/src/types/ViewportPresets.ts b/extensions/cornerstone/src/types/ViewportPresets.ts index fc27bd324..2cfeb363b 100644 --- a/extensions/cornerstone/src/types/ViewportPresets.ts +++ b/extensions/cornerstone/src/types/ViewportPresets.ts @@ -1,5 +1,3 @@ -import { CommandsManager } from '@ohif/core'; - export type ViewportPreset = { name: string; gradientOpacity: string; @@ -15,8 +13,6 @@ export type ViewportPreset = { export type VolumeRenderingPresetsProps = { viewportId: string; - servicesManager: AppTypes.ServicesManager; - commandsManager: CommandsManager; volumeRenderingPresets: ViewportPreset[]; }; @@ -24,13 +20,10 @@ export type VolumeRenderingPresetsContentProps = { presets: ViewportPreset[]; onClose: () => void; viewportId: string; - commandsManager: CommandsManager; }; export type VolumeRenderingOptionsProps = { viewportId: string; - commandsManager: CommandsManager; - servicesManager: AppTypes.ServicesManager; volumeRenderingQualityRange: VolumeRenderingQualityRange; }; @@ -42,27 +35,19 @@ export type VolumeRenderingQualityRange = { export type VolumeRenderingQualityProps = { viewportId: string; - commandsManager: CommandsManager; - servicesManager: AppTypes.ServicesManager; volumeRenderingQualityRange: VolumeRenderingQualityRange; }; export type VolumeShiftProps = { viewportId: string; - commandsManager: CommandsManager; - servicesManager: AppTypes.ServicesManager; }; export type VolumeShadeProps = { viewportId: string; - commandsManager: CommandsManager; - servicesManager: AppTypes.ServicesManager; onClickShade?: (bool: boolean) => void; }; export type VolumeLightingProps = { viewportId: string; - commandsManager: CommandsManager; - servicesManager: AppTypes.ServicesManager; hasShade: boolean; }; diff --git a/extensions/cornerstone/src/types/index.ts b/extensions/cornerstone/src/types/index.ts index 20967db54..4ae9d5cd1 100644 --- a/extensions/cornerstone/src/types/index.ts +++ b/extensions/cornerstone/src/types/index.ts @@ -1,5 +1,10 @@ import * as CornerstoneCacheService from './CornerstoneCacheService'; import type CornerstoneServices from './CornerstoneServices'; export { type SyncGroup } from '../services/SyncGroupService'; +export { + type ViewportActionCornerLocation, + type ViewportActionCornerService, + type CustomizationService, +} from '../hooks/useViewportActionCorners'; export type { CornerstoneCacheService, CornerstoneServices }; diff --git a/extensions/cornerstone/src/utils/colormaps.js b/extensions/cornerstone/src/utils/colormaps.js index ea3f8ab3f..3844c988c 100644 --- a/extensions/cornerstone/src/utils/colormaps.js +++ b/extensions/cornerstone/src/utils/colormaps.js @@ -13,6 +13,15 @@ const colormaps = [ RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0], description: 'X Ray', }, + { + ColorSpace: 'RGB', + Name: 'Isodose', + NanColor: [1, 0, 0], + RGBPoints: [ + 0, 0, 1, 0, 0.1, 0.5, 1, 0, 0.2, 1, 1, 0, 0.3, 1, 0.66, 0, 0.4, 1, 0.33, 0, 0.5, 1, 0, 0, + ], + description: 'Isodose', + }, { ColorSpace: 'RGB', Name: 'hsv', diff --git a/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts index 9fcf0f40b..bd55f5949 100644 --- a/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts +++ b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts @@ -24,9 +24,13 @@ export function getViewportPresentations( const { lutPresentationId, positionPresentationId, segmentationPresentationId } = presentationIds; + const positionPresentation = positionPresentationStore[positionPresentationId]; + const lutPresentation = lutPresentationStore[lutPresentationId]; + const segmentationPresentation = segmentationPresentationStore[segmentationPresentationId]; + return { - positionPresentation: positionPresentationStore[positionPresentationId], - lutPresentation: lutPresentationStore[lutPresentationId], - segmentationPresentation: segmentationPresentationStore[segmentationPresentationId], + positionPresentation, + lutPresentation, + segmentationPresentation, }; } diff --git a/extensions/default/src/Components/MoreDropdownMenu.tsx b/extensions/default/src/Components/MoreDropdownMenu.tsx index 66cd5f85f..2c8c2a2e6 100644 --- a/extensions/default/src/Components/MoreDropdownMenu.tsx +++ b/extensions/default/src/Components/MoreDropdownMenu.tsx @@ -7,17 +7,14 @@ import { Icons, Button, } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; /** * The default sub-menu appearance and setup is defined here, but this can be * replaced by */ -const getMenuItemsDefault = ({ - commandsManager, - items, - servicesManager, - ...props -}: withAppTypes) => { +const getMenuItemsDefault = ({ commandsManager, items, ...props }: withAppTypes) => { + const { servicesManager } = useSystem(); const { customizationService } = servicesManager.services; // This allows replacing the default child item for menus, whereas the entire @@ -36,7 +33,7 @@ const getMenuItemsDefault = ({ onClick: ({ commandsManager, ...props }: withAppTypes) => () => void; }; }) => ( - item.onClick({ commandsManager, ...props })}> + item.onClick({ commandsManager, servicesManager, ...props })}>
{item.iconName && } {item.label} @@ -76,12 +73,8 @@ const getMenuItemsDefault = ({ * @returns Component bound to the bindProps */ export default function MoreDropdownMenu(bindProps) { - const { - menuItemsKey, - getMenuItems = getMenuItemsDefault, - commandsManager, - servicesManager, - } = bindProps; + const { menuItemsKey, getMenuItems = getMenuItemsDefault, commandsManager } = bindProps; + const { servicesManager } = useSystem(); const { customizationService } = servicesManager.services; const items = customizationService.getCustomization(menuItemsKey); diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 668c321f1..f84f13e1f 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -3,6 +3,12 @@ import { Types, DicomMetadataStore } from '@ohif/core'; import { ContextMenuController } from './CustomizableContextMenu'; import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser'; import reuseCachedLayouts from './utils/reuseCachedLayouts'; +import { + configureViewportForLayerAddition, + configureViewportForLayerRemoval, + canAddDisplaySetToViewport, + DERIVED_OVERLAY_MODALITIES, +} from './utils/layerConfigurationUtils'; import findViewportsByPosition, { findOrCreateViewport as layoutFindOrCreate, } from './findViewportsByPosition'; @@ -51,6 +57,137 @@ const commandsModule = ({ const contextMenuController = new ContextMenuController(servicesManager, commandsManager); const actions = { + /** + * Adds a display set as a layer to the specified viewport + * + * @param options.viewportId - The ID of the viewport to add the layer to + * @param options.displaySetInstanceUID - The UID of the display set to add as a layer + * @param options.removeFirst - Optional flag to remove the display set first if it's already added + */ + addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => { + if (!viewportId || !displaySetInstanceUID) { + console.warn('Missing required parameters for addDisplaySetAsLayer command'); + return; + } + + const { displaySetService, viewportGridService, hangingProtocolService } = + servicesManager.services; + + // Get the display set + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (!displaySet) { + return; + } + + // Get current display sets for the viewport + const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + // Check if we can add this display set to the viewport + const canAdd = canAddDisplaySetToViewport({ + viewportId, + displaySetInstanceUID, + servicesManager, + }); + + if (!canAdd) { + return; + } + + // Add the display set to the viewport + const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + displaySetInstanceUID + ); + + // Configure each viewport for layer addition + updatedViewports.forEach(viewport => { + configureViewportForLayerAddition({ + viewport, + displaySetInstanceUID, + currentDisplaySetUIDs, + servicesManager, + }); + }); + + // Update position presentation + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs, + }); + + // Run command to update viewports + commandsManager.run('setDisplaySetsForViewports', { + viewportsToUpdate: updatedViewports, + }); + }, + + /** + * Removes a display set layer from the specified viewport + * + * @param options.viewportId - The ID of the viewport to remove the layer from + * @param options.displaySetInstanceUID - The UID of the display set to remove + */ + removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => { + if (!viewportId || !displaySetInstanceUID) { + console.warn('Missing required parameters for removeDisplaySetLayer command'); + return; + } + + const { + displaySetService, + viewportGridService, + hangingProtocolService, + segmentationService, + } = servicesManager.services; + + // Get the display set + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (!displaySet) { + return; + } + + // Get current display sets for the viewport + const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + // If the display set is not in the viewport, no need to remove it + if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) { + return; + } + + // Check if it's a segmentation and handle accordingly + const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality); + if (isSegmentation) { + segmentationService.removeSegmentationRepresentations(viewportId, { + segmentationId: displaySetInstanceUID, + }); + } + + const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + displaySetInstanceUID + ); + + // Configure each viewport for layer removal + updatedViewports.forEach(viewport => { + configureViewportForLayerRemoval({ + viewport, + displaySetInstanceUID, + currentDisplaySetUIDs, + servicesManager, + }); + }); + + // Update position presentation + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs, + }); + + // Update the viewports + commandsManager.run('setDisplaySetsForViewports', { + viewportsToUpdate: updatedViewports, + }); + }, /** * Runs a command in multi-monitor mode. No-op if not multi-monitor. */ @@ -644,6 +781,8 @@ const commandsModule = ({ openDICOMTagViewer: actions.openDICOMTagViewer, updateViewportDisplaySet: actions.updateViewportDisplaySet, scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView, + addDisplaySetAsLayer: actions.addDisplaySetAsLayer, + removeDisplaySetLayer: actions.removeDisplaySetLayer, }; return { diff --git a/extensions/default/src/customizations/studyBrowserCustomization.ts b/extensions/default/src/customizations/studyBrowserCustomization.ts index 000fdb9c5..7bc01ba32 100644 --- a/extensions/default/src/customizations/studyBrowserCustomization.ts +++ b/extensions/default/src/customizations/studyBrowserCustomization.ts @@ -14,6 +14,26 @@ export default { }); }, }, + { + id: 'addAsLayer', + label: 'Add as Layer', + iconName: 'ViewportViews', + onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => { + const { viewportGridService } = servicesManager.services; + + // Get the active viewport + const { activeViewportId } = viewportGridService.getState(); + if (!activeViewportId) { + return; + } + + // Use the new command to add the display set as a layer + commandsManager.runCommand('addDisplaySetAsLayer', { + viewportId: activeViewportId, + displaySetInstanceUID, + }); + }, + }, ], 'studyBrowser.sortFunctions': [ { diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 74f8cea5a..3d2781ecd 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -1,4 +1,5 @@ import { utils, classes } from '@ohif/core'; +import i18n from '@ohif/i18n'; import { id } from './id'; import getDisplaySetMessages from './getDisplaySetMessages'; import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries'; @@ -99,6 +100,11 @@ const makeDisplaySet = instances => { averageSpacingBetweenFrames: averageSpacingBetweenFrames || null, isDynamicVolume, dynamicVolumeInfo, + supportsWindowLevel: true, + label: + instance.SeriesDescription || + `${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`, + FrameOfReferenceUID: instance.FrameOfReferenceUID, }); // Sort the images in this series if needed @@ -260,6 +266,7 @@ const sopClassUids = [ sopClassDictionary.LegacyConvertedEnhancedPETImageStorage, sopClassDictionary.RTImageStorage, sopClassDictionary.EnhancedUSVolumeStorage, + sopClassDictionary.RTDoseStorage, ]; function getSopClassHandlerModule(appContextParam) { diff --git a/extensions/default/src/getViewportModule.tsx b/extensions/default/src/getViewportModule.tsx index 322bbfc45..bba53b678 100644 --- a/extensions/default/src/getViewportModule.tsx +++ b/extensions/default/src/getViewportModule.tsx @@ -1,15 +1,6 @@ -import { CommandsManager, ExtensionManager } from '@ohif/core'; import LineChartViewport from './Components/LineChartViewport/index'; -const getViewportModule = ({ - servicesManager, - commandsManager, - extensionManager, -}: { - servicesManager: AppTypes.ServicesManager; - commandsManager: CommandsManager; - extensionManager: ExtensionManager; -}) => { +const getViewportModule = () => { return [ { name: 'chartViewport', diff --git a/extensions/default/src/init.ts b/extensions/default/src/init.ts index ee5eee7ef..19bff11b2 100644 --- a/extensions/default/src/init.ts +++ b/extensions/default/src/init.ts @@ -22,11 +22,11 @@ export default function init({ cineService.EVENTS.CINE_STATE_CHANGED, ]); // Add - DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata); + DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handleScalingModules); // If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight) // we need to recalculate the SUV Scaling Factors - DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata); + DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handleScalingModules); // Adds extra custom attributes for use by hanging protocols registerHangingProtocolAttributes({ servicesManager }); @@ -71,7 +71,7 @@ export default function init({ }); } -const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => { +const handleScalingModules = ({ SeriesInstanceUID, StudyInstanceUID }) => { const { instances } = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID); if (!instances?.length) { @@ -80,12 +80,41 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => { const modality = instances[0].Modality; - if (!modality || modality !== 'PT') { + const allowedModality = ['PT', 'RTDOSE']; + + if (!allowedModality.includes(modality)) { return; } const imageIds = instances.map(instance => instance.imageId); const instanceMetadataArray = []; + + if (modality === 'RTDOSE') { + const DoseGridScaling = instances[0].DoseGridScaling; + const DoseSummation = instances[0].DoseSummation; + const DoseType = instances[0].DoseType; + const DoseUnit = instances[0].DoseUnit; + const NumberOfFrames = instances[0].NumberOfFrames; + const imageId = imageIds[0]; + + // add scaling module to the metadata + // since RTDOSE is always a multiframe we should add the scaling module to each frame + for (let i = 0; i < NumberOfFrames; i++) { + const frameIndex = i + 1; + + // Todo: we should support other things like wadouri, local etc + const newImageId = `${imageId.replace(/\/frames\/\d+$/, '')}/frames/${frameIndex}`; + metadataProvider.addCustomMetadata(newImageId, 'scalingModule', { + DoseGridScaling, + DoseSummation, + DoseType, + DoseUnit, + }); + } + + return; + } + // try except block to prevent errors when the metadata is not correct try { imageIds.forEach(imageId => { diff --git a/extensions/default/src/utils/layerConfigurationUtils.ts b/extensions/default/src/utils/layerConfigurationUtils.ts new file mode 100644 index 000000000..66cdc2a8e --- /dev/null +++ b/extensions/default/src/utils/layerConfigurationUtils.ts @@ -0,0 +1,157 @@ +export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT']; +export const DEFAULT_COLORMAP = 'hsv'; +export const DEFAULT_OPACITY = 0.9; +export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100; + +/** + * Get modality-specific color and opacity settings from the customization service + */ +export function getModalityOverlayColormap(customizationService, modality) { + const modalityOverlayDefaultColorMaps = customizationService?.getCustomization( + 'cornerstone.modalityOverlayDefaultColorMaps' + ) || { defaultSettings: {} }; + + return ( + modalityOverlayDefaultColorMaps.defaultSettings[modality] || { + colormap: DEFAULT_COLORMAP, + opacity: DEFAULT_OPACITY, + } + ); +} + +/** + * Create display set options based on modality and opacity settings + */ +export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) { + if (displaySet.Modality === 'SEG') { + return {}; + } + + const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality); + return { + colormap: { + name: modalitySettings.colormap || DEFAULT_COLORMAP, + opacity: opacity / 100, // Convert from percentage to 0-1 range + }, + }; +} + +/** + * Configure viewport for adding a display set layer + */ +export function configureViewportForLayerAddition(params: { + viewport: any; + displaySetInstanceUID: string; + currentDisplaySetUIDs: string[]; + servicesManager: AppTypes.ServicesManager; +}): any { + const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params; + const { cornerstoneViewportService, displaySetService, customizationService } = + servicesManager.services; + + const { viewportId } = viewport; + + // Set the display set UIDs for the viewport + const allDisplaySetInstanceUIDs = [...currentDisplaySetUIDs, displaySetInstanceUID]; + viewport.displaySetInstanceUIDs = allDisplaySetInstanceUIDs; + + if (!viewport.viewportOptions) { + viewport.viewportOptions = {}; + } + + const requestedLayerDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + if (!viewport.viewportOptions.orientation) { + viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId); + } + + // Do not force volume for SEG and RTSTRUCT if there is only one display set + if (requestedLayerDisplaySet.isOverlayDisplaySet && currentDisplaySetUIDs.length === 1) { + viewport.viewportOptions.viewportType = 'stack'; + } else { + viewport.viewportOptions.viewportType = 'volume'; + } + + // create same amount of display set options as the number of display set UIDs + const displaySetOptions = allDisplaySetInstanceUIDs.map((uid, index) => { + if (index === 0) { + // no colormap for background + return {}; + } + + const displaySet = displaySetService.getDisplaySetByUID(uid); + return createColormapOverlayDisplaySetOptions(displaySet, 90, customizationService); + }); + + viewport.displaySetOptions = displaySetOptions; + + return viewport; +} + +/** + * Configure viewport for removing a display set layer + */ +export function configureViewportForLayerRemoval(params: { + viewport: any; + displaySetInstanceUID: string; + currentDisplaySetUIDs: string[]; + servicesManager: AppTypes.ServicesManager; +}): any { + const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params; + const { cornerstoneViewportService, displaySetService } = servicesManager.services; + + const { viewportId } = viewport; + + // Filter out the display set to remove + viewport.displaySetInstanceUIDs = currentDisplaySetUIDs.filter( + uid => uid !== displaySetInstanceUID + ); + + if (!viewport.viewportOptions) { + viewport.viewportOptions = {}; + } + + viewport.viewportOptions.viewportType = 'volume'; + + // orientation + if (!viewport.viewportOptions.orientation) { + viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId); + } + + // Recreate the display set options + viewport.displaySetOptions = viewport.displaySetInstanceUIDs.map(() => { + // For simplicity, we're returning empty options for now + // In a more complete implementation, we would need to preserve existing display set options + return {}; + }); + + return viewport; +} + +/** + * Check if a display set can be added as a layer to the specified viewport + */ +export function canAddDisplaySetToViewport(params: { + viewportId: string; + displaySetInstanceUID: string; + servicesManager: AppTypes.ServicesManager; +}): boolean { + const { viewportId, displaySetInstanceUID, servicesManager } = params; + const { displaySetService, viewportGridService } = servicesManager.services; + + // Check if the display set exists + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (!displaySet) { + return false; + } + + // Get current display sets in the viewport + const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + // Check if the display set is already in the viewport + if (currentDisplaySetUIDs.includes(displaySetInstanceUID)) { + return false; + } + + return true; +} diff --git a/extensions/dicom-pdf/src/getSopClassHandlerModule.js b/extensions/dicom-pdf/src/getSopClassHandlerModule.js index e0ef0e255..fd16d83bf 100644 --- a/extensions/dicom-pdf/src/getSopClassHandlerModule.js +++ b/extensions/dicom-pdf/src/getSopClassHandlerModule.js @@ -1,7 +1,6 @@ import { SOPClassHandlerId } from './id'; -import { utils, classes } from '@ohif/core'; - -const { ImageSet } = classes; +import { utils, Types as OhifTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; const SOP_CLASS_UIDS = { ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1', @@ -50,12 +49,15 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) numImageFrames: 0, numInstances: 1, instance, + supportsWindowLevel: true, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t(Modality)}`, }; return displaySet; }); }; -export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { +export default function getSopClassHandlerModule(params) { + const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; diff --git a/extensions/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js index a49512775..70bb1ee17 100644 --- a/extensions/dicom-video/src/getSopClassHandlerModule.js +++ b/extensions/dicom-video/src/getSopClassHandlerModule.js @@ -1,5 +1,6 @@ import { SOPClassHandlerId } from './id'; import { utils } from '@ohif/core'; +import i18n from '@ohif/i18n'; import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core'; const SOP_CLASS_UIDS = { @@ -90,6 +91,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) sopClassUids, numImageFrames: NumberOfFrames, instance, + supportsWindowLevel: true, + label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t(Modality)}`, }; csUtils.genericMetadataProvider.add(imageId, { type: 'imageUrlModule', @@ -99,7 +102,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) }); }; -export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { +export default function getSopClassHandlerModule(params) { + const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 1e69f3d40..5d2020d61 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/tools": "^3.9.5", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/tools": "^3.11.6", "@ohif/core": "3.11.0-beta.23", "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23", "@ohif/extension-default": "3.11.0-beta.23", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index 6290d3cdd..319a9abc6 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -113,7 +113,7 @@ function TrackedMeasurementsContextProvider( // it will be in the correct position zoom and pan commandsManager.runCommand('updateStoredPositionPresentation', { viewportId: activeViewportId, - displaySetInstanceUID: referencedDisplaySetUID, + displaySetInstanceUIDs: [referencedDisplaySetUID], referencedImageId: trackedMeasurement.referencedImageId, }); diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 8f2600adc..ed99a7238 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -207,7 +207,7 @@ function TrackedCornerstoneViewport( id: 'viewportStatusComponent', component: statusComponent, indexPriority: -100, - location: viewportActionCornersService.LOCATIONS.topLeft, + location: viewportActionCornersService.LOCATIONS.topRight, }, { viewportId, diff --git a/platform/app/package.json b/platform/app/package.json index 502dd5d7f..9e93c7c07 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -53,7 +53,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", + "@cornerstonejs/dicom-image-loader": "^3.11.6", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.11.0-beta.23", "@ohif/extension-cornerstone": "3.11.0-beta.23", diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 001a2ce46..bbc623647 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -230,7 +230,7 @@ function ViewerViewportGrid(props: withAppTypes) { // Update stored position presentation commandsManager.run('updateStoredPositionPresentation', { viewportId: viewport.viewportId, - displaySetInstanceUID: referencedDisplaySetInstanceUID, + displaySetInstanceUIDs: [referencedDisplaySetInstanceUID], referencedImageId: measurement.referencedImageId, options: { ...measurement.metadata, diff --git a/platform/core/package.json b/platform/core/package.json index a1601250a..26ec65442 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,8 +37,8 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/core": "^3.9.5", - "@cornerstonejs/dicom-image-loader": "^3.9.5", + "@cornerstonejs/core": "^3.11.6", + "@cornerstonejs/dicom-image-loader": "^3.11.6", "@ohif/ui": "3.11.0-beta.23", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 1683bbba5..651070c94 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -321,7 +321,7 @@ class MeasurementService extends PubSubService { this.mappings[source.uid] = [mapping]; } - log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`); + // log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`); } /** diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index 90334c13a..7edf04bba 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -239,7 +239,10 @@ export default class ToolbarService extends PubSubService { const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id); return { ...props, disabled, disabledText, className, isActive }; } else { - const evaluated = props.evaluate?.({ ...refreshProps, button }); + const evaluated = + typeof props.evaluate === 'function' + ? props.evaluate({ ...refreshProps, button }) + : undefined; const updatedProps = { ...props, ...evaluated, diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts index 52c19ac0d..30935b0fc 100644 --- a/platform/core/src/types/DisplaySet.ts +++ b/platform/core/src/types/DisplaySet.ts @@ -13,6 +13,9 @@ export type DisplaySet = { Modality?: string; imageIds?: string[]; images?: unknown[]; + label?: string; + /** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */ + isOverlayDisplaySet?: boolean; // Details about how to display: /** A URL that can be used to display the thumbnail. Typically a data url */ diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts index efb07c01f..64c6f85e5 100644 --- a/platform/core/src/utils/combineFrameInstance.ts +++ b/platform/core/src/utils/combineFrameInstance.ts @@ -19,11 +19,14 @@ const combineFrameInstance = (frame, instance) => { ImageType, } = instance; + if (NumberOfFrames < 2) { + return instance; + } + instance.ImageType = dicomSplit(ImageType); + const frameNumber = Number.parseInt(frame || 1); - if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) { - const frameNumber = Number.parseInt(frame || 1); - + if (PerFrameFunctionalGroupsSequence && SharedFunctionalGroupsSequence) { // this is to fix NM multiframe datasets with position and orientation // information inside DetectorInformationSequence if (!instance.ImageOrientationPatient && instance.DetectorInformationSequence) { @@ -96,9 +99,52 @@ const combineFrameInstance = (frame, instance) => { configurable: true, }); return newInstance; - } else { - return instance; } + + // For RTDOSE datasets + if (instance.GridFrameOffsetVector) { + if (!instance._parentInstance) { + Object.defineProperty(instance, '_parentInstance', { + value: { ...instance }, + }); + } + + const sharedInstance = createCombinedValue( + instance._parentInstance, + SharedFunctionalGroupsSequence?.[0], + '_shared' + ); + + const newInstance = createCombinedValue( + sharedInstance, + PerFrameFunctionalGroupsSequence?.[frameNumber - 1], + frameNumber + ); + + const origin = newInstance.ImagePositionPatient?.map(Number); + const orientation = newInstance.ImageOrientationPatient?.map(Number); + const offset = Number(instance.GridFrameOffsetVector[frameNumber - 1]); + + if (origin && orientation && !Number.isNaN(offset)) { + const row = vec3.fromValues(orientation[0], orientation[1], orientation[2]); + const col = vec3.fromValues(orientation[3], orientation[4], orientation[5]); + const normal = vec3.cross(vec3.create(), row, col); + + const position = vec3.scaleAndAdd(vec3.create(), vec3.fromValues(...origin), normal, offset); + newInstance.ImagePositionPatient = [position[0], position[1], position[2]]; + } + + Object.defineProperty(newInstance, 'frameNumber', { + value: frameNumber, + writable: true, + enumerable: true, + configurable: true, + }); + + return newInstance; + } + + return instance; }; /** diff --git a/platform/core/src/utils/isLowPriorityModality.ts b/platform/core/src/utils/isLowPriorityModality.ts index cfcee34e4..3545d4afd 100644 --- a/platform/core/src/utils/isLowPriorityModality.ts +++ b/platform/core/src/utils/isLowPriorityModality.ts @@ -1,4 +1,14 @@ -const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT', 'RTDOSE', 'RTPLAN', 'RTRECORD', 'REG']); +const LOW_PRIORITY_MODALITIES = Object.freeze([ + 'SEG', + 'KO', + 'PR', + 'SR', + 'RTSTRUCT', + 'RTDOSE', + 'RTPLAN', + 'RTRECORD', + 'REG', +]); export default function isLowPriorityModality(Modality) { return LOW_PRIORITY_MODALITIES.includes(Modality); diff --git a/platform/docs/docs/faq/add-viewport-icon.md b/platform/docs/docs/faq/add-viewport-icon.md new file mode 100644 index 000000000..11c35cdc2 --- /dev/null +++ b/platform/docs/docs/faq/add-viewport-icon.md @@ -0,0 +1,178 @@ +--- +id: add-viewport-icon +title: How to add a custom icon to the viewport corners +summary: Learn how to add a custom icon or dropdown to one of the viewport corners in OHIF. +--- + +# How to add a custom icon to the viewport corners + +## Question +How can I add a custom icon or dropdown to one of the viewport corners in OHIF? + +## Answer +OHIF provides a customizable viewport action menu system that allows you to add icons, buttons, or dropdowns to any of the four corners of a viewport (topLeft, topRight, bottomLeft, bottomRight). This is done through the `customizationService` and the viewport action corners API. + +Here's a complete example that shows how to add a mode switch dropdown to the top-left corner of the viewport: + +```tsx +import React from 'react'; +import { Icons } from '@ohif/ui-next'; +import { useSystem } from '@ohif/core'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + Button, +} from '@ohif/ui-next'; + +// This is a complete, self-contained component that shows a dropdown menu +// for switching modes when clicked +function getModeSwitchMenu({ viewportId, element, location }) { + const ModeSwitchMenu = () => { + const { servicesManager } = useSystem(); + const { router } = servicesManager.services; + const { viewportActionCornersService } = servicesManager.services; + + const handleModeSwitch = (mode) => { + const currentStudyInstanceUID = router.query.StudyInstanceUIDs; + // Navigate to the selected mode with the current study + router.navigate(`/${mode}?StudyInstanceUIDs=${currentStudyInstanceUID}`); + }; + + // Get proper alignment based on the location + let align = 'center'; + let side = 'bottom'; + + if (location !== undefined) { + const positioning = viewportActionCornersService.getAlignAndSide(location); + align = positioning.align; + side = positioning.side; + } + + return ( +
+ + + + + + Mode + handleModeSwitch('longitudinal')}> + Longitudinal + + handleModeSwitch('segmentation')}> + Segmentation + + handleModeSwitch('tmtv')}> + TMTV + + handleModeSwitch('microscopy')}> + Microscopy + + + +
+ ); + }; + + return ; +} + +// In your mode or extension, add this to the customizations +// This example shows how to add it in the onModeEnter lifecycle hook +function onModeEnter({ servicesManager }) { + const { customizationService } = servicesManager.services; + + // Add the mode switch icon to the top-left corner of the viewport + customizationService.setCustomizations({ + 'viewportActionMenu.topLeft': { + // Use $push to add to existing items or $set to replace all items + $push: [ + { + id: 'modeSwitch', + enabled: true, + component: getModeSwitchMenu, + }, + ], + }, + }); +} +``` + +## Key Concepts + +1. **Location-based customization**: The viewport is divided into four corners identified by: + - `viewportActionMenu.topLeft` + - `viewportActionMenu.topRight` + - `viewportActionMenu.bottomLeft` + - `viewportActionMenu.bottomRight` + +2. **Component structure**: + - `id` - A unique identifier for your component + - `enabled` - Boolean to control if the component should be displayed + - `component` - A function that returns a React component to render + +3. **Component positioning**: The component's position within a corner is determined by its order in the array. Components are rendered in the order they appear. + +4. **Dropdown positioning**: Use the `viewportActionCornersService.getAlignAndSide()` method to get the correct alignment for your dropdown menu based on its location. + +## Adding Multiple Components + +If you want to add multiple components to the same corner or different corners, you can do it in a single customization: + +```js +customizationService.setCustomizations({ + 'viewportActionMenu.topLeft': { + $push: [ + { + id: 'modeSwitch', + enabled: true, + component: getModeSwitchMenu, + }, + ], + }, + 'viewportActionMenu.topRight': { + $push: [ + { + id: 'anotherComponent', + enabled: true, + component: getAnotherComponent, + }, + ], + }, +}); +``` + +## Replacing Existing Components + +If you want to replace all components in a corner instead of adding to them, use `$set` instead of `$push`: + +```js +customizationService.setCustomizations({ + 'viewportActionMenu.topLeft': { + $set: [ + { + id: 'modeSwitch', + enabled: true, + component: getModeSwitchMenu, + }, + // This will be the only components in the top-left corner + ], + }, +}); +``` + +Remember that this customization will affect all viewports in the active mode. If you need different behavior for different viewports, you should check the `viewportId` parameter in your component's logic. diff --git a/platform/docs/docs/faq/technical.md b/platform/docs/docs/faq/technical.md index 1b65ec67d..785138076 100644 --- a/platform/docs/docs/faq/technical.md +++ b/platform/docs/docs/faq/technical.md @@ -6,6 +6,7 @@ summary: Technical explanations and solutions for OHIF Viewer implementation cha # Technical FAQ +* [How to add a custom icon to the viewport corners](./add-viewport-icon.md) ## Viewer opens but does not show any thumbnails diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/index.md b/platform/docs/docs/migration-guide/3p10-to-3p11/index.md new file mode 100644 index 000000000..bee159b84 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p10-to-3p11/index.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 1 +sidebar_label: 3.10 -> 3.11 beta +--- + +# Migration Guide + +This guide provides information about migrating from OHIF version 3.10 to version 3.11. + +## General + +`viewportActionMenu.segmentationOverlay` is renamed to `viewportActionMenu.dataOverlay` +as it handles now both segmentation and data overlay. + +## Viewport Action Menu Customization + +The structure for defining viewport action menu customizations has changed. See the [Viewport Action Menu](./viewport-action-menu.md) migration guide for details. + + +## updateStoredPositionPresentation + +now uses displaySetInstanceUIDs instead of displaySetInstanceUID. diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md b/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md new file mode 100644 index 000000000..2b7f36d98 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 2 +sidebar_label: Viewport Action Menu +summary: Migration guide for OHIF 3.11's viewport action menu customization changes, including the transition from individual item configurations to location-based arrays and the removal of index priorities. +--- + +# Viewport Action Menu Customization + +In OHIF 3.11, we've redesigned how viewport action menu customizations are defined to make them more intuitive and organized by location. + +## Changes + +Previously, viewport action menu customizations were defined with individual item configurations that specified location and priority: + +```ts +export default { + 'viewportActionMenu.orientationMenu': { + enabled: true, + location: viewportActionCornersService.LOCATIONS.topLeft, + indexPriority: 1, + }, + 'viewportActionMenu.dataOverlay': { + enabled: true, + location: viewportActionCornersService.LOCATIONS.topLeft, + indexPriority: 2, + }, + // ... +}; +``` + +Now, viewport action menu customizations are organized by location, with each location having its own customization ID: + +```ts +export default { + 'viewportActionMenu.topLeft': [ + { + id: 'orientationMenu', + enabled: true, + }, + { + id: 'dataOverlay', + enabled: true, + }, + { + id: 'windowLevelActionMenu', + enabled: true, + }, + ], + 'viewportActionMenu.topRight': [], + 'viewportActionMenu.bottomLeft': [], + 'viewportActionMenu.bottomRight': [], +}; +``` + +## Migration Steps + +1. Reorganize your viewport action menu customizations by using location-based customization IDs (`viewportActionMenu.topLeft`, `viewportActionMenu.topRight`, etc.). +2. For each component, move it into the appropriate location array. +3. Replace the component key with an `id` property that doesn't include the prefix (just use `orientationMenu` instead of `viewportActionMenu.orientationMenu`). +4. Remove the `location` property (since it's now implied by the customization ID). +5. Remove the `indexPriority` property (order in the array now determines display order). +6. For each component, provide a `component` function that returns the component instance. + +## Component Rendering + +Component rendering logic is now included directly in the item configuration via a `component` function: + +```ts +const createOrientationMenu = ({ viewportId, element, location }) => { + return getViewportOrientationMenu({ + viewportId, + element, + location, + }); +}; + +const createDataOverlay = ({ viewportId, element, displaySets, location }) => { + return getViewportDataOverlaySettingsMenu({ + viewportId, + element, + displaySets, + location, + }); +}; + +export default { + 'viewportActionMenu.topLeft': [ + { + id: 'orientationMenu', + enabled: true, + component: createOrientationMenu, + }, + { + id: 'dataOverlay', + enabled: true, + component: createDataOverlay, + }, + // other components... + ], + // other locations... +}; +``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/index.md b/platform/docs/docs/migration-guide/3p9-to-3p10/index.md index cf77d26f1..e2da5ae32 100644 --- a/platform/docs/docs/migration-guide/3p9-to-3p10/index.md +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/index.md @@ -1,6 +1,6 @@ --- sidebar_position: 1 -sidebar_label: 3.9 -> 3.10 beta +sidebar_label: 3.9 -> 3.10 title: Migration Guide from 3.9 to 3.10 beta summary: Migration guide for upgrading from OHIF 3.9 to 3.10 beta, covering general changes, customization service improvements, UI component upgrades, command handling, hotkey updates, routing changes, and testing strategies. --- diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index 6ab5b84ee..4b70c0252 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -937,8 +937,8 @@ window.config = { `, }, { - id: 'viewportActionMenu.segmentationOverlay', - description: 'Configures the display and location of the segmentation overlay in the viewport.', + id: 'viewportActionMenu.dataOverlay', + description: 'Configures the display and location of the data overlay in the viewport.', image: segmentationOverlay, default: null, configuration: ` @@ -946,7 +946,7 @@ window.config = { // rest of window config customizationService: [ { - 'viewportActionMenu.segmentationOverlay': { + 'viewportActionMenu.dataOverlay': { $merge: { enabled: true, location: 1, // Set the location of the overlay in the viewport. diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx index 31e5ef47d..e0b654063 100644 --- a/platform/ui-next/src/components/DataRow/DataRow.tsx +++ b/platform/ui-next/src/components/DataRow/DataRow.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useRef } from 'react'; import { Button } from '../../components/Button/Button'; import { DropdownMenu, @@ -57,7 +57,7 @@ import { cn } from '../../lib/utils'; * @property {() => void} onColor - Callback when color change is requested */ interface DataRowProps { - number: number; + number: number | null; disableEditing: boolean; description: string; details?: { primary: string[]; secondary: string[] }; @@ -208,15 +208,18 @@ export const DataRow: React.FC = ({
{/* Number Box */} -
- {number} -
+ {number !== null && ( +
+ {number} +
+ )} - {/* Color Circle (Optional) */} + {/* add some space if there is not segment index */} + {number === null &&
} {colorHex && (
void; showNumberInputs?: boolean; + lockMode?: boolean; } const DoubleSlider = React.forwardRef( @@ -24,10 +25,19 @@ const DoubleSlider = React.forwardRef( step = 1, defaultValue = [min, max], showNumberInputs = false, + lockMode = false, }, ref ) => { const [value, setValue] = React.useState<[number, number]>(defaultValue); + const trackRef = React.useRef(null); + const dragStateRef = React.useRef<{ + startX: number; + startValue: [number, number]; + rangeWidth: number; + trackLeft: number; + trackWidth: number; + } | null>(null); const prevDefaultValueRef = React.useRef<[number, number] | null>(null); @@ -50,8 +60,172 @@ const DoubleSlider = React.forwardRef( return Math.round(num * inverse) / inverse; }; + const handleRangePointerDown = (event: React.PointerEvent) => { + if (!lockMode) { + return; + } + event.preventDefault(); + const rect = trackRef.current!.getBoundingClientRect(); + dragStateRef.current = { + startX: event.clientX, + startValue: [...value] as [number, number], + rangeWidth: value[1] - value[0], + trackLeft: rect.left, + trackWidth: rect.width, + }; + window.addEventListener('pointermove', handleRangePointerMove); + window.addEventListener('pointerup', handleRangePointerUp, { once: true }); + }; + + const handleRangePointerMove = (event: PointerEvent) => { + const state = dragStateRef.current; + if (!state) { + return; + } + const { startX, startValue, rangeWidth, trackWidth } = state; + const dxPx = event.clientX - startX; + const pct = dxPx / trackWidth; + const delta = pct * (max - min); + + let newLeft = startValue[0] + delta; + let newRight = newLeft + rangeWidth; + + // clamp to bounds + if (newLeft < min) { + newLeft = min; + newRight = min + rangeWidth; + } else if (newRight > max) { + newRight = max; + newLeft = max - rangeWidth; + } + + const clampedLeft = roundToStep(newLeft); + const clampedRight = roundToStep(newRight); + setValue([clampedLeft, clampedRight]); + onValueChange?.([clampedLeft, clampedRight]); + }; + + const handleRangePointerUp = () => { + dragStateRef.current = null; + window.removeEventListener('pointermove', handleRangePointerMove); + }; + const handleSliderChange = React.useCallback( (newValue: number[]) => { + // Default behavior (no lock mode) + if (!lockMode) { + const clampedValue: [number, number] = [ + roundToStep(Math.max(min, Math.min(newValue[0], max))), + roundToStep(Math.min(max, Math.max(newValue[1], min))), + ]; + setValue(clampedValue); + onValueChange?.(clampedValue); + return; + } + + // Lock mode behavior + // First, determine if this is a symmetric expansion/contraction or a range shift + // by checking which thumb(s) moved + const isLeftThumbMoved = newValue[0] !== value[0]; + const isRightThumbMoved = newValue[1] !== value[1]; + + // Case 1: Left thumb moved, right thumb needs to move symmetrically (opposite) + if (isLeftThumbMoved && !isRightThumbMoved) { + const delta = newValue[0] - value[0]; + const centerPoint = (value[0] + value[1]) / 2; + + // Symmetric movement: move right thumb in opposite direction + const newRight = value[1] - delta; + + // Ensure values stay within bounds + const clampedLeft = roundToStep(Math.max(min, Math.min(newValue[0], max))); + const clampedRight = roundToStep(Math.min(max, Math.max(newRight, min))); + + // Special case: If right would go out of bounds, adjust left too to maintain center + if (newRight !== clampedRight) { + // Calculate new delta given the clamped right value + const adjustedDelta = value[1] - clampedRight; + // Apply same delta to left but in opposite direction + const adjustedLeft = value[0] + adjustedDelta; + const finalClampedLeft = roundToStep(Math.max(min, Math.min(adjustedLeft, max))); + + const finalValues: [number, number] = [finalClampedLeft, clampedRight]; + setValue(finalValues); + onValueChange?.(finalValues); + return; + } + + const finalValues: [number, number] = [clampedLeft, clampedRight]; + setValue(finalValues); + onValueChange?.(finalValues); + return; + } + + // Case 2: Right thumb moved, left thumb needs to move symmetrically (opposite) + if (!isLeftThumbMoved && isRightThumbMoved) { + const delta = newValue[1] - value[1]; + const centerPoint = (value[0] + value[1]) / 2; + + // Symmetric movement: move left thumb in opposite direction + const newLeft = value[0] - delta; + + // Ensure values stay within bounds + const clampedRight = roundToStep(Math.min(max, Math.max(newValue[1], min))); + const clampedLeft = roundToStep(Math.max(min, Math.min(newLeft, max))); + + // Special case: If left would go out of bounds, adjust right too to maintain center + if (newLeft !== clampedLeft) { + // Calculate new delta given the clamped left value + const adjustedDelta = value[0] - clampedLeft; + // Apply same delta to right but in opposite direction + const adjustedRight = value[1] + adjustedDelta; + const finalClampedRight = roundToStep(Math.min(max, Math.max(adjustedRight, min))); + + const finalValues: [number, number] = [clampedLeft, finalClampedRight]; + setValue(finalValues); + onValueChange?.(finalValues); + return; + } + + const finalValues: [number, number] = [clampedLeft, clampedRight]; + setValue(finalValues); + onValueChange?.(finalValues); + return; + } + + // Case 3: Both thumbs moved (range shift) - maintain the same distance between thumbs + if (isLeftThumbMoved && isRightThumbMoved) { + const rangeWidth = value[1] - value[0]; + + // Calculate how much the left thumb moved + const leftDelta = newValue[0] - value[0]; + + // New values maintaining the same range width + let newLeft = newValue[0]; + let newRight = newLeft + rangeWidth; + + // Handle bounds checking + if (newRight > max) { + newRight = max; + newLeft = newRight - rangeWidth; + } + + if (newLeft < min) { + newLeft = min; + newRight = newLeft + rangeWidth; + } + + const clampedValues: [number, number] = [ + roundToStep(Math.max(min, Math.min(newLeft, max))), + roundToStep(Math.min(max, Math.max(newRight, min))), + ]; + + setValue(clampedValues); + onValueChange?.(clampedValues); + return; + } + + // Fallback to default behavior const clampedValue: [number, number] = [ roundToStep(Math.max(min, Math.min(newValue[0], max))), roundToStep(Math.min(max, Math.max(newValue[1], min))), @@ -59,25 +233,86 @@ const DoubleSlider = React.forwardRef( setValue(clampedValue); onValueChange?.(clampedValue); }, - [min, max, onValueChange, step] + [min, max, onValueChange, step, value, lockMode] ); const handleInputChange = React.useCallback( (index: 0 | 1, inputValue: string) => { const newValue = parseFloat(inputValue); if (!isNaN(newValue)) { - const clampedValue: [number, number] = [...value]; - clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max)); - if (index === 0 && clampedValue[0] > clampedValue[1]) { - clampedValue[1] = clampedValue[0]; - } else if (index === 1 && clampedValue[1] < clampedValue[0]) { - clampedValue[0] = clampedValue[1]; + // Default behavior (no lock mode) + if (!lockMode) { + const clampedValue: [number, number] = [...value]; + clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max)); + if (index === 0 && clampedValue[0] > clampedValue[1]) { + clampedValue[1] = clampedValue[0]; + } else if (index === 1 && clampedValue[1] < clampedValue[0]) { + clampedValue[0] = clampedValue[1]; + } + setValue(clampedValue); + onValueChange?.(clampedValue); + return; } + + // Lock mode behavior + const centerPoint = (value[0] + value[1]) / 2; + const rangeWidth = value[1] - value[0]; + const clampedValue: [number, number] = [...value]; + + // Calculate new value with constraints + const boundedNewValue = roundToStep(Math.min(Math.max(newValue, min), max)); + + if (index === 0) { + // Left thumb changed + const delta = boundedNewValue - value[0]; + + // Symmetric change: move right thumb in opposite direction + const newRight = value[1] - delta; + const boundedNewRight = roundToStep(Math.min(Math.max(newRight, min), max)); + + // If right would go out of bounds, adjust left too + if (newRight !== boundedNewRight) { + // Adjust to maintain the same width but respect bounds + if (boundedNewRight === max) { + clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min)); + clampedValue[1] = max; + } else if (boundedNewRight === min) { + clampedValue[0] = min; + clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max)); + } + } else { + clampedValue[0] = boundedNewValue; + clampedValue[1] = boundedNewRight; + } + } else { + // Right thumb changed + const delta = boundedNewValue - value[1]; + + // Symmetric change: move left thumb in opposite direction + const newLeft = value[0] - delta; + const boundedNewLeft = roundToStep(Math.min(Math.max(newLeft, min), max)); + + // If left would go out of bounds, adjust right too + if (newLeft !== boundedNewLeft) { + // Adjust to maintain the same width but respect bounds + if (boundedNewLeft === min) { + clampedValue[0] = min; + clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max)); + } else if (boundedNewLeft === max) { + clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min)); + clampedValue[1] = max; + } + } else { + clampedValue[0] = boundedNewLeft; + clampedValue[1] = boundedNewValue; + } + } + setValue(clampedValue); onValueChange?.(clampedValue); } }, - [value, min, max, onValueChange, step] + [value, min, max, onValueChange, step, lockMode] ); const formatValue = (val: number) => { @@ -109,8 +344,14 @@ const DoubleSlider = React.forwardRef( value={value} onValueChange={handleSliderChange} > - - + + diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx index 4b06a5b96..953161288 100644 --- a/platform/ui-next/src/components/Icons/Icons.tsx +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -75,6 +75,10 @@ import IconTransferring from './Sources/IconTransferring'; import Alert from './Sources/Alert'; import AlertOutline from './Sources/AlertOutline'; import Clipboard from './Sources/Clipboard'; +import OrientationSwitch from './Sources/OrientationSwitch'; +import LayerBackground from './Sources/LayerBackground'; +import LayerForeground from './Sources/LayerForeground'; +import LayerSegmentation from './Sources/LayerSegmentation'; import { Tool3DRotate, ToolAngle, @@ -378,6 +382,10 @@ export const Icons = { /> ), // Icons + LayerBackground, + LayerForeground, + LayerSegmentation, + OrientationSwitch, Clipboard, ActionNewDialog, GroupLayers, diff --git a/platform/ui-next/src/components/Icons/Sources/LayerBackground.tsx b/platform/ui-next/src/components/Icons/Sources/LayerBackground.tsx new file mode 100644 index 000000000..d25f0387d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LayerBackground.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LayerBackground = (props: IconProps) => ( + + + + +); + +export default LayerBackground; diff --git a/platform/ui-next/src/components/Icons/Sources/LayerForeground.tsx b/platform/ui-next/src/components/Icons/Sources/LayerForeground.tsx new file mode 100644 index 000000000..fa9ed849b --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LayerForeground.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LayerForeground = (props: IconProps) => ( + + + + +); + +export default LayerForeground; diff --git a/platform/ui-next/src/components/Icons/Sources/LayerSegmentation.tsx b/platform/ui-next/src/components/Icons/Sources/LayerSegmentation.tsx new file mode 100644 index 000000000..7395da9e4 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LayerSegmentation.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LayerSegmentation = (props: IconProps) => ( + + + +); + +export default LayerSegmentation; diff --git a/platform/ui-next/src/components/Icons/Sources/OrientationSwitch.tsx b/platform/ui-next/src/components/Icons/Sources/OrientationSwitch.tsx new file mode 100644 index 000000000..440976e3f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/OrientationSwitch.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const OrientationSwitch = (props: IconProps) => ( + + + + + + +); + +export default OrientationSwitch; diff --git a/platform/ui-next/src/components/Numeric/Numeric.tsx b/platform/ui-next/src/components/Numeric/Numeric.tsx index 40badb61f..eca111654 100644 --- a/platform/ui-next/src/components/Numeric/Numeric.tsx +++ b/platform/ui-next/src/components/Numeric/Numeric.tsx @@ -29,6 +29,7 @@ interface NumericMetaContextValue { min: number; max: number; step: number; + lockMode?: boolean; } const NumericMetaContext = createContext(null); @@ -47,6 +48,7 @@ interface NumericMetaContainerProps { max?: number; step?: number; className?: string; + lockMode?: boolean; // Add lock mode prop for doubleRange } function NumericMetaContainer({ @@ -60,6 +62,7 @@ function NumericMetaContainer({ max = 100, step = 1, className, + lockMode = false, children, }: PropsWithChildren) { // Calculate default values based on min and max @@ -115,6 +118,7 @@ function NumericMetaContainer({ min, max, step, + lockMode, }} >
{children}
@@ -236,7 +240,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) { throw new Error('DoubleRange must be used inside .'); } - const { mode, doubleValue, setDoubleValue, min, max, step } = ctx; + const { mode, doubleValue, setDoubleValue, min, max, step, lockMode } = ctx; const handleSliderChange = useCallback( (values: [number, number]) => { @@ -258,6 +262,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) { defaultValue={doubleValue} onValueChange={handleSliderChange} showNumberInputs={showNumberInputs} + lockMode={lockMode} />
); diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx index 48b99efdf..206d8359b 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx @@ -16,6 +16,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea onSegmentEdit, onSegmentDelete, data, + showSegmentIndex = true, } = useSegmentationTableContext('SegmentationSegments'); // Try to get segmentation data from expanded context first, then fall back to table context @@ -76,7 +77,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea const DataRowComponent = ( span]:line-clamp-1 hover:bg-primary/10 flex h-7 w-full items-center justify-between whitespace-nowrap rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50', + 'border-input text-foreground ring-offset-background placeholder:text-muted-foreground focus:ring-ring hover:bg-primary/10 flex h-7 w-full min-w-0 items-center justify-between rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate', className )} {...props} diff --git a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx index 1acc27738..4e44953e1 100644 --- a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx +++ b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx @@ -64,4 +64,4 @@ ViewportActionCorners.propTypes = { cornerComponents: PropTypes.object.isRequired, }; -export { ViewportActionCorners }; \ No newline at end of file +export { ViewportActionCorners }; diff --git a/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx index bf5d60649..f0e187603 100644 --- a/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx @@ -124,6 +124,7 @@ interface ViewportGridApi { getActiveViewportOptionByKey: (key: string) => any; setViewportGridSizeChanged: (props: any) => void; publishViewportsReady: () => void; + getDisplaySetsUIDsForViewport: (viewportId: string) => string[]; } // Update the context type @@ -539,6 +540,7 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props), publishViewportsReady: () => service.publishViewportsReady(), getLayoutOptionsFromState: state => service.getLayoutOptionsFromState(state), + getDisplaySetsUIDsForViewport: viewportId => service.getDisplaySetsUIDsForViewport(viewportId), }; return ( diff --git a/platform/ui-next/src/types/ViewportActionCornersTypes.ts b/platform/ui-next/src/types/ViewportActionCornersTypes.ts index f5f2f69ff..b71fef4e0 100644 --- a/platform/ui-next/src/types/ViewportActionCornersTypes.ts +++ b/platform/ui-next/src/types/ViewportActionCornersTypes.ts @@ -1,6 +1,13 @@ import { ReactNode } from 'react'; +import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners'; export interface ViewportActionCornersComponentInfo { id: string; component: ReactNode; } + +export interface ViewportActionCornersProps { + cornerComponents: Record; + visibleItemsPerCorner?: number; + isActiveViewport: boolean; +} diff --git a/playwright.config.ts b/playwright.config.ts index 7adc8070a..0975b89c0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,14 +5,14 @@ export default defineConfig({ fullyParallel: !!process.env.CI, forbidOnly: !!process.env.CI, retries: process.env.CI ? 3 : 0, - workers: process.env.CI ? 8 : undefined, + workers: process.env.CI ? 6 : undefined, snapshotPathTemplate: './tests/screenshots{/projectName}/{testFilePath}/{arg}{ext}', outputDir: './tests/test-results', reporter: [[process.env.CI ? 'blob' : 'html', { outputFolder: './tests/playwright-report' }]], globalTimeout: 800_000, timeout: 800_000, use: { - baseURL: 'http://localhost:3000', + baseURL: 'http://localhost:3335', trace: 'on-first-retry', video: 'on', testIdAttribute: 'data-cy', @@ -38,8 +38,8 @@ export default defineConfig({ //}, ], webServer: { - command: 'cross-env APP_CONFIG=config/e2e.js COVERAGE=true nyc yarn start', - url: 'http://localhost:3000', + command: 'cross-env APP_CONFIG=config/e2e.js COVERAGE=true OHIF_PORT=3335 nyc yarn start', + url: 'http://localhost:3335', reuseExistingServer: !process.env.CI, timeout: 360_000, }, diff --git a/tests/JumpToMeasurementMPR.spec.ts b/tests/JumpToMeasurementMPR.spec.ts index bcf30e2c1..4b78723f0 100644 --- a/tests/JumpToMeasurementMPR.spec.ts +++ b/tests/JumpToMeasurementMPR.spec.ts @@ -60,8 +60,25 @@ test('should hydrate in MPR correctly', async ({ page }) => { // scroll away await checkForScreenshot(page, page, screenShotPaths.jumpToMeasurementMPR.initialDraw); - // use mouse wheel to scroll away - await page.mouse.wheel(0, 100); + // Focus on the canvas first, then use mouse wheel to scroll away + await page.evaluate(() => { + // Access cornerstone directly from the window object + const cornerstone = window.cornerstone; + if (!cornerstone) { + return; + } + + const enabledElements = cornerstone.getEnabledElements(); + if (enabledElements.length === 0) { + return; + } + + const viewport = enabledElements[0].viewport; + if (viewport) { + viewport.setImageIdIndex(0); + viewport.render(); + } + }); // wait 2 seconds await page.waitForTimeout(2000); diff --git a/yarn.lock b/yarn.lock index c356eece1..404a9b206 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1393,10 +1393,10 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.9.5.tgz#9957a97d22d79747a6deb4f2b4c2a86b8fe48331" - integrity sha512-I8nPFmwNBqBwrH26w06gZfsJaHyZPN4nZC8GO7IerkVSkyYXgFEFC5iRdVuU+Kr94nWm8zPKyLqHx7hAeNudgw== +"@cornerstonejs/adapters@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.11.6.tgz#a4ec9e2206e1956b2a1f524e08737a994ddd7f82" + integrity sha512-EYLkBXfXF6UcmcZPitIkDNe02HdKykiFDnG9CtXPJkTSqFfCZnkF4Mqe4WN/husc9wia6BUMlESQRBYEgBithA== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1404,10 +1404,10 @@ gl-matrix "^3.4.3" ndarray "^1.0.19" -"@cornerstonejs/ai@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.9.5.tgz#40ac983158c325f204f605385d9bd4ad125d5844" - integrity sha512-AgVPLVoYJeefPcp6tf5UNfS8qoEJc8VxrqZab6f+LybhSa2se52h+57rCB5Ihd7SziNvWyIdux7v1Iy3GiZm+g== +"@cornerstonejs/ai@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.11.6.tgz#146bed97e452eb941013e437beb8b3416e412252" + integrity sha512-F17IDzbn1PC2Zt1S2RS4OaLqO5ccC71Mmk2/9tU3wQS7g7qmyh8JzyB3B6t3iHkSlv20p3HNh8MqSug7zHOcrg== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1443,20 +1443,20 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.9.5.tgz#b42d8b93d2862b9673963f238e56109f299ca11e" - integrity sha512-EwXeTNSlL7UnXmj8o1DigNZOsVbhtNDKT2Bbi66jhnzZQm+rTPyYeTva+FnnqvpxBb/6QqwDEJ8gwlXwV0Rk5w== +"@cornerstonejs/core@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.11.6.tgz#6d4192981af13911705914d6f5f0bd48303fc8a4" + integrity sha512-8IlnDj7MZV/pfbtdF5gdpL53hLkriBgqcTY4hBIY4iW1A1plmwADsbVXzgyXl3Bi63aovmJhiQZ1VtQx0TzZGQ== dependencies: "@kitware/vtk.js" "32.12.1" comlink "^4.4.1" gl-matrix "^3.4.3" loglevel "^1.9.2" -"@cornerstonejs/dicom-image-loader@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.9.5.tgz#4ba9c97e2875fe67c524788833bdf2e38154869d" - integrity sha512-g0GZNkZ6PBRCl/dvi6NoQlmpmZhuLLeI4cdiFwV0Ug81HPkHdn8FQaqgy1XPOMknLSL6sJGZsJN2l2nW5COQSA== +"@cornerstonejs/dicom-image-loader@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.11.6.tgz#7abebb1b091fad6401a96df47bea08b38ba81fbf" + integrity sha512-SJZ/LAw2gIyxzyQTEFWqBtyZELakqjcHOJnFxhkEmmpuJdpX4tgtV4k+6fnbKTg9OhybyP3prouMVPj5myjCZA== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" @@ -1468,25 +1468,25 @@ pako "^2.0.4" uuid "^9.0.0" -"@cornerstonejs/labelmap-interpolation@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.9.5.tgz#159b4a9be8002d996ff8f1a5d39b0a9b90a3bbcc" - integrity sha512-U7XnaqrrEHGvH3kKdTwGiwLYaydxp5GVo0aqVvZaRDXMXfP3tbOpCfMQcqD+bv4Sh+Rr0719O5aDJ0+ENpA/MA== +"@cornerstonejs/labelmap-interpolation@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.11.6.tgz#1969c44400067fa67eb6467c4adfb831149e85e8" + integrity sha512-N55MxFxlRNXHVKpsJgeO8Z2aEURINxdmyPvGODGgYjZSOXZ+iR12Aly/iZHW9d/crq4GU440d5B6EmEZ6iuW8w== dependencies: "@itk-wasm/morphological-contour-interpolation" "1.1.0" itk-wasm "1.0.0-b.165" -"@cornerstonejs/polymorphic-segmentation@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.9.5.tgz#d54145b90929f9e9e90f272acf5f416c28ca33e3" - integrity sha512-6ucJkb0X5CBBx00g3CzJGuLd6x4T99bPyKXa+y9Bek3jC+RyKI4vySS5RRKtQ4N59yuealZkBvV7dT0FseE8og== +"@cornerstonejs/polymorphic-segmentation@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.11.6.tgz#12a749443cdc2ea94a57f20fdae2c1377b6f0704" + integrity sha512-dk+G12MqqmKSd2Rz8gHo1piEDAeijBcz1EAdF+50UQlUEds1fczHwFkMrh9mlHc9treyHi5+QEIr2MKRmwaJIw== dependencies: "@icr/polyseg-wasm" "0.4.0" -"@cornerstonejs/tools@^3.9.5": - version "3.9.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.9.5.tgz#65fa00b2e20f0442cde7f0383e96253393229bcf" - integrity sha512-vdPsm4szolXmyBdDNTB1gyRCP5EEicsf+TOsTU5P6QYWR7cB+nyio7GeGK5zFQrUi0hb2a2qkVjjLQLQH9inAw== +"@cornerstonejs/tools@^3.11.6": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.11.6.tgz#63c051410d83c8ea5f68bb62b7691395e0890704" + integrity sha512-Sp1QkEaA952GgZikTNzyj125FzpYtVAYbeqCOn0E3WS7K1yw2XPuRbSCTRkRcV/J66ZoeKl4KgZVrhDlxJGAnw== dependencies: "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1"