feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)
This commit is contained in:
parent
c1fced0573
commit
cd4e6408bc
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -109,6 +109,8 @@
|
||||
"wado",
|
||||
"wadors",
|
||||
"wadouri",
|
||||
"workerpool"
|
||||
"workerpool",
|
||||
"Colorbar",
|
||||
"Colorbars"
|
||||
]
|
||||
}
|
||||
|
||||
278
bun.lock
278
bun.lock
@ -40,7 +40,7 @@
|
||||
},
|
||||
"addOns/externals/devDependencies": {
|
||||
"name": "@externals/devDependencies",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
@ -127,22 +127,22 @@
|
||||
},
|
||||
"addOns/externals/dicom-microscopy-viewer": {
|
||||
"name": "@externals/dicom-microscopy-viewer",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"dicom-microscopy-viewer": "^0.46.1",
|
||||
},
|
||||
},
|
||||
"extensions/cornerstone": {
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/ai": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.9.5",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/ai": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.11.6",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
@ -159,9 +159,9 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -173,19 +173,19 @@
|
||||
},
|
||||
"extensions/cornerstone-dicom-pmap": {
|
||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -196,16 +196,16 @@
|
||||
},
|
||||
"extensions/cornerstone-dicom-rt": {
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"react-color": "^2.19.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -216,19 +216,19 @@
|
||||
},
|
||||
"extensions/cornerstone-dicom-seg": {
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -239,19 +239,19 @@
|
||||
},
|
||||
"extensions/cornerstone-dicom-sr": {
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"classnames": "^2.3.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -261,19 +261,19 @@
|
||||
},
|
||||
"extensions/cornerstone-dynamic-volume": {
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"classnames": "^2.3.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -283,7 +283,7 @@
|
||||
},
|
||||
"extensions/default": {
|
||||
"name": "@ohif/extension-default",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/calculate-suv": "^1.1.0",
|
||||
@ -291,8 +291,8 @@
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicomweb-client": "^0.10.4",
|
||||
"prop-types": "^15.6.2",
|
||||
@ -306,7 +306,7 @@
|
||||
},
|
||||
"extensions/dicom-microscopy": {
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||
@ -317,10 +317,10 @@
|
||||
"mathjs": "^12.4.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -331,14 +331,14 @@
|
||||
},
|
||||
"extensions/dicom-pdf": {
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -348,14 +348,14 @@
|
||||
},
|
||||
"extensions/dicom-video": {
|
||||
"name": "@ohif/extension-dicom-video",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -365,20 +365,20 @@
|
||||
},
|
||||
"extensions/measurement-tracking": {
|
||||
"name": "@ohif/extension-measurement-tracking",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"xstate": "^4.10.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"classnames": "^2.3.2",
|
||||
"dcmjs": "*",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@ -391,14 +391,14 @@
|
||||
},
|
||||
"extensions/test-extension": {
|
||||
"name": "@ohif/extension-test",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "0.38.0",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -408,7 +408,7 @@
|
||||
},
|
||||
"extensions/tmtv": {
|
||||
"name": "@ohif/extension-tmtv",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
@ -425,7 +425,7 @@
|
||||
},
|
||||
"modes/basic-dev-mode": {
|
||||
"name": "@ohif/mode-basic-dev-mode",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
@ -435,17 +435,17 @@
|
||||
"webpack-merge": "^5.7.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-video": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-video": "3.11.0-beta.23",
|
||||
},
|
||||
},
|
||||
"modes/basic-test-mode": {
|
||||
"name": "@ohif/mode-test",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
@ -455,19 +455,19 @@
|
||||
"webpack-merge": "^5.7.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-video": "3.10.0-beta.158",
|
||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.158",
|
||||
"@ohif/extension-test": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-video": "3.11.0-beta.23",
|
||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.23",
|
||||
"@ohif/extension-test": "3.11.0-beta.23",
|
||||
},
|
||||
},
|
||||
"modes/longitudinal": {
|
||||
"name": "@ohif/mode-longitudinal",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
@ -477,32 +477,32 @@
|
||||
"webpack-merge": "^5.7.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-video": "3.10.0-beta.158",
|
||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-video": "3.11.0-beta.23",
|
||||
"@ohif/extension-measurement-tracking": "3.11.0-beta.23",
|
||||
},
|
||||
},
|
||||
"modes/microscopy": {
|
||||
"name": "@ohif/mode-microscopy",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-microscopy": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-microscopy": "3.11.0-beta.23",
|
||||
},
|
||||
},
|
||||
"modes/preclinical-4d": {
|
||||
"name": "@ohif/mode-preclinical-4d",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
},
|
||||
@ -521,7 +521,7 @@
|
||||
},
|
||||
"modes/segmentation": {
|
||||
"name": "@ohif/mode-segmentation",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
@ -565,7 +565,7 @@
|
||||
},
|
||||
"modes/tmtv": {
|
||||
"name": "@ohif/mode-tmtv",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next": "^17.0.3",
|
||||
@ -586,32 +586,32 @@
|
||||
},
|
||||
"platform/app": {
|
||||
"name": "@ohif/app",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.158",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.158",
|
||||
"@ohif/extension-default": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-microscopy": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.158",
|
||||
"@ohif/extension-dicom-video": "3.10.0-beta.158",
|
||||
"@ohif/extension-test": "3.10.0-beta.158",
|
||||
"@ohif/i18n": "3.10.0-beta.158",
|
||||
"@ohif/mode-basic-dev-mode": "3.10.0-beta.158",
|
||||
"@ohif/mode-longitudinal": "3.10.0-beta.158",
|
||||
"@ohif/mode-microscopy": "3.10.0-beta.158",
|
||||
"@ohif/mode-test": "3.10.0-beta.158",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@ohif/ui-next": "3.10.0-beta.158",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-rt": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-seg": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-microscopy": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-pdf": "3.11.0-beta.23",
|
||||
"@ohif/extension-dicom-video": "3.11.0-beta.23",
|
||||
"@ohif/extension-test": "3.11.0-beta.23",
|
||||
"@ohif/i18n": "3.11.0-beta.23",
|
||||
"@ohif/mode-basic-dev-mode": "3.11.0-beta.23",
|
||||
"@ohif/mode-longitudinal": "3.11.0-beta.23",
|
||||
"@ohif/mode-microscopy": "3.11.0-beta.23",
|
||||
"@ohif/mode-test": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"@ohif/ui-next": "3.11.0-beta.23",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"classnames": "^2.3.2",
|
||||
@ -652,7 +652,7 @@
|
||||
},
|
||||
"platform/cli": {
|
||||
"name": "@ohif/cli",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"bin": {
|
||||
"ohif-cli": "src/index.js",
|
||||
},
|
||||
@ -676,7 +676,7 @@
|
||||
},
|
||||
"platform/core": {
|
||||
"name": "@ohif/core",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"dcmjs": "*",
|
||||
@ -701,16 +701,16 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@ohif/ui": "3.10.0-beta.158",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21",
|
||||
},
|
||||
},
|
||||
"platform/i18n": {
|
||||
"name": "@ohif/i18n",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"i18next-locize-backend": "^2.0.0",
|
||||
@ -735,7 +735,7 @@
|
||||
},
|
||||
"platform/ui": {
|
||||
"name": "@ohif/ui",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@testing-library/react": "^13.1.0",
|
||||
"browser-detect": "^0.2.28",
|
||||
@ -795,7 +795,7 @@
|
||||
},
|
||||
"platform/ui-next": {
|
||||
"name": "@ohif/ui-next",
|
||||
"version": "3.11.0-beta.5",
|
||||
"version": "3.11.0-beta.23",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
@ -1106,9 +1106,9 @@
|
||||
|
||||
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
|
||||
|
||||
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.9.5", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5" } }, "sha512-I8nPFmwNBqBwrH26w06gZfsJaHyZPN4nZC8GO7IerkVSkyYXgFEFC5iRdVuU+Kr94nWm8zPKyLqHx7hAeNudgw=="],
|
||||
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.11.6", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6" } }, "sha512-EYLkBXfXF6UcmcZPitIkDNe02HdKykiFDnG9CtXPJkTSqFfCZnkF4Mqe4WN/husc9wia6BUMlESQRBYEgBithA=="],
|
||||
|
||||
"@cornerstonejs/ai": ["@cornerstonejs/ai@3.9.5", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5" } }, "sha512-AgVPLVoYJeefPcp6tf5UNfS8qoEJc8VxrqZab6f+LybhSa2se52h+57rCB5Ihd7SziNvWyIdux7v1Iy3GiZm+g=="],
|
||||
"@cornerstonejs/ai": ["@cornerstonejs/ai@3.11.6", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6" } }, "sha512-F17IDzbn1PC2Zt1S2RS4OaLqO5ccC71Mmk2/9tU3wQS7g7qmyh8JzyB3B6t3iHkSlv20p3HNh8MqSug7zHOcrg=="],
|
||||
|
||||
"@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="],
|
||||
|
||||
@ -1120,15 +1120,15 @@
|
||||
|
||||
"@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="],
|
||||
|
||||
"@cornerstonejs/core": ["@cornerstonejs/core@3.9.5", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-EwXeTNSlL7UnXmj8o1DigNZOsVbhtNDKT2Bbi66jhnzZQm+rTPyYeTva+FnnqvpxBb/6QqwDEJ8gwlXwV0Rk5w=="],
|
||||
"@cornerstonejs/core": ["@cornerstonejs/core@3.11.6", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-8IlnDj7MZV/pfbtdF5gdpL53hLkriBgqcTY4hBIY4iW1A1plmwADsbVXzgyXl3Bi63aovmJhiQZ1VtQx0TzZGQ=="],
|
||||
|
||||
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.9.5", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5" } }, "sha512-g0GZNkZ6PBRCl/dvi6NoQlmpmZhuLLeI4cdiFwV0Ug81HPkHdn8FQaqgy1XPOMknLSL6sJGZsJN2l2nW5COQSA=="],
|
||||
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.11.6", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6" } }, "sha512-SJZ/LAw2gIyxzyQTEFWqBtyZELakqjcHOJnFxhkEmmpuJdpX4tgtV4k+6fnbKTg9OhybyP3prouMVPj5myjCZA=="],
|
||||
|
||||
"@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.9.5", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5", "@kitware/vtk.js": "32.12.1" } }, "sha512-U7XnaqrrEHGvH3kKdTwGiwLYaydxp5GVo0aqVvZaRDXMXfP3tbOpCfMQcqD+bv4Sh+Rr0719O5aDJ0+ENpA/MA=="],
|
||||
"@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.11.6", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6", "@kitware/vtk.js": "32.12.1" } }, "sha512-N55MxFxlRNXHVKpsJgeO8Z2aEURINxdmyPvGODGgYjZSOXZ+iR12Aly/iZHW9d/crq4GU440d5B6EmEZ6iuW8w=="],
|
||||
|
||||
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.9.5", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@cornerstonejs/tools": "^3.9.5", "@kitware/vtk.js": "32.12.1" } }, "sha512-6ucJkb0X5CBBx00g3CzJGuLd6x4T99bPyKXa+y9Bek3jC+RyKI4vySS5RRKtQ4N59yuealZkBvV7dT0FseE8og=="],
|
||||
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.11.6", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@cornerstonejs/tools": "^3.11.6", "@kitware/vtk.js": "32.12.1" } }, "sha512-dk+G12MqqmKSd2Rz8gHo1piEDAeijBcz1EAdF+50UQlUEds1fczHwFkMrh9mlHc9treyHi5+QEIr2MKRmwaJIw=="],
|
||||
|
||||
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.9.5", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.9.5", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-vdPsm4szolXmyBdDNTB1gyRCP5EEicsf+TOsTU5P6QYWR7cB+nyio7GeGK5zFQrUi0hb2a2qkVjjLQLQH9inAw=="],
|
||||
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.11.6", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.11.6", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-Sp1QkEaA952GgZikTNzyj125FzpYtVAYbeqCOn0E3WS7K1yw2XPuRbSCTRkRcV/J66ZoeKl4KgZVrhDlxJGAnw=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
|
||||
@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
||||
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomLoaderService } from '@ohif/extension-cornerstone';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
import { dicomLoaderService, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone';
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
@ -57,7 +56,8 @@ function _getDisplaySetsFromSeries(
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
supportsWindowLevel: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@ -227,7 +227,8 @@ async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
|
||||
return derivedVolume;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@ -27,7 +27,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import loadRTStruct from './loadRTStruct';
|
||||
@ -53,6 +54,7 @@ function _getDisplaySetsFromSeries(
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`,
|
||||
};
|
||||
|
||||
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@ -94,12 +96,19 @@ function _getDisplaySetsFromSeries(
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
}
|
||||
|
||||
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
||||
displaySet.load = ({ headers, createSegmentation = true }) =>
|
||||
_load(displaySet, servicesManager, extensionManager, headers, createSegmentation);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
function _load(
|
||||
rtDisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers,
|
||||
createSegmentation = true
|
||||
) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@ -121,16 +130,21 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
|
||||
rtDisplaySet.structureSet = structureSet;
|
||||
}
|
||||
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
rtDisplaySet.loading = false;
|
||||
reject(error);
|
||||
});
|
||||
if (createSegmentation) {
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
rtDisplaySet.loading = false;
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
return loadPromises[SOPInstanceUID];
|
||||
@ -171,20 +185,14 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(
|
||||
rtDisplaySet,
|
||||
segmentationService: AppTypes.SegmentationService
|
||||
) {
|
||||
function _segmentationExistsInCache() {
|
||||
// Todo: fix this
|
||||
return false;
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
const rtContourId = rtDisplaySet.displaySetInstanceUID;
|
||||
const contour = segmentationService.getContour(rtContourId);
|
||||
|
||||
return contour !== undefined;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dicom-rt',
|
||||
|
||||
@ -323,7 +323,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
|
||||
@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { metaData, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
@ -57,6 +58,7 @@ function _getDisplaySetsFromSeries(
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SEG')}`,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@ -232,7 +234,8 @@ function _segmentationExists(segDisplaySet) {
|
||||
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@ -157,7 +157,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
@ -304,7 +304,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +273,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
servicesManager,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
@ -122,6 +123,7 @@ function _getDisplaySetsFromSeries(
|
||||
sopClassUids,
|
||||
instance,
|
||||
addInstances,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
|
||||
};
|
||||
|
||||
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
||||
@ -354,7 +356,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
||||
* @param {Object} options.extensionManager - The extension manager.
|
||||
* @returns {Array} An array containing the SOP class handler module.
|
||||
*/
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"dcmjs": "*",
|
||||
@ -51,12 +51,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.9.5",
|
||||
"@cornerstonejs/ai": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.9.5",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/adapters": "^3.11.6",
|
||||
"@cornerstonejs/ai": "^3.11.6",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.11.6",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { useViewportDialog } from '@ohif/ui-next';
|
||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
@ -14,12 +13,11 @@ import CinePlayer from '../components/CinePlayer';
|
||||
import type { Types } from '@ohif/core';
|
||||
|
||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
||||
import { useViewportActionCorners } from '../hooks';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@ -321,49 +319,15 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
};
|
||||
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
|
||||
|
||||
// Set up the window level action menu in the viewport action corners.
|
||||
useEffect(() => {
|
||||
const windowLevelActionMenu = customizationService.getCustomization(
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
const segmentationOverlay = customizationService.getCustomization(
|
||||
'viewportActionMenu.segmentationOverlay'
|
||||
);
|
||||
|
||||
if (windowLevelActionMenu?.enabled) {
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: 'windowLevelActionMenu',
|
||||
component: getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
location: windowLevelActionMenu.location,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
||||
}),
|
||||
location: windowLevelActionMenu.location,
|
||||
});
|
||||
}
|
||||
|
||||
if (segmentationOverlay?.enabled) {
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: 'segmentation',
|
||||
component: getViewportDataOverlaySettingsMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
location: segmentationOverlay.location,
|
||||
}),
|
||||
location: segmentationOverlay.location,
|
||||
});
|
||||
}
|
||||
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
|
||||
// Set up the window level action menu in the viewport action corners using the custom hook
|
||||
useViewportActionCorners({
|
||||
viewportId,
|
||||
elementRef,
|
||||
displaySets,
|
||||
viewportActionCornersService,
|
||||
customizationService,
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
const Notification = customizationService.getCustomization('ui.notificationComponent');
|
||||
|
||||
@ -524,22 +488,18 @@ OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||
|
||||
function areEqual(prevProps, nextProps) {
|
||||
if (nextProps.needsRerendering) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -547,12 +507,10 @@ function areEqual(prevProps, nextProps) {
|
||||
nextProps.viewportOptions.viewportType &&
|
||||
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
|
||||
) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextProps.viewportOptions.needsRerendering) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -560,7 +518,6 @@ function areEqual(prevProps, nextProps) {
|
||||
const nextDisplaySets = nextProps.displaySets;
|
||||
|
||||
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -573,13 +530,11 @@ function areEqual(prevProps, nextProps) {
|
||||
);
|
||||
|
||||
if (!foundDisplaySet) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// check they contain the same image
|
||||
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -587,7 +542,6 @@ function areEqual(prevProps, nextProps) {
|
||||
if (foundDisplaySet.images?.length) {
|
||||
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
||||
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ function CornerstoneImageScrollbar({
|
||||
}
|
||||
const { viewportType } = viewportData;
|
||||
const eventId =
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
|
||||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
|
||||
Enums.Events.IMAGE_RENDERED;
|
||||
|
||||
@ -72,11 +72,11 @@ function CornerstoneImageScrollbar({
|
||||
if (!viewport || viewport instanceof VolumeViewport3D) {
|
||||
return;
|
||||
}
|
||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
imageIndex: newImageIdIndex ?? imageIdIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
StackViewport,
|
||||
VolumeViewport,
|
||||
utilities as csUtils,
|
||||
Enums as CoreEnums,
|
||||
Types as CoreTypes,
|
||||
BaseVolumeViewport,
|
||||
} from '@cornerstonejs/core';
|
||||
@ -248,7 +249,7 @@ function commandsModule({
|
||||
},
|
||||
updateStoredPositionPresentation: ({
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
displaySetInstanceUIDs,
|
||||
referencedImageId,
|
||||
options,
|
||||
}) => {
|
||||
@ -256,11 +257,24 @@ function commandsModule({
|
||||
const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
|
||||
usePositionPresentationStore.getState();
|
||||
|
||||
// Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
|
||||
// Look inside positionPresentationStore and find the key that includes ALL the displaySetInstanceUIDs
|
||||
// and the value has viewportId as activeViewportId.
|
||||
const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
|
||||
([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
|
||||
)?.[0];
|
||||
let previousReferencedDisplaySetStoreKey;
|
||||
|
||||
if (
|
||||
displaySetInstanceUIDs &&
|
||||
Array.isArray(displaySetInstanceUIDs) &&
|
||||
displaySetInstanceUIDs.length > 0
|
||||
) {
|
||||
previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
|
||||
([key, value]) => {
|
||||
return (
|
||||
displaySetInstanceUIDs.every(uid => key.includes(uid)) &&
|
||||
value.viewportId === viewportId
|
||||
);
|
||||
}
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
// Create presentation data with referencedImageId and options if provided
|
||||
const presentationData = referencedImageId
|
||||
@ -282,7 +296,7 @@ function commandsModule({
|
||||
// so we need to grab the positionPresentationId directly from the store,
|
||||
// Todo: this is really hacky, we should have a better way for this
|
||||
const positionPresentationId = getPositionPresentationId({
|
||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||
displaySetInstanceUIDs,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
@ -719,7 +733,7 @@ function commandsModule({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
if (!toolGroup?.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1019,7 +1033,7 @@ function commandsModule({
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
if (!toolGroup?.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1189,6 +1203,9 @@ function commandsModule({
|
||||
},
|
||||
/**
|
||||
* Creates a labelmap for the active viewport
|
||||
*
|
||||
* The created labelmap will be registered as a display set and also added
|
||||
* as a segmentation representation to the viewport.
|
||||
*/
|
||||
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
|
||||
const { viewportGridService, displaySetService, segmentationService } =
|
||||
@ -1209,6 +1226,7 @@ function commandsModule({
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// This will create the segmentation and register it as a display set
|
||||
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
@ -1225,6 +1243,7 @@ function commandsModule({
|
||||
}
|
||||
);
|
||||
|
||||
// Also add the segmentation representation to the viewport
|
||||
await segmentationService.addSegmentationRepresentation(viewportId, {
|
||||
segmentationId,
|
||||
type: Enums.SegmentationRepresentations.Labelmap,
|
||||
@ -1741,6 +1760,30 @@ function commandsModule({
|
||||
});
|
||||
});
|
||||
},
|
||||
setViewportOrientation: ({ viewportId, orientation }) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) {
|
||||
console.warn('Orientation can only be set on volume viewports');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get display sets for this viewport to verify at least one is reconstructable
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const displaySets = displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid));
|
||||
|
||||
if (!displaySets.some(ds => ds.isReconstructable)) {
|
||||
console.warn('Cannot change orientation: No reconstructable display sets in viewport');
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.setOrientation(orientation);
|
||||
viewport.render();
|
||||
|
||||
// update the orientation in the viewport info
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
viewportInfo.setOrientation(orientation);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -2021,6 +2064,7 @@ function commandsModule({
|
||||
decreaseBrushSize: actions.decreaseBrushSize,
|
||||
addNewSegment: actions.addNewSegment,
|
||||
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
|
||||
setViewportOrientation: actions.setViewportOrientation,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
|
||||
export type OHIFViewportActionCornersProps = {
|
||||
viewportId: string;
|
||||
@ -9,8 +10,13 @@ export type OHIFViewportActionCornersProps = {
|
||||
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const [viewportActionCornersState] = useViewportActionCornersContext();
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const isActiveViewport = viewportGrid.activeViewportId === viewportId;
|
||||
|
||||
const ViewportActionCorners =
|
||||
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
|
||||
|
||||
if (!viewportActionCornersState[viewportId]) {
|
||||
return null;
|
||||
}
|
||||
@ -18,7 +24,8 @@ function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProp
|
||||
return (
|
||||
<ViewportActionCorners
|
||||
cornerComponents={viewportActionCornersState[viewportId]}
|
||||
></ViewportActionCorners>
|
||||
isActiveViewport={isActiveViewport}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,478 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Icons,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
} from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
|
||||
|
||||
function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: string }>) {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const [pendingForegrounds, setPendingForegrounds] = useState<string[]>([]);
|
||||
const [pendingSegmentations, setPendingSegmentations] = useState<string[]>([]);
|
||||
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
|
||||
|
||||
const { hangingProtocolService } = servicesManager.services;
|
||||
|
||||
const {
|
||||
backgroundDisplaySet,
|
||||
potentialOverlayDisplaySets,
|
||||
potentialForegroundDisplaySets,
|
||||
potentialBackgroundDisplaySets,
|
||||
overlayDisplaySets,
|
||||
foregroundDisplaySets,
|
||||
} = useViewportDisplaySets(viewportId);
|
||||
|
||||
/**
|
||||
* Change the background display set
|
||||
*/
|
||||
const handleBackgroundSelection = (newBackgroundDisplaySet: AppTypes.DisplaySet) => {
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
newBackgroundDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: updatedViewports,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle threshold and opacity toggle
|
||||
*/
|
||||
const handleThresholdOpacityToggle = (checked: boolean) => {
|
||||
setThresholdOpacityEnabled(checked);
|
||||
|
||||
if (foregroundDisplaySets.length > 0) {
|
||||
// Example implementation of threshold/opacity adjustment
|
||||
commandsManager.runCommand('setForegroundThresholdOpacity', {
|
||||
viewportId,
|
||||
enabled: checked,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace a display set layer with a new one
|
||||
*/
|
||||
const handleReplaceDisplaySetLayer = (
|
||||
currentDisplaySetInstanceUID: string,
|
||||
newDisplaySetInstanceUID: string
|
||||
) => {
|
||||
// Remove current display set
|
||||
commandsManager.runCommand('removeDisplaySetLayer', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: currentDisplaySetInstanceUID,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
commandsManager.runCommand('addDisplaySetAsLayer', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: newDisplaySetInstanceUID,
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a display set layer
|
||||
*/
|
||||
const handleRemoveDisplaySetLayer = (displaySetInstanceUID: string) => {
|
||||
commandsManager.runCommand('removeDisplaySetLayer', {
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a display set as a layer
|
||||
*/
|
||||
const handleAddDisplaySetAsLayer = (displaySetInstanceUID: string) => {
|
||||
commandsManager.runCommand('addDisplaySetAsLayer', {
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle overlay display set selection change
|
||||
*/
|
||||
const handleOverlaySelectionChange = (
|
||||
currentDisplaySet: AppTypes.DisplaySet,
|
||||
newDisplaySetInstanceUID: string
|
||||
) => {
|
||||
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected display set
|
||||
const selectedDisplaySet = potentialOverlayDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (selectedDisplaySet) {
|
||||
handleReplaceDisplaySetLayer(
|
||||
currentDisplaySet.displaySetInstanceUID,
|
||||
selectedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle foreground display set selection change
|
||||
*/
|
||||
const handleForegroundSelectionChange = (
|
||||
currentDisplaySet: AppTypes.DisplaySet,
|
||||
newDisplaySetInstanceUID: string
|
||||
) => {
|
||||
if (newDisplaySetInstanceUID === currentDisplaySet.displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected display set
|
||||
const selectedDisplaySet = potentialForegroundDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === newDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (selectedDisplaySet) {
|
||||
handleReplaceDisplaySetLayer(
|
||||
currentDisplaySet.displaySetInstanceUID,
|
||||
selectedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle pending segmentation selection
|
||||
*/
|
||||
const handlePendingSegmentationSelection = (pendingId: string, displaySetInstanceUID: string) => {
|
||||
const selectedDisplaySet = potentialOverlayDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (selectedDisplaySet) {
|
||||
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
|
||||
// Remove this pending segmentation from the list
|
||||
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle pending foreground selection
|
||||
*/
|
||||
const handlePendingForegroundSelection = (pendingId: string, displaySetInstanceUID: string) => {
|
||||
const selectedDisplaySet = potentialForegroundDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (selectedDisplaySet) {
|
||||
handleAddDisplaySetAsLayer(selectedDisplaySet.displaySetInstanceUID);
|
||||
// Remove this pending foreground from the list
|
||||
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-popover flex h-full w-[275px] flex-col rounded rounded-md p-1.5">
|
||||
{/* Top buttons row */}
|
||||
<div className={`flex`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-primary flex items-center p-1"
|
||||
onClick={() => {
|
||||
// Add a new pending foreground slot with a unique ID
|
||||
setPendingForegrounds([...pendingForegrounds, `pending-${Date.now()}`]);
|
||||
}}
|
||||
disabled={potentialForegroundDisplaySets.length === 0}
|
||||
>
|
||||
<Icons.Plus className="h-4 w-4" />
|
||||
Foreground
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-primary ml-2 flex items-center"
|
||||
disabled={potentialOverlayDisplaySets.length === 0}
|
||||
onClick={() => {
|
||||
setPendingSegmentations([...pendingSegmentations, `seg-${Date.now()}`]);
|
||||
}}
|
||||
>
|
||||
<Icons.Plus className="h-4 w-4" />
|
||||
Segmentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{/* Overlays Segmentation section */}
|
||||
<div className="my-2 ml-1">
|
||||
{overlayDisplaySets.map((displaySet, index) => (
|
||||
<div
|
||||
key={displaySet.displaySetInstanceUID}
|
||||
className="mb-1 flex items-center"
|
||||
>
|
||||
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||
<Select
|
||||
value={displaySet.displaySetInstanceUID}
|
||||
onValueChange={value => handleOverlaySelectionChange(displaySet, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* Include both potential overlays and the current overlay */}
|
||||
<SelectItem
|
||||
key={displaySet.displaySetInstanceUID}
|
||||
value={displaySet.displaySetInstanceUID}
|
||||
>
|
||||
{displaySet.label}
|
||||
</SelectItem>
|
||||
{potentialOverlayDisplaySets.map(item => (
|
||||
<SelectItem
|
||||
key={item.displaySetInstanceUID}
|
||||
value={item.displaySetInstanceUID}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
<Icons.More className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
|
||||
>
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pendingSegmentations.map(pendingId => (
|
||||
<div
|
||||
key={pendingId}
|
||||
className="mb-2 flex items-center"
|
||||
>
|
||||
<Icons.LayerSegmentation className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={value => handlePendingSegmentationSelection(pendingId, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="SELECT A SEGMENTATION" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{potentialOverlayDisplaySets.map(item => (
|
||||
<SelectItem
|
||||
key={item.displaySetInstanceUID}
|
||||
value={item.displaySetInstanceUID}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
<Icons.More className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// Remove this pending segmentation
|
||||
setPendingSegmentations(pendingSegmentations.filter(id => id !== pendingId));
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Foregrounds section */}
|
||||
<div className="my-2 px-1">
|
||||
{foregroundDisplaySets.map((displaySet, index) => (
|
||||
<div
|
||||
key={displaySet.displaySetInstanceUID}
|
||||
className="mb-1 flex items-center"
|
||||
>
|
||||
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||
<Select
|
||||
value={displaySet.displaySetInstanceUID}
|
||||
onValueChange={value => handleForegroundSelectionChange(displaySet, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue>{displaySet.label?.toUpperCase()}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* Include both potential foregrounds and the current foreground */}
|
||||
<SelectItem
|
||||
key={displaySet.displaySetInstanceUID}
|
||||
value={displaySet.displaySetInstanceUID}
|
||||
>
|
||||
{displaySet.label}
|
||||
</SelectItem>
|
||||
{potentialForegroundDisplaySets.map(item => (
|
||||
<SelectItem
|
||||
key={item.displaySetInstanceUID}
|
||||
value={item.displaySetInstanceUID}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
<Icons.More className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleRemoveDisplaySetLayer(displaySet.displaySetInstanceUID)}
|
||||
>
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pendingForegrounds.map(pendingId => (
|
||||
<div
|
||||
key={pendingId}
|
||||
className="mb-2 flex items-center"
|
||||
>
|
||||
<Icons.LayerForeground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={value => handlePendingForegroundSelection(pendingId, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="SELECT A FOREGROUND" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{potentialForegroundDisplaySets.map(item => (
|
||||
<SelectItem
|
||||
key={item.displaySetInstanceUID}
|
||||
value={item.displaySetInstanceUID}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
<Icons.More className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// Remove this pending foreground
|
||||
setPendingForegrounds(pendingForegrounds.filter(id => id !== pendingId));
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Background section */}
|
||||
<div className="mt-1 mb-1 flex items-center px-1">
|
||||
<Icons.LayerBackground className="text-muted-foreground mr-1 h-6 w-6 flex-shrink-0" />
|
||||
<Select
|
||||
value={backgroundDisplaySet.displaySetInstanceUID}
|
||||
onValueChange={value => {
|
||||
const selectedDisplaySet = potentialBackgroundDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === value
|
||||
);
|
||||
if (selectedDisplaySet) {
|
||||
handleBackgroundSelection(selectedDisplaySet);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue>
|
||||
{(
|
||||
backgroundDisplaySet.SeriesDescription ||
|
||||
backgroundDisplaySet.label ||
|
||||
'background'
|
||||
).toUpperCase()}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{potentialBackgroundDisplaySets.map(displaySet => (
|
||||
<SelectItem
|
||||
key={displaySet.displaySetInstanceUID}
|
||||
value={displaySet.displaySetInstanceUID}
|
||||
>
|
||||
{displaySet.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* {foregroundDisplaySets.length > 0 && (
|
||||
<div className="mt-1 ml-7">
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
id="threshold-opacity-switch"
|
||||
className="mr-2"
|
||||
checked={thresholdOpacityEnabled}
|
||||
onCheckedChange={handleThresholdOpacityToggle}
|
||||
/>
|
||||
<label
|
||||
htmlFor="threshold-opacity-switch"
|
||||
className="text-muted-foreground cursor-pointer text-sm"
|
||||
onClick={() => setThresholdOpacityEnabled(!thresholdOpacityEnabled)}
|
||||
>
|
||||
Control threshold & opacity
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportDataOverlayMenu;
|
||||
@ -0,0 +1,62 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Icons,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
useViewportGrid,
|
||||
} from '@ohif/ui-next';
|
||||
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
|
||||
import classNames from 'classnames';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function ViewportDataOverlayMenuWrapper({
|
||||
viewportId,
|
||||
displaySets,
|
||||
location,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
}>): ReactNode {
|
||||
const { servicesManager } = useSystem();
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
|
||||
const { align, side } = viewportActionCornersService.getAlignAndSide(location);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
viewportGrid.activeViewportId === viewportId
|
||||
? 'visible'
|
||||
: 'invisible group-hover/pane:visible'
|
||||
)}
|
||||
>
|
||||
<Icons.ViewportViews className={classNames('text-highlight')} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="border-none bg-transparent p-0 shadow-none"
|
||||
side={side}
|
||||
align={align}
|
||||
alignOffset={0}
|
||||
sideOffset={5}
|
||||
>
|
||||
<ViewportDataOverlayMenu
|
||||
className="w-full"
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Icons, Separator } from '@ohif/ui-next';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
function ViewportSegmentationMenu({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}: withAppTypes<{ viewportId: string }>) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const [activeSegmentations, setActiveSegmentations] = useState([]);
|
||||
const [availableSegmentations, setAvailableSegmentations] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateSegmentations = () => {
|
||||
const active = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
setActiveSegmentations(active);
|
||||
|
||||
const all = segmentationService.getSegmentations();
|
||||
const available = all.filter(
|
||||
seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
|
||||
);
|
||||
setAvailableSegmentations(available);
|
||||
};
|
||||
|
||||
updateSegmentations();
|
||||
|
||||
const subscriptions = [
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
].map(event => segmentationService.subscribe(event, updateSegmentations));
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
};
|
||||
}, [segmentationService, viewportId]);
|
||||
|
||||
const toggleSegmentationRepresentationVisibility = (
|
||||
segmentationId,
|
||||
type = SegmentationRepresentations.Labelmap
|
||||
) => {
|
||||
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
|
||||
segmentationId,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
const addSegmentationToViewport = segmentationId => {
|
||||
segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
|
||||
};
|
||||
|
||||
const removeSegmentationFromViewport = segmentationId => {
|
||||
segmentationService.removeSegmentationRepresentations(viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
|
||||
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
|
||||
<ul className="space-y-1">
|
||||
{activeSegmentations.map(segmentation => (
|
||||
<li
|
||||
key={segmentation.id}
|
||||
className="flex items-center text-sm"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground mr-2"
|
||||
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
|
||||
>
|
||||
<Icons.Minus className="h-6 w-6" />
|
||||
</Button>
|
||||
<span className="text-foreground flex-grow">{segmentation.label}</span>
|
||||
{segmentation.visible ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
onClick={() =>
|
||||
toggleSegmentationRepresentationVisibility(
|
||||
segmentation.segmentationId,
|
||||
segmentation.type
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icons.Hide className="h-6 w-6" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
onClick={() =>
|
||||
toggleSegmentationRepresentationVisibility(
|
||||
segmentation.segmentationId,
|
||||
segmentation.type
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icons.Show className="h-6 w-6" />
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{availableSegmentations.length > 0 && (
|
||||
<>
|
||||
<Separator className="bg-input mb-3" />
|
||||
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
|
||||
<ul className="space-y-1">
|
||||
{availableSegmentations.map(({ segmentationId, label }) => (
|
||||
<li
|
||||
key={segmentationId}
|
||||
className="flex items-center text-sm"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground mr-2"
|
||||
onClick={() => addSegmentationToViewport(segmentationId)}
|
||||
>
|
||||
<Icons.Plus className="h-6 w-6" />
|
||||
</Button>
|
||||
<span className="text-foreground/60">{label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportSegmentationMenu;
|
||||
@ -1,83 +0,0 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
|
||||
import ViewportSegmentationMenu from './ViewportSegmentationMenu';
|
||||
import classNames from 'classnames';
|
||||
import { useSegmentations } from '../../hooks/useSegmentations';
|
||||
|
||||
export function ViewportSegmentationMenuWrapper({
|
||||
viewportId,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
location,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
}>): ReactNode {
|
||||
const { viewportActionCornersService, viewportGridService } = servicesManager.services;
|
||||
|
||||
const segmentations = useSegmentations({ servicesManager });
|
||||
|
||||
const activeViewportId = viewportGridService.getActiveViewportId();
|
||||
const isActiveViewport = viewportId === activeViewportId;
|
||||
|
||||
const { align, side } = getAlignAndSide(viewportActionCornersService, location);
|
||||
|
||||
if (!segmentations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.ViewportViews
|
||||
className={classNames(
|
||||
'text-highlight',
|
||||
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="border-none bg-transparent p-0 shadow-none"
|
||||
side={side}
|
||||
align={align}
|
||||
alignOffset={-15}
|
||||
sideOffset={5}
|
||||
>
|
||||
<ViewportSegmentationMenu
|
||||
className="w-full"
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets}
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const getAlignAndSide = (viewportActionCornersService, location) => {
|
||||
const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
|
||||
|
||||
switch (location) {
|
||||
case ViewportActionCornersLocations.topLeft:
|
||||
return { align: 'start', side: 'bottom' };
|
||||
case ViewportActionCornersLocations.topRight:
|
||||
return { align: 'end', side: 'bottom' };
|
||||
case ViewportActionCornersLocations.bottomLeft:
|
||||
return { align: 'start', side: 'top' };
|
||||
case ViewportActionCornersLocations.bottomRight:
|
||||
return { align: 'end', side: 'top' };
|
||||
default:
|
||||
console.debug('Unknown location, defaulting to bottom-start');
|
||||
return { align: 'start', side: 'bottom' };
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
|
||||
import { ViewportDataOverlayMenuWrapper } from './ViewportDataOverlayMenuWrapper';
|
||||
|
||||
export function getViewportDataOverlaySettingsMenu(
|
||||
props: withAppTypes<{
|
||||
@ -7,5 +7,5 @@ export function getViewportDataOverlaySettingsMenu(
|
||||
element: HTMLElement;
|
||||
}>
|
||||
): ReactNode {
|
||||
return <ViewportSegmentationMenuWrapper {...props} />;
|
||||
return <ViewportDataOverlayMenuWrapper {...props} />;
|
||||
}
|
||||
|
||||
@ -0,0 +1,156 @@
|
||||
import { utilities as csUtils } from '@cornerstonejs/core';
|
||||
|
||||
export const DEFAULT_COLORMAP = 'hsv';
|
||||
export const DEFAULT_OPACITY = 0.5;
|
||||
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
|
||||
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
|
||||
|
||||
/**
|
||||
* Get modality-specific color and opacity settings from the customization service
|
||||
*/
|
||||
export function getModalityOverlayColormap(customizationService, modality) {
|
||||
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
|
||||
'cornerstone.modalityOverlayDefaultColorMaps'
|
||||
) || { defaultSettings: {} };
|
||||
|
||||
return (
|
||||
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
|
||||
colormap: DEFAULT_COLORMAP,
|
||||
opacity: DEFAULT_OPACITY,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies display sets that can be used as overlays for a specific viewport.
|
||||
*
|
||||
* "Enhanced" display sets are those that:
|
||||
* 1. Are not already in the viewport
|
||||
* 2. Are evaluated for their ability to be overlaid onto the background display set
|
||||
* 3. Have an "isOverlayable" flag indicating if they're compatible with the viewport
|
||||
*
|
||||
* A display set is considered overlayable when:
|
||||
* - The background display set is reconstructable
|
||||
* - The display set is not unsupported
|
||||
* - The Frame of Reference matches the background display set
|
||||
* - For non-derived modalities: background can be a volume and display set is either multiframe or valid volume
|
||||
*
|
||||
* @returns {Object} Object containing:
|
||||
* - viewportDisplaySets: Display sets already in the viewport
|
||||
* - enhancedDisplaySets: Other display sets with overlayability assessment
|
||||
*/
|
||||
export function getEnhancedDisplaySets({ viewportId, services }) {
|
||||
const { displaySetService, viewportGridService } = services;
|
||||
const displaySetsUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const allDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
displaySet => !displaySetsUIDs.includes(displaySet.displaySetInstanceUID)
|
||||
);
|
||||
|
||||
const viewportDisplaySets = displaySetsUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const backgroundCanBeVolume = csUtils.isValidVolume(viewportDisplaySets[0].imageIds);
|
||||
const backgroundDisplaySet = viewportDisplaySets[0];
|
||||
|
||||
const enhancedDisplaySets = otherDisplaySets.map(displaySet => {
|
||||
if (!backgroundDisplaySet.isReconstructable) {
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (displaySet.unsupported) {
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if Frame of Reference matches
|
||||
if (
|
||||
displaySet.FrameOfReferenceUID &&
|
||||
displaySet.FrameOfReferenceUID !== backgroundDisplaySet.FrameOfReferenceUID
|
||||
) {
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Special handling for derived modalities
|
||||
if (!DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality)) {
|
||||
if (!backgroundCanBeVolume) {
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: false,
|
||||
};
|
||||
}
|
||||
|
||||
const imageIds = displaySet.imageIds || displaySet.images?.map(image => image.imageId);
|
||||
const isMultiframe = displaySet.isMultiFrame;
|
||||
|
||||
if (!isMultiframe && imageIds?.length > 0 && !csUtils.isValidVolume(imageIds)) {
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...displaySet,
|
||||
isOverlayable: true,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
viewportDisplaySets,
|
||||
enhancedDisplaySets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort function: puts disabled items (isOverlayable: false) at the end
|
||||
*/
|
||||
export const sortByOverlayable = (a, b) => {
|
||||
if (a.isOverlayable === b.isOverlayable) {
|
||||
return 0;
|
||||
}
|
||||
return a.isOverlayable ? -1 : 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create display set options based on modality and opacity settings
|
||||
*/
|
||||
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
|
||||
return {
|
||||
colormap: {
|
||||
name: modalitySettings.colormap || DEFAULT_COLORMAP,
|
||||
opacity: opacity / 100, // Convert from percentage to 0-1 range
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get segmentations that can be added as overlays to the viewport
|
||||
*
|
||||
* Note: This function is deprecated as we now use display sets for segmentations
|
||||
*/
|
||||
export function getAvailableSegmentations(segmentationService) {
|
||||
const segmentations = segmentationService.getSegmentations() || [];
|
||||
return segmentations.map(segmentation => ({
|
||||
segmentationId: segmentation.segmentationId,
|
||||
label: segmentation.label || 'Segmentation',
|
||||
segments: segmentation.segments,
|
||||
frameOfReferenceUID: segmentation.frameOfReferenceUID,
|
||||
}));
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { Icons, useViewportGrid } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||
|
||||
const viewportId = viewportGridState.activeViewportId;
|
||||
|
||||
const handleOrientationChange = (orientation: string) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const currentViewportType = viewportInfo?.getViewportType();
|
||||
|
||||
// Get the displaySets in this viewport
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const displaySets = displaySetUIDs
|
||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if at least one displaySet is reconstructable
|
||||
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
|
||||
|
||||
if (!hasReconstructableDisplaySet) {
|
||||
console.warn('Cannot change orientation: No reconstructable display sets in viewport');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the orientation enum based on the selected orientation
|
||||
let orientationEnum;
|
||||
switch (orientation.toLowerCase()) {
|
||||
case 'axial':
|
||||
orientationEnum = Enums.OrientationAxis.AXIAL;
|
||||
break;
|
||||
case 'sagittal':
|
||||
orientationEnum = Enums.OrientationAxis.SAGITTAL;
|
||||
break;
|
||||
case 'coronal':
|
||||
orientationEnum = Enums.OrientationAxis.CORONAL;
|
||||
break;
|
||||
default:
|
||||
orientationEnum = Enums.OrientationAxis.ACQUISITION;
|
||||
}
|
||||
|
||||
// If viewport is not already a volume type, we need to convert it
|
||||
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
// Configure the viewport to be a volume viewport with current display sets
|
||||
const updatedViewport = {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: displaySetUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
orientation: orientationEnum,
|
||||
},
|
||||
displaySetOptions: displaySetUIDs.map(() => ({})),
|
||||
};
|
||||
|
||||
// Update the viewport to be a volume viewport
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: [updatedViewport],
|
||||
});
|
||||
} else {
|
||||
// Set the viewport orientation
|
||||
commandsManager.runCommand('setViewportOrientation', {
|
||||
viewportId,
|
||||
orientation: orientationEnum,
|
||||
});
|
||||
}
|
||||
};
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
const displaySets = displaySetUIDs
|
||||
.map(uid => displaySetService.getDisplaySetByUID(uid))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!displaySets.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasReconstructableDisplaySet = displaySets.some(ds => ds.isReconstructable);
|
||||
|
||||
// Get proper alignment and side based on the location
|
||||
let align = 'center';
|
||||
let side = 'bottom';
|
||||
|
||||
if (location !== undefined) {
|
||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
||||
align = positioning.align;
|
||||
side = positioning.side;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-highlight"
|
||||
disabled={!hasReconstructableDisplaySet}
|
||||
>
|
||||
<Icons.OrientationSwitch />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[160px]"
|
||||
align={align}
|
||||
side={side}
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenuLabel className="-ml-1">Orientation</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('axial')}>Axial</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('sagittal')}>
|
||||
Sagittal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('coronal')}>
|
||||
Coronal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleOrientationChange('acquisition')}>
|
||||
Acquisition
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportOrientationMenu;
|
||||
@ -0,0 +1,27 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { cn } from '@ohif/ui-next';
|
||||
import ViewportOrientationMenu from './ViewportOrientationMenu';
|
||||
|
||||
export function ViewportOrientationMenuWrapper({
|
||||
viewportId,
|
||||
location,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>): ReactNode {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-end',
|
||||
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
)}
|
||||
>
|
||||
<ViewportOrientationMenu location={location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ViewportOrientationMenuWrapper } from './ViewportOrientationMenuWrapper';
|
||||
|
||||
export function getViewportOrientationMenu(
|
||||
props: withAppTypes<{
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
location: string;
|
||||
}>
|
||||
): ReactNode {
|
||||
return <ViewportOrientationMenuWrapper {...props} />;
|
||||
}
|
||||
|
||||
export * from './ViewportOrientationMenu';
|
||||
@ -1,18 +1,18 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
|
||||
import { StackViewport } from '@cornerstonejs/core';
|
||||
import { ColorbarProps } from '../../types/Colorbar';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function setViewportColorbar(
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
colorbarOptions
|
||||
) {
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||
@ -32,36 +32,18 @@ export function setViewportColorbar(
|
||||
};
|
||||
}
|
||||
|
||||
const displaySetInstanceUIDs = [];
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
displaySetInstanceUIDs.push(viewportId);
|
||||
}
|
||||
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
displaySets.forEach(ds => {
|
||||
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
|
||||
});
|
||||
}
|
||||
|
||||
commandsManager.run({
|
||||
commandName: 'toggleViewportColorbar',
|
||||
commandOptions: {
|
||||
viewportId,
|
||||
options: colorbarOptions,
|
||||
displaySetInstanceUIDs,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
commandsManager.run('toggleViewportColorbar', {
|
||||
viewportId,
|
||||
options: colorbarOptions,
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
|
||||
export function Colorbar({
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
}: withAppTypes<ColorbarProps>): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { colorbarService } = servicesManager.services;
|
||||
const {
|
||||
width: colorbarWidth,
|
||||
@ -73,7 +55,7 @@ export function Colorbar({
|
||||
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
viewportId,
|
||||
colormaps,
|
||||
ticks: {
|
||||
|
||||
@ -3,14 +3,10 @@ import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next
|
||||
|
||||
import { StackViewport, Types } from '@cornerstonejs/core';
|
||||
import { ColormapProps } from '../../types/Colormap';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function Colormap({
|
||||
colormaps,
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
}: ColormapProps): ReactElement {
|
||||
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
|
||||
import { VolumeLightingProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeLighting({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
viewportId,
|
||||
hasShade,
|
||||
}: VolumeLightingProps): ReactElement {
|
||||
export function VolumeLighting({ viewportId, hasShade }: VolumeLightingProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [lightingValues, setLightingValues] = useState({
|
||||
ambient: null,
|
||||
|
||||
@ -5,43 +5,31 @@ import { VolumeRenderingQuality } from './VolumeRenderingQuality';
|
||||
import { VolumeShift } from './VolumeShift';
|
||||
import { VolumeLighting } from './VolumeLighting';
|
||||
import { VolumeShade } from './VolumeShade';
|
||||
|
||||
export function VolumeRenderingOptions({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
volumeRenderingQualityRange,
|
||||
servicesManager,
|
||||
}: VolumeRenderingOptionsProps): ReactElement {
|
||||
const [hasShade, setShade] = useState(false);
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
<VolumeRenderingQuality
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
|
||||
<VolumeShift
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<VolumeShift viewportId={viewportId} />
|
||||
<div className="mt-2 flex h-8 !h-[20px] w-full flex-shrink-0 items-center justify-start px-2 text-base">
|
||||
<div className="text-muted-foreground text-sm">Lighting</div>
|
||||
</div>
|
||||
<div className="bg-background mt-1 mb-1 h-px w-full"></div>
|
||||
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 items-center px-2 text-base hover:rounded">
|
||||
<VolumeShade
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
onClickShade={setShade}
|
||||
/>
|
||||
</div>
|
||||
<VolumeLighting
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
hasShade={hasShade}
|
||||
/>
|
||||
</AllInOneMenu.ItemPanel>
|
||||
|
||||
@ -3,13 +3,13 @@ import { Icons } from '@ohif/ui-next';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
|
||||
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeRenderingPresets({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
volumeRenderingPresets,
|
||||
}: VolumeRenderingPresetsProps): ReactElement {
|
||||
const { servicesManager } = useSystem();
|
||||
const { uiDialogService } = servicesManager.services;
|
||||
|
||||
const onClickPresets = () => {
|
||||
@ -21,7 +21,6 @@ export function VolumeRenderingPresets({
|
||||
contentProps: {
|
||||
presets: volumeRenderingPresets,
|
||||
viewportId,
|
||||
commandsManager,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
|
||||
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeRenderingQuality({
|
||||
volumeRenderingQualityRange,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
viewportId,
|
||||
}: VolumeRenderingQualityProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const { min, max, step } = volumeRenderingQualityRange;
|
||||
const [quality, setQuality] = useState(null);
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { VolumeShadeProps } from '../../types/ViewportPresets';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeShade({
|
||||
commandsManager,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
onClickShade = bool => {},
|
||||
}: VolumeShadeProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [shade, setShade] = useState(true);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { VolumeShiftProps } from '../../types/ViewportPresets';
|
||||
import { Numeric } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function VolumeShift({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
}: VolumeShiftProps): ReactElement {
|
||||
export function VolumeShift({ viewportId }: VolumeShiftProps): ReactElement {
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [minShift, setMinShift] = useState<number | null>(null);
|
||||
const [maxShift, setMaxShift] = useState<number | null>(null);
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
import { WindowLevelPreset } from '../../types/WindowLevel';
|
||||
import { CommandsManager } from '@ohif/core';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type WindowLevelProps = {
|
||||
viewportId: string;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
export function WindowLevel({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
presets,
|
||||
}: WindowLevelProps): ReactElement {
|
||||
export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement {
|
||||
const { commandsManager } = useSystem();
|
||||
const { t } = useTranslation('WindowLevelActionMenu');
|
||||
|
||||
const onSetWindowLevel = useCallback(
|
||||
|
||||
@ -15,8 +15,7 @@ import { VolumeRenderingOptions } from './VolumeRenderingOptions';
|
||||
import { ViewportPreset } from '../../types/ViewportPresets';
|
||||
import { VolumeViewport3D } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
|
||||
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
@ -34,13 +33,12 @@ export function WindowLevelActionMenu({
|
||||
presets,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
displaySets,
|
||||
volumeRenderingPresets,
|
||||
volumeRenderingQualityRange,
|
||||
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const {
|
||||
colormaps,
|
||||
colorbarContainerPosition,
|
||||
@ -48,6 +46,7 @@ export function WindowLevelActionMenu({
|
||||
colorbarTickPosition,
|
||||
width: colorbarWidth,
|
||||
} = colorbarProperties;
|
||||
|
||||
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@ -64,7 +63,7 @@ export function WindowLevelActionMenu({
|
||||
const [is3DVolume, setIs3DVolume] = useState(false);
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
setViewportColorbar(viewportId, commandsManager, servicesManager, {
|
||||
colormaps,
|
||||
ticks: {
|
||||
position: colorbarTickPosition,
|
||||
@ -117,7 +116,6 @@ export function WindowLevelActionMenu({
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
iconClassName={classNames(
|
||||
// Visible on hover and for the active viewport
|
||||
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
|
||||
'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight',
|
||||
isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30'
|
||||
@ -132,9 +130,7 @@ export function WindowLevelActionMenu({
|
||||
{!is3DVolume && (
|
||||
<Colorbar
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
colorbarProperties={colorbarProperties}
|
||||
/>
|
||||
)}
|
||||
@ -150,9 +146,7 @@ export function WindowLevelActionMenu({
|
||||
className="flex h-full w-full flex-col"
|
||||
colormaps={colormaps}
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
@ -165,7 +159,6 @@ export function WindowLevelActionMenu({
|
||||
>
|
||||
<WindowLevel
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
presets={presets}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
@ -173,9 +166,7 @@ export function WindowLevelActionMenu({
|
||||
|
||||
{volumeRenderingPresets && is3DVolume && (
|
||||
<VolumeRenderingPresets
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
/>
|
||||
)}
|
||||
@ -184,9 +175,7 @@ export function WindowLevelActionMenu({
|
||||
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
|
||||
<VolumeRenderingOptions
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { nonWLModalities } from './WindowLevelActionMenu';
|
||||
import { WindowLevelActionMenuProps } from './WindowLevelActionMenu';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu';
|
||||
|
||||
export function getWindowLevelActionMenu({
|
||||
function WindowLevelActionMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
}: withAppTypes<{
|
||||
@ -14,31 +14,25 @@ export function getWindowLevelActionMenu({
|
||||
element: HTMLElement;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
}>): ReactNode {
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
|
||||
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
|
||||
const { volumeRenderingPresets, volumeRenderingQualityRange } =
|
||||
customizationService.getCustomization('cornerstone.3dVolumeRendering');
|
||||
const WindowLevelActionMenu = customizationService.getCustomization(
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
const displaySetPresets = displaySets
|
||||
.filter(displaySet => presets[displaySet.Modality])
|
||||
.map(displaySet => {
|
||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||
});
|
||||
|
||||
const modalities = displaySets
|
||||
.map(displaySet => displaySet.Modality)
|
||||
.filter(modality => !nonWLModalities.includes(modality));
|
||||
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
|
||||
|
||||
if (modalities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const WindowLevelActionMenuComponent = WindowLevelActionMenu?.component;
|
||||
|
||||
return (
|
||||
<WindowLevelActionMenuComponent
|
||||
viewportId={viewportId}
|
||||
@ -46,8 +40,6 @@ export function getWindowLevelActionMenu({
|
||||
presets={displaySetPresets}
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
colorbarProperties={colorbarProperties}
|
||||
displaySets={displaySets}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
@ -55,3 +47,7 @@ export function getWindowLevelActionMenu({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getWindowLevelActionMenu(props: WindowLevelActionMenuProps) {
|
||||
return <WindowLevelActionMenu {...props} />;
|
||||
}
|
||||
|
||||
3
extensions/cornerstone/src/constants/index.ts
Normal file
3
extensions/cornerstone/src/constants/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Volume loader schemes
|
||||
export const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
export const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
|
||||
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
'cornerstone.modalityOverlayDefaultColorMaps': {
|
||||
defaultSettings: {
|
||||
PT: {
|
||||
colormap: 'hsv',
|
||||
// Note: Right now, there is a nonlinear relationship between the opacity value
|
||||
// below and how it will get applied to the image. The limitation is in rendering.
|
||||
// We are working on this and will remove this note when it's fixed.
|
||||
// But don't expect 0.5 to be 50% opacity, but rather close to that.
|
||||
opacity: 0.5,
|
||||
},
|
||||
RTDOSE: {
|
||||
colormap: 'Isodose',
|
||||
// Note: Right now, there is a nonlinear relationship between the opacity value
|
||||
// below and how it will get applied to the image. The limitation is in rendering.
|
||||
// We are working on this and will remove this note when it's fixed.
|
||||
// But don't expect 0.5 to be 50% opacity, but rather close to that.
|
||||
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,14 +1,55 @@
|
||||
import viewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||
import { getViewportOrientationMenu } from '../components/ViewportOrientationMenu';
|
||||
import { AllInOneMenu } from '@ohif/ui-next';
|
||||
|
||||
// Generate component renderer functions for each component type
|
||||
const createOrientationMenu = ({ viewportId, element, location }) => {
|
||||
return getViewportOrientationMenu({
|
||||
viewportId,
|
||||
element,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
const createDataOverlay = ({ viewportId, element, displaySets, location }) => {
|
||||
return getViewportDataOverlaySettingsMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
const createWindowLevelMenu = ({ viewportId, element, displaySets }) => {
|
||||
return getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.LeftToRight,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
'viewportActionMenu.windowLevelActionMenu': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
component: WindowLevelActionMenu,
|
||||
},
|
||||
'viewportActionMenu.segmentationOverlay': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
component: createOrientationMenu,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
component: createDataOverlay,
|
||||
},
|
||||
{
|
||||
id: 'windowLevelActionMenu',
|
||||
enabled: true,
|
||||
component: createWindowLevelMenu,
|
||||
},
|
||||
],
|
||||
'viewportActionMenu.topRight': [],
|
||||
'viewportActionMenu.bottomLeft': [],
|
||||
'viewportActionMenu.bottomRight': [],
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import viewportClickCommandsCustomization from './customizations/viewportClickCo
|
||||
import measurementsCustomization from './customizations/measurementsCustomization';
|
||||
import volumeRenderingCustomization from './customizations/volumeRenderingCustomization';
|
||||
import colorbarCustomization from './customizations/colorbarCustomization';
|
||||
import modalityColorMapCustomization from './customizations/modalityColorMapCustomization';
|
||||
import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization';
|
||||
import miscCustomization from './customizations/miscCustomization';
|
||||
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
|
||||
@ -25,6 +26,7 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
|
||||
...measurementsCustomization,
|
||||
...volumeRenderingCustomization,
|
||||
...colorbarCustomization,
|
||||
...modalityColorMapCustomization,
|
||||
...windowLevelPresetsCustomization,
|
||||
...miscCustomization,
|
||||
...captureViewportModalCustomization,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { dicomWebUtils } from '@ohif/extension-default';
|
||||
@ -112,6 +113,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
instances,
|
||||
othersFrameOfReferenceUID,
|
||||
imageIds: instances.map(instance => instance.imageId),
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SM')}`,
|
||||
};
|
||||
// The microscopy viewer directly accesses the metadata already loaded, and
|
||||
// uses the DICOMweb client library directly for loading, so it has to be
|
||||
|
||||
@ -233,7 +233,7 @@ function _evaluateToggle({
|
||||
}
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
if (!toolGroup?.hasTool(toolName)) {
|
||||
return getDisabledState(disabledText);
|
||||
}
|
||||
|
||||
|
||||
5
extensions/cornerstone/src/hooks/index.ts
Normal file
5
extensions/cornerstone/src/hooks/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as useViewportActionCorners } from './useViewportActionCorners';
|
||||
export * from './useViewportActionCorners';
|
||||
export * from './useActiveViewportSegmentationRepresentations';
|
||||
export * from './useMeasurements';
|
||||
export * from './useSegmentations';
|
||||
@ -84,7 +84,7 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||
* @returns An array of segmentation data.
|
||||
*/
|
||||
export function useSegmentations(options: {
|
||||
export function useSegmentations(options?: {
|
||||
subscribeToDataModified?: boolean;
|
||||
debounceTime?: number;
|
||||
}): SegmentationData[] {
|
||||
|
||||
113
extensions/cornerstone/src/hooks/useViewportActionCorners.ts
Normal file
113
extensions/cornerstone/src/hooks/useViewportActionCorners.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { ViewportActionCornersLocations } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
|
||||
/**
|
||||
* Viewport action corner location type
|
||||
*/
|
||||
export interface ViewportActionCornerLocation {
|
||||
type: 'VIEWPORT';
|
||||
id: string;
|
||||
corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport action corner service interface
|
||||
*/
|
||||
export interface ViewportActionCornerService {
|
||||
addComponent: (params: {
|
||||
viewportId: string;
|
||||
id: string;
|
||||
component: React.ReactNode;
|
||||
location: ViewportActionCornersLocations | string;
|
||||
indexPriority?: number;
|
||||
}) => void;
|
||||
clear: (viewportId: string) => void;
|
||||
getAlignAndSide: (location: ViewportActionCornersLocations | string) => {
|
||||
align: 'start' | 'center' | 'end';
|
||||
side: 'top' | 'right' | 'bottom' | 'left';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization service interface
|
||||
*/
|
||||
export interface CustomizationService {
|
||||
getCustomization: (name: string) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the useViewportActionCorners hook
|
||||
*/
|
||||
interface UseViewportActionCornersProps {
|
||||
viewportId: string;
|
||||
elementRef: React.MutableRefObject<HTMLDivElement>;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
}
|
||||
|
||||
// Map of customization keys to their corresponding enum values
|
||||
const locationMap = {
|
||||
'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
|
||||
'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
|
||||
'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
|
||||
'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to manage viewport action corners for a Cornerstone viewport
|
||||
*/
|
||||
export function useViewportActionCorners({
|
||||
viewportId,
|
||||
elementRef,
|
||||
displaySets,
|
||||
}: UseViewportActionCornersProps): void {
|
||||
const { servicesManager } = useSystem();
|
||||
const { viewportActionCornersService, customizationService } = servicesManager.services;
|
||||
|
||||
useEffect(() => {
|
||||
// Process each location
|
||||
Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
|
||||
const items = customizationService.getCustomization(locationKey);
|
||||
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
if (!item.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = item.id;
|
||||
|
||||
if (item.component) {
|
||||
// Use the component renderer provided directly in the item
|
||||
const component = item.component({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
location: locationValue,
|
||||
});
|
||||
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: componentId,
|
||||
component,
|
||||
location: locationValue,
|
||||
});
|
||||
} else if (item.component) {
|
||||
// Handle static components
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: item.id,
|
||||
component: item.component,
|
||||
location: locationValue,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [displaySets, viewportId, viewportActionCornersService, elementRef]);
|
||||
}
|
||||
|
||||
export default useViewportActionCorners;
|
||||
247
extensions/cornerstone/src/hooks/useViewportDisplaySets.ts
Normal file
247
extensions/cornerstone/src/hooks/useViewportDisplaySets.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSystem, utils } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import {
|
||||
getEnhancedDisplaySets,
|
||||
sortByOverlayable,
|
||||
DERIVED_OVERLAY_MODALITIES,
|
||||
} from '../components/ViewportDataOverlaySettingMenu/utils';
|
||||
|
||||
const sortByPriority = (a, b) => {
|
||||
if (utils.isLowPriorityModality(a.Modality)) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for the useViewportDisplaySets hook
|
||||
*/
|
||||
export type UseViewportDisplaySetsOptions = {
|
||||
/**
|
||||
* Whether to include background display set
|
||||
*/
|
||||
includeBackground?: boolean;
|
||||
/**
|
||||
* Whether to include foreground display sets
|
||||
*/
|
||||
includeForeground?: boolean;
|
||||
/**
|
||||
* Whether to include overlay display sets
|
||||
*/
|
||||
includeOverlay?: boolean;
|
||||
/**
|
||||
* Whether to include potential overlay display sets
|
||||
*/
|
||||
includePotentialOverlay?: boolean;
|
||||
/**
|
||||
* Whether to include potential foreground display sets
|
||||
*/
|
||||
includePotentialForeground?: boolean;
|
||||
/**
|
||||
* Whether to include potential background display sets
|
||||
*/
|
||||
includePotentialBackground?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return type for useViewportDisplaySets
|
||||
*/
|
||||
export type ViewportDisplaySets = {
|
||||
/**
|
||||
* The primary display set for the viewport (base image)
|
||||
*/
|
||||
backgroundDisplaySet?: any;
|
||||
/**
|
||||
* Display sets currently shown with background (non-overlay layers)
|
||||
*/
|
||||
foregroundDisplaySets?: any[];
|
||||
/**
|
||||
* Segmentation display sets currently applied as overlays
|
||||
*/
|
||||
overlayDisplaySets?: any[];
|
||||
/**
|
||||
* Display sets that could be toggled on as overlays (derived modalities)
|
||||
*/
|
||||
potentialOverlayDisplaySets?: any[];
|
||||
/**
|
||||
* Display sets that could be added as foreground layers
|
||||
*/
|
||||
potentialForegroundDisplaySets?: any[];
|
||||
/**
|
||||
* Display sets that could replace the current background
|
||||
*/
|
||||
potentialBackgroundDisplaySets?: any[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to provide display sets and overlay information for a viewport based on options.
|
||||
*
|
||||
* @param viewportId - The viewport ID to get display sets for
|
||||
* @param options - Options to control which display sets to compute
|
||||
* @returns Object containing requested display set collections based on options
|
||||
*/
|
||||
export function useViewportDisplaySets(
|
||||
viewportId: string,
|
||||
options?: UseViewportDisplaySetsOptions
|
||||
): ViewportDisplaySets {
|
||||
const { servicesManager } = useSystem();
|
||||
const { displaySetService, segmentationService } = servicesManager.services;
|
||||
|
||||
// Note: this is very important we should use the useViewportGrid hook here,
|
||||
// since if the viewport displaySet is changed we should re-run this hook
|
||||
// to get the latest displaySets
|
||||
const [_, viewportGridService] = useViewportGrid();
|
||||
|
||||
// Apply defaults - include everything if no options specified
|
||||
const {
|
||||
includeBackground = true,
|
||||
includeForeground = true,
|
||||
includeOverlay = true,
|
||||
includePotentialOverlay = true,
|
||||
includePotentialForeground = true,
|
||||
includePotentialBackground = true,
|
||||
} = options || {};
|
||||
|
||||
// Get all available display sets (only if needed)
|
||||
const needsAllDisplaySets = includePotentialBackground;
|
||||
const allDisplaySets = useMemo(
|
||||
() => (needsAllDisplaySets ? displaySetService.getActiveDisplaySets() : []),
|
||||
[displaySetService, needsAllDisplaySets]
|
||||
);
|
||||
|
||||
// Get all available segmentations (only if needed)
|
||||
const needsSegmentations = includeOverlay;
|
||||
const segmentationRepresentations = useMemo(
|
||||
() =>
|
||||
needsSegmentations ? segmentationService.getSegmentationRepresentations(viewportId) : [],
|
||||
[segmentationService, viewportId, needsSegmentations]
|
||||
);
|
||||
|
||||
const overlayDisplaySets = useMemo(() => {
|
||||
if (!includeOverlay) {
|
||||
return [];
|
||||
}
|
||||
return segmentationRepresentations.map(repr => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(repr.segmentationId);
|
||||
return displaySet;
|
||||
});
|
||||
}, [includeOverlay, segmentationRepresentations, displaySetService]);
|
||||
|
||||
const overlayDisplaySetUIDs = useMemo(
|
||||
() => overlayDisplaySets.map(ds => ds.displaySetInstanceUID),
|
||||
[overlayDisplaySets]
|
||||
);
|
||||
|
||||
// Get enhanced display sets (only if needed)
|
||||
const needsEnhancedDisplaySets =
|
||||
includeBackground || includeForeground || includePotentialOverlay || includePotentialForeground;
|
||||
|
||||
const { viewportDisplaySets, enhancedDisplaySets } = useMemo(() => {
|
||||
if (!needsEnhancedDisplaySets) {
|
||||
return { viewportDisplaySets: [], enhancedDisplaySets: [] };
|
||||
}
|
||||
return getEnhancedDisplaySets({
|
||||
viewportId,
|
||||
services: { displaySetService, viewportGridService },
|
||||
});
|
||||
}, [viewportId, displaySetService, viewportGridService, needsEnhancedDisplaySets]);
|
||||
|
||||
const backgroundDisplaySet = useMemo(
|
||||
() => (includeBackground ? viewportDisplaySets[0] : undefined),
|
||||
[includeBackground, viewportDisplaySets]
|
||||
);
|
||||
|
||||
const foregroundDisplaySets = useMemo(() => {
|
||||
if (!includeForeground || !backgroundDisplaySet) {
|
||||
return [];
|
||||
}
|
||||
return viewportDisplaySets.filter(
|
||||
ds =>
|
||||
!DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) &&
|
||||
ds.displaySetInstanceUID !== backgroundDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}, [includeForeground, viewportDisplaySets, backgroundDisplaySet]);
|
||||
|
||||
const foregroundDisplaySetUIDs = useMemo(
|
||||
() => foregroundDisplaySets.map(ds => ds.displaySetInstanceUID),
|
||||
[foregroundDisplaySets]
|
||||
);
|
||||
|
||||
const potentialOverlayDisplaySets = useMemo(() => {
|
||||
if (!includePotentialOverlay) {
|
||||
return [];
|
||||
}
|
||||
return enhancedDisplaySets
|
||||
.filter(
|
||||
ds =>
|
||||
DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) &&
|
||||
!overlayDisplaySetUIDs.includes(ds.displaySetInstanceUID) &&
|
||||
ds.isOverlayable
|
||||
)
|
||||
.sort(sortByOverlayable);
|
||||
}, [includePotentialOverlay, enhancedDisplaySets, overlayDisplaySetUIDs]);
|
||||
|
||||
const potentialForegroundDisplaySets = useMemo(() => {
|
||||
if (!includePotentialForeground) {
|
||||
return [];
|
||||
}
|
||||
return enhancedDisplaySets
|
||||
.filter(
|
||||
ds =>
|
||||
!DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) &&
|
||||
!foregroundDisplaySetUIDs.includes(ds.displaySetInstanceUID) &&
|
||||
ds.isOverlayable
|
||||
)
|
||||
.sort(sortByPriority);
|
||||
}, [includePotentialForeground, enhancedDisplaySets, foregroundDisplaySetUIDs]);
|
||||
|
||||
const potentialBackgroundDisplaySets = useMemo(() => {
|
||||
if (!includePotentialBackground || !backgroundDisplaySet) {
|
||||
return [];
|
||||
}
|
||||
return allDisplaySets
|
||||
.filter(
|
||||
ds =>
|
||||
!DERIVED_OVERLAY_MODALITIES.includes(ds.Modality) &&
|
||||
ds.displaySetInstanceUID !== backgroundDisplaySet.displaySetInstanceUID &&
|
||||
!overlayDisplaySetUIDs.includes(ds.displaySetInstanceUID) &&
|
||||
!foregroundDisplaySetUIDs.includes(ds.displaySetInstanceUID)
|
||||
)
|
||||
.sort(sortByPriority);
|
||||
}, [
|
||||
includePotentialBackground,
|
||||
allDisplaySets,
|
||||
backgroundDisplaySet,
|
||||
overlayDisplaySetUIDs,
|
||||
foregroundDisplaySetUIDs,
|
||||
]);
|
||||
|
||||
const result: ViewportDisplaySets = {};
|
||||
|
||||
if (includeBackground) {
|
||||
result.backgroundDisplaySet = backgroundDisplaySet;
|
||||
}
|
||||
|
||||
if (includeForeground) {
|
||||
result.foregroundDisplaySets = foregroundDisplaySets;
|
||||
}
|
||||
|
||||
if (includeOverlay) {
|
||||
result.overlayDisplaySets = overlayDisplaySets;
|
||||
}
|
||||
|
||||
if (includePotentialOverlay) {
|
||||
result.potentialOverlayDisplaySets = potentialOverlayDisplaySets;
|
||||
}
|
||||
|
||||
if (includePotentialForeground) {
|
||||
result.potentialForegroundDisplaySets = potentialForegroundDisplaySets;
|
||||
}
|
||||
|
||||
if (includePotentialBackground) {
|
||||
result.potentialBackgroundDisplaySets = potentialBackgroundDisplaySets;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -269,4 +269,7 @@ export {
|
||||
CornerstoneViewportDownloadForm,
|
||||
utils,
|
||||
};
|
||||
|
||||
// Export constants
|
||||
export { VOLUME_LOADER_SCHEME, DYNAMIC_VOLUME_LOADER_SCHEME } from './constants';
|
||||
export default cornerstoneExtension;
|
||||
|
||||
@ -96,6 +96,8 @@ export default async function init({
|
||||
viewportGridService,
|
||||
segmentationService,
|
||||
measurementService,
|
||||
colorbarService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
@ -255,8 +257,17 @@ export default async function init({
|
||||
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
|
||||
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
|
||||
|
||||
const getDisplaySetFromVolumeId = (volumeId: string) => {
|
||||
const allDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
const volume = cornerstone.cache.getVolume(volumeId);
|
||||
const imageIds = volume.imageIds;
|
||||
return allDisplaySets.find(ds => ds.imageIds?.some(id => imageIds.includes(id)));
|
||||
};
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
const { viewport } = getEnabledElement(element);
|
||||
initViewTiming({ element });
|
||||
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, evt => {
|
||||
const { element } = evt.detail;
|
||||
@ -268,7 +279,10 @@ export default async function init({
|
||||
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
||||
});
|
||||
|
||||
initViewTiming({ element });
|
||||
// limitation: currently supporting only volume viewports with fusion
|
||||
if (viewport.type !== cornerstone.Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
@ -294,12 +308,48 @@ export default async function init({
|
||||
100
|
||||
);
|
||||
|
||||
// Subscribe to actor events to dynamically update colorbars
|
||||
|
||||
// Call this function when initializing
|
||||
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
|
||||
|
||||
const { unsubscribe: unsubscribeSegmentationCreated } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
evt => {
|
||||
const { segmentationId } = evt;
|
||||
const displaySet = displaySetService.getDisplaySetByUID(segmentationId);
|
||||
if (displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const label = segmentation.cachedStats.info;
|
||||
const imageIds = segmentation.representationData.Labelmap.imageIds;
|
||||
|
||||
// Create a display set for the segmentation
|
||||
const segmentationDisplaySet = {
|
||||
displaySetInstanceUID: segmentationId,
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: label,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: imageIds.length,
|
||||
imageIds,
|
||||
isOverlayDisplaySet: true,
|
||||
label,
|
||||
madeInClient: true,
|
||||
segmentationId: segmentationId,
|
||||
isDerived: true,
|
||||
};
|
||||
|
||||
displaySetService.addDisplaySets(segmentationDisplaySet);
|
||||
}
|
||||
);
|
||||
|
||||
const unsubscriptions = [
|
||||
unsubscribeSegmentationDataModifiedHandler,
|
||||
unsubscribeSegmentationModifiedHandler,
|
||||
unsubscribeSegmentationCreated,
|
||||
];
|
||||
|
||||
return { unsubscriptions };
|
||||
|
||||
@ -38,14 +38,16 @@ export default class ColorbarService extends PubSubService {
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'colorbarService',
|
||||
create: () => {
|
||||
return new ColorbarService();
|
||||
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => {
|
||||
return new ColorbarService(servicesManager);
|
||||
},
|
||||
};
|
||||
colorbars = {};
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
|
||||
constructor() {
|
||||
constructor(servicesManager: AppTypes.ServicesManager) {
|
||||
super(ColorbarService.EVENTS);
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,6 +70,7 @@ export default class ColorbarService extends PubSubService {
|
||||
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
|
||||
*/
|
||||
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
|
||||
const { displaySetService } = this.servicesManager.services;
|
||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
|
||||
@ -95,6 +98,12 @@ export default class ColorbarService extends PubSubService {
|
||||
);
|
||||
|
||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
||||
// don't show colorbar for overlay display sets (e.g. segmentation)
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (displaySet.isOverlayDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
|
||||
const properties = viewport?.getProperties(volumeId);
|
||||
const colormap = properties?.colormap;
|
||||
@ -123,9 +132,19 @@ export default class ColorbarService extends PubSubService {
|
||||
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
|
||||
});
|
||||
if (this.colorbars[viewportId]) {
|
||||
this.colorbars[viewportId].push({ colorbar, container: colorbarContainer });
|
||||
this.colorbars[viewportId].push({
|
||||
colorbar,
|
||||
container: colorbarContainer,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
} else {
|
||||
this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }];
|
||||
this.colorbars[viewportId] = [
|
||||
{
|
||||
colorbar,
|
||||
container: colorbarContainer,
|
||||
displaySetInstanceUID,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@ -136,24 +155,50 @@ export default class ColorbarService extends PubSubService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references.
|
||||
* Removes a colorbar from a specific viewport. If displaySetInstanceUID is provided,
|
||||
* only the colorbar associated with that specific displaySetInstanceUID will be removed.
|
||||
* Otherwise, all colorbars for the given viewport will be removed.
|
||||
*
|
||||
* @param viewportId The identifier for the viewport from which the colorbar will be removed.
|
||||
* @param displaySetInstanceUID Optional. The specific display set instance UID associated with the colorbar to remove.
|
||||
*/
|
||||
public removeColorbar(viewportId) {
|
||||
public removeColorbar(viewportId, displaySetInstanceUID?: string) {
|
||||
const colorbarInfo = this.colorbars[viewportId];
|
||||
if (!colorbarInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
colorbarInfo.forEach(({ colorbar, container }) => {
|
||||
container.parentNode.removeChild(container);
|
||||
});
|
||||
if (displaySetInstanceUID) {
|
||||
// Find the index of the colorbar with the matching displaySetInstanceUID
|
||||
const index = colorbarInfo.findIndex(
|
||||
info => info.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
|
||||
delete this.colorbars[viewportId];
|
||||
if (index !== -1) {
|
||||
// Remove only the specific colorbar container
|
||||
const { container } = colorbarInfo[index];
|
||||
container.parentNode.removeChild(container);
|
||||
|
||||
// Remove the colorbar from the array
|
||||
colorbarInfo.splice(index, 1);
|
||||
|
||||
// If there are no more colorbars for this viewport, remove the entry
|
||||
if (colorbarInfo.length === 0) {
|
||||
delete this.colorbars[viewportId];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove all colorbars for the viewport (original behavior)
|
||||
colorbarInfo.forEach(({ container }) => {
|
||||
container.parentNode.removeChild(container);
|
||||
});
|
||||
|
||||
delete this.colorbars[viewportId];
|
||||
}
|
||||
|
||||
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
changeType: ChangeTypes.Removed,
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,8 +3,7 @@ import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
import { VOLUME_LOADER_SCHEME } from '../../constants';
|
||||
|
||||
class CornerstoneCacheService {
|
||||
static REGISTRATION = {
|
||||
|
||||
@ -8,26 +8,29 @@
|
||||
* and geometry Id
|
||||
*/
|
||||
export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) {
|
||||
return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => {
|
||||
const data = contourPoints.map(({ points, ...rest }) => {
|
||||
const newPoints = points.map(({ x, y, z }) => {
|
||||
return [x, y, z];
|
||||
return structureSet.ROIContours.map(
|
||||
({ contourPoints, ROINumber, ROIName, colorArray, ROIGroup }) => {
|
||||
const data = contourPoints.map(({ points, ...rest }) => {
|
||||
const newPoints = points.map(({ x, y, z }) => {
|
||||
return [x, y, z];
|
||||
});
|
||||
|
||||
return {
|
||||
...rest,
|
||||
points: newPoints,
|
||||
};
|
||||
});
|
||||
|
||||
const id = ROIName || ROINumber;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
points: newPoints,
|
||||
data,
|
||||
id,
|
||||
segmentIndex: ROINumber,
|
||||
color: colorArray,
|
||||
group: ROIGroup,
|
||||
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
|
||||
};
|
||||
});
|
||||
|
||||
const id = ROIName || ROINumber;
|
||||
|
||||
return {
|
||||
data,
|
||||
id,
|
||||
segmentIndex: ROINumber,
|
||||
color: colorArray,
|
||||
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/
|
||||
import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents';
|
||||
import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap';
|
||||
import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation';
|
||||
import { VOLUME_LOADER_SCHEME } from '../../constants';
|
||||
|
||||
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
|
||||
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
|
||||
@ -80,8 +81,6 @@ const EVENTS = {
|
||||
|
||||
const VALUE_TYPES = {};
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
|
||||
class SegmentationService extends PubSubService {
|
||||
static REGISTRATION = {
|
||||
name: 'segmentationService',
|
||||
@ -615,7 +614,7 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
// Process each segment similarly to the SEG function
|
||||
for (const rtStructData of allRTStructData) {
|
||||
const { data, id, color, segmentIndex, geometryId } = rtStructData;
|
||||
const { data, id, color, segmentIndex, geometryId, group } = rtStructData;
|
||||
|
||||
// Add the color to the colorLUT array
|
||||
colorLUT.push(color);
|
||||
@ -646,6 +645,7 @@ class SegmentationService extends PubSubService {
|
||||
cachedStats: segmentsCachedStats,
|
||||
locked: false,
|
||||
active: false,
|
||||
group,
|
||||
};
|
||||
|
||||
// Broadcast segment loading progress
|
||||
|
||||
@ -10,6 +10,11 @@ export type ActionComponentInfo = {
|
||||
indexPriority?: number;
|
||||
};
|
||||
|
||||
export type AlignAndSide = {
|
||||
align: 'start' | 'end' | 'center';
|
||||
side: 'top' | 'bottom' | 'left' | 'right';
|
||||
};
|
||||
|
||||
class ViewportActionCornersService extends PubSubService {
|
||||
public static readonly EVENTS = {};
|
||||
public static readonly LOCATIONS = ViewportActionCornersLocations;
|
||||
@ -66,6 +71,22 @@ class ViewportActionCornersService extends PubSubService {
|
||||
public clear(viewportId: string) {
|
||||
this.serviceImplementation._clear(viewportId);
|
||||
}
|
||||
|
||||
public getAlignAndSide(location: ViewportActionCornersLocations): AlignAndSide {
|
||||
switch (location) {
|
||||
case ViewportActionCornersLocations.topLeft:
|
||||
return { align: 'start', side: 'bottom' };
|
||||
case ViewportActionCornersLocations.topRight:
|
||||
return { align: 'end', side: 'bottom' };
|
||||
case ViewportActionCornersLocations.bottomLeft:
|
||||
return { align: 'start', side: 'top' };
|
||||
case ViewportActionCornersLocations.bottomRight:
|
||||
return { align: 'end', side: 'top' };
|
||||
default:
|
||||
console.debug('Unknown location, defaulting to bottom-start');
|
||||
return { align: 'start', side: 'bottom' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewportActionCornersService;
|
||||
|
||||
@ -32,6 +32,7 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
|
||||
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
|
||||
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
|
||||
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
|
||||
import { VOLUME_LOADER_SCHEME } from '../../constants';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
||||
@ -407,11 +408,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
// and we would lose the presentation.
|
||||
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
|
||||
|
||||
// Todo: i don't like this here, move it
|
||||
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
|
||||
viewportInfo.getViewportId()
|
||||
);
|
||||
|
||||
if (!viewportInfo) {
|
||||
throw new Error('element is not enabled for the given viewportId');
|
||||
}
|
||||
@ -513,6 +509,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
return this.viewportsById.get(viewportId);
|
||||
}
|
||||
|
||||
public getOrientation(viewportId: string): string {
|
||||
const viewportInfo = this.getViewportInfo(viewportId);
|
||||
return viewportInfo.getOrientation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks through the viewports to see if the specified measurement can be
|
||||
* displayed in one of the viewports. This function tries to get a "best fit"
|
||||
@ -761,8 +762,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const { imageIds, displaySetInstanceUID } = data;
|
||||
let volume = data.volume;
|
||||
|
||||
if (!volume) {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!volume && displaySet.images) {
|
||||
volume = csToolsUtils.getOrCreateImageVolume(displaySet.images.map(image => image.imageId));
|
||||
}
|
||||
|
||||
@ -780,6 +781,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
volumeInputArray.push({
|
||||
imageIds,
|
||||
volumeId,
|
||||
modality: displaySet.Modality,
|
||||
displaySetInstanceUID,
|
||||
blendMode: displaySetOptions.blendMode,
|
||||
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
||||
});
|
||||
@ -822,8 +825,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||
const displaySetModality = displaySet?.Modality;
|
||||
|
||||
// filter overlay display sets (e.g. segmentation) since they will get handled below via the segmentation service
|
||||
const filteredVolumeInputArray = volumeInputArray.filter(volumeInput => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(volumeInput.displaySetInstanceUID);
|
||||
return !displaySet?.isOverlayDisplaySet;
|
||||
});
|
||||
|
||||
// Todo: use presentations states
|
||||
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
|
||||
const volumesProperties = filteredVolumeInputArray.map((volumeInput, index) => {
|
||||
const { volumeId } = volumeInput;
|
||||
const displaySetOption = displaySetOptions[index];
|
||||
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
|
||||
@ -853,9 +863,29 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
});
|
||||
|
||||
// For SEG and RT viewports
|
||||
const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {};
|
||||
const { addOverlayFn, imageIds } = this._processExtraDisplaySetsForViewport(viewport) || {};
|
||||
|
||||
await viewport.setVolumes(volumeInputArray);
|
||||
if (!filteredVolumeInputArray.length && addOverlayFn) {
|
||||
// if there is no volume input array, and there is an addOverlayFn, means we need to take
|
||||
// care of the background overlay display set first then the addOverlayFn will add the
|
||||
// SEG displaySet
|
||||
const sampleImageId = imageIds[0];
|
||||
const backgroundDisplaySet = displaySetService.getDisplaySetsBy(
|
||||
displaySet =>
|
||||
!displaySet.isOverlayDisplaySet &&
|
||||
displaySet.images.some(image => image.imageId === sampleImageId)
|
||||
);
|
||||
|
||||
if (backgroundDisplaySet.length !== 1) {
|
||||
throw new Error('Background display set not found');
|
||||
}
|
||||
|
||||
await viewport.setVolumes([
|
||||
{ volumeId: `${VOLUME_LOADER_SCHEME}:${backgroundDisplaySet[0].displaySetInstanceUID}` },
|
||||
]);
|
||||
} else {
|
||||
await viewport.setVolumes(filteredVolumeInputArray);
|
||||
}
|
||||
|
||||
if (addOverlayFn) {
|
||||
addOverlayFn();
|
||||
@ -896,31 +926,30 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
|
||||
// Can be SEG or RTSTRUCT for now but not PMAP
|
||||
const segOrRTSOverlayDisplaySet = displaySetInstanceUIDs
|
||||
// Find overlay display sets (e.g. SEG, RTSTRUCT)
|
||||
const overlayDisplaySet = displaySetInstanceUIDs
|
||||
.map(displaySetService.getDisplaySetByUID)
|
||||
.find(
|
||||
displaySet =>
|
||||
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
|
||||
);
|
||||
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
||||
|
||||
// if it is only the overlay displaySet, then we need to get the reference
|
||||
// displaySet imageIds and set them as the imageIds for the viewport,
|
||||
// here we can do some logic if the reference is missing
|
||||
// then find the most similar match of displaySet instead
|
||||
if (!segOrRTSOverlayDisplaySet) {
|
||||
if (!overlayDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
|
||||
let imageIds;
|
||||
if (overlayDisplaySet.referencedDisplaySetInstanceUID) {
|
||||
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
overlayDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
imageIds = referenceDisplaySet.images.map(image => image.imageId);
|
||||
}
|
||||
|
||||
return {
|
||||
imageIds,
|
||||
addOverlayFn: () =>
|
||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport),
|
||||
addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -316,6 +316,10 @@ class ViewportInfo {
|
||||
return this.viewportOptions.orientation;
|
||||
}
|
||||
|
||||
public setOrientation(orientation: Enums.OrientationAxis): void {
|
||||
this.viewportOptions.orientation = orientation;
|
||||
}
|
||||
|
||||
public getDisplayArea(): Types.DisplayArea {
|
||||
return this.viewportOptions.displayArea;
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { CommandsManager } from '@ohif/core';
|
||||
|
||||
export type ViewportPreset = {
|
||||
name: string;
|
||||
gradientOpacity: string;
|
||||
@ -15,8 +13,6 @@ export type ViewportPreset = {
|
||||
|
||||
export type VolumeRenderingPresetsProps = {
|
||||
viewportId: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
commandsManager: CommandsManager;
|
||||
volumeRenderingPresets: ViewportPreset[];
|
||||
};
|
||||
|
||||
@ -24,13 +20,10 @@ export type VolumeRenderingPresetsContentProps = {
|
||||
presets: ViewportPreset[];
|
||||
onClose: () => void;
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
export type VolumeRenderingOptionsProps = {
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
||||
};
|
||||
|
||||
@ -42,27 +35,19 @@ export type VolumeRenderingQualityRange = {
|
||||
|
||||
export type VolumeRenderingQualityProps = {
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
volumeRenderingQualityRange: VolumeRenderingQualityRange;
|
||||
};
|
||||
|
||||
export type VolumeShiftProps = {
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
};
|
||||
|
||||
export type VolumeShadeProps = {
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
onClickShade?: (bool: boolean) => void;
|
||||
};
|
||||
|
||||
export type VolumeLightingProps = {
|
||||
viewportId: string;
|
||||
commandsManager: CommandsManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
hasShade: boolean;
|
||||
};
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import * as CornerstoneCacheService from './CornerstoneCacheService';
|
||||
import type CornerstoneServices from './CornerstoneServices';
|
||||
export { type SyncGroup } from '../services/SyncGroupService';
|
||||
export {
|
||||
type ViewportActionCornerLocation,
|
||||
type ViewportActionCornerService,
|
||||
type CustomizationService,
|
||||
} from '../hooks/useViewportActionCorners';
|
||||
|
||||
export type { CornerstoneCacheService, CornerstoneServices };
|
||||
|
||||
@ -13,6 +13,15 @@ const colormaps = [
|
||||
RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0],
|
||||
description: 'X Ray',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'Isodose',
|
||||
NanColor: [1, 0, 0],
|
||||
RGBPoints: [
|
||||
0, 0, 1, 0, 0.1, 0.5, 1, 0, 0.2, 1, 1, 0, 0.3, 1, 0.66, 0, 0.4, 1, 0.33, 0, 0.5, 1, 0, 0,
|
||||
],
|
||||
description: 'Isodose',
|
||||
},
|
||||
{
|
||||
ColorSpace: 'RGB',
|
||||
Name: 'hsv',
|
||||
|
||||
@ -24,9 +24,13 @@ export function getViewportPresentations(
|
||||
|
||||
const { lutPresentationId, positionPresentationId, segmentationPresentationId } = presentationIds;
|
||||
|
||||
const positionPresentation = positionPresentationStore[positionPresentationId];
|
||||
const lutPresentation = lutPresentationStore[lutPresentationId];
|
||||
const segmentationPresentation = segmentationPresentationStore[segmentationPresentationId];
|
||||
|
||||
return {
|
||||
positionPresentation: positionPresentationStore[positionPresentationId],
|
||||
lutPresentation: lutPresentationStore[lutPresentationId],
|
||||
segmentationPresentation: segmentationPresentationStore[segmentationPresentationId],
|
||||
positionPresentation,
|
||||
lutPresentation,
|
||||
segmentationPresentation,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,17 +7,14 @@ import {
|
||||
Icons,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* The default sub-menu appearance and setup is defined here, but this can be
|
||||
* replaced by
|
||||
*/
|
||||
const getMenuItemsDefault = ({
|
||||
commandsManager,
|
||||
items,
|
||||
servicesManager,
|
||||
...props
|
||||
}: withAppTypes) => {
|
||||
const getMenuItemsDefault = ({ commandsManager, items, ...props }: withAppTypes) => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
// This allows replacing the default child item for menus, whereas the entire
|
||||
@ -36,7 +33,7 @@ const getMenuItemsDefault = ({
|
||||
onClick: ({ commandsManager, ...props }: withAppTypes) => () => void;
|
||||
};
|
||||
}) => (
|
||||
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, ...props })}>
|
||||
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, servicesManager, ...props })}>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.iconName && <Icons.ByName name={item.iconName} />}
|
||||
<span>{item.label}</span>
|
||||
@ -76,12 +73,8 @@ const getMenuItemsDefault = ({
|
||||
* @returns Component bound to the bindProps
|
||||
*/
|
||||
export default function MoreDropdownMenu(bindProps) {
|
||||
const {
|
||||
menuItemsKey,
|
||||
getMenuItems = getMenuItemsDefault,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
} = bindProps;
|
||||
const { menuItemsKey, getMenuItems = getMenuItemsDefault, commandsManager } = bindProps;
|
||||
const { servicesManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const items = customizationService.getCustomization(menuItemsKey);
|
||||
|
||||
@ -3,6 +3,12 @@ import { Types, DicomMetadataStore } from '@ohif/core';
|
||||
import { ContextMenuController } from './CustomizableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||
import {
|
||||
configureViewportForLayerAddition,
|
||||
configureViewportForLayerRemoval,
|
||||
canAddDisplaySetToViewport,
|
||||
DERIVED_OVERLAY_MODALITIES,
|
||||
} from './utils/layerConfigurationUtils';
|
||||
import findViewportsByPosition, {
|
||||
findOrCreateViewport as layoutFindOrCreate,
|
||||
} from './findViewportsByPosition';
|
||||
@ -51,6 +57,137 @@ const commandsModule = ({
|
||||
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Adds a display set as a layer to the specified viewport
|
||||
*
|
||||
* @param options.viewportId - The ID of the viewport to add the layer to
|
||||
* @param options.displaySetInstanceUID - The UID of the display set to add as a layer
|
||||
* @param options.removeFirst - Optional flag to remove the display set first if it's already added
|
||||
*/
|
||||
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
|
||||
if (!viewportId || !displaySetInstanceUID) {
|
||||
console.warn('Missing required parameters for addDisplaySetAsLayer command');
|
||||
return;
|
||||
}
|
||||
|
||||
const { displaySetService, viewportGridService, hangingProtocolService } =
|
||||
servicesManager.services;
|
||||
|
||||
// Get the display set
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current display sets for the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// Check if we can add this display set to the viewport
|
||||
const canAdd = canAddDisplaySetToViewport({
|
||||
viewportId,
|
||||
displaySetInstanceUID,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
if (!canAdd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the display set to the viewport
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Configure each viewport for layer addition
|
||||
updatedViewports.forEach(viewport => {
|
||||
configureViewportForLayerAddition({
|
||||
viewport,
|
||||
displaySetInstanceUID,
|
||||
currentDisplaySetUIDs,
|
||||
servicesManager,
|
||||
});
|
||||
});
|
||||
|
||||
// Update position presentation
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
||||
});
|
||||
|
||||
// Run command to update viewports
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: updatedViewports,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a display set layer from the specified viewport
|
||||
*
|
||||
* @param options.viewportId - The ID of the viewport to remove the layer from
|
||||
* @param options.displaySetInstanceUID - The UID of the display set to remove
|
||||
*/
|
||||
removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => {
|
||||
if (!viewportId || !displaySetInstanceUID) {
|
||||
console.warn('Missing required parameters for removeDisplaySetLayer command');
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
hangingProtocolService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// Get the display set
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current display sets for the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// If the display set is not in the viewport, no need to remove it
|
||||
if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a segmentation and handle accordingly
|
||||
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
|
||||
if (isSegmentation) {
|
||||
segmentationService.removeSegmentationRepresentations(viewportId, {
|
||||
segmentationId: displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Configure each viewport for layer removal
|
||||
updatedViewports.forEach(viewport => {
|
||||
configureViewportForLayerRemoval({
|
||||
viewport,
|
||||
displaySetInstanceUID,
|
||||
currentDisplaySetUIDs,
|
||||
servicesManager,
|
||||
});
|
||||
});
|
||||
|
||||
// Update position presentation
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
||||
});
|
||||
|
||||
// Update the viewports
|
||||
commandsManager.run('setDisplaySetsForViewports', {
|
||||
viewportsToUpdate: updatedViewports,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Runs a command in multi-monitor mode. No-op if not multi-monitor.
|
||||
*/
|
||||
@ -644,6 +781,8 @@ const commandsModule = ({
|
||||
openDICOMTagViewer: actions.openDICOMTagViewer,
|
||||
updateViewportDisplaySet: actions.updateViewportDisplaySet,
|
||||
scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView,
|
||||
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
||||
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -14,6 +14,26 @@ export default {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'addAsLayer',
|
||||
label: 'Add as Layer',
|
||||
iconName: 'ViewportViews',
|
||||
onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
|
||||
// Get the active viewport
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
if (!activeViewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the new command to add the display set as a layer
|
||||
commandsManager.runCommand('addDisplaySetAsLayer', {
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUID,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
'studyBrowser.sortFunctions': [
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { utils, classes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { id } from './id';
|
||||
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
|
||||
@ -99,6 +100,11 @@ const makeDisplaySet = instances => {
|
||||
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
||||
isDynamicVolume,
|
||||
dynamicVolumeInfo,
|
||||
supportsWindowLevel: true,
|
||||
label:
|
||||
instance.SeriesDescription ||
|
||||
`${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
});
|
||||
|
||||
// Sort the images in this series if needed
|
||||
@ -260,6 +266,7 @@ const sopClassUids = [
|
||||
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
|
||||
sopClassDictionary.RTImageStorage,
|
||||
sopClassDictionary.EnhancedUSVolumeStorage,
|
||||
sopClassDictionary.RTDoseStorage,
|
||||
];
|
||||
|
||||
function getSopClassHandlerModule(appContextParam) {
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import { CommandsManager, ExtensionManager } from '@ohif/core';
|
||||
import LineChartViewport from './Components/LineChartViewport/index';
|
||||
|
||||
const getViewportModule = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
commandsManager: CommandsManager;
|
||||
extensionManager: ExtensionManager;
|
||||
}) => {
|
||||
const getViewportModule = () => {
|
||||
return [
|
||||
{
|
||||
name: 'chartViewport',
|
||||
|
||||
@ -22,11 +22,11 @@ export default function init({
|
||||
cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
]);
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata);
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handleScalingModules);
|
||||
|
||||
// If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight)
|
||||
// we need to recalculate the SUV Scaling Factors
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata);
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handleScalingModules);
|
||||
|
||||
// Adds extra custom attributes for use by hanging protocols
|
||||
registerHangingProtocolAttributes({ servicesManager });
|
||||
@ -71,7 +71,7 @@ export default function init({
|
||||
});
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
const handleScalingModules = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
const { instances } = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
if (!instances?.length) {
|
||||
@ -80,12 +80,41 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
const modality = instances[0].Modality;
|
||||
|
||||
if (!modality || modality !== 'PT') {
|
||||
const allowedModality = ['PT', 'RTDOSE'];
|
||||
|
||||
if (!allowedModality.includes(modality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = instances.map(instance => instance.imageId);
|
||||
const instanceMetadataArray = [];
|
||||
|
||||
if (modality === 'RTDOSE') {
|
||||
const DoseGridScaling = instances[0].DoseGridScaling;
|
||||
const DoseSummation = instances[0].DoseSummation;
|
||||
const DoseType = instances[0].DoseType;
|
||||
const DoseUnit = instances[0].DoseUnit;
|
||||
const NumberOfFrames = instances[0].NumberOfFrames;
|
||||
const imageId = imageIds[0];
|
||||
|
||||
// add scaling module to the metadata
|
||||
// since RTDOSE is always a multiframe we should add the scaling module to each frame
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
const frameIndex = i + 1;
|
||||
|
||||
// Todo: we should support other things like wadouri, local etc
|
||||
const newImageId = `${imageId.replace(/\/frames\/\d+$/, '')}/frames/${frameIndex}`;
|
||||
metadataProvider.addCustomMetadata(newImageId, 'scalingModule', {
|
||||
DoseGridScaling,
|
||||
DoseSummation,
|
||||
DoseType,
|
||||
DoseUnit,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// try except block to prevent errors when the metadata is not correct
|
||||
try {
|
||||
imageIds.forEach(imageId => {
|
||||
|
||||
157
extensions/default/src/utils/layerConfigurationUtils.ts
Normal file
157
extensions/default/src/utils/layerConfigurationUtils.ts
Normal file
@ -0,0 +1,157 @@
|
||||
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
|
||||
export const DEFAULT_COLORMAP = 'hsv';
|
||||
export const DEFAULT_OPACITY = 0.9;
|
||||
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
|
||||
|
||||
/**
|
||||
* Get modality-specific color and opacity settings from the customization service
|
||||
*/
|
||||
export function getModalityOverlayColormap(customizationService, modality) {
|
||||
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
|
||||
'cornerstone.modalityOverlayDefaultColorMaps'
|
||||
) || { defaultSettings: {} };
|
||||
|
||||
return (
|
||||
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
|
||||
colormap: DEFAULT_COLORMAP,
|
||||
opacity: DEFAULT_OPACITY,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create display set options based on modality and opacity settings
|
||||
*/
|
||||
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
|
||||
return {
|
||||
colormap: {
|
||||
name: modalitySettings.colormap || DEFAULT_COLORMAP,
|
||||
opacity: opacity / 100, // Convert from percentage to 0-1 range
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure viewport for adding a display set layer
|
||||
*/
|
||||
export function configureViewportForLayerAddition(params: {
|
||||
viewport: any;
|
||||
displaySetInstanceUID: string;
|
||||
currentDisplaySetUIDs: string[];
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): any {
|
||||
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
|
||||
const { cornerstoneViewportService, displaySetService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewportId } = viewport;
|
||||
|
||||
// Set the display set UIDs for the viewport
|
||||
const allDisplaySetInstanceUIDs = [...currentDisplaySetUIDs, displaySetInstanceUID];
|
||||
viewport.displaySetInstanceUIDs = allDisplaySetInstanceUIDs;
|
||||
|
||||
if (!viewport.viewportOptions) {
|
||||
viewport.viewportOptions = {};
|
||||
}
|
||||
|
||||
const requestedLayerDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (!viewport.viewportOptions.orientation) {
|
||||
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
|
||||
}
|
||||
|
||||
// Do not force volume for SEG and RTSTRUCT if there is only one display set
|
||||
if (requestedLayerDisplaySet.isOverlayDisplaySet && currentDisplaySetUIDs.length === 1) {
|
||||
viewport.viewportOptions.viewportType = 'stack';
|
||||
} else {
|
||||
viewport.viewportOptions.viewportType = 'volume';
|
||||
}
|
||||
|
||||
// create same amount of display set options as the number of display set UIDs
|
||||
const displaySetOptions = allDisplaySetInstanceUIDs.map((uid, index) => {
|
||||
if (index === 0) {
|
||||
// no colormap for background
|
||||
return {};
|
||||
}
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(uid);
|
||||
return createColormapOverlayDisplaySetOptions(displaySet, 90, customizationService);
|
||||
});
|
||||
|
||||
viewport.displaySetOptions = displaySetOptions;
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure viewport for removing a display set layer
|
||||
*/
|
||||
export function configureViewportForLayerRemoval(params: {
|
||||
viewport: any;
|
||||
displaySetInstanceUID: string;
|
||||
currentDisplaySetUIDs: string[];
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): any {
|
||||
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
|
||||
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
|
||||
|
||||
const { viewportId } = viewport;
|
||||
|
||||
// Filter out the display set to remove
|
||||
viewport.displaySetInstanceUIDs = currentDisplaySetUIDs.filter(
|
||||
uid => uid !== displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!viewport.viewportOptions) {
|
||||
viewport.viewportOptions = {};
|
||||
}
|
||||
|
||||
viewport.viewportOptions.viewportType = 'volume';
|
||||
|
||||
// orientation
|
||||
if (!viewport.viewportOptions.orientation) {
|
||||
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
|
||||
}
|
||||
|
||||
// Recreate the display set options
|
||||
viewport.displaySetOptions = viewport.displaySetInstanceUIDs.map(() => {
|
||||
// For simplicity, we're returning empty options for now
|
||||
// In a more complete implementation, we would need to preserve existing display set options
|
||||
return {};
|
||||
});
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a display set can be added as a layer to the specified viewport
|
||||
*/
|
||||
export function canAddDisplaySetToViewport(params: {
|
||||
viewportId: string;
|
||||
displaySetInstanceUID: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}): boolean {
|
||||
const { viewportId, displaySetInstanceUID, servicesManager } = params;
|
||||
const { displaySetService, viewportGridService } = servicesManager.services;
|
||||
|
||||
// Check if the display set exists
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current display sets in the viewport
|
||||
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
// Check if the display set is already in the viewport
|
||||
if (currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { utils, classes } from '@ohif/core';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1',
|
||||
@ -50,12 +49,15 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
numImageFrames: 0,
|
||||
numInstances: 1,
|
||||
instance,
|
||||
supportsWindowLevel: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t(Modality)}`,
|
||||
};
|
||||
return displaySet;
|
||||
});
|
||||
};
|
||||
|
||||
export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
export default function getSopClassHandlerModule(params) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { utils } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core';
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
@ -90,6 +91,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
sopClassUids,
|
||||
numImageFrames: NumberOfFrames,
|
||||
instance,
|
||||
supportsWindowLevel: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t(Modality)}`,
|
||||
};
|
||||
csUtils.genericMetadataProvider.add(imageId, {
|
||||
type: 'imageUrlModule',
|
||||
@ -99,7 +102,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
});
|
||||
};
|
||||
|
||||
export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
export default function getSopClassHandlerModule(params) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/tools": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/tools": "^3.11.6",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.23",
|
||||
"@ohif/extension-default": "3.11.0-beta.23",
|
||||
|
||||
@ -113,7 +113,7 @@ function TrackedMeasurementsContextProvider(
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUID: referencedDisplaySetUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySetUID],
|
||||
referencedImageId: trackedMeasurement.referencedImageId,
|
||||
});
|
||||
|
||||
|
||||
@ -207,7 +207,7 @@ function TrackedCornerstoneViewport(
|
||||
id: 'viewportStatusComponent',
|
||||
component: statusComponent,
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@ohif/core": "3.11.0-beta.23",
|
||||
"@ohif/extension-cornerstone": "3.11.0-beta.23",
|
||||
|
||||
@ -230,7 +230,7 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
// Update stored position presentation
|
||||
commandsManager.run('updateStoredPositionPresentation', {
|
||||
viewportId: viewport.viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySetInstanceUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
|
||||
referencedImageId: measurement.referencedImageId,
|
||||
options: {
|
||||
...measurement.metadata,
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/core": "^3.9.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.9.5",
|
||||
"@cornerstonejs/core": "^3.11.6",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.11.6",
|
||||
"@ohif/ui": "3.11.0-beta.23",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@ -321,7 +321,7 @@ class MeasurementService extends PubSubService {
|
||||
this.mappings[source.uid] = [mapping];
|
||||
}
|
||||
|
||||
log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`);
|
||||
// log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -239,7 +239,10 @@ export default class ToolbarService extends PubSubService {
|
||||
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, disabledText, className, isActive };
|
||||
} else {
|
||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||
const evaluated =
|
||||
typeof props.evaluate === 'function'
|
||||
? props.evaluate({ ...refreshProps, button })
|
||||
: undefined;
|
||||
const updatedProps = {
|
||||
...props,
|
||||
...evaluated,
|
||||
|
||||
@ -13,6 +13,9 @@ export type DisplaySet = {
|
||||
Modality?: string;
|
||||
imageIds?: string[];
|
||||
images?: unknown[];
|
||||
label?: string;
|
||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||
isOverlayDisplaySet?: boolean;
|
||||
|
||||
// Details about how to display:
|
||||
/** A URL that can be used to display the thumbnail. Typically a data url */
|
||||
|
||||
@ -19,11 +19,14 @@ const combineFrameInstance = (frame, instance) => {
|
||||
ImageType,
|
||||
} = instance;
|
||||
|
||||
if (NumberOfFrames < 2) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
instance.ImageType = dicomSplit(ImageType);
|
||||
const frameNumber = Number.parseInt(frame || 1);
|
||||
|
||||
if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) {
|
||||
const frameNumber = Number.parseInt(frame || 1);
|
||||
|
||||
if (PerFrameFunctionalGroupsSequence && SharedFunctionalGroupsSequence) {
|
||||
// this is to fix NM multiframe datasets with position and orientation
|
||||
// information inside DetectorInformationSequence
|
||||
if (!instance.ImageOrientationPatient && instance.DetectorInformationSequence) {
|
||||
@ -96,9 +99,52 @@ const combineFrameInstance = (frame, instance) => {
|
||||
configurable: true,
|
||||
});
|
||||
return newInstance;
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// For RTDOSE datasets
|
||||
if (instance.GridFrameOffsetVector) {
|
||||
if (!instance._parentInstance) {
|
||||
Object.defineProperty(instance, '_parentInstance', {
|
||||
value: { ...instance },
|
||||
});
|
||||
}
|
||||
|
||||
const sharedInstance = createCombinedValue(
|
||||
instance._parentInstance,
|
||||
SharedFunctionalGroupsSequence?.[0],
|
||||
'_shared'
|
||||
);
|
||||
|
||||
const newInstance = createCombinedValue(
|
||||
sharedInstance,
|
||||
PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const origin = newInstance.ImagePositionPatient?.map(Number);
|
||||
const orientation = newInstance.ImageOrientationPatient?.map(Number);
|
||||
const offset = Number(instance.GridFrameOffsetVector[frameNumber - 1]);
|
||||
|
||||
if (origin && orientation && !Number.isNaN(offset)) {
|
||||
const row = vec3.fromValues(orientation[0], orientation[1], orientation[2]);
|
||||
const col = vec3.fromValues(orientation[3], orientation[4], orientation[5]);
|
||||
const normal = vec3.cross(vec3.create(), row, col);
|
||||
|
||||
const position = vec3.scaleAndAdd(vec3.create(), vec3.fromValues(...origin), normal, offset);
|
||||
newInstance.ImagePositionPatient = [position[0], position[1], position[2]];
|
||||
}
|
||||
|
||||
Object.defineProperty(newInstance, 'frameNumber', {
|
||||
value: frameNumber,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT', 'RTDOSE', 'RTPLAN', 'RTRECORD', 'REG']);
|
||||
const LOW_PRIORITY_MODALITIES = Object.freeze([
|
||||
'SEG',
|
||||
'KO',
|
||||
'PR',
|
||||
'SR',
|
||||
'RTSTRUCT',
|
||||
'RTDOSE',
|
||||
'RTPLAN',
|
||||
'RTRECORD',
|
||||
'REG',
|
||||
]);
|
||||
|
||||
export default function isLowPriorityModality(Modality) {
|
||||
return LOW_PRIORITY_MODALITIES.includes(Modality);
|
||||
|
||||
178
platform/docs/docs/faq/add-viewport-icon.md
Normal file
178
platform/docs/docs/faq/add-viewport-icon.md
Normal file
@ -0,0 +1,178 @@
|
||||
---
|
||||
id: add-viewport-icon
|
||||
title: How to add a custom icon to the viewport corners
|
||||
summary: Learn how to add a custom icon or dropdown to one of the viewport corners in OHIF.
|
||||
---
|
||||
|
||||
# How to add a custom icon to the viewport corners
|
||||
|
||||
## Question
|
||||
How can I add a custom icon or dropdown to one of the viewport corners in OHIF?
|
||||
|
||||
## Answer
|
||||
OHIF provides a customizable viewport action menu system that allows you to add icons, buttons, or dropdowns to any of the four corners of a viewport (topLeft, topRight, bottomLeft, bottomRight). This is done through the `customizationService` and the viewport action corners API.
|
||||
|
||||
Here's a complete example that shows how to add a mode switch dropdown to the top-left corner of the viewport:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
// This is a complete, self-contained component that shows a dropdown menu
|
||||
// for switching modes when clicked
|
||||
function getModeSwitchMenu({ viewportId, element, location }) {
|
||||
const ModeSwitchMenu = () => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { router } = servicesManager.services;
|
||||
const { viewportActionCornersService } = servicesManager.services;
|
||||
|
||||
const handleModeSwitch = (mode) => {
|
||||
const currentStudyInstanceUID = router.query.StudyInstanceUIDs;
|
||||
// Navigate to the selected mode with the current study
|
||||
router.navigate(`/${mode}?StudyInstanceUIDs=${currentStudyInstanceUID}`);
|
||||
};
|
||||
|
||||
// Get proper alignment based on the location
|
||||
let align = 'center';
|
||||
let side = 'bottom';
|
||||
|
||||
if (location !== undefined) {
|
||||
const positioning = viewportActionCornersService.getAlignAndSide(location);
|
||||
align = positioning.align;
|
||||
side = positioning.side;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-highlight"
|
||||
>
|
||||
<Icons.Tool />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[160px]"
|
||||
align={align}
|
||||
side={side}
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenuLabel className="-ml-1">Mode</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('longitudinal')}>
|
||||
Longitudinal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('segmentation')}>
|
||||
Segmentation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('tmtv')}>
|
||||
TMTV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleModeSwitch('microscopy')}>
|
||||
Microscopy
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <ModeSwitchMenu />;
|
||||
}
|
||||
|
||||
// In your mode or extension, add this to the customizations
|
||||
// This example shows how to add it in the onModeEnter lifecycle hook
|
||||
function onModeEnter({ servicesManager }) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
// Add the mode switch icon to the top-left corner of the viewport
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
// Use $push to add to existing items or $set to replace all items
|
||||
$push: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
1. **Location-based customization**: The viewport is divided into four corners identified by:
|
||||
- `viewportActionMenu.topLeft`
|
||||
- `viewportActionMenu.topRight`
|
||||
- `viewportActionMenu.bottomLeft`
|
||||
- `viewportActionMenu.bottomRight`
|
||||
|
||||
2. **Component structure**:
|
||||
- `id` - A unique identifier for your component
|
||||
- `enabled` - Boolean to control if the component should be displayed
|
||||
- `component` - A function that returns a React component to render
|
||||
|
||||
3. **Component positioning**: The component's position within a corner is determined by its order in the array. Components are rendered in the order they appear.
|
||||
|
||||
4. **Dropdown positioning**: Use the `viewportActionCornersService.getAlignAndSide()` method to get the correct alignment for your dropdown menu based on its location.
|
||||
|
||||
## Adding Multiple Components
|
||||
|
||||
If you want to add multiple components to the same corner or different corners, you can do it in a single customization:
|
||||
|
||||
```js
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
$push: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
],
|
||||
},
|
||||
'viewportActionMenu.topRight': {
|
||||
$push: [
|
||||
{
|
||||
id: 'anotherComponent',
|
||||
enabled: true,
|
||||
component: getAnotherComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Replacing Existing Components
|
||||
|
||||
If you want to replace all components in a corner instead of adding to them, use `$set` instead of `$push`:
|
||||
|
||||
```js
|
||||
customizationService.setCustomizations({
|
||||
'viewportActionMenu.topLeft': {
|
||||
$set: [
|
||||
{
|
||||
id: 'modeSwitch',
|
||||
enabled: true,
|
||||
component: getModeSwitchMenu,
|
||||
},
|
||||
// This will be the only components in the top-left corner
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Remember that this customization will affect all viewports in the active mode. If you need different behavior for different viewports, you should check the `viewportId` parameter in your component's logic.
|
||||
@ -6,6 +6,7 @@ summary: Technical explanations and solutions for OHIF Viewer implementation cha
|
||||
|
||||
# Technical FAQ
|
||||
|
||||
* [How to add a custom icon to the viewport corners](./add-viewport-icon.md)
|
||||
|
||||
|
||||
## Viewer opens but does not show any thumbnails
|
||||
|
||||
22
platform/docs/docs/migration-guide/3p10-to-3p11/index.md
Normal file
22
platform/docs/docs/migration-guide/3p10-to-3p11/index.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: 3.10 -> 3.11 beta
|
||||
---
|
||||
|
||||
# Migration Guide
|
||||
|
||||
This guide provides information about migrating from OHIF version 3.10 to version 3.11.
|
||||
|
||||
## General
|
||||
|
||||
`viewportActionMenu.segmentationOverlay` is renamed to `viewportActionMenu.dataOverlay`
|
||||
as it handles now both segmentation and data overlay.
|
||||
|
||||
## Viewport Action Menu Customization
|
||||
|
||||
The structure for defining viewport action menu customizations has changed. See the [Viewport Action Menu](./viewport-action-menu.md) migration guide for details.
|
||||
|
||||
|
||||
## updateStoredPositionPresentation
|
||||
|
||||
now uses displaySetInstanceUIDs instead of displaySetInstanceUID.
|
||||
@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Viewport Action Menu
|
||||
summary: Migration guide for OHIF 3.11's viewport action menu customization changes, including the transition from individual item configurations to location-based arrays and the removal of index priorities.
|
||||
---
|
||||
|
||||
# Viewport Action Menu Customization
|
||||
|
||||
In OHIF 3.11, we've redesigned how viewport action menu customizations are defined to make them more intuitive and organized by location.
|
||||
|
||||
## Changes
|
||||
|
||||
Previously, viewport action menu customizations were defined with individual item configurations that specified location and priority:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
'viewportActionMenu.orientationMenu': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
indexPriority: 1,
|
||||
},
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
enabled: true,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
indexPriority: 2,
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Now, viewport action menu customizations are organized by location, with each location having its own customization ID:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'windowLevelActionMenu',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
'viewportActionMenu.topRight': [],
|
||||
'viewportActionMenu.bottomLeft': [],
|
||||
'viewportActionMenu.bottomRight': [],
|
||||
};
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. Reorganize your viewport action menu customizations by using location-based customization IDs (`viewportActionMenu.topLeft`, `viewportActionMenu.topRight`, etc.).
|
||||
2. For each component, move it into the appropriate location array.
|
||||
3. Replace the component key with an `id` property that doesn't include the prefix (just use `orientationMenu` instead of `viewportActionMenu.orientationMenu`).
|
||||
4. Remove the `location` property (since it's now implied by the customization ID).
|
||||
5. Remove the `indexPriority` property (order in the array now determines display order).
|
||||
6. For each component, provide a `component` function that returns the component instance.
|
||||
|
||||
## Component Rendering
|
||||
|
||||
Component rendering logic is now included directly in the item configuration via a `component` function:
|
||||
|
||||
```ts
|
||||
const createOrientationMenu = ({ viewportId, element, location }) => {
|
||||
return getViewportOrientationMenu({
|
||||
viewportId,
|
||||
element,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
const createDataOverlay = ({ viewportId, element, displaySets, location }) => {
|
||||
return getViewportDataOverlaySettingsMenu({
|
||||
viewportId,
|
||||
element,
|
||||
displaySets,
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
'viewportActionMenu.topLeft': [
|
||||
{
|
||||
id: 'orientationMenu',
|
||||
enabled: true,
|
||||
component: createOrientationMenu,
|
||||
},
|
||||
{
|
||||
id: 'dataOverlay',
|
||||
enabled: true,
|
||||
component: createDataOverlay,
|
||||
},
|
||||
// other components...
|
||||
],
|
||||
// other locations...
|
||||
};
|
||||
```
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: 3.9 -> 3.10 beta
|
||||
sidebar_label: 3.9 -> 3.10
|
||||
title: Migration Guide from 3.9 to 3.10 beta
|
||||
summary: Migration guide for upgrading from OHIF 3.9 to 3.10 beta, covering general changes, customization service improvements, UI component upgrades, command handling, hotkey updates, routing changes, and testing strategies.
|
||||
---
|
||||
|
||||
@ -937,8 +937,8 @@ window.config = {
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'viewportActionMenu.segmentationOverlay',
|
||||
description: 'Configures the display and location of the segmentation overlay in the viewport.',
|
||||
id: 'viewportActionMenu.dataOverlay',
|
||||
description: 'Configures the display and location of the data overlay in the viewport.',
|
||||
image: segmentationOverlay,
|
||||
default: null,
|
||||
configuration: `
|
||||
@ -946,7 +946,7 @@ window.config = {
|
||||
// rest of window config
|
||||
customizationService: [
|
||||
{
|
||||
'viewportActionMenu.segmentationOverlay': {
|
||||
'viewportActionMenu.dataOverlay': {
|
||||
$merge: {
|
||||
enabled: true,
|
||||
location: 1, // Set the location of the overlay in the viewport.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -57,7 +57,7 @@ import { cn } from '../../lib/utils';
|
||||
* @property {() => void} onColor - Callback when color change is requested
|
||||
*/
|
||||
interface DataRowProps {
|
||||
number: number;
|
||||
number: number | null;
|
||||
disableEditing: boolean;
|
||||
description: string;
|
||||
details?: { primary: string[]; secondary: string[] };
|
||||
@ -208,15 +208,18 @@ export const DataRow: React.FC<DataRowProps> = ({
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
|
||||
|
||||
{/* Number Box */}
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
{number !== null && (
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Color Circle (Optional) */}
|
||||
{/* add some space if there is not segment index */}
|
||||
{number === null && <div className="ml-1 h-7"></div>}
|
||||
{colorHex && (
|
||||
<div className="flex h-7 w-5 items-center justify-center">
|
||||
<span
|
||||
|
||||
@ -12,6 +12,7 @@ interface DoubleSliderProps {
|
||||
defaultValue?: [number, number];
|
||||
onValueChange?: (value: [number, number]) => void;
|
||||
showNumberInputs?: boolean;
|
||||
lockMode?: boolean;
|
||||
}
|
||||
|
||||
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
@ -24,10 +25,19 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
step = 1,
|
||||
defaultValue = [min, max],
|
||||
showNumberInputs = false,
|
||||
lockMode = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [value, setValue] = React.useState<[number, number]>(defaultValue);
|
||||
const trackRef = React.useRef<HTMLDivElement>(null);
|
||||
const dragStateRef = React.useRef<{
|
||||
startX: number;
|
||||
startValue: [number, number];
|
||||
rangeWidth: number;
|
||||
trackLeft: number;
|
||||
trackWidth: number;
|
||||
} | null>(null);
|
||||
|
||||
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
|
||||
|
||||
@ -50,8 +60,172 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
return Math.round(num * inverse) / inverse;
|
||||
};
|
||||
|
||||
const handleRangePointerDown = (event: React.PointerEvent) => {
|
||||
if (!lockMode) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const rect = trackRef.current!.getBoundingClientRect();
|
||||
dragStateRef.current = {
|
||||
startX: event.clientX,
|
||||
startValue: [...value] as [number, number],
|
||||
rangeWidth: value[1] - value[0],
|
||||
trackLeft: rect.left,
|
||||
trackWidth: rect.width,
|
||||
};
|
||||
window.addEventListener('pointermove', handleRangePointerMove);
|
||||
window.addEventListener('pointerup', handleRangePointerUp, { once: true });
|
||||
};
|
||||
|
||||
const handleRangePointerMove = (event: PointerEvent) => {
|
||||
const state = dragStateRef.current;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const { startX, startValue, rangeWidth, trackWidth } = state;
|
||||
const dxPx = event.clientX - startX;
|
||||
const pct = dxPx / trackWidth;
|
||||
const delta = pct * (max - min);
|
||||
|
||||
let newLeft = startValue[0] + delta;
|
||||
let newRight = newLeft + rangeWidth;
|
||||
|
||||
// clamp to bounds
|
||||
if (newLeft < min) {
|
||||
newLeft = min;
|
||||
newRight = min + rangeWidth;
|
||||
} else if (newRight > max) {
|
||||
newRight = max;
|
||||
newLeft = max - rangeWidth;
|
||||
}
|
||||
|
||||
const clampedLeft = roundToStep(newLeft);
|
||||
const clampedRight = roundToStep(newRight);
|
||||
setValue([clampedLeft, clampedRight]);
|
||||
onValueChange?.([clampedLeft, clampedRight]);
|
||||
};
|
||||
|
||||
const handleRangePointerUp = () => {
|
||||
dragStateRef.current = null;
|
||||
window.removeEventListener('pointermove', handleRangePointerMove);
|
||||
};
|
||||
|
||||
const handleSliderChange = React.useCallback(
|
||||
(newValue: number[]) => {
|
||||
// Default behavior (no lock mode)
|
||||
if (!lockMode) {
|
||||
const clampedValue: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newValue[0], max))),
|
||||
roundToStep(Math.min(max, Math.max(newValue[1], min))),
|
||||
];
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock mode behavior
|
||||
// First, determine if this is a symmetric expansion/contraction or a range shift
|
||||
// by checking which thumb(s) moved
|
||||
const isLeftThumbMoved = newValue[0] !== value[0];
|
||||
const isRightThumbMoved = newValue[1] !== value[1];
|
||||
|
||||
// Case 1: Left thumb moved, right thumb needs to move symmetrically (opposite)
|
||||
if (isLeftThumbMoved && !isRightThumbMoved) {
|
||||
const delta = newValue[0] - value[0];
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
|
||||
// Symmetric movement: move right thumb in opposite direction
|
||||
const newRight = value[1] - delta;
|
||||
|
||||
// Ensure values stay within bounds
|
||||
const clampedLeft = roundToStep(Math.max(min, Math.min(newValue[0], max)));
|
||||
const clampedRight = roundToStep(Math.min(max, Math.max(newRight, min)));
|
||||
|
||||
// Special case: If right would go out of bounds, adjust left too to maintain center
|
||||
if (newRight !== clampedRight) {
|
||||
// Calculate new delta given the clamped right value
|
||||
const adjustedDelta = value[1] - clampedRight;
|
||||
// Apply same delta to left but in opposite direction
|
||||
const adjustedLeft = value[0] + adjustedDelta;
|
||||
const finalClampedLeft = roundToStep(Math.max(min, Math.min(adjustedLeft, max)));
|
||||
|
||||
const finalValues: [number, number] = [finalClampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Right thumb moved, left thumb needs to move symmetrically (opposite)
|
||||
if (!isLeftThumbMoved && isRightThumbMoved) {
|
||||
const delta = newValue[1] - value[1];
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
|
||||
// Symmetric movement: move left thumb in opposite direction
|
||||
const newLeft = value[0] - delta;
|
||||
|
||||
// Ensure values stay within bounds
|
||||
const clampedRight = roundToStep(Math.min(max, Math.max(newValue[1], min)));
|
||||
const clampedLeft = roundToStep(Math.max(min, Math.min(newLeft, max)));
|
||||
|
||||
// Special case: If left would go out of bounds, adjust right too to maintain center
|
||||
if (newLeft !== clampedLeft) {
|
||||
// Calculate new delta given the clamped left value
|
||||
const adjustedDelta = value[0] - clampedLeft;
|
||||
// Apply same delta to right but in opposite direction
|
||||
const adjustedRight = value[1] + adjustedDelta;
|
||||
const finalClampedRight = roundToStep(Math.min(max, Math.max(adjustedRight, min)));
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, finalClampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalValues: [number, number] = [clampedLeft, clampedRight];
|
||||
setValue(finalValues);
|
||||
onValueChange?.(finalValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: Both thumbs moved (range shift) - maintain the same distance between thumbs
|
||||
if (isLeftThumbMoved && isRightThumbMoved) {
|
||||
const rangeWidth = value[1] - value[0];
|
||||
|
||||
// Calculate how much the left thumb moved
|
||||
const leftDelta = newValue[0] - value[0];
|
||||
|
||||
// New values maintaining the same range width
|
||||
let newLeft = newValue[0];
|
||||
let newRight = newLeft + rangeWidth;
|
||||
|
||||
// Handle bounds checking
|
||||
if (newRight > max) {
|
||||
newRight = max;
|
||||
newLeft = newRight - rangeWidth;
|
||||
}
|
||||
|
||||
if (newLeft < min) {
|
||||
newLeft = min;
|
||||
newRight = newLeft + rangeWidth;
|
||||
}
|
||||
|
||||
const clampedValues: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newLeft, max))),
|
||||
roundToStep(Math.min(max, Math.max(newRight, min))),
|
||||
];
|
||||
|
||||
setValue(clampedValues);
|
||||
onValueChange?.(clampedValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to default behavior
|
||||
const clampedValue: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newValue[0], max))),
|
||||
roundToStep(Math.min(max, Math.max(newValue[1], min))),
|
||||
@ -59,25 +233,86 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
},
|
||||
[min, max, onValueChange, step]
|
||||
[min, max, onValueChange, step, value, lockMode]
|
||||
);
|
||||
|
||||
const handleInputChange = React.useCallback(
|
||||
(index: 0 | 1, inputValue: string) => {
|
||||
const newValue = parseFloat(inputValue);
|
||||
if (!isNaN(newValue)) {
|
||||
const clampedValue: [number, number] = [...value];
|
||||
clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
if (index === 0 && clampedValue[0] > clampedValue[1]) {
|
||||
clampedValue[1] = clampedValue[0];
|
||||
} else if (index === 1 && clampedValue[1] < clampedValue[0]) {
|
||||
clampedValue[0] = clampedValue[1];
|
||||
// Default behavior (no lock mode)
|
||||
if (!lockMode) {
|
||||
const clampedValue: [number, number] = [...value];
|
||||
clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
if (index === 0 && clampedValue[0] > clampedValue[1]) {
|
||||
clampedValue[1] = clampedValue[0];
|
||||
} else if (index === 1 && clampedValue[1] < clampedValue[0]) {
|
||||
clampedValue[0] = clampedValue[1];
|
||||
}
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock mode behavior
|
||||
const centerPoint = (value[0] + value[1]) / 2;
|
||||
const rangeWidth = value[1] - value[0];
|
||||
const clampedValue: [number, number] = [...value];
|
||||
|
||||
// Calculate new value with constraints
|
||||
const boundedNewValue = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
|
||||
if (index === 0) {
|
||||
// Left thumb changed
|
||||
const delta = boundedNewValue - value[0];
|
||||
|
||||
// Symmetric change: move right thumb in opposite direction
|
||||
const newRight = value[1] - delta;
|
||||
const boundedNewRight = roundToStep(Math.min(Math.max(newRight, min), max));
|
||||
|
||||
// If right would go out of bounds, adjust left too
|
||||
if (newRight !== boundedNewRight) {
|
||||
// Adjust to maintain the same width but respect bounds
|
||||
if (boundedNewRight === max) {
|
||||
clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
|
||||
clampedValue[1] = max;
|
||||
} else if (boundedNewRight === min) {
|
||||
clampedValue[0] = min;
|
||||
clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
|
||||
}
|
||||
} else {
|
||||
clampedValue[0] = boundedNewValue;
|
||||
clampedValue[1] = boundedNewRight;
|
||||
}
|
||||
} else {
|
||||
// Right thumb changed
|
||||
const delta = boundedNewValue - value[1];
|
||||
|
||||
// Symmetric change: move left thumb in opposite direction
|
||||
const newLeft = value[0] - delta;
|
||||
const boundedNewLeft = roundToStep(Math.min(Math.max(newLeft, min), max));
|
||||
|
||||
// If left would go out of bounds, adjust right too
|
||||
if (newLeft !== boundedNewLeft) {
|
||||
// Adjust to maintain the same width but respect bounds
|
||||
if (boundedNewLeft === min) {
|
||||
clampedValue[0] = min;
|
||||
clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
|
||||
} else if (boundedNewLeft === max) {
|
||||
clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
|
||||
clampedValue[1] = max;
|
||||
}
|
||||
} else {
|
||||
clampedValue[0] = boundedNewLeft;
|
||||
clampedValue[1] = boundedNewValue;
|
||||
}
|
||||
}
|
||||
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
}
|
||||
},
|
||||
[value, min, max, onValueChange, step]
|
||||
[value, min, max, onValueChange, step, lockMode]
|
||||
);
|
||||
|
||||
const formatValue = (val: number) => {
|
||||
@ -109,8 +344,14 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
value={value}
|
||||
onValueChange={handleSliderChange}
|
||||
>
|
||||
<SliderPrimitive.Track className="bg-primary/30 relative h-1 w-full grow overflow-hidden rounded-full">
|
||||
<SliderPrimitive.Range className="bg-primary absolute h-full" />
|
||||
<SliderPrimitive.Track
|
||||
ref={trackRef}
|
||||
className="bg-primary/30 relative h-1 w-full grow overflow-hidden rounded-full"
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
className={cn('bg-primary absolute h-full', lockMode && 'cursor-grab')}
|
||||
onPointerDown={handleRangePointerDown}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
|
||||
@ -75,6 +75,10 @@ import IconTransferring from './Sources/IconTransferring';
|
||||
import Alert from './Sources/Alert';
|
||||
import AlertOutline from './Sources/AlertOutline';
|
||||
import Clipboard from './Sources/Clipboard';
|
||||
import OrientationSwitch from './Sources/OrientationSwitch';
|
||||
import LayerBackground from './Sources/LayerBackground';
|
||||
import LayerForeground from './Sources/LayerForeground';
|
||||
import LayerSegmentation from './Sources/LayerSegmentation';
|
||||
import {
|
||||
Tool3DRotate,
|
||||
ToolAngle,
|
||||
@ -378,6 +382,10 @@ export const Icons = {
|
||||
/>
|
||||
),
|
||||
// Icons
|
||||
LayerBackground,
|
||||
LayerForeground,
|
||||
LayerSegmentation,
|
||||
OrientationSwitch,
|
||||
Clipboard,
|
||||
ActionNewDialog,
|
||||
GroupLayers,
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerBackground = (props: IconProps) => (
|
||||
<svg
|
||||
width="29"
|
||||
height="28"
|
||||
viewBox="0 0 29 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20.0801 8.92383C22.013 8.92383 23.5799 10.4909 23.5801 12.4238V19.0801C23.5801 21.0131 22.0131 22.5801 20.0801 22.5801H13.4238C11.4908 22.5801 9.92383 21.0131 9.92383 19.0801V16.6562H14.6562C16.3131 16.6562 17.6562 15.3131 17.6562 13.6562V8.92383H20.0801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="5.5"
|
||||
y="4.5"
|
||||
width="11.6565"
|
||||
height="11.6565"
|
||||
rx="2.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerBackground;
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerForeground = (props: IconProps) => (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="10.3999"
|
||||
y="9.3999"
|
||||
width="12.6001"
|
||||
height="12.6001"
|
||||
rx="3"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<rect
|
||||
x="5.5"
|
||||
y="4.5"
|
||||
width="11.6001"
|
||||
height="11.6001"
|
||||
rx="2.5"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerForeground;
|
||||
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const LayerSegmentation = (props: IconProps) => (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 9.16547L9.16546 7.00002M7 12.4512L12.4999 6.95129M7 15.7369L15.7369 6.99999M7.18447 18.8381L18.8381 7.1845M8.77085 20.5375L20.4043 8.90397M11.794 20.8L20.8 11.794M15.0797 20.8L20.8 15.0797M18.4323 20.7333L20.7332 18.4324M10 20.8H17.8C19.4569 20.8 20.8 19.4569 20.8 17.8V10C20.8 8.34315 19.4569 7.00001 17.8 7.00001H10C8.34315 7.00001 7 8.34316 7 10V17.8C7 19.4569 8.34315 20.8 10 20.8Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LayerSegmentation;
|
||||
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const OrientationSwitch = (props: IconProps) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.07778 16.4182L9.96563 18.8288L7.30469 20.4557"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.0917 14C20.3502 14.1669 20.5669 14.3909 20.7252 14.6547C20.8835 14.9186 20.9792 15.2152 21.0048 15.5218C21.0048 17.3123 17.4909 18.7938 13.0151 18.946"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.96782 18.828C6.23931 18.4354 3.5 17.1038 3.5 15.5218C3.52464 15.2179 3.61841 14.9236 3.77416 14.6614C3.9299 14.3992 4.1435 14.1761 4.39865 14.0091"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.02539 14.1079C8.52393 14.1079 8.2002 13.8159 8.2002 13.3525C8.2002 13.2256 8.23193 13.0479 8.30811 12.8511L10.9678 5.62109C11.1899 5.01172 11.5581 4.73242 12.1548 4.73242C12.7578 4.73242 13.126 4.99902 13.3545 5.61475L16.0205 12.8511C16.0967 13.0605 16.1284 13.2065 16.1284 13.3525C16.1284 13.7969 15.7856 14.1079 15.3096 14.1079C14.8652 14.1079 14.6177 13.9048 14.4717 13.416L13.8687 11.6641H10.4473L9.84424 13.4033C9.69189 13.8984 9.45068 14.1079 9.02539 14.1079ZM10.8345 10.3691H13.4688L12.1675 6.45898H12.123L10.8345 10.3691Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default OrientationSwitch;
|
||||
@ -29,6 +29,7 @@ interface NumericMetaContextValue {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
lockMode?: boolean;
|
||||
}
|
||||
|
||||
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
||||
@ -47,6 +48,7 @@ interface NumericMetaContainerProps {
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
lockMode?: boolean; // Add lock mode prop for doubleRange
|
||||
}
|
||||
|
||||
function NumericMetaContainer({
|
||||
@ -60,6 +62,7 @@ function NumericMetaContainer({
|
||||
max = 100,
|
||||
step = 1,
|
||||
className,
|
||||
lockMode = false,
|
||||
children,
|
||||
}: PropsWithChildren<NumericMetaContainerProps>) {
|
||||
// Calculate default values based on min and max
|
||||
@ -115,6 +118,7 @@ function NumericMetaContainer({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
lockMode,
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex flex-col', className)}>{children}</div>
|
||||
@ -236,7 +240,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step, lockMode } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(values: [number, number]) => {
|
||||
@ -258,6 +262,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
defaultValue={doubleValue}
|
||||
onValueChange={handleSliderChange}
|
||||
showNumberInputs={showNumberInputs}
|
||||
lockMode={lockMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -16,6 +16,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
onSegmentEdit,
|
||||
onSegmentDelete,
|
||||
data,
|
||||
showSegmentIndex = true,
|
||||
} = useSegmentationTableContext('SegmentationSegments');
|
||||
|
||||
// Try to get segmentation data from expanded context first, then fall back to table context
|
||||
@ -76,7 +77,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
const DataRowComponent = (
|
||||
<DataRow
|
||||
key={segmentIndex}
|
||||
number={segmentIndex}
|
||||
number={showSegmentIndex ? segmentIndex : null}
|
||||
title={label}
|
||||
// details={displayText}
|
||||
description={displayText}
|
||||
|
||||
@ -62,6 +62,7 @@ export interface SegmentationTableContextType {
|
||||
activeSegmentation?: Segmentation;
|
||||
disableEditing: boolean;
|
||||
showAddSegment?: boolean;
|
||||
showSegmentIndex?: boolean;
|
||||
renderInactiveSegmentations?: boolean;
|
||||
|
||||
// Function handlers
|
||||
|
||||
@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input text-foreground ring-offset-background placeholder:text-muted-foreground focus:ring-ring [&>span]:line-clamp-1 hover:bg-primary/10 flex h-7 w-full items-center justify-between whitespace-nowrap rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'border-input text-foreground ring-offset-background placeholder:text-muted-foreground focus:ring-ring hover:bg-primary/10 flex h-7 w-full min-w-0 items-center justify-between rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -64,4 +64,4 @@ ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { ViewportActionCorners };
|
||||
export { ViewportActionCorners };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user