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": {
|
"optionalDependencies": {
|
||||||
"@percy/cypress": "^3.1.1",
|
"@percy/cypress": "^3.1.1",
|
||||||
"@playwright/test": "^1.48.0",
|
"@playwright/test": "^1.48.0",
|
||||||
"cypress": "^14.1.0",
|
"cypress": "14.3.1",
|
||||||
"cypress-file-upload": "^5.0.8",
|
"cypress-file-upload": "^5.0.8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"addOns/externals/devDependencies": {
|
"addOns/externals/devDependencies": {
|
||||||
"name": "@externals/devDependencies",
|
"name": "@externals/devDependencies",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
@ -127,22 +127,22 @@
|
|||||||
},
|
},
|
||||||
"addOns/externals/dicom-microscopy-viewer": {
|
"addOns/externals/dicom-microscopy-viewer": {
|
||||||
"name": "@externals/dicom-microscopy-viewer",
|
"name": "@externals/dicom-microscopy-viewer",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dicom-microscopy-viewer": "^0.46.1",
|
"dicom-microscopy-viewer": "^0.46.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"extensions/cornerstone": {
|
"extensions/cornerstone": {
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/ai": "^3.12.2",
|
"@cornerstonejs/ai": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/labelmap-interpolation": "^3.12.2",
|
"@cornerstonejs/labelmap-interpolation": "^3.15.1",
|
||||||
"@cornerstonejs/polymorphic-segmentation": "^3.12.2",
|
"@cornerstonejs/polymorphic-segmentation": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@ -159,9 +159,9 @@
|
|||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@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.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -173,19 +173,19 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-pmap": {
|
"extensions/cornerstone-dicom-pmap": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -196,16 +196,16 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-rt": {
|
"extensions/cornerstone-dicom-rt": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -216,19 +216,19 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-seg": {
|
"extensions/cornerstone-dicom-seg": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -239,19 +239,19 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-sr": {
|
"extensions/cornerstone-dicom-sr": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
|
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -261,19 +261,19 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dynamic-volume": {
|
"extensions/cornerstone-dynamic-volume": {
|
||||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/default": {
|
"extensions/default": {
|
||||||
"name": "@ohif/extension-default",
|
"name": "@ohif/extension-default",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/calculate-suv": "^1.1.0",
|
"@cornerstonejs/calculate-suv": "^1.1.0",
|
||||||
@ -291,8 +291,8 @@
|
|||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicomweb-client": "^0.10.4",
|
"dicomweb-client": "^0.10.4",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -306,7 +306,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-microscopy": {
|
"extensions/dicom-microscopy": {
|
||||||
"name": "@ohif/extension-dicom-microscopy",
|
"name": "@ohif/extension-dicom-microscopy",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||||
@ -317,10 +317,10 @@
|
|||||||
"mathjs": "^12.4.2",
|
"mathjs": "^12.4.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -331,14 +331,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-pdf": {
|
"extensions/dicom-pdf": {
|
||||||
"name": "@ohif/extension-dicom-pdf",
|
"name": "@ohif/extension-dicom-pdf",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -348,14 +348,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-video": {
|
"extensions/dicom-video": {
|
||||||
"name": "@ohif/extension-dicom-video",
|
"name": "@ohif/extension-dicom-video",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -365,20 +365,20 @@
|
|||||||
},
|
},
|
||||||
"extensions/measurement-tracking": {
|
"extensions/measurement-tracking": {
|
||||||
"name": "@ohif/extension-measurement-tracking",
|
"name": "@ohif/extension-measurement-tracking",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@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/react": "^3.2.2",
|
||||||
"xstate": "^4.10.0",
|
"xstate": "^4.10.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
@ -391,14 +391,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/test-extension": {
|
"extensions/test-extension": {
|
||||||
"name": "@ohif/extension-test",
|
"name": "@ohif/extension-test",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "0.38.0",
|
"dcmjs": "0.38.0",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -408,7 +408,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/tmtv": {
|
"extensions/tmtv": {
|
||||||
"name": "@ohif/extension-tmtv",
|
"name": "@ohif/extension-tmtv",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@ -425,7 +425,7 @@
|
|||||||
},
|
},
|
||||||
"modes/basic-dev-mode": {
|
"modes/basic-dev-mode": {
|
||||||
"name": "@ohif/mode-basic-dev-mode",
|
"name": "@ohif/mode-basic-dev-mode",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -435,17 +435,17 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
|
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-video": "3.11.0-beta.27",
|
"@ohif/extension-dicom-video": "3.11.0-beta.32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/basic-test-mode": {
|
"modes/basic-test-mode": {
|
||||||
"name": "@ohif/mode-test",
|
"name": "@ohif/mode-test",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -455,19 +455,19 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
|
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-video": "3.11.0-beta.27",
|
"@ohif/extension-dicom-video": "3.11.0-beta.32",
|
||||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
|
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
|
||||||
"@ohif/extension-test": "3.11.0-beta.27",
|
"@ohif/extension-test": "3.11.0-beta.32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/longitudinal": {
|
"modes/longitudinal": {
|
||||||
"name": "@ohif/mode-longitudinal",
|
"name": "@ohif/mode-longitudinal",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -477,32 +477,32 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
|
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-video": "3.11.0-beta.27",
|
"@ohif/extension-dicom-video": "3.11.0-beta.32",
|
||||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.27",
|
"@ohif/extension-measurement-tracking": "3.11.0-beta.32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/microscopy": {
|
"modes/microscopy": {
|
||||||
"name": "@ohif/mode-microscopy",
|
"name": "@ohif/mode-microscopy",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-microscopy": "3.11.0-beta.27",
|
"@ohif/extension-dicom-microscopy": "3.11.0-beta.32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/preclinical-4d": {
|
"modes/preclinical-4d": {
|
||||||
"name": "@ohif/mode-preclinical-4d",
|
"name": "@ohif/mode-preclinical-4d",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
},
|
},
|
||||||
@ -521,7 +521,7 @@
|
|||||||
},
|
},
|
||||||
"modes/segmentation": {
|
"modes/segmentation": {
|
||||||
"name": "@ohif/mode-segmentation",
|
"name": "@ohif/mode-segmentation",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -565,7 +565,7 @@
|
|||||||
},
|
},
|
||||||
"modes/tmtv": {
|
"modes/tmtv": {
|
||||||
"name": "@ohif/mode-tmtv",
|
"name": "@ohif/mode-tmtv",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -586,32 +586,32 @@
|
|||||||
},
|
},
|
||||||
"platform/app": {
|
"platform/app": {
|
||||||
"name": "@ohif/app",
|
"name": "@ohif/app",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@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",
|
"@emotion/serialize": "^1.1.3",
|
||||||
"@ohif/core": "3.11.0-beta.27",
|
"@ohif/core": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.32",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.27",
|
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.32",
|
||||||
"@ohif/extension-default": "3.11.0-beta.27",
|
"@ohif/extension-default": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-microscopy": "3.11.0-beta.27",
|
"@ohif/extension-dicom-microscopy": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.27",
|
"@ohif/extension-dicom-pdf": "3.11.0-beta.32",
|
||||||
"@ohif/extension-dicom-video": "3.11.0-beta.27",
|
"@ohif/extension-dicom-video": "3.11.0-beta.32",
|
||||||
"@ohif/extension-test": "3.11.0-beta.27",
|
"@ohif/extension-test": "3.11.0-beta.32",
|
||||||
"@ohif/i18n": "3.11.0-beta.27",
|
"@ohif/i18n": "3.11.0-beta.32",
|
||||||
"@ohif/mode-basic-dev-mode": "3.11.0-beta.27",
|
"@ohif/mode-basic-dev-mode": "3.11.0-beta.32",
|
||||||
"@ohif/mode-longitudinal": "3.11.0-beta.27",
|
"@ohif/mode-longitudinal": "3.11.0-beta.32",
|
||||||
"@ohif/mode-microscopy": "3.11.0-beta.27",
|
"@ohif/mode-microscopy": "3.11.0-beta.32",
|
||||||
"@ohif/mode-test": "3.11.0-beta.27",
|
"@ohif/mode-test": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"@ohif/ui-next": "3.11.0-beta.27",
|
"@ohif/ui-next": "3.11.0-beta.32",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@ -652,7 +652,7 @@
|
|||||||
},
|
},
|
||||||
"platform/cli": {
|
"platform/cli": {
|
||||||
"name": "@ohif/cli",
|
"name": "@ohif/cli",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ohif-cli": "src/index.js",
|
"ohif-cli": "src/index.js",
|
||||||
},
|
},
|
||||||
@ -676,7 +676,7 @@
|
|||||||
},
|
},
|
||||||
"platform/core": {
|
"platform/core": {
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
@ -701,16 +701,16 @@
|
|||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/dicom-image-loader": "^3.12.2",
|
"@cornerstonejs/dicom-image-loader": "^3.15.1",
|
||||||
"@ohif/ui": "3.11.0-beta.27",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"cornerstone-math": "0.1.9",
|
"cornerstone-math": "0.1.9",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"platform/i18n": {
|
"platform/i18n": {
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next-locize-backend": "^2.0.0",
|
"i18next-locize-backend": "^2.0.0",
|
||||||
@ -735,7 +735,7 @@
|
|||||||
},
|
},
|
||||||
"platform/ui": {
|
"platform/ui": {
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/react": "^13.1.0",
|
"@testing-library/react": "^13.1.0",
|
||||||
"browser-detect": "^0.2.28",
|
"browser-detect": "^0.2.28",
|
||||||
@ -795,7 +795,7 @@
|
|||||||
},
|
},
|
||||||
"platform/ui-next": {
|
"platform/ui-next": {
|
||||||
"name": "@ohif/ui-next",
|
"name": "@ohif/ui-next",
|
||||||
"version": "3.11.0-beta.27",
|
"version": "3.11.0-beta.32",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
@ -1106,9 +1106,9 @@
|
|||||||
|
|
||||||
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
|
"@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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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/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/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=="],
|
"@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/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/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=="],
|
"@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": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { useViewportGrid } from '@ohif/ui-next';
|
||||||
|
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||||
const {
|
const { displaySets, children, viewportOptions, displaySetOptions, servicesManager } = props;
|
||||||
displaySets,
|
|
||||||
children,
|
|
||||||
viewportOptions,
|
|
||||||
displaySetOptions,
|
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
} = props;
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
|
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
|
||||||
servicesManager.services;
|
servicesManager.services;
|
||||||
@ -59,9 +53,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
const getCornerstoneViewport = useCallback(() => {
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
|
||||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySetOptions.unshift({});
|
displaySetOptions.unshift({});
|
||||||
const [pmapDisplaySetOptions] = displaySetOptions;
|
const [pmapDisplaySetOptions] = displaySetOptions;
|
||||||
@ -93,7 +84,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
// Referenced + PMAP displaySets must be passed as parameter in this order
|
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||||
@ -104,15 +95,16 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
|||||||
presentationIds: viewportOptions.presentationIds,
|
presentationIds: viewportOptions.presentationIds,
|
||||||
}}
|
}}
|
||||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||||
></Component>
|
/>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
extensionManager,
|
|
||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
props,
|
props,
|
||||||
pmapDisplaySet,
|
pmapDisplaySet,
|
||||||
viewportOptions.orientation,
|
viewportOptions.orientation,
|
||||||
viewportOptions.viewportId,
|
viewportOptions.viewportId,
|
||||||
|
viewportOptions.presentationIds,
|
||||||
|
uiNotificationService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Cleanup the PMAP viewport when the viewport is destroyed
|
// Cleanup the PMAP viewport when the viewport is destroyed
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
const RESPONSE = {
|
import { utils, Types } from '@ohif/extension-cornerstone';
|
||||||
NO_NEVER: -1,
|
|
||||||
CANCEL: 0,
|
|
||||||
HYDRATE_SEG: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
function promptHydrateRT({
|
function promptHydrateRT({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -10,74 +6,20 @@ function promptHydrateRT({
|
|||||||
viewportId,
|
viewportId,
|
||||||
preHydrateCallbacks,
|
preHydrateCallbacks,
|
||||||
hydrateRTDisplaySet,
|
hydrateRTDisplaySet,
|
||||||
}: withAppTypes) {
|
}: {
|
||||||
const { uiViewportDialogService, customizationService } = servicesManager.services;
|
servicesManager: AppTypes.ServicesManager;
|
||||||
const extensionManager = servicesManager._extensionManager;
|
rtDisplaySet: AppTypes.DisplaySet;
|
||||||
const appConfig = extensionManager._appConfig;
|
viewportId: string;
|
||||||
return new Promise(async function (resolve, reject) {
|
preHydrateCallbacks?: Types.HydrationCallback[];
|
||||||
const promptResult = appConfig?.disableConfirmationPrompts
|
hydrateRTDisplaySet: Types.HydrationCallback;
|
||||||
? RESPONSE.HYDRATE_SEG
|
}) {
|
||||||
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
|
return utils.promptHydrationDialog({
|
||||||
|
servicesManager,
|
||||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
viewportId,
|
||||||
preHydrateCallbacks?.forEach(callback => {
|
displaySet: rtDisplaySet,
|
||||||
callback();
|
preHydrateCallbacks,
|
||||||
});
|
hydrateCallback: hydrateRTDisplaySet,
|
||||||
|
type: 'RTSTRUCT',
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 PropTypes from 'prop-types';
|
||||||
import { ViewportActionArrows } from '@ohif/ui-next';
|
|
||||||
import { useViewportGrid } 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 promptHydrateRT from '../utils/promptHydrateRT';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
|
||||||
|
|
||||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
import { useSystem } from '@ohif/core/src';
|
||||||
|
|
||||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||||
const {
|
const { servicesManager, commandsManager } = useSystem();
|
||||||
children,
|
const { children, displaySets, viewportOptions } = props as {
|
||||||
displaySets,
|
children: React.ReactNode;
|
||||||
viewportOptions,
|
displaySets: AppTypes.DisplaySet[];
|
||||||
servicesManager,
|
viewportOptions: AppTypes.ViewportOptions;
|
||||||
extensionManager,
|
};
|
||||||
commandsManager,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
const { displaySetService, toolGroupService, segmentationService, customizationService } =
|
||||||
displaySetService,
|
servicesManager.services;
|
||||||
toolGroupService,
|
|
||||||
segmentationService,
|
|
||||||
uiNotificationService,
|
|
||||||
customizationService,
|
|
||||||
viewportActionCornersService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
@ -46,30 +38,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
const rtDisplaySet = displaySets[0];
|
const rtDisplaySet = displaySets[0];
|
||||||
|
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [{ viewports, activeViewportId }, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const selectedSegmentObjectIndex: number = 0;
|
const selectedSegmentObjectIndex: number = 0;
|
||||||
const { setPositionPresentation } = usePositionPresentationStore();
|
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 [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded);
|
||||||
const [element, setElement] = useState(null);
|
|
||||||
const [processingProgress, setProcessingProgress] = useState({
|
const [processingProgress, setProcessingProgress] = useState({
|
||||||
percentComplete: null,
|
percentComplete: null,
|
||||||
totalSegments: null,
|
totalSegments: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// refs
|
|
||||||
const referencedDisplaySetRef = useRef(null);
|
const referencedDisplaySetRef = useRef(null);
|
||||||
|
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
|
||||||
|
|
||||||
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
referencedDisplaySetInstanceUID
|
referencedDisplaySetInstanceUID
|
||||||
@ -80,100 +61,31 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
displaySet: referencedDisplaySet,
|
displaySet: referencedDisplaySet,
|
||||||
metadata: referencedDisplaySetMetadata,
|
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(() => {
|
useEffect(() => {
|
||||||
if (rtIsLoading) {
|
if (rtIsLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not active viewport, return
|
||||||
|
if (viewportId !== activeViewportId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
promptHydrateRT({
|
promptHydrateRT({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
viewportId,
|
viewportId,
|
||||||
rtDisplaySet,
|
rtDisplaySet,
|
||||||
preHydrateCallbacks: [storePresentationState],
|
hydrateRTDisplaySet: async () => {
|
||||||
hydrateRTDisplaySet,
|
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
|
||||||
}).then(isHydrated => {
|
displaySet: rtDisplaySet,
|
||||||
if (isHydrated) {
|
viewportId,
|
||||||
setIsHydrated(true);
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager, activeViewportId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// I'm not sure what is this, since in RT we support Overlapping segments
|
|
||||||
// via contours
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
evt => {
|
evt => {
|
||||||
@ -193,14 +105,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
viewPresentation: {},
|
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]);
|
}, [rtDisplaySet]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const segmentLoadingSubscription = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||||
({ percentComplete, numSegments }) => {
|
({ percentComplete, numSegments }) => {
|
||||||
setProcessingProgress({
|
setProcessingProgress({
|
||||||
@ -220,16 +124,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
const displaySetsRemovedSubscription = displaySetService.subscribe(
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [rtDisplaySet]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
Cleanup the SEG viewport when the viewport is destroyed
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
|
||||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||||
({ displaySetInstanceUIDs }) => {
|
({ displaySetInstanceUIDs }) => {
|
||||||
const activeViewport = viewports.get(activeViewportId);
|
const activeViewport = viewports.get(activeViewportId);
|
||||||
@ -243,9 +138,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
onDisplaySetsRemovedSubscription.unsubscribe();
|
segmentLoadingSubscription.unsubscribe();
|
||||||
|
displaySetsRemovedSubscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [rtDisplaySet, displaySetService, viewports, activeViewportId, viewportGridService]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
@ -259,38 +155,33 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
return () => {
|
return () => {
|
||||||
// remove the segmentation representations if seg displayset changed
|
// remove the segmentation representations if seg displayset changed
|
||||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||||
|
referencedDisplaySetRef.current = null;
|
||||||
toolGroupService.destroyToolGroup(toolGroupId);
|
toolGroupService.destroyToolGroup(toolGroupId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
setIsHydrated(rtDisplaySet.isHydrated);
|
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||||
|
|
||||||
return () => {
|
// Todo: jump to the center of the first segment
|
||||||
// remove the segmentation representations if seg displayset changed
|
return (
|
||||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
<OHIFCornerstoneViewport
|
||||||
referencedDisplaySetRef.current = null;
|
{...props}
|
||||||
};
|
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||||
}, [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;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
if (
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
<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": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
const RESPONSE = {
|
import { utils, Types } from '@ohif/extension-cornerstone';
|
||||||
NO_NEVER: -1,
|
|
||||||
CANCEL: 0,
|
|
||||||
HYDRATE_SEG: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
function promptHydrateSEG({
|
function promptHydrateSEG({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -10,75 +6,20 @@ function promptHydrateSEG({
|
|||||||
viewportId,
|
viewportId,
|
||||||
preHydrateCallbacks,
|
preHydrateCallbacks,
|
||||||
hydrateCallback,
|
hydrateCallback,
|
||||||
}: withAppTypes) {
|
}: {
|
||||||
const { uiViewportDialogService, customizationService } = servicesManager.services;
|
servicesManager: AppTypes.ServicesManager;
|
||||||
const extensionManager = servicesManager._extensionManager;
|
segDisplaySet: AppTypes.DisplaySet;
|
||||||
const appConfig = extensionManager._appConfig;
|
viewportId: string;
|
||||||
|
preHydrateCallbacks?: Types.HydrationCallback[];
|
||||||
return new Promise(async function (resolve, reject) {
|
hydrateCallback: Types.HydrationCallback;
|
||||||
const promptResult = appConfig?.disableConfirmationPrompts
|
}) {
|
||||||
? RESPONSE.HYDRATE_SEG
|
return utils.promptHydrationDialog({
|
||||||
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
|
servicesManager,
|
||||||
|
viewportId,
|
||||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
displaySet: segDisplaySet as AppTypes.DisplaySet,
|
||||||
preHydrateCallbacks?.forEach(callback => {
|
preHydrateCallbacks,
|
||||||
callback();
|
hydrateCallback,
|
||||||
});
|
type: 'SEG',
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,24 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
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 { useViewportGrid } from '@ohif/ui-next';
|
||||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||||
import _getStatusComponent from './_getStatusComponent';
|
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
|
||||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
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';
|
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||||
const {
|
const { servicesManager, commandsManager } = useSystem();
|
||||||
children,
|
const { children, displaySets, viewportOptions } = props as {
|
||||||
displaySets,
|
children: React.ReactNode;
|
||||||
viewportOptions,
|
displaySets: AppTypes.DisplaySet[];
|
||||||
servicesManager,
|
viewportOptions: AppTypes.ViewportOptions;
|
||||||
extensionManager,
|
};
|
||||||
commandsManager,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { t } = useTranslation('SEGViewport');
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
const {
|
const { displaySetService, toolGroupService, segmentationService, customizationService } =
|
||||||
displaySetService,
|
servicesManager.services;
|
||||||
toolGroupService,
|
|
||||||
segmentationService,
|
|
||||||
customizationService,
|
|
||||||
viewportActionCornersService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
|
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
|
||||||
'ui.loadingIndicatorTotalPercent'
|
'ui.loadingIndicatorTotalPercent'
|
||||||
@ -47,7 +35,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const selectedSegmentObjectIndex: number = 0;
|
|
||||||
const { setPositionPresentation } = usePositionPresentationStore();
|
const { setPositionPresentation } = usePositionPresentationStore();
|
||||||
|
|
||||||
// Hydration means that the SEG is opened and segments are loaded into the
|
// 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,
|
// same frameOfReferenceUID as the referencedSeriesUID of the SEG. However,
|
||||||
// loading basically means SEG loading over network and bit unpacking of the
|
// loading basically means SEG loading over network and bit unpacking of the
|
||||||
// SEG data.
|
// SEG data.
|
||||||
const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated);
|
|
||||||
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
||||||
const [element, setElement] = useState(null);
|
|
||||||
const [processingProgress, setProcessingProgress] = useState({
|
const [processingProgress, setProcessingProgress] = useState({
|
||||||
percentComplete: null,
|
percentComplete: null,
|
||||||
totalSegments: null,
|
totalSegments: null,
|
||||||
@ -82,36 +67,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
displaySet: referencedDisplaySet,
|
displaySet: referencedDisplaySet,
|
||||||
metadata: referencedDisplaySetMetadata,
|
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 getCornerstoneViewport = useCallback(() => {
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
|
||||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Todo: jump to the center of the first segment
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
displaySets={[segDisplaySet]}
|
displaySets={[segDisplaySet]}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
@ -123,66 +82,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
|||||||
}}
|
}}
|
||||||
onElementEnabled={evt => {
|
onElementEnabled={evt => {
|
||||||
props.onElementEnabled?.(evt);
|
props.onElementEnabled?.(evt);
|
||||||
onElementEnabled(evt);
|
|
||||||
}}
|
}}
|
||||||
onElementDisabled={onElementDisabled}
|
/>
|
||||||
></Component>
|
|
||||||
);
|
);
|
||||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (segIsLoading) {
|
if (segIsLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not active viewport, return
|
||||||
|
if (viewportId !== activeViewportId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
promptHydrateSEG({
|
promptHydrateSEG({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
viewportId,
|
viewportId,
|
||||||
segDisplaySet,
|
segDisplaySet,
|
||||||
preHydrateCallbacks: [storePresentationState],
|
hydrateCallback: async () => {
|
||||||
hydrateCallback: hydrateSEG,
|
await commandsManager.runCommand('hydrateSecondaryDisplaySet', {
|
||||||
}).then(isHydrated => {
|
displaySet: segDisplaySet,
|
||||||
if (isHydrated) {
|
viewportId,
|
||||||
setIsHydrated(true);
|
});
|
||||||
}
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
|
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, commandsManager, activeViewportId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// on new seg display set, remove all segmentations from all viewports
|
// 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;
|
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": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"classnames": "^2.3.2"
|
"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
|
* Loads measurements by hydrating and loading the SR for the given display set instance UID
|
||||||
* and displays it in the active viewport.
|
* and displays it in the active viewport.
|
||||||
*/
|
*/
|
||||||
loadSRMeasurements: ({ displaySetInstanceUID }) => {
|
hydrateStructuredReport: ({ displaySetInstanceUID }) => {
|
||||||
const { SeriesInstanceUIDs } = hydrateStructuredReport(
|
return hydrateStructuredReport(
|
||||||
{ servicesManager, extensionManager, commandsManager },
|
{ servicesManager, extensionManager, commandsManager },
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
|
||||||
if (displaySets.length) {
|
|
||||||
commandsManager.run('setDisplaySetsForViewports', {
|
|
||||||
viewportsToUpdate: [
|
|
||||||
{
|
|
||||||
viewportId: viewportGridService.getActiveViewportId(),
|
|
||||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
downloadReport: {
|
downloadReport: actions.downloadReport,
|
||||||
commandFn: actions.downloadReport,
|
storeMeasurements: actions.storeMeasurements,
|
||||||
},
|
hydrateStructuredReport: actions.hydrateStructuredReport,
|
||||||
storeMeasurements: {
|
|
||||||
commandFn: actions.storeMeasurements,
|
|
||||||
},
|
|
||||||
loadSRMeasurements: {
|
|
||||||
commandFn: actions.loadSRMeasurements,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { ExtensionManager, useToolbar } from '@ohif/core';
|
|
||||||
|
|
||||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||||
|
|
||||||
import { ViewportActionArrows } from '@ohif/ui-next';
|
|
||||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
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 { useViewportGrid } from '@ohif/ui-next';
|
||||||
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
|
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
|
||||||
|
|
||||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
|
||||||
|
|
||||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||||
|
|
||||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
function OHIFCornerstoneSRMeasurementViewport(props) {
|
||||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
const { servicesManager } = useSystem();
|
||||||
props;
|
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;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
@ -34,40 +33,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
|
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||||
const [measurementCount, setMeasurementCount] = useState(1);
|
|
||||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
|
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
|
||||||
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
|
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
|
||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const { viewports, activeViewportId } = viewportGrid;
|
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(
|
const setTrackingIdentifiers = useCallback(
|
||||||
measurementSelected => {
|
measurementSelected => {
|
||||||
const { measurements } = srDisplaySet;
|
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.');
|
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
_getViewportReferencedDisplaySetData(
|
_getViewportReferencedDisplaySetData(
|
||||||
srDisplaySet,
|
srDisplaySet,
|
||||||
newMeasurementSelected,
|
newMeasurementSelected,
|
||||||
@ -140,10 +106,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
|
||||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { measurements } = srDisplaySet;
|
const { measurements } = srDisplaySet;
|
||||||
const measurement = measurements[measurementSelected];
|
const measurement = measurements[measurementSelected];
|
||||||
|
|
||||||
@ -152,7 +114,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
// should be passed second since we don't want SR displaySet to
|
// should be passed second since we don't want SR displaySet to
|
||||||
// override the activeImageDisplaySetData
|
// override the activeImageDisplaySetData
|
||||||
@ -176,27 +138,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
onElementEnabled(evt);
|
onElementEnabled(evt);
|
||||||
}}
|
}}
|
||||||
isJumpToMeasurementDisabled={true}
|
isJumpToMeasurementDisabled={true}
|
||||||
></Component>
|
/>
|
||||||
);
|
);
|
||||||
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
|
}, [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
|
Cleanup the SR viewport when the viewport is destroyed
|
||||||
*/
|
*/
|
||||||
@ -231,8 +176,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
if (!srDisplaySet.isLoaded) {
|
if (!srDisplaySet.isLoaded) {
|
||||||
await srDisplaySet.load();
|
await srDisplaySet.load();
|
||||||
}
|
}
|
||||||
const numMeasurements = srDisplaySet.measurements.length;
|
|
||||||
setMeasurementCount(numMeasurements);
|
|
||||||
updateViewport(measurementSelected);
|
updateViewport(measurementSelected);
|
||||||
};
|
};
|
||||||
loadSR();
|
loadSR();
|
||||||
@ -255,42 +198,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
|||||||
updateSR();
|
updateSR();
|
||||||
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
}, [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;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
@ -327,8 +234,6 @@ OHIFCornerstoneSRMeasurementViewport.propTypes = {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
viewportLabel: PropTypes.string,
|
viewportLabel: PropTypes.string,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
servicesManager: PropTypes.object.isRequired,
|
|
||||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _getViewportReferencedDisplaySetData(
|
async function _getViewportReferencedDisplaySetData(
|
||||||
@ -372,130 +277,4 @@ async function _getViewportReferencedDisplaySetData(
|
|||||||
return { referencedDisplaySetMetadata, referencedDisplaySet };
|
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;
|
export default OHIFCornerstoneSRMeasurementViewport;
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
|
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
|
||||||
import { ViewportActionButton } from '@ohif/ui-next';
|
|
||||||
import i18n from '@ohif/i18n';
|
|
||||||
|
|
||||||
export default function onModeEnter({ servicesManager }) {
|
export default function onModeEnter({ servicesManager }) {
|
||||||
const { displaySetService, toolbarService } = servicesManager.services;
|
const { displaySetService } = servicesManager.services;
|
||||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
|
||||||
const srDisplaySets = [...displaySetCache.values()].filter(
|
const srDisplaySets = [...displaySetCache.values()].filter(
|
||||||
@ -16,24 +12,4 @@ export default function onModeEnter({ servicesManager }) {
|
|||||||
// New mode route, allow SRs to be hydrated again
|
// New mode route, allow SRs to be hydrated again
|
||||||
ds.isHydrated = false;
|
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.
|
* was not triggering annotation_added events.
|
||||||
*/
|
*/
|
||||||
annotation.state.addAnnotation(SRAnnotation);
|
annotation.state.addAnnotation(SRAnnotation);
|
||||||
console.debug('Adding SR annotation:', SRAnnotation);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"classnames": "^2.3.2"
|
"classnames": "^2.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@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/core": "3.11.0-beta.32",
|
||||||
"@ohif/ui": "3.11.0-beta.32",
|
"@ohif/ui": "3.11.0-beta.32",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
@ -51,12 +51,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.12.2",
|
"@cornerstonejs/adapters": "^3.15.1",
|
||||||
"@cornerstonejs/ai": "^3.12.2",
|
"@cornerstonejs/ai": "^3.15.1",
|
||||||
"@cornerstonejs/core": "^3.12.2",
|
"@cornerstonejs/core": "^3.15.1",
|
||||||
"@cornerstonejs/labelmap-interpolation": "^3.12.2",
|
"@cornerstonejs/labelmap-interpolation": "^3.15.1",
|
||||||
"@cornerstonejs/polymorphic-segmentation": "^3.12.2",
|
"@cornerstonejs/polymorphic-segmentation": "^3.15.1",
|
||||||
"@cornerstonejs/tools": "^3.12.2",
|
"@cornerstonejs/tools": "^3.15.1",
|
||||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import * as cs3DTools from '@cornerstonejs/tools';
|
import * as cs3DTools from '@cornerstonejs/tools';
|
||||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
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 { useViewportDialog } from '@ohif/ui-next';
|
||||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||||
|
|
||||||
@ -21,6 +21,9 @@ import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewport
|
|||||||
|
|
||||||
const STACK = 'stack';
|
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
|
// 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.
|
// Then we don't need to worry about the re-renders if the props change.
|
||||||
const OHIFCornerstoneViewport = React.memo(
|
const OHIFCornerstoneViewport = React.memo(
|
||||||
@ -79,6 +82,7 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||||
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||||
|
const viewportRef = useViewportRef(viewportId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displaySetService,
|
displaySetService,
|
||||||
@ -88,7 +92,6 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
segmentationService,
|
segmentationService,
|
||||||
cornerstoneCacheService,
|
cornerstoneCacheService,
|
||||||
viewportActionCornersService,
|
|
||||||
customizationService,
|
customizationService,
|
||||||
measurementService,
|
measurementService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
@ -101,12 +104,28 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
}, [elementRef]);
|
}, [elementRef]);
|
||||||
|
|
||||||
// useCallback for onResize
|
// useCallback for onResize
|
||||||
const onResize = useCallback(() => {
|
const onResize = useCallback(
|
||||||
if (elementRef.current) {
|
(entries: ResizeObserverEntry[]) => {
|
||||||
cornerstoneViewportService.resize();
|
if (elementRef.current && entries?.length) {
|
||||||
setImageScrollBarHeight();
|
const entry = entries[0];
|
||||||
}
|
const { width, height } = entry.contentRect;
|
||||||
}, [elementRef, cornerstoneViewportService, setImageScrollBarHeight]);
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
@ -133,16 +152,8 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||||
|
|
||||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||||
|
|
||||||
viewportActionCornersService.clear(viewportId);
|
|
||||||
},
|
},
|
||||||
[
|
[viewportId, segmentationService, syncGroupService, toolGroupService]
|
||||||
viewportId,
|
|
||||||
segmentationService,
|
|
||||||
syncGroupService,
|
|
||||||
toolGroupService,
|
|
||||||
viewportActionCornersService,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const elementEnabledHandler = useCallback(
|
const elementEnabledHandler = useCallback(
|
||||||
@ -210,6 +221,7 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cornerstoneViewportService.disableElement(viewportId);
|
cornerstoneViewportService.disableElement(viewportId);
|
||||||
|
viewportRef.unregister();
|
||||||
|
|
||||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||||
};
|
};
|
||||||
@ -328,7 +340,11 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
onMouseDown={e => e.preventDefault()}
|
onMouseDown={e => e.preventDefault()}
|
||||||
ref={elementRef}
|
data-viewportId={viewportId}
|
||||||
|
ref={el => {
|
||||||
|
elementRef.current = el;
|
||||||
|
if (el) viewportRef.register(el);
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
<CornerstoneOverlays
|
<CornerstoneOverlays
|
||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
@ -346,10 +362,6 @@ const OHIFCornerstoneViewport = React.memo(
|
|||||||
viewportId={viewportId}
|
viewportId={viewportId}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
<ViewportColorbarsContainer
|
|
||||||
viewportId={viewportId}
|
|
||||||
viewportElementRef={elementRef}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/* top offset of 24px to account for ViewportActionCorners. */}
|
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||||
<div className="absolute top-[24px] w-full">
|
<div className="absolute top-[24px] w-full">
|
||||||
|
|||||||
@ -11,18 +11,22 @@
|
|||||||
.ViewportOrientationMarkers .top-mid {
|
.ViewportOrientationMarkers .top-mid {
|
||||||
top: 0.38rem;
|
top: 0.38rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .left-mid {
|
.ViewportOrientationMarkers .left-mid {
|
||||||
top: 47%;
|
top: 50%;
|
||||||
left: 0.38rem;
|
left: 0.38rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .right-mid {
|
.ViewportOrientationMarkers .right-mid {
|
||||||
top: 47%;
|
top: 50%;
|
||||||
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .bottom-mid {
|
.ViewportOrientationMarkers .bottom-mid {
|
||||||
top: calc(100% - var(--marker-height) - 0.6rem);
|
top: calc(100% - var(--marker-height) - 0.6rem);
|
||||||
left: 47%;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownload
|
|||||||
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
|
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
|
||||||
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
|
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
|
||||||
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
|
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
|
||||||
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||||
|
|
||||||
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
||||||
const toggleSyncFunctions = {
|
const toggleSyncFunctions = {
|
||||||
@ -127,6 +128,68 @@ function commandsModule({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
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 } = {}) => {
|
runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => {
|
||||||
// Get active segmentation if not specified
|
// Get active segmentation if not specified
|
||||||
const targetSegmentation =
|
const targetSegmentation =
|
||||||
@ -576,10 +639,20 @@ function commandsModule({
|
|||||||
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
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
|
// convert to numbers
|
||||||
const windowWidthNum = Number(window);
|
const windowWidthNum = Number(windowWidth);
|
||||||
const windowCenterNum = Number(level);
|
const windowCenterNum = Number(windowCenter);
|
||||||
|
|
||||||
// get actor from the viewport
|
// get actor from the viewport
|
||||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
@ -587,12 +660,28 @@ function commandsModule({
|
|||||||
|
|
||||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
|
||||||
|
|
||||||
viewport.setProperties({
|
if (viewport instanceof BaseVolumeViewport) {
|
||||||
voiRange: {
|
const volumeId = actions.getVolumeIdForDisplaySet({
|
||||||
upper,
|
viewportId,
|
||||||
lower,
|
displaySetInstanceUID,
|
||||||
},
|
});
|
||||||
});
|
viewport.setProperties(
|
||||||
|
{
|
||||||
|
voiRange: {
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
volumeId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
viewport.setProperties({
|
||||||
|
voiRange: {
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
|
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
|
||||||
@ -641,10 +730,19 @@ function commandsModule({
|
|||||||
|
|
||||||
actions.setViewportWindowLevel({
|
actions.setViewportWindowLevel({
|
||||||
viewportId: activeViewport,
|
viewportId: activeViewport,
|
||||||
window: windowLevelPreset.window,
|
windowWidth: windowLevelPreset.window,
|
||||||
level: windowLevelPreset.level,
|
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 }) => {
|
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
@ -1753,11 +1851,11 @@ function commandsModule({
|
|||||||
displaySetInstanceUIDs,
|
displaySetInstanceUIDs,
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedViewports.forEach(viewport => {
|
actions.setDisplaySetsForViewports({
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
viewportsToUpdate: updatedViewports.map(viewport => ({
|
||||||
viewportId: viewport.viewportId,
|
viewportId: viewport.viewportId,
|
||||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||||
});
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setViewportOrientation: ({ viewportId, orientation }) => {
|
setViewportOrientation: ({ viewportId, orientation }) => {
|
||||||
@ -2065,6 +2163,8 @@ function commandsModule({
|
|||||||
addNewSegment: actions.addNewSegment,
|
addNewSegment: actions.addNewSegment,
|
||||||
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
|
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
|
||||||
setViewportOrientation: actions.setViewportOrientation,
|
setViewportOrientation: actions.setViewportOrientation,
|
||||||
|
hydrateSecondaryDisplaySet: actions.hydrateSecondaryDisplaySet,
|
||||||
|
getVolumeIdForDisplaySet: actions.getVolumeIdForDisplaySet,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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 React, { memo } from 'react';
|
||||||
import {
|
import { ViewportActionCorners, IconPresentationProvider, ToolButton } from '@ohif/ui-next';
|
||||||
useViewportActionCorners,
|
import { Toolbar } from '@ohif/extension-default/src/Toolbar/Toolbar';
|
||||||
ViewportActionCorners,
|
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
|
||||||
ViewportActionCornersLocations,
|
import { useViewportHover } from '../hooks';
|
||||||
} from '@ohif/ui-next';
|
|
||||||
|
|
||||||
export type OHIFViewportActionCornersProps = {
|
export type OHIFViewportActionCornersProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
function OHIFViewportActionCornersComponent({ viewportId }: OHIFViewportActionCornersProps) {
|
||||||
const [state] = useViewportActionCorners();
|
// 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;
|
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 (
|
return (
|
||||||
<ViewportActionCorners.Container>
|
<IconPresentationProvider
|
||||||
{renderCorner(ViewportActionCornersLocations.topLeft, ViewportActionCorners.TopLeft)}
|
size="medium"
|
||||||
{renderCorner(ViewportActionCornersLocations.topRight, ViewportActionCorners.TopRight)}
|
IconContainer={ToolButton}
|
||||||
{renderCorner(ViewportActionCornersLocations.bottomLeft, ViewportActionCorners.BottomLeft)}
|
containerProps={{
|
||||||
{renderCorner(ViewportActionCornersLocations.bottomRight, ViewportActionCorners.BottomRight)}
|
size: 'tiny',
|
||||||
</ViewportActionCorners.Container>
|
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;
|
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 { utilities } from '@cornerstonejs/tools';
|
||||||
import { useSystem } from '@ohif/core';
|
import { useSystem, useViewportRef, useViewportSize } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
ColorbarPositionType,
|
ColorbarPositionType,
|
||||||
TickPositionType,
|
TickPositionType,
|
||||||
@ -24,6 +24,7 @@ type ColorbarProps = {
|
|||||||
tickStyles?: TickStyleType;
|
tickStyles?: TickStyleType;
|
||||||
containerStyles?: ContainerStyleType;
|
containerStyles?: ContainerStyleType;
|
||||||
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
numColorbars: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +32,7 @@ type ColorbarProps = {
|
|||||||
* A React wrapper for the cornerstone ViewportColorbar that adds a close button
|
* A React wrapper for the cornerstone ViewportColorbar that adds a close button
|
||||||
* positioned appropriately based on the colorbar position.
|
* positioned appropriately based on the colorbar position.
|
||||||
*/
|
*/
|
||||||
const ViewportColorbar = ({
|
const ViewportColorbar = memo(function ViewportColorbar({
|
||||||
viewportId,
|
viewportId,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
colormaps,
|
colormaps,
|
||||||
@ -40,11 +41,50 @@ const ViewportColorbar = ({
|
|||||||
position,
|
position,
|
||||||
tickPosition,
|
tickPosition,
|
||||||
tickStyles,
|
tickStyles,
|
||||||
viewportElementRef,
|
numColorbars,
|
||||||
}: ColorbarProps) => {
|
}: ColorbarProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { servicesManager } = useSystem();
|
const { servicesManager } = useSystem();
|
||||||
const { customizationService } = servicesManager.services;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || !colormaps || !activeColormapName) {
|
if (!containerRef.current || !colormaps || !activeColormapName) {
|
||||||
@ -53,29 +93,21 @@ const ViewportColorbar = ({
|
|||||||
|
|
||||||
const viewportElement = viewportElementRef?.current;
|
const viewportElement = viewportElementRef?.current;
|
||||||
|
|
||||||
if (!viewportElement) {
|
if (!viewportElement || !colormaps?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorbarCustomization = customizationService.getCustomization(
|
// Using stable references from memoized values
|
||||||
'cornerstone.colorbar'
|
|
||||||
) as unknown as ColorbarCustomization;
|
|
||||||
|
|
||||||
const positionTickStyles = colorbarCustomization?.positionTickStyles?.[position];
|
|
||||||
const csColorbar = new CornerstoneViewportColorbar({
|
const csColorbar = new CornerstoneViewportColorbar({
|
||||||
id: `Colorbar-${viewportId}-${displaySetInstanceUID}`,
|
id: colorbarId,
|
||||||
element: viewportElement,
|
element: viewportElement,
|
||||||
container: containerRef.current,
|
container: containerRef.current,
|
||||||
colormaps: colormaps,
|
colormaps: colormaps,
|
||||||
activeColormapName: activeColormapName,
|
activeColormapName: activeColormapName,
|
||||||
volumeId,
|
volumeId,
|
||||||
ticks: {
|
ticks: {
|
||||||
position: tickPosition as ColorbarRangeTextPosition,
|
position: appropriateTickPosition as ColorbarRangeTextPosition,
|
||||||
style: {
|
style: mergedTickStyles,
|
||||||
...(colorbarCustomization?.tickStyles || {}),
|
|
||||||
...(positionTickStyles?.style || {}),
|
|
||||||
...(tickStyles || {}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,37 +122,33 @@ const ViewportColorbar = ({
|
|||||||
colormaps,
|
colormaps,
|
||||||
activeColormapName,
|
activeColormapName,
|
||||||
volumeId,
|
volumeId,
|
||||||
position,
|
colorbarId,
|
||||||
tickPosition,
|
appropriateTickPosition,
|
||||||
tickStyles,
|
mergedTickStyles,
|
||||||
viewportElementRef,
|
viewportElementRef,
|
||||||
customizationService,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get position styles from customization service
|
if (!height || !width) {
|
||||||
const colorbarCustomization = customizationService.getCustomization(
|
return null;
|
||||||
'cornerstone.colorbar'
|
}
|
||||||
) as unknown as ColorbarCustomization;
|
|
||||||
|
|
||||||
const positionStylesFromConfig = colorbarCustomization?.positionStyles?.[position] || {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`colorbar-container-${viewportId}-${displaySetInstanceUID}`}
|
id={`colorbar-container-${viewportId}-${displaySetInstanceUID}`}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{
|
style={{
|
||||||
width: position === 'bottom' ? '100%' : '20px',
|
|
||||||
height: position === 'bottom' ? '20px' : '100%',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
minWidth: position === 'bottom' ? width / 2.5 : '17px',
|
||||||
|
minHeight: position === 'bottom' ? '20px' : numColorbars === 1 ? height / 3 : height / 4,
|
||||||
...positionStylesFromConfig,
|
...positionStylesFromConfig,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ViewportColorbar;
|
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 { useSystem } from '@ohif/core';
|
||||||
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
|
|
||||||
import { ColorbarCustomization } from '../../types/Colorbar';
|
import { ColorbarCustomization } from '../../types/Colorbar';
|
||||||
import type { ColorMapPreset } from '../../types/Colormap';
|
import type { ColorMapPreset } from '../../types/Colormap';
|
||||||
import ViewportColorbar from './ViewportColorbar';
|
import ViewportColorbar from './ViewportColorbar';
|
||||||
import { deepMerge } from '@cornerstonejs/core/utilities';
|
import useViewportRendering from '../../hooks/useViewportRendering';
|
||||||
|
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||||
type ViewportColorbarsContainerProps = {
|
type ViewportColorbarsContainerProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
viewportElementRef?: React.RefObject<HTMLDivElement>;
|
location: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ColorbarData = {
|
type ColorbarData = {
|
||||||
@ -24,18 +23,38 @@ type ColorbarData = {
|
|||||||
* Container component that manages multiple colorbars for a viewport
|
* Container component that manages multiple colorbars for a viewport
|
||||||
* It interacts with the colorbarService to get/set colorbar states
|
* It interacts with the colorbarService to get/set colorbar states
|
||||||
*/
|
*/
|
||||||
const ViewportColorbarsContainer = ({
|
const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({
|
||||||
viewportId,
|
viewportId,
|
||||||
viewportElementRef,
|
location,
|
||||||
}: ViewportColorbarsContainerProps) => {
|
}: ViewportColorbarsContainerProps) {
|
||||||
const [colorbars, setColorbars] = useState<ColorbarData[]>([]);
|
const [colorbars, setColorbars] = useState<ColorbarData[]>([]);
|
||||||
const { servicesManager } = useSystem();
|
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(() => {
|
useEffect(() => {
|
||||||
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
|
setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
|
||||||
}, [viewportId, colorbarService]);
|
}, [viewportId, colorbarService]);
|
||||||
|
|
||||||
|
// Subscribe to colorbar state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = colorbarService.subscribe(
|
const { unsubscribe } = colorbarService.subscribe(
|
||||||
colorbarService.EVENTS.STATE_CHANGED,
|
colorbarService.EVENTS.STATE_CHANGED,
|
||||||
@ -51,90 +70,59 @@ const ViewportColorbarsContainer = ({
|
|||||||
};
|
};
|
||||||
}, [viewportId, colorbarService]);
|
}, [viewportId, colorbarService]);
|
||||||
|
|
||||||
const handleClose = (displaySetInstanceUID?: string): void => {
|
|
||||||
if (displaySetInstanceUID) {
|
|
||||||
colorbarService.removeColorbar(viewportId, displaySetInstanceUID);
|
|
||||||
} else {
|
|
||||||
colorbarService.removeColorbar(viewportId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!colorbars.length) {
|
if (!colorbars.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorbarCustomization = customizationService.getCustomization(
|
const isBottom = position === 'bottom';
|
||||||
'cornerstone.colorbar'
|
|
||||||
) as unknown as ColorbarCustomization;
|
|
||||||
|
|
||||||
const defaultPosition = colorbarCustomization?.colorbarContainerPosition;
|
const isSingleViewport = viewportDisplaySets.length === 1;
|
||||||
const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
|
const showFullList = isSingleViewport || !isBottom;
|
||||||
|
|
||||||
const position = colorbarCustomization?.colorbarContainerPosition || defaultPosition;
|
const colorbarsToUse = showFullList
|
||||||
const tickPosition = colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
|
? colorbars
|
||||||
const positionStyles = colorbarCustomization?.positionStyles;
|
: 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 =
|
return dsUID === targetUID;
|
||||||
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute"
|
|
||||||
style={{
|
style={{
|
||||||
...finalPositionStyle,
|
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{position !== 'bottom' ? (
|
<div
|
||||||
<div
|
className="flex h-full flex-col items-center justify-center"
|
||||||
className="flex h-full flex-col items-center justify-center"
|
style={{ pointerEvents: 'auto' }}
|
||||||
style={{ pointerEvents: 'auto' }}
|
>
|
||||||
>
|
{colorbarsToUse.map(colorbarInfo => {
|
||||||
{colorbars.map((colorbarInfo, index) => {
|
const { colorbar, displaySetInstanceUID } = colorbarInfo;
|
||||||
const { colorbar, displaySetInstanceUID } = colorbarInfo;
|
return (
|
||||||
return (
|
<ViewportColorbar
|
||||||
<ViewportColorbar
|
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
|
||||||
key={`colorbar-${viewportId}-${displaySetInstanceUID}`}
|
viewportId={viewportId}
|
||||||
viewportId={viewportId}
|
displaySetInstanceUID={displaySetInstanceUID}
|
||||||
displaySetInstanceUID={displaySetInstanceUID}
|
colormaps={colorbar.colormaps}
|
||||||
colormaps={colorbar.colormaps}
|
activeColormapName={colorbar.activeColormapName}
|
||||||
activeColormapName={colorbar.activeColormapName}
|
volumeId={colorbar.volumeId}
|
||||||
volumeId={colorbar.volumeId}
|
position={position}
|
||||||
viewportElementRef={viewportElementRef}
|
tickPosition={tickPosition}
|
||||||
position={position}
|
tickStyles={colorbarCustomization?.tickStyles}
|
||||||
tickPosition={tickPosition}
|
numColorbars={colorbars.length}
|
||||||
tickStyles={colorbarCustomization?.tickStyles}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<AdvancedColorbarWithControls
|
|
||||||
viewportId={viewportId}
|
|
||||||
colorbars={colorbars}
|
|
||||||
position={position}
|
|
||||||
tickPosition={defaultTickPosition}
|
|
||||||
colorbarCustomization={colorbarCustomization}
|
|
||||||
onClose={handleClose}
|
|
||||||
viewportElementRef={viewportElementRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ViewportColorbarsContainer;
|
export default ViewportColorbarsContainer;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import ViewportColorbar from './ViewportColorbar';
|
import ViewportColorbar from './ViewportColorbar';
|
||||||
import ViewportColorbarsContainer from './ViewportColorbarsContainer';
|
import ViewportColorbarsContainer from './ViewportColorbarsContainer';
|
||||||
import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
|
|
||||||
|
|
||||||
export { ViewportColorbar, ViewportColorbarsContainer, AdvancedColorbarWithControls };
|
export { ViewportColorbar, ViewportColorbarsContainer };
|
||||||
export default ViewportColorbarsContainer;
|
export default ViewportColorbarsContainer;
|
||||||
|
|||||||
@ -16,14 +16,16 @@ import {
|
|||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||||
|
import SelectItemWithModality from '../SelectItemWithModality';
|
||||||
|
import { useViewportRendering } from '../../hooks';
|
||||||
|
|
||||||
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
|
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
|
||||||
const { commandsManager, servicesManager } = useSystem();
|
const { commandsManager, servicesManager } = useSystem();
|
||||||
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
|
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
|
||||||
const [pendingSegmentations, setPendingSegmentations] = 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 {
|
const {
|
||||||
backgroundDisplaySet,
|
backgroundDisplaySet,
|
||||||
@ -34,6 +36,8 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
foregroundDisplaySets,
|
foregroundDisplaySets,
|
||||||
} = useViewportDisplaySets(viewportId);
|
} = useViewportDisplaySets(viewportId);
|
||||||
|
|
||||||
|
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the background display set
|
* 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
|
* 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 (
|
return (
|
||||||
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
|
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
|
||||||
{/* Top buttons row */}
|
{/* Top buttons row */}
|
||||||
@ -232,15 +233,17 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={displaySet.displaySetInstanceUID}
|
key={displaySet.displaySetInstanceUID}
|
||||||
value={displaySet.displaySetInstanceUID}
|
value={displaySet.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{displaySet.label}
|
<SelectItemWithModality displaySet={displaySet} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{potentialOverlayDisplaySets.map(item => (
|
{potentialOverlayDisplaySets.map(item => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.displaySetInstanceUID}
|
key={item.displaySetInstanceUID}
|
||||||
value={item.displaySetInstanceUID}
|
value={item.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{item.label}
|
<SelectItemWithModality displaySet={item} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -284,8 +287,9 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.displaySetInstanceUID}
|
key={item.displaySetInstanceUID}
|
||||||
value={item.displaySetInstanceUID}
|
value={item.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{item.label}
|
<SelectItemWithModality displaySet={item} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -334,15 +338,17 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={displaySet.displaySetInstanceUID}
|
key={displaySet.displaySetInstanceUID}
|
||||||
value={displaySet.displaySetInstanceUID}
|
value={displaySet.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{displaySet.label}
|
<SelectItemWithModality displaySet={displaySet} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{potentialForegroundDisplaySets.map(item => (
|
{potentialForegroundDisplaySets.map(item => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.displaySetInstanceUID}
|
key={item.displaySetInstanceUID}
|
||||||
value={item.displaySetInstanceUID}
|
value={item.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{item.label}
|
<SelectItemWithModality displaySet={item} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -386,8 +392,9 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={item.displaySetInstanceUID}
|
key={item.displaySetInstanceUID}
|
||||||
value={item.displaySetInstanceUID}
|
value={item.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{item.label}
|
<SelectItemWithModality displaySet={item} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -420,7 +427,7 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<div className="mt-1 mb-1 flex items-center px-1">
|
<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" />
|
<Icons.LayerBackground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||||
<Select
|
<Select
|
||||||
value={backgroundDisplaySet.displaySetInstanceUID}
|
value={backgroundDisplaySet?.displaySetInstanceUID}
|
||||||
onValueChange={value => {
|
onValueChange={value => {
|
||||||
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
|
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
|
||||||
ds => ds.displaySetInstanceUID === value
|
ds => ds.displaySetInstanceUID === value
|
||||||
@ -433,8 +440,8 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{(
|
{(
|
||||||
backgroundDisplaySet.SeriesDescription ||
|
backgroundDisplaySet?.SeriesDescription ||
|
||||||
backgroundDisplaySet.label ||
|
backgroundDisplaySet?.label ||
|
||||||
'background'
|
'background'
|
||||||
).toUpperCase()}
|
).toUpperCase()}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
@ -444,15 +451,16 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
<SelectItem
|
<SelectItem
|
||||||
key={displaySet.displaySetInstanceUID}
|
key={displaySet.displaySetInstanceUID}
|
||||||
value={displaySet.displaySetInstanceUID}
|
value={displaySet.displaySetInstanceUID}
|
||||||
|
className="pr-2"
|
||||||
>
|
>
|
||||||
{displaySet.label}
|
<SelectItemWithModality displaySet={displaySet} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* {foregroundDisplaySets.length > 0 && (
|
{foregroundDisplaySets.length > 0 && (hasAdvancedRenderingControls || hasOpacityMenu) && (
|
||||||
<div className="mt-1 ml-7">
|
<div className="mt-1 ml-7">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Switch
|
<Switch
|
||||||
@ -470,7 +478,7 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,73 +1,74 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
cn,
|
|
||||||
Icons,
|
Icons,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
useViewportGrid,
|
useIconPresentation,
|
||||||
useViewportActionCorners,
|
|
||||||
} from '@ohif/ui-next';
|
} from '@ohif/ui-next';
|
||||||
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
||||||
import classNames from 'classnames';
|
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||||
import { MENU_IDS } from '../menus/menu-ids';
|
|
||||||
|
|
||||||
export function ViewportDataOverlayMenuWrapper({
|
type DataOverlayMenuProps = {
|
||||||
viewportId,
|
|
||||||
displaySets,
|
|
||||||
location,
|
|
||||||
}: withAppTypes<{
|
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
element: HTMLElement;
|
|
||||||
location: string;
|
location: string;
|
||||||
}>): ReactNode {
|
isOpen?: boolean;
|
||||||
const [viewportGrid] = useViewportGrid();
|
onOpen?: () => void;
|
||||||
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
|
onClose?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const isMenuOpen =
|
export function ViewportDataOverlayMenuWrapper(props: DataOverlayMenuProps): ReactNode {
|
||||||
actionCornerState.viewports[viewportId]?.[location]?.find(
|
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
|
||||||
item => item.id === MENU_IDS.DATA_OVERLAY_MENU
|
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportId);
|
||||||
)?.isOpen ?? false;
|
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
|
||||||
|
|
||||||
const handleOpenChange = (openState: boolean) => {
|
const handleOpenChange = (openState: boolean) => {
|
||||||
if (openState) {
|
if (openState) {
|
||||||
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
|
onOpen?.();
|
||||||
} else {
|
} else {
|
||||||
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
|
onClose?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get proper alignment and side based on the location
|
const { servicesManager } = useSystem();
|
||||||
let align = 'center';
|
const { toolbarService } = servicesManager.services;
|
||||||
let side = 'bottom';
|
|
||||||
|
|
||||||
if (location !== undefined) {
|
const { align, side } = toolbarService.getAlignAndSide(location);
|
||||||
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
|
|
||||||
align = positioning.align;
|
const Icon = <Icons.ViewportViews className={iconClassName} />;
|
||||||
side = positioning.side;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={isMenuOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
asChild
|
asChild
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Button
|
<div>
|
||||||
variant="ghost"
|
{IconContainer ? (
|
||||||
size="icon"
|
<IconContainer
|
||||||
className={cn(
|
disabled={disabled}
|
||||||
viewportGrid.activeViewportId === viewportId
|
icon="ViewportViews"
|
||||||
? 'visible'
|
{...rest}
|
||||||
: 'invisible group-hover/pane:visible'
|
{...containerProps}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</IconContainer>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
<Icons.ViewportViews className={classNames('text-highlight')} />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="border-none bg-transparent p-0 shadow-none"
|
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 }) {
|
export function getEnhancedDisplaySets({ viewportId, services }) {
|
||||||
const { displaySetService, viewportGridService } = services;
|
const { displaySetService, viewportGridService } = services;
|
||||||
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
|
|
||||||
|
if (!displaySetsUIDs?.length) {
|
||||||
|
return {
|
||||||
|
viewportDisplaySets: [],
|
||||||
|
enhancedDisplaySets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const allDisplaySets = displaySetService.getActiveDisplaySets();
|
const allDisplaySets = displaySetService.getActiveDisplaySets();
|
||||||
|
|
||||||
const otherDisplaySets = allDisplaySets.filter(
|
const otherDisplaySets = allDisplaySets.filter(
|
||||||
@ -52,7 +60,7 @@ export function getEnhancedDisplaySets({ viewportId, services }) {
|
|||||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||||
);
|
);
|
||||||
|
|
||||||
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds);
|
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds || []);
|
||||||
const backgroundDisplaySet = viewportDisplaySets[0];
|
const backgroundDisplaySet = viewportDisplaySets[0];
|
||||||
|
|
||||||
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {
|
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {
|
||||||
|
|||||||
@ -1,40 +1,37 @@
|
|||||||
import React from 'react';
|
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 { useSystem } from '@ohif/core';
|
||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
import {
|
import { Popover, PopoverTrigger, PopoverContent, Button, useViewportGrid } from '@ohif/ui-next';
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
Button,
|
|
||||||
} from '@ohif/ui-next';
|
|
||||||
import { MENU_IDS } from '../menus/menu-ids';
|
|
||||||
|
|
||||||
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 { servicesManager, commandsManager } = useSystem();
|
||||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
|
||||||
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 handleOrientationChange = (orientation: string) => {
|
const handleOrientationChange = (orientation: string) => {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportIdToUse);
|
||||||
const currentViewportType = viewportInfo?.getViewportType();
|
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) {
|
if (!displaySets.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -63,11 +60,13 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
|||||||
orientationEnum = Enums.OrientationAxis.ACQUISITION;
|
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 viewport is not already a volume type, we need to convert it
|
||||||
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
// Configure the viewport to be a volume viewport with current display sets
|
// Configure the viewport to be a volume viewport with current display sets
|
||||||
const updatedViewport = {
|
const updatedViewport = {
|
||||||
viewportId,
|
viewportId: viewportIdToUse,
|
||||||
displaySetInstanceUIDs: displaySetUIDs,
|
displaySetInstanceUIDs: displaySetUIDs,
|
||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||||
@ -83,78 +82,99 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
|
|||||||
} else {
|
} else {
|
||||||
// Set the viewport orientation
|
// Set the viewport orientation
|
||||||
commandsManager.runCommand('setViewportOrientation', {
|
commandsManager.runCommand('setViewportOrientation', {
|
||||||
viewportId,
|
viewportId: viewportIdToUse,
|
||||||
orientation: orientationEnum,
|
orientation: orientationEnum,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the menu after selection
|
// Close the menu after selection
|
||||||
viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenChange = (openState: boolean) => {
|
const handleOpenChange = (openState: boolean) => {
|
||||||
if (openState) {
|
if (openState) {
|
||||||
viewportActionCornersServiceAPI.openItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
|
onOpen?.();
|
||||||
} else {
|
} 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) {
|
if (!displaySets.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
|
// Get proper alignment and side based on the location using toolbar service
|
||||||
|
const { align, side } = toolbarService.getAlignAndSide(Number(location));
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const Icon = <Icons.OrientationSwitch className={iconClassName} />;
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<Popover
|
||||||
open={isMenuOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<PopoverTrigger
|
||||||
<Button
|
asChild
|
||||||
variant="ghost"
|
className={cn('flex items-center justify-center')}
|
||||||
size="icon"
|
>
|
||||||
className="text-highlight"
|
<div>
|
||||||
disabled={!hasReconstructableDisplaySet}
|
{IconContainer ? (
|
||||||
>
|
<IconContainer
|
||||||
<Icons.OrientationSwitch />
|
disabled={disabled}
|
||||||
</Button>
|
icon="OrientationSwitch"
|
||||||
</DropdownMenuTrigger>
|
{...props}
|
||||||
<DropdownMenuContent
|
{...containerProps}
|
||||||
className="min-w-[160px]"
|
>
|
||||||
|
{Icon}
|
||||||
|
</IconContainer>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[100px] p-1"
|
||||||
align={align}
|
align={align}
|
||||||
side={side}
|
side={side}
|
||||||
sideOffset={5}
|
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="-ml-1">Orientation</DropdownMenuLabel>
|
<div className="flex flex-col">
|
||||||
<DropdownMenuItem onClick={() => handleOrientationChange('axial')}>Axial</DropdownMenuItem>
|
<Button
|
||||||
<DropdownMenuItem onClick={() => handleOrientationChange('sagittal')}>
|
variant="ghost"
|
||||||
Sagittal
|
className="justify-start"
|
||||||
</DropdownMenuItem>
|
onClick={() => handleOrientationChange('axial')}
|
||||||
<DropdownMenuItem onClick={() => handleOrientationChange('coronal')}>
|
>
|
||||||
Coronal
|
Axial
|
||||||
</DropdownMenuItem>
|
</Button>
|
||||||
<DropdownMenuItem onClick={() => handleOrientationChange('acquisition')}>
|
<Button
|
||||||
Acquisition
|
variant="ghost"
|
||||||
</DropdownMenuItem>
|
className="justify-start"
|
||||||
</DropdownMenuContent>
|
onClick={() => handleOrientationChange('sagittal')}
|
||||||
</DropdownMenu>
|
>
|
||||||
|
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 React, { ReactNode } from 'react';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
|
||||||
import { cn } from '@ohif/ui-next';
|
|
||||||
import ViewportOrientationMenu from './ViewportOrientationMenu';
|
import ViewportOrientationMenu from './ViewportOrientationMenu';
|
||||||
|
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||||
|
|
||||||
export function ViewportOrientationMenuWrapper({
|
export function ViewportOrientationMenuWrapper(
|
||||||
viewportId,
|
props: withAppTypes<{
|
||||||
location,
|
viewportId: string;
|
||||||
}: withAppTypes<{
|
location: string;
|
||||||
viewportId: string;
|
isOpen?: boolean;
|
||||||
element: HTMLElement;
|
onOpen?: () => void;
|
||||||
location: string;
|
onClose?: () => void;
|
||||||
}>): ReactNode {
|
iconSize?: number;
|
||||||
const [viewportGrid] = useViewportGrid();
|
disabled?: boolean;
|
||||||
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
|
}>
|
||||||
|
): ReactNode {
|
||||||
|
const { viewportId } = props;
|
||||||
|
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
|
||||||
|
|
||||||
|
if (!viewportDisplaySets.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ViewportOrientationMenu
|
||||||
className={cn(
|
{...props}
|
||||||
'flex justify-end',
|
displaySets={viewportDisplaySets}
|
||||||
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ViewportOrientationMenu location={location} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 PropTypes from 'prop-types';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { PanelSection, WindowLevel } from '@ohif/ui-next';
|
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 { useActiveViewportDisplaySets } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
getNodeOpacity,
|
getNodeOpacity,
|
||||||
@ -22,7 +22,7 @@ const ViewportWindowLevel = ({
|
|||||||
const { cornerstoneViewportService } = servicesManager.services;
|
const { cornerstoneViewportService } = servicesManager.services;
|
||||||
const [windowLevels, setWindowLevels] = useState<any[]>([]);
|
const [windowLevels, setWindowLevels] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const displaySets = useActiveViewportDisplaySets({ servicesManager });
|
const displaySets = useActiveViewportDisplaySets();
|
||||||
|
|
||||||
const getViewportsWithVolumeIds = useCallback(
|
const getViewportsWithVolumeIds = useCallback(
|
||||||
(volumeIds: string[]) => {
|
(volumeIds: string[]) => {
|
||||||
@ -30,7 +30,7 @@ const ViewportWindowLevel = ({
|
|||||||
const viewports = renderingEngine.getVolumeViewports();
|
const viewports = renderingEngine.getVolumeViewports();
|
||||||
|
|
||||||
return viewports.filter(vp => {
|
return viewports.filter(vp => {
|
||||||
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
|
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : [];
|
||||||
return (
|
return (
|
||||||
volumeIds.length === viewportVolumeIds.length &&
|
volumeIds.length === viewportVolumeIds.length &&
|
||||||
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||||
@ -124,7 +124,8 @@ const ViewportWindowLevel = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
|
const viewportVolumeIds =
|
||||||
|
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : [];
|
||||||
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||||
|
|
||||||
viewports.forEach(vp => {
|
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 { Switch } from '@ohif/ui-next';
|
||||||
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
import { utilities } from '@cornerstonejs/core';
|
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
|
|
||||||
export function setViewportColorbar(
|
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||||
viewportId: string,
|
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
|
||||||
commandsManager: any,
|
|
||||||
servicesManager: AppTypes.ServicesManager,
|
|
||||||
colorbarOptions: Partial<ColorbarOptions>
|
|
||||||
) {
|
|
||||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
|
||||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
|
||||||
|
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
const handleToggle = useCallback(() => {
|
||||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
toggleColorbar();
|
||||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
}, [toggleColorbar]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded">
|
||||||
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="flex w-7 flex-shrink-0 items-center justify-center"></div>
|
<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
|
<Switch
|
||||||
className="ml-2 flex-shrink-0"
|
className="ml-2 flex-shrink-0"
|
||||||
checked={showColorbar}
|
checked={!!hasColorbar}
|
||||||
onClick={e => {
|
onCheckedChange={handleToggle}
|
||||||
e.stopPropagation();
|
|
||||||
onSetColorbar();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,91 +1,66 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||||
import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
||||||
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
|
|
||||||
import { StackViewport, Types } from '@cornerstonejs/core';
|
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||||
import { ColormapProps } from '../../types/Colormap';
|
const { viewportDisplaySets } = useViewportRendering(viewportId);
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
|
|
||||||
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
|
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
|
||||||
const { servicesManager, commandsManager } = useSystem();
|
viewportDisplaySets?.[0]?.displaySetInstanceUID
|
||||||
const { cornerstoneViewportService } = servicesManager.services;
|
);
|
||||||
|
|
||||||
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 [showPreview, setShowPreview] = useState(false);
|
||||||
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
|
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
|
||||||
|
const [currentColormap, setCurrentColormap] = useState(null);
|
||||||
|
|
||||||
const showPreviewRef = useRef(showPreview);
|
const showPreviewRef = useRef(showPreview);
|
||||||
showPreviewRef.current = showPreview;
|
showPreviewRef.current = showPreview;
|
||||||
const prePreviewColormapRef = useRef(prePreviewColormap);
|
const prePreviewColormapRef = useRef(prePreviewColormap);
|
||||||
prePreviewColormapRef.current = prePreviewColormap;
|
prePreviewColormapRef.current = prePreviewColormap;
|
||||||
const activeDisplaySetRef = useRef(activeDisplaySet);
|
const currentColormapRef = useRef(currentColormap);
|
||||||
activeDisplaySetRef.current = activeDisplaySet;
|
currentColormapRef.current = currentColormap;
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveDisplaySet(displaySets[displaySets.length - 1]);
|
setCurrentColormap(null);
|
||||||
}, [displaySets]);
|
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 (
|
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">
|
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={String(activeIndex)}
|
value={activeDisplaySetUID}
|
||||||
onValueChange={val => {
|
onValueChange={displaySetUID => {
|
||||||
const index = parseInt(val, 10);
|
setActiveDisplaySetUID(displaySetUID);
|
||||||
setActiveDisplaySet(displaySets[index]);
|
|
||||||
setPrePreviewColormap(null);
|
setPrePreviewColormap(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{displaySets.map((ds, i) => (
|
{viewportDisplaySets.map(ds => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={i}
|
key={ds.displaySetInstanceUID}
|
||||||
value={String(i)}
|
value={ds.displaySetInstanceUID}
|
||||||
>
|
>
|
||||||
{ds.Modality}
|
{ds.Modality}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -94,6 +69,7 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
|
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)}
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
@ -104,49 +80,47 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
|
|||||||
checked={showPreview}
|
checked={showPreview}
|
||||||
onCheckedChange={checked => {
|
onCheckedChange={checked => {
|
||||||
setShowPreview(checked);
|
setShowPreview(checked);
|
||||||
|
|
||||||
|
if (!checked && currentColormapRef.current) {
|
||||||
|
handleSetColorLUT(currentColormapRef.current);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AllInOneMenu.DividerItem />
|
<AllInOneMenu.DividerItem />
|
||||||
<AllInOneMenu.ItemPanel
|
|
||||||
maxHeight="calc(100vh - 250px)"
|
<div className="h-[300px] flex-grow">
|
||||||
className="min-h-[200px] flex-grow"
|
<ScrollArea className="h-full w-full">
|
||||||
>
|
<div className="p-1">
|
||||||
{colormaps.map((colormap, index) => (
|
{colormaps.map((colormap, index) => (
|
||||||
<AllInOneMenu.Item
|
<AllInOneMenu.Item
|
||||||
key={index}
|
key={index}
|
||||||
label={colormap.description}
|
label={colormap.description}
|
||||||
useIconSpace={false}
|
useIconSpace={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSetColorLUT({
|
setCurrentColormap(colormap);
|
||||||
viewportId,
|
handleSetColorLUT(colormap);
|
||||||
colormap,
|
setPrePreviewColormap(null);
|
||||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
}}
|
||||||
});
|
onMouseEnter={() => {
|
||||||
setPrePreviewColormap(null);
|
if (showPreviewRef.current) {
|
||||||
}}
|
if (!prePreviewColormapRef.current) {
|
||||||
onMouseEnter={() => {
|
setPrePreviewColormap(colormap);
|
||||||
if (showPreviewRef.current) {
|
}
|
||||||
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
|
handleSetColorLUT(colormap);
|
||||||
onSetColorLUT({
|
}
|
||||||
viewportId,
|
}}
|
||||||
colormap,
|
onMouseLeave={() => {
|
||||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
if (showPreviewRef.current && prePreviewColormapRef.current) {
|
||||||
});
|
handleSetColorLUT(prePreviewColormapRef.current);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
/>
|
||||||
if (showPreviewRef.current && prePreviewColormapRef.current) {
|
))}
|
||||||
onSetColorLUT({
|
</div>
|
||||||
viewportId,
|
</ScrollArea>
|
||||||
colormap: prePreviewColormapRef.current,
|
</div>
|
||||||
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></AllInOneMenu.Item>
|
|
||||||
))}
|
|
||||||
</AllInOneMenu.ItemPanel>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
import React, { ReactElement, useState } from 'react';
|
import React, { ReactElement, useState } from 'react';
|
||||||
import { AllInOneMenu } from '@ohif/ui-next';
|
import { AllInOneMenu } from '@ohif/ui-next';
|
||||||
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
|
|
||||||
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
|
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
|
||||||
import { VolumeShift } from './VolumeShift';
|
import { VolumeShift } from './VolumeShift';
|
||||||
import { VolumeLighting } from './VolumeLighting';
|
import { VolumeLighting } from './VolumeLighting';
|
||||||
import { VolumeShade } from './VolumeShade';
|
import { VolumeShade } from './VolumeShade';
|
||||||
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
|
|
||||||
export function VolumeRenderingOptions({
|
export function VolumeRenderingOptions({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||||
viewportId,
|
const { volumeRenderingQualityRange } = useViewportRendering(viewportId);
|
||||||
volumeRenderingQualityRange,
|
|
||||||
}: VolumeRenderingOptionsProps): ReactElement {
|
|
||||||
const [hasShade, setShade] = useState(false);
|
const [hasShade, setShade] = useState(false);
|
||||||
return (
|
return (
|
||||||
<AllInOneMenu.ItemPanel>
|
<AllInOneMenu.ItemPanel>
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { AllInOneMenu } from '@ohif/ui-next';
|
import { AllInOneMenu } from '@ohif/ui-next';
|
||||||
import { Icons } from '@ohif/ui-next';
|
import { Icons } from '@ohif/ui-next';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
|
|
||||||
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
|
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
|
||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
|
|
||||||
export function VolumeRenderingPresets({
|
export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||||
viewportId,
|
const { volumeRenderingPresets } = useViewportRendering(viewportId);
|
||||||
volumeRenderingPresets,
|
|
||||||
}: VolumeRenderingPresetsProps): ReactElement {
|
|
||||||
const { servicesManager } = useSystem();
|
const { servicesManager } = useSystem();
|
||||||
const { uiDialogService } = servicesManager.services;
|
const { uiDialogService } = servicesManager.services;
|
||||||
|
|
||||||
|
|||||||
@ -1,54 +1,122 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||||
import { AllInOneMenu } from '@ohif/ui-next';
|
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
|
||||||
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
import { WindowLevelPreset } from '../../types/WindowLevel';
|
import { WindowLevelPreset } from '../../types/WindowLevel';
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export type WindowLevelProps = {
|
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
|
||||||
viewportId: string;
|
const { viewportDisplaySets } = useViewportRendering(viewportId);
|
||||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
|
||||||
};
|
viewportDisplaySets?.[0]?.displaySetInstanceUID
|
||||||
|
|
||||||
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]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<AllInOneMenu.ItemPanel>
|
<>
|
||||||
{presets.map((modalityPresets, modalityIndex) => (
|
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
|
||||||
<React.Fragment key={modalityIndex}>
|
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
|
||||||
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
|
<Tabs
|
||||||
<React.Fragment key={modality}>
|
value={activeDisplaySetUID}
|
||||||
<AllInOneMenu.HeaderItem>
|
onValueChange={displaySetUID => {
|
||||||
{t('Modality Presets', { modality })}
|
setActiveDisplaySetUID(displaySetUID);
|
||||||
</AllInOneMenu.HeaderItem>
|
}}
|
||||||
{presetsArray.map((preset, index) => (
|
>
|
||||||
<AllInOneMenu.Item
|
<TabsList>
|
||||||
key={`${modality}-${index}`}
|
{viewportDisplaySets.map(ds => (
|
||||||
label={preset.description}
|
<TabsTrigger
|
||||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
key={ds.displaySetInstanceUID}
|
||||||
useIconSpace={false}
|
value={ds.displaySetInstanceUID}
|
||||||
onClick={() => onSetWindowLevel(preset)}
|
>
|
||||||
/>
|
{ds.Modality}
|
||||||
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</TabsList>
|
||||||
))}
|
</Tabs>
|
||||||
</React.Fragment>
|
</div>
|
||||||
))}
|
)}
|
||||||
</AllInOneMenu.ItemPanel>
|
|
||||||
|
<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 { useTranslation } from 'react-i18next';
|
||||||
import { AllInOneMenu, cn } from '@ohif/ui-next';
|
import { AllInOneMenu } from '@ohif/ui-next';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
|
||||||
import { Colormap } from './Colormap';
|
import { Colormap } from './Colormap';
|
||||||
import { Colorbar } from './Colorbar';
|
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 { WindowLevel } from './WindowLevel';
|
||||||
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
|
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
|
||||||
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
|
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
|
||||||
import { ViewportPreset } from '../../types/ViewportPresets';
|
import { useViewportRendering } from '../../hooks/useViewportRendering';
|
||||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
|
||||||
import { utilities } from '@cornerstonejs/core';
|
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
|
|
||||||
export type WindowLevelActionMenuProps = {
|
export type WindowLevelActionMenuProps = {
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
element: HTMLElement;
|
element?: HTMLElement;
|
||||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
align?: 'start' | 'end' | 'center';
|
||||||
colorbarProperties: ColorbarProperties;
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
displaySets: Array<any>;
|
|
||||||
volumeRenderingPresets: Array<ViewportPreset>;
|
|
||||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
|
||||||
location: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WindowLevelActionMenu({
|
export function WindowLevelActionMenu({
|
||||||
viewportId,
|
viewportId,
|
||||||
element,
|
element,
|
||||||
presets,
|
align,
|
||||||
verticalDirection,
|
side,
|
||||||
horizontalDirection,
|
}: WindowLevelActionMenuProps): ReactElement {
|
||||||
colorbarProperties,
|
return (
|
||||||
displaySets,
|
<WindowLevelActionMenuContent
|
||||||
volumeRenderingPresets,
|
viewportId={viewportId}
|
||||||
volumeRenderingQualityRange,
|
align={align}
|
||||||
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
|
side={side}
|
||||||
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;
|
|
||||||
|
|
||||||
|
export function WindowLevelActionMenuContent({
|
||||||
|
viewportId,
|
||||||
|
align,
|
||||||
|
side,
|
||||||
|
}: {
|
||||||
|
viewportId: string;
|
||||||
|
align?: string;
|
||||||
|
side?: string;
|
||||||
|
}): ReactElement {
|
||||||
const { t } = useTranslation('WindowLevelActionMenu');
|
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 {
|
||||||
const { activeViewportId } = viewportGrid;
|
is3DVolume,
|
||||||
|
|
||||||
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,
|
|
||||||
colorbarProperties,
|
colorbarProperties,
|
||||||
activeViewportId,
|
windowLevelPresets,
|
||||||
viewportGrid,
|
volumeRenderingPresets,
|
||||||
]);
|
volumeRenderingQualityRange,
|
||||||
|
} = useViewportRendering(viewportId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AllInOneMenu.Menu
|
<AllInOneMenu.Menu
|
||||||
@ -116,60 +57,38 @@ export function WindowLevelActionMenu({
|
|||||||
key={menuKey}
|
key={menuKey}
|
||||||
// the visibility is handled by the parent component
|
// the visibility is handled by the parent component
|
||||||
isVisible={true}
|
isVisible={true}
|
||||||
horizontalDirection={horizontalDirection}
|
align={align}
|
||||||
verticalDirection={verticalDirection}
|
side={side}
|
||||||
>
|
>
|
||||||
<AllInOneMenu.ItemPanel>
|
<AllInOneMenu.ItemPanel>
|
||||||
{!is3DVolume && (
|
{!is3DVolume && <Colorbar viewportId={viewportId} />}
|
||||||
<Colorbar
|
|
||||||
viewportId={viewportId}
|
|
||||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
|
||||||
colorbarProperties={colorbarProperties}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{colormaps && !is3DVolume && (
|
{colorbarProperties?.colormaps && !is3DVolume && (
|
||||||
<AllInOneMenu.SubMenu
|
<AllInOneMenu.SubMenu
|
||||||
key="colorLUTPresets"
|
key="colorLUTPresets"
|
||||||
itemLabel="Color LUT"
|
itemLabel="Color LUT"
|
||||||
itemIcon="icon-color-lut"
|
itemIcon="icon-color-lut"
|
||||||
className="flex h-[calc(100%-32px)] flex-col"
|
className="flex h-[calc(100%-32px)] flex-col"
|
||||||
>
|
>
|
||||||
<Colormap
|
<Colormap viewportId={viewportId} />
|
||||||
className="flex h-full w-full flex-col"
|
|
||||||
colormaps={colormaps}
|
|
||||||
viewportId={viewportId}
|
|
||||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
|
||||||
/>
|
|
||||||
</AllInOneMenu.SubMenu>
|
</AllInOneMenu.SubMenu>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{presets && presets.length > 0 && !is3DVolume && (
|
{windowLevelPresets?.length > 0 && !is3DVolume && (
|
||||||
<AllInOneMenu.SubMenu
|
<AllInOneMenu.SubMenu
|
||||||
key="windowLevelPresets"
|
key="windowLevelPresets"
|
||||||
itemLabel={t('Modality Window Presets')}
|
itemLabel={t('Modality Window Presets')}
|
||||||
itemIcon="viewport-window-level"
|
itemIcon="viewport-window-level"
|
||||||
>
|
>
|
||||||
<WindowLevel
|
<WindowLevel viewportId={viewportId} />
|
||||||
viewportId={viewportId}
|
|
||||||
presets={presets}
|
|
||||||
/>
|
|
||||||
</AllInOneMenu.SubMenu>
|
</AllInOneMenu.SubMenu>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{volumeRenderingPresets && is3DVolume && (
|
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
|
||||||
<VolumeRenderingPresets
|
|
||||||
viewportId={viewportId}
|
|
||||||
volumeRenderingPresets={volumeRenderingPresets}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{volumeRenderingQualityRange && is3DVolume && (
|
{volumeRenderingQualityRange && is3DVolume && (
|
||||||
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
|
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
|
||||||
<VolumeRenderingOptions
|
<VolumeRenderingOptions viewportId={viewportId} />
|
||||||
viewportId={viewportId}
|
|
||||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
|
||||||
/>
|
|
||||||
</AllInOneMenu.SubMenu>
|
</AllInOneMenu.SubMenu>
|
||||||
)}
|
)}
|
||||||
</AllInOneMenu.ItemPanel>
|
</AllInOneMenu.ItemPanel>
|
||||||
|
|||||||
@ -2,63 +2,75 @@ import React, { ReactNode } from 'react';
|
|||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
cn,
|
|
||||||
Icons,
|
Icons,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
useViewportActionCorners,
|
useIconPresentation,
|
||||||
AllInOneMenu,
|
|
||||||
} from '@ohif/ui-next';
|
} from '@ohif/ui-next';
|
||||||
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
|
||||||
import { MENU_IDS } from '../menus/menu-ids';
|
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||||
|
import { useViewportRendering } from '../../hooks';
|
||||||
|
|
||||||
export function WindowLevelActionMenuWrapper({
|
export function WindowLevelActionMenuWrapper(
|
||||||
viewportId,
|
props: withAppTypes<{
|
||||||
element,
|
viewportId: string;
|
||||||
location,
|
element?: HTMLElement;
|
||||||
displaySets,
|
location?: number;
|
||||||
}: withAppTypes<{
|
isOpen?: boolean;
|
||||||
viewportId: string;
|
onOpen?: () => void;
|
||||||
element: HTMLElement;
|
onClose?: () => void;
|
||||||
location: string;
|
displaySets?: Array<AppTypes.DisplaySet>;
|
||||||
displaySets: Array<AppTypes.DisplaySet>;
|
disabled?: boolean;
|
||||||
}>): ReactNode {
|
isEmbedded?: boolean;
|
||||||
const [viewportGrid] = useViewportGrid();
|
}>
|
||||||
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
|
): ReactNode {
|
||||||
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
|
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 { servicesManager } = useSystem();
|
||||||
const { customizationService } = servicesManager.services;
|
const { toolbarService } = servicesManager.services;
|
||||||
|
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
|
||||||
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
|
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
|
||||||
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 handleOpenChange = (openState: boolean) => {
|
const handleOpenChange = (openState: boolean) => {
|
||||||
|
const shouldToggleColorbar = hasColorbar && !isEmbedded;
|
||||||
|
|
||||||
|
if (isOpen && shouldToggleColorbar && openState) {
|
||||||
|
toggleColorbar();
|
||||||
|
onClose?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen && openState && shouldToggleColorbar) {
|
||||||
|
toggleColorbar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (openState) {
|
if (openState) {
|
||||||
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
onOpen?.();
|
||||||
} else {
|
} else {
|
||||||
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
|
onClose?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { align, side, horizontalDirection, verticalDirection } = getMenuDirections(
|
const { align, side } = toolbarService.getAlignAndSide(location);
|
||||||
location,
|
|
||||||
viewportActionCornersAPI
|
|
||||||
);
|
|
||||||
|
|
||||||
const displaySetPresets = displaySets
|
|
||||||
.filter(displaySet => presets[displaySet.Modality])
|
|
||||||
.map(displaySet => {
|
|
||||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
||||||
|
|
||||||
@ -66,25 +78,41 @@ export function WindowLevelActionMenuWrapper({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Icon =
|
||||||
|
hasColorbar && !isEmbedded && hasEmbeddedVariantToUse ? (
|
||||||
|
<Icons.Close className={iconClassName} />
|
||||||
|
) : (
|
||||||
|
<Icons.ViewportWindowLevel className={iconClassName} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={isMenuOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
asChild
|
asChild
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Button
|
<div>
|
||||||
variant="ghost"
|
{IconContainer ? (
|
||||||
size="icon"
|
<IconContainer
|
||||||
className={cn(
|
disabled={disabled}
|
||||||
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible',
|
{...rest}
|
||||||
'text-highlight'
|
{...containerProps}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</IconContainer>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
<Icons.ByName name="viewport-window-level" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="border-none bg-transparent p-0 shadow-none"
|
className="border-none bg-transparent p-0 shadow-none"
|
||||||
@ -94,45 +122,12 @@ export function WindowLevelActionMenuWrapper({
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
>
|
>
|
||||||
<WindowLevelActionMenu
|
<WindowLevelActionMenu
|
||||||
viewportId={viewportId}
|
viewportId={viewportIdToUse}
|
||||||
element={element}
|
element={element}
|
||||||
presets={displaySetPresets}
|
align={align}
|
||||||
horizontalDirection={horizontalDirection}
|
side={side}
|
||||||
verticalDirection={verticalDirection}
|
|
||||||
colorbarProperties={colorbarProperties}
|
|
||||||
displaySets={displaySets}
|
|
||||||
volumeRenderingPresets={volumeRenderingPresets}
|
|
||||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</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 './SeriesMeasurements';
|
||||||
export * from './StudyMeasurementsActions';
|
export * from './StudyMeasurementsActions';
|
||||||
export * from './MeasurementsOrAdditionalFindings';
|
export * from './MeasurementsOrAdditionalFindings';
|
||||||
export * from './ViewportDataOverlaySettingMenu';
|
|
||||||
export * from './ViewportOrientationMenu';
|
|
||||||
export * from './WindowLevelActionMenu';
|
export * from './WindowLevelActionMenu';
|
||||||
|
export * from './ModalityLoadBadge';
|
||||||
|
|
||||||
export { DicomUpload };
|
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';
|
} from '../types/Colorbar';
|
||||||
import { ColorMapPreset } from '../types/Colormap';
|
import { ColorMapPreset } from '../types/Colormap';
|
||||||
|
|
||||||
const defaultPosition: ColorbarPositionType = 'right';
|
const defaultPosition: ColorbarPositionType = 'bottom';
|
||||||
const DefaultColormap = 'Grayscale';
|
const DefaultColormap = 'Grayscale';
|
||||||
|
|
||||||
const positionStyles: PositionStylesMapType = {
|
const positionStyles: PositionStylesMapType = {
|
||||||
left: { left: '5%', width: '15px' },
|
left: { width: '15px' },
|
||||||
right: { right: '5%', width: '15px' },
|
right: { width: '15px' },
|
||||||
bottom: { bottom: '1%', height: '18px' },
|
bottom: { height: '15px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typed position-specific tick styles
|
// 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 miscCustomization from './customizations/miscCustomization';
|
||||||
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
|
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
|
||||||
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
|
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
|
||||||
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
|
|
||||||
|
|
||||||
function getCustomizationModule({ commandsManager, servicesManager }) {
|
function getCustomizationModule({ commandsManager, servicesManager }) {
|
||||||
return [
|
return [
|
||||||
@ -31,7 +30,6 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
|
|||||||
...miscCustomization,
|
...miscCustomization,
|
||||||
...captureViewportModalCustomization,
|
...captureViewportModalCustomization,
|
||||||
...viewportDownloadWarningCustomization,
|
...viewportDownloadWarningCustomization,
|
||||||
...viewportActionMenuCustomizations,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,23 +1,358 @@
|
|||||||
import { Enums } from '@cornerstonejs/tools';
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
import { utils } from '@ohif/ui-next';
|
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) => ({
|
const getDisabledState = (disabledText?: string) => ({
|
||||||
disabled: true,
|
disabled: true,
|
||||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
export default function getToolbarModule({ servicesManager, extensionManager }: withAppTypes) {
|
||||||
const {
|
const {
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
toolbarService,
|
toolbarService,
|
||||||
syncGroupService,
|
syncGroupService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
hangingProtocolService,
|
colorbarService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
|
segmentationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
return [
|
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
|
// functions/helpers to be used by the toolbar buttons to decide if they should
|
||||||
// enabled or not
|
// enabled or not
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
export * from './useViewportActionCorners';
|
|
||||||
export * from './useActiveViewportSegmentationRepresentations';
|
export * from './useActiveViewportSegmentationRepresentations';
|
||||||
export * from './useMeasurements';
|
export * from './useMeasurements';
|
||||||
export * from './useSegmentations';
|
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 { useViewportGrid } from '@ohif/ui-next';
|
||||||
import debounce from 'lodash.debounce';
|
import { useViewportSegmentations } from './useViewportSegmentations';
|
||||||
import { roundNumber } from '@ohif/core/src/utils';
|
|
||||||
import {
|
|
||||||
SegmentationData,
|
|
||||||
SegmentationRepresentation,
|
|
||||||
} from '../services/SegmentationService/SegmentationService';
|
|
||||||
|
|
||||||
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
|
function useActiveViewportSegmentationRepresentations() {
|
||||||
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
|
||||||
function mapSegmentationToDisplay(segmentation, customizationService) {
|
const viewportId = viewportGrid?.activeViewportId;
|
||||||
const { label, segments } = segmentation;
|
|
||||||
|
|
||||||
// Get the readable text mapping once
|
const segmentations = useViewportSegmentations({ viewportId });
|
||||||
const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText');
|
|
||||||
|
|
||||||
// Helper function to recursively map cachedStats to readable display text
|
return segmentations;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export { useActiveViewportSegmentationRepresentations };
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|||||||
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 { useState, useEffect } from 'react';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
function mapMeasurementToDisplay(measurement, displaySetService) {
|
function mapMeasurementToDisplay(measurement, displaySetService) {
|
||||||
const { referenceSeriesUID } = measurement;
|
const { referenceSeriesUID } = measurement;
|
||||||
|
|
||||||
@ -56,13 +56,14 @@ function mapMeasurementToDisplay(measurement, displaySetService) {
|
|||||||
* @param {Object} options.valueTypes - The value types for mapping measurements.
|
* @param {Object} options.valueTypes - The value types for mapping measurements.
|
||||||
* @returns {Array} An array of mapped and filtered 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 { measurementService, displaySetService } = servicesManager.services;
|
||||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDisplayMeasurements = () => {
|
const updateDisplayMeasurements = () => {
|
||||||
let measurements = measurementService.getMeasurements(measurementFilter);
|
const measurements = measurementService.getMeasurements(measurementFilter);
|
||||||
const mappedMeasurements = measurements.map(m =>
|
const mappedMeasurements = measurements.map(m =>
|
||||||
mapMeasurementToDisplay(m, displaySetService)
|
mapMeasurementToDisplay(m, displaySetService)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -48,30 +48,38 @@ export type UseViewportDisplaySetsOptions = {
|
|||||||
* Return type for useViewportDisplaySets
|
* Return type for useViewportDisplaySets
|
||||||
*/
|
*/
|
||||||
export type ViewportDisplaySets = {
|
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)
|
* The primary display set for the viewport (base image)
|
||||||
*/
|
*/
|
||||||
backgroundDisplaySet?: any;
|
backgroundDisplaySet?: AppTypes.DisplaySet;
|
||||||
/**
|
/**
|
||||||
* Display sets currently shown with background (non-overlay layers)
|
* Display sets currently shown with background (non-overlay layers)
|
||||||
*/
|
*/
|
||||||
foregroundDisplaySets?: any[];
|
foregroundDisplaySets?: AppTypes.DisplaySet[];
|
||||||
/**
|
/**
|
||||||
* Segmentation display sets currently applied as overlays
|
* Segmentation display sets currently applied as overlays
|
||||||
*/
|
*/
|
||||||
overlayDisplaySets?: any[];
|
overlayDisplaySets?: AppTypes.DisplaySet[];
|
||||||
/**
|
/**
|
||||||
* Display sets that could be toggled on as overlays (derived modalities)
|
* 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
|
* Display sets that could be added as foreground layers
|
||||||
*/
|
*/
|
||||||
potentialForegroundDisplaySets?: any[];
|
potentialForegroundDisplaySets?: AppTypes.DisplaySet[];
|
||||||
/**
|
/**
|
||||||
* Display sets that could replace the current background
|
* 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
|
* @returns Object containing requested display set collections based on options
|
||||||
*/
|
*/
|
||||||
export function useViewportDisplaySets(
|
export function useViewportDisplaySets(
|
||||||
viewportId: string,
|
viewportId?: string,
|
||||||
options?: UseViewportDisplaySetsOptions
|
options?: UseViewportDisplaySetsOptions
|
||||||
): ViewportDisplaySets {
|
): ViewportDisplaySets {
|
||||||
const { servicesManager } = useSystem();
|
const { servicesManager } = useSystem();
|
||||||
@ -91,7 +99,9 @@ export function useViewportDisplaySets(
|
|||||||
// Note: this is very important we should use the useViewportGrid hook here,
|
// 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
|
// since if the viewport displaySet is changed we should re-run this hook
|
||||||
// to get the latest displaySets
|
// 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
|
// Apply defaults - include everything if no options specified
|
||||||
const {
|
const {
|
||||||
@ -114,8 +124,8 @@ export function useViewportDisplaySets(
|
|||||||
const needsSegmentations = includeOverlay;
|
const needsSegmentations = includeOverlay;
|
||||||
const segmentationRepresentations = useMemo(
|
const segmentationRepresentations = useMemo(
|
||||||
() =>
|
() =>
|
||||||
needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportId) : [],
|
needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportIdToUse) : [],
|
||||||
[segmentationService, viewportId, needsSegmentations]
|
[segmentationService, viewportIdToUse, needsSegmentations]
|
||||||
);
|
);
|
||||||
|
|
||||||
const overlayDisplaySets = useMemo(() => {
|
const overlayDisplaySets = useMemo(() => {
|
||||||
@ -142,10 +152,10 @@ export function useViewportDisplaySets(
|
|||||||
return { viewportDisplaySets: [], enhancedDisplaySets: [] };
|
return { viewportDisplaySets: [], enhancedDisplaySets: [] };
|
||||||
}
|
}
|
||||||
return getEnhancedDisplaySets({
|
return getEnhancedDisplaySets({
|
||||||
viewportId,
|
viewportId: viewportIdToUse,
|
||||||
services: { displaySetService, viewportGridService },
|
services: { displaySetService, viewportGridService },
|
||||||
});
|
});
|
||||||
}, [viewportId, displaySetService, viewportGridService, needsEnhancedDisplaySets]);
|
}, [viewportIdToUse, displaySetService, viewportGridService, needsEnhancedDisplaySets]);
|
||||||
|
|
||||||
const backgroundDisplaySet = useMemo(
|
const backgroundDisplaySet = useMemo(
|
||||||
() => (includeBackground ? viewportDisplaySets[0] : undefined),
|
() => (includeBackground ? viewportDisplaySets[0] : undefined),
|
||||||
@ -217,7 +227,7 @@ export function useViewportDisplaySets(
|
|||||||
foregroundDisplaySetUIDs,
|
foregroundDisplaySetUIDs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result: ViewportDisplaySets = {};
|
const result: ViewportDisplaySets = { allDisplaySets, viewportDisplaySets };
|
||||||
|
|
||||||
if (includeBackground) {
|
if (includeBackground) {
|
||||||
result.backgroundDisplaySet = backgroundDisplaySet;
|
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 RectangleROI from './utils/measurementServiceMappings/RectangleROI';
|
||||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
|
||||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||||
@ -56,6 +55,7 @@ import { useSegmentations } from './hooks/useSegmentations';
|
|||||||
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
import { useMeasurementTracking } from './hooks/useMeasurementTracking';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
||||||
@ -158,7 +158,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
||||||
servicesManager.registerService(SegmentationService.REGISTRATION);
|
servicesManager.registerService(SegmentationService.REGISTRATION);
|
||||||
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||||
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
|
|
||||||
servicesManager.registerService(ColorbarService.REGISTRATION);
|
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||||
|
|
||||||
const { syncGroupService } = servicesManager.services;
|
const { syncGroupService } = servicesManager.services;
|
||||||
@ -262,6 +261,8 @@ export {
|
|||||||
StudySummaryFromMetadata,
|
StudySummaryFromMetadata,
|
||||||
CornerstoneViewportDownloadForm,
|
CornerstoneViewportDownloadForm,
|
||||||
utils,
|
utils,
|
||||||
|
OHIFCornerstoneViewport,
|
||||||
|
useMeasurementTracking,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export constants
|
// Export constants
|
||||||
|
|||||||
@ -98,8 +98,13 @@ export default async function init({
|
|||||||
measurementService,
|
measurementService,
|
||||||
colorbarService,
|
colorbarService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
|
toolbarService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
toolbarService.registerEventForToolbarUpdate(colorbarService, [
|
||||||
|
colorbarService.EVENTS.STATE_CHANGED,
|
||||||
|
]);
|
||||||
|
|
||||||
window.services = servicesManager.services;
|
window.services = servicesManager.services;
|
||||||
window.extensionManager = extensionManager;
|
window.extensionManager = extensionManager;
|
||||||
window.commandsManager = commandsManager;
|
window.commandsManager = commandsManager;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSystem } from '@ohif/core';
|
|
||||||
|
|
||||||
import { useMeasurements } from '../hooks/useMeasurements';
|
import { useMeasurements } from '../hooks/useMeasurements';
|
||||||
import StudyMeasurements from '../components/StudyMeasurements';
|
import StudyMeasurements from '../components/StudyMeasurements';
|
||||||
@ -12,7 +11,7 @@ import StudyMeasurements from '../components/StudyMeasurements';
|
|||||||
*
|
*
|
||||||
* Creates a default study measurements panel with default children:
|
* Creates a default study measurements panel with default children:
|
||||||
* ```
|
* ```
|
||||||
* <PanelMEasurement>
|
* <PanelMeasurement>
|
||||||
* <StudyMeasurements />
|
* <StudyMeasurements />
|
||||||
* </PanelMeasurement>
|
* </PanelMeasurement>
|
||||||
* ```
|
* ```
|
||||||
@ -60,10 +59,7 @@ import StudyMeasurements from '../components/StudyMeasurements';
|
|||||||
export default function PanelMeasurement(props): React.ReactNode {
|
export default function PanelMeasurement(props): React.ReactNode {
|
||||||
const { measurementFilter, emptyComponent: EmptyComponent, children } = props;
|
const { measurementFilter, emptyComponent: EmptyComponent, children } = props;
|
||||||
|
|
||||||
const system = useSystem();
|
const displayMeasurements = useMeasurements({ measurementFilter });
|
||||||
const displayMeasurements = useMeasurements(system.servicesManager, {
|
|
||||||
measurementFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!displayMeasurements.length) {
|
if (!displayMeasurements.length) {
|
||||||
return EmptyComponent ? (
|
return EmptyComponent ? (
|
||||||
|
|||||||
@ -9,9 +9,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
|||||||
const { customizationService, displaySetService } = servicesManager.services;
|
const { customizationService, displaySetService } = servicesManager.services;
|
||||||
|
|
||||||
const { segmentationsWithRepresentations, disabled } =
|
const { segmentationsWithRepresentations, disabled } =
|
||||||
useActiveViewportSegmentationRepresentations({
|
useActiveViewportSegmentationRepresentations();
|
||||||
servicesManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract customization options
|
// Extract customization options
|
||||||
const segmentationTableMode = customizationService.getCustomization(
|
const segmentationTableMode = customizationService.getCustomization(
|
||||||
|
|||||||
@ -291,7 +291,7 @@ class SegmentationService extends PubSubService {
|
|||||||
const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
|
const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
|
||||||
csViewport,
|
csViewport,
|
||||||
segmentation
|
segmentation
|
||||||
);
|
) || { isVolumeViewport: false, isVolumeSegmentation: false };
|
||||||
|
|
||||||
({ representationTypeToUse, isConverted } = await this.handleViewportConversion(
|
({ representationTypeToUse, isConverted } = await this.handleViewportConversion(
|
||||||
isVolumeViewport,
|
isVolumeViewport,
|
||||||
@ -677,7 +677,6 @@ class SegmentationService extends PubSubService {
|
|||||||
|
|
||||||
// Mark the RT display set as loaded
|
// Mark the RT display set as loaded
|
||||||
rtDisplaySet.isLoaded = true;
|
rtDisplaySet.isLoaded = true;
|
||||||
|
|
||||||
// Add or update the segmentation in the state
|
// Add or update the segmentation in the state
|
||||||
this.addOrUpdateSegmentation(segmentation);
|
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 { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
|
||||||
import { IViewportService } from './IViewportService';
|
import { IViewportService } from './IViewportService';
|
||||||
import { RENDERING_ENGINE_ID } from './constants';
|
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 { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
import {
|
import {
|
||||||
LutPresentation,
|
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.
|
* Retrieves the Cornerstone viewport with the specified ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import CornerstoneViewportServiceType from '../services/ViewportService/Cornerst
|
|||||||
import SegmentationServiceType from '../services/SegmentationService';
|
import SegmentationServiceType from '../services/SegmentationService';
|
||||||
import SyncGroupServiceType from '../services/SyncGroupService';
|
import SyncGroupServiceType from '../services/SyncGroupService';
|
||||||
import ToolGroupServiceType from '../services/ToolGroupService';
|
import ToolGroupServiceType from '../services/ToolGroupService';
|
||||||
import ViewportActionCornersServiceType from '../services/ViewportActionCornersService/ViewportActionCornersService';
|
|
||||||
import ColorbarServiceType from '../services/ColorbarService';
|
import ColorbarServiceType from '../services/ColorbarService';
|
||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
@ -15,7 +14,7 @@ import type {
|
|||||||
SegmentationRepresentation as SegRep,
|
SegmentationRepresentation as SegRep,
|
||||||
SegmentationInfo as SegInfo,
|
SegmentationInfo as SegInfo,
|
||||||
} from '../services/SegmentationService/SegmentationService';
|
} from '../services/SegmentationService/SegmentationService';
|
||||||
|
import type { ViewportOptions as ViewportOpts } from '../services/ViewportService/Viewport';
|
||||||
declare global {
|
declare global {
|
||||||
namespace AppTypes {
|
namespace AppTypes {
|
||||||
export type CornerstoneCacheService = CornerstoneCacheServiceType;
|
export type CornerstoneCacheService = CornerstoneCacheServiceType;
|
||||||
@ -23,7 +22,6 @@ declare global {
|
|||||||
export type SegmentationService = SegmentationServiceType;
|
export type SegmentationService = SegmentationServiceType;
|
||||||
export type SyncGroupService = SyncGroupServiceType;
|
export type SyncGroupService = SyncGroupServiceType;
|
||||||
export type ToolGroupService = ToolGroupServiceType;
|
export type ToolGroupService = ToolGroupServiceType;
|
||||||
export type ViewportActionCornersService = ViewportActionCornersServiceType;
|
|
||||||
export type ColorbarService = ColorbarServiceType;
|
export type ColorbarService = ColorbarServiceType;
|
||||||
|
|
||||||
export interface Services {
|
export interface Services {
|
||||||
@ -32,7 +30,6 @@ declare global {
|
|||||||
syncGroupService?: SyncGroupServiceType;
|
syncGroupService?: SyncGroupServiceType;
|
||||||
segmentationService?: SegmentationServiceType;
|
segmentationService?: SegmentationServiceType;
|
||||||
cornerstoneCacheService?: CornerstoneCacheServiceType;
|
cornerstoneCacheService?: CornerstoneCacheServiceType;
|
||||||
viewportActionCornersService?: ViewportActionCornersServiceType;
|
|
||||||
colorbarService?: ColorbarServiceType;
|
colorbarService?: ColorbarServiceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,5 +51,7 @@ declare global {
|
|||||||
cornerstone?: typeof cornerstone;
|
cornerstone?: typeof cornerstone;
|
||||||
cornerstoneTools?: typeof cornerstoneTools;
|
cornerstoneTools?: typeof cornerstoneTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ViewportOptions = ViewportOpts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import SyncGroupService from '../services/SyncGroupService';
|
|||||||
import SegmentationService from '../services/SegmentationService';
|
import SegmentationService from '../services/SegmentationService';
|
||||||
import CornerstoneCacheService from '../services/CornerstoneCacheService';
|
import CornerstoneCacheService from '../services/CornerstoneCacheService';
|
||||||
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
|
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
|
||||||
import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
|
|
||||||
import ColorbarService from '../services/ColorbarService';
|
import ColorbarService from '../services/ColorbarService';
|
||||||
|
|
||||||
interface CornerstoneServices extends Types.Services {
|
interface CornerstoneServices extends Types.Services {
|
||||||
@ -13,7 +12,6 @@ interface CornerstoneServices extends Types.Services {
|
|||||||
syncGroupService: SyncGroupService;
|
syncGroupService: SyncGroupService;
|
||||||
segmentationService: SegmentationService;
|
segmentationService: SegmentationService;
|
||||||
cornerstoneCacheService: CornerstoneCacheService;
|
cornerstoneCacheService: CornerstoneCacheService;
|
||||||
viewportActionCornersService: ViewportActionCornersService;
|
|
||||||
colorbarService: ColorbarService;
|
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 * as CornerstoneCacheService from './CornerstoneCacheService';
|
||||||
import type CornerstoneServices from './CornerstoneServices';
|
import type CornerstoneServices from './CornerstoneServices';
|
||||||
export { type SyncGroup } from '../services/SyncGroupService';
|
export { type SyncGroup } from '../services/SyncGroupService';
|
||||||
export {
|
export { type HydrationCallback } from '../utils/promptHydrationDialog';
|
||||||
type ViewportActionCornerLocation,
|
|
||||||
type ViewportActionCornerService,
|
|
||||||
type CustomizationService,
|
|
||||||
} from '../hooks/useViewportActionCorners';
|
|
||||||
|
|
||||||
export type { CornerstoneCacheService, CornerstoneServices };
|
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,
|
setupSegmentationDataModifiedHandler,
|
||||||
setupSegmentationModifiedHandler,
|
setupSegmentationModifiedHandler,
|
||||||
} from './segmentationHandlers';
|
} from './segmentationHandlers';
|
||||||
|
import promptHydrationDialog, {
|
||||||
|
HydrationDialogProps,
|
||||||
|
HydrationCallback,
|
||||||
|
HydrationSRResult,
|
||||||
|
} from './promptHydrationDialog';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
handleSegmentChange,
|
handleSegmentChange,
|
||||||
isReferenceViewable,
|
isReferenceViewable,
|
||||||
setupSegmentationDataModifiedHandler,
|
setupSegmentationDataModifiedHandler,
|
||||||
setupSegmentationModifiedHandler,
|
setupSegmentationModifiedHandler,
|
||||||
|
promptHydrationDialog,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { HydrationDialogProps, HydrationCallback, HydrationSRResult };
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const Length = {
|
|||||||
|
|
||||||
const isLocked = getIsLocked(annotationUID);
|
const isLocked = getIsLocked(annotationUID);
|
||||||
const isVisible = getIsVisible(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]
|
// 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
|
// Todo: this should be in a utility
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const LivewireContour = {
|
|||||||
*/
|
*/
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsEventDetail,
|
csToolsEventDetail,
|
||||||
DisplaySetService,
|
displaySetService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
getValueTypeFromToolType,
|
getValueTypeFromToolType,
|
||||||
customizationService
|
customizationService
|
||||||
@ -43,16 +43,16 @@ const LivewireContour = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } =
|
const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } =
|
||||||
getSOPInstanceAttributes(referencedImageId);
|
getSOPInstanceAttributes(referencedImageId, displaySetService, annotation);
|
||||||
|
|
||||||
let displaySet;
|
let displaySet;
|
||||||
if (SOPInstanceUID) {
|
if (SOPInstanceUID) {
|
||||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -64,13 +64,14 @@ const LivewireContour = {
|
|||||||
metadata,
|
metadata,
|
||||||
frameNumber,
|
frameNumber,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referencedImageId,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
isLocked,
|
isLocked,
|
||||||
isVisible,
|
isVisible,
|
||||||
displayText: getDisplayText(annotation, displaySet),
|
displayText: getDisplayText(annotation, displaySet, displaySetService),
|
||||||
data: data.cachedStats,
|
data: data.cachedStats,
|
||||||
type: getValueTypeFromToolType(toolName),
|
type: getValueTypeFromToolType(toolName),
|
||||||
getReport: () => getColumnValueReport(annotation, customizationService),
|
getReport: () => getColumnValueReport(annotation, customizationService),
|
||||||
@ -122,20 +123,27 @@ function getColumnValueReport(annotation, customizationService) {
|
|||||||
*
|
*
|
||||||
* @param {Object} annotation - The annotation object.
|
* @param {Object} annotation - The annotation object.
|
||||||
* @param {Object} displaySet - The display set 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 { metadata, data } = annotation;
|
||||||
|
|
||||||
|
const displayText = {
|
||||||
|
primary: [],
|
||||||
|
secondary: [],
|
||||||
|
};
|
||||||
|
|
||||||
if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
|
if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
|
||||||
return [];
|
return displayText;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { area, areaUnit } = data.cachedStats[`imageId:${metadata.referencedImageId}`];
|
const { area, areaUnit } = data.cachedStats[`imageId:${metadata.referencedImageId}`];
|
||||||
|
|
||||||
const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId);
|
const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(
|
||||||
|
metadata.referencedImageId,
|
||||||
const displayText = [];
|
displaySetService,
|
||||||
|
annotation
|
||||||
|
);
|
||||||
|
|
||||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||||
let InstanceNumber;
|
let InstanceNumber;
|
||||||
@ -152,21 +160,15 @@ function getDisplayText(annotation, displaySet) {
|
|||||||
seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
|
seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const texts = [];
|
|
||||||
if (area) {
|
if (area) {
|
||||||
const roundedArea = utils.roundNumber(area || 0, 2);
|
const roundedArea = utils.roundNumber(area || 0, 2);
|
||||||
texts.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
|
displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seriesText) {
|
if (seriesText) {
|
||||||
texts.push(seriesText);
|
displayText.secondary.push(seriesText);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayText.push({
|
|
||||||
text: texts,
|
|
||||||
series: seriesText,
|
|
||||||
});
|
|
||||||
|
|
||||||
return displayText;
|
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 = ({
|
export const handleSegmentChange = ({
|
||||||
direction,
|
direction,
|
||||||
segDisplaySet,
|
segmentationId,
|
||||||
viewportId,
|
viewportId,
|
||||||
selectedSegmentObjectIndex,
|
selectedSegmentObjectIndex,
|
||||||
segmentationService,
|
segmentationService,
|
||||||
}: {
|
}: {
|
||||||
direction: number;
|
direction: number;
|
||||||
segDisplaySet: AppTypes.DisplaySet;
|
segmentationId: string;
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
selectedSegmentObjectIndex: number;
|
selectedSegmentObjectIndex: number;
|
||||||
segmentationService: SegmentationServiceType;
|
segmentationService: SegmentationServiceType;
|
||||||
}) => {
|
}) => {
|
||||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
const { segments } = segmentation;
|
const { segments } = segmentation;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
|
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
|
||||||
import { ProgressDropdown } from '@ohif/ui-next';
|
import { ProgressDropdown } from '@ohif/ui-next';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
const workflowStepsToDropdownOptions = (steps = []) =>
|
const workflowStepsToDropdownOptions = (steps = []) =>
|
||||||
steps.map(step => ({
|
steps.map(step => ({
|
||||||
@ -10,7 +11,8 @@ const workflowStepsToDropdownOptions = (steps = []) =>
|
|||||||
completed: false,
|
completed: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function ProgressDropdownWithService({ servicesManager }: withAppTypes): ReactElement {
|
export function ProgressDropdownWithService(): ReactElement {
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
const { workflowStepsService } = servicesManager.services;
|
const { workflowStepsService } = servicesManager.services;
|
||||||
const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id);
|
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 { useImageViewer } from '@ohif/ui-next';
|
||||||
import { useSystem, utils } from '@ohif/core';
|
import { useSystem, utils } from '@ohif/core';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -31,6 +31,7 @@ function PanelStudyBrowser({
|
|||||||
|
|
||||||
const internalImageViewer = useImageViewer();
|
const internalImageViewer = useImageViewer();
|
||||||
const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs;
|
const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs;
|
||||||
|
const fetchedStudiesRef = useRef(new Set());
|
||||||
|
|
||||||
const [{ activeViewportId, viewports, isHangingProtocolLayout }] = useViewportGrid();
|
const [{ activeViewportId, viewports, isHangingProtocolLayout }] = useViewportGrid();
|
||||||
const [activeTabName, setActiveTabName] = useState(studyMode);
|
const [activeTabName, setActiveTabName] = useState(studyMode);
|
||||||
@ -105,6 +106,13 @@ function PanelStudyBrowser({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch all studies for the patient in each primary study
|
// Fetch all studies for the patient in each primary study
|
||||||
async function fetchStudiesForPatient(StudyInstanceUID) {
|
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
|
// current study qido
|
||||||
const qidoForStudyUID = await dataSource.query.studies.search({
|
const qidoForStudyUID = await dataSource.query.studies.search({
|
||||||
studyInstanceUid: StudyInstanceUID,
|
studyInstanceUid: StudyInstanceUID,
|
||||||
|
|||||||
@ -8,12 +8,12 @@ import { useToolbar } from '@ohif/core/src/hooks/useToolbar';
|
|||||||
* @param props - Component props
|
* @param props - Component props
|
||||||
* @returns Component
|
* @returns Component
|
||||||
*/
|
*/
|
||||||
export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props }) {
|
export function ToolBoxButtonGroupWrapper({ buttonSection, id }) {
|
||||||
const { onInteraction, toolbarButtons } = useToolbar({
|
const { onInteraction, toolbarButtons } = useToolbar({
|
||||||
buttonSection,
|
buttonSection,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!groupId) {
|
if (!buttonSection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props })
|
|||||||
onInteraction={event => {
|
onInteraction={event => {
|
||||||
onInteraction?.({
|
onInteraction?.({
|
||||||
event,
|
event,
|
||||||
groupId,
|
id,
|
||||||
commands: item.commands,
|
commands: item.commands,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
item,
|
item,
|
||||||
|
|||||||
@ -10,13 +10,9 @@ import {
|
|||||||
import { useToolbar } from '@ohif/core/src';
|
import { useToolbar } from '@ohif/core/src';
|
||||||
|
|
||||||
interface ToolButtonListWrapperProps {
|
interface ToolButtonListWrapperProps {
|
||||||
groupId: string;
|
|
||||||
buttonSection: string;
|
buttonSection: string;
|
||||||
onInteraction?: (details: {
|
onInteraction?: (details: { itemId: string; commands?: Record<string, unknown> }) => void;
|
||||||
groupId: string;
|
id: string;
|
||||||
itemId: string;
|
|
||||||
commands?: Record<string, unknown>;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,10 +21,7 @@ interface ToolButtonListWrapperProps {
|
|||||||
* @returns Component
|
* @returns Component
|
||||||
* // test
|
* // test
|
||||||
*/
|
*/
|
||||||
export default function ToolButtonListWrapper({
|
export default function ToolButtonListWrapper({ buttonSection, id }: ToolButtonListWrapperProps) {
|
||||||
groupId,
|
|
||||||
buttonSection,
|
|
||||||
}: ToolButtonListWrapperProps) {
|
|
||||||
const { onInteraction, toolbarButtons } = useToolbar({
|
const { onInteraction, toolbarButtons } = useToolbar({
|
||||||
buttonSection,
|
buttonSection,
|
||||||
});
|
});
|
||||||
@ -47,21 +40,21 @@ export default function ToolButtonListWrapper({
|
|||||||
<ToolButtonList>
|
<ToolButtonList>
|
||||||
<ToolButtonListDefault>
|
<ToolButtonListDefault>
|
||||||
<div
|
<div
|
||||||
data-cy={`${groupId}-split-button-primary`}
|
data-cy={`${id}-split-button-primary`}
|
||||||
data-tool={primary.id}
|
data-tool={primary.id}
|
||||||
data-active={primary.isActive}
|
data-active={primary.isActive}
|
||||||
>
|
>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
{...primary}
|
{...primary}
|
||||||
onInteraction={({ itemId }) =>
|
onInteraction={({ itemId }) =>
|
||||||
onInteraction?.({ groupId, itemId, commands: primary.commands })
|
onInteraction?.({ id, itemId, commands: primary.commands })
|
||||||
}
|
}
|
||||||
className={primary.className}
|
className={primary.className}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ToolButtonListDefault>
|
</ToolButtonListDefault>
|
||||||
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
|
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
|
||||||
<div data-cy={`${groupId}-split-button-secondary`}>
|
<div data-cy={`${id}-split-button-secondary`}>
|
||||||
<ToolButtonListDropDown>
|
<ToolButtonListDropDown>
|
||||||
{items.map(item => {
|
{items.map(item => {
|
||||||
return (
|
return (
|
||||||
@ -71,9 +64,7 @@ export default function ToolButtonListWrapper({
|
|||||||
data-cy={item.id}
|
data-cy={item.id}
|
||||||
data-tool={item.id}
|
data-tool={item.id}
|
||||||
data-active={item.isActive}
|
data-active={item.isActive}
|
||||||
onSelect={() =>
|
onSelect={() => onInteraction?.({ id, itemId: item.id, commands: item.commands })}
|
||||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
|
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
|
||||||
</ToolButtonListItem>
|
</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 React from 'react';
|
||||||
import { useToolbar } from '@ohif/core';
|
import { useToolbar } from '@ohif/core';
|
||||||
|
|
||||||
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
|
interface ToolbarProps {
|
||||||
const { toolbarButtons, onInteraction } = useToolbar({
|
buttonSection?: string;
|
||||||
servicesManager,
|
viewportId?: string;
|
||||||
|
location?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toolbar({ buttonSection = 'primary', viewportId, location }: ToolbarProps) {
|
||||||
|
const {
|
||||||
|
toolbarButtons,
|
||||||
|
onInteraction,
|
||||||
|
isItemOpen,
|
||||||
|
isItemLocked,
|
||||||
|
openItem,
|
||||||
|
closeItem,
|
||||||
|
toggleLock,
|
||||||
|
} = useToolbar({
|
||||||
buttonSection,
|
buttonSection,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,13 +32,31 @@ export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id, Component, componentProps } = toolDef;
|
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 = (
|
const tool = (
|
||||||
<Component
|
<Component
|
||||||
key={id}
|
key={id}
|
||||||
id={id}
|
id={id}
|
||||||
onInteraction={onInteraction}
|
location={location}
|
||||||
servicesManager={servicesManager}
|
onInteraction={args => {
|
||||||
{...componentProps}
|
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