feat(actions): simplify action corner api through the toolbarService (#5033)

This commit is contained in:
Alireza 2025-05-15 12:08:12 -04:00 committed by GitHub
parent f71b88e639
commit 7c15bb8901
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
246 changed files with 7930 additions and 4270 deletions

288
bun.lock
View File

@ -34,13 +34,13 @@
"optionalDependencies": {
"@percy/cypress": "^3.1.1",
"@playwright/test": "^1.48.0",
"cypress": "^14.1.0",
"cypress": "14.3.1",
"cypress-file-upload": "^5.0.8",
},
},
"addOns/externals/devDependencies": {
"name": "@externals/devDependencies",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1",
},
},
"extensions/cornerstone": {
"name": "@ohif/extension-cornerstone",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/ai": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/labelmap-interpolation": "^3.12.2",
"@cornerstonejs/polymorphic-segmentation": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/ai": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/labelmap-interpolation": "^3.15.1",
"@cornerstonejs/polymorphic-segmentation": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@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.12.2",
"@ohif/core": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@cornerstonejs/dicom-image-loader": "^3.15.1",
"@ohif/core": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"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.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3",
@ -317,10 +317,10 @@
"mathjs": "^12.4.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.32",
"@xstate/react": "^3.2.2",
"xstate": "^4.10.0",
},
"peerDependencies": {
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -435,17 +435,17 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
"@ohif/extension-dicom-video": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
"@ohif/extension-dicom-video": "3.11.0-beta.32",
},
},
"modes/basic-test-mode": {
"name": "@ohif/mode-test",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -455,19 +455,19 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
"@ohif/extension-dicom-video": "3.11.0-beta.27",
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
"@ohif/extension-test": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
"@ohif/extension-dicom-video": "3.11.0-beta.32",
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
"@ohif/extension-test": "3.11.0-beta.32",
},
},
"modes/longitudinal": {
"name": "@ohif/mode-longitudinal",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -477,32 +477,32 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
"@ohif/extension-dicom-video": "3.11.0-beta.27",
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
"@ohif/extension-dicom-video": "3.11.0-beta.32",
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
},
},
"modes/microscopy": {
"name": "@ohif/mode-microscopy",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
},
"peerDependencies": {
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-dicom-microscopy": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-dicom-microscopy": "3.11.0-beta.32",
},
},
"modes/preclinical-4d": {
"name": "@ohif/mode-preclinical-4d",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
},
@ -521,7 +521,7 @@
},
"modes/segmentation": {
"name": "@ohif/mode-segmentation",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -586,32 +586,32 @@
},
"platform/app": {
"name": "@ohif/app",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"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.12.2",
"@cornerstonejs/dicom-image-loader": "^3.15.1",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.11.0-beta.27",
"@ohif/extension-cornerstone": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.27",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
"@ohif/extension-default": "3.11.0-beta.27",
"@ohif/extension-dicom-microscopy": "3.11.0-beta.27",
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
"@ohif/extension-dicom-video": "3.11.0-beta.27",
"@ohif/extension-test": "3.11.0-beta.27",
"@ohif/i18n": "3.11.0-beta.27",
"@ohif/mode-basic-dev-mode": "3.11.0-beta.27",
"@ohif/mode-longitudinal": "3.11.0-beta.27",
"@ohif/mode-microscopy": "3.11.0-beta.27",
"@ohif/mode-test": "3.11.0-beta.27",
"@ohif/ui": "3.11.0-beta.27",
"@ohif/ui-next": "3.11.0-beta.27",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.32",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
"@ohif/extension-default": "3.11.0-beta.32",
"@ohif/extension-dicom-microscopy": "3.11.0-beta.32",
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
"@ohif/extension-dicom-video": "3.11.0-beta.32",
"@ohif/extension-test": "3.11.0-beta.32",
"@ohif/i18n": "3.11.0-beta.32",
"@ohif/mode-basic-dev-mode": "3.11.0-beta.32",
"@ohif/mode-longitudinal": "3.11.0-beta.32",
"@ohif/mode-microscopy": "3.11.0-beta.32",
"@ohif/mode-test": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"@ohif/ui-next": "3.11.0-beta.32",
"@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.27",
"version": "3.11.0-beta.32",
"bin": {
"ohif-cli": "src/index.js",
},
@ -676,7 +676,7 @@
},
"platform/core": {
"name": "@ohif/core",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"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.12.2",
"@cornerstonejs/dicom-image-loader": "^3.12.2",
"@ohif/ui": "3.11.0-beta.27",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/dicom-image-loader": "^3.15.1",
"@ohif/ui": "3.11.0-beta.32",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21",
},
},
"platform/i18n": {
"name": "@ohif/i18n",
"version": "3.11.0-beta.27",
"version": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"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.27",
"version": "3.11.0-beta.32",
"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.12.2", "", { "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.12.2", "@cornerstonejs/tools": "^3.12.2" } }, "sha512-yKPT9U1/CUtXu/h3xXB8KYX3x3bHgedw3nPX6eOn8oDhynfjuvA+i+4xJxKtO182mi2WnAQwFPnzkgmEVyrSqA=="],
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.15.1", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.40.0", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.15.1", "@cornerstonejs/tools": "^3.15.1" } }, "sha512-nwUPk2SXukPkkKrpzAOIOMI0lNqwTl1euI1iTLP1DB1gt8r2wy1V1vYu7+TVT9CfifhfJ+yrJnYCo1VwqnnPDA=="],
"@cornerstonejs/ai": ["@cornerstonejs/ai@3.12.2", "", { "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.12.2", "@cornerstonejs/tools": "^3.12.2" } }, "sha512-Btyey9KNcJqzIG7trFqDHo08zJVO3XtVEbfbJiUfRoztwQTKrtxOJ40dsz3pU3+PNQgG0bmTPhe9yRSpcVdwRA=="],
"@cornerstonejs/ai": ["@cornerstonejs/ai@3.15.1", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.40.0", "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.15.1", "@cornerstonejs/tools": "^3.15.1" } }, "sha512-rFcS8Hcu7Bjr+BLYMR8Y0m/eno+ynEE3Wd2gM48VNV5pXQ4HfjbA4kl7r4JyZKqyPZYgV2Api42v0vKLeSSZ6g=="],
"@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.12.2", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-AKFHr7XF5zKUo1BiclJmUz913yD9NiQgLl3krTmvtlWPuOUhpxoCfipW0De5dMeVuQ86iQDV1F+p7HuWOWfdwg=="],
"@cornerstonejs/core": ["@cornerstonejs/core@3.15.1", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-yXYjNyAlbKOfx6Xjd2xXnRuFrOCEHRP3yC6SQDywp8/WJ4aIiIXqU8Hgxht+/22UDjjoe5DcF2H+d5yX6SZLMg=="],
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.12.2", "", { "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.12.2" } }, "sha512-LaKnv5OPMlRPzkuqReVl1Gpa9ERTQJXl/1/Iku9wbWjnGrkzdF4Ef8bG0DseLx57qMOJPJP++PsxeXCaOgXvyw=="],
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.15.1", "", { "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.15.1" } }, "sha512-/L2pL8121Vx4OgCw4R5ZxLLfZbBSGTItWASFDeImlT9jI++KHnOpVQRugYHChMj4dHb45yX8dAify+AhSMNkXg=="],
"@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.12.2", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.12.2", "@cornerstonejs/tools": "^3.12.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-5T2OtNyV2jMy66k2clG/Krk1x4xFOxlS9sGxT1eMOrELClzpY+cL+IuA6f0aD6vTwkp1fFojEIa0IFbwuCK8HQ=="],
"@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.15.1", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.15.1", "@cornerstonejs/tools": "^3.15.1", "@kitware/vtk.js": "32.12.1" } }, "sha512-94zxc91G9V+Dp3dodqSVde5ia9sUZNaP7Eud3Tsyej4unu4frIIx2/fRGVNSdq3pLNM7MT0KLfGeut+I7S3cHQ=="],
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.12.2", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.12.2", "@cornerstonejs/tools": "^3.12.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-pmOpmOKUaAM3EIklZLufWJ9IVTRpp54MF+B6cnirGEYYobjFT+uTrLwrExwVC1kEVHi8o4n5X8SJnawtIu/ugA=="],
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.15.1", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.15.1", "@cornerstonejs/tools": "^3.15.1", "@kitware/vtk.js": "32.12.1" } }, "sha512-C/A/SLhOkUi0I7yXkWwu96LhaajQSIcMZX6kmU2KEuBIwRNZ2oaZm4vDF2srTJZtxKeJ/gaD0BJEn5LJFYHFfw=="],
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.12.2", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.12.2", "@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-Zx9BFl2OAuCQGrr6o+e1tV3fMSoAARC7HlVWprUrGWwDA29LVRxQT+cc+Wo76GjxcKSMo5RuLt1+sgRj0+CsAA=="],
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.15.1", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.15.1", "@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-QyowzqiitrwFomZGKvIeAbVMpjlzPjOOiMkLZUAGoSvLCBCjS9zDZFtnZ33cRgIqzVrzmVFWAn1Fi3fzqugQZQ=="],
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
@ -2674,7 +2674,7 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"cypress": ["cypress@14.3.0", "", { "dependencies": { "@cypress/request": "^3.0.8", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.5", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "semver": "^7.7.1", "supports-color": "^8.1.1", "tmp": "~0.2.3", "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" } }, "sha512-rRfPl9Z0/CczuYybBEoLbDVuT1OGkhYaJ0+urRCshgiDRz6QnoA0KQIQnPx7MJ3zy+VCsbUU1pV74n+6cbJEdg=="],
"cypress": ["cypress@14.3.1", "", { "dependencies": { "@cypress/request": "^3.0.8", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.5", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "semver": "^7.7.1", "supports-color": "^8.1.1", "tmp": "~0.2.3", "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" } }, "sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg=="],
"cypress-file-upload": ["cypress-file-upload@5.0.8", "", { "peerDependencies": { "cypress": ">3.0.0" } }, "sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g=="],
@ -5454,11 +5454,11 @@
"@cornerstonejs/adapters/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"@cornerstonejs/adapters/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="],
"@cornerstonejs/adapters/dcmjs": ["dcmjs@0.40.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-FId8dPpDg45ynIjpWF1mXlA77YcQBcE/eQGgkQpJqB0k9NF4simMYbSm9pJ5ORizYdDzOzjZpVNVym/vnFQJzQ=="],
"@cornerstonejs/ai/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"@cornerstonejs/ai/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="],
"@cornerstonejs/ai/dcmjs": ["dcmjs@0.40.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-FId8dPpDg45ynIjpWF1mXlA77YcQBcE/eQGgkQpJqB0k9NF4simMYbSm9pJ5ORizYdDzOzjZpVNVym/vnFQJzQ=="],
"@cornerstonejs/core/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "xml2json": "Utilities/XMLConverter/xml2json-cli.js", "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="],
@ -5596,6 +5596,8 @@
"@ohif/app/core-js": ["core-js@3.42.0", "", {}, "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g=="],
"@ohif/extension-test/dcmjs": ["dcmjs@0.40.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-FId8dPpDg45ynIjpWF1mXlA77YcQBcE/eQGgkQpJqB0k9NF4simMYbSm9pJ5ORizYdDzOzjZpVNVym/vnFQJzQ=="],
"@ohif/mode-segmentation/@babel/preset-env": ["@babel/preset-env@7.23.2", "", { "dependencies": { "@babel/compat-data": "^7.23.2", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.22.5", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.22.5", "@babel/plugin-transform-async-generator-functions": "^7.23.2", "@babel/plugin-transform-async-to-generator": "^7.22.5", "@babel/plugin-transform-block-scoped-functions": "^7.22.5", "@babel/plugin-transform-block-scoping": "^7.23.0", "@babel/plugin-transform-class-properties": "^7.22.5", "@babel/plugin-transform-class-static-block": "^7.22.11", "@babel/plugin-transform-classes": "^7.22.15", "@babel/plugin-transform-computed-properties": "^7.22.5", "@babel/plugin-transform-destructuring": "^7.23.0", "@babel/plugin-transform-dotall-regex": "^7.22.5", "@babel/plugin-transform-duplicate-keys": "^7.22.5", "@babel/plugin-transform-dynamic-import": "^7.22.11", "@babel/plugin-transform-exponentiation-operator": "^7.22.5", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-for-of": "^7.22.15", "@babel/plugin-transform-function-name": "^7.22.5", "@babel/plugin-transform-json-strings": "^7.22.11", "@babel/plugin-transform-literals": "^7.22.5", "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", "@babel/plugin-transform-member-expression-literals": "^7.22.5", "@babel/plugin-transform-modules-amd": "^7.23.0", "@babel/plugin-transform-modules-commonjs": "^7.23.0", "@babel/plugin-transform-modules-systemjs": "^7.23.0", "@babel/plugin-transform-modules-umd": "^7.22.5", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.22.5", "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", "@babel/plugin-transform-numeric-separator": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.22.15", "@babel/plugin-transform-object-super": "^7.22.5", "@babel/plugin-transform-optional-catch-binding": "^7.22.11", "@babel/plugin-transform-optional-chaining": "^7.23.0", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.11", "@babel/plugin-transform-property-literals": "^7.22.5", "@babel/plugin-transform-regenerator": "^7.22.10", "@babel/plugin-transform-reserved-words": "^7.22.5", "@babel/plugin-transform-shorthand-properties": "^7.22.5", "@babel/plugin-transform-spread": "^7.22.5", "@babel/plugin-transform-sticky-regex": "^7.22.5", "@babel/plugin-transform-template-literals": "^7.22.5", "@babel/plugin-transform-typeof-symbol": "^7.22.5", "@babel/plugin-transform-unicode-escapes": "^7.22.10", "@babel/plugin-transform-unicode-property-regex": "^7.22.5", "@babel/plugin-transform-unicode-regex": "^7.22.5", "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "0.1.6-no-external-plugins", "@babel/types": "^7.23.0", "babel-plugin-polyfill-corejs2": "^0.4.6", "babel-plugin-polyfill-corejs3": "^0.8.5", "babel-plugin-polyfill-regenerator": "^0.5.3", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ=="],
"@ohif/mode-segmentation/babel-eslint": ["babel-eslint@10.1.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", "@babel/traverse": "^7.7.0", "@babel/types": "^7.7.0", "eslint-visitor-keys": "^1.0.0", "resolve": "^1.12.0" }, "peerDependencies": { "eslint": ">= 4.12.1" } }, "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg=="],

View File

@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}

View File

@ -1,16 +1,10 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const { displaySets, children, viewportOptions, displaySetOptions, servicesManager } = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
servicesManager.services;
@ -59,9 +53,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
displaySetOptions.unshift({});
const [pmapDisplaySetOptions] = displaySetOptions;
@ -93,7 +84,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
});
return (
<Component
<OHIFCornerstoneViewport
{...props}
// Referenced + PMAP displaySets must be passed as parameter in this order
displaySets={[referencedDisplaySet, pmapDisplaySet]}
@ -104,15 +95,16 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
presentationIds: viewportOptions.presentationIds,
}}
displaySetOptions={[{}, pmapDisplaySetOptions]}
></Component>
/>
);
}, [
extensionManager,
displaySetOptions,
props,
pmapDisplaySet,
viewportOptions.orientation,
viewportOptions.viewportId,
viewportOptions.presentationIds,
uiNotificationService,
]);
// Cleanup the PMAP viewport when the viewport is destroyed

View File

@ -1,8 +1,4 @@
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
import { utils, Types } from '@ohif/extension-cornerstone';
function promptHydrateRT({
servicesManager,
@ -10,74 +6,20 @@ function promptHydrateRT({
viewportId,
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
const { uiViewportDialogService, customizationService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}
});
}
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = customizationService.getCustomization('viewportNotification.hydrateRTMessage');
const actions = [
{
id: 'no-hydrate',
type: 'secondary',
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: 'primary',
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
];
const onSubmit = result => {
uiViewportDialogService.hide();
resolve(result);
};
uiViewportDialogService.show({
id: 'promptHydrateRT',
viewportId,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
}: {
servicesManager: AppTypes.ServicesManager;
rtDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateRTDisplaySet: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: rtDisplaySet,
preHydrateCallbacks,
hydrateCallback: hydrateRTDisplaySet,
type: 'RTSTRUCT',
});
}

View File

@ -1,35 +1,27 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { utils } from '@ohif/extension-cornerstone';
import {
utils,
usePositionPresentationStore,
OHIFCornerstoneViewport,
} from '@ohif/extension-cornerstone';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { useSystem } from '@ohif/core/src';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props: withAppTypes) {
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const {
displaySetService,
toolGroupService,
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -46,30 +38,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const rtDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const [{ viewports, activeViewportId }, viewportGridService] = useViewportGrid();
// States
const selectedSegmentObjectIndex: number = 0;
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the RT is opened and segments are loaded into the
// segmentation panel, and RT is also rendered on any viewport that is in the
// same frameOfReferenceUID as the referencedSeriesUID of the RT. However,
// loading basically means RT loading over network and bit unpacking of the
// RT data.
const [isHydrated, setIsHydrated] = useState(rtDisplaySet.isHydrated);
const [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded);
const [element, setElement] = useState(null);
const [processingProgress, setProcessingProgress] = useState({
percentComplete: null,
totalSegments: null,
});
// refs
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
@ -80,100 +61,31 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
const onElementDisabled = () => {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
utils.handleSegmentChange({
direction,
segDisplaySet: rtDisplaySet,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
});
},
[selectedSegmentObjectIndex]
);
useEffect(() => {
if (rtIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateRT({
servicesManager,
viewportId,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateRTDisplaySet,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
hydrateRTDisplaySet: async () => {
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
});
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager, activeViewportId]);
useEffect(() => {
// I'm not sure what is this, since in RT we support Overlapping segments
// via contours
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@ -193,14 +105,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
viewPresentation: {},
});
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);
@ -210,7 +114,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}, [rtDisplaySet]);
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
const segmentLoadingSubscription = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
({ percentComplete, numSegments }) => {
setProcessingProgress({
@ -220,16 +124,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}
);
return () => {
unsubscribe();
};
}, [rtDisplaySet]);
/**
Cleanup the SEG viewport when the viewport is destroyed
*/
useEffect(() => {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
const displaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports.get(activeViewportId);
@ -243,9 +138,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
);
return () => {
onDisplaySetsRemovedSubscription.unsubscribe();
segmentLoadingSubscription.unsubscribe();
displaySetsRemovedSubscription.unsubscribe();
};
}, []);
}, [rtDisplaySet, displaySetService, viewports, activeViewportId, viewportGridService]);
useEffect(() => {
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
@ -259,38 +155,33 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(rtDisplaySet.isHydrated);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
// Todo: jump to the center of the first segment
return (
<OHIFCornerstoneViewport
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
}}
/>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
if (
@ -313,43 +204,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
useEffect(() => {
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topRight,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">

View File

@ -1,59 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
switch (isHydrated) {
case true:
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}

View File

@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}

View File

@ -1,8 +1,4 @@
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
import { utils, Types } from '@ohif/extension-cornerstone';
function promptHydrateSEG({
servicesManager,
@ -10,75 +6,20 @@ function promptHydrateSEG({
viewportId,
preHydrateCallbacks,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService, customizationService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
resolve(isHydrated);
}, 0);
}
});
}
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = customizationService.getCustomization('viewportNotification.hydrateSEGMessage');
const actions = [
{
id: 'no-hydrate',
type: 'secondary',
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: 'primary',
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
];
const onSubmit = result => {
uiViewportDialogService.hide();
resolve(result);
};
uiViewportDialogService.show({
viewportId,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
}: {
servicesManager: AppTypes.ServicesManager;
segDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateCallback: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: segDisplaySet as AppTypes.DisplaySet,
preHydrateCallbacks,
hydrateCallback,
type: 'SEG',
});
}

View File

@ -1,36 +1,24 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { utils } from '@ohif/extension-cornerstone';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const viewportId = viewportOptions.viewportId;
const {
displaySetService,
toolGroupService,
segmentationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
@ -47,7 +35,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const selectedSegmentObjectIndex: number = 0;
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the SEG is opened and segments are loaded into the
@ -55,9 +42,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// same frameOfReferenceUID as the referencedSeriesUID of the SEG. However,
// loading basically means SEG loading over network and bit unpacking of the
// SEG data.
const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated);
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
const [element, setElement] = useState(null);
const [processingProgress, setProcessingProgress] = useState({
percentComplete: null,
totalSegments: null,
@ -82,36 +67,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
const onElementDisabled = () => {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
// Todo: jump to the center of the first segment
return (
<Component
<OHIFCornerstoneViewport
{...props}
displaySets={[segDisplaySet]}
viewportOptions={{
@ -123,66 +82,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
/>
);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
utils.handleSegmentChange({
direction,
segDisplaySet: segDisplaySet,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
});
},
[selectedSegmentObjectIndex]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
useEffect(() => {
if (segIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateSEG({
servicesManager,
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
hydrateCallback: async () => {
await commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: segDisplaySet,
viewportId,
});
return true;
},
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, commandsManager, activeViewportId]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
@ -283,54 +211,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
};
}, []);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
useEffect(() => {
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topRight,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;

View File

@ -1,59 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
switch (isHydrated) {
case true:
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}

View File

@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"classnames": "^2.3.2"
}
}

View File

@ -160,36 +160,18 @@ const commandsModule = (props: withAppTypes) => {
* Loads measurements by hydrating and loading the SR for the given display set instance UID
* and displays it in the active viewport.
*/
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
hydrateStructuredReport: ({ displaySetInstanceUID }) => {
return hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: [
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
],
});
}
},
};
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
downloadReport: actions.downloadReport,
storeMeasurements: actions.storeMeasurements,
hydrateStructuredReport: actions.hydrateStructuredReport,
};
return {

View File

@ -1,25 +1,24 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ExtensionManager, useToolbar } from '@ohif/core';
import React, { useCallback, useEffect, useState } from 'react';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { ViewportActionArrows } from '@ohif/ui-next';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { useViewportGrid } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
function OHIFCornerstoneSRMeasurementViewport(props) {
const { servicesManager } = useSystem();
const { children, dataSource, displaySets, viewportOptions } = props as {
children: React.ReactNode;
dataSource: unknown;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const { displaySetService, viewportActionCornersService } = servicesManager.services;
const { displaySetService } = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@ -34,40 +33,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
const [element, setElement] = useState(null);
const { viewports, activeViewportId } = viewportGrid;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
MEASUREMENT_TRACKING_EXTENSION_ID
);
if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
);
const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
* when multiple viewports are used.
*/
const setTrackingIdentifiers = useCallback(
measurementSelected => {
const { measurements } = srDisplaySet;
@ -105,10 +75,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@ -140,10 +106,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
return null;
}
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { measurements } = srDisplaySet;
const measurement = measurements[measurementSelected];
@ -152,7 +114,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
}
return (
<Component
<OHIFCornerstoneViewport
{...props}
// should be passed second since we don't want SR displaySet to
// override the activeImageDisplaySetData
@ -176,27 +138,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
onElementEnabled(evt);
}}
isJumpToMeasurementDisabled={true}
></Component>
/>
);
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
const onMeasurementChange = useCallback(
direction => {
let newMeasurementSelected = measurementSelected;
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected);
},
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
);
/**
Cleanup the SR viewport when the viewport is destroyed
*/
@ -231,8 +176,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
@ -255,42 +198,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
t,
servicesManager,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topRight,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
index: 0,
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [isLocked, onMeasurementChange, srDisplaySet, t, viewportActionCornersService, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@ -327,8 +234,6 @@ OHIFCornerstoneSRMeasurementViewport.propTypes = {
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
async function _getViewportReferencedDisplaySetData(
@ -372,130 +277,4 @@ async function _getViewportReferencedDisplaySetData(
return { referencedDisplaySetMetadata, referencedDisplaySet };
}
function _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable,
isLocked,
t,
servicesManager,
}) {
const loadStr = t('LOAD');
// 1 - Incompatible
// 2 - Locked
// 3 - Rehydratable / Open
const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
let ToolTipMessage = null;
let StatusIcon = null;
switch (state) {
case 1:
StatusIcon = () => (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is not compatible
<br />
with this application.
</div>
);
break;
case 2:
StatusIcon = () => (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is currently read-only
<br />
because you are tracking measurements in
<br />
another viewport.
</div>
);
break;
case 3:
StatusIcon = () => (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => {
const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'loadSRMeasurements',
});
const commandOptions = {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
};
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon className="h-4 w-4" />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<>
{loadSRMeasurementsButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
const tool = (
<Component
key={id}
id={id}
onInteraction={args => onInteraction({ ...args, ...commandOptions })}
{...componentProps}
/>
);
return <div key={id}>{tool}</div>;
})}
</>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
>
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
export default OHIFCornerstoneSRMeasurementViewport;

View File

@ -1,11 +1,7 @@
import React from 'react';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { ViewportActionButton } from '@ohif/ui-next';
import i18n from '@ohif/i18n';
export default function onModeEnter({ servicesManager }) {
const { displaySetService, toolbarService } = servicesManager.services;
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
@ -16,24 +12,4 @@ export default function onModeEnter({ servicesManager }) {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
toolbarService.addButtons([
{
// A base/default button for loading measurements. It is added to the toolbar below.
// Customizations to this button can be made in the mode or by another extension.
// For example, the button label can be changed and/or the command to clear
// the measurements can be dropped.
id: 'loadSRMeasurements',
component: props => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
props: {
commands: ['clearMeasurements', 'loadSRMeasurements'],
},
},
]);
// The toolbar used in the viewport's status bar. Modes and extensions can further customize
// it to optionally add other buttons.
toolbarService.createButtonSection('loadSRMeasurements', ['loadSRMeasurements']);
}

View File

@ -63,5 +63,4 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}

View File

@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"classnames": "^2.3.2"
}
}

View File

@ -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.12.2",
"@cornerstonejs/dicom-image-loader": "^3.15.1",
"@ohif/core": "3.11.0-beta.32",
"@ohif/ui": "3.11.0-beta.32",
"dcmjs": "*",
@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.12.2",
"@cornerstonejs/ai": "^3.12.2",
"@cornerstonejs/core": "^3.12.2",
"@cornerstonejs/labelmap-interpolation": "^3.12.2",
"@cornerstonejs/polymorphic-segmentation": "^3.12.2",
"@cornerstonejs/tools": "^3.12.2",
"@cornerstonejs/adapters": "^3.15.1",
"@cornerstonejs/ai": "^3.15.1",
"@cornerstonejs/core": "^3.15.1",
"@cornerstonejs/labelmap-interpolation": "^3.15.1",
"@cornerstonejs/polymorphic-segmentation": "^3.15.1",
"@cornerstonejs/tools": "^3.15.1",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",

View File

@ -1,7 +1,7 @@
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 { MeasurementService, useViewportRef } from '@ohif/core';
import { useViewportDialog } from '@ohif/ui-next';
import type { Types as csTypes } from '@cornerstonejs/core';
@ -21,6 +21,9 @@ import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewport
const STACK = 'stack';
// Cache for viewport dimensions, persists across component remounts
const viewportDimensions = new Map<string, { width: number; height: number }>();
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo(
@ -79,6 +82,7 @@ const OHIFCornerstoneViewport = React.memo(
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const viewportRef = useViewportRef(viewportId);
const {
displaySetService,
@ -88,7 +92,6 @@ const OHIFCornerstoneViewport = React.memo(
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
customizationService,
measurementService,
} = servicesManager.services;
@ -101,12 +104,28 @@ const OHIFCornerstoneViewport = React.memo(
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}, [elementRef, cornerstoneViewportService, setImageScrollBarHeight]);
const onResize = useCallback(
(entries: ResizeObserverEntry[]) => {
if (elementRef.current && entries?.length) {
const entry = entries[0];
const { width, height } = entry.contentRect;
const prevDimensions = viewportDimensions.get(viewportId) || { width: 0, height: 0 };
// Check if dimensions actually changed and then only resize if they have changed
const hasDimensionsChanged =
prevDimensions.width !== width || prevDimensions.height !== height;
if (width > 0 && height > 0 && hasDimensionsChanged) {
viewportDimensions.set(viewportId, { width, height });
// Perform resize operations
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}
},
[viewportId, elementRef, cornerstoneViewportService, setImageScrollBarHeight]
);
useEffect(() => {
const element = elementRef.current;
@ -133,16 +152,8 @@ const OHIFCornerstoneViewport = React.memo(
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
segmentationService.clearSegmentationRepresentations(viewportId);
viewportActionCornersService.clear(viewportId);
},
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
[viewportId, segmentationService, syncGroupService, toolGroupService]
);
const elementEnabledHandler = useCallback(
@ -210,6 +221,7 @@ const OHIFCornerstoneViewport = React.memo(
}
cornerstoneViewportService.disableElement(viewportId);
viewportRef.unregister();
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
@ -328,7 +340,11 @@ const OHIFCornerstoneViewport = React.memo(
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={elementRef}
data-viewportId={viewportId}
ref={el => {
elementRef.current = el;
if (el) viewportRef.register(el);
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
@ -346,10 +362,6 @@ const OHIFCornerstoneViewport = React.memo(
viewportId={viewportId}
servicesManager={servicesManager}
/>
<ViewportColorbarsContainer
viewportId={viewportId}
viewportElementRef={elementRef}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">

View File

@ -11,18 +11,22 @@
.ViewportOrientationMarkers .top-mid {
top: 0.38rem;
left: 50%;
transform: translateX(-50%);
}
.ViewportOrientationMarkers .left-mid {
top: 47%;
top: 50%;
left: 0.38rem;
transform: translateY(-50%);
}
.ViewportOrientationMarkers .right-mid {
top: 47%;
top: 50%;
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
transform: translateY(-50%);
}
.ViewportOrientationMarkers .bottom-mid {
top: calc(100% - var(--marker-height) - 0.6rem);
left: 47%;
left: 50%;
transform: translateX(-50%);
}
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
display: flex;

View File

@ -37,6 +37,7 @@ import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownload
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
@ -127,6 +128,68 @@ function commandsModule({
}
const actions = {
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) {
return;
}
if (displaySet.isOverlayDisplaySet) {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet,
type:
displaySet.Modality === 'SEG'
? SegmentationRepresentations.Labelmap
: SegmentationRepresentations.Contour,
});
}
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
const storePositionPresentation = refDisplaySet => {
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUIDs: [refDisplaySet.displaySetInstanceUID],
});
};
if (displaySet.Modality === 'SEG' || displaySet.Modality === 'RTSTRUCT') {
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
storePositionPresentation(referencedDisplaySet);
return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
} else if (displaySet.Modality === 'SR') {
const results = commandsManager.runCommand('hydrateStructuredReport', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
});
const { SeriesInstanceUIDs } = results;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
SeriesInstanceUIDs[0]
);
referencedDisplaySets.forEach(storePositionPresentation);
if (referencedDisplaySets.length) {
actions.setDisplaySetsForViewports({
viewportsToUpdate: [
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [referencedDisplaySets[0].displaySetInstanceUID],
},
],
});
}
return results;
}
},
runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => {
// Get active segmentation if not specified
const targetSegmentation =
@ -576,10 +639,20 @@ function commandsModule({
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setViewportWindowLevel({ viewportId, window, level }) {
setViewportWindowLevel({
viewportId,
windowWidth,
windowCenter,
displaySetInstanceUID,
}: {
viewportId: string;
windowWidth: number;
windowCenter: number;
displaySetInstanceUID?: string;
}) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const windowWidthNum = Number(windowWidth);
const windowCenterNum = Number(windowCenter);
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
@ -587,12 +660,28 @@ function commandsModule({
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
if (viewport instanceof BaseVolumeViewport) {
const volumeId = actions.getVolumeIdForDisplaySet({
viewportId,
displaySetInstanceUID,
});
viewport.setProperties(
{
voiRange: {
upper,
lower,
},
},
volumeId
);
} else {
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
}
viewport.render();
},
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
@ -641,10 +730,19 @@ function commandsModule({
actions.setViewportWindowLevel({
viewportId: activeViewport,
window: windowLevelPreset.window,
level: windowLevelPreset.level,
windowWidth: windowLevelPreset.window,
windowCenter: windowLevelPreset.level,
});
},
getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
return volumeId;
}
return null;
},
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
const { viewports } = viewportGridService.getState();
@ -1753,11 +1851,11 @@ function commandsModule({
displaySetInstanceUIDs,
});
updatedViewports.forEach(viewport => {
viewportGridService.setDisplaySetsForViewport({
actions.setDisplaySetsForViewports({
viewportsToUpdate: updatedViewports.map(viewport => ({
viewportId: viewport.viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
});
})),
});
},
setViewportOrientation: ({ viewportId, orientation }) => {
@ -2065,6 +2163,8 @@ function commandsModule({
addNewSegment: actions.addNewSegment,
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
setViewportOrientation: actions.setViewportOrientation,
hydrateSecondaryDisplaySet: actions.hydrateSecondaryDisplaySet,
getVolumeIdForDisplaySet: actions.getVolumeIdForDisplaySet,
};
return {

View File

@ -0,0 +1,116 @@
import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks';
import React, { useState, useEffect, useRef } from 'react';
import { useViewportRendering } from '../../hooks';
function AdvancedRenderingControls({
viewportId,
location,
buttonSection,
}: {
viewportId: string;
location: number;
buttonSection: string;
}) {
const {
onInteraction,
toolbarButtons,
isItemOpen,
isItemLocked,
openItem,
closeItem,
toggleLock,
} = useToolbar({
buttonSection,
});
const mousePosition = useViewportMousePosition(viewportId);
const [isAtBottom, setIsAtBottom] = useState(false);
const [showAllIcons, setShowAllIcons] = useState(true);
const firstMountRef = useRef(true);
const { hasColorbar } = useViewportRendering(viewportId);
useEffect(() => {
if (firstMountRef.current) {
firstMountRef.current = false;
const timer = setTimeout(() => {
setShowAllIcons(false);
}, 3000);
return () => clearTimeout(timer);
}
}, []);
useEffect(() => {
if (!showAllIcons && mousePosition.isInViewport) {
if (mousePosition.isInBottomPercentage(10)) {
setIsAtBottom(true);
} else {
setIsAtBottom(false);
}
}
}, [mousePosition, showAllIcons]);
if (!toolbarButtons?.length) {
return null;
}
if (!hasColorbar) {
return null;
}
return (
<div className="flex flex-row gap-2">
{toolbarButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
// Enhanced props with state and actions - respecting viewport specificity
const enhancedProps = {
...componentProps,
isOpen: isItemOpen(id, viewportId),
isLocked: isItemLocked(id, viewportId),
onOpen: () => openItem(id, viewportId),
onClose: () => closeItem(id, viewportId),
onToggleLock: () => toggleLock(id, viewportId),
viewportId,
};
const tool = (
<Component
key={id}
id={id}
location={location}
onInteraction={args => {
onInteraction({
...args,
itemId: id,
viewportId,
});
}}
{...enhancedProps}
/>
);
// Always show all icons on first mount for 3 seconds
// After that, always show Colorbar, show others only when mouse is at bottom
const shouldBeVisible = showAllIcons || id === 'Colorbar' || isAtBottom;
return (
<div
key={id}
className={shouldBeVisible ? 'opacity-100' : 'pointer-events-none opacity-0'}
style={{ transition: 'opacity 0.2s ease-in-out' }}
>
{tool}
</div>
);
})}
</div>
);
}
export default AdvancedRenderingControls;

View File

@ -0,0 +1 @@
export { default } from './AdvancedRenderingControls';

View File

@ -0,0 +1,168 @@
import React, { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
/**
* ModalityLoadBadge displays the status and actionable buttons for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) or when tracking measurements
*/
function ModalityLoadBadge({ viewportId }: { viewportId: string }) {
const { commandsManager } = useSystem();
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
const { isTracked, isLocked } = useMeasurementTracking({ viewportId });
const { backgroundDisplaySet, overlayDisplaySets } = useViewportDisplaySets(viewportId);
const [specialDisplaySet, setSpecialDisplaySet] = useState(null);
const allDisplaySets = useMemo(() => {
return [backgroundDisplaySet, ...overlayDisplaySets].filter(Boolean);
}, [backgroundDisplaySet, overlayDisplaySets]);
useEffect(() => {
const displaySet = allDisplaySets.find(ds => ds.isOverlayDisplaySet || ds?.Modality === 'SR');
setSpecialDisplaySet(displaySet || null);
}, [allDisplaySets]);
const statusInfo = useMemo(() => {
if (isTracked && !specialDisplaySet) {
return {
type: 'SR',
isHydrated: true,
isTracked,
displaySet: null,
tooltip: 'Currently tracking measurements in this viewport',
};
}
if (!specialDisplaySet) {
return {
type: null,
};
}
const type = specialDisplaySet.Modality;
const isHydrated = specialDisplaySet.isHydrated || false;
let tooltip = null;
const isRehydratable = specialDisplaySet.isRehydratable || false;
if (type === 'SEG') {
tooltip = isHydrated
? 'This Segmentation is loaded in the segmentation panel'
: 'Click LOAD to load segmentation.';
} else if (type === 'RTSTRUCT') {
tooltip = isHydrated
? 'This RTSTRUCT is loaded in the segmentation panel'
: 'Click LOAD to load RTSTRUCT.';
} else if (type === 'SR') {
if (!isRehydratable) {
tooltip = 'This structured report is not compatible with this application.';
} else if (isRehydratable && isLocked) {
tooltip =
'This structured report is currently read-only because you are tracking measurements in another viewport.';
} else {
tooltip = `Click ${loadStr} to restore measurements.`;
}
}
return {
type,
isHydrated,
displaySet: specialDisplaySet,
isRehydratable,
tooltip,
};
}, [specialDisplaySet, loadStr, isLocked, isTracked]);
// Nothing to show if there's no special display set type or tracking
if (!statusInfo.type && !isTracked) {
return null;
}
const StatusIcon = () => {
if (statusInfo.type === 'SR') {
if (statusInfo.isRehydratable && !isLocked) {
return (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
}
if (statusInfo.isRehydratable && isLocked) {
return (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
} else {
return (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
}
} else {
return <Icons.StatusUntracked className="h-4 w-4" />;
}
};
const StatusArea = () => {
if (!statusInfo.isHydrated) {
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">{statusInfo.type}</span>
</div>
{/* We don't show the load button for SRs because we handle it in the SR extension
via the tracked measurement context that works with state machine, this is not a regression right now */}
{statusInfo.type !== 'SR' && (
<ViewportActionButton
onInteraction={() => {
commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: statusInfo.displaySet,
viewportId,
});
}}
>
{loadStr}
</ViewportActionButton>
)}
</div>
);
}
return null;
};
return (
<>
{statusInfo.tooltip && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<div>{statusInfo.tooltip}</div>
</TooltipContent>
</Tooltip>
)}
{!statusInfo.tooltip && <StatusArea />}
</>
);
}
export default ModalityLoadBadge;

View File

@ -0,0 +1 @@
export { default } from './ModalityLoadBadge';

View File

@ -0,0 +1,151 @@
import React, { useCallback, useState } from 'react';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { utils } from '../..';
import { useViewportSegmentations } from '../../hooks';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
/**
* NavigationComponent provides navigation controls for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) to navigate between segments or measurements
*/
function NavigationComponent({ viewportId }: { viewportId: string }) {
const { servicesManager } = useSystem();
const { segmentationService, cornerstoneViewportService, measurementService } =
servicesManager.services;
// Get tracking information
const { isTracked, trackedMeasurementUIDs } = useMeasurementTracking({ viewportId });
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const [measurementSelected, setMeasurementSelected] = useState(0);
const isSRDisplaySet = viewportDisplaySets.some(displaySet => displaySet?.Modality === 'SR');
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
// Get segmentation information
const { segmentationsWithRepresentations } = useViewportSegmentations({
viewportId,
});
const hasSegmentations = segmentationsWithRepresentations.length > 0;
// prefer segment navigation if available
const navigationMode = hasSegmentations
? 'segment'
: isSRDisplaySet
? 'measurement'
: isTracked
? 'measurement'
: null;
const handleMeasurementNavigation = useCallback(
(direction: number) => {
const measurementDisplaySet = viewportDisplaySets.find(
displaySet => displaySet?.Modality === 'SR'
);
if (measurementDisplaySet) {
const measurements = measurementDisplaySet.measurements;
if (measurements.length <= 0) {
return;
}
const newIndex = getNextIndex(measurementSelected, direction, measurements.length);
setMeasurementSelected(newIndex);
const measurement = measurements[newIndex];
cornerstoneViewport.setViewReference({
referencedImageId: measurement.imageId,
});
return;
}
if (isTracked && trackedMeasurementUIDs.length > 0) {
const newIndex = getNextIndex(
measurementSelected,
direction,
trackedMeasurementUIDs.length
);
setMeasurementSelected(newIndex);
measurementService.jumpToMeasurement(viewportId, trackedMeasurementUIDs[newIndex]);
}
},
[
viewportId,
cornerstoneViewport,
measurementSelected,
measurementService,
isTracked,
trackedMeasurementUIDs,
viewportDisplaySets,
]
);
const handleSegmentNavigation = useCallback(
(direction: number) => {
if (!segmentationsWithRepresentations.length) {
return;
}
const segmentationId = segmentationsWithRepresentations[0].segmentation.segmentationId;
utils.handleSegmentChange({
direction,
segmentationId,
viewportId,
selectedSegmentObjectIndex: 0,
segmentationService,
});
},
[segmentationsWithRepresentations, viewportId, segmentationService]
);
// Handle navigation between segments/measurements
const handleNavigate = useCallback(
(direction: number) => {
if (navigationMode === 'segment') {
handleSegmentNavigation(direction);
} else if (navigationMode === 'measurement') {
handleMeasurementNavigation(direction);
}
},
[navigationMode, handleSegmentNavigation, handleMeasurementNavigation]
);
// Only render if we have a navigation mode
if (!navigationMode) {
return null;
}
return (
<ViewportActionArrows
onArrowsClick={handleNavigate}
className="h-6"
/>
);
}
/**
* Calculate the next index with circular navigation support
* @param currentIndex Current index position
* @param direction Direction of movement (1 for next, -1 for previous)
* @param totalItems Total number of items to navigate through
* @returns The next index with wrap-around support
*/
function getNextIndex(currentIndex: number, direction: number, totalItems: number): number {
if (totalItems <= 0) {
return 0;
}
// Use modulo to handle circular navigation
let nextIndex = (currentIndex + direction) % totalItems;
// Handle negative index when going backwards from index 0
if (nextIndex < 0) {
nextIndex = totalItems - 1;
}
return nextIndex;
}
export default NavigationComponent;

View File

@ -0,0 +1,3 @@
import NavigationComponent from './NavigationComponent';
export default NavigationComponent;

View File

@ -1,55 +1,94 @@
import React from 'react';
import {
useViewportActionCorners,
ViewportActionCorners,
ViewportActionCornersLocations,
} from '@ohif/ui-next';
import React, { memo } from 'react';
import { ViewportActionCorners, IconPresentationProvider, ToolButton } from '@ohif/ui-next';
import { Toolbar } from '@ohif/extension-default/src/Toolbar/Toolbar';
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
import { useViewportHover } from '../hooks';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const [state] = useViewportActionCorners();
function OHIFViewportActionCornersComponent({ viewportId }: OHIFViewportActionCornersProps) {
// Use the viewport hover hook to track if viewport is hovered or active
const { isHovered, isActive } = useViewportHover(viewportId);
if (!state.viewports[viewportId]) {
const shouldShowCorners = isHovered || isActive;
if (!shouldShowCorners) {
return null;
}
const components = state.viewports[viewportId];
const renderCorner = (location: ViewportActionCornersLocations, CornerComponent) => {
const cornerComponents = components[location];
if (!cornerComponents?.length) {
return null;
}
return (
<CornerComponent>
{cornerComponents
.filter(componentInfo => componentInfo.isVisible !== false)
.map(componentInfo => (
<div
key={componentInfo.id}
className={
componentInfo.isLocked === true ? 'pointer-events-none opacity-50' : undefined
}
>
{componentInfo.component}
</div>
))}
</CornerComponent>
);
};
return (
<ViewportActionCorners.Container>
{renderCorner(ViewportActionCornersLocations.topLeft, ViewportActionCorners.TopLeft)}
{renderCorner(ViewportActionCornersLocations.topRight, ViewportActionCorners.TopRight)}
{renderCorner(ViewportActionCornersLocations.bottomLeft, ViewportActionCorners.BottomLeft)}
{renderCorner(ViewportActionCornersLocations.bottomRight, ViewportActionCorners.BottomRight)}
</ViewportActionCorners.Container>
<IconPresentationProvider
size="medium"
IconContainer={ToolButton}
containerProps={{
size: 'tiny',
className: 'font-normal text-primary hover:bg-primary/25',
}}
>
<ViewportActionCorners.Container>
<ViewportActionCorners.TopLeft>
<Toolbar
buttonSection="viewportActionMenu.topLeft"
viewportId={viewportId}
location={ButtonLocation.TopLeft}
/>
</ViewportActionCorners.TopLeft>
<ViewportActionCorners.TopMiddle>
<Toolbar
buttonSection="viewportActionMenu.topMiddle"
viewportId={viewportId}
location={ButtonLocation.TopMiddle}
/>
</ViewportActionCorners.TopMiddle>
<ViewportActionCorners.TopRight>
<Toolbar
buttonSection="viewportActionMenu.topRight"
viewportId={viewportId}
location={ButtonLocation.TopRight}
/>
</ViewportActionCorners.TopRight>
<ViewportActionCorners.LeftMiddle>
<Toolbar
buttonSection="viewportActionMenu.leftMiddle"
viewportId={viewportId}
location={ButtonLocation.LeftMiddle}
/>
</ViewportActionCorners.LeftMiddle>
<ViewportActionCorners.RightMiddle>
<Toolbar
buttonSection="viewportActionMenu.rightMiddle"
viewportId={viewportId}
location={ButtonLocation.RightMiddle}
/>
</ViewportActionCorners.RightMiddle>
<ViewportActionCorners.BottomLeft>
<Toolbar
buttonSection="viewportActionMenu.bottomLeft"
viewportId={viewportId}
location={ButtonLocation.BottomLeft}
/>
</ViewportActionCorners.BottomLeft>
<ViewportActionCorners.BottomMiddle>
<Toolbar
buttonSection="viewportActionMenu.bottomMiddle"
viewportId={viewportId}
location={ButtonLocation.BottomMiddle}
/>
</ViewportActionCorners.BottomMiddle>
<ViewportActionCorners.BottomRight>
<Toolbar
buttonSection="viewportActionMenu.bottomRight"
viewportId={viewportId}
location={ButtonLocation.BottomRight}
/>
</ViewportActionCorners.BottomRight>
</ViewportActionCorners.Container>
</IconPresentationProvider>
);
}
const OHIFViewportActionCorners = memo(OHIFViewportActionCornersComponent);
export default OHIFViewportActionCorners;

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Numeric } from '@ohif/ui-next';
import { useViewportDisplaySets, useViewportRendering } from '../../hooks';
interface OpacityMenuProps {
viewportId: string;
className?: string;
}
function OpacityMenu({ viewportId, className }: OpacityMenuProps) {
const { backgroundDisplaySet, foregroundDisplaySets } = useViewportDisplaySets(viewportId, {
includeForeground: true,
includeBackground: true,
});
const { opacityLinear, setOpacityLinear } = useViewportRendering(viewportId, {
displaySetInstanceUID: foregroundDisplaySets[0].displaySetInstanceUID,
});
const opacityValue = opacityLinear !== undefined ? opacityLinear : 0;
const backgroundModality = backgroundDisplaySet?.Modality || '';
const foregroundModality = foregroundDisplaySets[0]?.Modality || '';
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-3">
<div className="mb-2 flex items-center justify-center">
<span className="text-muted-foreground text-base">Opacity</span>
</div>
<div className="">
<Numeric.Container
mode="singleRange"
value={opacityValue}
onChange={(val: number | [number, number]) => {
if (typeof val === 'number') {
setOpacityLinear(val);
}
}}
min={0}
max={1}
step={0.0001}
>
<div className="flex items-center">
<span className="text-foreground mr-2 text-sm">{backgroundModality}</span>
<Numeric.SingleRange showNumberInput={false} />
<span className="text-foreground ml-2 text-sm">{foregroundModality}</span>
</div>
</Numeric.Container>
</div>
</div>
</div>
);
}
export default OpacityMenu;

View File

@ -0,0 +1,85 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import OpacityMenu from './OpacityMenu';
type OpacityMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function OpacityMenuWrapper(props: OpacityMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.Opacity className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="Opacity"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<OpacityMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,2 @@
export { OpacityMenuWrapper } from './OpacityMenuWrapper';
export { default as OpacityMenu } from './OpacityMenu';

View File

@ -0,0 +1,21 @@
import React from 'react';
// should be used in a Select component
const SelectItemWithModality = ({
displaySet,
showModality = true,
}: {
displaySet: AppTypes.DisplaySet;
showModality?: boolean;
}): JSX.Element => (
<div className="flex w-[90%] items-center justify-between">
<span className="text-foreground truncate text-base">{displaySet.label}</span>
{showModality && displaySet.Modality && (
<span className="text-muted-foreground flex-shrink-0 whitespace-nowrap text-xs">
{displaySet.Modality}
</span>
)}
</div>
);
export default SelectItemWithModality;

View File

@ -0,0 +1,104 @@
import React, { useState, useEffect } from 'react';
import {
Numeric,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Button,
} from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import SelectItemWithModality from '../SelectItemWithModality';
interface ThresholdMenuProps {
viewportId: string;
className?: string;
}
function ThresholdMenu({ viewportId, className }: ThresholdMenuProps) {
const { viewportDisplaySets, foregroundDisplaySets } = useViewportDisplaySets(viewportId);
const [selectedDisplaySetUID, setSelectedDisplaySetUID] = useState<string | undefined>(
foregroundDisplaySets.length > 0 ? foregroundDisplaySets[0].displaySetInstanceUID : undefined
);
const { threshold, setThreshold, pixelValueRange } = useViewportRendering(viewportId, {
displaySetInstanceUID: selectedDisplaySetUID,
});
const { min, max } = pixelValueRange;
const thresholdValue = threshold ?? min;
useEffect(() => {
if (foregroundDisplaySets.length > 0 && !selectedDisplaySetUID) {
setSelectedDisplaySetUID(foregroundDisplaySets[0].displaySetInstanceUID);
}
}, [foregroundDisplaySets, selectedDisplaySetUID]);
const selectedDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetUID
);
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-base">Threshold</span>
{viewportDisplaySets.length > 1 && (
<div className="w-32">
<Select
value={selectedDisplaySetUID}
onValueChange={setSelectedDisplaySetUID}
>
<SelectTrigger>
<SelectValue>{selectedDisplaySet?.label || 'Select Display Set'}</SelectValue>
</SelectTrigger>
<SelectContent>
{viewportDisplaySets.map(ds => (
<SelectItem
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
<SelectItemWithModality displaySet={ds} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => setThreshold(min)}
className="text-sm"
>
Reset
</Button>
</div>
<Numeric.Container
mode="singleRange"
value={thresholdValue}
onChange={(val: number | [number, number]) => {
if (typeof val === 'number') {
setThreshold(val);
}
}}
min={min}
max={max}
step={0.01}
>
<Numeric.SingleRange />
<div className="mt-1 flex justify-between">
<span className="text-muted-foreground text-sm">{min.toFixed(0)}</span>
<span className="text-muted-foreground text-sm">{max.toFixed(0)}</span>
</div>
</Numeric.Container>
</div>
</div>
);
}
export default ThresholdMenu;

View File

@ -0,0 +1,85 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import ThresholdMenu from './ThresholdMenu';
type ThresholdMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function ThresholdMenuWrapper(props: ThresholdMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.Threshold className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="Threshold"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<ThresholdMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,2 @@
export { ThresholdMenuWrapper } from './ThresholdMenuWrapper';
export { default as ThresholdMenu } from './ThresholdMenu';

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
/**
* TrackingStatus displays an indicator icon for viewports that have
* tracked measurements
*/
function TrackingStatus({ viewportId }: { viewportId: string }) {
const { isTracked } = useMeasurementTracking({ viewportId });
if (!isTracked) {
return null;
}
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Icons.StatusTracking className="h-4 w-4" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<div>Tracking</div>
</TooltipContent>
</Tooltip>
</>
);
}
export default TrackingStatus;

View File

@ -0,0 +1 @@
export { default } from './TrackingStatus';

View File

@ -0,0 +1,190 @@
import React, { useState, useEffect } from 'react';
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
Numeric,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from '@ohif/ui-next';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { useSystem } from '@ohif/core';
import { cache } from '@cornerstonejs/core';
interface VOIManualControlMenuProps {
viewportId: string;
className?: string;
}
const TABS = {
MINMAX: 'minmax',
MANUAL: 'manual',
};
function VOIManualControlMenu({ viewportId, className }: VOIManualControlMenuProps) {
const { servicesManager } = useSystem();
const { displaySetService } = servicesManager.services;
const [activeTab, setActiveTab] = useState(TABS.MINMAX);
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const [selectedDisplaySetUID, setSelectedDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets.length > 0 ? viewportDisplaySets[0].displaySetInstanceUID : undefined
);
const { voiRange, setVOIRange, windowLevel, setWindowLevel } = useViewportRendering(viewportId, {
displaySetInstanceUID: selectedDisplaySetUID,
});
useEffect(() => {
if (viewportDisplaySets.length > 0 && !selectedDisplaySetUID) {
setSelectedDisplaySetUID(viewportDisplaySets[0].displaySetInstanceUID);
}
}, [viewportDisplaySets, selectedDisplaySetUID]);
if (!voiRange) {
return null;
}
const { upper, lower } = voiRange;
const { windowWidth, windowCenter } = windowLevel;
const selectedViewportImageIds =
displaySetService.getDisplaySetByUID(selectedDisplaySetUID)?.imageIds;
let min = Infinity;
let max = -Infinity;
if (selectedViewportImageIds) {
for (const imageId of selectedViewportImageIds) {
const image = cache.getImage(imageId);
if (image) {
min = Math.min(min, image.minPixelValue);
max = Math.max(max, image.maxPixelValue);
}
}
}
// Provide reasonable defaults if min/max couldn't be determined
if (min === Infinity || max === -Infinity) {
min = 0;
max = 255;
}
const selectedDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetUID
);
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-4 shadow-md">
<Tabs
defaultValue={activeTab}
onValueChange={setActiveTab}
>
<div className="mb-4 flex items-center space-x-2">
{viewportDisplaySets.length > 1 && (
<Select
value={selectedDisplaySetUID}
onValueChange={setSelectedDisplaySetUID}
>
<SelectTrigger>
<SelectValue>{selectedDisplaySet?.label || 'Select Display Set'}</SelectValue>
</SelectTrigger>
<SelectContent>
{viewportDisplaySets.map(ds => (
<SelectItem
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{`${ds.SeriesDescription || ''}`.trim()}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<TabsList className="w-full flex-1">
<TabsTrigger
value={TABS.MINMAX}
className="flex-1"
>
Min/Max
</TabsTrigger>
<TabsTrigger
value={TABS.MANUAL}
className="flex-1"
>
Manual
</TabsTrigger>
</TabsList>
</div>
<TabsContent value={TABS.MINMAX}>
<Numeric.Container
mode="doubleRange"
min={min}
max={max}
values={[lower, upper]}
step={1}
className="space-y-1"
onChange={(vals: [number, number]) => {
const [newLower, newUpper] = vals;
if (newLower !== lower || newUpper !== upper) {
setVOIRange({ lower: newLower, upper: newUpper });
}
}}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</TabsContent>
<TabsContent value={TABS.MANUAL}>
<div className="space-y-1">
<Numeric.Container
mode="singleRange"
min={0}
max={max}
step={1}
value={windowWidth}
className="space-y-1"
onChange={(val: number) => {
if (val !== windowWidth) {
setWindowLevel({ windowWidth: val, windowCenter });
}
}}
>
<div className="flex items-center space-x-2">
<Numeric.Label>W:</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</div>
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={min}
max={max}
step={1}
value={windowCenter}
className="space-y-1"
onChange={(val: number) => {
if (val !== windowCenter) {
setWindowLevel({ windowWidth, windowCenter: val });
}
}}
>
<div className="flex items-center space-x-2">
<Numeric.Label>L:</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</div>
</Numeric.Container>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
}
export default VOIManualControlMenu;

View File

@ -0,0 +1,85 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import VOIManualControlMenu from './VOIManualControlMenu';
type VOIManualControlMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function VOIManualControlMenuWrapper(props: VOIManualControlMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.WindowLevelAdvanced className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="WindowLevelAdvanced"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<VOIManualControlMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,2 @@
export { VOIManualControlMenuWrapper } from './VOIManualControlMenuWrapper';
export { default as VOIManualControlMenu } from './VOIManualControlMenu';

View File

@ -1,117 +0,0 @@
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import ViewportColorbar from './ViewportColorbar';
import {
ColorbarCustomization,
ColorbarPositionType,
TickPositionType,
} from '../../types/Colorbar';
import type { ColorMapPreset } from '../../types/Colormap';
type ColorbarData = {
colorbar: {
activeColormapName: string;
colormaps: ColorMapPreset[];
volumeId?: string;
};
displaySetInstanceUID: string;
};
type AdvancedColorbarWithControlsProps = {
viewportId: string;
colorbars: ColorbarData[];
position: string;
tickPosition: string;
colorbarCustomization: ColorbarCustomization;
onClose: (displaySetInstanceUID?: string) => void;
viewportElementRef?: React.RefObject<HTMLDivElement>;
};
/**
* AdvancedColorbarWithControls Component
* A specialized colorbar component with additional control buttons for advanced window level controls
* This component handles the rendering portion of the ViewportColorbarsContainer
*/
const AdvancedColorbarWithControls = ({
viewportId,
colorbars,
position,
tickPosition,
colorbarCustomization,
onClose,
viewportElementRef,
}: AdvancedColorbarWithControlsProps) => {
const handleClose = (displaySetInstanceUID?: string) => {
onClose(displaySetInstanceUID);
};
// Get bottom position styles from customization
const positionStyles = colorbarCustomization?.positionStyles || {};
const bottomPositionStyles = positionStyles.bottom || {};
const heightStyle = bottomPositionStyles.height;
return (
<div className="mx-auto flex h-[20px] w-1/2 flex-row items-center justify-between">
<div className="flex flex-shrink-0 flex-row">
<Button
size="icon"
variant="ghost"
onClick={() => handleClose()}
className="flex-shrink-0 p-0"
>
<Icons.Close />
</Button>
<Button
size="icon"
variant="ghost"
className="flex-shrink-0 p-0"
>
<Icons.ToolZoom />
</Button>
</div>
<div
className="mx-2"
style={{ width: 'calc(75% - 16px)', height: heightStyle }}
>
{colorbars.map((colorbarInfo, index) => {
const { colorbar, displaySetInstanceUID } = colorbarInfo;
return (
<ViewportColorbar
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
viewportId={viewportId}
viewportElementRef={viewportElementRef}
displaySetInstanceUID={displaySetInstanceUID}
colormaps={colorbar.colormaps}
activeColormapName={colorbar.activeColormapName}
volumeId={colorbar.volumeId}
position={position as ColorbarPositionType}
tickPosition={tickPosition as TickPositionType}
tickStyles={colorbarCustomization?.tickStyles}
/>
);
})}
</div>
<div className="flex flex-shrink-0 flex-row">
<Button
size="icon"
variant="ghost"
className="flex-shrink-0 p-0"
>
<Icons.Redo />
</Button>
<Button
size="icon"
variant="ghost"
className="flex-shrink-0 p-0"
>
<Icons.Pencil />
</Button>
</div>
</div>
);
};
export default AdvancedColorbarWithControls;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useMemo, memo } from 'react';
import { utilities } from '@cornerstonejs/tools';
import { useSystem } from '@ohif/core';
import { useSystem, useViewportRef, useViewportSize } from '@ohif/core';
import {
ColorbarPositionType,
TickPositionType,
@ -24,6 +24,7 @@ type ColorbarProps = {
tickStyles?: TickStyleType;
containerStyles?: ContainerStyleType;
viewportElementRef?: React.RefObject<HTMLDivElement>;
numColorbars: number;
};
/**
@ -31,7 +32,7 @@ type ColorbarProps = {
* A React wrapper for the cornerstone ViewportColorbar that adds a close button
* positioned appropriately based on the colorbar position.
*/
const ViewportColorbar = ({
const ViewportColorbar = memo(function ViewportColorbar({
viewportId,
displaySetInstanceUID,
colormaps,
@ -40,11 +41,50 @@ const ViewportColorbar = ({
position,
tickPosition,
tickStyles,
viewportElementRef,
}: ColorbarProps) => {
numColorbars,
}: ColorbarProps) {
const containerRef = useRef<HTMLDivElement>(null);
const { servicesManager } = useSystem();
const { customizationService } = servicesManager.services;
const viewportElementRef = useViewportRef(viewportId);
const { height, width } = useViewportSize(viewportId);
// Memoize colorbar customization to prevent rerenders from unrelated customization changes
const colorbarCustomization = useMemo(() => {
return customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
}, [customizationService]);
const appropriateTickPosition = useMemo(() => {
let tickPos = tickPosition;
if (position === 'left' || position === 'right') {
tickPos = position === 'left' ? 'right' : 'left';
} else {
tickPos = position === 'top' ? 'bottom' : 'top';
}
return tickPos;
}, [position, tickPosition]);
const positionTickStyles = useMemo(() => {
return colorbarCustomization?.positionTickStyles?.[position];
}, [colorbarCustomization, position]);
const positionStylesFromConfig = useMemo(() => {
return colorbarCustomization?.positionStyles?.[position] || {};
}, [colorbarCustomization, position]);
const mergedTickStyles = useMemo(() => {
return {
...(colorbarCustomization?.tickStyles || {}),
...(positionTickStyles?.style || {}),
...(tickStyles || {}),
};
}, [colorbarCustomization, positionTickStyles, tickStyles]);
const colorbarId = useMemo(() => {
return `Colorbar-${viewportId}-${displaySetInstanceUID}`;
}, [viewportId, displaySetInstanceUID]);
useEffect(() => {
if (!containerRef.current || !colormaps || !activeColormapName) {
@ -53,29 +93,21 @@ const ViewportColorbar = ({
const viewportElement = viewportElementRef?.current;
if (!viewportElement) {
if (!viewportElement || !colormaps?.length) {
return;
}
const colorbarCustomization = customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
const positionTickStyles = colorbarCustomization?.positionTickStyles?.[position];
// Using stable references from memoized values
const csColorbar = new CornerstoneViewportColorbar({
id: `Colorbar-${viewportId}-${displaySetInstanceUID}`,
id: colorbarId,
element: viewportElement,
container: containerRef.current,
colormaps: colormaps,
activeColormapName: activeColormapName,
volumeId,
ticks: {
position: tickPosition as ColorbarRangeTextPosition,
style: {
...(colorbarCustomization?.tickStyles || {}),
...(positionTickStyles?.style || {}),
...(tickStyles || {}),
},
position: appropriateTickPosition as ColorbarRangeTextPosition,
style: mergedTickStyles,
},
});
@ -90,37 +122,33 @@ const ViewportColorbar = ({
colormaps,
activeColormapName,
volumeId,
position,
tickPosition,
tickStyles,
colorbarId,
appropriateTickPosition,
mergedTickStyles,
viewportElementRef,
customizationService,
]);
// Get position styles from customization service
const colorbarCustomization = customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
const positionStylesFromConfig = colorbarCustomization?.positionStyles?.[position] || {};
if (!height || !width) {
return null;
}
return (
<div
id={`colorbar-container-${viewportId}-${displaySetInstanceUID}`}
ref={containerRef}
style={{
width: position === 'bottom' ? '100%' : '20px',
height: position === 'bottom' ? '20px' : '100%',
position: 'relative',
zIndex: 1000,
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
pointerEvents: 'auto',
minWidth: position === 'bottom' ? width / 2.5 : '17px',
minHeight: position === 'bottom' ? '20px' : numColorbars === 1 ? height / 3 : height / 4,
...positionStylesFromConfig,
}}
></div>
);
};
});
export default ViewportColorbar;

View File

@ -1,14 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo, memo, useCallback } from 'react';
import { useSystem } from '@ohif/core';
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
import { ColorbarCustomization } from '../../types/Colorbar';
import type { ColorMapPreset } from '../../types/Colormap';
import ViewportColorbar from './ViewportColorbar';
import { deepMerge } from '@cornerstonejs/core/utilities';
import useViewportRendering from '../../hooks/useViewportRendering';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
type ViewportColorbarsContainerProps = {
viewportId: string;
viewportElementRef?: React.RefObject<HTMLDivElement>;
location: number;
};
type ColorbarData = {
@ -24,18 +23,38 @@ type ColorbarData = {
* Container component that manages multiple colorbars for a viewport
* It interacts with the colorbarService to get/set colorbar states
*/
const ViewportColorbarsContainer = ({
const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({
viewportId,
viewportElementRef,
}: ViewportColorbarsContainerProps) => {
location,
}: ViewportColorbarsContainerProps) {
const [colorbars, setColorbars] = useState<ColorbarData[]>([]);
const { servicesManager } = useSystem();
const { colorbarService, customizationService } = servicesManager.services;
const { colorbarService, customizationService, displaySetService } = servicesManager.services;
const { viewportDisplaySets, backgroundDisplaySet, foregroundDisplaySets } =
useViewportDisplaySets(viewportId);
const { colorbarPosition: position, opacity } = useViewportRendering(viewportId, {
location,
});
// Memoize the customization to prevent recomputation
const colorbarCustomization = useMemo(() => {
return customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
}, [customizationService]);
// Memoize tick position
const tickPosition = useMemo(() => {
const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
return colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
}, [colorbarCustomization]);
// Initial load of colorbars
useEffect(() => {
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
}, [viewportId, colorbarService]);
// Subscribe to colorbar state changes
useEffect(() => {
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
@ -51,90 +70,59 @@ const ViewportColorbarsContainer = ({
};
}, [viewportId, colorbarService]);
const handleClose = (displaySetInstanceUID?: string): void => {
if (displaySetInstanceUID) {
colorbarService.removeColorbar(viewportId, displaySetInstanceUID);
} else {
colorbarService.removeColorbar(viewportId);
}
};
if (!colorbars.length) {
return null;
}
const colorbarCustomization = customizationService.getCustomization(
'cornerstone.colorbar'
) as unknown as ColorbarCustomization;
const isBottom = position === 'bottom';
const defaultPosition = colorbarCustomization?.colorbarContainerPosition;
const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
const isSingleViewport = viewportDisplaySets.length === 1;
const showFullList = isSingleViewport || !isBottom;
const position = colorbarCustomization?.colorbarContainerPosition || defaultPosition;
const tickPosition = colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
const positionStyles = colorbarCustomization?.positionStyles;
const colorbarsToUse = showFullList
? colorbars
: colorbars.filter(({ displaySetInstanceUID }) => {
const { displaySetInstanceUID: dsUID } =
displaySetService.getDisplaySetByUID(displaySetInstanceUID) ?? {};
const positionStyle = positionStyles?.[position];
const targetUID =
opacity === 0 || opacity == null
? backgroundDisplaySet?.displaySetInstanceUID
: foregroundDisplaySets[0].displaySetInstanceUID;
const defaultPositionStyle =
position !== 'bottom'
? {
width: '20px',
height: colorbars.length === 1 ? '55%' : '75%',
top: '50%',
transform: 'translateY(-50%)',
}
: {
width: '100%',
height: '20px',
};
const finalPositionStyle = deepMerge(defaultPositionStyle, positionStyle);
return dsUID === targetUID;
});
return (
<div
className="absolute"
style={{
...finalPositionStyle,
pointerEvents: 'auto',
}}
>
{position !== 'bottom' ? (
<div
className="flex h-full flex-col items-center justify-center"
style={{ pointerEvents: 'auto' }}
>
{colorbars.map((colorbarInfo, index) => {
const { colorbar, displaySetInstanceUID } = colorbarInfo;
return (
<ViewportColorbar
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
viewportId={viewportId}
displaySetInstanceUID={displaySetInstanceUID}
colormaps={colorbar.colormaps}
activeColormapName={colorbar.activeColormapName}
volumeId={colorbar.volumeId}
viewportElementRef={viewportElementRef}
position={position}
tickPosition={tickPosition}
tickStyles={colorbarCustomization?.tickStyles}
/>
);
})}
</div>
) : (
<AdvancedColorbarWithControls
viewportId={viewportId}
colorbars={colorbars}
position={position}
tickPosition={defaultTickPosition}
colorbarCustomization={colorbarCustomization}
onClose={handleClose}
viewportElementRef={viewportElementRef}
/>
)}
<div
className="flex h-full flex-col items-center justify-center"
style={{ pointerEvents: 'auto' }}
>
{colorbarsToUse.map(colorbarInfo => {
const { colorbar, displaySetInstanceUID } = colorbarInfo;
return (
<ViewportColorbar
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
viewportId={viewportId}
displaySetInstanceUID={displaySetInstanceUID}
colormaps={colorbar.colormaps}
activeColormapName={colorbar.activeColormapName}
volumeId={colorbar.volumeId}
position={position}
tickPosition={tickPosition}
tickStyles={colorbarCustomization?.tickStyles}
numColorbars={colorbars.length}
/>
);
})}
</div>
</div>
);
};
});
export default ViewportColorbarsContainer;

View File

@ -1,6 +1,5 @@
import ViewportColorbar from './ViewportColorbar';
import ViewportColorbarsContainer from './ViewportColorbarsContainer';
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
export { ViewportColorbar, ViewportColorbarsContainer, AdvancedColorbarWithControls };
export { ViewportColorbar, ViewportColorbarsContainer };
export default ViewportColorbarsContainer;

View File

@ -16,14 +16,16 @@ import {
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import SelectItemWithModality from '../SelectItemWithModality';
import { useViewportRendering } from '../../hooks';
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
const { commandsManager, servicesManager } = useSystem();
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
const [pendingSegmentations, setPendingSegmentations] = useState<string[]>([]);
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
const { toggleColorbar } = useViewportRendering(viewportId);
const { hangingProtocolService } = servicesManager.services;
const { hangingProtocolService, toolbarService } = servicesManager.services;
const {
backgroundDisplaySet,
@ -34,6 +36,8 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
foregroundDisplaySets,
} = useViewportDisplaySets(viewportId);
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
/**
* Change the background display set
*/
@ -48,21 +52,6 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
});
};
/**
* 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
*/
@ -182,6 +171,18 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
}
};
// Check if the advanced window level components exist in toolbar
const hasAdvancedRenderingControls = !!toolbarService.getButton('advancedRenderingControls');
const hasOpacityMenu = !!toolbarService.getButton('opacityMenu');
const handleThresholdOpacityToggle = () => {
const newValue = !thresholdOpacityEnabled;
if (hasAdvancedRenderingControls) {
toggleColorbar();
}
setThresholdOpacityEnabled(newValue);
};
return (
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
{/* Top buttons row */}
@ -232,15 +233,17 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
{displaySet.label}
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
{potentialOverlayDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
{item.label}
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
@ -284,8 +287,9 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
{item.label}
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
@ -334,15 +338,17 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
{displaySet.label}
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
{potentialForegroundDisplaySets.map(item => (
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
{item.label}
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
@ -386,8 +392,9 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectItem
key={item.displaySetInstanceUID}
value={item.displaySetInstanceUID}
className="pr-2"
>
{item.label}
<SelectItemWithModality displaySet={item} />
</SelectItem>
))}
</SelectContent>
@ -420,7 +427,7 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<div className="mt-1 mb-1 flex items-center px-1">
<Icons.LayerBackground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
<Select
value={backgroundDisplaySet.displaySetInstanceUID}
value={backgroundDisplaySet?.displaySetInstanceUID}
onValueChange={value => {
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
ds => ds.displaySetInstanceUID === value
@ -433,8 +440,8 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectTrigger className="flex-1">
<SelectValue>
{(
backgroundDisplaySet.SeriesDescription ||
backgroundDisplaySet.label ||
backgroundDisplaySet?.SeriesDescription ||
backgroundDisplaySet?.label ||
'background'
).toUpperCase()}
</SelectValue>
@ -444,15 +451,16 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
<SelectItem
key={displaySet.displaySetInstanceUID}
value={displaySet.displaySetInstanceUID}
className="pr-2"
>
{displaySet.label}
<SelectItemWithModality displaySet={displaySet} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* {foregroundDisplaySets.length > 0 && (
{foregroundDisplaySets.length > 0 && (hasAdvancedRenderingControls || hasOpacityMenu) && (
<div className="mt-1 ml-7">
<div className="flex items-center">
<Switch
@ -470,7 +478,7 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
</label>
</div>
</div>
)} */}
)}
</div>
);
}

View File

@ -1,73 +1,74 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
cn,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
useViewportActionCorners,
useIconPresentation,
} from '@ohif/ui-next';
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
import classNames from 'classnames';
import { MENU_IDS } from '../menus/menu-ids';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
export function ViewportDataOverlayMenuWrapper({
viewportId,
displaySets,
location,
}: withAppTypes<{
type DataOverlayMenuProps = {
viewportId: string;
element: HTMLElement;
location: string;
}>): ReactNode {
const [viewportGrid] = useViewportGrid();
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
const isMenuOpen =
actionCornerState.viewports[viewportId]?.[location]?.find(
item => item.id === MENU_IDS.DATA_OVERLAY_MENU
)?.isOpen ?? false;
export function ViewportDataOverlayMenuWrapper(props: DataOverlayMenuProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportId);
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
onOpen?.();
} else {
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
onClose?.();
}
};
// Get proper alignment and side based on the location
let align = 'center';
let side = 'bottom';
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
if (location !== undefined) {
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
align = positioning.align;
side = positioning.side;
}
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.ViewportViews className={iconClassName} />;
return (
<Popover
open={isMenuOpen}
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
className={cn(
viewportGrid.activeViewportId === viewportId
? 'visible'
: 'invisible group-hover/pane:visible'
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="ViewportViews"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
>
<Icons.ViewportViews className={classNames('text-highlight')} />
</Button>
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"

View File

@ -1,12 +0,0 @@
import React, { ReactNode } from 'react';
import { ViewportDataOverlayMenuWrapper } from './ViewportDataOverlayMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
}>
): ReactNode {
return <ViewportDataOverlayMenuWrapper {...props} />;
}

View File

@ -42,6 +42,14 @@ export function getModalityOverlayColormap(customizationService, modality) {
export function getEnhancedDisplaySets({ viewportId, services }) {
const { displaySetService, viewportGridService } = services;
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetsUIDs?.length) {
return {
viewportDisplaySets: [],
enhancedDisplaySets: [],
};
}
const allDisplaySets = displaySetService.getActiveDisplaySets();
const otherDisplaySets = allDisplaySets.filter(
@ -52,7 +60,7 @@ export function getEnhancedDisplaySets({ viewportId, services }) {
displaySetService.getDisplaySetByUID(displaySetUID)
);
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds);
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds || []);
const backgroundDisplaySet = viewportDisplaySets[0];
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {

View File

@ -1,40 +1,37 @@
import React from 'react';
import { Icons, useViewportActionCorners, useViewportGrid } from '@ohif/ui-next';
import { cn, Icons, ToolButton, useIconPresentation } 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';
import { MENU_IDS } from '../menus/menu-ids';
import { Popover, PopoverTrigger, PopoverContent, Button, useViewportGrid } from '@ohif/ui-next';
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
function ViewportOrientationMenu({
location,
viewportId,
displaySets,
isOpen = false,
onOpen,
onClose,
disabled,
...props
}: withAppTypes<{
location?: string;
viewportId: string;
displaySets: AppTypes.DisplaySet[];
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
}>) {
const [gridState] = useViewportGrid();
const viewportIdToUse = viewportId || gridState.activeViewportId;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const { servicesManager, commandsManager } = useSystem();
const [viewportGridState, viewportGridService] = useViewportGrid();
const [actionCornerState, viewportActionCornersServiceAPI] = useViewportActionCorners();
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
const viewportId = viewportGridState.activeViewportId;
const isMenuOpen =
actionCornerState.viewports[viewportId]?.[location]?.find(
item => item.id === MENU_IDS.ORIENTATION_MENU
)?.isOpen ?? false;
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const handleOrientationChange = (orientation: string) => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportIdToUse);
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;
}
@ -63,11 +60,13 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
orientationEnum = Enums.OrientationAxis.ACQUISITION;
}
const displaySetUIDs = displaySets.map(ds => ds.displaySetInstanceUID);
// 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,
viewportId: viewportIdToUse,
displaySetInstanceUIDs: displaySetUIDs,
viewportOptions: {
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
@ -83,78 +82,99 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
} else {
// Set the viewport orientation
commandsManager.runCommand('setViewportOrientation', {
viewportId,
viewportId: viewportIdToUse,
orientation: orientationEnum,
});
}
// Close the menu after selection
viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
onClose?.();
};
const handleOpenChange = (openState: boolean) => {
if (openState) {
viewportActionCornersServiceAPI.openItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
onOpen?.();
} else {
viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
onClose?.();
}
};
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 = viewportActionCornersServiceAPI.getAlignAndSide(location);
align = positioning.align;
side = positioning.side;
}
// Get proper alignment and side based on the location using toolbar service
const { align, side } = toolbarService.getAlignAndSide(Number(location));
const Icon = <Icons.OrientationSwitch className={iconClassName} />;
return (
<DropdownMenu
open={isMenuOpen}
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-highlight"
disabled={!hasReconstructableDisplaySet}
>
<Icons.OrientationSwitch />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-[160px]"
<PopoverTrigger
asChild
className={cn('flex items-center justify-center')}
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="OrientationSwitch"
{...props}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
onClick={() => {}}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="w-[100px] p-1"
align={align}
side={side}
sideOffset={5}
>
<DropdownMenuLabel className="-ml-1">Orientation</DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleOrientationChange('axial')}>Axial</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleOrientationChange('sagittal')}>
Sagittal
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleOrientationChange('coronal')}>
Coronal
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleOrientationChange('acquisition')}>
Acquisition
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex flex-col">
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('axial')}
>
Axial
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('sagittal')}
>
Sagittal
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('coronal')}
>
Coronal
</Button>
<Button
variant="ghost"
className="justify-start"
onClick={() => handleOrientationChange('acquisition')}
>
Acquisition
</Button>
</div>
</PopoverContent>
</Popover>
);
}

View File

@ -1,27 +1,29 @@
import React, { ReactNode } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
import { cn } from '@ohif/ui-next';
import ViewportOrientationMenu from './ViewportOrientationMenu';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
export function ViewportOrientationMenuWrapper({
viewportId,
location,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
}>): ReactNode {
const [viewportGrid] = useViewportGrid();
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
export function ViewportOrientationMenuWrapper(
props: withAppTypes<{
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
iconSize?: number;
disabled?: boolean;
}>
): ReactNode {
const { viewportId } = props;
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
if (!viewportDisplaySets.length) {
return null;
}
return (
<div
className={cn(
'flex justify-end',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
>
<ViewportOrientationMenu location={location} />
</div>
<ViewportOrientationMenu
{...props}
displaySets={viewportDisplaySets}
/>
);
}

View File

@ -1,14 +0,0 @@
import React, { ReactNode } from 'react';
import { ViewportOrientationMenuWrapper } from './ViewportOrientationMenuWrapper';
export function getViewportOrientationMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
}>
): ReactNode {
return <ViewportOrientationMenuWrapper {...props} />;
}
export * from './ViewportOrientationMenu';

View File

@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from '
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
@ -22,7 +22,7 @@ const ViewportWindowLevel = ({
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager });
const displaySets = useActiveViewportDisplaySets();
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
@ -30,7 +30,7 @@ const ViewportWindowLevel = ({
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : [];
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@ -124,7 +124,8 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewportVolumeIds =
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : [];
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {

View File

@ -1,103 +1,27 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { ReactElement, useCallback } from 'react';
import { Switch } from '@ohif/ui-next';
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function setViewportColorbar(
viewportId: string,
commandsManager: any,
servicesManager: AppTypes.ServicesManager,
colorbarOptions: Partial<ColorbarOptions>
) {
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '13px Inter',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
commandsManager.run('toggleViewportColorbar', {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
});
}
export function Colorbar({
viewportId,
colorbarProperties,
}: withAppTypes<ColorbarProps>): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { colorbarService } = servicesManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, commandsManager, servicesManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
const handleToggle = useCallback(() => {
toggleColorbar();
}, [toggleColorbar]);
return (
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
>
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded">
<div className="flex w-7 flex-shrink-0 items-center justify-center"></div>
<span className="flex-grow">Display Color bar</span>
<span
className="flex-grow"
onClick={handleToggle}
>
Display Color bar
</span>
<Switch
className="ml-2 flex-shrink-0"
checked={showColorbar}
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
checked={!!hasColorbar}
onCheckedChange={handleToggle}
/>
</div>
);

View File

@ -1,91 +1,66 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
import { useSystem } from '@ohif/core';
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService } = servicesManager.services;
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
);
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
// Use the hook with the active display set
const { colorbarProperties, setColormap } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const { colormaps } = colorbarProperties;
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const [currentColormap, setCurrentColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const activeDisplaySetRef = useRef(activeDisplaySet);
activeDisplaySetRef.current = activeDisplaySet;
const onSetColorLUT = useCallback(
props => {
// TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
...props,
opacity,
immediate: true,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
const getViewportColormap = (viewportId, displaySet) => {
const { displaySetInstanceUID } = displaySet;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(displaySetInstanceUID)
);
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
};
const activeIndex = displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
);
const currentColormapRef = useRef(currentColormap);
currentColormapRef.current = currentColormap;
useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
setCurrentColormap(null);
setPrePreviewColormap(null);
}, [activeDisplaySetUID]);
const handleSetColorLUT = (colormap, immediate = true) => {
// Check if it's a fusion viewport
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
viewportDisplaySets.length > 1 && !oneOpacityColormaps.includes(colormap.name) ? 0.5 : 1;
setColormap({
colormap,
opacity,
immediate,
});
};
return (
<>
{displaySets.length > 1 && (
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={String(activeIndex)}
onValueChange={val => {
const index = parseInt(val, 10);
setActiveDisplaySet(displaySets[index]);
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
setPrePreviewColormap(null);
}}
>
<TabsList>
{displaySets.map((ds, i) => (
{viewportDisplaySets.map(ds => (
<TabsTrigger
key={i}
value={String(i)}
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{ds.Modality}
</TabsTrigger>
@ -94,6 +69,7 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
@ -104,49 +80,47 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
if (!checked && currentColormapRef.current) {
handleSetColorLUT(currentColormapRef.current);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel
maxHeight="calc(100vh - 250px)"
className="min-h-[200px] flex-grow"
>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
onSetColorLUT({
viewportId,
colormap: prePreviewColormapRef.current,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
<div className="h-[300px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
setCurrentColormap(colormap);
handleSetColorLUT(colormap);
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewColormapRef.current) {
setPrePreviewColormap(colormap);
}
handleSetColorLUT(colormap);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
handleSetColorLUT(prePreviewColormapRef.current);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
</>
);
}

View File

@ -1,15 +1,13 @@
import React, { ReactElement, useState } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingOptions({
viewportId,
volumeRenderingQualityRange,
}: VolumeRenderingOptionsProps): ReactElement {
export function VolumeRenderingOptions({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingQualityRange } = useViewportRendering(viewportId);
const [hasShade, setShade] = useState(false);
return (
<AllInOneMenu.ItemPanel>

View File

@ -1,14 +1,12 @@
import { AllInOneMenu } from '@ohif/ui-next';
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';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingPresets({
viewportId,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingPresets } = useViewportRendering(viewportId);
const { servicesManager } = useSystem();
const { uiDialogService } = servicesManager.services;

View File

@ -1,54 +1,122 @@
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { useSystem } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export type WindowLevelProps = {
viewportId: string;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
};
export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement {
const { commandsManager } = useSystem();
const { t } = useTranslation('WindowLevelActionMenu');
const onSetWindowLevel = useCallback(
props => {
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...props,
viewportId,
},
context: 'CORNERSTONE',
});
},
[commandsManager, viewportId]
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
);
// Use the hook with the active display set
const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const [showPreview, setShowPreview] = useState(false);
const [prePreviewPreset, setPrePreviewPreset] = useState<WindowLevelPreset | null>(null);
const [currentPreset, setCurrentPreset] = useState<WindowLevelPreset | null>(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewPresetRef = useRef(prePreviewPreset);
prePreviewPresetRef.current = prePreviewPreset;
const currentPresetRef = useRef(currentPreset);
currentPresetRef.current = currentPreset;
// Reset presets when active display set changes
useEffect(() => {
setCurrentPreset(null);
setPrePreviewPreset(null);
}, [activeDisplaySetUID]);
// Handle applying window level preset
const handleSetWindowLevel = (preset: WindowLevelPreset, immediate = false) => {
setWindowLevel({
windowWidth: Number(preset.window),
windowCenter: Number(preset.level),
immediate,
});
};
return (
<AllInOneMenu.ItemPanel>
{presets.map((modalityPresets, modalityIndex) => (
<React.Fragment key={modalityIndex}>
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
<React.Fragment key={modality}>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality })}
</AllInOneMenu.HeaderItem>
{presetsArray.map((preset, index) => (
<AllInOneMenu.Item
key={`${modality}-${index}`}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => onSetWindowLevel(preset)}
/>
<>
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
}}
>
<TabsList>
{viewportDisplaySets.map(ds => (
<TabsTrigger
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{ds.Modality}
</TabsTrigger>
))}
</React.Fragment>
))}
</React.Fragment>
))}
</AllInOneMenu.ItemPanel>
</TabsList>
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
>
<span className="flex-shrink-0">Preview in viewport</span>
<Switch
className="ml-auto flex-shrink-0"
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
// When turning off preview, restore the current preset if one exists
if (!checked && currentPresetRef.current) {
handleSetWindowLevel(currentPresetRef.current, true);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<div className="h-[175px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{windowLevelPresets.map((preset, index) => (
<AllInOneMenu.Item
key={index}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => {
setCurrentPreset(preset);
handleSetWindowLevel(preset, true);
setPrePreviewPreset(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewPresetRef.current) {
setPrePreviewPreset(currentPresetRef.current || preset);
}
handleSetWindowLevel(preset, true);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewPresetRef.current) {
handleSetWindowLevel(prePreviewPresetRef.current, true);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
</>
);
}

View File

@ -1,114 +1,55 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { ReactElement, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AllInOneMenu, cn } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { AllInOneMenu } from '@ohif/ui-next';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
import { setViewportColorbar } from './Colorbar';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { ColorbarProperties } from '../../types/Colorbar';
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
location: string;
element?: HTMLElement;
align?: 'start' | 'end' | 'center';
side?: 'top' | 'bottom' | 'left' | 'right';
};
export function WindowLevelActionMenu({
viewportId,
element,
presets,
verticalDirection,
horizontalDirection,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
const { commandsManager, servicesManager } = useSystem();
const {
colormaps,
colorbarContainerPosition,
colorbarInitialColormap,
colorbarTickPosition,
width: colorbarWidth,
} = colorbarProperties;
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const backgroundColor = viewportInfo?.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
align,
side,
}: WindowLevelActionMenuProps): ReactElement {
return (
<WindowLevelActionMenuContent
viewportId={viewportId}
align={align}
side={side}
/>
);
}
export function WindowLevelActionMenuContent({
viewportId,
align,
side,
}: {
viewportId: string;
align?: string;
side?: string;
}): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
// Use a stable key for the menu to avoid infinite re-renders
const menuKey = useMemo(() => `${viewportId}`, [viewportId]);
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
const [menuKey, setMenuKey] = useState(0);
const [is3DVolume, setIs3DVolume] = useState(false);
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, commandsManager, servicesManager, {
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const newVpHeight = element?.clientHeight;
if (vpHeight !== newVpHeight) {
setVpHeight(newVpHeight);
}
}, [element, vpHeight]);
useEffect(() => {
if (!colorbarService.hasColorbar(viewportId)) {
return;
}
window.setTimeout(() => {
colorbarService.removeColorbar(viewportId);
onSetColorbar();
}, 0);
}, [viewportId, displaySets, viewport]);
useEffect(() => {
setMenuKey(menuKey + 1);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport3D) {
setIs3DVolume(true);
} else {
setIs3DVolume(false);
}
}, [
displaySets,
viewportId,
presets,
volumeRenderingQualityRange,
volumeRenderingPresets,
const {
is3DVolume,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
windowLevelPresets,
volumeRenderingPresets,
volumeRenderingQualityRange,
} = useViewportRendering(viewportId);
return (
<AllInOneMenu.Menu
@ -116,60 +57,38 @@ export function WindowLevelActionMenu({
key={menuKey}
// the visibility is handled by the parent component
isVisible={true}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
align={align}
side={side}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
colorbarProperties={colorbarProperties}
/>
)}
{!is3DVolume && <Colorbar viewportId={viewportId} />}
{colormaps && !is3DVolume && (
{colorbarProperties?.colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
className="flex h-[calc(100%-32px)] flex-col"
>
<Colormap
className="flex h-full w-full flex-col"
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
/>
<Colormap viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
{presets && presets.length > 0 && !is3DVolume && (
{windowLevelPresets?.length > 0 && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets')}
itemIcon="viewport-window-level"
>
<WindowLevel
viewportId={viewportId}
presets={presets}
/>
<WindowLevel viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
viewportId={viewportId}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions
viewportId={viewportId}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeRenderingOptions viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>

View File

@ -2,63 +2,75 @@ import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
cn,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
useViewportActionCorners,
AllInOneMenu,
useIconPresentation,
} from '@ohif/ui-next';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { MENU_IDS } from '../menus/menu-ids';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useViewportRendering } from '../../hooks';
export function WindowLevelActionMenuWrapper({
viewportId,
element,
location,
displaySets,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
displaySets: Array<AppTypes.DisplaySet>;
}>): ReactNode {
const [viewportGrid] = useViewportGrid();
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
export function WindowLevelActionMenuWrapper(
props: withAppTypes<{
viewportId: string;
element?: HTMLElement;
location?: number;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
displaySets?: Array<AppTypes.DisplaySet>;
disabled?: boolean;
isEmbedded?: boolean;
}>
): ReactNode {
const {
viewportId,
element,
location,
isOpen = false,
onOpen,
onClose,
disabled,
isEmbedded = false,
onInteraction: onInteractionProps,
hasEmbeddedVariantToUse,
...rest
} = props;
const [gridState] = useViewportGrid();
const viewportIdToUse = viewportId || gridState.activeViewportId;
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportIdToUse);
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 isMenuOpen =
actionCornerState.viewports[viewportId]?.[location]?.find(
item => item.id === MENU_IDS.WINDOW_LEVEL_MENU
)?.isOpen ?? false;
const { toolbarService } = servicesManager.services;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const handleOpenChange = (openState: boolean) => {
const shouldToggleColorbar = hasColorbar && !isEmbedded;
if (isOpen && shouldToggleColorbar && openState) {
toggleColorbar();
onClose?.();
return;
}
if (!isOpen && openState && shouldToggleColorbar) {
toggleColorbar();
return;
}
if (openState) {
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
onOpen?.();
} else {
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
onClose?.();
}
};
const { align, side, horizontalDirection, verticalDirection } = getMenuDirections(
location,
viewportActionCornersAPI
);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const { align, side } = toolbarService.getAlignAndSide(location);
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
@ -66,25 +78,41 @@ export function WindowLevelActionMenuWrapper({
return null;
}
const Icon =
hasColorbar && !isEmbedded && hasEmbeddedVariantToUse ? (
<Icons.Close className={iconClassName} />
) : (
<Icons.ViewportWindowLevel className={iconClassName} />
);
return (
<Popover
open={isMenuOpen}
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
className={cn(
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible',
'text-highlight'
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
>
<Icons.ByName name="viewport-window-level" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
@ -94,45 +122,12 @@ export function WindowLevelActionMenuWrapper({
sideOffset={5}
>
<WindowLevelActionMenu
viewportId={viewportId}
viewportId={viewportIdToUse}
element={element}
presets={displaySetPresets}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
align={align}
side={side}
/>
</PopoverContent>
</Popover>
);
}
const getMenuDirections = (location, viewportActionCornersAPI) => {
let align = 'center';
let side = 'bottom';
if (location !== undefined) {
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
align = positioning.align;
side = positioning.side;
}
let horizontalDirection;
let verticalDirection;
if (side === 'bottom') {
verticalDirection = AllInOneMenu.VerticalDirection.TopToBottom;
} else {
verticalDirection = AllInOneMenu.VerticalDirection.BottomToTop;
}
if (align === 'start') {
horizontalDirection = AllInOneMenu.HorizontalDirection.LeftToRight;
} else {
horizontalDirection = AllInOneMenu.HorizontalDirection.RightToLeft;
}
return { align, side, horizontalDirection, verticalDirection };
};

View File

@ -1,13 +0,0 @@
import React from 'react';
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
export function getWindowLevelActionMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
displaySets: Array<any>;
}>
) {
return <WindowLevelActionMenuWrapper {...props} />;
}

View File

@ -6,8 +6,7 @@ export * from './MeasurementsMenu';
export * from './SeriesMeasurements';
export * from './StudyMeasurementsActions';
export * from './MeasurementsOrAdditionalFindings';
export * from './ViewportDataOverlaySettingMenu';
export * from './ViewportOrientationMenu';
export * from './WindowLevelActionMenu';
export * from './ModalityLoadBadge';
export { DicomUpload };

View File

@ -1,5 +0,0 @@
export const MENU_IDS = {
ORIENTATION_MENU: 'orientationMenu',
DATA_OVERLAY_MENU: 'dataOverlayMenu',
WINDOW_LEVEL_MENU: 'windowLevelActionMenu',
};

View File

@ -10,13 +10,13 @@ import {
} from '../types/Colorbar';
import { ColorMapPreset } from '../types/Colormap';
const defaultPosition: ColorbarPositionType = 'right';
const defaultPosition: ColorbarPositionType = 'bottom';
const DefaultColormap = 'Grayscale';
const positionStyles: PositionStylesMapType = {
left: { left: '5%', width: '15px' },
right: { right: '5%', width: '15px' },
bottom: { bottom: '1%', height: '18px' },
left: { width: '15px' },
right: { width: '15px' },
bottom: { height: '15px' },
};
// Typed position-specific tick styles

View File

@ -1,53 +0,0 @@
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportOrientationMenu } from '../components/ViewportOrientationMenu';
import { ViewportActionCorners } from '@ohif/ui-next';
import { MENU_IDS } from '../components/menus/menu-ids';
// 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, location }) => {
return getWindowLevelActionMenu({
viewportId,
element,
displaySets,
location,
});
};
export default {
'ui.viewportActionCorner': ViewportActionCorners,
'viewportActionMenu.topLeft': [
{
id: MENU_IDS.ORIENTATION_MENU,
component: createOrientationMenu,
},
{
id: MENU_IDS.DATA_OVERLAY_MENU,
component: createDataOverlay,
},
{
id: MENU_IDS.WINDOW_LEVEL_MENU,
component: createWindowLevelMenu,
},
],
'viewportActionMenu.topRight': [],
'viewportActionMenu.bottomLeft': [],
'viewportActionMenu.bottomRight': [],
};

View File

@ -11,7 +11,6 @@ import windowLevelPresetsCustomization from './customizations/windowLevelPresets
import miscCustomization from './customizations/miscCustomization';
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
function getCustomizationModule({ commandsManager, servicesManager }) {
return [
@ -31,7 +30,6 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
...miscCustomization,
...captureViewportModalCustomization,
...viewportDownloadWarningCustomization,
...viewportActionMenuCustomizations,
},
},
];

View File

@ -1,23 +1,358 @@
import { Enums } from '@cornerstonejs/tools';
import { utils } from '@ohif/ui-next';
import { ViewportDataOverlayMenuWrapper } from './components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper';
import { ViewportOrientationMenuWrapper } from './components/ViewportOrientationMenu/ViewportOrientationMenuWrapper';
import { WindowLevelActionMenuWrapper } from './components/WindowLevelActionMenu/WindowLevelActionMenuWrapper';
import { VOIManualControlMenuWrapper } from './components/VOIManualControlMenu';
import { ThresholdMenuWrapper } from './components/ThresholdMenu/ThresholdMenuWrapper';
import { OpacityMenuWrapper } from './components/OpacityMenu/OpacityMenuWrapper';
import ModalityLoadBadge from './components/ModalityLoadBadge/ModalityLoadBadge';
import NavigationComponent from './components/NavigationComponent/NavigationComponent';
import TrackingStatus from './components/TrackingStatus/TrackingStatus';
import ViewportColorbarsContainer from './components/ViewportColorbar';
import AdvancedRenderingControls from './components/AdvancedRenderingControls';
const getDisabledState = (disabledText?: string) => ({
disabled: true,
disabledText: disabledText ?? 'Not available on the current viewport',
});
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
export default function getToolbarModule({ servicesManager, extensionManager }: withAppTypes) {
const {
toolGroupService,
toolbarService,
syncGroupService,
cornerstoneViewportService,
hangingProtocolService,
colorbarService,
displaySetService,
viewportGridService,
segmentationService,
} = servicesManager.services;
return [
{
name: 'ohif.advancedRenderingControls',
defaultComponent: AdvancedRenderingControls,
},
{
name: 'evaluate.advancedRenderingControls',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
const hasColorbar = colorbarService?.hasColorbar(viewportId) || false;
return {
disabled: !hasColorbar,
};
},
},
{
name: 'ohif.colorbar',
defaultComponent: ViewportColorbarsContainer,
},
{
name: 'ohif.trackingStatus',
defaultComponent: TrackingStatus,
},
{
name: 'evaluate.trackingStatus',
evaluate: ({ viewportId }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return {
disabled: true,
};
}
return {
disabled: false,
};
},
},
// ModalityLoadBadge
{
name: 'ohif.modalityLoadBadge',
defaultComponent: ModalityLoadBadge,
},
{
name: 'evaluate.modalityLoadBadge',
evaluate: ({ viewportId }) => {
// We can't use useViewportDisplaySets hook here since we're in a non-React context,
// but we'll follow the same pattern by getting only the display sets for this viewport
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return {
disabled: true,
};
}
// Get the display sets that are specifically in this viewport
const viewportDisplaySets = displaySetUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
// Only show status for supported types like SR, SEG, RTSTRUCT
const isSupportedType = viewportDisplaySets.some(
displaySet =>
displaySet?.Modality === 'SR' ||
displaySet?.Modality === 'SEG' ||
displaySet?.Modality === 'RTSTRUCT'
);
return {
disabled: !isSupportedType,
};
},
},
// NavigationComponent
{
name: 'ohif.navigationComponent',
defaultComponent: NavigationComponent,
},
{
name: 'evaluate.navigationComponent',
evaluate: ({ viewportId }) => {
const { trackedMeasurementsService } = servicesManager.services;
// Same logic as statusComponent - only show for SR, SEG, RTSTRUCT
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return {
disabled: true,
};
}
// Get the display sets that are specifically in this viewport
const viewportDisplaySets = displaySetUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
// Check if there's a need for navigation:
// 1. Segmentations are present (for SEG/RTSTRUCT navigation)
// 2. There are tracked measurements in the viewport (for SR navigation)
// Check for SEG/RTSTRUCT navigation
const hasSegmentation =
segmentationService.getSegmentationRepresentations(viewportId).length > 0;
if (!trackedMeasurementsService) {
return {
disabled: !hasSegmentation,
};
}
// Check if any of the viewport's series are being tracked
const hasTrackedInViewport = viewportDisplaySets.some(
displaySet =>
displaySet?.SeriesInstanceUID &&
trackedMeasurementsService.isSeriesTracked(displaySet.SeriesInstanceUID)
);
const isSRDisplaySet = viewportDisplaySets.some(
displaySet => displaySet?.Modality === 'SR'
);
// Enable navigation if:
// - There's a segmentation to navigate (SEG/RTSTRUCT)
// - OR there are tracked measurements in the viewport (SR/etc.)
const needsNavigation = hasSegmentation || hasTrackedInViewport || isSRDisplaySet;
return {
disabled: !needsNavigation,
};
},
},
{
name: 'ohif.dataOverlayMenu',
defaultComponent: ViewportDataOverlayMenuWrapper,
},
{
name: 'evaluate.dataOverlayMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
// Example: Show data overlay menu only for certain modalities
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return {
disabled: true,
};
}
return {
disabled: false,
};
},
},
{
name: 'ohif.orientationMenu',
defaultComponent: ViewportOrientationMenuWrapper,
},
{
name: 'evaluate.orientationMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
// Only show orientation menu for 3D capable viewports
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const isNotReconstructable = displaySets.some(displaySet => !displaySet?.isReconstructable);
const disabled = isNotReconstructable;
return {
disabled,
};
},
},
{
name: 'ohif.windowLevelMenu',
defaultComponent: WindowLevelActionMenuWrapper,
},
{
name: 'ohif.voiManualControlMenu',
defaultComponent: VOIManualControlMenuWrapper,
},
{
name: 'ohif.windowLevelMenuEmbedded',
defaultComponent: WindowLevelActionMenuWrapper,
},
{
name: 'evaluate.windowLevelMenuEmbedded',
evaluate: () => {
return {
isEmbedded: true,
};
},
},
{
name: 'ohif.thresholdMenu',
defaultComponent: ThresholdMenuWrapper,
},
{
name: 'ohif.opacityMenu',
defaultComponent: OpacityMenuWrapper,
},
{
name: 'evaluate.windowLevelMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const supportWindowLevel = displaySets.some(displaySet => displaySet?.supportsWindowLevel);
const isInAnySection = toolbarService.isInAnySection('windowLevelMenuEmbedded');
return {
disabled: !supportWindowLevel,
hasEmbeddedVariantToUse: !!isInAnySection,
};
},
},
{
name: 'evaluate.voiManualControlMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const supportWindowLevel = displaySets.some(displaySet => displaySet?.supportsWindowLevel);
return {
disabled: !supportWindowLevel,
};
},
},
{
name: 'evaluate.thresholdMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return {
disabled: true,
};
}
if (viewport.type !== 'orthographic') {
return {
disabled: true,
};
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs.length) {
return {
disabled: true,
};
}
return {
disabled: false,
};
},
},
{
name: 'evaluate.opacityMenu',
evaluate: ({ viewportId }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.type !== 'orthographic') {
return {
disabled: true,
};
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (displaySetUIDs.length <= 1) {
return {
disabled: true,
};
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const hasOverlayable = displaySets.some(displaySet => displaySet?.isOverlayDisplaySet);
return {
disabled: hasOverlayable,
};
},
},
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{

View File

@ -1,4 +1,8 @@
export * from './useViewportActionCorners';
export * from './useActiveViewportSegmentationRepresentations';
export * from './useMeasurements';
export * from './useSegmentations';
export * from './useViewportSegmentations';
export * from './useViewportHover';
export * from './useViewportDisplaySets';
export * from './useMeasurementTracking';
export * from './useViewportRendering';

View File

@ -1,216 +1,14 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { roundNumber } from '@ohif/core/src/utils';
import {
SegmentationData,
SegmentationRepresentation,
} from '../services/SegmentationService/SegmentationService';
import { useViewportGrid } from '@ohif/ui-next';
import { useViewportSegmentations } from './useViewportSegmentations';
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
function useActiveViewportSegmentationRepresentations() {
const [viewportGrid] = useViewportGrid();
function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
const viewportId = viewportGrid?.activeViewportId;
// Get the readable text mapping once
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
const segmentations = useViewportSegmentations({ viewportId });
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
const primary = [];
const indentation = ' '.repeat(indent);
for (const key in stats) {
if (Object.prototype.hasOwnProperty.call(stats, key)) {
const value = stats[key];
const readableText = readableTextMap?.[key];
if (!readableText) {
continue;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Add empty row before category (except for the first category)
if (primary.length > 0) {
primary.push('');
}
// Add category title
primary.push(`${indentation}${readableText}`);
// Recursively handle nested objects
primary.push(...mapStatsToDisplay(value, indent + 1));
} else {
// For non-nested values, don't add empty rows
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
}
}
}
return primary;
}
// Get customization for display text mapping
const displayTextMapper = segment => {
const defaultDisplay = {
primary: [],
secondary: [],
};
// If the segment has cachedStats, map it to readable text
if (segment.cachedStats) {
const primary = mapStatsToDisplay(segment.cachedStats);
defaultDisplay.primary = primary;
}
return defaultDisplay;
};
const updatedSegments = {};
Object.entries(segments).forEach(([segmentIndex, segment]) => {
updatedSegments[segmentIndex] = {
...segment,
displayText: displayTextMapper(segment),
};
});
// Map the segments and apply the display text mapper
return {
...segmentation,
label,
segments: updatedSegments,
};
return segmentations;
}
/**
* Represents the combination of segmentation data and its representation in a viewport.
*/
type ViewportSegmentationRepresentation = {
segmentationsWithRepresentations: {
representation: SegmentationRepresentation;
segmentation: SegmentationData;
}[];
disabled: boolean;
};
/**
* Custom hook that provides segmentation data and their representations for the active viewport.
* @param options - The options object.
* @param options.servicesManager - The services manager object.
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
* @param options.debounceTime - Debounce time in milliseconds for updates.
* @returns An array of segmentation data and their representations for the active viewport.
*/
export function useActiveViewportSegmentationRepresentations({
servicesManager,
subscribeToDataModified = false,
debounceTime = 0,
}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
const { segmentationService, viewportGridService, customizationService, displaySetService } =
servicesManager.services;
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
useState<ViewportSegmentationRepresentation>({
segmentationsWithRepresentations: [],
disabled: false,
});
useEffect(() => {
const update = () => {
const viewportId = viewportGridService.getActiveViewportId();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
if (!displaySet) {
return;
}
if (excludedModalities.includes(displaySet.Modality)) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: true,
}));
return;
}
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: false,
}));
return;
}
const representations = segmentationService.getSegmentationRepresentations(viewportId);
const newSegmentationsWithRepresentations = representations.map(representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
});
setSegmentationsWithRepresentations({
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
disabled: false,
});
};
const debouncedUpdate =
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
update();
const subscriptions = [
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
debouncedUpdate
),
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
debouncedUpdate
),
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
];
if (subscribeToDataModified) {
subscriptions.push(
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
debouncedUpdate
)
);
}
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
if (debounceTime > 0) {
debouncedUpdate.cancel();
}
};
}, [
segmentationService,
viewportGridService,
customizationService,
displaySetService,
debounceTime,
subscribeToDataModified,
]);
return segmentationsWithRepresentations;
}
export { useActiveViewportSegmentationRepresentations };

View File

@ -0,0 +1,132 @@
import { useState, useEffect, useCallback } from 'react';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import { BaseVolumeViewport } from '@cornerstonejs/core';
/**
* Hook that provides measurement tracking information for a viewport
*
* @param options - The hook options
* @param options.viewportId - The ID of the viewport to track
* @returns An object containing the tracking state, related information, and tracked measurement UIDs
*/
export function useMeasurementTracking({ viewportId }: { viewportId: string }) {
const { servicesManager } = useSystem();
const { cornerstoneViewportService, trackedMeasurementsService, measurementService } =
servicesManager.services;
const { backgroundDisplaySet } = useViewportDisplaySets(viewportId);
// Tracking states
const [isTracked, setIsTracked] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [trackedMeasurementUIDs, setTrackedMeasurementUIDs] = useState<string[]>([]);
const updateTrackedMeasurements = useCallback(() => {
if (!measurementService || !backgroundDisplaySet?.SeriesInstanceUID || !isTracked) {
setTrackedMeasurementUIDs([]);
return;
}
const seriesInstanceUID = backgroundDisplaySet.SeriesInstanceUID;
const seriesMeasurements = measurementService.getMeasurements(
measurement => measurement.referenceSeriesUID === seriesInstanceUID
);
const uids = seriesMeasurements.map(measurement => measurement.uid);
setTrackedMeasurementUIDs(uids);
}, [measurementService, backgroundDisplaySet, isTracked]);
const updateIsTracked = useCallback(() => {
if (!trackedMeasurementsService || !backgroundDisplaySet?.SeriesInstanceUID) {
setIsTracked(false);
return;
}
const trackedSeries = trackedMeasurementsService.getTrackedSeries();
if (!trackedSeries?.length) {
setIsTracked(false);
return;
}
const viewport = cornerstoneViewportService?.getCornerstoneViewport(viewportId);
const SeriesInstanceUID = backgroundDisplaySet.SeriesInstanceUID;
if (viewport instanceof BaseVolumeViewport) {
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
setIsTracked(false);
return;
}
}
const seriesIsTracked = trackedSeries.includes(SeriesInstanceUID);
setIsTracked(seriesIsTracked);
}, [viewportId, backgroundDisplaySet, cornerstoneViewportService, trackedMeasurementsService]);
// Update tracked measurements whenever tracking state changes
useEffect(() => {
updateTrackedMeasurements();
}, [isTracked, updateTrackedMeasurements]);
useEffect(() => {
if (!trackedMeasurementsService) {
return;
}
setIsLocked(trackedMeasurementsService.isTrackingEnabled());
updateIsTracked();
const subscriptions = [
trackedMeasurementsService.subscribe(trackedMeasurementsService.EVENTS.TRACKING_ENABLED, () =>
setIsLocked(true)
),
trackedMeasurementsService.subscribe(
trackedMeasurementsService.EVENTS.TRACKING_DISABLED,
() => setIsLocked(false)
),
trackedMeasurementsService.subscribe(
trackedMeasurementsService.EVENTS.TRACKED_SERIES_CHANGED,
() => updateIsTracked()
),
trackedMeasurementsService.subscribe(trackedMeasurementsService.EVENTS.SERIES_ADDED, () =>
updateIsTracked()
),
trackedMeasurementsService.subscribe(trackedMeasurementsService.EVENTS.SERIES_REMOVED, () =>
updateIsTracked()
),
];
// Subscribe to measurement service events to update trackedMeasurementUIDs
if (measurementService) {
[
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
measurementService.EVENTS.MEASUREMENT_UPDATED,
measurementService.EVENTS.MEASUREMENT_REMOVED,
measurementService.EVENTS.MEASUREMENTS_CLEARED,
].forEach(eventType => {
subscriptions.push(
measurementService.subscribe(eventType, () => updateTrackedMeasurements())
);
});
}
return () => {
subscriptions.forEach(sub => sub.unsubscribe());
};
}, [trackedMeasurementsService, measurementService, updateIsTracked, updateTrackedMeasurements]);
return {
isTracked,
isLocked,
seriesInstanceUID: backgroundDisplaySet?.SeriesInstanceUID,
trackedMeasurementUIDs,
};
}
export default useMeasurementTracking;

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { useSystem } from '@ohif/core';
function mapMeasurementToDisplay(measurement, displaySetService) {
const { referenceSeriesUID } = measurement;
@ -56,13 +56,14 @@ function mapMeasurementToDisplay(measurement, displaySetService) {
* @param {Object} options.valueTypes - The value types for mapping measurements.
* @returns {Array} An array of mapped and filtered measurements.
*/
export function useMeasurements(servicesManager, { measurementFilter }) {
export function useMeasurements({ measurementFilter }) {
const { servicesManager } = useSystem();
const { measurementService, displaySetService } = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
const updateDisplayMeasurements = () => {
let measurements = measurementService.getMeasurements(measurementFilter);
const measurements = measurementService.getMeasurements(measurementFilter);
const mappedMeasurements = measurements.map(m =>
mapMeasurementToDisplay(m, displaySetService)
);

View File

@ -48,30 +48,38 @@ export type UseViewportDisplaySetsOptions = {
* Return type for useViewportDisplaySets
*/
export type ViewportDisplaySets = {
/**
* All display sets for the viewport
*/
allDisplaySets: AppTypes.DisplaySet[];
/**
* The viewport display sets for the viewport
*/
viewportDisplaySets: AppTypes.DisplaySet[];
/**
* The primary display set for the viewport (base image)
*/
backgroundDisplaySet?: any;
backgroundDisplaySet?: AppTypes.DisplaySet;
/**
* Display sets currently shown with background (non-overlay layers)
*/
foregroundDisplaySets?: any[];
foregroundDisplaySets?: AppTypes.DisplaySet[];
/**
* Segmentation display sets currently applied as overlays
*/
overlayDisplaySets?: any[];
overlayDisplaySets?: AppTypes.DisplaySet[];
/**
* Display sets that could be toggled on as overlays (derived modalities)
*/
potentialOverlayDisplaySets?: any[];
potentialOverlayDisplaySets?: AppTypes.DisplaySet[];
/**
* Display sets that could be added as foreground layers
*/
potentialForegroundDisplaySets?: any[];
potentialForegroundDisplaySets?: AppTypes.DisplaySet[];
/**
* Display sets that could replace the current background
*/
potentialBackgroundDisplaySets?: any[];
potentialBackgroundDisplaySets?: AppTypes.DisplaySet[];
};
/**
@ -82,7 +90,7 @@ export type ViewportDisplaySets = {
* @returns Object containing requested display set collections based on options
*/
export function useViewportDisplaySets(
viewportId: string,
viewportId?: string,
options?: UseViewportDisplaySetsOptions
): ViewportDisplaySets {
const { servicesManager } = useSystem();
@ -91,7 +99,9 @@ export function useViewportDisplaySets(
// 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();
const [viewportGridState, viewportGridService] = useViewportGrid();
const viewportIdToUse = viewportId || viewportGridState.activeViewportId;
// Apply defaults - include everything if no options specified
const {
@ -114,8 +124,8 @@ export function useViewportDisplaySets(
const needsSegmentations = includeOverlay;
const segmentationRepresentations = useMemo(
() =>
needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportId) : [],
[segmentationService, viewportId, needsSegmentations]
needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportIdToUse) : [],
[segmentationService, viewportIdToUse, needsSegmentations]
);
const overlayDisplaySets = useMemo(() => {
@ -142,10 +152,10 @@ export function useViewportDisplaySets(
return { viewportDisplaySets: [], enhancedDisplaySets: [] };
}
return getEnhancedDisplaySets({
viewportId,
viewportId: viewportIdToUse,
services: { displaySetService, viewportGridService },
});
}, [viewportId, displaySetService, viewportGridService, needsEnhancedDisplaySets]);
}, [viewportIdToUse, displaySetService, viewportGridService, needsEnhancedDisplaySets]);
const backgroundDisplaySet = useMemo(
() => (includeBackground ? viewportDisplaySets[0] : undefined),
@ -217,7 +227,7 @@ export function useViewportDisplaySets(
foregroundDisplaySetUIDs,
]);
const result: ViewportDisplaySets = {};
const result: ViewportDisplaySets = { allDisplaySets, viewportDisplaySets };
if (includeBackground) {
result.backgroundDisplaySet = backgroundDisplaySet;

View File

@ -0,0 +1,77 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
/**
* Hook to track whether the mouse is hovering over a specific viewport
* and whether the viewport is active
*
* @param viewportId - The ID of the viewport to track
* @returns { isHovered, isActive } - Whether the viewport is hovered and active
*/
export function useViewportHover(viewportId: string): { isHovered: boolean; isActive: boolean } {
const [isHovered, setIsHovered] = useState(false);
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const isActive = activeViewportId === viewportId;
const setupListeners = useCallback(() => {
const viewportElement = document.querySelector(`[data-viewportId="${viewportId}"]`);
const element = viewportElement?.closest('.viewport-wrapper') || viewportElement;
if (!element) {
return null;
}
let elementRect = (element as HTMLElement).getBoundingClientRect();
let lastIsInside = false;
// Update rectangle when window is resized
const updateRect = () => {
elementRect = (element as HTMLElement).getBoundingClientRect();
};
const isPointInViewport = (x, y) => {
return (
x >= elementRect.left &&
x <= elementRect.right &&
y >= elementRect.top &&
y <= elementRect.bottom
);
};
const handleMouseMove = event => {
const isInside = isPointInViewport(event.clientX, event.clientY);
if (isInside !== lastIsInside) {
lastIsInside = isInside;
setIsHovered(isInside);
}
};
let resizeTimeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateRect, 10);
};
window.addEventListener('resize', handleResize);
document.addEventListener('mousemove', handleMouseMove);
updateRect();
return () => {
window.removeEventListener('resize', handleResize);
document.removeEventListener('mousemove', handleMouseMove);
clearTimeout(resizeTimeout);
};
}, [viewportId]);
useEffect(() => {
const cleanup = setupListeners();
return cleanup;
}, [setupListeners]);
// Memoize the return value to prevent unnecessary re-renders
return useMemo(() => ({ isHovered, isActive }), [isHovered, isActive]);
}

View File

@ -0,0 +1,882 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import {
StackViewport,
Types,
VolumeViewport3D,
utilities,
Enums,
BaseVolumeViewport,
cache,
} from '@cornerstonejs/core';
import { WindowLevelPreset } from '../types/WindowLevel';
import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar';
import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig';
import { VolumeLightingParams } from '../types';
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
interface ViewportRenderingOptions {
location?: number;
displaySetInstanceUID?: string;
}
interface PixelValueRange {
min: number;
max: number;
}
interface WindowLevelHook {
// Viewport information
is3DVolume: boolean;
isViewportBackgroundLight: boolean;
viewportDisplaySets: AppTypes.DisplaySet[] | undefined;
voiRange: { lower: number; upper: number } | undefined;
windowLevel: { windowWidth: number; windowCenter: number } | undefined;
// Window level functions
setWindowLevel: (preset: {
windowWidth: number;
windowCenter: number;
immediate?: boolean;
}) => void;
setVOIRange: (params: { lower: number; upper: number }) => void;
windowLevelPresets: WindowLevelPreset[];
allWindowLevelPresets: Array<{
displaySetInstanceUID: string;
modality: string;
presets: WindowLevelPreset[];
}>;
// Colorbar functions
hasColorbar: boolean;
toggleColorbar: (options?: Partial<ColorbarOptions>) => void;
colorbarProperties: ColorbarProperties;
colorbarPosition: ColorbarPositionType;
setColorbarPosition: React.Dispatch<React.SetStateAction<ColorbarPositionType>>;
// Colormap functions
setColormap: (params: { colormap: any; opacity?: number; immediate?: boolean }) => void;
colormap: any;
// Opacity functions
opacity: number | undefined;
setOpacity: (opacity: number) => void;
opacityLinear: number | undefined;
setOpacityLinear: (opacityLinear: number) => void;
// Threshold functions
threshold: number | undefined;
setThreshold: (threshold: number) => void;
pixelValueRange: PixelValueRange;
// 3D volume rendering functions
setVolumeRenderingPreset: (preset: any) => void;
setVolumeRenderingQuality: (quality: number) => void;
setVolumeLighting: (params: { ambient: number; diffuse: number; specular: number }) => void;
setVolumeShading: (enabled: boolean) => void;
volumeRenderingPresets: Array<any>;
volumeRenderingQualityRange: { min: number; max: number; step: number };
}
const getPosition = (location: number): ColorbarPositionType => {
switch (location) {
case ButtonLocation.LeftMiddle:
return 'left';
case ButtonLocation.RightMiddle:
return 'right';
case ButtonLocation.BottomMiddle:
return 'bottom';
case ButtonLocation.TopMiddle:
return 'top';
default:
return 'bottom'; // Default to bottom if location doesn't match a middle position
}
};
const GAMMA = 1 / 5;
const linearToOpacity = (linearValue: number): number => {
return Math.pow(linearValue, GAMMA);
};
const opacityToLinear = (opacityValue: number): number => {
return Math.pow(opacityValue, 1.0 / GAMMA);
};
/**
* Hook to access window level functionality for a specific viewport
*
* @param viewportId - The ID of the viewport to get window level functionality for
* @param options - Options for the hook, including location and displaySetInstanceUID
* @returns Window level API for the specified viewport
*/
export function useViewportRendering(
viewportId?: string,
options?: ViewportRenderingOptions
): WindowLevelHook {
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService, colorbarService, customizationService } =
servicesManager.services;
const [is3DVolume, setIs3DVolume] = useState(false);
const [hasColorbar, setHasColorbar] = useState(colorbarService.hasColorbar(viewportId));
const [colorbarPosition, setColorbarPosition] = useState<ColorbarPositionType>(
options?.location ? getPosition(options.location) : 'bottom'
);
const [voiRange, setVoiRange] = useState<{ lower: number; upper: number } | undefined>();
const voiRangeRef = React.useRef<{ lower: number; upper: number } | undefined>();
const [opacity, setOpacityState] = useState<number | undefined>();
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
const [threshold, setThresholdState] = useState<number | undefined>();
const [pixelValueRange, setPixelValueRange] = useState<PixelValueRange>({ min: 0, max: 255 });
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const { displaySetService } = servicesManager.services;
// Determine the active display set instance UID (internal only, not exposed)
const activeDisplaySetInstanceUID = useMemo(() => {
if (options?.displaySetInstanceUID) {
return options.displaySetInstanceUID;
}
if (viewportDisplaySets && viewportDisplaySets.length > 0) {
return viewportDisplaySets[0].displaySetInstanceUID;
}
return undefined;
}, [options?.displaySetInstanceUID, viewportDisplaySets]);
const viewportInfo = viewportId ? cornerstoneViewportService.getViewportInfo(viewportId) : null;
const { presets, colorbarProperties, volumeRenderingPresets, volumeRenderingQualityRange } =
getCustomizationData(customizationService);
const backgroundColor = viewportInfo?.getViewportOptions().background;
const isViewportBackgroundLight = backgroundColor
? utilities.isEqual(backgroundColor, [1, 1, 1])
: false;
const allWindowLevelPresets = useMemo(() => {
return (
viewportDisplaySets
?.filter(displaySet => presets?.[displaySet.Modality as string])
.map(displaySet => {
return {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
modality: displaySet.Modality as string,
presets: presets[displaySet.Modality as string],
};
}) || []
);
}, [viewportDisplaySets, presets]);
// Calculate pixel value range for the active display set
useEffect(() => {
if (!activeDisplaySetInstanceUID) {
return;
}
const selectedDisplaySet = displaySetService.getDisplaySetByUID(activeDisplaySetInstanceUID);
if (!selectedDisplaySet?.imageIds?.length) {
return;
}
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
if (!(csViewport instanceof BaseVolumeViewport)) {
return;
}
const volumeIds = csViewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID));
if (!volumeId) {
return;
}
// only handle volume viewports for now
const imageData = csViewport.getImageData(volumeId);
if (!imageData) {
return;
}
const imageDataVtk = imageData.imageData;
const { voxelManager } = imageDataVtk.get('voxelManager');
const range = voxelManager.getRange();
setPixelValueRange({ min: range[0], max: range[1] });
}, [activeDisplaySetInstanceUID, displaySetService, cornerstoneViewportService, viewportId]);
// Get the presets specifically for the active display set
const activeDisplaySetPresets = useMemo(() => {
if (!activeDisplaySetInstanceUID || allWindowLevelPresets.length === 0) {
return [];
}
const activePresetData = allWindowLevelPresets.find(
preset => preset.displaySetInstanceUID === activeDisplaySetInstanceUID
);
if (!activePresetData) {
return [];
}
return activePresetData.presets;
}, [allWindowLevelPresets, activeDisplaySetInstanceUID]);
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
setIs3DVolume(viewport instanceof VolumeViewport3D);
// Initialize the VOI range from the viewport
if (viewport && activeDisplaySetInstanceUID) {
try {
let properties;
if (viewport instanceof StackViewport) {
properties = viewport.getProperties();
if (properties.voiRange) {
setVoiRange(properties.voiRange);
voiRangeRef.current = properties.voiRange;
}
} else if (viewport instanceof BaseVolumeViewport) {
// For volume viewports, find the actor for the active display set
const volumeIds = viewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID));
if (volumeId) {
properties = viewport.getProperties(volumeId);
if (properties.voiRange) {
setVoiRange(properties.voiRange);
voiRangeRef.current = properties.voiRange;
}
// Get opacity from colormap if available
if (properties.colormap && properties.colormap.opacity !== undefined) {
const isArray = Array.isArray(properties.colormap.opacity);
const opacity = isArray
? properties.colormap.opacity.reduce((max, current) => Math.max(max, current), 0)
: properties.colormap.opacity;
setOpacityState(opacity);
setOpacityLinearState(opacityToLinear(opacity));
}
// Get threshold from colormap if available
if (properties.colormap && properties.colormap.threshold !== undefined) {
setThresholdState(properties.colormap.threshold);
}
}
}
} catch (error) {
console.error('Error initializing VOI range:', error);
}
}
}, [cornerstoneViewportService, viewportId, activeDisplaySetInstanceUID]);
useEffect(() => {
if (!viewportId) {
return;
}
const updateColorbarState = () => {
const hasColorbarValue = colorbarService.hasColorbar(viewportId);
setHasColorbar(hasColorbarValue);
};
updateColorbarState();
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [colorbarService, viewportId]);
useEffect(() => {
if (!viewportId || !activeDisplaySetInstanceUID) {
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const element = viewport.element;
if (!element) {
return;
}
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
// Check if this update is coming from our own setVOIRange or setWindowLevel call
// If so, we already updated our state and don't need to do it again
const isInternalUpdate = voiRangeRef.current && areVoiRangesClose(voiRangeRef.current, range);
if (!isInternalUpdate) {
voiRangeRef.current = range;
setVoiRange(range);
}
};
const updateColormap = eventDetail => {
const { colormap } = eventDetail.detail;
if (!colormap) {
return;
}
// Extract threshold from colormap in the event detail
if (colormap.threshold !== undefined) {
setThresholdState(colormap.threshold);
}
if (colormap.opacity !== undefined) {
setOpacityState(colormap.opacity);
setOpacityLinearState(opacityToLinear(colormap.opacity));
}
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
element.addEventListener(Enums.Events.COLORMAP_MODIFIED, updateColormap);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
element.removeEventListener(Enums.Events.COLORMAP_MODIFIED, updateColormap);
};
}, [viewportId, activeDisplaySetInstanceUID, cornerstoneViewportService, opacityToLinear]);
const validateActiveDisplaySet = useCallback(() => {
if (!activeDisplaySetInstanceUID) {
throw new Error('No active display set instance UID is available');
}
if (viewportDisplaySets?.length === 0) {
throw new Error('No display sets are available for this viewport');
}
// Verify the active display set is in the viewport
const isDisplaySetInViewport = viewportDisplaySets?.some(
ds => ds.displaySetInstanceUID === activeDisplaySetInstanceUID
);
if (!isDisplaySetInViewport) {
throw new Error(
`Display set with UID ${activeDisplaySetInstanceUID} is not present in the viewport`
);
}
return activeDisplaySetInstanceUID;
}, [activeDisplaySetInstanceUID, viewportDisplaySets]);
const setWindowLevel = useCallback(
(preset: { windowWidth: number; windowCenter: number; immediate?: boolean }) => {
if (!viewportId) {
return;
}
const displaySetInstanceUID = validateActiveDisplaySet();
// Update voiRange as well, to ensure immediate UI updates
const { lower, upper } = utilities.windowLevel.toLowHighRange(
preset.windowWidth,
preset.windowCenter
);
const newVoiRange = { lower, upper };
if (!voiRangeRef.current || !areVoiRangesClose(voiRangeRef.current, newVoiRange)) {
voiRangeRef.current = newVoiRange;
setVoiRange(newVoiRange);
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...preset,
viewportId,
displaySetInstanceUID,
},
context: 'CORNERSTONE',
});
}
},
[commandsManager, viewportId, validateActiveDisplaySet]
);
const setVOIRange = useCallback(
(params: { lower: number; upper: number }) => {
if (!viewportId) {
return;
}
// Only update if VOI values have actually changed
if (!voiRangeRef.current || !areVoiRangesClose(voiRangeRef.current, params)) {
// Update the ref and state immediately to avoid race conditions with the event listener
voiRangeRef.current = params;
setVoiRange(params);
const windowLevel = utilities.windowLevel.toWindowLevel(params.lower, params.upper);
// Set window level using the command manager directly to avoid circular calls
const displaySetInstanceUID = validateActiveDisplaySet();
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...windowLevel,
viewportId,
displaySetInstanceUID,
},
context: 'CORNERSTONE',
});
}
},
[viewportId, commandsManager, validateActiveDisplaySet]
);
const toggleColorbar = useCallback(
(options?: Partial<ColorbarOptions>) => {
if (!viewportId) {
return;
}
if (viewportDisplaySets?.length === 0) {
return;
}
if (!colorbarProperties) {
return;
}
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarInitialColormap,
} = colorbarProperties as ColorbarProperties;
let appropriateTickPosition = colorbarTickPosition || 'top';
if (colorbarPosition === 'left' || colorbarPosition === 'right') {
appropriateTickPosition = colorbarPosition === 'left' ? 'right' : 'left';
} else {
appropriateTickPosition = colorbarPosition === 'top' ? 'bottom' : 'top';
}
const colorbarOptions = {
viewportId,
colormaps: colorbarProperties.colormaps || {},
ticks: {
position: appropriateTickPosition,
},
width: colorbarWidth,
position: colorbarPosition,
activeColormapName: colorbarInitialColormap || 'Grayscale',
...options,
};
// If light background, adjust tick style but keep the appropriate position
if (isViewportBackgroundLight) {
colorbarOptions.ticks = {
position: appropriateTickPosition,
style: {
font: '13px Inter',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
const displaySetInstanceUIDs = viewportDisplaySets.map(ds => ds.displaySetInstanceUID);
try {
commandsManager.run('toggleViewportColorbar', {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
});
} catch (error) {
console.error('Error toggling colorbar:', error);
}
},
[
commandsManager,
viewportId,
colorbarProperties,
isViewportBackgroundLight,
viewportDisplaySets,
colorbarPosition,
]
);
const setColormap = useCallback(
({ colormap, opacity = 1, immediate = false }) => {
if (!viewportId) {
return;
}
const displaySetInstanceUID = validateActiveDisplaySet();
// Update local opacity state
if (opacity !== undefined) {
setOpacityState(opacity);
}
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
viewportId,
colormap,
displaySetInstanceUID,
opacity,
immediate,
},
context: 'CORNERSTONE',
});
},
[commandsManager, viewportId, validateActiveDisplaySet]
);
const setOpacity = useCallback(
(opacityValue: number) => {
if (!viewportId) {
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
return;
}
// Apply the actual opacity value
setOpacityState(opacityValue);
// Update the linear value for UI
setOpacityLinearState(opacityToLinear(opacityValue));
const displaySetInstanceUID = validateActiveDisplaySet();
const volumeIds = viewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
if (!volumeId) {
return;
}
// Get current properties including colormap
const properties = viewport.getProperties(volumeId);
const currentColormap = properties.colormap || {};
// Update colormap with new opacity
const updatedColormap = {
...currentColormap,
opacity: opacityValue,
};
// Apply updated colormap
viewport.setProperties(
{
colormap: updatedColormap,
},
volumeId
);
viewport.render();
},
[cornerstoneViewportService, viewportId, validateActiveDisplaySet, opacityToLinear]
);
const setOpacityLinear = useCallback(
(linearValue: number) => {
// Convert linear UI value to actual opacity value and apply it
const actualOpacity = linearToOpacity(linearValue);
setOpacity(actualOpacity);
},
[linearToOpacity, setOpacity]
);
const setThreshold = useCallback(
(thresholdValue: number) => {
if (!viewportId) {
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
return;
}
setThresholdState(thresholdValue);
const displaySetInstanceUID = validateActiveDisplaySet();
const volumeIds = viewport.getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
if (!volumeId) {
return;
}
console.debug('🚀 ~ thresholdValue:', thresholdValue);
viewport.setProperties(
{
colormap: {
threshold: thresholdValue,
},
},
volumeId
);
viewport.render();
},
[cornerstoneViewportService, viewportId, validateActiveDisplaySet]
);
// Get the current colormap for the active display set
const colormap = useMemo(() => {
if (!viewportId || !activeDisplaySetInstanceUID || !viewportDisplaySets?.length) {
return null;
}
try {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return null;
}
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return (
colorbarProperties?.colormaps?.find(c => c.Name === 'Grayscale') ||
colorbarProperties?.colormaps?.[0]
);
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(activeDisplaySetInstanceUID)
);
if (!actorEntry) {
return (
colorbarProperties?.colormaps?.find(c => c.Name === 'Grayscale') ||
colorbarProperties?.colormaps?.[0]
);
}
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(
actorEntry.referencedId
);
if (!colormap) {
return (
colorbarProperties?.colormaps?.find(c => c.Name === 'Grayscale') ||
colorbarProperties?.colormaps?.[0]
);
}
return colormap;
} catch (error) {
console.error('Error getting viewport colormap:', error);
return (
colorbarProperties?.colormaps?.find(c => c.Name === 'Grayscale') ||
colorbarProperties?.colormaps?.[0]
);
}
}, [
cornerstoneViewportService,
viewportId,
activeDisplaySetInstanceUID,
viewportDisplaySets,
colorbarProperties?.colormaps,
]);
// 3D volume rendering functions
const setVolumeRenderingPreset = useCallback(
(preset: any) => {
if (!viewportId) {
return;
}
const displaySetInstanceUID = validateActiveDisplaySet();
commandsManager.run({
commandName: 'setVolumesPreset',
commandOptions: {
preset,
viewportId,
displaySetInstanceUID,
},
});
},
[commandsManager, viewportId, validateActiveDisplaySet]
);
const setVolumeRenderingQuality = useCallback(
(quality: number) => {
if (!viewportId) {
return;
}
commandsManager.run({
commandName: 'setVolumeRenderingQuality',
commandOptions: {
quality,
viewportId,
},
});
},
[commandsManager, viewportId]
);
const setVolumeLighting = useCallback(
({ ambient, diffuse, specular }: VolumeLightingParams) => {
if (!viewportId) {
return;
}
commandsManager.run({
commandName: 'setVolumeLighting',
commandOptions: {
ambient,
diffuse,
specular,
viewportId,
},
});
},
[commandsManager, viewportId]
);
const setVolumeShading = useCallback(
(enabled: boolean) => {
if (!viewportId) {
return;
}
commandsManager.run({
commandName: 'setVolumeShading',
commandOptions: {
enabled,
viewportId,
},
});
},
[commandsManager, viewportId]
);
return {
is3DVolume,
isViewportBackgroundLight,
// Window level functions
setWindowLevel,
setVOIRange,
voiRange,
windowLevel: utilities.windowLevel.toWindowLevel(voiRange?.lower, voiRange?.upper),
// Colorbar functions
hasColorbar,
toggleColorbar,
// Colormap functions
setColormap,
colormap,
// Opacity functions
opacity,
setOpacity,
opacityLinear,
setOpacityLinear,
// Threshold functions
threshold,
setThreshold,
pixelValueRange,
// 3D volume rendering functions
setVolumeRenderingPreset,
setVolumeRenderingQuality,
setVolumeLighting,
setVolumeShading,
// Display sets
viewportDisplaySets,
// Colorbar properties
colorbarProperties,
colorbarPosition,
setColorbarPosition,
// Presets
windowLevelPresets: activeDisplaySetPresets,
allWindowLevelPresets,
volumeRenderingPresets,
volumeRenderingQualityRange,
};
}
export default useViewportRendering;
/**
* Helper function to compare two VOI ranges with a small tolerance.
* @param rangeA - The first VOI range { lower, upper }.
* @param rangeB - The second VOI range { lower, upper }.
* @param epsilon - The tolerance for comparison.
* @returns True if the ranges are considered close, false otherwise.
*/
function areVoiRangesClose(
rangeA: { lower: number; upper: number },
rangeB: { lower: number; upper: number },
epsilon = 0.001
): boolean {
if (!rangeA || !rangeB) {
return false;
}
return (
Math.abs(rangeA.lower - rangeB.lower) < epsilon &&
Math.abs(rangeA.upper - rangeB.upper) < epsilon
);
}
function getCustomizationData(customizationService) {
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.getCustomization(
'cornerstone.colorbar'
) as ColorbarProperties;
const {
volumeRenderingPresets = [],
volumeRenderingQualityRange = { min: 0, max: 1, step: 0.1 },
} =
(customizationService.getCustomization(
'cornerstone.3dVolumeRendering'
) as VolumeRenderingConfig) || {};
return {
presets,
colorbarProperties,
volumeRenderingPresets,
volumeRenderingQualityRange,
};
}

View File

@ -0,0 +1,221 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { roundNumber } from '@ohif/core/src/utils';
import {
SegmentationData,
SegmentationRepresentation,
} from '../services/SegmentationService/SegmentationService';
import { useSystem } from '@ohif/core';
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
const primary = [];
const indentation = ' '.repeat(indent);
for (const key in stats) {
if (Object.prototype.hasOwnProperty.call(stats, key)) {
const value = stats[key];
const readableText = readableTextMap?.[key];
if (!readableText) {
continue;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Add empty row before category (except for the first category)
if (primary.length > 0) {
primary.push('');
}
// Add category title
primary.push(`${indentation}${readableText}`);
// Recursively handle nested objects
primary.push(...mapStatsToDisplay(value, indent + 1));
} else {
// For non-nested values, don't add empty rows
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
}
}
}
return primary;
}
// Get customization for display text mapping
const displayTextMapper = segment => {
const defaultDisplay = {
primary: [],
secondary: [],
};
// If the segment has cachedStats, map it to readable text
if (segment.cachedStats) {
const primary = mapStatsToDisplay(segment.cachedStats);
defaultDisplay.primary = primary;
}
return defaultDisplay;
};
const updatedSegments = {};
Object.entries(segments).forEach(([segmentIndex, segment]) => {
updatedSegments[segmentIndex] = {
...segment,
displayText: displayTextMapper(segment),
};
});
// Map the segments and apply the display text mapper
return {
...segmentation,
label,
segments: updatedSegments,
};
}
/**
* Represents the combination of segmentation data and its representation in a viewport.
*/
type ViewportSegmentationRepresentation = {
segmentationsWithRepresentations: {
representation: SegmentationRepresentation;
segmentation: SegmentationData;
}[];
disabled: boolean;
};
/**
* Custom hook that provides segmentation data and their representations for the active viewport.
* @param options - The options object.
* @param options.servicesManager - The services manager object.
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
* @param options.debounceTime - Debounce time in milliseconds for updates.
* @returns An array of segmentation data and their representations for the active viewport.
*/
export function useViewportSegmentations({
viewportId,
subscribeToDataModified = false,
debounceTime = 0,
}: {
viewportId: string;
subscribeToDataModified?: boolean;
debounceTime?: number;
}): ViewportSegmentationRepresentation {
const { servicesManager } = useSystem();
const { segmentationService, viewportGridService, customizationService, displaySetService } =
servicesManager.services;
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
useState<ViewportSegmentationRepresentation>({
segmentationsWithRepresentations: [],
disabled: false,
});
useEffect(() => {
const update = () => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
if (!displaySet) {
return;
}
if (excludedModalities.includes(displaySet.Modality)) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: true,
}));
return;
}
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: false,
}));
return;
}
const representations = segmentationService.getSegmentationRepresentations(viewportId);
const newSegmentationsWithRepresentations = representations.map(representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
});
setSegmentationsWithRepresentations({
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
disabled: false,
});
};
const debouncedUpdate =
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
update();
const subscriptions = [
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
debouncedUpdate
),
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
debouncedUpdate
),
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
];
if (subscribeToDataModified) {
subscriptions.push(
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
debouncedUpdate
)
);
}
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
if (debounceTime > 0) {
debouncedUpdate.cancel();
}
};
}, [
segmentationService,
viewportGridService,
customizationService,
displaySetService,
debounceTime,
subscribeToDataModified,
viewportId,
]);
return segmentationsWithRepresentations;
}

View File

@ -34,7 +34,6 @@ import PlanarFreehandROI from './utils/measurementServiceMappings/PlanarFreehand
import RectangleROI from './utils/measurementServiceMappings/RectangleROI';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { findNearbyToolData } from './utils/findNearbyToolData';
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
@ -56,6 +55,7 @@ import { useSegmentations } from './hooks/useSegmentations';
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import utils from './utils';
import { useMeasurementTracking } from './hooks/useMeasurementTracking';
export * from './components';
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
@ -158,7 +158,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
servicesManager.registerService(SyncGroupService.REGISTRATION);
servicesManager.registerService(SegmentationService.REGISTRATION);
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
servicesManager.registerService(ColorbarService.REGISTRATION);
const { syncGroupService } = servicesManager.services;
@ -262,6 +261,8 @@ export {
StudySummaryFromMetadata,
CornerstoneViewportDownloadForm,
utils,
OHIFCornerstoneViewport,
useMeasurementTracking,
};
// Export constants

View File

@ -98,8 +98,13 @@ export default async function init({
measurementService,
colorbarService,
displaySetService,
toolbarService,
} = servicesManager.services;
toolbarService.registerEventForToolbarUpdate(colorbarService, [
colorbarService.EVENTS.STATE_CHANGED,
]);
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;

View File

@ -1,5 +1,4 @@
import React from 'react';
import { useSystem } from '@ohif/core';
import { useMeasurements } from '../hooks/useMeasurements';
import StudyMeasurements from '../components/StudyMeasurements';
@ -12,7 +11,7 @@ import StudyMeasurements from '../components/StudyMeasurements';
*
* Creates a default study measurements panel with default children:
* ```
* <PanelMEasurement>
* <PanelMeasurement>
* <StudyMeasurements />
* </PanelMeasurement>
* ```
@ -60,10 +59,7 @@ import StudyMeasurements from '../components/StudyMeasurements';
export default function PanelMeasurement(props): React.ReactNode {
const { measurementFilter, emptyComponent: EmptyComponent, children } = props;
const system = useSystem();
const displayMeasurements = useMeasurements(system.servicesManager, {
measurementFilter,
});
const displayMeasurements = useMeasurements({ measurementFilter });
if (!displayMeasurements.length) {
return EmptyComponent ? (

View File

@ -9,9 +9,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
const { customizationService, displaySetService } = servicesManager.services;
const { segmentationsWithRepresentations, disabled } =
useActiveViewportSegmentationRepresentations({
servicesManager,
});
useActiveViewportSegmentationRepresentations();
// Extract customization options
const segmentationTableMode = customizationService.getCustomization(

View File

@ -291,7 +291,7 @@ class SegmentationService extends PubSubService {
const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
csViewport,
segmentation
);
) || { isVolumeViewport: false, isVolumeSegmentation: false };
({ representationTypeToUse, isConverted } = await this.handleViewportConversion(
isVolumeViewport,
@ -677,7 +677,6 @@ class SegmentationService extends PubSubService {
// Mark the RT display set as loaded
rtDisplaySet.isLoaded = true;
// Add or update the segmentation in the state
this.addOrUpdateSegmentation(segmentation);

View File

@ -1,160 +0,0 @@
import { PubSubService } from '@ohif/core';
import { ViewportActionCornersLocations, Types } from '@ohif/ui-next';
class ViewportActionCornersService extends PubSubService {
public static readonly EVENTS = {};
public static readonly LOCATIONS = ViewportActionCornersLocations;
public static REGISTRATION = {
name: 'viewportActionCornersService',
altName: 'ViewportActionCornersService',
create: ({ configuration = {} }) => {
return new ViewportActionCornersService();
},
};
serviceImplementation = {};
public LOCATIONS = ViewportActionCornersService.LOCATIONS;
constructor() {
super(ViewportActionCornersService.EVENTS);
this.serviceImplementation = {};
}
public setServiceImplementation({
getState: getStateImplementation,
addComponent: addComponentImplementation,
addComponents: addComponentsImplementation,
clear: clearImplementation,
lockItem: lockItemImplementation,
unlockItem: unlockItemImplementation,
toggleLock: toggleLockImplementation,
isItemLocked: isItemLockedImplementation,
showItem: showItemImplementation,
hideItem: hideItemImplementation,
toggleVisibility: toggleVisibilityImplementation,
isItemVisible: isItemVisibleImplementation,
openItem: openItemImplementation,
closeItem: closeItemImplementation,
closeAllItems: closeAllItemsImplementation,
isItemOpen: isItemOpenImplementation,
}): void {
this.serviceImplementation._getState =
getStateImplementation ?? this.serviceImplementation._getState;
this.serviceImplementation._addComponent =
addComponentImplementation ?? this.serviceImplementation._addComponent;
this.serviceImplementation._addComponents =
addComponentsImplementation ?? this.serviceImplementation._addComponents;
this.serviceImplementation._clear = clearImplementation ?? this.serviceImplementation._clear;
this.serviceImplementation._lockItem =
lockItemImplementation ?? this.serviceImplementation._lockItem;
this.serviceImplementation._unlockItem =
unlockItemImplementation ?? this.serviceImplementation._unlockItem;
this.serviceImplementation._toggleLock =
toggleLockImplementation ?? this.serviceImplementation._toggleLock;
this.serviceImplementation._isItemLocked =
isItemLockedImplementation ?? this.serviceImplementation._isItemLocked;
this.serviceImplementation._showItem =
showItemImplementation ?? this.serviceImplementation._showItem;
this.serviceImplementation._hideItem =
hideItemImplementation ?? this.serviceImplementation._hideItem;
this.serviceImplementation._toggleVisibility =
toggleVisibilityImplementation ?? this.serviceImplementation._toggleVisibility;
this.serviceImplementation._isItemVisible =
isItemVisibleImplementation ?? this.serviceImplementation._isItemVisible;
this.serviceImplementation._openItem =
openItemImplementation ?? this.serviceImplementation._openItem;
this.serviceImplementation._closeItem =
closeItemImplementation ?? this.serviceImplementation._closeItem;
this.serviceImplementation._closeAllItems =
closeAllItemsImplementation ?? this.serviceImplementation._closeAllItems;
this.serviceImplementation._isItemOpen =
isItemOpenImplementation ?? this.serviceImplementation._isItemOpen;
}
public getState() {
return this.serviceImplementation._getState();
}
public addComponent(component: Types.ActionComponentInfo) {
this.serviceImplementation._addComponent(component);
}
public addComponents(components: Array<Types.ActionComponentInfo>) {
this.serviceImplementation._addComponents(components);
}
public clear(viewportId: string): void {
this.serviceImplementation._clear(viewportId);
}
/* lock / unlock */
public lockItem(viewportId: string, itemId: string): void {
this.serviceImplementation._lockItem(viewportId, itemId);
}
public unlockItem(viewportId: string, itemId: string): void {
this.serviceImplementation._unlockItem(viewportId, itemId);
}
public toggleLock(viewportId: string, itemId: string): void {
this.serviceImplementation._toggleLock(viewportId, itemId);
}
public isItemLocked(viewportId: string, itemId: string): boolean {
return this.serviceImplementation._isItemLocked(viewportId, itemId);
}
/* visibility */
public showItem(viewportId: string, itemId: string): void {
this.serviceImplementation._showItem(viewportId, itemId);
}
public hideItem(viewportId: string, itemId: string): void {
this.serviceImplementation._hideItem(viewportId, itemId);
}
public toggleVisibility(viewportId: string, itemId: string): void {
this.serviceImplementation._toggleVisibility(viewportId, itemId);
}
public isItemVisible(viewportId: string, itemId: string): boolean {
return this.serviceImplementation._isItemVisible(viewportId, itemId);
}
/* open / close */
public openItem(viewportId: string, itemId: string): void {
this.serviceImplementation._openItem(viewportId, itemId);
}
public closeItem(viewportId: string, itemId: string): void {
this.serviceImplementation._closeItem(viewportId, itemId);
}
public closeAllItems(viewportId: string): void {
this.serviceImplementation._closeAllItems(viewportId);
}
public isItemOpen(viewportId: string, itemId: string): boolean {
return this.serviceImplementation._isItemOpen(viewportId, itemId);
}
public getAlignAndSide(location: ViewportActionCornersLocations): Types.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;

View File

@ -16,7 +16,11 @@ import {
import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import ViewportInfo, {
DisplaySetOptions,
PublicViewportOptions,
ViewportOptions,
} from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import {
LutPresentation,
@ -479,6 +483,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
public getViewportOptions(viewportId: string): ViewportOptions {
return this.viewportsById.get(viewportId).getViewportOptions();
}
/**
* Retrieves the Cornerstone viewport with the specified ID.
*

View File

@ -4,7 +4,6 @@ import CornerstoneViewportServiceType from '../services/ViewportService/Cornerst
import SegmentationServiceType from '../services/SegmentationService';
import SyncGroupServiceType from '../services/SyncGroupService';
import ToolGroupServiceType from '../services/ToolGroupService';
import ViewportActionCornersServiceType from '../services/ViewportActionCornersService/ViewportActionCornersService';
import ColorbarServiceType from '../services/ColorbarService';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
@ -15,7 +14,7 @@ import type {
SegmentationRepresentation as SegRep,
SegmentationInfo as SegInfo,
} from '../services/SegmentationService/SegmentationService';
import type { ViewportOptions as ViewportOpts } from '../services/ViewportService/Viewport';
declare global {
namespace AppTypes {
export type CornerstoneCacheService = CornerstoneCacheServiceType;
@ -23,7 +22,6 @@ declare global {
export type SegmentationService = SegmentationServiceType;
export type SyncGroupService = SyncGroupServiceType;
export type ToolGroupService = ToolGroupServiceType;
export type ViewportActionCornersService = ViewportActionCornersServiceType;
export type ColorbarService = ColorbarServiceType;
export interface Services {
@ -32,7 +30,6 @@ declare global {
syncGroupService?: SyncGroupServiceType;
segmentationService?: SegmentationServiceType;
cornerstoneCacheService?: CornerstoneCacheServiceType;
viewportActionCornersService?: ViewportActionCornersServiceType;
colorbarService?: ColorbarServiceType;
}
@ -54,5 +51,7 @@ declare global {
cornerstone?: typeof cornerstone;
cornerstoneTools?: typeof cornerstoneTools;
}
export type ViewportOptions = ViewportOpts;
}
}

View File

@ -4,7 +4,6 @@ import SyncGroupService from '../services/SyncGroupService';
import SegmentationService from '../services/SegmentationService';
import CornerstoneCacheService from '../services/CornerstoneCacheService';
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
import ColorbarService from '../services/ColorbarService';
interface CornerstoneServices extends Types.Services {
@ -13,7 +12,6 @@ interface CornerstoneServices extends Types.Services {
syncGroupService: SyncGroupService;
segmentationService: SegmentationService;
cornerstoneCacheService: CornerstoneCacheService;
viewportActionCornersService: ViewportActionCornersService;
colorbarService: ColorbarService;
}

View File

@ -0,0 +1,7 @@
interface VolumeLightingParams {
ambient: number;
diffuse: number;
specular: number;
}
export type { VolumeLightingParams };

View File

@ -0,0 +1,8 @@
export interface VolumeRenderingConfig {
volumeRenderingPresets?: Array<any>;
volumeRenderingQualityRange?: {
min: number;
max: number;
step: number;
};
}

View File

@ -1,10 +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 HydrationCallback } from '../utils/promptHydrationDialog';
export type { CornerstoneCacheService, CornerstoneServices };
export * from './Presentation';
export * from './ViewportPresets';
export * from './WindowLevel';
export * from './VolumeRenderingConfig';
export * from './VolumeLighting';

View File

@ -4,12 +4,20 @@ import {
setupSegmentationDataModifiedHandler,
setupSegmentationModifiedHandler,
} from './segmentationHandlers';
import promptHydrationDialog, {
HydrationDialogProps,
HydrationCallback,
HydrationSRResult,
} from './promptHydrationDialog';
const utils = {
handleSegmentChange,
isReferenceViewable,
setupSegmentationDataModifiedHandler,
setupSegmentationModifiedHandler,
promptHydrationDialog,
};
export type { HydrationDialogProps, HydrationCallback, HydrationSRResult };
export default utils;

View File

@ -26,7 +26,7 @@ const Length = {
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
const colorString = config.style.getStyleProperty('color', { annotationUID });
// const colorString = config.style.getStyleProperty('color', { annotationUID });
// color string is like 'rgb(255, 255, 255)' we need them to be in RGBA array [255, 255, 255, 255]
// Todo: this should be in a utility

View File

@ -21,7 +21,7 @@ const LivewireContour = {
*/
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
@ -43,16 +43,16 @@ const LivewireContour = {
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } =
getSOPInstanceAttributes(referencedImageId);
getSOPInstanceAttributes(referencedImageId, displaySetService, annotation);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
return {
@ -64,13 +64,14 @@ const LivewireContour = {
metadata,
frameNumber,
referenceSeriesUID: SeriesInstanceUID,
referencedImageId,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
isLocked,
isVisible,
displayText: getDisplayText(annotation, displaySet),
displayText: getDisplayText(annotation, displaySet, displaySetService),
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport: () => getColumnValueReport(annotation, customizationService),
@ -122,20 +123,27 @@ function getColumnValueReport(annotation, customizationService) {
*
* @param {Object} annotation - The annotation object.
* @param {Object} displaySet - The display set object.
* @returns {string[]} - An array of display text.
* @returns {object} - An object with primary and secondary text arrays.
*/
function getDisplayText(annotation, displaySet) {
function getDisplayText(annotation, displaySet, displaySetService) {
const { metadata, data } = annotation;
const displayText = {
primary: [],
secondary: [],
};
if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
return [];
return displayText;
}
const { area, areaUnit } = data.cachedStats[`imageId:${metadata.referencedImageId}`];
const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId);
const displayText = [];
const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(
metadata.referencedImageId,
displaySetService,
annotation
);
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber;
@ -152,21 +160,15 @@ function getDisplayText(annotation, displaySet) {
seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
}
const texts = [];
if (area) {
const roundedArea = utils.roundNumber(area || 0, 2);
texts.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
}
if (seriesText) {
texts.push(seriesText);
displayText.secondary.push(seriesText);
}
displayText.push({
text: texts,
series: seriesText,
});
return displayText;
}

View File

@ -0,0 +1,189 @@
export type HydrationCallback = (params: any) => Promise<boolean>;
export const HydrationType = {
SEG: 'SEG',
SR: 'SR',
RTSTRUCT: 'RTSTRUCT',
} as const;
export interface HydrationDialogProps {
servicesManager: AppTypes.ServicesManager;
viewportId: string;
displaySet: AppTypes.DisplaySet;
preHydrateCallbacks?: HydrationCallback[];
hydrateCallback: HydrationCallback;
type: string;
}
export interface HydrationSRResult {
userResponse: number;
displaySetInstanceUID: string;
srSeriesInstanceUID: string;
viewportId: string;
StudyInstanceUID?: string;
SeriesInstanceUIDs?: string[];
}
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
CREATE_REPORT: 1,
ADD_SERIES: 2,
SET_STUDY_AND_SERIES: 3,
NO_NOT_FOR_SERIES: 4,
HYDRATE: 5,
};
function getCustomizationMessageKey(type: string): string {
switch (type) {
case HydrationType.RTSTRUCT:
return 'viewportNotification.hydrateRTMessage';
case HydrationType.SEG:
return 'viewportNotification.hydrateSEGMessage';
case HydrationType.SR:
return 'viewportNotification.hydrateSRMessage';
default:
return 'viewportNotification.hydrateMessage';
}
}
function getDialogId(type: string): string {
switch (type) {
case HydrationType.RTSS:
return 'promptHydrateRT';
case HydrationType.SEG:
return 'promptHydrateSEG';
case HydrationType.SR:
return 'promptHydrateSR';
default:
return 'promptHydrate';
}
}
function promptHydrationDialog({
servicesManager,
viewportId,
displaySet,
preHydrateCallbacks = [],
hydrateCallback,
type,
}: HydrationDialogProps): Promise<boolean | HydrationSRResult> {
const { uiViewportDialogService, customizationService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
// Todo: make this use enum from the extension, we should move the enum
const standardMode = appConfig?.measurementTrackingMode === 'standard';
return new Promise(async function (resolve, reject) {
// For RT and SEG, we check disableConfirmationPrompts
// For SR, we check if standardMode is true
const shouldPrompt =
type === HydrationType.SR ? standardMode : !appConfig?.disableConfirmationPrompts;
const promptResult = shouldPrompt
? await _askHydrate(uiViewportDialogService, customizationService, viewportId, type)
: RESPONSE.HYDRATE;
if (promptResult === RESPONSE.HYDRATE) {
// Execute preHydrate callbacks
preHydrateCallbacks?.forEach(callback => {
callback();
});
if (type === HydrationType.SEG) {
// SEG needs setTimeout
window.setTimeout(async () => {
const isHydrated = await hydrateCallback({
segDisplaySet: displaySet,
viewportId,
});
resolve(isHydrated);
}, 0);
} else if (type === HydrationType.RTSTRUCT) {
// RT hydration
const isHydrated = await hydrateCallback({
rtDisplaySet: displaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
} else if (type === HydrationType.SR) {
// SR has a different result structure
const hydrationResult = await hydrateCallback(displaySet);
resolve({
userResponse: promptResult,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
srSeriesInstanceUID: displaySet.SeriesInstanceUID,
viewportId,
StudyInstanceUID: hydrationResult?.StudyInstanceUID,
SeriesInstanceUIDs: hydrationResult?.SeriesInstanceUIDs,
});
}
} else {
if (type === HydrationType.SR) {
resolve({
userResponse: promptResult,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
srSeriesInstanceUID: displaySet.SeriesInstanceUID,
viewportId,
});
} else {
resolve(false);
}
}
});
}
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId: string,
type: string
) {
return new Promise(function (resolve, reject) {
const messageKey = getCustomizationMessageKey(type);
const message = customizationService.getCustomization(messageKey);
const actions = [
{
id: 'no-hydrate',
type: 'secondary',
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: 'primary',
text: 'Yes',
value: RESPONSE.HYDRATE,
},
];
const onSubmit = result => {
uiViewportDialogService.hide();
resolve(result);
};
uiViewportDialogService.show({
id: getDialogId(type),
viewportId,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE);
}
},
});
});
}
export default promptHydrationDialog;

View File

@ -2,18 +2,17 @@ import SegmentationServiceType from '../services/SegmentationService';
export const handleSegmentChange = ({
direction,
segDisplaySet,
segmentationId,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
}: {
direction: number;
segDisplaySet: AppTypes.DisplaySet;
segmentationId: string;
viewportId: string;
selectedSegmentObjectIndex: number;
segmentationService: SegmentationServiceType;
}) => {
const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
const { segments } = segmentation;

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
import { ProgressDropdown } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
const workflowStepsToDropdownOptions = (steps = []) =>
steps.map(step => ({
@ -10,7 +11,8 @@ const workflowStepsToDropdownOptions = (steps = []) =>
completed: false,
}));
export function ProgressDropdownWithService({ servicesManager }: withAppTypes): ReactElement {
export function ProgressDropdownWithService(): ReactElement {
const { servicesManager } = useSystem();
const { workflowStepsService } = servicesManager.services;
const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id);

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useImageViewer } from '@ohif/ui-next';
import { useSystem, utils } from '@ohif/core';
import { useNavigate } from 'react-router-dom';
@ -31,6 +31,7 @@ function PanelStudyBrowser({
const internalImageViewer = useImageViewer();
const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs;
const fetchedStudiesRef = useRef(new Set());
const [{ activeViewportId, viewports, isHangingProtocolLayout }] = useViewportGrid();
const [activeTabName, setActiveTabName] = useState(studyMode);
@ -105,6 +106,13 @@ function PanelStudyBrowser({
useEffect(() => {
// Fetch all studies for the patient in each primary study
async function fetchStudiesForPatient(StudyInstanceUID) {
// Skip fetching if we've already fetched this study
if (fetchedStudiesRef.current.has(StudyInstanceUID)) {
return;
}
fetchedStudiesRef.current.add(StudyInstanceUID);
// current study qido
const qidoForStudyUID = await dataSource.query.studies.search({
studyInstanceUid: StudyInstanceUID,

View File

@ -8,12 +8,12 @@ import { useToolbar } from '@ohif/core/src/hooks/useToolbar';
* @param props - Component props
* @returns Component
*/
export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props }) {
export function ToolBoxButtonGroupWrapper({ buttonSection, id }) {
const { onInteraction, toolbarButtons } = useToolbar({
buttonSection,
});
if (!groupId) {
if (!buttonSection) {
return null;
}
@ -30,7 +30,7 @@ export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props })
onInteraction={event => {
onInteraction?.({
event,
groupId,
id,
commands: item.commands,
itemId: item.id,
item,

View File

@ -10,13 +10,9 @@ import {
import { useToolbar } from '@ohif/core/src';
interface ToolButtonListWrapperProps {
groupId: string;
buttonSection: string;
onInteraction?: (details: {
groupId: string;
itemId: string;
commands?: Record<string, unknown>;
}) => void;
onInteraction?: (details: { itemId: string; commands?: Record<string, unknown> }) => void;
id: string;
}
/**
@ -25,10 +21,7 @@ interface ToolButtonListWrapperProps {
* @returns Component
* // test
*/
export default function ToolButtonListWrapper({
groupId,
buttonSection,
}: ToolButtonListWrapperProps) {
export default function ToolButtonListWrapper({ buttonSection, id }: ToolButtonListWrapperProps) {
const { onInteraction, toolbarButtons } = useToolbar({
buttonSection,
});
@ -47,21 +40,21 @@ export default function ToolButtonListWrapper({
<ToolButtonList>
<ToolButtonListDefault>
<div
data-cy={`${groupId}-split-button-primary`}
data-cy={`${id}-split-button-primary`}
data-tool={primary.id}
data-active={primary.isActive}
>
<ToolButton
{...primary}
onInteraction={({ itemId }) =>
onInteraction?.({ groupId, itemId, commands: primary.commands })
onInteraction?.({ id, itemId, commands: primary.commands })
}
className={primary.className}
/>
</div>
</ToolButtonListDefault>
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
<div data-cy={`${groupId}-split-button-secondary`}>
<div data-cy={`${id}-split-button-secondary`}>
<ToolButtonListDropDown>
{items.map(item => {
return (
@ -71,9 +64,7 @@ export default function ToolButtonListWrapper({
data-cy={item.id}
data-tool={item.id}
data-active={item.isActive}
onSelect={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
onSelect={() => onInteraction?.({ id, itemId: item.id, commands: item.commands })}
>
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
</ToolButtonListItem>

View File

@ -0,0 +1,32 @@
import React from 'react';
import { useIconPresentation, Icons, Button } from '@ohif/ui-next';
export default function ToolButtonWrapper(props) {
const { IconContainer, containerProps } = useIconPresentation();
const Icon = <Icons.ByName name={props.icon} />;
return (
<div>
{IconContainer ? (
<IconContainer
disabled={props.disabled}
{...props}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={props.disabled}
>
{Icon}
</Button>
)}
</div>
);
}
export { ToolButtonWrapper };

View File

@ -0,0 +1,41 @@
import React from 'react';
import { useToolbar, ButtonLocation } from '@ohif/core';
interface ToolRowWrapperProps {
buttonSection: string;
className?: string;
show?: boolean;
}
function ToolRowWrapper({ buttonSection, className = '', show = true }: ToolRowWrapperProps) {
const { onInteraction, toolbarButtons } = useToolbar({
buttonSection,
});
// No need for debugger statement
if (!toolbarButtons?.length) {
return null;
}
return (
<div className={`space-x-1} flex flex-row items-center ${className}`}>
{toolbarButtons.map((button, index) => {
const { id, Component, componentProps } = button;
return (
<div
key={id || index}
className="flex-shrink-0"
>
<Component
{...componentProps}
onInteraction={onInteraction}
location={componentProps.location || buttonSection}
/>
</div>
);
})}
</div>
);
}
export default ToolRowWrapper;

View File

@ -1,9 +1,22 @@
import React from 'react';
import { useToolbar } from '@ohif/core';
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
const { toolbarButtons, onInteraction } = useToolbar({
servicesManager,
interface ToolbarProps {
buttonSection?: string;
viewportId?: string;
location?: number;
}
export function Toolbar({ buttonSection = 'primary', viewportId, location }: ToolbarProps) {
const {
toolbarButtons,
onInteraction,
isItemOpen,
isItemLocked,
openItem,
closeItem,
toggleLock,
} = useToolbar({
buttonSection,
});
@ -19,13 +32,31 @@ export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
}
const { id, Component, componentProps } = toolDef;
// Enhanced props with state and actions - respecting viewport specificity
const enhancedProps = {
...componentProps,
isOpen: isItemOpen(id, viewportId),
isLocked: isItemLocked(id, viewportId),
onOpen: () => openItem(id, viewportId),
onClose: () => closeItem(id, viewportId),
onToggleLock: () => toggleLock(id, viewportId),
viewportId,
};
const tool = (
<Component
key={id}
id={id}
onInteraction={onInteraction}
servicesManager={servicesManager}
{...componentProps}
location={location}
onInteraction={args => {
onInteraction({
...args,
itemId: id,
viewportId,
});
}}
{...enhancedProps}
/>
);

View File

@ -1,37 +0,0 @@
import React, { useCallback } from 'react';
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) {
const getSplitButtonItems = useCallback(
items =>
items.map((item, index) => (
<ToolbarButton
key={item.id}
icon={item.icon}
label={item.label}
disabled={item.disabled}
className={item.className}
disabledText={item.disabledText}
id={item.id}
size={size}
onClick={() => {
onInteraction({
groupId,
itemId: item.id,
commands: item.commands,
item,
});
}}
// Note: this is necessary since tooltip will add
// default styles to the tooltip container which
// we don't want for groups
toolTipClassName=""
/>
)),
[onInteraction, groupId]
);
return <ButtonGroup>{getSplitButtonItems(items)}</ButtonGroup>;
}
export default ToolbarButtonGroupWithServices;

View File

@ -1,89 +0,0 @@
import { SplitButton, ToolbarButton } from '@ohif/ui';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
function ToolbarSplitButtonWithServices({
groupId,
primary,
secondary,
items,
renderer,
onInteraction,
servicesManager,
}: withAppTypes) {
const { toolbarService } = servicesManager?.services;
/* Bubbles up individual item clicks */
const getSplitButtonItems = useCallback(
items =>
items.map((item, index) => ({
...item,
index,
onClick: () => {
onInteraction({
groupId,
itemId: item.id,
item,
});
},
})),
[groupId, onInteraction]
);
const PrimaryButtonComponent =
toolbarService?.getButtonComponentForUIType(primary.uiType) ?? ToolbarButton;
const listItemRenderer = renderer;
return (
<SplitButton
primary={primary}
secondary={secondary}
items={getSplitButtonItems(items)}
groupId={groupId}
renderer={listItemRenderer}
onInteraction={onInteraction}
Component={props => (
<PrimaryButtonComponent
{...props}
servicesManager={servicesManager}
/>
)}
/>
);
}
ToolbarSplitButtonWithServices.propTypes = {
groupId: PropTypes.string,
primary: PropTypes.shape({
id: PropTypes.string.isRequired,
uiType: PropTypes.string,
}),
secondary: PropTypes.shape({
id: PropTypes.string,
icon: PropTypes.string.isRequired,
label: PropTypes.string,
tooltip: PropTypes.string.isRequired,
disabled: PropTypes.bool,
className: PropTypes.string,
}),
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
tooltip: PropTypes.string,
disabled: PropTypes.bool,
className: PropTypes.string,
})
),
renderer: PropTypes.func,
onInteraction: PropTypes.func.isRequired,
servicesManager: PropTypes.shape({
services: PropTypes.shape({
toolbarService: PropTypes.object,
}),
}),
};
export default ToolbarSplitButtonWithServices;

View File

@ -0,0 +1,7 @@
export * from './Toolbar';
export * from './ToolButtonWrapper';
export * from './ToolButtonListWrapper';
export * from './ToolRowWrapper';
export * from './ToolBoxWrapper';
export * from './ToolbarDivider';
export * from './ToolbarLayoutSelector';

Some files were not shown because too many files have changed in this diff Show More