feat(actions): simplify action corner api through the toolbarService (#5033)
This commit is contained in:
parent
f71b88e639
commit
7c15bb8901
288
bun.lock
288
bun.lock
@ -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=="],
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './AdvancedRenderingControls';
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ModalityLoadBadge';
|
||||
@ -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;
|
||||
@ -0,0 +1,3 @@
|
||||
import NavigationComponent from './NavigationComponent';
|
||||
|
||||
export default NavigationComponent;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export { OpacityMenuWrapper } from './OpacityMenuWrapper';
|
||||
export { default as OpacityMenu } from './OpacityMenu';
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export { ThresholdMenuWrapper } from './ThresholdMenuWrapper';
|
||||
export { default as ThresholdMenu } from './ThresholdMenu';
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './TrackingStatus';
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export { VOIManualControlMenuWrapper } from './VOIManualControlMenuWrapper';
|
||||
export { default as VOIManualControlMenu } from './VOIManualControlMenu';
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
@ -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 => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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';
|
||||
@ -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 => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
@ -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 };
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const MENU_IDS = {
|
||||
ORIENTATION_MENU: 'orientationMenu',
|
||||
DATA_OVERLAY_MENU: 'dataOverlayMenu',
|
||||
WINDOW_LEVEL_MENU: 'windowLevelActionMenu',
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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': [],
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 };
|
||||
|
||||
132
extensions/cornerstone/src/hooks/useMeasurementTracking.ts
Normal file
132
extensions/cornerstone/src/hooks/useMeasurementTracking.ts
Normal 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;
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
77
extensions/cornerstone/src/hooks/useViewportHover.ts
Normal file
77
extensions/cornerstone/src/hooks/useViewportHover.ts
Normal 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]);
|
||||
}
|
||||
882
extensions/cornerstone/src/hooks/useViewportRendering.tsx
Normal file
882
extensions/cornerstone/src/hooks/useViewportRendering.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
221
extensions/cornerstone/src/hooks/useViewportSegmentations.ts
Normal file
221
extensions/cornerstone/src/hooks/useViewportSegmentations.ts
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
7
extensions/cornerstone/src/types/VolumeLighting.ts
Normal file
7
extensions/cornerstone/src/types/VolumeLighting.ts
Normal file
@ -0,0 +1,7 @@
|
||||
interface VolumeLightingParams {
|
||||
ambient: number;
|
||||
diffuse: number;
|
||||
specular: number;
|
||||
}
|
||||
|
||||
export type { VolumeLightingParams };
|
||||
@ -0,0 +1,8 @@
|
||||
export interface VolumeRenderingConfig {
|
||||
volumeRenderingPresets?: Array<any>;
|
||||
volumeRenderingQualityRange?: {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
};
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
189
extensions/cornerstone/src/utils/promptHydrationDialog.ts
Normal file
189
extensions/cornerstone/src/utils/promptHydrationDialog.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
32
extensions/default/src/Toolbar/ToolButtonWrapper.tsx
Normal file
32
extensions/default/src/Toolbar/ToolButtonWrapper.tsx
Normal 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 };
|
||||
41
extensions/default/src/Toolbar/ToolRowWrapper.tsx
Normal file
41
extensions/default/src/Toolbar/ToolRowWrapper.tsx
Normal 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;
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
7
extensions/default/src/Toolbar/index.ts
Normal file
7
extensions/default/src/Toolbar/index.ts
Normal 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
Loading…
Reference in New Issue
Block a user