feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)

This commit is contained in:
Alireza 2025-03-17 22:34:34 -04:00 committed by GitHub
parent 694f3e9611
commit ec79b5523e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 3603 additions and 1563 deletions

402
bun.lock
View File

@ -33,7 +33,7 @@
},
"addOns/externals/devDependencies": {
"name": "@externals/devDependencies",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.12.0",
@ -121,20 +121,21 @@
},
"addOns/externals/dicom-microscopy-viewer": {
"name": "@externals/dicom-microscopy-viewer",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1",
},
},
"extensions/cornerstone": {
"name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/polymorphic-segmentation": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/labelmap-interpolation": "^3.7.0",
"@cornerstonejs/polymorphic-segmentation": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
@ -151,9 +152,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.5.0",
"@ohif/core": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@ -165,19 +166,19 @@
},
"extensions/cornerstone-dicom-pmap": {
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -188,16 +189,16 @@
},
"extensions/cornerstone-dicom-rt": {
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -208,19 +209,19 @@
},
"extensions/cornerstone-dicom-seg": {
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -231,19 +232,19 @@
},
"extensions/cornerstone-dicom-sr": {
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@ -253,19 +254,19 @@
},
"extensions/cornerstone-dynamic-volume": {
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@ -275,7 +276,7 @@
},
"extensions/default": {
"name": "@ohif/extension-default",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.1.0",
@ -283,8 +284,8 @@
"lodash.uniqby": "^4.7.0",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"dcmjs": "*",
"dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2",
@ -298,7 +299,7 @@
},
"extensions/dicom-microscopy": {
"name": "@ohif/extension-dicom-microscopy",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3",
@ -309,10 +310,10 @@
"mathjs": "^12.4.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -323,14 +324,14 @@
},
"extensions/dicom-pdf": {
"name": "@ohif/extension-dicom-pdf",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@ -340,14 +341,14 @@
},
"extensions/dicom-video": {
"name": "@ohif/extension-dicom-video",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@ -357,20 +358,20 @@
},
"extensions/measurement-tracking": {
"name": "@ohif/extension-measurement-tracking",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.125",
"@xstate/react": "^3.2.2",
"xstate": "^4.10.0",
},
"peerDependencies": {
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"classnames": "^2.3.2",
"dcmjs": "*",
"lodash.debounce": "^4.0.8",
@ -383,14 +384,14 @@
},
"extensions/test-extension": {
"name": "@ohif/extension-test",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "0.38.0",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@ -400,14 +401,14 @@
},
"extensions/tmtv": {
"name": "@ohif/extension-tmtv",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@ -417,7 +418,7 @@
},
"modes/basic-dev-mode": {
"name": "@ohif/mode-basic-dev-mode",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -427,17 +428,17 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
},
},
"modes/basic-test-mode": {
"name": "@ohif/mode-test",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -447,19 +448,19 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121",
"@ohif/extension-test": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.125",
"@ohif/extension-test": "3.10.0-beta.125",
},
},
"modes/longitudinal": {
"name": "@ohif/mode-longitudinal",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -469,32 +470,32 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.125",
},
},
"modes/microscopy": {
"name": "@ohif/mode-microscopy",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.125",
},
},
"modes/preclinical-4d": {
"name": "@ohif/mode-preclinical-4d",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
},
@ -503,17 +504,17 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-tmtv": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-tmtv": "3.10.0-beta.125",
},
},
"modes/segmentation": {
"name": "@ohif/mode-segmentation",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -545,19 +546,19 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
},
},
"modes/tmtv": {
"name": "@ohif/mode-tmtv",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@ -567,43 +568,43 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.125",
},
},
"platform/app": {
"name": "@ohif/app",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"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.5.0",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.121",
"@ohif/extension-cornerstone": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121",
"@ohif/extension-default": "3.10.0-beta.121",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.121",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121",
"@ohif/extension-dicom-video": "3.10.0-beta.121",
"@ohif/extension-test": "3.10.0-beta.121",
"@ohif/i18n": "3.10.0-beta.121",
"@ohif/mode-basic-dev-mode": "3.10.0-beta.121",
"@ohif/mode-longitudinal": "3.10.0-beta.121",
"@ohif/mode-microscopy": "3.10.0-beta.121",
"@ohif/mode-test": "3.10.0-beta.121",
"@ohif/ui": "3.10.0-beta.121",
"@ohif/ui-next": "3.10.0-beta.121",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-test": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.125",
"@ohif/mode-basic-dev-mode": "3.10.0-beta.125",
"@ohif/mode-longitudinal": "3.10.0-beta.125",
"@ohif/mode-microscopy": "3.10.0-beta.125",
"@ohif/mode-test": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"@ohif/ui-next": "3.10.0-beta.125",
"@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.3",
"classnames": "^2.3.2",
@ -644,7 +645,7 @@
},
"platform/cli": {
"name": "@ohif/cli",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"bin": {
"ohif-cli": "src/index.js",
},
@ -668,7 +669,7 @@
},
"platform/core": {
"name": "@ohif/core",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"dcmjs": "*",
@ -693,15 +694,15 @@
"@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.5.0",
"@ohif/ui": "3.10.0-beta.121",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/ui": "3.10.0-beta.125",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21",
},
},
"platform/i18n": {
"name": "@ohif/i18n",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next-locize-backend": "^2.0.0",
@ -726,7 +727,7 @@
},
"platform/ui": {
"name": "@ohif/ui",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28",
@ -786,13 +787,14 @@
},
"platform/ui-next": {
"name": "@ohif/ui-next",
"version": "3.10.0-beta.121",
"version": "3.10.0-beta.125",
"dependencies": {
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.4",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.7",
@ -1093,7 +1095,7 @@
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.5.0", "", { "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.5.0", "@cornerstonejs/tools": "^3.5.0" } }, "sha512-xkixGbCDS2wwAHmggx1mW0ewEjWGntbE5L1svdkmRpGBvTtEkQJbNkNG6dUiHXWZ9mICxwahyiMc7YVDdH3tfA=="],
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.7.0", "", { "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.7.0", "@cornerstonejs/tools": "^3.7.0" } }, "sha512-7ROFACNjT4W8dukUoKwggHhl+tjg0rdDpYDq8rM6b0c0gO2MgYKbU55t9MoT682yIuebt81jCsAbFSRGVzf45Q=="],
"@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="],
@ -1105,13 +1107,15 @@
"@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="],
"@cornerstonejs/core": ["@cornerstonejs/core@3.5.0", "", { "dependencies": { "@kitware/vtk.js": "32.12.0", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-Ai4dyxZameDz+cp3nBST6KfK1WK/Ic5R6DoTFrBlph4DuK7IUradAx2N9uEydhUJTuF6606mF3VeaAdPG9Hfig=="],
"@cornerstonejs/core": ["@cornerstonejs/core@3.7.0", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-eRCovUYO3JvFV3Dzuwc6EWn9bpm+fvp/x6SXvr3MwiO+LRw1o5bmZfeh0+m4+5xzvQJPbjNYiXPPEpLYGQIOqQ=="],
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.5.0", "", { "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", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "dicom-parser": "^1.8.9" } }, "sha512-Jel/an3IHe68sebtRLYB3/aJ1Dggw8D11cWBiBUqzXfxGTlg1SUd3KnvU5OrgNJ+ZpNUEdwCrR3OoiU65hL6UA=="],
"@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.7.0", "", { "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.7.0" } }, "sha512-MSv5A7ZcAp7uVm1ofrTaQppPm1DyOAXN0BdX8a2/fMZ2Ud4OmEtk0aoUlaJ52/hQtUvfdCqfYObBos2oBIMQ1w=="],
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.5.0", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "@cornerstonejs/tools": "^3.5.0", "@kitware/vtk.js": "32.12.0" } }, "sha512-S2zqXNKlFURPCq99fPj5CAnBO7chuVk7bIRWmBOBYQL21TnL4rzyvY+Td5pvHBal4sZ8uTs6PW72rrwGCospAQ=="],
"@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.7.0", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@cornerstonejs/tools": "^3.7.0", "@kitware/vtk.js": "32.12.1" } }, "sha512-/1l1XsKFYeov+hrMKindJ4n6l4H2zcXFwcO+wtqE4kmfoFhXRN6RSlIXbCRzEZtFCmh0hafg7SUUcXxr0QoClQ=="],
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.5.0", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "@kitware/vtk.js": "32.12.0", "@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-wyT7runGpPvY9dMJOUBAtxDBSqabk6Ks/SNjbTGXAIUemAYuonP+33pN4zK/4n/zFfa0gpKFP7LFc461xFK16g=="],
"@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.7.0", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@cornerstonejs/tools": "^3.7.0", "@kitware/vtk.js": "32.12.1" } }, "sha512-MYb8aXE2EwfsyPmhvhTmNdTkCq5pIlMJuVRmT6GuQ4nf2dIhx5AeZ+Ya/TRaNNumV2Fx9/9PetI8+Z8XJLAILw=="],
"@cornerstonejs/tools": ["@cornerstonejs/tools@3.7.0", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@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-72lbmaAm/dnOANAUNc0OK+9aL2pFklaWBLR+kOKn8OeSh1i9RRTSEaP8zWzStaSNAcdhwQ+JOQzcMR40Eo8oNQ=="],
"@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@1.1.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA=="],
@ -1539,7 +1543,7 @@
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw=="],
@ -1565,6 +1569,8 @@
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="],
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ=="],
"@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
@ -1575,7 +1581,7 @@
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="],
@ -1593,7 +1599,7 @@
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g=="],
@ -3555,7 +3561,7 @@
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
"itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="],
"itk-wasm": ["itk-wasm@1.0.0-b.165", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-Rq0+AL2BuRqcFh9r3h69pB3nt84tw/gTxnI6GjIOYw5Bfah9qO+C8hRJpD8aWNBa5o3wNHRAMVlp0pt/L4K8gg=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
@ -5381,6 +5387,14 @@
"@cornerstonejs/adapters/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="],
"@cornerstonejs/core/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="],
"@cornerstonejs/labelmap-interpolation/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="],
"@cornerstonejs/polymorphic-segmentation/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="],
"@cornerstonejs/tools/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="],
"@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@cypress/xvfb/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@ -5411,6 +5425,8 @@
"@itk-wasm/dam/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="],
"@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
@ -5527,6 +5543,38 @@
"@ohif/ui/postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-hover-card/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@rollup/plugin-node-resolve/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
@ -5985,8 +6033,6 @@
"istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"itk-wasm/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
"itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
@ -6629,6 +6675,14 @@
"@babel/register/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"@cornerstonejs/core/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="],
"@cornerstonejs/labelmap-interpolation/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="],
"@cornerstonejs/polymorphic-segmentation/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="],
"@cornerstonejs/tools/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
@ -6645,6 +6699,12 @@
"@itk-wasm/dam/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@ -6719,6 +6779,14 @@
"@ohif/ui/postcss-loader/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="],
"@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
"@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
"@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
"@storybook/builder-webpack5/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@storybook/builder-webpack5/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="],
@ -7051,8 +7119,6 @@
"ipfs-unixfs-exporter/p-queue/p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
"itk-wasm/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"jake/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@ -7465,6 +7531,10 @@
"@itk-wasm/dam/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"@lerna/child-process/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"@lerna/create/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@ -7729,6 +7799,8 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
"@ohif/ui/postcss-loader/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],

View File

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

View File

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

View File

@ -42,6 +42,13 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
};
}
if (!toolNames) {
return {
disabled: false,
// isActive: true,
};
}
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName) && !toolNames) {

View File

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

View File

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

View File

@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*",
@ -51,10 +51,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0",
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/polymorphic-segmentation": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/polymorphic-segmentation": "^3.7.0",
"@cornerstonejs/labelmap-interpolation": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",

View File

@ -4,6 +4,7 @@ import {
VolumeViewport,
utilities as csUtils,
Types as CoreTypes,
cache,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import {
@ -14,6 +15,7 @@ import {
annotation,
} from '@cornerstonejs/tools';
import * as cornerstoneTools from '@cornerstonejs/tools';
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
import { Types as OhifTypes, utils } from '@ohif/core';
import i18n from '@ohif/i18n';
@ -31,6 +33,8 @@ import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@ -39,7 +43,6 @@ const toggleSyncFunctions = {
function commandsModule({
servicesManager,
extensionManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
@ -54,7 +57,9 @@ function commandsModule({
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services;
segmentationService,
displaySetService,
} = servicesManager.services as AppTypes.Services;
const { measurementServiceSource } = this;
@ -67,7 +72,75 @@ function commandsModule({
return toolGroupService.getToolGroupForViewport(viewport.id);
}
function _getActiveSegmentationInfo() {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const segmentationId = activeSegmentation?.segmentationId;
const activeSegmentIndex = segmentationService.getActiveSegment(viewportId).segmentIndex;
return {
segmentationId,
segmentIndex: activeSegmentIndex,
};
}
const actions = {
runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => {
// Get active segmentation if not specified
const targetSegmentation =
segmentationId && segmentIndex
? { segmentationId, segmentIndex }
: _getActiveSegmentationInfo();
const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation;
// Get bidirectional measurement data
const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({
segmentationId: targetId,
segmentIndices: [targetIndex],
});
const activeViewportId = viewportGridService.getActiveViewportId();
// Process each bidirectional measurement
bidirectionalData.forEach(measurement => {
const { segmentIndex, majorAxis, minorAxis } = measurement;
// Create annotation
const annotation = cornerstoneTools.SegmentBidirectionalTool.hydrate(
activeViewportId,
[majorAxis, minorAxis],
{
segmentIndex,
segmentationId: targetId,
}
);
// Update segmentation stats
const updatedSegmentation = updateSegmentBidirectionalStats({
segmentationId: targetId,
segmentIndex: targetIndex,
bidirectionalData: measurement,
segmentationService,
annotation,
});
// Save changes if needed
if (updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId: targetId,
segments: updatedSegmentation.segments,
});
}
});
},
interpolateLabelmap: () => {
const { segmentationId, segmentIndex } = _getActiveSegmentationInfo();
labelmapInterpolation.interpolate({
segmentationId,
segmentIndex,
});
},
/**
* Generates the selector props for the context menu, specific to
* the cornerstone viewport, and then runs the context menu.
@ -373,6 +446,44 @@ function commandsModule({
utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter));
},
downloadCSVSegmentationReport: ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const cachedStats = segmentation.cachedStats;
const { representationData } = segmentation;
const { Labelmap } = representationData;
const { referencedImageIds } = Labelmap;
const firstImageId = referencedImageIds[0];
// find displaySet for firstImageId
const displaySet = displaySetService
.getActiveDisplaySets()
.find(ds => ds.imageIds?.some(i => i === firstImageId));
const {
SeriesNumber,
SeriesInstanceUID,
StudyInstanceUID,
SeriesDate,
SeriesTime,
SeriesDescription,
} = displaySet;
const additionalInfo = {
reference: {
SeriesNumber,
SeriesInstanceUID,
StudyInstanceUID,
SeriesDate,
SeriesTime,
SeriesDescription,
},
};
generateSegmentationCSVReport(segmentation, additionalInfo);
},
// Retrieve value commands
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
@ -1094,7 +1205,12 @@ function commandsModule({
* @param props.segmentIndex - The index of the segment to activate
*/
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
const { segmentationService, viewportGridService } = servicesManager.services;
// set both active segmentation and active segment
segmentationService.setActiveSegmentation(
viewportGridService.getActiveViewportId(),
segmentationId
);
segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
},
@ -1636,6 +1752,9 @@ function commandsModule({
},
undo: actions.undo,
redo: actions.redo,
interpolateLabelmap: actions.interpolateLabelmap,
runSegmentBidirectional: actions.runSegmentBidirectional,
downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport,
};
return {

View File

@ -0,0 +1,127 @@
import React from 'react';
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
Icons,
useSegmentationTableContext,
useSegmentationExpanded,
} from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
import { useSystem } from '@ohif/core/src';
/**
* Custom dropdown menu component for segmentation panel that uses context for data
*/
export const CustomDropdownMenuContent = () => {
const { commandsManager } = useSystem();
const { t } = useTranslation('SegmentationTable');
const {
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
exportOptions,
activeSegmentation,
activeSegmentationId,
} = useSegmentationTableContext('CustomDropdownMenu');
// Try to get segmentation data from expanded context first, fall back to table context
let segmentation;
let segmentationId;
let allowExport = false;
try {
// Try to get from expanded context
const context = useSegmentationExpanded();
segmentation = context.segmentation;
segmentationId = segmentation.segmentationId;
} catch (e) {
// If not in expanded context, fallback to active segmentation from table context
segmentation = activeSegmentation;
segmentationId = activeSegmentationId;
}
// Determine if export is allowed for this segmentation
if (exportOptions && segmentationId) {
const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId);
allowExport = exportOption?.isExportable || false;
}
if (!segmentation || !segmentationId) {
return null;
}
const actions = {
storeSegmentation: async segmentationId => {
commandsManager.run({
commandName: 'storeSegmentation',
commandOptions: { segmentationId },
context: 'CORNERSTONE',
});
},
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
downloadCSVSegmentationReport: segmentationId => {
commandsManager.run('downloadCSVSegmentationReport', { segmentationId });
},
};
return (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => onSegmentationRemoveFromViewport(segmentationId)}>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => actions.downloadCSVSegmentationReport(segmentationId)}>
{t('Download CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.storeSegmentation(segmentationId)}
disabled={!allowExport}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => actions.onSegmentationDownload(segmentationId)}
disabled={!allowExport}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
);
};

View File

@ -0,0 +1,138 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
import React from 'react';
import { Separator, Button, Tooltip, TooltipTrigger, TooltipContent, Icons } from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
import { roundNumber } from '@ohif/core/src/utils';
import { useSystem } from '@ohif/core/src';
interface CustomSegmentStatisticsHeaderProps {
segmentationId: string;
segmentIndex: number;
}
/**
* Custom header component for segment statistics
*/
export const CustomSegmentStatisticsHeader = ({
segmentationId,
segmentIndex,
}: CustomSegmentStatisticsHeaderProps) => {
const { servicesManager, commandsManager } = useSystem();
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('SegmentationTable');
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const cachedStats = segment.cachedStats;
const namedStats = cachedStats.namedStats;
if (!namedStats) {
return null;
}
const bidirectional = namedStats.bidirectional;
if (!bidirectional) {
return (
<div className="-mt-2 space-y-2">
<div className="flex">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-primary flex items-center gap-2"
onClick={() => {
commandsManager.run('runSegmentBidirectional', {
segmentationId,
segmentIndex,
});
}}
>
<Icons.ToolBidirectionalSegment className="h-5 w-5" />
<span>{t('Compute Largest Bidirectional')}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Add bidirectional measurement')}</TooltipContent>
</Tooltip>
</div>
<Separator className="bg-input" />
</div>
);
}
const { value, unit } = bidirectional;
const maxMajor = value.maxMajor;
const maxMinor = value.maxMinor;
const max = Math.max(maxMajor, maxMinor);
const min = Math.min(maxMajor, maxMinor);
const isVisible = cornerstoneTools.annotation.visibility.isAnnotationVisible(
bidirectional.annotationUID
);
return (
<div className="-mt-2 space-y-2">
<div className="flex items-center justify-between">
<div className="text-foreground">
<div>
L: {roundNumber(max)} {unit}
</div>
<div>
W: {roundNumber(min)} {unit}
</div>
</div>
<div className="flex gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity`}
onClick={e => {
e.stopPropagation();
cornerstoneTools.annotation.visibility.setAnnotationVisibility(
bidirectional.annotationUID,
!isVisible
);
segmentationService.addOrUpdateSegmentation({
segmentationId,
segmentIndex,
});
}}
>
{isVisible ? (
<Icons.Hide className="h-6 w-6" />
) : (
<Icons.Show className="h-6 w-6" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Toggle visibility')}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => {
if (bidirectional.annotationUID) {
commandsManager.run('jumpToMeasurement', {
uid: bidirectional.annotationUID,
});
}
}}
>
<Icons.JumpToSlice />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Jump to measurement')}</TooltipContent>
</Tooltip>
</div>
</div>
<Separator className="bg-input" />
</div>
);
};

View File

@ -1,77 +1,10 @@
import React from 'react';
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
Icons,
} from '@ohif/ui-next';
import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
return {
'panelSegmentation.customDropdownMenuContent': ({
activeSegmentation,
onSegmentationAdd,
onSegmentationRemoveFromViewport,
onSegmentationEdit,
onSegmentationDelete,
allowExport,
storeSegmentation,
onSegmentationDownload,
onSegmentationDownloadRTSS,
t,
}) => (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)}
>
<Icons.Series className="text-foreground" />
<span className="pl-2">{t('Remove from Viewport')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.segmentationId)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={!allowExport}
className="pl-1"
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Export & Download')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => storeSegmentation(activeSegmentation.segmentationId)}
>
{t('Export DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onSegmentationDownload(activeSegmentation.segmentationId)}
>
{t('Download DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.segmentationId)}>
<Icons.Delete className="text-red-600" />
<span className="pl-2 text-red-600">{t('Delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
),
'panelSegmentation.customDropdownMenuContent': CustomDropdownMenuContent,
'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader,
'panelSegmentation.disableEditing': false,
'panelSegmentation.showAddSegment': true,
'panelSegmentation.onSegmentationAdd': () => {
@ -81,15 +14,21 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
},
'panelSegmentation.tableMode': 'collapsed',
'panelSegmentation.readableText': {
lesionStats: 'Lesion Statistics',
minValue: 'Minimum Value',
maxValue: 'Maximum Value',
meanValue: 'Mean Value',
volume: 'Volume (ml)',
suvPeak: 'SUV Peak',
suvMax: 'Maximum SUV',
suvMaxIJK: 'SUV Max IJK',
lesionGlyoclysisStats: 'Lesion Glycolysis',
// the values will appear in this order
min: 'Min Value',
minLPS: 'Min Coord',
max: 'Max Value',
maxLPS: 'Max Coord',
mean: 'Mean Value',
stdDev: 'Standard Deviation',
count: 'Voxel Count',
median: 'Median',
skewness: 'Skewness',
kurtosis: 'Kurtosis',
peakValue: 'Peak Value',
peakLPS: 'Peak Coord',
volume: 'Volume',
lesionGlycolysis: 'Lesion Glycolysis',
},
};
}

View File

@ -190,16 +190,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
},
},
{
name: 'evaluate.mpr',
name: 'evaluate.displaySetIsReconstructable',
evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
if (!viewport) {
return;
}
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const areReconstructable = displaySets.every(displaySet => {
@ -210,11 +210,8 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return getDisabledState(disabledText);
}
const isMpr = protocol?.id === 'mpr';
return {
disabled: false,
className: utils.getToggledClassName(isMpr),
};
},
},

View File

@ -39,6 +39,7 @@ import { useLutPresentationStore } from './stores/useLutPresentationStore';
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities';
import { updateSegmentationStats } from './utils/updateSegmentationStats';
const { registerColormap } = csUtilities.colormap;
@ -90,6 +91,7 @@ export default async function init({
cornerstoneViewportService,
hangingProtocolService,
viewportGridService,
segmentationService,
} = servicesManager.services;
window.services = servicesManager.services;
@ -150,8 +152,6 @@ export default async function init({
);
});
// ... existing code ...
// add metadata providers
metaData.addProvider(
csUtilities.calibratedPixelSpacingMetadataProvider.get.bind(
@ -177,6 +177,45 @@ export default async function init({
initCineService(servicesManager);
initStudyPrefetcherService(servicesManager);
segmentationService.subscribeDebounced(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const readableText = customizationService.getCustomization('panelSegmentation.readableText');
const updatedSegmentation = await updateSegmentationStats({
segmentation,
segmentationId,
readableText,
});
if (updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId,
segments: updatedSegmentation.segments,
});
}
// Check for segments with bidirectional measurements and update them
if (segmentation) {
const segmentIndices = Object.keys(segmentation.segments)
.map(index => parseInt(index))
.filter(index => index > 0);
for (const segmentIndex of segmentIndices) {
const segment = segmentation.segments[segmentIndex];
if (segment?.cachedStats?.namedStats?.bidirectional) {
// Run the command to update the bidirectional measurement
commandsManager.runCommand('runSegmentBidirectional', {
segmentationId,
segmentIndex,
});
}
}
}
},
1000
);
// When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
@ -278,35 +317,35 @@ export default async function init({
function initializeWebWorkerProgressHandler(uiNotificationService) {
const activeToasts = new Map();
eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
const { progress, type, id } = detail;
// eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
// const { progress, type, id } = detail;
const cacheKey = `${type}-${id}`;
if (progress === 0 && !activeToasts.has(cacheKey)) {
const progressPromise = new Promise((resolve, reject) => {
activeToasts.set(cacheKey, { resolve, reject });
});
// const cacheKey = `${type}-${id}`;
// if (progress === 0 && !activeToasts.has(cacheKey)) {
// const progressPromise = new Promise((resolve, reject) => {
// activeToasts.set(cacheKey, { resolve, reject });
// });
uiNotificationService.show({
id: cacheKey,
title: `${type}`,
message: `${type}: ${progress}%`,
autoClose: false,
promise: progressPromise,
promiseMessages: {
loading: `Computing...`,
success: `Completed successfully`,
error: 'Web Worker failed',
},
});
} else {
if (progress === 100) {
const { resolve } = activeToasts.get(cacheKey);
resolve({ progress, type });
activeToasts.delete(cacheKey);
}
}
});
// uiNotificationService.show({
// id: cacheKey,
// title: `${type}`,
// message: `${type}: ${progress}%`,
// autoClose: false,
// promise: progressPromise,
// promiseMessages: {
// loading: `Computing...`,
// success: `Completed successfully`,
// error: 'Web Worker failed',
// },
// });
// } else {
// if (progress === 100) {
// const { resolve } = activeToasts.get(cacheKey);
// resolve({ progress, type });
// activeToasts.delete(cacheKey);
// }
// }
// });
}
/**

View File

@ -1,6 +1,7 @@
import {
PanTool,
WindowLevelTool,
SegmentBidirectionalTool,
StackScrollTool,
VolumeRotateTool,
ZoomTool,
@ -52,8 +53,14 @@ export default function initCornerstoneTools(configuration = {}) {
addons: {
polySeg,
},
computeWorker: {
autoTerminateOnIdle: {
enabled: false,
},
},
});
addTool(PanTool);
addTool(SegmentBidirectionalTool);
addTool(WindowLevelTool);
addTool(StackScrollTool);
addTool(VolumeRotateTool);
@ -142,6 +149,7 @@ const toolNames = {
OrientationMarker: OrientationMarkerTool.toolName,
WindowLevelRegion: WindowLevelRegionTool.toolName,
PlanarFreehandContourSegmentation: PlanarFreehandContourSegmentationTool.toolName,
SegmentBidirectional: SegmentBidirectionalTool.toolName,
};
export { toolNames };

View File

@ -34,6 +34,7 @@ const initMeasurementService = (
LivewireContour,
Probe,
UltrasoundDirectional,
SegmentBidirectional,
} = measurementServiceMappingsFactory(
measurementService,
displaySetService,
@ -172,6 +173,14 @@ const initMeasurementService = (
UltrasoundDirectional.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SegmentBidirectional',
SegmentBidirectional.matchingCriteria,
SegmentBidirectional.toAnnotation,
SegmentBidirectional.toMeasurement
);
return csTools3DVer1MeasurementSource;
};

View File

@ -15,217 +15,191 @@ export default function PanelSegmentation({
servicesManager,
});
const handlers = {
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
},
onSegmentEdit: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
},
onSegmentationEdit: segmentationId => {
commandsManager.run('editSegmentationLabel', { segmentationId });
},
onSegmentColorClick: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
},
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
onToggleSegmentLock: (segmentationId, segmentIndex) => {
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
},
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
storeSegmentation: async segmentationId => {
commandsManager.run({
commandName: 'storeSegmentation',
commandOptions: { segmentationId },
context: 'CORNERSTONE',
});
},
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
setStyle: (segmentationId, type, key, value) => {
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
},
toggleRenderInactiveSegmentations: () => {
commandsManager.run('toggleRenderInactiveSegmentations');
},
onSegmentationRemoveFromViewport: segmentationId => {
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
},
onSegmentationDelete: segmentationId => {
commandsManager.run('deleteSegmentation', { segmentationId });
},
setFillAlpha: ({ type }, value) => {
commandsManager.run('setFillAlpha', { type, value });
},
setOutlineWidth: ({ type }, value) => {
commandsManager.run('setOutlineWidth', { type, value });
},
setRenderFill: ({ type }, value) => {
commandsManager.run('setRenderFill', { type, value });
},
setRenderOutline: ({ type }, value) => {
commandsManager.run('setRenderOutline', { type, value });
},
setFillAlphaInactive: ({ type }, value) => {
commandsManager.run('setFillAlphaInactive', { type, value });
},
getRenderInactiveSegmentations: () => {
return commandsManager.run('getRenderInactiveSegmentations');
},
};
// Extract customization options
const segmentationTableMode = customizationService.getCustomization(
'panelSegmentation.tableMode'
);
// custom onSegmentationAdd if provided
) as unknown as string;
const onSegmentationAdd = customizationService.getCustomization(
'panelSegmentation.onSegmentationAdd'
);
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
const CustomDropdownMenuContent = customizationService.getCustomization(
'panelSegmentation.customDropdownMenuContent'
);
const CustomSegmentStatisticsHeader = customizationService.getCustomization(
'panelSegmentation.customSegmentStatisticsHeader'
);
// Create handlers object for all command runs
const handlers = {
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
},
onSegmentEdit: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
},
onSegmentationEdit: segmentationId => {
commandsManager.run('editSegmentationLabel', { segmentationId });
},
onSegmentColorClick: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
},
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
onToggleSegmentLock: (segmentationId, segmentIndex) => {
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
},
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
setStyle: (segmentationId, type, key, value) => {
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
},
toggleRenderInactiveSegmentations: () => {
commandsManager.run('toggleRenderInactiveSegmentations');
},
onSegmentationRemoveFromViewport: segmentationId => {
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
},
onSegmentationDelete: segmentationId => {
commandsManager.run('deleteSegmentation', { segmentationId });
},
setFillAlpha: ({ type }, value) => {
commandsManager.run('setFillAlpha', { type, value });
},
setOutlineWidth: ({ type }, value) => {
commandsManager.run('setOutlineWidth', { type, value });
},
setRenderFill: ({ type }, value) => {
commandsManager.run('setRenderFill', { type, value });
},
setRenderOutline: ({ type }, value) => {
commandsManager.run('setRenderOutline', { type, value });
},
setFillAlphaInactive: ({ type }, value) => {
commandsManager.run('setFillAlphaInactive', { type, value });
},
getRenderInactiveSegmentations: () => {
return commandsManager.run('getRenderInactiveSegmentations');
},
};
// Generate export options
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
const { representationData, segmentationId } = segmentation;
const { Labelmap } = representationData;
if (!Labelmap) {
return {
segmentationId,
isExportable: true,
};
return { segmentationId, isExportable: true };
}
const referencedImageIds = Labelmap.referencedImageIds;
const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId);
if (!instance) {
return {
segmentationId,
isExportable: false,
};
return { segmentationId, isExportable: false };
}
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
const SeriesInstanceUID = instance.SeriesInstanceUID;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
const isExportable = displaySet?.isReconstructable;
return {
segmentationId,
isExportable,
isExportable: displaySet?.isReconstructable,
};
});
return (
<>
<SegmentationTable
disabled={disabled}
data={segmentationsWithRepresentations}
mode={segmentationTableMode}
title="Segmentations"
exportOptions={exportOptions}
disableEditing={disableEditing}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={handlers.onSegmentationClick}
onSegmentationDelete={handlers.onSegmentationDelete}
showAddSegment={showAddSegment}
onSegmentAdd={handlers.onSegmentAdd}
onSegmentClick={handlers.onSegmentClick}
onSegmentEdit={handlers.onSegmentEdit}
onSegmentationEdit={handlers.onSegmentationEdit}
onSegmentColorClick={handlers.onSegmentColorClick}
onSegmentDelete={handlers.onSegmentDelete}
onToggleSegmentVisibility={handlers.onToggleSegmentVisibility}
onToggleSegmentLock={handlers.onToggleSegmentLock}
onToggleSegmentationRepresentationVisibility={
handlers.onToggleSegmentationRepresentationVisibility
}
onSegmentationDownload={handlers.onSegmentationDownload}
storeSegmentation={handlers.storeSegmentation}
onSegmentationDownloadRTSS={handlers.onSegmentationDownloadRTSS}
setStyle={handlers.setStyle}
toggleRenderInactiveSegmentations={handlers.toggleRenderInactiveSegmentations}
onSegmentationRemoveFromViewport={handlers.onSegmentationRemoveFromViewport}
setFillAlpha={handlers.setFillAlpha}
setOutlineWidth={handlers.setOutlineWidth}
setRenderFill={handlers.setRenderFill}
setRenderOutline={handlers.setRenderOutline}
setFillAlphaInactive={handlers.setFillAlphaInactive}
renderInactiveSegmentations={handlers.getRenderInactiveSegmentations()}
>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
// Common props for SegmentationTable
const tableProps = {
disabled,
data: segmentationsWithRepresentations,
mode: segmentationTableMode,
title: 'Segmentations',
exportOptions,
disableEditing,
onSegmentationAdd,
showAddSegment,
renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(),
...handlers,
};
{segmentationTableMode === 'collapsed' ? (
<SegmentationTable.Collapsed>
<SegmentationTable.SelectorHeader>
const renderSegments = () => {
return (
<SegmentationTable.Segments>
<SegmentationTable.SegmentStatistics.Header>
<CustomSegmentStatisticsHeader />
</SegmentationTable.SegmentStatistics.Header>
<SegmentationTable.SegmentStatistics.Body />
</SegmentationTable.Segments>
);
};
// Render content based on mode
const renderModeContent = () => {
if (tableProps.mode === 'collapsed') {
return (
<SegmentationTable.Collapsed>
<SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.DropdownMenu>
<CustomDropdownMenuContent />
</SegmentationTable.SelectorHeader>
</SegmentationTable.Collapsed.DropdownMenu>
<SegmentationTable.Collapsed.Selector />
<SegmentationTable.Collapsed.Info />
</SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.Content>
<SegmentationTable.AddSegmentRow />
<SegmentationTable.Segments />
</SegmentationTable.Collapsed>
) : (
<SegmentationTable.Expanded>
<SegmentationTable.Header>
{renderSegments()}
</SegmentationTable.Collapsed.Content>
</SegmentationTable.Collapsed>
);
}
return (
<>
<SegmentationTable.Expanded>
<SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.DropdownMenu>
<CustomDropdownMenuContent />
</SegmentationTable.Header>
{/* <SegmentationTable.AddSegmentRow /> */}
<SegmentationTable.Segments />
</SegmentationTable.Expanded>
)}
</SegmentationTable>
</>
</SegmentationTable.Expanded.DropdownMenu>
<SegmentationTable.Expanded.Label />
<SegmentationTable.Expanded.Info />
</SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.Content>
<SegmentationTable.AddSegmentRow />
{renderSegments()}
</SegmentationTable.Expanded.Content>
</SegmentationTable.Expanded>
</>
);
};
return (
<SegmentationTable {...tableProps}>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{renderModeContent()}
</SegmentationTable>
);
}

View File

@ -92,6 +92,7 @@ class SegmentationService extends PubSubService {
};
private _segmentationIdToColorLUTIndexMap: Map<string, number>;
private _segmentationGroupStatsMap: Map<string, any>;
readonly servicesManager: AppTypes.ServicesManager;
highlightIntervalId = null;
readonly EVENTS = EVENTS;
@ -102,6 +103,8 @@ class SegmentationService extends PubSubService {
this._segmentationIdToColorLUTIndexMap = new Map();
this.servicesManager = servicesManager;
this._segmentationGroupStatsMap = new Map();
}
public onModeEnter(): void {
@ -1058,6 +1061,33 @@ class SegmentationService extends PubSubService {
public getRenderInactiveSegmentations(viewportId: string): boolean {
return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId);
}
/**
* Sets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @param stats - Statistics object containing metrics for the segmentation group
*
* @remarks
* This method stores statistical data for a group of related segmentations.
* The stats are stored using a composite key created from the sorted and joined
*/
public setSegmentationGroupStats(segmentationIds: string[], stats: any) {
const groupId = this.getGroupId(segmentationIds);
this._segmentationGroupStatsMap.set(groupId, stats);
}
/**
* Gets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @returns The stored statistics object for the segmentation group if found, undefined otherwise
*/
public getSegmentationGroupStats(segmentationIds: string[]) {
const groupId = this.getGroupId(segmentationIds);
return this._segmentationGroupStatsMap.get(groupId);
}
private getGroupId(segmentationIds: string[]) {
return segmentationIds.sort().join(',');
}
/**
* Toggles the visibility of a segmentation in the state, and broadcasts the event.

View File

@ -0,0 +1,139 @@
export function generateSegmentationCSVReport(
segmentationData,
info: {
reference: {
SeriesNumber: string;
SeriesInstanceUID: string;
StudyInstanceUID: string;
SeriesDate: string;
SeriesTime: string;
SeriesDescription: string;
};
}
) {
// Initialize the rows for our CSV
const csvRows = [];
// Add segmentation-level information
csvRows.push(['Segmentation ID', segmentationData.segmentationId || '']);
csvRows.push(['Segmentation Label', segmentationData.label || '']);
csvRows.push([]);
const additionalInfo = info.reference;
// Add reference information
const referenceKeys = [
['Series Number', additionalInfo.SeriesNumber],
['Series Instance UID', additionalInfo.SeriesInstanceUID],
['Study Instance UID', additionalInfo.StudyInstanceUID],
['Series Date', additionalInfo.SeriesDate],
['Series Time', additionalInfo.SeriesTime],
['Series Description', additionalInfo.SeriesDescription],
];
referenceKeys.forEach(([key, value]) => {
if (value) {
csvRows.push([`reference ${key}`, value]);
}
});
// Add a blank row for separation
csvRows.push([]);
csvRows.push(['Segments Statistics']);
// Add segment information in columns
if (segmentationData.segments) {
// First row: Segment headers
const segmentHeaderRow = ['Label'];
for (const segmentId in segmentationData.segments) {
const segment = segmentationData.segments[segmentId];
segmentHeaderRow.push(`${segment.label || ''}`);
}
csvRows.push(segmentHeaderRow);
// Add segment properties
csvRows.push([
'Segment Index',
...Object.values(segmentationData.segments).map(s => s.segmentIndex || ''),
]);
csvRows.push([
'Locked',
...Object.values(segmentationData.segments).map(s => (s.locked ? 'Yes' : 'No')),
]);
csvRows.push([
'Active',
...Object.values(segmentationData.segments).map(s => (s.active ? 'Yes' : 'No')),
]);
// Add segment statistics
// First, collect all unique statistics across all segments
const allStats = new Set();
for (const segment of Object.values(segmentationData.segments)) {
if (segment.cachedStats && segment.cachedStats.namedStats) {
for (const statKey in segment.cachedStats.namedStats) {
const stat = segment.cachedStats.namedStats[statKey];
const statLabel = stat.label || stat.name;
const statUnit = stat.unit ? ` (${stat.unit})` : '';
allStats.add(`${statLabel}${statUnit}`);
}
}
}
// Then create a row for each statistic
for (const statName of allStats) {
const statRow = [statName];
for (const segment of Object.values(segmentationData.segments)) {
let statValue = '';
if (segment.cachedStats && segment.cachedStats.namedStats) {
for (const statKey in segment.cachedStats.namedStats) {
const stat = segment.cachedStats.namedStats[statKey];
const currentStatName = `${stat.label || stat.name}${stat.unit ? ` (${stat.unit})` : ''}`;
if (currentStatName === statName) {
statValue = stat.value !== undefined ? stat.value : '';
break;
}
}
}
statRow.push(statValue);
}
csvRows.push(statRow);
}
}
// Convert to CSV string
let csvString = '';
for (const row of csvRows) {
const formattedRow = row.map(cell => {
// Handle values that need to be quoted (contain commas, quotes, or newlines)
const cellValue = cell !== undefined && cell !== null ? cell.toString() : '';
if (cellValue.includes(',') || cellValue.includes('"') || cellValue.includes('\n')) {
// Escape quotes and wrap in quotes
return '"' + cellValue.replace(/"/g, '""') + '"';
}
return cellValue;
});
csvString += formattedRow.join(',') + '\n';
}
// Create a download link and trigger the download
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute(
'download',
`${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv`
);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

View File

@ -0,0 +1,185 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getDisplayUnit } from './utils';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
const SegmentBidirectional = {
toAnnotation: measurement => {},
toMeasurement: (
csToolsEventDetail,
displaySetService,
cornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
debugger;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.debug('SegmentBidirectional tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(
referencedImageId,
displaySetService,
annotation
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
}
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return [];
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes(
referencedImageId,
displaySetService,
annotation
);
const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
const { SeriesNumber } = displaySet;
const { length, width, unit } = targetStats;
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
length,
width,
});
});
return annotations;
}
function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:SegmentBidirectional');
mappedAnnotations.forEach(annotation => {
const { length, width, unit } = annotation;
columns.push(`Length`, `Width`, 'Unit');
values.push(length, width, unit);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
// Area is the same for all series
const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const roundedLength = utils.roundNumber(length, 2);
const roundedWidth = utils.roundNumber(width, 2);
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`);
displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
export default SegmentBidirectional;

View File

@ -13,6 +13,7 @@ const supportedTools = [
'LivewireContour',
'UltrasoundDirectionalTool',
'SCOORD3DPoint',
'SegmentBidirectional',
];
export default supportedTools;

View File

@ -12,6 +12,7 @@ import SplineROI from './SplineROI';
import LivewireContour from './LivewireContour';
import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional';
import SegmentBidirectional from './SegmentBidirectional';
const measurementServiceMappingsFactory = (
measurementService: MeasurementService,
@ -48,6 +49,7 @@ const measurementServiceMappingsFactory = (
LivewireContour: POLYLINE,
Probe: POINT,
UltrasoundDirectional: POLYLINE,
SegmentBidirectional: BIDIRECTIONAL,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
@ -94,6 +96,27 @@ const measurementServiceMappingsFactory = (
},
],
},
SegmentBidirectional: {
toAnnotation: SegmentBidirectional.toAnnotation,
toMeasurement: csToolsAnnotation =>
SegmentBidirectional.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType,
customizationService
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation =>

View File

@ -0,0 +1,185 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
interface BidirectionalAxis {
length: number;
// Add other axis properties as needed
}
interface BidirectionalData {
majorAxis: BidirectionalAxis;
minorAxis: BidirectionalAxis;
}
/**
* Updates the statistics for a segmentation by calculating stats for each segment
* and storing them in the segment's cachedStats property
*
* @param segmentation - The segmentation object containing segments to update stats for
* @param segmentationId - The ID of the segmentation
* @returns The updated segmentation object with new stats, or null if no updates were made
*/
export async function updateSegmentationStats({
segmentation,
segmentationId,
readableText,
}: {
segmentation: any;
segmentationId: string;
readableText: any;
}): Promise<any | null> {
if (!segmentation) {
console.debug('No segmentation found for id:', segmentationId);
return null;
}
const segmentIndices = Object.keys(segmentation.segments)
.map(index => parseInt(index))
.filter(index => index > 0); // Filter out segment 0 which is typically background
if (segmentIndices.length === 0) {
console.debug('No segments found in segmentation:', segmentationId);
return null;
}
const stats = await cornerstoneTools.utilities.segmentation.getStatistics({
segmentationId,
segmentIndices,
mode: 'individual',
});
if (!stats) {
return null;
}
const updatedSegmentation = { ...segmentation };
let hasUpdates = false;
// Loop through each segment's stats
Object.entries(stats).forEach(([segmentIndex, segmentStats]) => {
const index = parseInt(segmentIndex);
if (!updatedSegmentation.segments[index].cachedStats) {
updatedSegmentation.segments[index].cachedStats = {};
hasUpdates = true;
}
// Get existing namedStats or initialize if not present
const namedStats = updatedSegmentation.segments[index].cachedStats.namedStats || {};
if (segmentStats.array) {
segmentStats.array.forEach(stat => {
// only gather stats that are in the readableText
if (!readableText[stat.name]) {
return;
}
if (stat && stat.name) {
namedStats[stat.name] = {
name: stat.name,
label: readableText[stat.name],
value: stat.value,
unit: stat.unit,
order: Object.keys(readableText).indexOf(stat.name),
};
}
});
if (readableText.volume) {
// Add volume if it exists but isn't in the array
if (segmentStats.volume && !namedStats.volume) {
namedStats.volume = {
name: 'volume',
label: 'Volume',
value: segmentStats.volume.value,
unit: segmentStats.volume.unit,
order: Object.keys(readableText).indexOf('volume'),
};
}
}
// Update the segment's cachedStats with namedStats
updatedSegmentation.segments[index].cachedStats.namedStats = namedStats;
hasUpdates = true;
}
});
return hasUpdates ? updatedSegmentation : null;
}
/**
* Updates a segment's statistics with bidirectional measurement data
*
* @param segmentationId - The ID of the segmentation
* @param segmentIndex - The index of the segment to update
* @param bidirectionalData - The bidirectional measurement data to add
* @param segmentationService - The segmentation service to use for updating the segment
* @returns Whether the update was successful
*/
export function updateSegmentBidirectionalStats({
segmentationId,
segmentIndex,
bidirectionalData,
segmentationService,
annotation,
}: {
segmentationId: string;
segmentIndex: number;
bidirectionalData: BidirectionalData;
segmentationService: AppTypes.SegmentationService;
annotation: any;
}) {
if (!segmentationId || segmentIndex === undefined || !bidirectionalData) {
console.debug('Missing required data for bidirectional stats update');
return null;
}
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation || !segmentation.segments[segmentIndex]) {
console.debug('Segment not found:', segmentIndex, 'in segmentation:', segmentationId);
return null;
}
const updatedSegmentation = { ...segmentation };
const segment = updatedSegmentation.segments[segmentIndex];
if (!segment.cachedStats) {
segment.cachedStats = { namedStats: {} };
}
if (!segment.cachedStats.namedStats) {
segment.cachedStats.namedStats = {};
}
const { majorAxis, minorAxis, maxMajor, maxMinor } = bidirectionalData;
if (!majorAxis || !minorAxis) {
console.debug('Missing major or minor axis data');
return null;
}
let hasUpdates = false;
const namedStats = segment.cachedStats.namedStats;
// Only calculate and update if we have valid measurements
if (maxMajor > 0 && maxMinor > 0) {
namedStats.bidirectional = {
name: 'bidirectional',
label: 'Bidirectional',
annotationUID: annotation.annotationUID,
value: {
maxMajor,
maxMinor,
majorAxis,
minorAxis,
},
unit: 'mm',
};
hasUpdates = true;
}
if (hasUpdates) {
return updatedSegmentation;
}
return null;
}

View File

@ -1,5 +1,4 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
//
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
import getImageSrcFromImageId from './getImageSrcFromImageId';

View File

@ -26,7 +26,7 @@ export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props })
{...item}
key={item.id}
size="small"
className={props.disabled && 'text-primary'}
className={item.disabled && 'text-foreground/70'}
onInteraction={event => {
onInteraction?.({
event,
@ -49,7 +49,7 @@ export function ToolBoxButtonWrapper({ onInteraction, options, ...props }) {
{...props}
id={props.id}
size="small"
className={classNames(props.disabled && 'text-primary')}
className={classNames(props.disabled && 'text-foreground/70')}
onInteraction={event => {
onInteraction?.({
event,

View File

@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^3.5.0",
"@cornerstonejs/tools": "^3.5.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.125",

View File

@ -1,5 +1,4 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
//
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
import getImageSrcFromImageId from './getImageSrcFromImageId';

View File

@ -11,11 +11,9 @@ export default function PanelRoiThresholdSegmentation({
const { segmentationsWithRepresentations: segmentationsInfo } =
useActiveViewportSegmentationRepresentations({ servicesManager });
useEffect(() => {
const segmentationIds = segmentationsInfo.map(
segmentationInfo => segmentationInfo.segmentation.segmentationId
);
const segmentationIds = segmentationsInfo?.map(info => info.segmentation.segmentationId) || [];
useEffect(() => {
const initialRun = async () => {
for (const segmentationId of segmentationIds) {
await handleROIThresholding({
@ -54,10 +52,9 @@ export default function PanelRoiThresholdSegmentation({
}, [commandsManager, segmentationService]);
// Find the first segmentation with a TMTV value since all of them have the same value
const tmtvSegmentation = segmentationsInfo.find(
info => info.segmentation.cachedStats?.tmtv !== undefined
);
const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv;
const stats = segmentationService.getSegmentationGroupStats(segmentationIds);
const tmtvValue = stats?.tmtv;
const tlgValue = stats?.tlg;
return (
<div className="mt-2 mb-10 flex flex-col">
@ -68,6 +65,7 @@ export default function PanelRoiThresholdSegmentation({
{'TMTV:'}
</span>
<div className="text-white">{`${tmtvValue?.toFixed(3)} mL`}</div>
<div className="text-white">{`${tlgValue?.toFixed(3)} mL`}</div>
</div>
) : null}
</div>

View File

@ -24,20 +24,7 @@ const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
const workerManager = getWebWorkerManager();
const options = {
maxWorkerInstances: 1,
autoTerminateOnIdle: {
enabled: true,
idleTimeThreshold: 3000,
},
};
// Register the task
const workerFn = () => {
return new Worker(new URL('./utils/calculateSUVPeakWorker.js', import.meta.url), {
name: 'suv-peak-worker', // name used by the browser to name the worker
});
};
function getVolumesFromSegmentation(segmentationId) {
const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId);
@ -262,153 +249,22 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
{ overwrite: true, segmentIndex, segmentationId }
);
},
calculateSuvPeak: async ({ segmentationId, segmentIndex }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const { representationData } = segmentation;
const { volumeId, referencedVolumeId } = representationData[
SegmentationRepresentations.Labelmap
] as csTools.Types.LabelmapToolOperationDataVolume;
const labelmap = cs.cache.getVolume(volumeId);
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
// if we put it in the top, it will appear in other modes
workerManager.registerWorker('suv-peak-worker', workerFn, options);
const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
const annotations = annotationUIDs.map(annotationUID =>
csTools.annotation.state.getAnnotation(annotationUID)
);
const labelmapProps = {
dimensions: labelmap.dimensions,
origin: labelmap.origin,
direction: labelmap.direction,
spacing: labelmap.spacing,
metadata: labelmap.metadata,
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
};
const referenceVolumeProps = {
dimensions: referencedVolume.dimensions,
origin: referencedVolume.origin,
direction: referencedVolume.direction,
spacing: referencedVolume.spacing,
metadata: referencedVolume.metadata,
scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(),
};
// metadata in annotations has enabledElement which is not serializable
// we need to remove it
// Todo: we should probably have a sanitization function for this
const annotationsToSend = annotations.map(annotation => {
return {
...annotation,
metadata: {
...annotation.metadata,
enabledElement: {
...annotation.metadata.enabledElement,
viewport: null,
renderingEngine: null,
element: null,
},
},
};
});
const suvPeak =
(await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', {
labelmapProps,
referenceVolumeProps,
annotations: annotationsToSend,
segmentIndex,
})) || {};
return {
suvPeak: suvPeak.mean,
suvMax: suvPeak.max,
suvMaxIJK: suvPeak.maxIJK,
suvMaxLPS: suvPeak.maxLPS,
};
},
getLesionStats: ({ segmentationId, segmentIndex = 1 }) => {
const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId);
const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume;
const { voxelManager: refVoxelManager } = referencedVolume;
let segmentationMax = -Infinity;
let segmentationMin = Infinity;
const segmentationValues = [];
let voxelCount = 0;
const callback = ({ value, index }) => {
if (value === segmentIndex) {
const refValue = refVoxelManager.getAtIndex(index) as number;
segmentationValues.push(refValue);
if (refValue > segmentationMax) {
segmentationMax = refValue;
}
if (refValue < segmentationMin) {
segmentationMin = refValue;
}
voxelCount++;
}
};
segVoxelManager.forEach(callback, { imageData });
const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount;
const stats = {
minValue: segmentationMin,
maxValue: segmentationMax,
meanValue: mean,
stdValue: Math.sqrt(
segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) /
voxelCount
),
volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3,
};
return stats;
},
calculateLesionGlycolysis: ({ lesionStats }) => {
const { meanValue, volume } = lesionStats;
return {
lesionGlyoclysisStats: volume * meanValue,
};
},
calculateTMTV: async ({ segmentations }) => {
const labelmapProps = segmentations.map(segmentation => {
const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
return {
dimensions: labelmap.dimensions,
spacing: labelmap.spacing,
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
origin: labelmap.origin,
direction: labelmap.direction,
};
const segmentationIds = segmentations.map(segmentation => segmentation.segmentationId);
const stats = await csTools.utilities.segmentation.computeMetabolicStats({
segmentationIds,
segmentIndex: 1,
});
if (!labelmapProps.length) {
return;
}
const tmtv = await workerManager.executeTask(
'suv-peak-worker',
'calculateTMTV',
labelmapProps
);
return tmtv;
segmentationService.setSegmentationGroupStats(segmentationIds, stats);
return stats;
},
exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
segmentations,
});
const tlg = await actions.getTotalLesionGlycolysis({ segmentations });
const additionalReportRows = [
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
{ key: 'Threshold Configuration', value: { ...config } },
@ -423,35 +279,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
},
getTotalLesionGlycolysis: async ({ segmentations }) => {
const labelmapProps = segmentations.map(segmentation => {
const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
return {
dimensions: labelmap.dimensions,
spacing: labelmap.spacing,
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
origin: labelmap.origin,
direction: labelmap.direction,
};
});
const { referencedVolume: ptVolume } = getVolumesFromSegmentation(
segmentations[0].segmentationId
);
const ptVolumeProps = {
dimensions: ptVolume.dimensions,
spacing: ptVolume.spacing,
scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(),
origin: ptVolume.origin,
direction: ptVolume.direction,
};
return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', {
labelmapProps,
referenceVolumeProps: ptVolumeProps,
});
},
setStartSliceForROIThresholdTool: () => {
const { viewport } = _getActiveViewportsEnabledElement();
const { focalPoint } = viewport.getCamera();
@ -651,12 +479,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
getTotalLesionGlycolysis: {
commandFn: actions.getTotalLesionGlycolysis,
},
calculateSuvPeak: {
commandFn: actions.calculateSuvPeak,
},
getLesionStats: {
commandFn: actions.getLesionStats,
},
calculateTMTV: {
commandFn: actions.calculateTMTV,
},

View File

@ -1,209 +0,0 @@
import { utilities } from '@cornerstonejs/core';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import { vec3 } from 'gl-matrix';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import { expose } from 'comlink';
const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setOrigin(origin);
imageData.setDirection(direction);
imageData.setSpacing(spacing);
const scalarArray = vtkDataArray.newInstance({
name: 'Pixels',
numberOfComponents: 1,
values: scalarData,
});
imageData.getPointData().setScalars(scalarArray);
imageData.modified();
const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({
scalarData,
dimensions,
numberOfComponents: 1,
});
return {
imageData,
spacing,
origin,
direction,
metadata,
voxelManager,
};
};
/**
* This method calculates the SUV peak on a segmented ROI from a reference PET
* volume. If a rectangle annotation is provided, the peak is calculated within that
* rectangle. Otherwise, the calculation is performed on the entire volume which
* will be slower but same result.
* @param viewport Viewport to use for the calculation
* @param labelmap Labelmap from which the mask is taken
* @param referenceVolume PET volume to use for SUV calculation
* @param toolData [Optional] list of toolData to use for SUV calculation
* @param segmentIndex The index of the segment to use for masking
* @returns
*/
function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) {
const labelmapInfo = createVolume(labelmapProps);
const referenceInfo = createVolume(referenceVolumeProps);
if (referenceInfo.metadata.Modality !== 'PT') {
return;
}
const { dimensions, imageData: labelmapImageData } = labelmapInfo;
const { imageData: referenceVolumeImageData } = referenceInfo;
let boundsIJK;
// Todo: using the first annotation for now
if (annotations?.length && annotations[0].data?.cachedStats) {
const { projectionPoints } = annotations[0].data.cachedStats;
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
const rectangleCornersIJK = pointsToUse.map(world => {
const ijk = vec3.fromValues(0, 0, 0);
referenceVolumeImageData.worldToIndex(world, ijk);
return ijk;
});
boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
}
let max = 0;
let maxIJK = [0, 0, 0];
let maxLPS = [0, 0, 0];
const callback = ({ pointIJK, pointLPS }) => {
const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK);
if (value !== segmentIndex) {
return;
}
const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK);
if (referenceValue > max) {
max = referenceValue;
maxIJK = pointIJK;
maxLPS = pointLPS;
}
};
labelmapInfo.voxelManager.forEach(callback, {
boundsIJK,
imageData: labelmapImageData,
isInObject: () => true,
returnPoints: true,
});
const direction = labelmapImageData.getDirection().slice(0, 3);
/**
* 2. Find the bottom and top of the great circle for the second sphere (1cc sphere)
* V = (4/3)πr3
*/
const radius = Math.pow(1 / ((4 / 3) * Math.PI), 1 / 3) * 10;
const diameter = radius * 2;
const secondaryCircleWorld = vec3.create();
const bottomWorld = vec3.create();
const topWorld = vec3.create();
referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld);
vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2);
vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2);
const suvPeakCirclePoints = [bottomWorld, topWorld];
/**
* 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous
* sphere
*/
let count = 0;
let acc = 0;
const suvPeakMeanCallback = ({ value }) => {
acc += value;
count += 1;
};
cstUtils.pointInSurroundingSphereCallback(
referenceVolumeImageData,
suvPeakCirclePoints,
suvPeakMeanCallback
);
const mean = acc / count;
return {
max,
maxIJK,
maxLPS,
mean,
};
}
function calculateTMTV(labelmapProps, segmentIndex = 1) {
const labelmaps = labelmapProps.map(props => createVolume(props));
const mergedLabelmap =
labelmaps.length === 1
? labelmaps[0]
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
const { imageData, spacing } = mergedLabelmap;
const values = imageData.getPointData().getScalars().getData();
// count non-zero values inside the outputData, this would
// consider the overlapping regions to be only counted once
const numVoxels = values.reduce((acc, curr) => {
if (curr > 0) {
return acc + 1;
}
return acc;
}, 0);
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
}
function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) {
const labelmaps = labelmapProps.map(props => createVolume(props));
const mergedLabelmap =
labelmaps.length === 1
? labelmaps[0]
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
// grabbing the first labelmap referenceVolume since it will be the same for all
const { spacing } = labelmaps[0];
const ptVolume = createVolume(referenceVolumeProps);
let suv = 0;
let totalLesionVoxelCount = 0;
const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength();
for (let i = 0; i < scalarDataLength; i++) {
// if not background
if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) {
suv += ptVolume.voxelManager.getAtIndex(i);
totalLesionVoxelCount += 1;
}
}
// Average SUV for the merged labelmap
const averageSuv = suv / totalLesionVoxelCount;
// total Lesion Glycolysis [suv * ml]
return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
}
const obj = {
calculateSuvPeak,
calculateTMTV,
getTotalLesionGlycolysis,
};
expose(obj);

View File

@ -1,42 +0,0 @@
import { Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
/**
* Given a list of labelmaps (with the possibility of overlapping regions),
* and a referenceVolume, it calculates the total metabolic tumor volume (TMTV)
* by flattening and rasterizing each segment into a single labelmap and summing
* the total number of volume voxels. It should be noted that for this calculation
* we do not double count voxels that are part of multiple labelmaps.
* @param {} labelmaps
* @param {number} segmentIndex
* @returns {number} TMTV in ml
*/
function calculateTMTV(labelmaps: Array<Types.IImageVolume>, segmentIndex = 1): number {
const volumeId = 'mergedLabelmap';
const mergedLabelmap = utilities.segmentation.createMergedLabelmapForIndex(
labelmaps,
segmentIndex,
volumeId
);
const { imageData, spacing, voxelManager } = mergedLabelmap;
// count non-zero values inside the outputData, this would
// consider the overlapping regions to be only counted once
let numVoxels = 0;
const callback = ({ value }) => {
if (value > 0) {
numVoxels += 1;
}
};
voxelManager.forEach(callback, {
imageData,
isInObject: () => true,
});
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
}
export default calculateTMTV;

View File

@ -1,72 +1,12 @@
import { Segment, Segmentation } from '@cornerstonejs/tools/types';
import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core';
export const handleROIThresholding = async ({
segmentationId,
commandsManager,
segmentationService,
}: withAppTypes<{
segmentationId: string;
}>) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
progress: 0,
type: 'Calculate Lesion Stats',
id: segmentationId,
});
// re-calculating the cached stats for the active segmentation
const updatedPerSegmentCachedStats = {};
for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) {
if (!segment) {
continue;
}
const numericSegmentIndex = Number(segmentIndex);
const lesionStats = await commandsManager.run('getLesionStats', {
segmentationId,
segmentIndex: numericSegmentIndex,
});
const suvPeak = await commandsManager.run('calculateSuvPeak', {
segmentationId,
segmentIndex: numericSegmentIndex,
});
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
// update segDetails with the suv peak for the active segmentation
const cachedStats = {
lesionStats,
suvPeak,
lesionGlyoclysisStats,
};
const updatedSegment: Segment = {
...segment,
cachedStats: {
...segment.cachedStats,
...cachedStats,
},
};
updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats;
segmentation.segments[segmentIndex] = updatedSegment;
}
// all available segmentations
const segmentations = segmentationService.getSegmentations();
const tmtv = await commandsManager.run('calculateTMTV', { segmentations });
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
progress: 100,
type: 'Calculate Lesion Stats',
id: segmentationId,
});
// add the tmtv to all the segment cachedStats, although it is a global
// value but we don't have any other way to display it for now
// Update all segmentations with the calculated TMTV
@ -76,22 +16,6 @@ export const handleROIThresholding = async ({
tmtv,
};
// Update each segment within the segmentation
Object.keys(segmentation.segments).forEach(segmentIndex => {
segmentation.segments[segmentIndex].cachedStats = {
...segmentation.segments[segmentIndex].cachedStats,
tmtv,
};
});
// Update the segmentation object
const updatedSegmentation: Segmentation = {
...segmentation,
segments: {
...segmentation.segments,
},
};
segmentationService.addOrUpdateSegmentation(updatedSegmentation);
segmentationService.addOrUpdateSegmentation(segmentation);
});
};

View File

@ -214,7 +214,7 @@ const toolbarButtons: Button[] = [
protocolId: 'mpr',
},
},
evaluate: 'evaluate.mpr',
evaluate: 'evaluate.displaySetIsReconstructable',
},
},
{

View File

@ -12,6 +12,7 @@ const ohif = {
const cornerstone = {
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools',
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
};
const segmentation = {
@ -19,6 +20,10 @@ const segmentation = {
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};
const dicomRT = {
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
};
/**
* Just two dependencies to be able to render a viewport with panels in order
* to make sure that the mode is working.
@ -27,6 +32,7 @@ const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
};
function modeFactory({ modeConfiguration }) {
@ -81,7 +87,14 @@ function modeFactory({ modeConfiguration }) {
'TagBrowser',
]);
toolbarService.createButtonSection('segmentationToolbox', ['SegmentationTools']);
toolbarService.createButtonSection('segmentationToolbox', [
'SegmentationUtilities',
'SegmentationTools',
]);
toolbarService.createButtonSection('segmentationToolboxUtilitySection', [
'InterpolateLabelmap',
'SegmentBidirectional',
]);
toolbarService.createButtonSection('segmentationToolboxToolsSection', [
'BrushTools',
'Shapes',
@ -149,7 +162,7 @@ function modeFactory({ modeConfiguration }) {
props: {
leftPanels: [ohif.leftPanel],
leftPanelResizable: true,
rightPanels: [cornerstone.panelTool],
rightPanels: [cornerstone.panelTool, cornerstone.measurements],
rightPanelResizable: true,
// leftPanelClosed: true,
viewports: [
@ -161,6 +174,10 @@ function modeFactory({ modeConfiguration }) {
namespace: segmentation.viewport,
displaySetsToDisplay: [segmentation.sopClassHandler],
},
{
namespace: dicomRT.viewport,
displaySetsToDisplay: [dicomRT.sopClassHandler],
},
],
},
};
@ -174,7 +191,7 @@ function modeFactory({ modeConfiguration }) {
// The example is used for a grid layout to specify that as a preferred layout
// hangingProtocol: ['@ohif/mnGrid'],
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler],
sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler],
};
}

View File

@ -76,6 +76,9 @@ function createTools(utilityModule) {
},
},
},
{
toolName: toolNames.SegmentBidirectional,
},
{
toolName: 'ThresholdSphereBrushDynamic',
parentTool: 'Brush',

View File

@ -255,7 +255,7 @@ const toolbarButtons: Button[] = [
{
id: 'Brush',
uiType: 'ohif.toolButton',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'icon-tool-brush',
label: 'Brush',
@ -292,9 +292,39 @@ const toolbarButtons: Button[] = [
],
},
},
{
id: 'InterpolateLabelmap',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'icon-tool-interpolation',
label: 'Interpolate Labelmap',
evaluate: [
'evaluate.cornerstone.segmentation',
{
name: 'evaluate.displaySetIsReconstructable',
disabledText: 'The current viewport cannot handle interpolation.',
},
],
commands: 'interpolateLabelmap',
},
},
{
id: 'SegmentBidirectional',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'icon-tool-bidirectional-segment',
label: 'Segment Bidirectional',
tooltip: 'Finding the largest bidirectional measurement per lesion',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
disabledText: 'Create new segmentation to enable this tool.',
},
commands: 'runSegmentBidirectional',
},
},
{
id: 'Eraser',
uiType: 'ohif.toolButton',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'icon-tool-eraser',
label: 'Eraser',
@ -332,7 +362,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Threshold',
uiType: 'ohif.toolButton',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'icon-tool-threshold',
label: 'Threshold Tool',

View File

@ -54,7 +54,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.5.0",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125",

View File

@ -37,7 +37,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.5.0",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/ui": "3.10.0-beta.125",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"

View File

@ -585,7 +585,11 @@ export default class ToolbarService extends PubSubService {
});
props.evaluate = args => {
const results = evaluators.map(evaluator => evaluator(args));
const results = evaluators.map(evaluator => evaluator(args)).filter(Boolean);
// had at least one disabled button, so we need to disable the button
const hasDisabledButton = results?.some(result => result.disabled);
const mergedResult = results.reduce((acc, result) => {
return {
...acc,
@ -593,6 +597,10 @@ export default class ToolbarService extends PubSubService {
};
}, {});
if (hasDisabledButton) {
mergedResult.disabled = true;
}
return mergedResult;
};

View File

@ -1,4 +1,5 @@
import guid from '../../utils/guid';
import debounce from '../../utils/debounce';
/**
* Consumer must implement:
@ -92,14 +93,14 @@ function _broadcastEvent(eventName, callbackProps) {
/** Export a PubSubService class to be used instead of the individual items */
export class PubSubService {
EVENTS: any;
subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any };
_broadcastEvent: (eventName: string, callbackProps: any) => void;
EVENTS: Record<string, string>;
subscribe: (eventName: string, callback: (data: unknown) => void) => { unsubscribe: () => void };
_broadcastEvent: (eventName: string, callbackProps: unknown) => void;
_unsubscribe: (eventName: string, listenerId: string) => void;
_isValidEvent: (eventName: string) => boolean;
listeners: {};
unsubscriptions: any[];
constructor(EVENTS) {
listeners: Record<string, Array<{ id: string; callback: (data: unknown) => void }> | undefined>;
unsubscriptions: Array<() => void>;
constructor(EVENTS: Record<string, string>) {
this.EVENTS = EVENTS;
this.subscribe = subscribe;
this._broadcastEvent = _broadcastEvent;
@ -109,6 +110,27 @@ export class PubSubService {
this.unsubscriptions = [];
}
/**
* Subscribe to updates with debouncing to limit callback execution frequency
* @param eventName - The name of the event
* @param callback - Events callback
* @param wait - Debounce wait time in milliseconds
* @param immediate - If true, trigger on the leading edge instead of trailing
*/
subscribeDebounced(
eventName: string,
callback: (data: unknown) => void,
wait = 300,
immediate = false
) {
if (this._isValidEvent(eventName)) {
const debouncedCallback = debounce(callback, wait, immediate);
return this.subscribe(eventName, debouncedCallback);
} else {
throw new Error(`Event ${eventName} not supported.`);
}
}
reset() {
this.unsubscriptions.forEach(unsub => unsub());
this.unsubscriptions = [];
@ -120,7 +142,9 @@ export class PubSubService {
* Check eventData.isConsumed to see if it is consumed or not.
* @param props - to include in the event
*/
protected createConsumableEvent(props) {
protected createConsumableEvent<T extends Record<string, unknown>>(
props: T
): T & { isConsumed: boolean; consume: () => void } {
return {
...props,
isConsumed: false,

View File

@ -0,0 +1,83 @@
---
title: Segmentation Table
---
# SegmentationTable Migration Guide
This guide will help you migrate your code to use the refactored SegmentationTable component.
## Key Changes
* **Context System Refactoring**: The context system has been completely redesigned with dedicated providers for different aspects of the segmentation UI
* **Compound Component Pattern**: Components now follow a more structured compound component pattern with clearer parent-child relationships
* **Header Components Changed**: The `SegmentationTable.SelectorHeader` has been replaced with specialized header components
* **New Features**: Added segment statistics, hover cards, and better customization options
* **Better Customization**: Added support for custom dropdown menus and segment statistics headers
## Migration Steps
### 1. Migrate from SelectorHeader to New Header Components
The `SegmentationTable.SelectorHeader` has been removed. Use the new Collapsed pattern instead:
```diff
- <SegmentationTable.SelectorHeader>
- <CustomDropdownMenuContent />
- </SegmentationTable.SelectorHeader>
+ <SegmentationTable.Collapsed>
+ <SegmentationTable.Collapsed.Header>
+ <SegmentationTable.Collapsed.DropdownMenu>
+ <CustomDropdownMenuContent />
+ </SegmentationTable.Collapsed.DropdownMenu>
+ <SegmentationTable.Collapsed.Selector />
+ <SegmentationTable.Collapsed.Info />
+ </SegmentationTable.Collapsed.Header>
+ <SegmentationTable.Collapsed.Content>
+ {/* Content here */}
+ </SegmentationTable.Collapsed.Content>
+ </SegmentationTable.Collapsed>
```
### 2. Update Component Hierarchy for Expanded View
The expanded view structure has also changed:
```diff
- <SegmentationTable.Expanded>
- <SegmentationTable.Header>
- <CustomDropdownMenuContent />
- </SegmentationTable.Header>
- <SegmentationTable.Segments />
- </SegmentationTable.Expanded>
+ <SegmentationTable.Expanded>
+ <SegmentationTable.Expanded.Header>
+ <SegmentationTable.Expanded.DropdownMenu>
+ <CustomDropdownMenuContent />
+ </SegmentationTable.Expanded.DropdownMenu>
+ <SegmentationTable.Expanded.Label />
+ <SegmentationTable.Expanded.Info />
+ </SegmentationTable.Expanded.Header>
+ <SegmentationTable.Expanded.Content>
+ <SegmentationTable.AddSegmentRow />
+ <SegmentationTable.Segments />
+ </SegmentationTable.Expanded.Content>
+ </SegmentationTable.Expanded>
```
### 3. Using the New Segment Statistics Component
The new `SegmentStatistics` component provides a way to display segment statistics:
```diff
+ <SegmentationTable.Segments>
+ <SegmentationTable.SegmentStatistics.Header>
+ <CustomSegmentStatisticsHeader />
+ </SegmentationTable.SegmentStatistics.Header>
+ <SegmentationTable.SegmentStatistics.Body />
+ </SegmentationTable.Segments>
```

View File

@ -37,6 +37,11 @@ import { Input } from '../../../../ui-next/src/components/Input';
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
import {
HoverCard,
HoverCardTrigger,
HoverCardContent,
} from '../../../../ui-next/src/components/HoverCard';
interface DataItem {
id: number;
@ -44,8 +49,29 @@ interface DataItem {
description: string;
optionalField?: string;
colorHex?: string;
details?: string;
details?: { primary: string[]; secondary: string[] };
series?: string;
statistics?: {
centroidX: string;
centroidY: string;
centroidZ: string;
frameDuration: string;
kurtosis: string;
max: string;
maxSlice: string;
mean: string;
median: string;
min: string;
regions: string;
skewness: string;
sphereDiameter: string;
standardDeviation: string;
suvPeak: string;
total: string;
glycolysis: string;
volume: string;
voxelCount: string;
};
}
interface ListGroup {
@ -67,13 +93,189 @@ export default function SegmentationPanel() {
};
const organSegmentationGroup = dataList.find(
(listGroup: ListGroup) => listGroup.type === 'Organ Segmentation'
);
(listGroup: any) => listGroup.type === 'Organ Segmentation'
) as unknown as ListGroup;
if (!organSegmentationGroup) {
return <div className="text-red-500">Organ Segmentation data not found.</div>;
}
// Create a state to track which item's statistics to show
// Function to render statistics panel
const renderStatisticsPanel = (item: DataItem) => {
if (!item.statistics) {
return null;
}
const stats = item.statistics;
return (
<div className="w-full">
<div className="mb-4 flex items-center space-x-2">
<div
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: item.colorHex }}
></div>
<h3 className="text-muted-foreground break-words text-lg font-semibold">{item.title}</h3>
</div>
<div className="space-y-1">
<div className="flex justify-between">
<div className="">Centroid X</div>
<div>
<span className="text-white">{stats.centroidX.value}</span>{' '}
<span className="">{stats.centroidX.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Centroid Y</div>
<div>
<span className="text-white">{stats.centroidY.value}</span>{' '}
<span className="">{stats.centroidY.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Centroid Z</div>
<div>
<span className="text-white">{stats.centroidZ.value}</span>{' '}
<span className="">{stats.centroidZ.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Frame Duration</div>
<div>
<span className="text-white">{stats.frameDuration.value}</span>{' '}
<span className="">{stats.frameDuration.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Kurtosis</div>
<div>
<span className="text-white">{stats.kurtosis.value}</span>{' '}
<span className="">{stats.kurtosis.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Max</div>
<div>
<span className="text-white">{stats.max.value}</span>{' '}
<span className="">{stats.max.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Max Slice</div>
<div>
<span className="text-white">{stats.maxSlice.value}</span>{' '}
<span className="">{stats.maxSlice.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Mean</div>
<div>
<span className="text-white">{stats.mean.value}</span>{' '}
<span className="">{stats.mean.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Median</div>
<div>
<span className="text-white">{stats.median.value}</span>{' '}
<span className="">{stats.median.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Min</div>
<div>
<span className="text-white">{stats.min.value}</span>{' '}
<span className="">{stats.min.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Regions</div>
<div>
<span className="text-white">{stats.regions.value}</span>{' '}
<span className="">{stats.regions.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Skewness</div>
<div>
<span className="text-white">{stats.skewness.value}</span>{' '}
<span className="">{stats.skewness.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Sphere Diameter</div>
<div>
<span className="text-white">{stats.sphereDiameter.value}</span>{' '}
<span className="">{stats.sphereDiameter.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Standard Deviation</div>
<div>
<span className="text-white">{stats.standardDeviation.value}</span>{' '}
<span className="">{stats.standardDeviation.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">SUV Peak</div>
<div>
<span className="text-white">{stats.suvPeak.value}</span>{' '}
<span className="">{stats.suvPeak.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Total</div>
<div>
<span className="text-white">{stats.total.value}</span>{' '}
<span className="">{stats.total.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Glycolysis</div>
<div>
<span className="text-white">{stats.glycolysis.value}</span>{' '}
<span className="">{stats.glycolysis.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Volume</div>
<div>
<span className="text-white">{stats.volume.value}</span>{' '}
<span className="">{stats.volume.unit}</span>
</div>
</div>
<div className="flex justify-between">
<div className="">Voxel Count</div>
<div>
<span className="text-white">{stats.voxelCount.value}</span>{' '}
<span className="">{stats.voxelCount.unit}</span>
</div>
</div>
</div>
</div>
);
};
return (
<div className="px-auto flex min-h-screen w-full justify-center bg-black py-12">
<div className="w-64 space-y-0">
@ -284,20 +486,46 @@ export default function SegmentationPanel() {
{organSegmentationGroup.items.map((item, index) => {
const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID
return (
<DataRow
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
number={index + 1}
title={item.title}
description={item.description}
optionalField={item.optionalField}
colorHex={item.colorHex}
details={item.details}
series={item.series}
actionOptions={actionOptionsMap[organSegmentationGroup.type] || ['Action']}
onAction={(action: string) => handleAction(compositeId, action)}
isSelected={selectedRowId === compositeId}
onSelect={() => handleRowSelect(compositeId)}
/>
<HoverCard
key={`hover-${compositeId}`}
openDelay={300}
closeDelay={200}
// open={true}
>
<HoverCardTrigger asChild>
<div>
<DataRow
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
number={index + 1}
title={item.title}
description={item.description}
colorHex={item.colorHex}
details={item.details || { primary: [], secondary: [] }}
actionOptions={
actionOptionsMap[organSegmentationGroup.type] || ['Action']
}
onAction={(action: string) => handleAction(compositeId, action)}
isSelected={selectedRowId === compositeId}
onSelect={() => handleRowSelect(compositeId)}
isVisible={true}
isLocked={false}
onToggleVisibility={() => console.debug('Toggle visibility')}
onToggleLocked={() => console.debug('Toggle locked')}
onRename={() => console.debug('Rename')}
onDelete={() => console.debug('Delete')}
onColor={() => console.debug('Color')}
disableEditing={false}
/>
</div>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="w-72 border"
>
{renderStatisticsPanel(item)}
</HoverCardContent>
</HoverCard>
);
})}
</div>

View File

@ -118,84 +118,378 @@ const dataList = [
title: 'Spleen',
description: 'Description for Spleen.',
colorHex: '#6B8E23',
statistics: {
centroidX: { value: -3.45, unit: 'cm' },
centroidY: { value: 4.78, unit: 'cm' },
centroidZ: { value: 118.32, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.22, unit: '' },
max: { value: 14.92, unit: 'SUV' },
maxSlice: { value: 78, unit: '' },
mean: { value: 7.14, unit: 'SUV' },
median: { value: 6.85, unit: 'SUV' },
min: { value: 3.8, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.76, unit: '' },
sphereDiameter: { value: 7.12, unit: 'cm' },
standardDeviation: { value: 2.31, unit: 'SUV' },
suvPeak: { value: 12.95, unit: 'SUV' },
total: { value: 18245.63, unit: 'SUV' },
glycolysis: { value: 1245.78, unit: 'SUV ml' },
volume: { value: 178.34, unit: 'ml' },
voxelCount: { value: 2834, unit: '' },
},
},
{
id: 16,
title: 'Kidney',
description: 'Description for Kidney.',
colorHex: '#4682B4',
statistics: {
centroidX: { value: 2.18, unit: 'cm' },
centroidY: { value: 6.32, unit: 'cm' },
centroidZ: { value: 125.47, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.18, unit: '' },
max: { value: 16.23, unit: 'SUV' },
maxSlice: { value: 92, unit: '' },
mean: { value: 8.05, unit: 'SUV' },
median: { value: 7.92, unit: 'SUV' },
min: { value: 4.2, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.91, unit: '' },
sphereDiameter: { value: 8.45, unit: 'cm' },
standardDeviation: { value: 2.67, unit: 'SUV' },
suvPeak: { value: 14.32, unit: 'SUV' },
total: { value: 22567.89, unit: 'SUV' },
glycolysis: { value: 1532.45, unit: 'SUV ml' },
volume: { value: 215.67, unit: 'ml' },
voxelCount: { value: 3245, unit: '' },
},
},
{
id: 17,
title: 'Kidney very long title name lorem ipsum dolor sit amet segmentation',
description: 'Description for Kidney.',
colorHex: '#9ACD32',
statistics: {
centroidX: { value: -2.34, unit: 'cm' },
centroidY: { value: 5.87, unit: 'cm' },
centroidZ: { value: 124.65, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.25, unit: '' },
max: { value: 15.45, unit: 'SUV' },
maxSlice: { value: 88, unit: '' },
mean: { value: 7.82, unit: 'SUV' },
median: { value: 7.45, unit: 'SUV' },
min: { value: 4.1, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.85, unit: '' },
sphereDiameter: { value: 7.95, unit: 'cm' },
standardDeviation: { value: 2.54, unit: 'SUV' },
suvPeak: { value: 13.78, unit: 'SUV' },
total: { value: 21345.67, unit: 'SUV' },
glycolysis: { value: 1423.56, unit: 'SUV ml' },
volume: { value: 198.45, unit: 'ml' },
voxelCount: { value: 3012, unit: '' },
},
},
{
id: 18,
title: 'Gallbladder',
description: 'Description for Gallbladder.',
colorHex: '#20B2AA',
statistics: {
centroidX: { value: -1.87, unit: 'cm' },
centroidY: { value: 4.23, unit: 'cm' },
centroidZ: { value: 119.78, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.14, unit: '' },
max: { value: 12.67, unit: 'SUV' },
maxSlice: { value: 72, unit: '' },
mean: { value: 6.34, unit: 'SUV' },
median: { value: 6.12, unit: 'SUV' },
min: { value: 3.5, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.72, unit: '' },
sphereDiameter: { value: 5.23, unit: 'cm' },
standardDeviation: { value: 2.12, unit: 'SUV' },
suvPeak: { value: 11.45, unit: 'SUV' },
total: { value: 9876.54, unit: 'SUV' },
glycolysis: { value: 876.32, unit: 'SUV ml' },
volume: { value: 98.45, unit: 'ml' },
voxelCount: { value: 1567, unit: '' },
},
},
{
id: 19,
title: 'Esophagus',
description: 'Description for Esophagus.',
colorHex: '#DAA520',
statistics: {
centroidX: { value: 0.45, unit: 'cm' },
centroidY: { value: 7.65, unit: 'cm' },
centroidZ: { value: 132.45, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.12, unit: '' },
max: { value: 11.34, unit: 'SUV' },
maxSlice: { value: 65, unit: '' },
mean: { value: 5.87, unit: 'SUV' },
median: { value: 5.65, unit: 'SUV' },
min: { value: 3.2, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.68, unit: '' },
sphereDiameter: { value: 4.56, unit: 'cm' },
standardDeviation: { value: 1.98, unit: 'SUV' },
suvPeak: { value: 10.23, unit: 'SUV' },
total: { value: 7654.32, unit: 'SUV' },
glycolysis: { value: 765.43, unit: 'SUV ml' },
volume: { value: 87.65, unit: 'ml' },
voxelCount: { value: 1345, unit: '' },
},
},
{
id: 20,
title: 'Liver',
description: 'Description for Liver.',
colorHex: '#CD5C5C',
statistics: {
centroidX: { value: -1.23, unit: 'cm' },
centroidY: { value: 5.43, unit: 'cm' },
centroidZ: { value: 121.34, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.19, unit: '' },
max: { value: 17.56, unit: 'SUV' },
maxSlice: { value: 95, unit: '' },
mean: { value: 8.76, unit: 'SUV' },
median: { value: 8.45, unit: 'SUV' },
min: { value: 4.5, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.95, unit: '' },
sphereDiameter: { value: 9.87, unit: 'cm' },
standardDeviation: { value: 2.87, unit: 'SUV' },
suvPeak: { value: 15.67, unit: 'SUV' },
total: { value: 32456.78, unit: 'SUV' },
glycolysis: { value: 2345.67, unit: 'SUV ml' },
volume: { value: 345.67, unit: 'ml' },
voxelCount: { value: 5678, unit: '' },
},
},
{
id: 21,
title: 'Stomach',
description: 'Description for Stomach.',
colorHex: '#778899',
statistics: {
centroidX: { value: -0.87, unit: 'cm' },
centroidY: { value: 4.98, unit: 'cm' },
centroidZ: { value: 120.56, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.15, unit: '' },
max: { value: 13.45, unit: 'SUV' },
maxSlice: { value: 82, unit: '' },
mean: { value: 6.78, unit: 'SUV' },
median: { value: 6.54, unit: 'SUV' },
min: { value: 3.7, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.79, unit: '' },
sphereDiameter: { value: 6.78, unit: 'cm' },
standardDeviation: { value: 2.23, unit: 'SUV' },
suvPeak: { value: 12.34, unit: 'SUV' },
total: { value: 15678.9, unit: 'SUV' },
glycolysis: { value: 1123.45, unit: 'SUV ml' },
volume: { value: 156.78, unit: 'ml' },
voxelCount: { value: 2456, unit: '' },
},
},
{
id: 22,
title: 'Abdominal aorta',
description: 'Description for Abdominal Aorta.',
colorHex: '#B8860B',
statistics: {
centroidX: { value: 0.12, unit: 'cm' },
centroidY: { value: 6.54, unit: 'cm' },
centroidZ: { value: 126.78, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.11, unit: '' },
max: { value: 10.23, unit: 'SUV' },
maxSlice: { value: 68, unit: '' },
mean: { value: 5.43, unit: 'SUV' },
median: { value: 5.21, unit: 'SUV' },
min: { value: 3.1, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.65, unit: '' },
sphereDiameter: { value: 3.45, unit: 'cm' },
standardDeviation: { value: 1.87, unit: 'SUV' },
suvPeak: { value: 9.45, unit: 'SUV' },
total: { value: 5432.1, unit: 'SUV' },
glycolysis: { value: 654.32, unit: 'SUV ml' },
volume: { value: 67.89, unit: 'ml' },
voxelCount: { value: 1123, unit: '' },
},
},
{
id: 23,
title: 'Inferior vena cava',
description: 'Description for Inferior Vena Cava.',
colorHex: '#556B2F',
statistics: {
centroidX: { value: 0.34, unit: 'cm' },
centroidY: { value: 6.12, unit: 'cm' },
centroidZ: { value: 125.43, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.13, unit: '' },
max: { value: 9.87, unit: 'SUV' },
maxSlice: { value: 64, unit: '' },
mean: { value: 5.12, unit: 'SUV' },
median: { value: 4.98, unit: 'SUV' },
min: { value: 2.9, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.62, unit: '' },
sphereDiameter: { value: 3.21, unit: 'cm' },
standardDeviation: { value: 1.76, unit: 'SUV' },
suvPeak: { value: 8.98, unit: 'SUV' },
total: { value: 4567.89, unit: 'SUV' },
glycolysis: { value: 598.76, unit: 'SUV ml' },
volume: { value: 59.87, unit: 'ml' },
voxelCount: { value: 987, unit: '' },
},
},
{
id: 24,
title: 'Portal vein',
description: 'Description for Portal Vein.',
colorHex: '#8B4513',
statistics: {
centroidX: { value: -0.45, unit: 'cm' },
centroidY: { value: 5.67, unit: 'cm' },
centroidZ: { value: 123.45, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.14, unit: '' },
max: { value: 10.56, unit: 'SUV' },
maxSlice: { value: 71, unit: '' },
mean: { value: 5.67, unit: 'SUV' },
median: { value: 5.45, unit: 'SUV' },
min: { value: 3.2, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.69, unit: '' },
sphereDiameter: { value: 3.78, unit: 'cm' },
standardDeviation: { value: 1.92, unit: 'SUV' },
suvPeak: { value: 9.67, unit: 'SUV' },
total: { value: 6789.01, unit: 'SUV' },
glycolysis: { value: 723.45, unit: 'SUV ml' },
volume: { value: 76.54, unit: 'ml' },
voxelCount: { value: 1234, unit: '' },
},
},
{
id: 25,
title: 'Pancreas',
description: 'Description for Pancreas.',
colorHex: '#2F4F4F',
statistics: {
centroidX: { value: -1.56, unit: 'cm' },
centroidY: { value: 5.23, unit: 'cm' },
centroidZ: { value: 120.87, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.17, unit: '' },
max: { value: 14.23, unit: 'SUV' },
maxSlice: { value: 79, unit: '' },
mean: { value: 7.23, unit: 'SUV' },
median: { value: 6.98, unit: 'SUV' },
min: { value: 3.9, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.81, unit: '' },
sphereDiameter: { value: 6.34, unit: 'cm' },
standardDeviation: { value: 2.34, unit: 'SUV' },
suvPeak: { value: 12.67, unit: 'SUV' },
total: { value: 14567.89, unit: 'SUV' },
glycolysis: { value: 1087.65, unit: 'SUV ml' },
volume: { value: 134.56, unit: 'ml' },
voxelCount: { value: 2134, unit: '' },
},
},
{
id: 26,
title: 'Adrenal gland',
description: 'Description for Adrenal Gland.',
colorHex: '#708090',
statistics: {
centroidX: { value: -2.12, unit: 'cm' },
centroidY: { value: 5.76, unit: 'cm' },
centroidZ: { value: 122.34, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.16, unit: '' },
max: { value: 12.34, unit: 'SUV' },
maxSlice: { value: 74, unit: '' },
mean: { value: 6.45, unit: 'SUV' },
median: { value: 6.23, unit: 'SUV' },
min: { value: 3.6, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.74, unit: '' },
sphereDiameter: { value: 5.67, unit: 'cm' },
standardDeviation: { value: 2.21, unit: 'SUV' },
suvPeak: { value: 11.23, unit: 'SUV' },
total: { value: 10987.65, unit: 'SUV' },
glycolysis: { value: 945.67, unit: 'SUV ml' },
volume: { value: 112.34, unit: 'ml' },
voxelCount: { value: 1789, unit: '' },
},
},
{
id: 27,
title: 'Adrenal gland',
description: 'Description for Adrenal Gland.',
colorHex: '#6A5ACD',
statistics: {
centroidX: { value: 2.34, unit: 'cm' },
centroidY: { value: 5.89, unit: 'cm' },
centroidZ: { value: 123.56, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.15, unit: '' },
max: { value: 11.98, unit: 'SUV' },
maxSlice: { value: 73, unit: '' },
mean: { value: 6.32, unit: 'SUV' },
median: { value: 6.12, unit: 'SUV' },
min: { value: 3.5, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.73, unit: '' },
sphereDiameter: { value: 5.45, unit: 'cm' },
standardDeviation: { value: 2.18, unit: 'SUV' },
suvPeak: { value: 10.87, unit: 'SUV' },
total: { value: 10456.78, unit: 'SUV' },
glycolysis: { value: 923.45, unit: 'SUV ml' },
volume: { value: 109.87, unit: 'ml' },
voxelCount: { value: 1756, unit: '' },
},
},
{
id: 28,
title: 'New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test ',
description: 'Description for New Seg Test.',
colorHex: '#4682B4',
statistics: {
centroidX: { value: -2.76, unit: 'cm' },
centroidY: { value: 4.98, unit: 'cm' },
centroidZ: { value: 121.23, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.21, unit: '' },
max: { value: 15.12, unit: 'SUV' },
maxSlice: { value: 83, unit: '' },
mean: { value: 7.45, unit: 'SUV' },
median: { value: 7.12, unit: 'SUV' },
min: { value: 4.1, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.82, unit: '' },
sphereDiameter: { value: 7.23, unit: 'cm' },
standardDeviation: { value: 2.45, unit: 'SUV' },
suvPeak: { value: 13.45, unit: 'SUV' },
total: { value: 19876.54, unit: 'SUV' },
glycolysis: { value: 1345.67, unit: 'SUV ml' },
volume: { value: 187.65, unit: 'ml' },
voxelCount: { value: 2987, unit: '' },
},
},
],
},

View File

@ -44,6 +44,7 @@
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-hover-card": "^1.1.6",
"class-variance-authority": "^0.7.0",
"clsx": "*",
"cmdk": "^1.0.0",

View File

@ -8,6 +8,7 @@ import {
} from '../../components/DropdownMenu';
import { Icons } from '../../components/Icons/Icons';
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip';
import { cn } from '../../lib/utils';
/**
* DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data.
@ -77,6 +78,7 @@ interface DataRowProps {
//
colorHex?: string;
onColor: () => void;
className?: string;
}
const DataRow: React.FC<DataRowProps> = ({
@ -94,16 +96,19 @@ const DataRow: React.FC<DataRowProps> = ({
isSelected = false,
isVisible = true,
disableEditing = false,
className,
}) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isTitleLong = title?.length > 25;
const rowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isSelected && rowRef.current) {
rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [isSelected]);
// useEffect(() => {
// if (isSelected && rowRef.current) {
// setTimeout(() => {
// rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
// }, 200);
// }
// }, [isSelected]);
const handleAction = (action: string, e: React.MouseEvent) => {
e.stopPropagation();
@ -190,7 +195,7 @@ const DataRow: React.FC<DataRowProps> = ({
return (
<div
ref={rowRef}
className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}
className={cn('flex flex-col', !isVisible && 'opacity-60', className)}
>
<div
className={`flex items-center ${

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import { cn } from '../../lib/utils';
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'bg-muted text-muted-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-input z-50 w-64 rounded-md border p-4 shadow-md outline-none',
className
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -0,0 +1 @@
export * from './HoverCard';

View File

@ -24,6 +24,7 @@ import IconMPR from './Sources/IconMPR';
import Info from './Sources/Info';
import InfoLink from './Sources/InfoLink';
import InfoSeries from './Sources/InfoSeries';
import JumpToSlice from './Sources/JumpToSlice';
import ListView from './Sources/ListView';
import LoadingSpinner from './Sources/LoadingSpinner';
import Lock from './Sources/Lock';
@ -38,6 +39,7 @@ import Pin from './Sources/Pin';
import PinFill from './Sources/PinFill';
import Plus from './Sources/Plus';
import PowerOff from './Sources/PowerOff';
import Redo from './Sources/Redo';
import Refresh from './Sources/Refresh';
import Rename from './Sources/Rename';
import Series from './Sources/Series';
@ -195,6 +197,7 @@ import NotificationWarning from './Sources/NotificationWarning';
import ArrowRight from './Sources/ArrowRight';
import ChevronLeft from './Sources/ChevronLeft';
import StatusAlert from './Sources/StatusAlert';
import Undo from './Sources/Undo';
//
//
type IconProps = React.HTMLAttributes<SVGElement>;
@ -717,6 +720,9 @@ export const Icons = {
'tool-point': (props: IconProps) => ToolCircle(props),
'tool-freehand-line': (props: IconProps) => ToolFreehand(props),
clipboard: (props: IconProps) => Clipboard(props),
Undo,
Redo,
JumpToSlice,
/** Adds an icon to the set of icons */
addIcon: (name: string, icon) => {

View File

@ -0,0 +1,69 @@
import React from 'react';
import type { IconProps } from '../types';
export const JumpToSlice = (props: IconProps) => (
<svg
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
{...props}
>
<g
id="icon-jump-to-slice"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<rect
id="Rectangle"
x="0"
y="0"
width="24"
height="24"
></rect>
<g
id="jump-to-slice"
opacity="0.99"
transform="translate(2, 6.5)"
fillRule="nonzero"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
>
<line
x1="20.5"
y1="0"
x2="9.5"
y2="0"
id="Vector"
></line>
<line
x1="20.5"
y1="11"
x2="9.5"
y2="11"
id="Vector"
></line>
<polyline
id="Vector"
points="4 8.49798584 6 5.49798584 0 5.49798584"
></polyline>
<line
x1="4"
y1="2.49798584"
x2="6"
y2="5.49798489"
id="Vector"
></line>
<path
d="M10.3461538,3.5 L19.6538462,3.5 C20.121164,3.5 20.5,3.79847623 20.5,4.16666614 L20.5,6.83333323 C20.5,7.20152314 20.121164,7.5 19.6538462,7.5 L10.3461538,7.5 C9.87883596,7.5 9.5,7.20152314 9.5,6.83333323 L9.5,4.16666614 C9.5,3.79847623 9.87883596,3.5 10.3461538,3.5 Z"
id="Vector"
></path>
</g>
</g>
</svg>
);
export default JumpToSlice;

View File

@ -0,0 +1,51 @@
import React from 'react';
import type { IconProps } from '../types';
export const Redo = (props: IconProps) => (
<svg
width="20px"
height="20px"
viewBox="0 0 20 20"
version="1.1"
{...props}
>
<g
id="Page-1"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g
id="Toolbar"
transform="translate(-1407, -246)"
>
<g
id="redo"
transform="translate(1417, 256) scale(-1, 1) translate(-1417, -256)translate(1405, 244)"
>
<g
id="icon-redo"
transform="translate(2, 2)"
>
<rect
id="Rectangle"
x="0"
y="0"
width="20"
height="20"
></rect>
<path
d="M7.35208562,2 C7.15515629,2 6.92990335,2.09653697 6.77177033,2.25466008 L1.74936988,7.20233994 C1.59094799,7.35387392 1.5,7.57447116 1.5,7.79610318 C1.5,8.02332566 1.58842308,8.23591732 1.75201182,8.39243102 L6.77309942,13.3388655 C6.92990335,13.4956694 7.15515629,13.5922064 7.35208562,13.5922064 C7.81787582,13.5922064 8.11909989,13.2870188 8.11909989,12.8345225 C8.11909989,12.6063308 8.0418091,12.4315864 7.88716003,12.2768955 L5.74465493,10.1984823 L4.25178995,8.93887748 L3.68918984,8.46418364 L4.42451621,8.49806964 L6.43770582,8.59110867 L11.8680022,8.59110867 C14.688432,8.59110867 16.4379802,10.2154905 16.4379802,12.6945664 C16.4379802,15.1936455 14.678791,16.863337 11.8680022,16.863337 L11.8059824,16.863337 C11.3318199,16.863337 10.9923161,17.2049494 10.9923161,17.6583425 C10.9923161,18.1117355 11.3318199,18.453348 11.8059824,18.453348 L11.9519759,18.453348 C15.7113807,18.453348 18,16.2610136 18,12.7692097 C18,9.27273433 15.6408003,7.00109769 11.8493414,7.00109769 L6.44921434,7.00083266 L4.42451621,7.09413672 L3.68918984,7.12802273 L4.25178995,6.65332889 L5.73143708,5.40570972 L7.88441979,3.30867885 C8.04444784,3.1486508 8.11909989,2.98311799 8.11909989,2.75768386 C8.11909989,2.30518755 7.81787582,2 7.35208562,2 Z"
id="Path"
fill="currentColor"
fillRule="nonzero"
></path>
</g>
</g>
</g>
</g>
</svg>
);
export default Redo;

View File

@ -0,0 +1,51 @@
import React from 'react';
import type { IconProps } from '../types';
export const Undo = (props: IconProps) => (
<svg
width="20px"
height="20px"
viewBox="0 0 20 20"
version="1.1"
{...props}
>
<g
id="Page-1"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g
id="Toolbar"
transform="translate(-1375, -246)"
>
<g
id="undo"
transform="translate(1373, 244)"
>
<g
id="icon-undo"
transform="translate(2, 2)"
>
<rect
id="Rectangle"
x="0"
y="0"
width="20"
height="20"
></rect>
<path
d="M7.35208562,2 C7.15515629,2 6.92990335,2.09653697 6.77177033,2.25466008 L1.74936988,7.20233994 C1.59094799,7.35387392 1.5,7.57447116 1.5,7.79610318 C1.5,8.02332566 1.58842308,8.23591732 1.75201182,8.39243102 L6.77309942,13.3388655 C6.92990335,13.4956694 7.15515629,13.5922064 7.35208562,13.5922064 C7.81787582,13.5922064 8.11909989,13.2870188 8.11909989,12.8345225 C8.11909989,12.6063308 8.0418091,12.4315864 7.88716003,12.2768955 L5.74465493,10.1984823 L4.25178995,8.93887748 L3.68918984,8.46418364 L4.42451621,8.49806964 L6.43770582,8.59110867 L11.8680022,8.59110867 C14.688432,8.59110867 16.4379802,10.2154905 16.4379802,12.6945664 C16.4379802,15.1936455 14.678791,16.863337 11.8680022,16.863337 L11.8059824,16.863337 C11.3318199,16.863337 10.9923161,17.2049494 10.9923161,17.6583425 C10.9923161,18.1117355 11.3318199,18.453348 11.8059824,18.453348 L11.9519759,18.453348 C15.7113807,18.453348 18,16.2610136 18,12.7692097 C18,9.27273433 15.6408003,7.00109769 11.8493414,7.00109769 L6.44921434,7.00083266 L4.42451621,7.09413672 L3.68918984,7.12802273 L4.25178995,6.65332889 L5.73143708,5.40570972 L7.88441979,3.30867885 C8.04444784,3.1486508 8.11909989,2.98311799 8.11909989,2.75768386 C8.11909989,2.30518755 7.81787582,2 7.35208562,2 Z"
id="Path"
fill="currentColor"
fillRule="nonzero"
></path>
</g>
</g>
</g>
</g>
</svg>
);
export default Undo;

View File

@ -1,26 +1,8 @@
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
/**
* Props interface for the AddSegmentRow component
*/
interface AddSegmentRowProps {
/** Optional child elements to render within the row */
children?: React.ReactNode;
/** Optional segmentation object to override the active segmentation */
segmentation?: unknown;
}
/**
* A component that renders a row with controls for adding segments and toggling visibility
* in the segmentation table.
*
* @param props - Component properties
* @param props.children - Optional child elements to render within the row
* @param props.segmentation - Optional segmentation object to override the active segmentation
*/
export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, segmentation }) => {
export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ children = null }) => {
const {
activeRepresentation,
disableEditing,
@ -29,18 +11,32 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
onToggleSegmentationRepresentationVisibility,
data,
showAddSegment,
} = useSegmentationTableContext('SegmentationTable');
} = useSegmentationTableContext('AddSegmentRow');
const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every(
segment => segment?.visible !== false
);
// Try to get from expanded context first, then fall back to active segmentation
let segmentationId = activeSegmentationId;
let representation = activeRepresentation;
const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId;
try {
const expandedContext = useSegmentationExpanded('AddSegmentRow');
if (expandedContext.isActive) {
segmentationId = expandedContext.segmentation.segmentationId;
representation = expandedContext.representation;
}
} catch (e) {
// Use the default values from table context
}
// If no segmentations, don't render
if (!data?.length) {
return null;
}
// Check if all segments are visible
const allSegmentsVisible = Object.values(representation?.segments || {}).every(
segment => segment?.visible !== false
);
const Icon = allSegmentsVisible ? (
<Icons.Hide className="h-6 w-6" />
) : (
@ -50,14 +46,14 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
const allowAddSegment = showAddSegment && !disableEditing;
return (
<div className="bg-primary-dark my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
<div className="my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
<div className="flex-1">
{allowAddSegment ? (
<Button
size="sm"
variant="ghost"
className="pr pl-0.5"
onClick={() => onSegmentAdd(segmentationIdToUse)}
onClick={() => onSegmentAdd(segmentationId)}
>
<Icons.Add />
Add Segment
@ -68,10 +64,7 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
size="icon"
variant="ghost"
onClick={() =>
onToggleSegmentationRepresentationVisibility(
segmentationIdToUse,
activeRepresentation?.type
)
onToggleSegmentationRepresentationVisibility(segmentationId, representation?.type)
}
>
{Icon}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Icons } from '../Icons';
import { useTranslation } from 'react-i18next';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext } from './contexts';
export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
children = null,
@ -9,7 +9,7 @@ export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
const { t } = useTranslation('SegmentationTable');
const { onSegmentationAdd, data, disableEditing, mode, disabled } =
useSegmentationTableContext('SegmentationTable');
useSegmentationTableContext('AddSegmentationRow');
const isEmpty = data.length === 0;

View File

@ -0,0 +1,104 @@
import React from 'react';
import { SegmentStatisticsProvider, useSegmentStatistics } from './contexts';
import { roundNumber } from '../../utils';
// Default statistics component
const DefaultStatsList = () => {
const { namedStats } = useSegmentStatistics('DefaultStatsList');
if (!namedStats) {
return null;
}
const handleNumber = (value: number) => {
if (value === null) {
return '';
}
if (Array.isArray(value)) {
return value.map(handleNumber).join(', ');
}
return roundNumber(value);
};
// Sort namedStats entries by order property
const sortedStats = Object.entries(namedStats)
.filter(([_, stat]) => stat && stat.value !== null)
.sort((a, b) => {
const orderA = a[1]?.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b[1]?.order ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
return (
<div className="space-y-1">
{sortedStats.map(([key, stat]) => {
const { label, value, unit } = stat;
return (
<div
key={key}
className="flex justify-between"
>
<div>{label}</div>
<div>
<span className="text-white">{handleNumber(value)}</span>{' '}
<span className="">{unit && unit !== 'none' ? unit : ''}</span>
</div>
</div>
);
})}
</div>
);
};
// Root component that serves as the container and context provider
const SegmentStatisticsRoot = ({ segment, segmentationId, children }) => {
if (!segment) {
return null;
}
return (
<SegmentStatisticsProvider
segment={segment}
segmentationId={segmentationId}
>
<div className="segment-statistics w-full">{children}</div>
</SegmentStatisticsProvider>
);
};
const SegmentStatisticsTitle = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="mb-2">{children}</div>;
};
const SegmentStatisticsHeader = ({ children = null }: { children?: React.ReactNode }) => {
const { segment, segmentationId } = useSegmentStatistics('SegmentStatisticsHeader');
const { segmentIndex } = segment;
return (
<div className="mb-3">
{children &&
React.cloneElement(children as React.ReactElement, { segmentationId, segmentIndex })}
</div>
);
};
const SegmentStatisticsBody = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="segment-statistics-body">{children || <DefaultStatsList />}</div>;
};
const SegmentStatisticsFooter = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="mt-3">{children}</div>;
};
// Create a compound component structure
const SegmentStatistics = Object.assign(SegmentStatisticsRoot, {
Title: SegmentStatisticsTitle,
Header: SegmentStatisticsHeader,
Body: SegmentStatisticsBody,
Footer: SegmentStatisticsFooter,
});
export { SegmentStatistics };
export { useSegmentStatistics };

View File

@ -1,7 +1,165 @@
import React from 'react';
import { PanelSection } from '../PanelSection';
import { useSegmentationTableContext, SegmentationExpandedProvider } from './contexts';
import { useTranslation } from 'react-i18next';
import {
Button,
Icons,
DropdownMenu,
DropdownMenuTrigger,
Tooltip,
TooltipTrigger,
TooltipContent,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '../../components';
export const SegmentationCollapsed: React.FC<{ children?: React.ReactNode }> = ({
// Main header component
const SegmentationCollapsedHeader = ({ children }: { children: React.ReactNode }) => {
return (
<div className="bg-primary-dark flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
{children}
</div>
);
};
// Dropdown menu component - specifically for dropdown menu content
const SegmentationCollapsedDropdownMenu = ({ children }: { children: React.ReactNode }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
{children}
</DropdownMenu>
);
};
// Selector component - for the segmentation selection dropdown
const SegmentationCollapsedSelector = () => {
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
const { data, activeSegmentationId, onSegmentationClick } = useSegmentationTableContext(
'SegmentationCollapsedSelector'
);
if (!data?.length) {
return null;
}
const segmentations = data.map(seg => ({
id: seg.segmentation.segmentationId,
label: seg.segmentation.label,
}));
return (
<Select
onValueChange={value => onSegmentationClick(value)}
value={activeSegmentationId}
>
<SelectTrigger className="w-full overflow-hidden">
<SelectValue placeholder={t('Select a segmentation')} />
</SelectTrigger>
<SelectContent>
{segmentations.map(seg => (
<SelectItem
key={seg.id}
value={seg.id}
>
{seg.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
// Info component - for displaying the info tooltip
const SegmentationCollapsedInfo = () => {
const { data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsedInfo');
const activeSegmentationObj = data.find(
seg => seg.segmentation.segmentationId === activeSegmentationId
);
const info = activeSegmentationObj?.segmentation.cachedStats?.info;
return (
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="end"
>
{info}
</TooltipContent>
</Tooltip>
);
};
// Content component - for the main collapsed view content
const SegmentationCollapsedContent = ({ children }: { children: React.ReactNode }) => {
return <div className="collapsed-content">{children}</div>;
};
// Main compound component
const SegmentationCollapsedRoot: React.FC<{ children?: React.ReactNode }> = ({
children = null,
}) => {
return <div>{children}</div>;
const { mode, data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsed');
// Check if we should render based on mode
if (mode !== 'collapsed' || !data || data.length === 0) {
return null;
}
// Find active segmentation
const activeSegmentationInfo = data.find(
info => info.segmentation.segmentationId === activeSegmentationId
);
if (!activeSegmentationInfo) {
return null;
}
return (
<div className="space-y-0">
<PanelSection className="mb-0">
<SegmentationExpandedProvider
segmentation={activeSegmentationInfo.segmentation}
representation={activeSegmentationInfo.representation}
isActive={true}
onSegmentationClick={() => {}} // No-op since it's already the active one
>
{children}
</SegmentationExpandedProvider>
</PanelSection>
</div>
);
};
// Add the subcomponents to the main component
const SegmentationCollapsed = Object.assign(SegmentationCollapsedRoot, {
Header: SegmentationCollapsedHeader,
DropdownMenu: SegmentationCollapsedDropdownMenu,
Selector: SegmentationCollapsedSelector,
Info: SegmentationCollapsedInfo,
Content: SegmentationCollapsedContent,
});
// Export the component
export { SegmentationCollapsed };

View File

@ -1,47 +1,132 @@
import React from 'react';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { PanelSection } from '../PanelSection';
import { SegmentationHeader } from './SegmentationHeader';
import { SegmentationTable } from './SegmentationTable';
import {
useSegmentationTableContext,
SegmentationExpandedProvider,
useSegmentationExpanded,
} from './contexts';
import { Button } from '../Button';
import { Icons } from '../Icons/Icons';
import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip';
export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { data } = useSegmentationTableContext('SegmentationExpanded');
// Separate the Header component from other children
const headerComponent = React.Children.toArray(children).find(
child => React.isValidElement(child) && child.type === SegmentationTable.Header
);
const otherChildren = React.Children.toArray(children).filter(
child => !(React.isValidElement(child) && child.type === SegmentationTable.Header)
);
// The Header container component
const SegmentationExpandedHeader = ({ children }: { children: React.ReactNode }) => {
const { segmentation, isActive } = useSegmentationExpanded('SegmentationExpandedHeader');
const { onSegmentationClick } = useSegmentationTableContext('SegmentationExpandedHeader');
return (
<>
{data.map(segmentationInfo => (
<PanelSection key={segmentationInfo.segmentation.segmentationId}>
<PanelSection.Header className="border-input border-t-2 bg-transparent pl-1">
{headerComponent ? (
React.cloneElement(headerComponent as React.ReactElement, {
segmentation: segmentationInfo.segmentation,
representation: segmentationInfo.representation,
})
) : (
<SegmentationHeader segmentation={segmentationInfo.segmentation} />
)}
</PanelSection.Header>
<PanelSection.Content>
<div className="segmentation-expanded-section">
{React.Children.map(otherChildren, child =>
React.isValidElement(child)
? React.cloneElement(child, {
segmentation: segmentationInfo.segmentation,
})
: child
)}
</div>
</PanelSection.Content>
</PanelSection>
))}
</>
<PanelSection.Header
className={`border-input border-t-2 bg-transparent pl-1 ${isActive ? 'border-primary' : ''} py-0`}
onClick={e => {
e.stopPropagation();
onSegmentationClick(segmentation.segmentationId);
}}
>
<div className="text-foreground flex h-8 w-full items-center">{children}</div>
</PanelSection.Header>
);
};
// Dropdown menu component - specifically for dropdown menu content
const SegmentationExpandedDropdownMenu = ({ children }: { children: React.ReactNode }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-1"
onClick={e => e.stopPropagation()}
>
<Icons.More />
</Button>
</DropdownMenuTrigger>
{children}
</DropdownMenu>
);
};
// Label component - for displaying the segmentation label
const SegmentationExpandedLabel = () => {
const { segmentation } = useSegmentationExpanded('SegmentationExpandedLabel');
return <div className="pl-1.5">{segmentation.label}</div>;
};
// Info component - for the info tooltip
const SegmentationExpandedInfo = () => {
const { segmentation } = useSegmentationExpanded('SegmentationExpandedInfo');
return (
<div className="ml-auto mr-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{segmentation.cachedStats?.info}</p>
</TooltipContent>
</Tooltip>
</div>
);
};
// Content component
const SegmentationExpandedContent = ({ children }: { children: React.ReactNode }) => {
return (
<PanelSection.Content className="py-0">
<div className="segmentation-expanded-section">{children}</div>
</PanelSection.Content>
);
};
// Main compound component
const SegmentationExpandedRoot = ({ children }) => {
const { data, activeSegmentationId, onSegmentationClick, mode } =
useSegmentationTableContext('SegmentationExpanded');
// Check if we should render based on mode
if (mode !== 'expanded' || !data || data.length === 0) {
return null;
}
return (
<div className="space-y-0">
{data.map(segmentationInfo => {
const isActive = segmentationInfo.segmentation.segmentationId === activeSegmentationId;
return (
<PanelSection
key={segmentationInfo.segmentation.segmentationId}
className="mb-0"
>
<SegmentationExpandedProvider
segmentation={segmentationInfo.segmentation}
representation={segmentationInfo.representation}
isActive={isActive}
onSegmentationClick={onSegmentationClick}
>
{children}
</SegmentationExpandedProvider>
</PanelSection>
);
})}
</div>
);
};
const SegmentationExpanded = Object.assign(SegmentationExpandedRoot, {
Header: SegmentationExpandedHeader,
DropdownMenu: SegmentationExpandedDropdownMenu,
Label: SegmentationExpandedLabel,
Info: SegmentationExpandedInfo,
Content: SegmentationExpandedContent,
});
export { SegmentationExpanded };

View File

@ -3,29 +3,23 @@ import { Button } from '../Button';
import { Icons } from '../Icons/Icons';
import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useTranslation } from 'react-i18next';
import { useSegmentationExpanded, useSegmentationTableContext } from './contexts';
// Then use it in your component:
export const SegmentationHeader: React.FC<{
children?: React.ReactNode;
segmentation?: any;
}> = ({ children, segmentation }) => {
const { t } = useTranslation('SegmentationTable');
const { ...contextProps } = useSegmentationTableContext('SegmentationHeader');
}> = ({ children }) => {
// Always call both hooks unconditionally at the top level
const expandedContext = useSegmentationExpanded('SegmentationHeader');
const tableContext = useSegmentationTableContext('SegmentationHeader');
// Determine which segmentation to use
const segmentation = expandedContext?.segmentation || tableContext.activeSegmentation;
if (!segmentation) {
return null;
}
const childrenWithProps = React.Children.map(children, child =>
React.isValidElement(child)
? React.cloneElement(child, {
t,
...contextProps,
})
: child
);
return (
<div className="text-foreground flex h-8 w-full items-center justify-between">
<div className="flex items-center space-x-1">
@ -40,7 +34,7 @@ export const SegmentationHeader: React.FC<{
<Icons.More />
</Button>
</DropdownMenuTrigger>
{childrenWithProps}
{children}
</DropdownMenu>
<div className="pl-1.5">{segmentation.label}</div>
</div>
@ -55,7 +49,7 @@ export const SegmentationHeader: React.FC<{
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{segmentation.cachedStats.info}</p>
<p>{segmentation.cachedStats?.info}</p>
</TooltipContent>
</Tooltip>
</div>

View File

@ -1,11 +1,10 @@
import React from 'react';
import { ScrollArea, DataRow } from '../../components';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
import { SegmentStatistics } from './SegmentStatistics';
export const SegmentationSegments: React.FC<{
segmentation?: unknown;
representation?: unknown;
}> = ({ segmentation, representation }) => {
export const SegmentationSegments = ({ children = null }: { children?: React.ReactNode }) => {
const {
activeSegmentationId,
disableEditing,
@ -13,37 +12,51 @@ export const SegmentationSegments: React.FC<{
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentClick,
mode,
onSegmentEdit,
onSegmentDelete,
data,
} = useSegmentationTableContext('SegmentationTable.Segments');
} = useSegmentationTableContext('SegmentationSegments');
let segmentationToUse = segmentation;
let representationToUse = representation;
let segmentationIdToUse = activeSegmentationId;
if (!segmentationToUse || !representationToUse) {
const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId);
segmentationToUse = entry?.segmentation;
representationToUse = entry?.representation;
segmentationIdToUse = entry?.segmentation.segmentationId;
// Try to get segmentation data from expanded context first, then fall back to table context
let segmentation;
let representation;
try {
// Try to use the SegmentationExpanded context if available
const segmentationInfo = useSegmentationExpanded('SegmentationSegments');
segmentation = segmentationInfo.segmentation;
representation = segmentationInfo.representation;
} catch (e) {
// Not within SegmentationExpanded context, get from active segmentation
const segmentationInfo = data.find(
entry => entry.segmentation.segmentationId === activeSegmentationId
);
segmentation = segmentationInfo?.segmentation;
representation = segmentationInfo?.representation;
}
if (!representationToUse || !segmentationToUse) {
if (!representation || !segmentation) {
return null;
}
const segments = Object.values(representation.segments);
const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId;
return (
<ScrollArea
className={`ohif-scrollbar invisible-scrollbar bg-bkg-low space-y-px h-[900px]`}
className={`ohif-scrollbar invisible-scrollbar bg-bkg-low max-h-80 space-y-px`}
showArrows={true}
>
{Object.values(representationToUse.segments).map(segment => {
{segments.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, visible } = segment;
const segmentFromSegmentation = segmentationToUse.segments[segmentIndex];
const { segmentIndex, color, visible } = segment as {
segmentIndex: number;
color: number[];
visible: boolean;
};
const segmentFromSegmentation = segmentation.segments[segmentIndex];
if (!segmentFromSegmentation) {
return null;
@ -53,27 +66,70 @@ export const SegmentationSegments: React.FC<{
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<DataRow
key={segmentIndex}
number={segmentIndex}
title={label}
details={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
onColor={() => onSegmentColorClick(segmentationIdToUse, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type)
}
onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)}
onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)}
onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)}
onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)}
/>
<HoverCard
key={`hover-${segmentIndex}`}
openDelay={300}
>
<HoverCardTrigger asChild>
{/* The div here is not random, it is needed for making it work with the hover card */}
<div>
<DataRow
key={segmentIndex}
number={segmentIndex}
title={label}
details={displayText}
description={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
className={!isActiveSegmentation ? 'opacity-80' : ''}
onColor={() => onSegmentColorClick(segmentation.segmentationId, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(
segmentation.segmentationId,
segmentIndex,
representation.type
)
}
onToggleLocked={() =>
onToggleSegmentLock(segmentation.segmentationId, segmentIndex)
}
onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)}
onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)}
onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)}
/>
</div>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="w-72 border"
>
<div className="mb-4 flex items-center space-x-2">
<div
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: cssColor }}
></div>
<h3 className="text-muted-foreground break-words font-semibold">{label}</h3>
</div>
<SegmentStatistics
segment={{
...segmentFromSegmentation,
segmentIndex,
}}
segmentationId={segmentation.segmentationId}
>
{children}
</SegmentStatistics>
</HoverCardContent>
</HoverCard>
);
})}
</ScrollArea>
);
};
SegmentationSegments.displayName = 'SegmentationTable.Segments';

View File

@ -1,108 +0,0 @@
import React from 'react';
import {
Icons,
Button,
DropdownMenu,
DropdownMenuTrigger,
Tooltip,
TooltipTrigger,
TooltipContent,
} from '../../components';
import { useTranslation } from 'react-i18next';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next';
import { useSegmentationTableContext } from './SegmentationTableContext';
export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({
children = null,
}) => {
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
const { data, activeSegmentationId, mode, onSegmentationClick, exportOptions, ...contextProps } =
useSegmentationTableContext('SegmentationTable.HeaderCollapsed');
if (mode !== 'collapsed' || !data?.length) {
return null;
}
const activeSegmentationObj = data.find(
seg => seg.segmentation.segmentationId === activeSegmentationId
);
const activeSegmentation = {
id: activeSegmentationObj?.segmentation.segmentationId,
label: activeSegmentationObj?.segmentation.label,
info: activeSegmentationObj?.segmentation.cachedStats?.info,
};
const segmentations = data.map(seg => ({
id: seg.segmentation.segmentationId,
label: seg.segmentation.label,
info: seg.segmentation.cachedStats?.info,
}));
const allowExport = exportOptions?.find(
({ segmentationId }) => segmentationId === activeSegmentation.id
)?.isExportable;
const childrenWithProps = React.Children.map(children, child =>
React.isValidElement(child)
? React.cloneElement(child, {
activeSegmentation,
allowExport,
t,
...contextProps,
})
: child
);
return (
<div className="bg-primary-dark flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
{childrenWithProps}
</DropdownMenu>
<Select
onValueChange={value => onSegmentationClick(value)}
value={activeSegmentation.id}
>
<SelectTrigger className="w-full overflow-hidden">
<SelectValue placeholder={t('Select a segmentation')} />
</SelectTrigger>
<SelectContent>
{segmentations.map(seg => (
<SelectItem
key={seg.id}
value={seg.id}
>
{seg.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="end"
>
{activeSegmentation.info}
</TooltipContent>
</Tooltip>
</div>
);
};

View File

@ -1,20 +1,26 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState, Children, isValidElement } from 'react';
import { useTranslation } from 'react-i18next';
import { PanelSection } from '../PanelSection';
import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext';
import {
SegmentationTableProvider,
SegmentationTableContextType,
ViewportSegmentationInfo,
} from './contexts';
import { SegmentationSegments } from './SegmentationSegments';
import { SegmentStatistics } from './SegmentStatistics';
import { SegmentationTableConfig } from './SegmentationTableConfig';
import { AddSegmentRow } from './AddSegmentRow';
import { AddSegmentationRow } from './AddSegmentationRow';
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
import { SegmentationHeader } from './SegmentationHeader';
import { SegmentationCollapsed } from './SegmentationCollapsed';
import { SegmentationExpanded } from './SegmentationExpanded';
import Icons from '../Icons';
interface SegmentationTableProps extends SegmentationTableContext {
disabled?: boolean;
// Only include props that aren't part of the context
interface SegmentationTableProps extends Omit<SegmentationTableContextType, 'setShowConfig'> {
title?: string;
children?: ReactNode;
setShowConfig?: (value: boolean) => void;
}
interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
@ -22,55 +28,118 @@ interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
Config: typeof SegmentationTableConfig;
AddSegmentRow: typeof AddSegmentRow;
AddSegmentationRow: typeof AddSegmentationRow;
SelectorHeader: typeof SegmentationSelectorHeader;
Header: typeof SegmentationHeader;
Collapsed: typeof SegmentationCollapsed;
Expanded: typeof SegmentationExpanded;
SegmentStatistics: typeof SegmentStatistics;
}
export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => {
export const SegmentationTableRoot = (props: SegmentationTableProps) => {
const { t } = useTranslation('SegmentationTable');
const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props;
const {
data = [],
mode,
title,
disableEditing = false,
disabled = false,
children,
showConfig: externalShowConfig,
...contextProps
} = props;
const [internalShowConfig, setInternalShowConfig] = useState(false);
const showConfig = externalShowConfig !== undefined ? externalShowConfig : internalShowConfig;
// Find the active segmentation info based on which representation is active
const activeSegmentationInfo = data.find(info => info.representation?.active);
const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId;
const activeRepresentation = activeSegmentationInfo?.representation;
const activeSegmentation = activeSegmentationInfo?.segmentation;
const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } =
activeRepresentation?.styles ?? {};
// Get the active segmentation ID
const activeSegmentationId =
props.activeSegmentationId || activeSegmentationInfo?.segmentation?.segmentationId;
const activeRepresentation = props.activeRepresentation || activeSegmentationInfo?.representation;
const activeSegmentation = props.activeSegmentation || activeSegmentationInfo?.segmentation;
// Extract style properties or use defaults
const {
fillAlpha = props.fillAlpha || 0.5,
fillAlphaInactive = props.fillAlphaInactive || 0.2,
outlineWidth = props.outlineWidth || 1,
renderFill = props.renderFill !== undefined ? props.renderFill : true,
renderOutline = props.renderOutline !== undefined ? props.renderOutline : true,
} = activeRepresentation?.styles ?? {};
// Check if SegmentationTableConfig is present in children
const hasConfigComponent = Children.toArray(children).some(
child => isValidElement(child) && child.type === SegmentationTableConfig
);
// Process children to conditionally render the config component based on showConfig
const processedChildren = Children.map(children, child => {
if (isValidElement(child) && child.type === SegmentationTableConfig) {
// Only render the Config component if showConfig is true
return showConfig ? child : null;
}
return child;
});
const toggleShowConfig = () => {
if (props.setShowConfig) {
props.setShowConfig(!showConfig);
} else {
setInternalShowConfig(!internalShowConfig);
}
};
return (
<SegmentationTableProvider
data={data}
mode={mode}
disabled={disabled}
disableEditing={disableEditing}
fillAlpha={fillAlpha}
fillAlphaInactive={fillAlphaInactive}
outlineWidth={outlineWidth}
renderFill={renderFill}
renderOutline={renderOutline}
activeSegmentationId={activeSegmentationId}
activeSegmentation={activeSegmentation}
activeRepresentation={activeRepresentation}
{...contextProps}
value={{
data,
mode,
showConfig,
disabled,
disableEditing,
fillAlpha,
fillAlphaInactive,
outlineWidth,
renderFill,
renderOutline,
activeSegmentationId,
activeSegmentation,
activeRepresentation,
...contextProps,
setShowConfig: toggleShowConfig,
}}
>
<PanelSection defaultOpen={true}>
<PanelSection.Header>
<PanelSection.Header className="flex items-center justify-between">
<span>{t(title)}</span>
{hasConfigComponent && (
<div className="ml-auto mr-2">
<Icons.Settings
className="text-primary-active h-4 w-4"
onClick={e => {
e.stopPropagation();
toggleShowConfig();
}}
/>
</div>
)}
</PanelSection.Header>
<PanelSection.Content>{children}</PanelSection.Content>
<PanelSection.Content>{processedChildren}</PanelSection.Content>
</PanelSection>
</SegmentationTableProvider>
);
};
SegmentationTable.Segments = SegmentationSegments;
SegmentationTable.Config = SegmentationTableConfig;
SegmentationTable.AddSegmentRow = AddSegmentRow;
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
SegmentationTable.Header = SegmentationHeader;
SegmentationTable.Collapsed = SegmentationCollapsed;
SegmentationTable.Expanded = SegmentationExpanded;
const SegmentationTable = Object.assign(SegmentationTableRoot, {
Segments: SegmentationSegments,
Config: SegmentationTableConfig,
AddSegmentRow: AddSegmentRow,
AddSegmentationRow: AddSegmentationRow,
Collapsed: SegmentationCollapsed,
Expanded: SegmentationExpanded,
SegmentStatistics: SegmentStatistics,
Header: SegmentationHeader,
}) as SegmentationTableComponent;
export { SegmentationTable };

View File

@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { PanelSection } from '../PanelSection';
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
import { Slider } from '../Slider';
import { Icons } from '../Icons';
import { Switch } from '../Switch';
import { Label } from '../Label';
import { Input } from '../Input';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext } from './contexts';
export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { t } = useTranslation('SegmentationTable.AppearanceSettings');
@ -26,152 +25,134 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> =
renderInactiveSegmentations,
toggleRenderInactiveSegmentations,
data,
} = useSegmentationTableContext('styles');
} = useSegmentationTableContext('SegmentationTableConfig');
if (!data?.length) {
return null;
}
return (
<PanelSection defaultOpen={false}>
<PanelSection.Header>
<div className="flex items-center">
<Icons.Settings className="mr-2 h-4 w-4" />
<span>{t('Appearance Settings')}</span>
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
<div className="my-1 flex items-center justify-between">
<span className="text-aqua-pale text-xs">
{t('Show')}:{' '}
{renderFill && renderOutline
? t('Fill & Outline')
: renderOutline
? t('Outline Only')
: t('Fill Only')}
</span>
<Tabs
value={
renderFill && renderOutline ? 'fill-and-outline' : renderOutline ? 'outline' : 'fill'
}
onValueChange={value => {
if (value === 'fill-and-outline') {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, true);
} else if (value === 'outline') {
setRenderFill({ type: activeRepresentation.type }, false);
setRenderOutline({ type: activeRepresentation.type }, true);
} else {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, false);
}
}}
>
<TabsList>
<TabsTrigger value="fill-and-outline">
<Icons.FillAndOutline className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="outline">
<Icons.OutlineOnly className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="fill">
<Icons.FillOnly className="text-primary-active" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="space-y-2">
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlpha]}
onValueChange={([value]) => setFillAlpha({ type: activeRepresentation.type }, value)}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlpha}
onChange={e =>
setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
</PanelSection.Header>
<PanelSection.Content>
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
<div className="my-1 flex items-center justify-between">
<span className="text-aqua-pale text-xs">
{t('Show')}:{' '}
{renderFill && renderOutline
? t('Fill & Outline')
: renderOutline
? t('Outline Only')
: t('Fill Only')}
</span>
<Tabs
value={
renderFill && renderOutline
? 'fill-and-outline'
: renderOutline
? 'outline'
: 'fill'
}
onValueChange={value => {
if (value === 'fill-and-outline') {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, true);
} else if (value === 'outline') {
setRenderFill({ type: activeRepresentation.type }, false);
setRenderOutline({ type: activeRepresentation.type }, true);
} else {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, false);
}
}}
>
<TabsList>
<TabsTrigger value="fill-and-outline">
<Icons.FillAndOutline className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="outline">
<Icons.OutlineOnly className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="fill">
<Icons.FillOnly className="text-primary-active" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="space-y-2">
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlpha]}
onValueChange={([value]) =>
setFillAlpha({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlpha}
onChange={e =>
setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
{t('Border')}
</Label>
<Slider
value={[outlineWidth]}
onValueChange={([value]) =>
setOutlineWidth({ type: activeRepresentation.type }, value)
}
max={10}
min={0}
step={0.1}
className="mx-1 flex-1"
/>
<Input
value={outlineWidth}
onChange={e =>
setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value))
}
className="mx-1 w-10 flex-none text-center"
/>
</div>
</div>
<div className="border-input w-full border"></div>
<div className="my-2 flex items-center pl-1">
<Switch
checked={renderInactiveSegmentations}
onCheckedChange={toggleRenderInactiveSegmentations}
/>
<Label className="text-muted-foreground mx-2 text-xs">
{t('Display inactive segmentations')}
</Label>
</div>
{renderInactiveSegmentations && (
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlphaInactive]}
onValueChange={([value]) =>
setFillAlphaInactive({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlphaInactive}
onChange={e =>
setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
)}
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
{t('Border')}
</Label>
<Slider
value={[outlineWidth]}
onValueChange={([value]) => setOutlineWidth({ type: activeRepresentation.type }, value)}
max={10}
min={0}
step={0.1}
className="mx-1 flex-1"
/>
<Input
value={outlineWidth}
onChange={e =>
setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value))
}
className="mx-1 w-10 flex-none text-center"
/>
</div>
{children}
</PanelSection.Content>
</PanelSection>
</div>
<div className="border-input w-full border"></div>
<div className="my-2 flex items-center pl-1">
<Switch
checked={renderInactiveSegmentations}
onCheckedChange={toggleRenderInactiveSegmentations}
/>
<Label className="text-muted-foreground mx-2 text-xs">
{t('Display inactive segmentations')}
</Label>
</div>
{renderInactiveSegmentations && (
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlphaInactive]}
onValueChange={([value]) =>
setFillAlphaInactive({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlphaInactive}
onChange={e =>
setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
)}
{children}
</div>
);
};

View File

@ -1,80 +0,0 @@
import { createContext } from '../../lib/createContext';
interface Segmentation {
segmentationId: string;
label: string;
cachedStats: {
info: string;
};
}
interface Representation {
active: boolean;
visible: boolean;
type: string;
styles: {
fillAlpha: number;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
};
}
interface ViewportSegmentationInfo {
segmentation: Segmentation;
representation: Representation;
}
interface SegmentationTableContext {
data: ViewportSegmentationInfo[];
disabled: boolean;
mode: 'collapsed' | 'expanded';
fillAlpha: number;
exportOptions: {
segmentationId: string;
isExportable: boolean;
}[];
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
activeSegmentationId: string;
activeRepresentation: Representation;
activeSegmentation: Segmentation;
setRenderFill: ({ type }, value: boolean) => void;
setRenderOutline: ({ type }, value: boolean) => void;
setOutlineWidth: ({ type }, value: number) => void;
setFillAlpha: ({ type }, value: number) => void;
setFillAlphaInactive: ({ type }, value: number) => void;
renderInactiveSegmentations: boolean;
toggleRenderInactiveSegmentations: () => void;
onSegmentationAdd: (segmentationId: string) => void;
onSegmentationClick: (segmentationId: string) => void;
onSegmentationDelete: (segmentationId: string) => void;
onSegmentAdd: (segmentationId: string) => void;
onSegmentClick: (segmentationId: string, segmentIndex: number) => void;
onSegmentEdit: (segmentationId: string, segmentIndex: number) => void;
onSegmentationEdit: (segmentationId: string) => void;
onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void;
onSegmentDelete: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void;
onSegmentationDownload: (segmentationId: string) => void;
storeSegmentation: (segmentationId: string) => void;
onSegmentationDownloadRTSS: (segmentationId: string) => void;
setStyle: (
segmentationId: string,
representationType: string,
styleKey: string,
value: unknown
) => void;
onSegmentationRemoveFromViewport: (segmentationId: string) => void;
disableEditing: boolean;
}
const [SegmentationTableProvider, useSegmentationTableContext] =
createContext<SegmentationTableContext>('SegmentationTable');
export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext };

View File

@ -0,0 +1,45 @@
import React, { createContext, useContext } from 'react';
interface SegmentStatisticsContextType {
segment: {
segmentIndex: number;
cachedStats: any;
};
namedStats: Record<string, any> | undefined;
segmentationId: string;
}
// Create a named context
const SegmentStatisticsContext = createContext<SegmentStatisticsContextType | undefined>(undefined);
SegmentStatisticsContext.displayName = 'SegmentStatisticsContext';
export const useSegmentStatistics = (componentName?: string) => {
const context = useContext(SegmentStatisticsContext);
if (context === undefined) {
throw new Error(
`useSegmentStatistics must be used within a SegmentStatisticsProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};
export const SegmentStatisticsProvider: React.FC<{
segment: {
segmentIndex: number;
cachedStats: any;
};
segmentationId: string;
children: React.ReactNode;
}> = ({ segment, segmentationId, children }) => {
const { cachedStats } = segment || {};
const { namedStats } = cachedStats || {};
return (
<SegmentStatisticsContext.Provider value={{ segment, namedStats, segmentationId }}>
{children}
</SegmentStatisticsContext.Provider>
);
};

View File

@ -0,0 +1,40 @@
import React, { createContext, useContext } from 'react';
// Context for expanded segmentation panel
interface SegmentationExpandedContextType {
segmentation: any;
representation: any;
isActive: boolean;
}
// Create a named context
const SegmentationExpandedContext = createContext<SegmentationExpandedContextType | undefined>(
undefined
);
SegmentationExpandedContext.displayName = 'SegmentationExpandedContext';
export const useSegmentationExpanded = (componentName?: string) => {
const context = useContext(SegmentationExpandedContext);
if (context === undefined) {
throw new Error(
`useSegmentationExpanded must be used within a SegmentationExpandedProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};
export const SegmentationExpandedProvider: React.FC<{
segmentation: any;
representation: any;
isActive: boolean;
children: React.ReactNode;
}> = ({ segmentation, representation, isActive, children }) => {
return (
<SegmentationExpandedContext.Provider value={{ segmentation, representation, isActive }}>
{children}
</SegmentationExpandedContext.Provider>
);
};

View File

@ -0,0 +1,129 @@
import React, { createContext, useContext } from 'react';
export interface SegmentStats {
info: string;
namedStats?: Record<string, number | string>;
}
export interface Segment {
color?: string;
label: string;
segmentIndex: number;
visible?: boolean;
locked?: boolean;
}
export interface Segmentation {
segmentationId: string;
label: string;
cachedStats: SegmentStats;
segments: Record<number, Segment>;
}
export interface RepresentationStyles {
fillAlpha: number;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
}
export interface Representation {
active: boolean;
visible: boolean;
type: string;
segments: Record<number, Segment>;
styles: RepresentationStyles;
}
export interface ViewportSegmentationInfo {
segmentation: Segmentation;
representation: Representation;
}
export interface ExportOption {
segmentationId: string;
isExportable: boolean;
}
export interface SegmentationTableContextType {
data: ViewportSegmentationInfo[];
disabled: boolean;
mode: 'collapsed' | 'expanded';
fillAlpha: number;
exportOptions?: ExportOption[];
showConfig: boolean;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
activeSegmentationId?: string;
activeRepresentation?: Representation;
activeSegmentation?: Segmentation;
disableEditing: boolean;
showAddSegment?: boolean;
renderInactiveSegmentations?: boolean;
// Function handlers
setShowConfig?: (show: boolean) => void;
setRenderFill?: ({ type }: { type: string }, value: boolean) => void;
setRenderOutline?: ({ type }: { type: string }, value: boolean) => void;
setOutlineWidth?: ({ type }: { type: string }, value: number) => void;
setFillAlpha?: ({ type }: { type: string }, value: number) => void;
setFillAlphaInactive?: ({ type }: { type: string }, value: number) => void;
toggleRenderInactiveSegmentations?: () => void;
onSegmentationAdd?: (segmentationId: string) => void;
onSegmentationClick?: (segmentationId: string) => void;
onSegmentationDelete?: (segmentationId: string) => void;
onSegmentAdd?: (segmentationId: string) => void;
onSegmentClick?: (segmentationId: string, segmentIndex: number) => void;
onSegmentEdit?: (segmentationId: string, segmentIndex: number) => void;
onSegmentationEdit?: (segmentationId: string) => void;
onSegmentColorClick?: (segmentationId: string, segmentIndex: number) => void;
onSegmentDelete?: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentVisibility?: (
segmentationId: string,
segmentIndex: number,
representationType?: string
) => void;
onToggleSegmentLock?: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentationRepresentationVisibility?: (segmentationId: string, type: string) => void;
onSegmentationDownload?: (segmentationId: string) => void;
storeSegmentation?: (segmentationId: string) => void;
onSegmentationDownloadRTSS?: (segmentationId: string) => void;
setStyle?: (
segmentationId: string,
representationType: string,
styleKey: string,
value: unknown
) => void;
onSegmentationRemoveFromViewport?: (segmentationId: string) => void;
}
const SegmentationTableContext = createContext<SegmentationTableContextType | undefined>(undefined);
SegmentationTableContext.displayName = 'SegmentationTableContext';
export const SegmentationTableProvider = ({
value,
children,
}: {
value: SegmentationTableContextType;
children: React.ReactNode;
}) => {
return (
<SegmentationTableContext.Provider value={value}>{children}</SegmentationTableContext.Provider>
);
};
export const useSegmentationTableContext = (componentName?: string) => {
const context = useContext(SegmentationTableContext);
if (context === undefined) {
throw new Error(
`useSegmentationTableContext must be used within a SegmentationTableProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};

View File

@ -0,0 +1,15 @@
import { useSegmentationTableContext, SegmentationTableProvider } from './SegmentationTableContext';
import {
useSegmentationExpanded,
SegmentationExpandedProvider,
} from './SegmentationExpandedContext';
import { useSegmentStatistics, SegmentStatisticsProvider } from './SegmentStatisticsContext';
export {
useSegmentationTableContext,
useSegmentationExpanded,
useSegmentStatistics,
SegmentationTableProvider,
SegmentationExpandedProvider,
SegmentStatisticsProvider,
};

View File

@ -3,20 +3,22 @@ import { SegmentationTableConfig } from './SegmentationTableConfig';
import { AddSegmentationRow } from './AddSegmentationRow';
import { AddSegmentRow } from './AddSegmentRow';
import { SegmentationSegments } from './SegmentationSegments';
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
import { SegmentationHeader } from './SegmentationHeader';
import { useSegmentationTableContext } from './SegmentationTableContext';
import {
useSegmentationTableContext,
useSegmentStatistics,
useSegmentationExpanded,
} from './contexts';
SegmentationTable.Segments = SegmentationSegments;
SegmentationTable.Config = SegmentationTableConfig;
SegmentationTable.AddSegmentRow = AddSegmentRow;
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
SegmentationTable.Header = SegmentationHeader;
export {
// context
useSegmentationTableContext,
useSegmentStatistics,
useSegmentationExpanded,
// components
SegmentationTable,
};

View File

@ -58,7 +58,7 @@ function ViewportPane({
onScroll={onInteractionHandler}
onWheel={onInteractionHandler}
className={classNames(
'group relative h-full w-full overflow-hidden transition duration-300',
'group/pane relative h-full w-full overflow-hidden transition duration-300',
className
)}
style={customStyle}

View File

@ -81,7 +81,12 @@ import { Onboarding } from './Onboarding';
import { DoubleSlider } from './DoubleSlider';
import { DataRow } from './DataRow';
import { MeasurementTable } from './MeasurementTable';
import { SegmentationTable, useSegmentationTableContext } from './SegmentationTable';
import {
SegmentationTable,
useSegmentationTableContext,
useSegmentationExpanded,
useSegmentStatistics,
} from './SegmentationTable';
import { Toaster, toast } from './Sonner';
import { StudySummary } from './StudySummary';
import { ErrorBoundary } from './Errorboundary';
@ -108,6 +113,9 @@ import {
} from './ToolButton';
import { ToolSettings } from './OHIFToolSettings';
// Segmentation Context Exports
export { useSegmentationTableContext, useSegmentationExpanded, useSegmentStatistics };
export {
Numeric,
ErrorBoundary,
@ -205,7 +213,6 @@ export {
Toaster,
toast,
SegmentationTable,
useSegmentationTableContext,
StudySummary,
Header,
Card,

View File

@ -77,6 +77,8 @@ import {
MeasurementTable,
SegmentationTable,
useSegmentationTableContext,
useSegmentationExpanded,
useSegmentStatistics,
TooltipProvider,
Tooltip,
TooltipTrigger,
@ -210,6 +212,7 @@ export {
MeasurementTable,
SegmentationTable,
useSegmentationTableContext,
useSegmentationExpanded,
TooltipProvider,
Tooltip,
TooltipTrigger,
@ -246,5 +249,6 @@ export {
useDialog,
ManagedDialog,
ToolSettings,
useSegmentStatistics,
InputFilter,
};
};

View File

@ -1,3 +1,4 @@
import { getToggledClassName } from './getToggledClassName';
import roundNumber from './roundNumber';
export { getToggledClassName };
export { getToggledClassName, roundNumber };

View File

@ -0,0 +1,43 @@
/**
* Truncates decimal points to that there is at least 1+precision significant
* digits.
*
* For example, with the default precision 2 (3 significant digits)
* * Values larger than 100 show no information after the decimal point
* * Values between 10 and 99 show 1 decimal point
* * Values between 1 and 9 show 2 decimal points
*
* @param value - to return a fixed measurement value from
* @param precision - defining how many digits after 1..9 are desired
*/
function roundNumber(value, precision = 2) {
if (Array.isArray(value)) {
return value.map(v => roundNumber(v, precision)).join(', ');
}
if (value === undefined || value === null || value === '') {
return 'NaN';
}
value = Number(value);
const absValue = Math.abs(value);
if (absValue < 0.0001) {
return `${value}`;
}
const fixedPrecision =
absValue >= 100
? precision - 2
: absValue >= 10
? precision - 1
: absValue >= 1
? precision
: absValue >= 0.1
? precision + 1
: absValue >= 0.01
? precision + 2
: absValue >= 0.001
? precision + 3
: precision + 4;
return value.toFixed(fixedPrecision);
}
export default roundNumber;

162
yarn.lock
View File

@ -1393,10 +1393,10 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.5.0.tgz#8c719bc39f4dc78466cfb1fefbb3cbf3c82666d3"
integrity sha512-xkixGbCDS2wwAHmggx1mW0ewEjWGntbE5L1svdkmRpGBvTtEkQJbNkNG6dUiHXWZ9mICxwahyiMc7YVDdH3tfA==
"@cornerstonejs/adapters@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.7.0.tgz#7e16d609e1e5035585e40d7a7d919534b03ae094"
integrity sha512-7ROFACNjT4W8dukUoKwggHhl+tjg0rdDpYDq8rM6b0c0gO2MgYKbU55t9MoT682yIuebt81jCsAbFSRGVzf45Q==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
@ -1429,41 +1429,50 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0"
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
"@cornerstonejs/core@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.5.0.tgz#8e57064c347f32c8ecd28c5e2f46e95aecb5c00c"
integrity sha512-Ai4dyxZameDz+cp3nBST6KfK1WK/Ic5R6DoTFrBlph4DuK7IUradAx2N9uEydhUJTuF6606mF3VeaAdPG9Hfig==
"@cornerstonejs/core@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.7.0.tgz#d31cafc237ccc7ae621d1a41e3979a721b2fdcff"
integrity sha512-eRCovUYO3JvFV3Dzuwc6EWn9bpm+fvp/x6SXvr3MwiO+LRw1o5bmZfeh0+m4+5xzvQJPbjNYiXPPEpLYGQIOqQ==
dependencies:
"@kitware/vtk.js" "32.12.0"
"@kitware/vtk.js" "32.12.1"
comlink "^4.4.1"
gl-matrix "^3.4.3"
loglevel "^1.9.2"
"@cornerstonejs/dicom-image-loader@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.5.0.tgz#78a01ad3587d547da7ca708713b42ced75451851"
integrity sha512-Jel/an3IHe68sebtRLYB3/aJ1Dggw8D11cWBiBUqzXfxGTlg1SUd3KnvU5OrgNJ+ZpNUEdwCrR3OoiU65hL6UA==
"@cornerstonejs/dicom-image-loader@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.7.0.tgz#6446bc6f9011674b5d000b275b6b0c34efd95c2c"
integrity sha512-MSv5A7ZcAp7uVm1ofrTaQppPm1DyOAXN0BdX8a2/fMZ2Ud4OmEtk0aoUlaJ52/hQtUvfdCqfYObBos2oBIMQ1w==
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"
"@cornerstonejs/polymorphic-segmentation@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.5.0.tgz#b97218b3140ad895b567b3e2dc8678ebf03be531"
integrity sha512-S2zqXNKlFURPCq99fPj5CAnBO7chuVk7bIRWmBOBYQL21TnL4rzyvY+Td5pvHBal4sZ8uTs6PW72rrwGCospAQ==
"@cornerstonejs/labelmap-interpolation@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.7.0.tgz#00d7bbd056fd292b2ea53681259efd0b8f6e5f5f"
integrity sha512-/1l1XsKFYeov+hrMKindJ4n6l4H2zcXFwcO+wtqE4kmfoFhXRN6RSlIXbCRzEZtFCmh0hafg7SUUcXxr0QoClQ==
dependencies:
"@itk-wasm/morphological-contour-interpolation" "1.1.0"
itk-wasm "1.0.0-b.165"
"@cornerstonejs/polymorphic-segmentation@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.7.0.tgz#6bc0c71451fc8bacfcc4b2ac311ba5efda43c51e"
integrity sha512-MYb8aXE2EwfsyPmhvhTmNdTkCq5pIlMJuVRmT6GuQ4nf2dIhx5AeZ+Ya/TRaNNumV2Fx9/9PetI8+Z8XJLAILw==
dependencies:
"@icr/polyseg-wasm" "0.4.0"
"@cornerstonejs/tools@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.5.0.tgz#cfb046cfe6095c2295e03cb66da74e675f9d60c8"
integrity sha512-wyT7runGpPvY9dMJOUBAtxDBSqabk6Ks/SNjbTGXAIUemAYuonP+33pN4zK/4n/zFfa0gpKFP7LFc461xFK16g==
"@cornerstonejs/tools@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.7.0.tgz#0df08e1e9e47ab5da103592488a3a29d567537c8"
integrity sha512-72lbmaAm/dnOANAUNc0OK+9aL2pFklaWBLR+kOKn8OeSh1i9RRTSEaP8zWzStaSNAcdhwQ+JOQzcMR40Eo8oNQ==
dependencies:
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"
@ -2299,6 +2308,29 @@
worker-loader "3.0.8"
xmlbuilder2 "3.0.2"
"@kitware/vtk.js@32.12.1":
version "32.12.1"
resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-32.12.1.tgz#426fe65112c36c4bf312044dee0aeb641616d09b"
integrity sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg==
dependencies:
"@babel/runtime" "7.22.11"
"@types/webxr" "^0.5.5"
commander "9.2.0"
d3-scale "4.0.2"
fast-deep-equal "^3.1.3"
fflate "0.7.3"
gl-matrix "3.4.3"
globalthis "1.0.3"
seedrandom "3.0.5"
shader-loader "1.3.1"
shelljs "0.8.5"
spark-md5 "3.0.2"
stream-browserify "3.0.0"
utif "3.1.0"
webworker-promise "0.5.0"
worker-loader "3.0.8"
xmlbuilder2 "3.0.2"
"@lerna/child-process@7.4.2":
version "7.4.2"
resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511"
@ -3020,6 +3052,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-arrow@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-checkbox@^1.1.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52"
@ -3224,6 +3263,17 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dismissable-layer@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924"
@ -3287,6 +3337,21 @@
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-hover-card@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5"
integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.5"
"@radix-ui/react-popper" "1.2.2"
"@radix-ui/react-portal" "1.1.4"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-icons@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
@ -3432,6 +3497,22 @@
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-popper@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029"
integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
@ -3464,6 +3545,14 @@
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8"
integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
@ -3511,6 +3600,13 @@
dependencies:
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-primitive@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==
dependencies:
"@radix-ui/react-slot" "1.1.2"
"@radix-ui/react-roving-focus@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
@ -3657,6 +3753,13 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-slot@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-switch@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a"
@ -12875,6 +12978,23 @@ iterator.prototype@^1.1.3:
reflect.getprototypeof "^1.0.4"
set-function-name "^2.0.1"
itk-wasm@1.0.0-b.165:
version "1.0.0-b.165"
resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.165.tgz#a5ba3c6aec94fe371ac297ecd3b22a4f85cc6566"
integrity sha512-Rq0+AL2BuRqcFh9r3h69pB3nt84tw/gTxnI6GjIOYw5Bfah9qO+C8hRJpD8aWNBa5o3wNHRAMVlp0pt/L4K8gg==
dependencies:
"@itk-wasm/dam" "^1.1.1"
"@thewtex/zstddec" "^0.2.0"
"@types/emscripten" "^1.39.10"
axios "^1.6.2"
comlink "^4.4.1"
commander "^11.1.0"
fs-extra "^11.2.0"
glob "^8.1.0"
markdown-table "^3.0.3"
mime-types "^2.1.35"
wasm-feature-detect "^1.6.1"
itk-wasm@1.0.0-b.173:
version "1.0.0-b.173"
resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.173.tgz#e484e1765f4205a5704f8fac205d2ecf74e726a2"