feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)
This commit is contained in:
parent
694f3e9611
commit
ec79b5523e
402
bun.lock
402
bun.lock
@ -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=="],
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@ -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);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -13,6 +13,7 @@ const supportedTools = [
|
||||
'LivewireContour',
|
||||
'UltrasoundDirectionalTool',
|
||||
'SCOORD3DPoint',
|
||||
'SegmentBidirectional',
|
||||
];
|
||||
|
||||
export default supportedTools;
|
||||
|
||||
@ -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 =>
|
||||
|
||||
185
extensions/cornerstone/src/utils/updateSegmentationStats.ts
Normal file
185
extensions/cornerstone/src/utils/updateSegmentationStats.ts
Normal 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;
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser';
|
||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
|
||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -214,7 +214,7 @@ const toolbarButtons: Button[] = [
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.mpr',
|
||||
evaluate: 'evaluate.displaySetIsReconstructable',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +76,9 @@ function createTools(utilityModule) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: toolNames.SegmentBidirectional,
|
||||
},
|
||||
{
|
||||
toolName: 'ThresholdSphereBrushDynamic',
|
||||
parentTool: 'Brush',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
```
|
||||
@ -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>
|
||||
|
||||
@ -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: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 ${
|
||||
|
||||
27
platform/ui-next/src/components/HoverCard/HoverCard.tsx
Normal file
27
platform/ui-next/src/components/HoverCard/HoverCard.tsx
Normal 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 };
|
||||
1
platform/ui-next/src/components/HoverCard/index.ts
Normal file
1
platform/ui-next/src/components/HoverCard/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './HoverCard';
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
51
platform/ui-next/src/components/Icons/Sources/Redo.tsx
Normal file
51
platform/ui-next/src/components/Icons/Sources/Redo.tsx
Normal 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;
|
||||
51
platform/ui-next/src/components/Icons/Sources/Undo.tsx
Normal file
51
platform/ui-next/src/components/Icons/Sources/Undo.tsx
Normal 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;
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 };
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { getToggledClassName } from './getToggledClassName';
|
||||
import roundNumber from './roundNumber';
|
||||
|
||||
export { getToggledClassName };
|
||||
export { getToggledClassName, roundNumber };
|
||||
|
||||
43
platform/ui-next/src/utils/roundNumber.ts
Normal file
43
platform/ui-next/src/utils/roundNumber.ts
Normal 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
162
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user