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

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

402
bun.lock
View File

@ -33,7 +33,7 @@
}, },
"addOns/externals/devDependencies": { "addOns/externals/devDependencies": {
"name": "@externals/devDependencies", "name": "@externals/devDependencies",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
@ -121,20 +121,21 @@
}, },
"addOns/externals/dicom-microscopy-viewer": { "addOns/externals/dicom-microscopy-viewer": {
"name": "@externals/dicom-microscopy-viewer", "name": "@externals/dicom-microscopy-viewer",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"dicom-microscopy-viewer": "^0.46.1", "dicom-microscopy-viewer": "^0.46.1",
}, },
}, },
"extensions/cornerstone": { "extensions/cornerstone": {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0", "@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/polymorphic-segmentation": "^3.5.0", "@cornerstonejs/labelmap-interpolation": "^3.7.0",
"@cornerstonejs/tools": "^3.5.0", "@cornerstonejs/polymorphic-segmentation": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
@ -151,9 +152,9 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0", "@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -165,19 +166,19 @@
}, },
"extensions/cornerstone-dicom-pmap": { "extensions/cornerstone-dicom-pmap": {
"name": "@ohif/extension-cornerstone-dicom-pmap", "name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0", "@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -188,16 +189,16 @@
}, },
"extensions/cornerstone-dicom-rt": { "extensions/cornerstone-dicom-rt": {
"name": "@ohif/extension-cornerstone-dicom-rt", "name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -208,19 +209,19 @@
}, },
"extensions/cornerstone-dicom-seg": { "extensions/cornerstone-dicom-seg": {
"name": "@ohif/extension-cornerstone-dicom-seg", "name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0", "@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -231,19 +232,19 @@
}, },
"extensions/cornerstone-dicom-sr": { "extensions/cornerstone-dicom-sr": {
"name": "@ohif/extension-cornerstone-dicom-sr", "name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.5.0", "@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.5.0", "@cornerstonejs/tools": "^3.7.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121", "@ohif/extension-measurement-tracking": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -253,19 +254,19 @@
}, },
"extensions/cornerstone-dynamic-volume": { "extensions/cornerstone-dynamic-volume": {
"name": "@ohif/extension-cornerstone-dynamic-volume", "name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.5.0", "@cornerstonejs/tools": "^3.7.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -275,7 +276,7 @@
}, },
"extensions/default": { "extensions/default": {
"name": "@ohif/extension-default", "name": "@ohif/extension-default",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/calculate-suv": "^1.1.0",
@ -283,8 +284,8 @@
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicomweb-client": "^0.10.4", "dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -298,7 +299,7 @@
}, },
"extensions/dicom-microscopy": { "extensions/dicom-microscopy": {
"name": "@ohif/extension-dicom-microscopy", "name": "@ohif/extension-dicom-microscopy",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "^1.2.3",
@ -309,10 +310,10 @@
"mathjs": "^12.4.2", "mathjs": "^12.4.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -323,14 +324,14 @@
}, },
"extensions/dicom-pdf": { "extensions/dicom-pdf": {
"name": "@ohif/extension-dicom-pdf", "name": "@ohif/extension-dicom-pdf",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -340,14 +341,14 @@
}, },
"extensions/dicom-video": { "extensions/dicom-video": {
"name": "@ohif/extension-dicom-video", "name": "@ohif/extension-dicom-video",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -357,20 +358,20 @@
}, },
"extensions/measurement-tracking": { "extensions/measurement-tracking": {
"name": "@ohif/extension-measurement-tracking", "name": "@ohif/extension-measurement-tracking",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@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/react": "^3.2.2",
"xstate": "^4.10.0", "xstate": "^4.10.0",
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/core": "^3.5.0", "@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.5.0", "@cornerstonejs/tools": "^3.7.0",
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dcmjs": "*", "dcmjs": "*",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
@ -383,14 +384,14 @@
}, },
"extensions/test-extension": { "extensions/test-extension": {
"name": "@ohif/extension-test", "name": "@ohif/extension-test",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "0.38.0", "dcmjs": "0.38.0",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -400,14 +401,14 @@
}, },
"extensions/tmtv": { "extensions/tmtv": {
"name": "@ohif/extension-tmtv", "name": "@ohif/extension-tmtv",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -417,7 +418,7 @@
}, },
"modes/basic-dev-mode": { "modes/basic-dev-mode": {
"name": "@ohif/mode-basic-dev-mode", "name": "@ohif/mode-basic-dev-mode",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -427,17 +428,17 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
}, },
}, },
"modes/basic-test-mode": { "modes/basic-test-mode": {
"name": "@ohif/mode-test", "name": "@ohif/mode-test",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -447,19 +448,19 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121", "@ohif/extension-measurement-tracking": "3.10.0-beta.125",
"@ohif/extension-test": "3.10.0-beta.121", "@ohif/extension-test": "3.10.0-beta.125",
}, },
}, },
"modes/longitudinal": { "modes/longitudinal": {
"name": "@ohif/mode-longitudinal", "name": "@ohif/mode-longitudinal",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -469,32 +470,32 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121", "@ohif/extension-measurement-tracking": "3.10.0-beta.125",
}, },
}, },
"modes/microscopy": { "modes/microscopy": {
"name": "@ohif/mode-microscopy", "name": "@ohif/mode-microscopy",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.121", "@ohif/extension-dicom-microscopy": "3.10.0-beta.125",
}, },
}, },
"modes/preclinical-4d": { "modes/preclinical-4d": {
"name": "@ohif/mode-preclinical-4d", "name": "@ohif/mode-preclinical-4d",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
}, },
@ -503,17 +504,17 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.121", "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-tmtv": "3.10.0-beta.121", "@ohif/extension-tmtv": "3.10.0-beta.125",
}, },
}, },
"modes/segmentation": { "modes/segmentation": {
"name": "@ohif/mode-segmentation", "name": "@ohif/mode-segmentation",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -545,19 +546,19 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
}, },
}, },
"modes/tmtv": { "modes/tmtv": {
"name": "@ohif/mode-tmtv", "name": "@ohif/mode-tmtv",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -567,43 +568,43 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-measurement-tracking": "3.10.0-beta.121", "@ohif/extension-measurement-tracking": "3.10.0-beta.125",
}, },
}, },
"platform/app": { "platform/app": {
"name": "@ohif/app", "name": "@ohif/app",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0", "@cornerstonejs/dicom-image-loader": "^3.7.0",
"@emotion/serialize": "^1.1.3", "@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.121", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.121", "@ohif/extension-cornerstone": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125",
"@ohif/extension-default": "3.10.0-beta.121", "@ohif/extension-default": "3.10.0-beta.125",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.121", "@ohif/extension-dicom-microscopy": "3.10.0-beta.125",
"@ohif/extension-dicom-pdf": "3.10.0-beta.121", "@ohif/extension-dicom-pdf": "3.10.0-beta.125",
"@ohif/extension-dicom-video": "3.10.0-beta.121", "@ohif/extension-dicom-video": "3.10.0-beta.125",
"@ohif/extension-test": "3.10.0-beta.121", "@ohif/extension-test": "3.10.0-beta.125",
"@ohif/i18n": "3.10.0-beta.121", "@ohif/i18n": "3.10.0-beta.125",
"@ohif/mode-basic-dev-mode": "3.10.0-beta.121", "@ohif/mode-basic-dev-mode": "3.10.0-beta.125",
"@ohif/mode-longitudinal": "3.10.0-beta.121", "@ohif/mode-longitudinal": "3.10.0-beta.125",
"@ohif/mode-microscopy": "3.10.0-beta.121", "@ohif/mode-microscopy": "3.10.0-beta.125",
"@ohif/mode-test": "3.10.0-beta.121", "@ohif/mode-test": "3.10.0-beta.125",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"@ohif/ui-next": "3.10.0-beta.121", "@ohif/ui-next": "3.10.0-beta.125",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -644,7 +645,7 @@
}, },
"platform/cli": { "platform/cli": {
"name": "@ohif/cli", "name": "@ohif/cli",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"bin": { "bin": {
"ohif-cli": "src/index.js", "ohif-cli": "src/index.js",
}, },
@ -668,7 +669,7 @@
}, },
"platform/core": { "platform/core": {
"name": "@ohif/core", "name": "@ohif/core",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"dcmjs": "*", "dcmjs": "*",
@ -693,15 +694,15 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0", "@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/ui": "3.10.0-beta.121", "@ohif/ui": "3.10.0-beta.125",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
}, },
}, },
"platform/i18n": { "platform/i18n": {
"name": "@ohif/i18n", "name": "@ohif/i18n",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next-locize-backend": "^2.0.0", "i18next-locize-backend": "^2.0.0",
@ -726,7 +727,7 @@
}, },
"platform/ui": { "platform/ui": {
"name": "@ohif/ui", "name": "@ohif/ui",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@testing-library/react": "^13.1.0", "@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28", "browser-detect": "^0.2.28",
@ -786,13 +787,14 @@
}, },
"platform/ui-next": { "platform/ui-next": {
"name": "@ohif/ui-next", "name": "@ohif/ui-next",
"version": "3.10.0-beta.121", "version": "3.10.0-beta.125",
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-context-menu": "^2.2.4",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.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-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
@ -1093,7 +1095,7 @@
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.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=="], "@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/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=="], "@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-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=="], "@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-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-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=="], "@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-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=="], "@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-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=="], "@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=="], "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=="], "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/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/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=="], "@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/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/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=="], "@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=="], "@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-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=="], "@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=="], "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/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=="], "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=="], "@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/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=="], "@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/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/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=="], "@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=="], "@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/@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=="], "@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=="], "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=="], "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=="], "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/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/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=="], "@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=="], "@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/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=="], "@ohif/ui/postcss-loader/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
VolumeViewport, VolumeViewport,
utilities as csUtils, utilities as csUtils,
Types as CoreTypes, Types as CoreTypes,
cache,
BaseVolumeViewport, BaseVolumeViewport,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { import {
@ -14,6 +15,7 @@ import {
annotation, annotation,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import * as cornerstoneTools 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 { Types as OhifTypes, utils } from '@ohif/core';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
@ -31,6 +33,8 @@ import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores'; import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import { toolNames } from './initCornerstoneTools'; import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
const { DefaultHistoryMemo } = csUtils.HistoryMemo; const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = { const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync, imageSlice: toggleImageSliceSync,
@ -39,7 +43,6 @@ const toggleSyncFunctions = {
function commandsModule({ function commandsModule({
servicesManager, servicesManager,
extensionManager,
commandsManager, commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const { const {
@ -54,7 +57,9 @@ function commandsModule({
colorbarService, colorbarService,
hangingProtocolService, hangingProtocolService,
syncGroupService, syncGroupService,
} = servicesManager.services; segmentationService,
displaySetService,
} = servicesManager.services as AppTypes.Services;
const { measurementServiceSource } = this; const { measurementServiceSource } = this;
@ -67,7 +72,75 @@ function commandsModule({
return toolGroupService.getToolGroupForViewport(viewport.id); 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 = { 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 * Generates the selector props for the context menu, specific to
* the cornerstone viewport, and then runs the context menu. * the cornerstone viewport, and then runs the context menu.
@ -373,6 +446,44 @@ function commandsModule({
utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); 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 // Retrieve value commands
getActiveViewportEnabledElement: _getActiveViewportEnabledElement, getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
@ -1094,7 +1205,12 @@ function commandsModule({
* @param props.segmentIndex - The index of the segment to activate * @param props.segmentIndex - The index of the segment to activate
*/ */
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => { 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.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex); segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
}, },
@ -1636,6 +1752,9 @@ function commandsModule({
}, },
undo: actions.undo, undo: actions.undo,
redo: actions.redo, redo: actions.redo,
interpolateLabelmap: actions.interpolateLabelmap,
runSegmentBidirectional: actions.runSegmentBidirectional,
downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport,
}; };
return { return {

View File

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

View File

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

View File

@ -1,77 +1,10 @@
import React from 'react'; import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
import { import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
Icons,
} from '@ohif/ui-next';
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) { export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
return { return {
'panelSegmentation.customDropdownMenuContent': ({ 'panelSegmentation.customDropdownMenuContent': CustomDropdownMenuContent,
activeSegmentation, 'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader,
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.disableEditing': false, 'panelSegmentation.disableEditing': false,
'panelSegmentation.showAddSegment': true, 'panelSegmentation.showAddSegment': true,
'panelSegmentation.onSegmentationAdd': () => { 'panelSegmentation.onSegmentationAdd': () => {
@ -81,15 +14,21 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
}, },
'panelSegmentation.tableMode': 'collapsed', 'panelSegmentation.tableMode': 'collapsed',
'panelSegmentation.readableText': { 'panelSegmentation.readableText': {
lesionStats: 'Lesion Statistics', // the values will appear in this order
minValue: 'Minimum Value', min: 'Min Value',
maxValue: 'Maximum Value', minLPS: 'Min Coord',
meanValue: 'Mean Value', max: 'Max Value',
volume: 'Volume (ml)', maxLPS: 'Max Coord',
suvPeak: 'SUV Peak', mean: 'Mean Value',
suvMax: 'Maximum SUV', stdDev: 'Standard Deviation',
suvMaxIJK: 'SUV Max IJK', count: 'Voxel Count',
lesionGlyoclysisStats: 'Lesion Glycolysis', median: 'Median',
skewness: 'Skewness',
kurtosis: 'Kurtosis',
peakValue: 'Peak Value',
peakLPS: 'Peak Coord',
volume: 'Volume',
lesionGlycolysis: 'Lesion Glycolysis',
}, },
}; };
} }

View File

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

View File

@ -39,6 +39,7 @@ import { useLutPresentationStore } from './stores/useLutPresentationStore';
import { usePositionPresentationStore } from './stores/usePositionPresentationStore'; import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore'; import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities'; import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities';
import { updateSegmentationStats } from './utils/updateSegmentationStats';
const { registerColormap } = csUtilities.colormap; const { registerColormap } = csUtilities.colormap;
@ -90,6 +91,7 @@ export default async function init({
cornerstoneViewportService, cornerstoneViewportService,
hangingProtocolService, hangingProtocolService,
viewportGridService, viewportGridService,
segmentationService,
} = servicesManager.services; } = servicesManager.services;
window.services = servicesManager.services; window.services = servicesManager.services;
@ -150,8 +152,6 @@ export default async function init({
); );
}); });
// ... existing code ...
// add metadata providers // add metadata providers
metaData.addProvider( metaData.addProvider(
csUtilities.calibratedPixelSpacingMetadataProvider.get.bind( csUtilities.calibratedPixelSpacingMetadataProvider.get.bind(
@ -177,6 +177,45 @@ export default async function init({
initCineService(servicesManager); initCineService(servicesManager);
initStudyPrefetcherService(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 // When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe( hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
@ -278,35 +317,35 @@ export default async function init({
function initializeWebWorkerProgressHandler(uiNotificationService) { function initializeWebWorkerProgressHandler(uiNotificationService) {
const activeToasts = new Map(); const activeToasts = new Map();
eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => { // eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
const { progress, type, id } = detail; // const { progress, type, id } = detail;
const cacheKey = `${type}-${id}`; // const cacheKey = `${type}-${id}`;
if (progress === 0 && !activeToasts.has(cacheKey)) { // if (progress === 0 && !activeToasts.has(cacheKey)) {
const progressPromise = new Promise((resolve, reject) => { // const progressPromise = new Promise((resolve, reject) => {
activeToasts.set(cacheKey, { resolve, reject }); // activeToasts.set(cacheKey, { resolve, reject });
}); // });
uiNotificationService.show({ // uiNotificationService.show({
id: cacheKey, // id: cacheKey,
title: `${type}`, // title: `${type}`,
message: `${type}: ${progress}%`, // message: `${type}: ${progress}%`,
autoClose: false, // autoClose: false,
promise: progressPromise, // promise: progressPromise,
promiseMessages: { // promiseMessages: {
loading: `Computing...`, // loading: `Computing...`,
success: `Completed successfully`, // success: `Completed successfully`,
error: 'Web Worker failed', // error: 'Web Worker failed',
}, // },
}); // });
} else { // } else {
if (progress === 100) { // if (progress === 100) {
const { resolve } = activeToasts.get(cacheKey); // const { resolve } = activeToasts.get(cacheKey);
resolve({ progress, type }); // resolve({ progress, type });
activeToasts.delete(cacheKey); // activeToasts.delete(cacheKey);
} // }
} // }
}); // });
} }
/** /**

View File

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

View File

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

View File

@ -15,217 +15,191 @@ export default function PanelSegmentation({
servicesManager, servicesManager,
}); });
const handlers = { // Extract customization options
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');
},
};
const segmentationTableMode = customizationService.getCustomization( const segmentationTableMode = customizationService.getCustomization(
'panelSegmentation.tableMode' 'panelSegmentation.tableMode'
); ) as unknown as string;
// custom onSegmentationAdd if provided
const onSegmentationAdd = customizationService.getCustomization( const onSegmentationAdd = customizationService.getCustomization(
'panelSegmentation.onSegmentationAdd' 'panelSegmentation.onSegmentationAdd'
); );
const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing'); const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing');
const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment'); const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment');
const CustomDropdownMenuContent = customizationService.getCustomization( const CustomDropdownMenuContent = customizationService.getCustomization(
'panelSegmentation.customDropdownMenuContent' '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 exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
const { representationData, segmentationId } = segmentation; const { representationData, segmentationId } = segmentation;
const { Labelmap } = representationData; const { Labelmap } = representationData;
if (!Labelmap) { if (!Labelmap) {
return { return { segmentationId, isExportable: true };
segmentationId,
isExportable: true,
};
} }
const referencedImageIds = Labelmap.referencedImageIds; const referencedImageIds = Labelmap.referencedImageIds;
const firstImageId = referencedImageIds[0]; const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId); const instance = metaData.get('instance', firstImageId);
if (!instance) { if (!instance) {
return { return { segmentationId, isExportable: false };
segmentationId,
isExportable: false,
};
} }
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
const SeriesInstanceUID = instance.SeriesInstanceUID; const SeriesInstanceUID = instance.SeriesInstanceUID;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
const isExportable = displaySet?.isReconstructable;
return { return {
segmentationId, segmentationId,
isExportable, isExportable: displaySet?.isReconstructable,
}; };
}); });
return ( // Common props for SegmentationTable
<> const tableProps = {
<SegmentationTable disabled,
disabled={disabled} data: segmentationsWithRepresentations,
data={segmentationsWithRepresentations} mode: segmentationTableMode,
mode={segmentationTableMode} title: 'Segmentations',
title="Segmentations" exportOptions,
exportOptions={exportOptions} disableEditing,
disableEditing={disableEditing} onSegmentationAdd,
onSegmentationAdd={onSegmentationAdd} showAddSegment,
onSegmentationClick={handlers.onSegmentationClick} renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(),
onSegmentationDelete={handlers.onSegmentationDelete} ...handlers,
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 />
{segmentationTableMode === 'collapsed' ? ( const renderSegments = () => {
<SegmentationTable.Collapsed> return (
<SegmentationTable.SelectorHeader> <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 /> <CustomDropdownMenuContent />
</SegmentationTable.SelectorHeader> </SegmentationTable.Collapsed.DropdownMenu>
<SegmentationTable.Collapsed.Selector />
<SegmentationTable.Collapsed.Info />
</SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.Content>
<SegmentationTable.AddSegmentRow /> <SegmentationTable.AddSegmentRow />
<SegmentationTable.Segments /> {renderSegments()}
</SegmentationTable.Collapsed> </SegmentationTable.Collapsed.Content>
) : ( </SegmentationTable.Collapsed>
<SegmentationTable.Expanded> );
<SegmentationTable.Header> }
return (
<>
<SegmentationTable.Expanded>
<SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.DropdownMenu>
<CustomDropdownMenuContent /> <CustomDropdownMenuContent />
</SegmentationTable.Header> </SegmentationTable.Expanded.DropdownMenu>
{/* <SegmentationTable.AddSegmentRow /> */} <SegmentationTable.Expanded.Label />
<SegmentationTable.Segments /> <SegmentationTable.Expanded.Info />
</SegmentationTable.Expanded> </SegmentationTable.Expanded.Header>
)}
</SegmentationTable> <SegmentationTable.Expanded.Content>
</> <SegmentationTable.AddSegmentRow />
{renderSegments()}
</SegmentationTable.Expanded.Content>
</SegmentationTable.Expanded>
</>
);
};
return (
<SegmentationTable {...tableProps}>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{renderModeContent()}
</SegmentationTable>
); );
} }

View File

@ -92,6 +92,7 @@ class SegmentationService extends PubSubService {
}; };
private _segmentationIdToColorLUTIndexMap: Map<string, number>; private _segmentationIdToColorLUTIndexMap: Map<string, number>;
private _segmentationGroupStatsMap: Map<string, any>;
readonly servicesManager: AppTypes.ServicesManager; readonly servicesManager: AppTypes.ServicesManager;
highlightIntervalId = null; highlightIntervalId = null;
readonly EVENTS = EVENTS; readonly EVENTS = EVENTS;
@ -102,6 +103,8 @@ class SegmentationService extends PubSubService {
this._segmentationIdToColorLUTIndexMap = new Map(); this._segmentationIdToColorLUTIndexMap = new Map();
this.servicesManager = servicesManager; this.servicesManager = servicesManager;
this._segmentationGroupStatsMap = new Map();
} }
public onModeEnter(): void { public onModeEnter(): void {
@ -1058,6 +1061,33 @@ class SegmentationService extends PubSubService {
public getRenderInactiveSegmentations(viewportId: string): boolean { public getRenderInactiveSegmentations(viewportId: string): boolean {
return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId); 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. * Toggles the visibility of a segmentation in the state, and broadcasts the event.

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import SplineROI from './SplineROI';
import LivewireContour from './LivewireContour'; import LivewireContour from './LivewireContour';
import Probe from './Probe'; import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional'; import UltrasoundDirectional from './UltrasoundDirectional';
import SegmentBidirectional from './SegmentBidirectional';
const measurementServiceMappingsFactory = ( const measurementServiceMappingsFactory = (
measurementService: MeasurementService, measurementService: MeasurementService,
@ -48,6 +49,7 @@ const measurementServiceMappingsFactory = (
LivewireContour: POLYLINE, LivewireContour: POLYLINE,
Probe: POINT, Probe: POINT,
UltrasoundDirectional: POLYLINE, UltrasoundDirectional: POLYLINE,
SegmentBidirectional: BIDIRECTIONAL,
}; };
return TOOL_TYPE_TO_VALUE_TYPE[toolType]; 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: { EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation, toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation => toMeasurement: csToolsAnnotation =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,20 +24,7 @@ const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
const workerManager = getWebWorkerManager(); const workerManager = getWebWorkerManager();
const options = {
maxWorkerInstances: 1,
autoTerminateOnIdle: {
enabled: true,
idleTimeThreshold: 3000,
},
};
// Register the task // 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) { function getVolumesFromSegmentation(segmentationId) {
const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId); const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId);
@ -262,153 +249,22 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
{ overwrite: true, segmentIndex, segmentationId } { 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 }) => { calculateTMTV: async ({ segmentations }) => {
const labelmapProps = segmentations.map(segmentation => { const segmentationIds = segmentations.map(segmentation => segmentation.segmentationId);
const labelmap = getLabelmapVolumeFromSegmentation(segmentation);
return { const stats = await csTools.utilities.segmentation.computeMetabolicStats({
dimensions: labelmap.dimensions, segmentationIds,
spacing: labelmap.spacing, segmentIndex: 1,
scalarData: labelmap.voxelManager.getCompleteScalarDataArray(),
origin: labelmap.origin,
direction: labelmap.direction,
};
}); });
if (!labelmapProps.length) { segmentationService.setSegmentationGroupStats(segmentationIds, stats);
return; return stats;
}
const tmtv = await workerManager.executeTask(
'suv-peak-worker',
'calculateTMTV',
labelmapProps
);
return tmtv;
}, },
exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => { exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => {
const segReport = commandsManager.runCommand('getSegmentationCSVReport', { const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
segmentations, segmentations,
}); });
const tlg = await actions.getTotalLesionGlycolysis({ segmentations });
const additionalReportRows = [ const additionalReportRows = [
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } }, { key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
{ key: 'Threshold Configuration', value: { ...config } }, { key: 'Threshold Configuration', value: { ...config } },
@ -423,35 +279,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
createAndDownloadTMTVReport(segReport, additionalReportRows, options); 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: () => { setStartSliceForROIThresholdTool: () => {
const { viewport } = _getActiveViewportsEnabledElement(); const { viewport } = _getActiveViewportsEnabledElement();
const { focalPoint } = viewport.getCamera(); const { focalPoint } = viewport.getCamera();
@ -651,12 +479,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
getTotalLesionGlycolysis: { getTotalLesionGlycolysis: {
commandFn: actions.getTotalLesionGlycolysis, commandFn: actions.getTotalLesionGlycolysis,
}, },
calculateSuvPeak: {
commandFn: actions.calculateSuvPeak,
},
getLesionStats: {
commandFn: actions.getLesionStats,
},
calculateTMTV: { calculateTMTV: {
commandFn: actions.calculateTMTV, commandFn: actions.calculateTMTV,
}, },

View File

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

View File

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

View File

@ -1,72 +1,12 @@
import { Segment, Segmentation } from '@cornerstonejs/tools/types';
import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core';
export const handleROIThresholding = async ({ export const handleROIThresholding = async ({
segmentationId,
commandsManager, commandsManager,
segmentationService, segmentationService,
}: withAppTypes<{ }: withAppTypes<{
segmentationId: string; 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 segmentations = segmentationService.getSegmentations();
const tmtv = await commandsManager.run('calculateTMTV', { segmentations }); 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 // 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 // value but we don't have any other way to display it for now
// Update all segmentations with the calculated TMTV // Update all segmentations with the calculated TMTV
@ -76,22 +16,6 @@ export const handleROIThresholding = async ({
tmtv, tmtv,
}; };
// Update each segment within the segmentation segmentationService.addOrUpdateSegmentation(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);
}); });
}; };

View File

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

View File

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

View File

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

View File

@ -255,7 +255,7 @@ const toolbarButtons: Button[] = [
{ {
id: 'Brush', id: 'Brush',
uiType: 'ohif.toolButton', uiType: 'ohif.toolBoxButton',
props: { props: {
icon: 'icon-tool-brush', icon: 'icon-tool-brush',
label: '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', id: 'Eraser',
uiType: 'ohif.toolButton', uiType: 'ohif.toolBoxButton',
props: { props: {
icon: 'icon-tool-eraser', icon: 'icon-tool-eraser',
label: 'Eraser', label: 'Eraser',
@ -332,7 +362,7 @@ const toolbarButtons: Button[] = [
}, },
{ {
id: 'Threshold', id: 'Threshold',
uiType: 'ohif.toolButton', uiType: 'ohif.toolBoxButton',
props: { props: {
icon: 'icon-tool-threshold', icon: 'icon-tool-threshold',
label: 'Threshold Tool', label: 'Threshold Tool',

View File

@ -54,7 +54,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0", "@cornerstonejs/dicom-image-loader": "^3.7.0",
"@emotion/serialize": "^1.1.3", "@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.125", "@ohif/core": "3.10.0-beta.125",
"@ohif/extension-cornerstone": "3.10.0-beta.125", "@ohif/extension-cornerstone": "3.10.0-beta.125",

View File

@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.5.0", "@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/ui": "3.10.0-beta.125", "@ohif/ui": "3.10.0-beta.125",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21" "dicom-parser": "^1.8.21"

View File

@ -585,7 +585,11 @@ export default class ToolbarService extends PubSubService {
}); });
props.evaluate = args => { 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) => { const mergedResult = results.reduce((acc, result) => {
return { return {
...acc, ...acc,
@ -593,6 +597,10 @@ export default class ToolbarService extends PubSubService {
}; };
}, {}); }, {});
if (hasDisabledButton) {
mergedResult.disabled = true;
}
return mergedResult; return mergedResult;
}; };

View File

@ -1,4 +1,5 @@
import guid from '../../utils/guid'; import guid from '../../utils/guid';
import debounce from '../../utils/debounce';
/** /**
* Consumer must implement: * Consumer must implement:
@ -92,14 +93,14 @@ function _broadcastEvent(eventName, callbackProps) {
/** Export a PubSubService class to be used instead of the individual items */ /** Export a PubSubService class to be used instead of the individual items */
export class PubSubService { export class PubSubService {
EVENTS: any; EVENTS: Record<string, string>;
subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any }; subscribe: (eventName: string, callback: (data: unknown) => void) => { unsubscribe: () => void };
_broadcastEvent: (eventName: string, callbackProps: any) => void; _broadcastEvent: (eventName: string, callbackProps: unknown) => void;
_unsubscribe: (eventName: string, listenerId: string) => void; _unsubscribe: (eventName: string, listenerId: string) => void;
_isValidEvent: (eventName: string) => boolean; _isValidEvent: (eventName: string) => boolean;
listeners: {}; listeners: Record<string, Array<{ id: string; callback: (data: unknown) => void }> | undefined>;
unsubscriptions: any[]; unsubscriptions: Array<() => void>;
constructor(EVENTS) { constructor(EVENTS: Record<string, string>) {
this.EVENTS = EVENTS; this.EVENTS = EVENTS;
this.subscribe = subscribe; this.subscribe = subscribe;
this._broadcastEvent = _broadcastEvent; this._broadcastEvent = _broadcastEvent;
@ -109,6 +110,27 @@ export class PubSubService {
this.unsubscriptions = []; 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() { reset() {
this.unsubscriptions.forEach(unsub => unsub()); this.unsubscriptions.forEach(unsub => unsub());
this.unsubscriptions = []; this.unsubscriptions = [];
@ -120,7 +142,9 @@ export class PubSubService {
* Check eventData.isConsumed to see if it is consumed or not. * Check eventData.isConsumed to see if it is consumed or not.
* @param props - to include in the event * @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 { return {
...props, ...props,
isConsumed: false, isConsumed: false,

View File

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

View File

@ -37,6 +37,11 @@ import { Input } from '../../../../ui-next/src/components/Input';
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
import {
HoverCard,
HoverCardTrigger,
HoverCardContent,
} from '../../../../ui-next/src/components/HoverCard';
interface DataItem { interface DataItem {
id: number; id: number;
@ -44,8 +49,29 @@ interface DataItem {
description: string; description: string;
optionalField?: string; optionalField?: string;
colorHex?: string; colorHex?: string;
details?: string; details?: { primary: string[]; secondary: string[] };
series?: 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 { interface ListGroup {
@ -67,13 +93,189 @@ export default function SegmentationPanel() {
}; };
const organSegmentationGroup = dataList.find( const organSegmentationGroup = dataList.find(
(listGroup: ListGroup) => listGroup.type === 'Organ Segmentation' (listGroup: any) => listGroup.type === 'Organ Segmentation'
); ) as unknown as ListGroup;
if (!organSegmentationGroup) { if (!organSegmentationGroup) {
return <div className="text-red-500">Organ Segmentation data not found.</div>; 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 ( return (
<div className="px-auto flex min-h-screen w-full justify-center bg-black py-12"> <div className="px-auto flex min-h-screen w-full justify-center bg-black py-12">
<div className="w-64 space-y-0"> <div className="w-64 space-y-0">
@ -284,20 +486,46 @@ export default function SegmentationPanel() {
{organSegmentationGroup.items.map((item, index) => { {organSegmentationGroup.items.map((item, index) => {
const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID
return ( return (
<DataRow <HoverCard
key={`panel-${compositeId}`} // Prefix to ensure uniqueness key={`hover-${compositeId}`}
number={index + 1} openDelay={300}
title={item.title} closeDelay={200}
description={item.description} // open={true}
optionalField={item.optionalField} >
colorHex={item.colorHex} <HoverCardTrigger asChild>
details={item.details} <div>
series={item.series} <DataRow
actionOptions={actionOptionsMap[organSegmentationGroup.type] || ['Action']} key={`panel-${compositeId}`} // Prefix to ensure uniqueness
onAction={(action: string) => handleAction(compositeId, action)} number={index + 1}
isSelected={selectedRowId === compositeId} title={item.title}
onSelect={() => handleRowSelect(compositeId)} 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> </div>

View File

@ -118,84 +118,378 @@ const dataList = [
title: 'Spleen', title: 'Spleen',
description: 'Description for Spleen.', description: 'Description for Spleen.',
colorHex: '#6B8E23', 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, id: 16,
title: 'Kidney', title: 'Kidney',
description: 'Description for Kidney.', description: 'Description for Kidney.',
colorHex: '#4682B4', 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, id: 17,
title: 'Kidney very long title name lorem ipsum dolor sit amet segmentation', title: 'Kidney very long title name lorem ipsum dolor sit amet segmentation',
description: 'Description for Kidney.', description: 'Description for Kidney.',
colorHex: '#9ACD32', 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, id: 18,
title: 'Gallbladder', title: 'Gallbladder',
description: 'Description for Gallbladder.', description: 'Description for Gallbladder.',
colorHex: '#20B2AA', 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, id: 19,
title: 'Esophagus', title: 'Esophagus',
description: 'Description for Esophagus.', description: 'Description for Esophagus.',
colorHex: '#DAA520', 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, id: 20,
title: 'Liver', title: 'Liver',
description: 'Description for Liver.', description: 'Description for Liver.',
colorHex: '#CD5C5C', 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, id: 21,
title: 'Stomach', title: 'Stomach',
description: 'Description for Stomach.', description: 'Description for Stomach.',
colorHex: '#778899', 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, id: 22,
title: 'Abdominal aorta', title: 'Abdominal aorta',
description: 'Description for Abdominal Aorta.', description: 'Description for Abdominal Aorta.',
colorHex: '#B8860B', 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, id: 23,
title: 'Inferior vena cava', title: 'Inferior vena cava',
description: 'Description for Inferior Vena Cava.', description: 'Description for Inferior Vena Cava.',
colorHex: '#556B2F', 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, id: 24,
title: 'Portal vein', title: 'Portal vein',
description: 'Description for Portal Vein.', description: 'Description for Portal Vein.',
colorHex: '#8B4513', 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, id: 25,
title: 'Pancreas', title: 'Pancreas',
description: 'Description for Pancreas.', description: 'Description for Pancreas.',
colorHex: '#2F4F4F', 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, id: 26,
title: 'Adrenal gland', title: 'Adrenal gland',
description: 'Description for Adrenal Gland.', description: 'Description for Adrenal Gland.',
colorHex: '#708090', 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, id: 27,
title: 'Adrenal gland', title: 'Adrenal gland',
description: 'Description for Adrenal Gland.', description: 'Description for Adrenal Gland.',
colorHex: '#6A5ACD', 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, id: 28,
title: 'New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test ', 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.', description: 'Description for New Seg Test.',
colorHex: '#4682B4', colorHex: '#4682B4',
statistics: {
centroidX: { value: -2.76, unit: 'cm' },
centroidY: { value: 4.98, unit: 'cm' },
centroidZ: { value: 121.23, unit: 'cm' },
frameDuration: { value: 'N/A', unit: 'ms' },
kurtosis: { value: -0.21, unit: '' },
max: { value: 15.12, unit: 'SUV' },
maxSlice: { value: 83, unit: '' },
mean: { value: 7.45, unit: 'SUV' },
median: { value: 7.12, unit: 'SUV' },
min: { value: 4.1, unit: 'SUV' },
regions: { value: 1, unit: '' },
skewness: { value: 0.82, unit: '' },
sphereDiameter: { value: 7.23, unit: 'cm' },
standardDeviation: { value: 2.45, unit: 'SUV' },
suvPeak: { value: 13.45, unit: 'SUV' },
total: { value: 19876.54, unit: 'SUV' },
glycolysis: { value: 1345.67, unit: 'SUV ml' },
volume: { value: 187.65, unit: 'ml' },
voxelCount: { value: 2987, unit: '' },
},
}, },
], ],
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,8 @@
import React from 'react'; import React from 'react';
import { Button, Icons } from '@ohif/ui-next'; import { Button, Icons } from '@ohif/ui-next';
import { useSegmentationTableContext } from './SegmentationTableContext'; import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
/** export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ children = null }) => {
* 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 }) => {
const { const {
activeRepresentation, activeRepresentation,
disableEditing, disableEditing,
@ -29,18 +11,32 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
onToggleSegmentationRepresentationVisibility, onToggleSegmentationRepresentationVisibility,
data, data,
showAddSegment, showAddSegment,
} = useSegmentationTableContext('SegmentationTable'); } = useSegmentationTableContext('AddSegmentRow');
const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every( // Try to get from expanded context first, then fall back to active segmentation
segment => segment?.visible !== false 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) { if (!data?.length) {
return null; return null;
} }
// Check if all segments are visible
const allSegmentsVisible = Object.values(representation?.segments || {}).every(
segment => segment?.visible !== false
);
const Icon = allSegmentsVisible ? ( const Icon = allSegmentsVisible ? (
<Icons.Hide className="h-6 w-6" /> <Icons.Hide className="h-6 w-6" />
) : ( ) : (
@ -50,14 +46,14 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
const allowAddSegment = showAddSegment && !disableEditing; const allowAddSegment = showAddSegment && !disableEditing;
return ( 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"> <div className="flex-1">
{allowAddSegment ? ( {allowAddSegment ? (
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="pr pl-0.5" className="pr pl-0.5"
onClick={() => onSegmentAdd(segmentationIdToUse)} onClick={() => onSegmentAdd(segmentationId)}
> >
<Icons.Add /> <Icons.Add />
Add Segment Add Segment
@ -68,10 +64,7 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
size="icon" size="icon"
variant="ghost" variant="ghost"
onClick={() => onClick={() =>
onToggleSegmentationRepresentationVisibility( onToggleSegmentationRepresentationVisibility(segmentationId, representation?.type)
segmentationIdToUse,
activeRepresentation?.type
)
} }
> >
{Icon} {Icon}

View File

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

View File

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

View File

@ -1,7 +1,165 @@
import React from 'react'; import 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, children = null,
}) => { }) => {
return <div>{children}</div>; const { mode, data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsed');
// Check if we should render based on mode
if (mode !== 'collapsed' || !data || data.length === 0) {
return null;
}
// Find active segmentation
const activeSegmentationInfo = data.find(
info => info.segmentation.segmentationId === activeSegmentationId
);
if (!activeSegmentationInfo) {
return null;
}
return (
<div className="space-y-0">
<PanelSection className="mb-0">
<SegmentationExpandedProvider
segmentation={activeSegmentationInfo.segmentation}
representation={activeSegmentationInfo.representation}
isActive={true}
onSegmentationClick={() => {}} // No-op since it's already the active one
>
{children}
</SegmentationExpandedProvider>
</PanelSection>
</div>
);
}; };
// Add the subcomponents to the main component
const SegmentationCollapsed = Object.assign(SegmentationCollapsedRoot, {
Header: SegmentationCollapsedHeader,
DropdownMenu: SegmentationCollapsedDropdownMenu,
Selector: SegmentationCollapsedSelector,
Info: SegmentationCollapsedInfo,
Content: SegmentationCollapsedContent,
});
// Export the component
export { SegmentationCollapsed };

View File

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

View File

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

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { ScrollArea, DataRow } from '../../components'; 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<{ export const SegmentationSegments = ({ children = null }: { children?: React.ReactNode }) => {
segmentation?: unknown;
representation?: unknown;
}> = ({ segmentation, representation }) => {
const { const {
activeSegmentationId, activeSegmentationId,
disableEditing, disableEditing,
@ -13,37 +12,51 @@ export const SegmentationSegments: React.FC<{
onToggleSegmentVisibility, onToggleSegmentVisibility,
onToggleSegmentLock, onToggleSegmentLock,
onSegmentClick, onSegmentClick,
mode,
onSegmentEdit, onSegmentEdit,
onSegmentDelete, onSegmentDelete,
data, data,
} = useSegmentationTableContext('SegmentationTable.Segments'); } = useSegmentationTableContext('SegmentationSegments');
let segmentationToUse = segmentation; // Try to get segmentation data from expanded context first, then fall back to table context
let representationToUse = representation; let segmentation;
let segmentationIdToUse = activeSegmentationId; let representation;
if (!segmentationToUse || !representationToUse) {
const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId); try {
segmentationToUse = entry?.segmentation; // Try to use the SegmentationExpanded context if available
representationToUse = entry?.representation; const segmentationInfo = useSegmentationExpanded('SegmentationSegments');
segmentationIdToUse = entry?.segmentation.segmentationId; 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; return null;
} }
const segments = Object.values(representation.segments);
const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId;
return ( return (
<ScrollArea <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} showArrows={true}
> >
{Object.values(representationToUse.segments).map(segment => { {segments.map(segment => {
if (!segment) { if (!segment) {
return null; return null;
} }
const { segmentIndex, color, visible } = segment; const { segmentIndex, color, visible } = segment as {
const segmentFromSegmentation = segmentationToUse.segments[segmentIndex]; segmentIndex: number;
color: number[];
visible: boolean;
};
const segmentFromSegmentation = segmentation.segments[segmentIndex];
if (!segmentFromSegmentation) { if (!segmentFromSegmentation) {
return null; return null;
@ -53,27 +66,70 @@ export const SegmentationSegments: React.FC<{
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return ( return (
<DataRow <HoverCard
key={segmentIndex} key={`hover-${segmentIndex}`}
number={segmentIndex} openDelay={300}
title={label} >
details={displayText} <HoverCardTrigger asChild>
colorHex={cssColor} {/* The div here is not random, it is needed for making it work with the hover card */}
isSelected={active} <div>
isVisible={visible} <DataRow
isLocked={locked} key={segmentIndex}
disableEditing={disableEditing} number={segmentIndex}
onColor={() => onSegmentColorClick(segmentationIdToUse, segmentIndex)} title={label}
onToggleVisibility={() => details={displayText}
onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type) description={displayText}
} colorHex={cssColor}
onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)} isSelected={active}
onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)} isVisible={visible}
onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)} isLocked={locked}
onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)} 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> </ScrollArea>
); );
}; };
SegmentationSegments.displayName = 'SegmentationTable.Segments';

View File

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

View File

@ -1,20 +1,26 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, useState, Children, isValidElement } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PanelSection } from '../PanelSection'; import { PanelSection } from '../PanelSection';
import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext'; import {
SegmentationTableProvider,
SegmentationTableContextType,
ViewportSegmentationInfo,
} from './contexts';
import { SegmentationSegments } from './SegmentationSegments'; import { SegmentationSegments } from './SegmentationSegments';
import { SegmentStatistics } from './SegmentStatistics';
import { SegmentationTableConfig } from './SegmentationTableConfig'; import { SegmentationTableConfig } from './SegmentationTableConfig';
import { AddSegmentRow } from './AddSegmentRow'; import { AddSegmentRow } from './AddSegmentRow';
import { AddSegmentationRow } from './AddSegmentationRow'; import { AddSegmentationRow } from './AddSegmentationRow';
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
import { SegmentationHeader } from './SegmentationHeader'; import { SegmentationHeader } from './SegmentationHeader';
import { SegmentationCollapsed } from './SegmentationCollapsed'; import { SegmentationCollapsed } from './SegmentationCollapsed';
import { SegmentationExpanded } from './SegmentationExpanded'; import { SegmentationExpanded } from './SegmentationExpanded';
import Icons from '../Icons';
interface SegmentationTableProps extends SegmentationTableContext { // Only include props that aren't part of the context
disabled?: boolean; interface SegmentationTableProps extends Omit<SegmentationTableContextType, 'setShowConfig'> {
title?: string; title?: string;
children?: ReactNode; children?: ReactNode;
setShowConfig?: (value: boolean) => void;
} }
interface SegmentationTableComponent extends React.FC<SegmentationTableProps> { interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
@ -22,55 +28,118 @@ interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
Config: typeof SegmentationTableConfig; Config: typeof SegmentationTableConfig;
AddSegmentRow: typeof AddSegmentRow; AddSegmentRow: typeof AddSegmentRow;
AddSegmentationRow: typeof AddSegmentationRow; AddSegmentationRow: typeof AddSegmentationRow;
SelectorHeader: typeof SegmentationSelectorHeader;
Header: typeof SegmentationHeader; Header: typeof SegmentationHeader;
Collapsed: typeof SegmentationCollapsed; Collapsed: typeof SegmentationCollapsed;
Expanded: typeof SegmentationExpanded; Expanded: typeof SegmentationExpanded;
SegmentStatistics: typeof SegmentStatistics;
} }
export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => { export const SegmentationTableRoot = (props: SegmentationTableProps) => {
const { t } = useTranslation('SegmentationTable'); 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 activeSegmentationInfo = data.find(info => info.representation?.active);
const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId; // Get the active segmentation ID
const activeRepresentation = activeSegmentationInfo?.representation; const activeSegmentationId =
const activeSegmentation = activeSegmentationInfo?.segmentation; props.activeSegmentationId || activeSegmentationInfo?.segmentation?.segmentationId;
const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } = const activeRepresentation = props.activeRepresentation || activeSegmentationInfo?.representation;
activeRepresentation?.styles ?? {}; 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 ( return (
<SegmentationTableProvider <SegmentationTableProvider
data={data} value={{
mode={mode} data,
disabled={disabled} mode,
disableEditing={disableEditing} showConfig,
fillAlpha={fillAlpha} disabled,
fillAlphaInactive={fillAlphaInactive} disableEditing,
outlineWidth={outlineWidth} fillAlpha,
renderFill={renderFill} fillAlphaInactive,
renderOutline={renderOutline} outlineWidth,
activeSegmentationId={activeSegmentationId} renderFill,
activeSegmentation={activeSegmentation} renderOutline,
activeRepresentation={activeRepresentation} activeSegmentationId,
{...contextProps} activeSegmentation,
activeRepresentation,
...contextProps,
setShowConfig: toggleShowConfig,
}}
> >
<PanelSection defaultOpen={true}> <PanelSection defaultOpen={true}>
<PanelSection.Header> <PanelSection.Header className="flex items-center justify-between">
<span>{t(title)}</span> <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.Header>
<PanelSection.Content>{children}</PanelSection.Content> <PanelSection.Content>{processedChildren}</PanelSection.Content>
</PanelSection> </PanelSection>
</SegmentationTableProvider> </SegmentationTableProvider>
); );
}; };
SegmentationTable.Segments = SegmentationSegments; const SegmentationTable = Object.assign(SegmentationTableRoot, {
SegmentationTable.Config = SegmentationTableConfig; Segments: SegmentationSegments,
SegmentationTable.AddSegmentRow = AddSegmentRow; Config: SegmentationTableConfig,
SegmentationTable.AddSegmentationRow = AddSegmentationRow; AddSegmentRow: AddSegmentRow,
SegmentationTable.SelectorHeader = SegmentationSelectorHeader; AddSegmentationRow: AddSegmentationRow,
SegmentationTable.Header = SegmentationHeader; Collapsed: SegmentationCollapsed,
SegmentationTable.Collapsed = SegmentationCollapsed; Expanded: SegmentationExpanded,
SegmentationTable.Expanded = SegmentationExpanded; SegmentStatistics: SegmentStatistics,
Header: SegmentationHeader,
}) as SegmentationTableComponent;
export { SegmentationTable };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ function ViewportPane({
onScroll={onInteractionHandler} onScroll={onInteractionHandler}
onWheel={onInteractionHandler} onWheel={onInteractionHandler}
className={classNames( 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 className
)} )}
style={customStyle} style={customStyle}

View File

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

View File

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

View File

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

View File

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

162
yarn.lock
View File

@ -1393,10 +1393,10 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^3.5.0": "@cornerstonejs/adapters@^3.7.0":
version "3.5.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.5.0.tgz#8c719bc39f4dc78466cfb1fefbb3cbf3c82666d3" resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.7.0.tgz#7e16d609e1e5035585e40d7a7d919534b03ae094"
integrity sha512-xkixGbCDS2wwAHmggx1mW0ewEjWGntbE5L1svdkmRpGBvTtEkQJbNkNG6dUiHXWZ9mICxwahyiMc7YVDdH3tfA== integrity sha512-7ROFACNjT4W8dukUoKwggHhl+tjg0rdDpYDq8rM6b0c0gO2MgYKbU55t9MoT682yIuebt81jCsAbFSRGVzf45Q==
dependencies: dependencies:
"@babel/runtime-corejs2" "^7.17.8" "@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3" buffer "^6.0.3"
@ -1429,41 +1429,50 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0"
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
"@cornerstonejs/core@^3.5.0": "@cornerstonejs/core@^3.7.0":
version "3.5.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.5.0.tgz#8e57064c347f32c8ecd28c5e2f46e95aecb5c00c" resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.7.0.tgz#d31cafc237ccc7ae621d1a41e3979a721b2fdcff"
integrity sha512-Ai4dyxZameDz+cp3nBST6KfK1WK/Ic5R6DoTFrBlph4DuK7IUradAx2N9uEydhUJTuF6606mF3VeaAdPG9Hfig== integrity sha512-eRCovUYO3JvFV3Dzuwc6EWn9bpm+fvp/x6SXvr3MwiO+LRw1o5bmZfeh0+m4+5xzvQJPbjNYiXPPEpLYGQIOqQ==
dependencies: dependencies:
"@kitware/vtk.js" "32.12.0" "@kitware/vtk.js" "32.12.1"
comlink "^4.4.1" comlink "^4.4.1"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
loglevel "^1.9.2" loglevel "^1.9.2"
"@cornerstonejs/dicom-image-loader@^3.5.0": "@cornerstonejs/dicom-image-loader@^3.7.0":
version "3.5.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.5.0.tgz#78a01ad3587d547da7ca708713b42ced75451851" resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.7.0.tgz#6446bc6f9011674b5d000b275b6b0c34efd95c2c"
integrity sha512-Jel/an3IHe68sebtRLYB3/aJ1Dggw8D11cWBiBUqzXfxGTlg1SUd3KnvU5OrgNJ+ZpNUEdwCrR3OoiU65hL6UA== integrity sha512-MSv5A7ZcAp7uVm1ofrTaQppPm1DyOAXN0BdX8a2/fMZ2Ud4OmEtk0aoUlaJ52/hQtUvfdCqfYObBos2oBIMQ1w==
dependencies: dependencies:
"@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
"@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2"
"@cornerstonejs/codec-openjph" "^2.4.5" "@cornerstonejs/codec-openjph" "^2.4.5"
comlink "^4.4.1" comlink "^4.4.1"
dicom-parser "^1.8.9"
jpeg-lossless-decoder-js "^2.1.0" jpeg-lossless-decoder-js "^2.1.0"
pako "^2.0.4" pako "^2.0.4"
uuid "^9.0.0" uuid "^9.0.0"
"@cornerstonejs/polymorphic-segmentation@^3.5.0": "@cornerstonejs/labelmap-interpolation@^3.7.0":
version "3.5.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.5.0.tgz#b97218b3140ad895b567b3e2dc8678ebf03be531" resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.7.0.tgz#00d7bbd056fd292b2ea53681259efd0b8f6e5f5f"
integrity sha512-S2zqXNKlFURPCq99fPj5CAnBO7chuVk7bIRWmBOBYQL21TnL4rzyvY+Td5pvHBal4sZ8uTs6PW72rrwGCospAQ== 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: dependencies:
"@icr/polyseg-wasm" "0.4.0" "@icr/polyseg-wasm" "0.4.0"
"@cornerstonejs/tools@^3.5.0": "@cornerstonejs/tools@^3.7.0":
version "3.5.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.5.0.tgz#cfb046cfe6095c2295e03cb66da74e675f9d60c8" resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.7.0.tgz#0df08e1e9e47ab5da103592488a3a29d567537c8"
integrity sha512-wyT7runGpPvY9dMJOUBAtxDBSqabk6Ks/SNjbTGXAIUemAYuonP+33pN4zK/4n/zFfa0gpKFP7LFc461xFK16g== integrity sha512-72lbmaAm/dnOANAUNc0OK+9aL2pFklaWBLR+kOKn8OeSh1i9RRTSEaP8zWzStaSNAcdhwQ+JOQzcMR40Eo8oNQ==
dependencies: dependencies:
"@types/offscreencanvas" "2019.7.3" "@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1" comlink "^4.4.1"
@ -2299,6 +2308,29 @@
worker-loader "3.0.8" worker-loader "3.0.8"
xmlbuilder2 "3.0.2" 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": "@lerna/child-process@7.4.2":
version "7.4.2" version "7.4.2"
resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511" resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511"
@ -3020,6 +3052,13 @@
dependencies: dependencies:
"@radix-ui/react-primitive" "2.0.1" "@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": "@radix-ui/react-checkbox@^1.1.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" 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-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "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": "@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" 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-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0" "@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": "@radix-ui/react-icons@^1.3.0":
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" 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/react-use-size" "1.1.0"
"@radix-ui/rect" "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": "@radix-ui/react-portal@1.0.3":
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" 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-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0" "@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": "@radix-ui/react-presence@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
@ -3511,6 +3600,13 @@
dependencies: dependencies:
"@radix-ui/react-slot" "1.1.1" "@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": "@radix-ui/react-roving-focus@1.1.0":
version "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" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
@ -3657,6 +3753,13 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.1" "@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": "@radix-ui/react-switch@^1.1.0":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a" 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" reflect.getprototypeof "^1.0.4"
set-function-name "^2.0.1" 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: itk-wasm@1.0.0-b.173:
version "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" resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.173.tgz#e484e1765f4205a5704f8fac205d2ecf74e726a2"