diff --git a/bun.lock b/bun.lock
index 307c2094d..82ce798af 100644
--- a/bun.lock
+++ b/bun.lock
@@ -33,7 +33,7 @@
},
"addOns/externals/devDependencies": {
"name": "@externals/devDependencies",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.12.0",
@@ -121,14 +121,14 @@
},
"addOns/externals/dicom-microscopy-viewer": {
"name": "@externals/dicom-microscopy-viewer",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1",
},
},
"extensions/cornerstone": {
"name": "@ohif/extension-cornerstone",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17",
@@ -153,8 +153,8 @@
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17",
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -166,7 +166,7 @@
},
"extensions/cornerstone-dicom-pmap": {
"name": "@ohif/extension-cornerstone-dicom-pmap",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17",
@@ -175,10 +175,10 @@
"react-color": "^2.19.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -189,16 +189,16 @@
},
"extensions/cornerstone-dicom-rt": {
"name": "@ohif/extension-cornerstone-dicom-rt",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -209,7 +209,7 @@
},
"extensions/cornerstone-dicom-seg": {
"name": "@ohif/extension-cornerstone-dicom-seg",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17",
@@ -218,10 +218,10 @@
"react-color": "^2.19.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -232,7 +232,7 @@
},
"extensions/cornerstone-dicom-sr": {
"name": "@ohif/extension-cornerstone-dicom-sr",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17",
@@ -241,10 +241,10 @@
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-measurement-tracking": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -254,7 +254,7 @@
},
"extensions/cornerstone-dynamic-volume": {
"name": "@ohif/extension-cornerstone-dynamic-volume",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.7.17",
@@ -262,11 +262,11 @@
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -276,7 +276,7 @@
},
"extensions/default": {
"name": "@ohif/extension-default",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.1.0",
@@ -284,8 +284,8 @@
"lodash.uniqby": "^4.7.0",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
"dcmjs": "*",
"dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2",
@@ -299,7 +299,7 @@
},
"extensions/dicom-microscopy": {
"name": "@ohif/extension-dicom-microscopy",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3",
@@ -310,10 +310,10 @@
"mathjs": "^12.4.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -324,14 +324,14 @@
},
"extensions/dicom-pdf": {
"name": "@ohif/extension-dicom-pdf",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -341,14 +341,14 @@
},
"extensions/dicom-video": {
"name": "@ohif/extension-dicom-video",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -358,20 +358,20 @@
},
"extensions/measurement-tracking": {
"name": "@ohif/extension-measurement-tracking",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/ui": "3.10.0-beta.142",
"@xstate/react": "^3.2.2",
"xstate": "^4.10.0",
},
"peerDependencies": {
"@cornerstonejs/core": "^3.7.17",
"@cornerstonejs/tools": "^3.7.17",
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"classnames": "^2.3.2",
"dcmjs": "*",
"lodash.debounce": "^4.0.8",
@@ -384,14 +384,14 @@
},
"extensions/test-extension": {
"name": "@ohif/extension-test",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "0.38.0",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -401,14 +401,14 @@
},
"extensions/tmtv": {
"name": "@ohif/extension-tmtv",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -418,7 +418,7 @@
},
"modes/basic-dev-mode": {
"name": "@ohif/mode-basic-dev-mode",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@@ -428,17 +428,17 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
},
},
"modes/basic-test-mode": {
"name": "@ohif/mode-test",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@@ -448,19 +448,19 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
- "@ohif/extension-measurement-tracking": "3.10.0-beta.140",
- "@ohif/extension-test": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
+ "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
+ "@ohif/extension-test": "3.10.0-beta.142",
},
},
"modes/longitudinal": {
"name": "@ohif/mode-longitudinal",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@@ -470,32 +470,32 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
- "@ohif/extension-measurement-tracking": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
+ "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
},
},
"modes/microscopy": {
"name": "@ohif/mode-microscopy",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-dicom-microscopy": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
},
},
"modes/preclinical-4d": {
"name": "@ohif/mode-preclinical-4d",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
},
@@ -504,17 +504,17 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-tmtv": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-tmtv": "3.10.0-beta.142",
},
},
"modes/segmentation": {
"name": "@ohif/mode-segmentation",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@@ -546,19 +546,19 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
},
},
"modes/tmtv": {
"name": "@ohif/mode-tmtv",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3",
@@ -568,18 +568,18 @@
"webpack-merge": "^5.7.3",
},
"peerDependencies": {
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
- "@ohif/extension-measurement-tracking": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
+ "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
},
},
"platform/app": {
"name": "@ohif/app",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3",
@@ -588,23 +588,23 @@
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17",
"@emotion/serialize": "^1.1.3",
- "@ohif/core": "3.10.0-beta.140",
- "@ohif/extension-cornerstone": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
- "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
- "@ohif/extension-default": "3.10.0-beta.140",
- "@ohif/extension-dicom-microscopy": "3.10.0-beta.140",
- "@ohif/extension-dicom-pdf": "3.10.0-beta.140",
- "@ohif/extension-dicom-video": "3.10.0-beta.140",
- "@ohif/extension-test": "3.10.0-beta.140",
- "@ohif/i18n": "3.10.0-beta.140",
- "@ohif/mode-basic-dev-mode": "3.10.0-beta.140",
- "@ohif/mode-longitudinal": "3.10.0-beta.140",
- "@ohif/mode-microscopy": "3.10.0-beta.140",
- "@ohif/mode-test": "3.10.0-beta.140",
- "@ohif/ui": "3.10.0-beta.140",
- "@ohif/ui-next": "3.10.0-beta.140",
+ "@ohif/core": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
+ "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
+ "@ohif/extension-default": "3.10.0-beta.142",
+ "@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
+ "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
+ "@ohif/extension-dicom-video": "3.10.0-beta.142",
+ "@ohif/extension-test": "3.10.0-beta.142",
+ "@ohif/i18n": "3.10.0-beta.142",
+ "@ohif/mode-basic-dev-mode": "3.10.0-beta.142",
+ "@ohif/mode-longitudinal": "3.10.0-beta.142",
+ "@ohif/mode-microscopy": "3.10.0-beta.142",
+ "@ohif/mode-test": "3.10.0-beta.142",
+ "@ohif/ui": "3.10.0-beta.142",
+ "@ohif/ui-next": "3.10.0-beta.142",
"@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.3",
"classnames": "^2.3.2",
@@ -645,7 +645,7 @@
},
"platform/cli": {
"name": "@ohif/cli",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"bin": {
"ohif-cli": "src/index.js",
},
@@ -669,7 +669,7 @@
},
"platform/core": {
"name": "@ohif/core",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"dcmjs": "*",
@@ -695,14 +695,14 @@
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17",
- "@ohif/ui": "3.10.0-beta.140",
+ "@ohif/ui": "3.10.0-beta.142",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21",
},
},
"platform/i18n": {
"name": "@ohif/i18n",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next-locize-backend": "^2.0.0",
@@ -727,7 +727,7 @@
},
"platform/ui": {
"name": "@ohif/ui",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28",
@@ -787,7 +787,7 @@
},
"platform/ui-next": {
"name": "@ohif/ui-next",
- "version": "3.10.0-beta.140",
+ "version": "3.10.0-beta.142",
"dependencies": {
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
@@ -830,13 +830,16 @@
},
},
"overrides": {
- "trim-newlines": "^5.0.0",
- "path-to-regexp": "0.1.12",
- "glob-parent": "^6.0.2",
- "rollup": "2.79.2",
"commander": "8.3.0",
- "body-parser": "1.20.3",
+ "path-to-regexp": "0.1.12",
"nth-check": "^2.1.1",
+ "trim-newlines": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "trim": "^1.0.0",
+ "package-json": "^8.1.0",
+ "sharp": "^0.32.6",
+ "rollup": "2.79.2",
+ "body-parser": "1.20.3",
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="],
diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
index 53aab398d..7c11485f1 100644
--- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
+++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx
@@ -1,12 +1,24 @@
import React, { useState } from 'react';
-import { Button, PanelSection, ButtonGroup, IconButton, InputNumber } from '@ohif/ui';
-import { Icons, Tooltip, TooltipTrigger, TooltipContent, Numeric } from '@ohif/ui-next';
+import {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ Button,
+ PanelSection,
+ Icons,
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ Numeric,
+} from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core';
-const controlClassNames = {
- sizeClassName: 'w-[58px] h-[28px]',
- arrowsDirection: 'horizontal',
- labelPosition: 'bottom',
+// Helper function to safely convert any value to uppercase string
+const toUpperCaseString = value => {
+ if (value === null || value === undefined) {
+ return '';
+ }
+ return String(value).toUpperCase();
};
const Header = ({ title, tooltip }) => (
@@ -16,18 +28,18 @@ const Header = ({ title, tooltip }) => (
{tooltip}
- {title}
+ {title}
);
@@ -51,112 +63,138 @@ const DynamicVolumeControls = ({
const [computedView, setComputedView] = useState(false);
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
+ // Wrapper for onGenerate to handle potential errors
+ const handleGenerate = () => {
+ try {
+ if (typeof onGenerate === 'function') {
+ onGenerate(computeViewMode);
+ } else {
+ console.error('onGenerate is not a function', onGenerate);
+ }
+ } catch (error) {
+ console.error('Error in onGenerate:', error);
+ }
+ };
+
return (
-
-
-
-
- {
- setComputedView(false);
- onDynamicClick?.();
+
+ Controls
+
+
+
+ {
+ const isComputed = value === 'computed';
+ setComputedView(isComputed);
+ if (!isComputed && typeof onDynamicClick === 'function') {
+ onDynamicClick();
+ }
}}
+ className="my-2 w-full"
>
- 4D
-
- {
- setComputedView(true);
- }}
- >
- Computed
-
-
-
-
-
-
-
-
- Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
- applied to the data set.
- Range Slider: Choose the numeric range of dimension groups within which the
- operation will be performed.
-
- Generate Button: Execute the chosen operation on the specified range of data.
-
- }
- />
-
- setComputeViewMode(Enums.DynamicOperatorType.SUM)}
- >
- {Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
-
- setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
- >
- {Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
-
- setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
- >
- {Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
-
-
-
-
-
-
+
+
+ 4D
+
+
+ Computed
+
+
+
- {
- onGenerate(computeViewMode);
- }}
- >
- Generate
-
-
+
+
+
+
+
+ Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to
+ be applied to the data set.
+ Range Slider: Choose the numeric range of dimension groups within which the
+ operation will be performed.
+
+ Generate Button: Execute the chosen operation on the specified range of data.
+
+ }
+ />
+ {
+ setComputeViewMode(value);
+ }}
+ className="mt-2 w-full"
+ >
+
+ {' '}
+
+ {toUpperCaseString(Enums.DynamicOperatorType.SUM)}
+
+
+ {toUpperCaseString(Enums.DynamicOperatorType.AVERAGE)}
+
+
+ {toUpperCaseString(Enums.DynamicOperatorType.SUBTRACT)}
+
+
+
+
+ {})}
+ >
+
+
+
+
+ Generate
+
+
+
);
@@ -179,7 +217,7 @@ function DimensionGroupControls({
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
return (
-
+
-
onPlayPauseChange(!isPlaying)}
+ variant="secondary"
+ size="default"
+ className="w-[58px]"
+ onClick={() => {
+ if (typeof onPlayPauseChange === 'function') {
+ onPlayPauseChange(!isPlaying);
+ }
+ }}
>
-
-
-
+
+
+
{})}
+ min={1}
+ max={numDimensionGroups || 1}
+ step={1}
+ >
+
+
+ Frame
+
+
+
+
{})}
+ min={minFps || 1}
+ max={maxFps || 30}
+ step={1}
+ >
+
+
+ FPS
+
+
);
diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
index 3810d5467..276dba05d 100644
--- a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
+++ b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
-import { PanelSection, WindowLevel } from '@ohif/ui';
+import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
@@ -20,7 +20,7 @@ const ViewportWindowLevel = ({
viewportId: string;
}>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
- const [windowLevels, setWindowLevels] = useState([]);
+ const [windowLevels, setWindowLevels] = useState
([]);
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager });
@@ -200,29 +200,41 @@ const ViewportWindowLevel = ({
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return (
-
- {windowLevels.map((windowLevel, i) => {
- if (!windowLevel.histogram) {
- return null;
- }
+
+ Window Level
+
+ {windowLevels.map((windowLevel, i) => {
+ if (!windowLevel.histogram) {
+ return null;
+ }
- return (
- handleVOIChange(windowLevel.volumeId, voi)}
- opacity={windowLevel.opacity}
- onOpacityChange={opacity =>
- handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
- }
- />
- );
- })}
+ return (
+ handleVOIChange(windowLevel.volumeId, voi)}
+ opacity={windowLevel.opacity}
+ onOpacityChange={opacity =>
+ handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
+ }
+ />
+ );
+ })}
+ {windowLevels.length === 0 && !isLoading && (
+
+ No window level data available
+
+ )}
+ {isLoading && (
+
+ Loading window level data...
+
+ )}
+
);
};
diff --git a/platform/docs/package.json b/platform/docs/package.json
index 4aad06878..3cb7bf8aa 100644
--- a/platform/docs/package.json
+++ b/platform/docs/package.json
@@ -60,6 +60,7 @@
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
+ "@radix-ui/react-hover-card":"^1.1.6",
"@svgr/webpack": "^5.5.0",
"@types/react": "^18.2.29",
"autoprefixer": "^10.4.20",
diff --git a/platform/docs/src/pages/components/NumericMetaShowcase.tsx b/platform/docs/src/pages/components/NumericMetaShowcase.tsx
index 010770864..b14b6dc88 100644
--- a/platform/docs/src/pages/components/NumericMetaShowcase.tsx
+++ b/platform/docs/src/pages/components/NumericMetaShowcase.tsx
@@ -9,6 +9,7 @@ import ShowcaseRow from './ShowcaseRow';
export default function NumericShowcase() {
const [controlledValue, setControlledValue] = useState(0);
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
+ const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
return (
@@ -87,6 +88,108 @@ export default function NumericShowcase() {
+ {/* Stepper Controls */}
+ setDimensionGroupNumber(val as number)}
+ min={1}
+ max={5}
+ step={1}
+>
+
+
+ Frame
+
+
+
+
+
+ Opacity
+
+
+
+
+
+
+ Zoom:
+
+
+ `}
+ >
+
+
setDimensionGroupNumber(val as number)}
+ min={1}
+ max={5}
+ step={1}
+ >
+
+
+ FPS
+
+ Frame
+
+
+
+
+
+ Opacity
+
+
+
+
+
+
+
+ Zoom
+
+
+
+
+
{/* Single Range Slider */}
([0, 1
+
+
+ Offset
+
+
+
+
+
([0, 1
+
+
+ Offset
+
+
+
+
{
+ if (Number.isInteger(step)) {
+ return 0;
+ }
+ const stepStr = step.toString();
+ if (stepStr.includes('.')) {
+ return stepStr.split('.')[1].length;
+ }
+ return 0;
+};
+
+// Enhanced context type - includes shared constraints
+type InputNumberContextType = {
+ value: number | string;
+ setValue: React.Dispatch>;
+ onChange: (value: number) => void;
+ // Shared constraints that can be used by subcomponents
+ constraints?: {
+ min?: number;
+ max?: number;
+ step?: number;
+ disabled?: boolean;
+ };
+};
+
+// Create context
+const InputNumberContext = React.createContext(undefined);
+
+// Hook to use context
+const useInputNumber = () => {
+ const context = React.useContext(InputNumberContext);
+ if (!context) {
+ throw new Error('useInputNumber must be used within an component');
+ }
+ return context;
+};
+
+// Simplified root component props
+export interface InputNumberProps {
+ value: number;
+ onChange: (value: number) => void;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+// The single top-level InputNumber component - much simpler now
+const InputNumber = React.forwardRef(
+ ({ value, onChange, className, children, ...props }, ref) => {
+ const [inputValue, setInputValue] = React.useState(value);
+
+ // Update internal state when prop changes
+ React.useEffect(() => {
+ setInputValue(value);
+ }, [value]);
+
+ // Context value - only core state
+ const contextValue = React.useMemo(
+ () => ({
+ value: inputValue,
+ setValue: setInputValue,
+ onChange,
+ }),
+ [inputValue, onChange]
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+
+InputNumber.displayName = 'InputNumber';
+
+// Input component with its own constraints
+export interface InputNumberInputProps
+ extends Omit, 'onChange'> {
+ min?: number;
+ max?: number;
+ step?: number;
+}
+
+const InputNumberInput = React.forwardRef(
+ ({ className, min, max, step, ...props }, ref) => {
+ // Get context which may include constraints from parent
+ const { value, setValue, onChange, constraints = {} } = useInputNumber();
+
+ // Use provided props or fall back to parent constraints or defaults
+ const effectiveMin = min ?? constraints.min ?? 0;
+ const effectiveMax = max ?? constraints.max ?? 100;
+ const effectiveStep = step ?? constraints.step ?? 1;
+ const decimalPlaces = getDecimalPlaces(effectiveStep);
+
+ // Format displayed value with proper decimal places
+ const displayValue = React.useMemo(() => {
+ if (typeof value === 'string') {
+ return value;
+ }
+ return decimalPlaces > 0 ? value.toFixed(decimalPlaces) : value.toString();
+ }, [value, decimalPlaces]);
+
+ // Handle input change with constraint awareness
+ const handleInputChange = React.useCallback(
+ (e: React.ChangeEvent) => {
+ const val = e.target.value;
+
+ // Allow empty string, minus sign, or decimal point for flexibility
+ if (val === '' || val === '-' || val === '.') {
+ setValue(val);
+ return;
+ }
+
+ const numValue = Number(val);
+ if (!isNaN(numValue)) {
+ setValue(numValue);
+ // Only call onChange if value is within boundaries
+ if (numValue >= effectiveMin && numValue <= effectiveMax) {
+ onChange(numValue);
+ }
+ }
+ },
+ [effectiveMin, effectiveMax, onChange, setValue]
+ );
+
+ // Handle blur to format and validate
+ const handleBlur = React.useCallback(() => {
+ if (typeof value === 'string') {
+ // Handle empty or partial inputs
+ if (value === '' || value === '-' || value === '.') {
+ setValue(effectiveMin);
+ onChange(effectiveMin);
+ return;
+ }
+
+ const numValue = parseFloat(value);
+ if (isNaN(numValue)) {
+ setValue(effectiveMin);
+ onChange(effectiveMin);
+ return;
+ }
+
+ // Constrain value to min/max
+ const boundedValue = Math.max(effectiveMin, Math.min(effectiveMax, numValue));
+ setValue(boundedValue);
+ onChange(boundedValue);
+ }
+ }, [value, effectiveMin, effectiveMax, onChange, setValue]);
+
+ return (
+
+ );
+ }
+);
+
+InputNumberInput.displayName = 'InputNumber.Input';
+
+// Label component with position prop
+export interface InputNumberLabelProps extends React.HTMLAttributes {
+ position?: 'left' | 'right' | 'top' | 'bottom';
+}
+
+const InputNumberLabel = React.forwardRef(
+ ({ className, position = 'left', children, ...props }, ref) => {
+ const positionClasses = {
+ left: 'mr-2',
+ right: 'ml-2',
+ top: 'mb-1',
+ bottom: 'mt-1',
+ };
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+InputNumberLabel.displayName = 'InputNumber.Label';
+
+// Container for horizontal button layout with constraints that are shared via context
+export interface InputNumberHorizontalControlsProps extends React.HTMLAttributes {
+ min?: number;
+ max?: number;
+ step?: number;
+ disabled?: boolean;
+}
+
+const InputNumberHorizontalControls = React.forwardRef<
+ HTMLDivElement,
+ InputNumberHorizontalControlsProps
+>(({ className, children, min = 0, max = 100, step = 1, disabled = false, ...props }, ref) => {
+ // Get existing context and enhance it with constraints
+ const context = useInputNumber();
+ const { value, onChange } = context;
+
+ // Set constraints in context for child components to use
+ React.useEffect(() => {
+ context.constraints = { min, max, step, disabled };
+ }, [context, min, max, step, disabled]);
+
+ // Increment function with local constraints
+ const increment = React.useCallback(() => {
+ if (disabled) {
+ return;
+ }
+
+ const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
+ const newValue = Math.min(currentValue + step, max);
+ onChange(newValue);
+ }, [value, min, max, step, onChange, disabled]);
+
+ // Decrement function with local constraints
+ const decrement = React.useCallback(() => {
+ if (disabled) {
+ return;
+ }
+
+ const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
+ const newValue = Math.max(currentValue - step, min);
+ onChange(newValue);
+ }, [value, min, max, step, onChange, disabled]);
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+});
+
+InputNumberHorizontalControls.displayName = 'InputNumber.HorizontalControls';
+
+// Vertical controls with its own constraints
+export interface InputNumberVerticalControlsProps extends React.HTMLAttributes {
+ min?: number;
+ max?: number;
+ step?: number;
+ disabled?: boolean;
+}
+
+const InputNumberVerticalControls = React.forwardRef<
+ HTMLDivElement,
+ InputNumberVerticalControlsProps
+>(({ className, children, min = 0, max = 100, step = 1, disabled = false, ...props }, ref) => {
+ // Get existing context and enhance it with constraints
+ const context = useInputNumber();
+ const { value, onChange } = context;
+
+ // Set constraints in context for child components to use
+ React.useEffect(() => {
+ context.constraints = { min, max, step, disabled };
+ }, [context, min, max, step, disabled]);
+
+ // Increment function with local constraints
+ const increment = React.useCallback(() => {
+ if (disabled) {
+ return;
+ }
+
+ const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
+ const newValue = Math.min(currentValue + step, max);
+ onChange(newValue);
+ }, [value, min, max, step, onChange, disabled]);
+
+ // Decrement function with local constraints
+ const decrement = React.useCallback(() => {
+ if (disabled) {
+ return;
+ }
+
+ const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
+ const newValue = Math.max(currentValue - step, min);
+ onChange(newValue);
+ }, [value, min, max, step, onChange, disabled]);
+
+ return (
+
+ {children}
+
+
+
+
+
+
+
+
+
+ );
+});
+
+InputNumberVerticalControls.displayName = 'InputNumber.VerticalControls';
+
+// Container with size prop
+const sizesClasses = {
+ sm: 'w-[45px] h-[28px]',
+ md: 'w-[58px] h-[28px]',
+ lg: 'w-[206px] h-[35px]',
+};
+
+export interface InputNumberContainerProps extends React.HTMLAttributes {
+ size?: 'sm' | 'md' | 'lg';
+ sizeClassName?: string;
+}
+
+const InputNumberContainer = React.forwardRef(
+ ({ className, size = 'md', sizeClassName, children, ...props }, ref) => {
+ const sizeToUse = sizeClassName || sizesClasses[size];
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+InputNumberContainer.displayName = 'InputNumber.Container';
+
+// Attach subcomponents
+Object.assign(InputNumber, {
+ Input: InputNumberInput,
+ Label: InputNumberLabel,
+ Container: InputNumberContainer,
+ HorizontalControls: InputNumberHorizontalControls,
+ VerticalControls: InputNumberVerticalControls,
+});
+
+export { InputNumber };
diff --git a/platform/ui-next/src/components/InputNumber/index.ts b/platform/ui-next/src/components/InputNumber/index.ts
new file mode 100644
index 000000000..d148bb55e
--- /dev/null
+++ b/platform/ui-next/src/components/InputNumber/index.ts
@@ -0,0 +1,3 @@
+import { InputNumber } from './InputNumber';
+export { InputNumber };
+export type { InputNumberProps } from './InputNumber';
diff --git a/platform/ui-next/src/components/Numeric/Numeric.tsx b/platform/ui-next/src/components/Numeric/Numeric.tsx
index b2cf66c68..dc1e6e081 100644
--- a/platform/ui-next/src/components/Numeric/Numeric.tsx
+++ b/platform/ui-next/src/components/Numeric/Numeric.tsx
@@ -5,9 +5,23 @@ import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
import { Slider } from '../Slider/Slider';
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
+import { Button } from '../Button/Button';
+import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
+
+// Calculate decimal places based on step
+const getDecimalPlaces = (step: number): number => {
+ if (Number.isInteger(step)) {
+ return 0;
+ }
+ const stepStr = step.toString();
+ if (stepStr.includes('.')) {
+ return stepStr.split('.')[1].length;
+ }
+ return 0;
+};
interface NumericMetaContextValue {
- mode: 'number' | 'singleRange' | 'doubleRange';
+ mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
singleValue: number;
doubleValue: [number, number];
setSingleValue: (val: number) => void;
@@ -23,7 +37,7 @@ const NumericMetaContext = createContext(null);
1) Container
---------------------------------------------------------------------------*/
interface NumericMetaContainerProps {
- mode: 'number' | 'singleRange' | 'doubleRange';
+ mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
value?: number; // for controlled single-value usage from parent
defaultValue?: number; // for uncontrolled single-value usage
values?: [number, number]; // for controlled double-range usage from parent
@@ -57,10 +71,10 @@ function NumericMetaContainer({
// Use useControllableState for both single and double values
const [internalSingleValue, setInternalSingleValue] = useControllableState({
- prop: mode === 'number' || mode === 'singleRange' ? value : undefined,
+ prop: mode === 'number' || mode === 'singleRange' || mode === 'stepper' ? value : undefined,
defaultProp: calculatedDefaultValue,
onChange: newVal => {
- if (mode === 'number' || mode === 'singleRange') {
+ if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
onChange?.(newVal);
}
},
@@ -127,7 +141,7 @@ function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelPr
let displayedValue = '';
let valueClasses = '';
- if (mode === 'number' || mode === 'singleRange') {
+ if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
displayedValue = singleValue.toString();
valueClasses = 'w-10';
} else if (mode === 'doubleRange') {
@@ -291,12 +305,177 @@ function NumberInput({ className }: NumberInputProps) {
);
}
+/* -------------------------------------------------------------------------
+ 6) NumberStepper sub-component
+---------------------------------------------------------------------------*/
+interface NumberStepperProps {
+ className?: string;
+ children?: React.ReactNode;
+ direction?: 'horizontal' | 'vertical';
+}
+
+// Modified NumberStepper component to properly position left/right controls
+function NumberStepper({ className, children, direction }: NumberStepperProps) {
+ const ctx = useContext(NumericMetaContext);
+ if (!ctx) {
+ throw new Error('NumberStepper must be used inside .');
+ }
+
+ const { mode, singleValue, setSingleValue, min, max, step } = ctx;
+ if (mode !== 'stepper') {
+ return null;
+ }
+
+ // Calculate decimal places based on step
+ const decimalPlaces = getDecimalPlaces(step);
+
+ // Format displayed value with proper decimal places
+ const displayValue = React.useMemo(() => {
+ return decimalPlaces > 0 ? singleValue.toFixed(decimalPlaces) : singleValue.toString();
+ }, [singleValue, decimalPlaces]);
+
+ const handleInputChange = (evt: React.ChangeEvent) => {
+ const val = evt.target.value;
+
+ // Allow empty string, minus sign, or decimal point for flexibility
+ if (val === '' || val === '-' || val === '.') {
+ return;
+ }
+
+ const numValue = Number(val);
+ if (!isNaN(numValue)) {
+ setSingleValue(Math.max(min, Math.min(numValue, max)));
+ }
+ };
+
+ const handleBlur = () => {
+ // Ensure value is within constraints when input loses focus
+ const boundedValue = Math.max(min, Math.min(singleValue, max));
+ if (boundedValue !== singleValue) {
+ setSingleValue(boundedValue);
+ }
+ };
+
+ // Check if children is HorizontalControls component
+ const hasHorizontalControls = direction === 'horizontal';
+
+ if (hasHorizontalControls) {
+ // We'll handle the control positioning ourselves
+ return (
+
+
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+
+
+ setSingleValue(singleValue + step)}
+ disabled={singleValue >= max}
+ className="text-primary h-3 w-5 pr-px"
+ >
+
+
+ setSingleValue(singleValue - step)}
+ disabled={singleValue <= min}
+ className="text-primary h-3 w-5 pr-px"
+ >
+
+
+
+
+ );
+}
+
+// New components for left and right controls
+function LeftControl({ min, step, value, setValue }) {
+ const decrement = useCallback(() => {
+ const newValue = Math.max(value - step, min);
+ setValue(newValue);
+ }, [value, min, step, setValue]);
+
+ return (
+
+
+
+ );
+}
+
+function RightControl({ max, step, value, setValue }) {
+ const increment = useCallback(() => {
+ const newValue = Math.min(value + step, max);
+ setValue(newValue);
+ }, [value, max, step, setValue]);
+
+ return (
+
+
+
+ );
+}
+
export const Numeric = {
Container: NumericMetaContainer,
Label: NumericMetaLabel,
SingleRange,
DoubleRange,
NumberInput,
+ NumberStepper,
};
-export default Numeric;
+export default Numeric;
\ No newline at end of file
diff --git a/platform/ui-next/src/components/OHIFPanels/WindowLevel.tsx b/platform/ui-next/src/components/OHIFPanels/WindowLevel.tsx
new file mode 100644
index 000000000..3b1ed7ed9
--- /dev/null
+++ b/platform/ui-next/src/components/OHIFPanels/WindowLevel.tsx
@@ -0,0 +1,162 @@
+import React, { useEffect, useState, useMemo } from 'react';
+import { Numeric } from '../Numeric/Numeric';
+import WindowLevelHistogram from './WindowLevelHistogram';
+import { VOI, VOIRange, Histogram, Colormap } from './types';
+
+interface WindowLevelProps {
+ title?: string;
+ step?: number;
+ histogram: Histogram;
+ voi: VOI;
+ opacity?: number;
+ showOpacitySlider?: boolean;
+ colormap?: Colormap;
+ style?: 'polygon' | 'bars';
+ fillColor?: string;
+ lineColor?: string;
+ containerClassName?: string;
+ onVOIChange?: (voi: VOI) => void;
+ onOpacityChange?: (opacity: number) => void;
+}
+
+const convertVOItoVOIRange = (voi: VOI): VOIRange => {
+ return {
+ min: voi.windowCenter - voi.windowWidth / 2,
+ max: voi.windowCenter + voi.windowWidth / 2,
+ };
+};
+
+const WindowLevel = ({
+ title,
+ step = 1,
+ histogram,
+ voi: voiProp,
+ opacity: opacityProp = 1,
+ showOpacitySlider = false,
+ colormap,
+ style = 'polygon',
+ fillColor = '#3f3f3f',
+ lineColor = '#707070',
+ containerClassName,
+ onVOIChange,
+ onOpacityChange,
+}: WindowLevelProps) => {
+ // Define range from histogram
+ const range = useMemo(
+ () => ({
+ min: histogram.range.min,
+ max: histogram.range.max,
+ }),
+ [histogram]
+ );
+
+ // Initialize VOI range from props
+ const [voiRange, setVOIRange] = useState(
+ voiProp
+ ? convertVOItoVOIRange(voiProp)
+ : {
+ min: range.min,
+ max: range.max,
+ }
+ );
+
+ // Update VOI range when props change
+ useEffect(() => {
+ if (voiProp) {
+ setVOIRange(convertVOItoVOIRange(voiProp));
+ }
+ }, [voiProp]);
+
+ // Handle VOI range changes (from slider)
+ const handleVOIRangeChange = (newValues: [number, number]) => {
+ const [min, max] = newValues;
+ if (min === voiRange.min && max === voiRange.max) {
+ return;
+ }
+
+ const windowWidth = max - min;
+ const windowCenter = min + windowWidth / 2;
+
+ setVOIRange({
+ min,
+ max,
+ });
+
+ if (onVOIChange) {
+ onVOIChange({ windowWidth, windowCenter });
+ }
+ };
+
+ // Handle opacity changes
+ const handleOpacityChange = (value: number) => {
+ if (onOpacityChange) {
+ onOpacityChange(value);
+ }
+ };
+
+ return (
+
+ {title &&
{title}
}
+
+
+ {/* Range Labels */}
+
+
+ {range.min}
+
+
+ {range.max}
+
+
+
+ {/* Histogram Visualization */}
+
+
+
+
+ {/* Window Level Range Slider */}
+
handleVOIRangeChange(values as [number, number])}
+ >
+
+
+
+ {/* Opacity Slider (optional) */}
+ {showOpacitySlider && (
+
+
+
Opacity
+
+ handleOpacityChange(value as number)}
+ >
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default WindowLevel;
diff --git a/platform/ui-next/src/components/OHIFPanels/WindowLevelHistogram.tsx b/platform/ui-next/src/components/OHIFPanels/WindowLevelHistogram.tsx
new file mode 100644
index 000000000..c2f54f088
--- /dev/null
+++ b/platform/ui-next/src/components/OHIFPanels/WindowLevelHistogram.tsx
@@ -0,0 +1,237 @@
+import React, { useLayoutEffect, useEffect, useRef } from 'react';
+import { Range, VOIRange, Histogram, Colormap } from './types';
+
+const DEFAULT_COLORMAP = {
+ Name: 'Grayscale',
+ RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
+};
+
+const clamp = (value: number, min: number, max: number) => Math.min(Math.max(min, value), max);
+
+const interpolateVec3 = (a: number[], b: number[], t: number) => {
+ return [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t, a[2] * (1 - t) + b[2] * t];
+};
+
+const drawBackground = (canvas: HTMLCanvasElement, range: Range, voiRange: VOIRange, colormap: Colormap) => {
+ const context = canvas.getContext('2d');
+ if (!context) return;
+
+ const { width, height } = canvas;
+ const windowWidth = voiRange.max - voiRange.min;
+ const rgbPoints = colormap.RGBPoints;
+ const colorsCount = rgbPoints.length / 4;
+
+ const getColorPoint = (index: number) => {
+ const offset = 4 * index;
+
+ return index < colorsCount
+ ? {
+ index,
+ position: rgbPoints[offset],
+ color: [rgbPoints[offset + 1], rgbPoints[offset + 2], rgbPoints[offset + 3]],
+ }
+ : undefined;
+ };
+
+ let leftColorPoint = undefined;
+ let rightColorPoint = getColorPoint(0);
+
+ const incRawPixelValue = (range.max - range.min) / (width - 1);
+ let rawPixelValue = range.min;
+
+ for (let x = 0; x < width; x++) {
+ const tVoiRange = (rawPixelValue - voiRange.min) / windowWidth;
+
+ // Find the color in a linear way (O(n) complexity)
+ if (rightColorPoint) {
+ for (let i = rightColorPoint.index; i < colorsCount; i++) {
+ if (tVoiRange <= rightColorPoint.position) {
+ break;
+ }
+
+ leftColorPoint = rightColorPoint;
+ rightColorPoint = getColorPoint(i + 1);
+ }
+ }
+
+ let normColor;
+
+ if (!leftColorPoint) {
+ normColor = [...rightColorPoint!.color];
+ } else if (!rightColorPoint) {
+ normColor = [...leftColorPoint.color];
+ } else {
+ const tColorRange =
+ (tVoiRange - leftColorPoint.position) /
+ (rightColorPoint.position - leftColorPoint.position);
+
+ normColor = interpolateVec3(leftColorPoint.color, rightColorPoint.color, tColorRange);
+ }
+
+ const color = normColor.map(color => clamp(Math.round(color * 255), 0, 255));
+
+ context.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
+ context.fillRect(x, 0, 1, height);
+
+ rawPixelValue += incRawPixelValue;
+ }
+};
+
+/* plots the histogram bins as a polygon that traces the centers of each bin */
+const drawPolygonHistogram = (
+ canvas: HTMLCanvasElement,
+ context: CanvasRenderingContext2D,
+ histogram: Histogram,
+ options: { scale: (val: number) => number, fillColor: string, lineColor: string }
+) => {
+ const xScale = canvas.width / histogram.numBins;
+ const { scale } = options;
+ const maxVal = scale(histogram.maxBinValue);
+
+ context.beginPath();
+ context.moveTo(0, canvas.height);
+ context.lineTo(0, canvas.height - (canvas.height * scale(histogram.bins[0])) / maxVal);
+
+ let x = xScale / 2;
+ for (let bin = 0; bin < histogram.numBins; bin++) {
+ context.lineTo(x, canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal);
+ x += xScale;
+ }
+ context.lineTo(
+ canvas.width,
+ canvas.height - (canvas.height * scale(histogram.bins[histogram.numBins - 1])) / maxVal
+ );
+ context.lineTo(canvas.width, canvas.height);
+ context.lineTo(0, canvas.height);
+
+ context.closePath();
+ context.fill();
+ context.stroke();
+};
+
+/* plots the histogram bins as a series of n vertical bars (n = number of bins) */
+const drawBarHistogram = (
+ canvas: HTMLCanvasElement,
+ context: CanvasRenderingContext2D,
+ histogram: Histogram,
+ options: { scale: (val: number) => number, fillColor: string, lineColor: string }
+) => {
+ const xScale = canvas.width / histogram.numBins;
+ const { scale } = options;
+ const maxVal = scale(histogram.maxBinValue);
+
+ context.beginPath();
+
+ for (let bin = 0; bin < histogram.numBins; bin++) {
+ context.moveTo(xScale * bin, canvas.height);
+ context.lineTo(
+ xScale * bin,
+ canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal
+ );
+ }
+
+ context.closePath();
+ context.strokeStyle = options.fillColor;
+ context.lineWidth = xScale;
+ context.stroke();
+};
+
+const drawHistogram = (
+ canvas: HTMLCanvasElement,
+ histogram: Histogram,
+ options: {
+ style: 'polygon' | 'bars',
+ fillColor: string,
+ lineColor: string,
+ scale: (val: number) => number
+ }
+) => {
+ const context = canvas.getContext('2d');
+ if (!context) return;
+
+ context.fillStyle = options.fillColor;
+ context.strokeStyle = options.lineColor;
+
+ if (options.style === 'polygon') {
+ drawPolygonHistogram(canvas, context, histogram, options);
+ } else if (options.style === 'bars') {
+ drawBarHistogram(canvas, context, histogram, options);
+ } else {
+ throw new Error(`Invalid style (${options.style})`);
+ }
+};
+
+interface WindowLevelHistogramProps {
+ range: Range;
+ voiRange: VOIRange;
+ histogram: Histogram;
+ colormap?: Colormap;
+ style?: 'polygon' | 'bars';
+ fillColor?: string;
+ lineColor?: string;
+}
+
+const WindowLevelHistogram = ({
+ range,
+ voiRange,
+ histogram,
+ colormap = DEFAULT_COLORMAP,
+ style = 'polygon',
+ fillColor = '#3f3f3f',
+ lineColor = '#707070',
+}: WindowLevelHistogramProps) => {
+ const containerRef = useRef(null);
+ const canvasRef = useRef(null);
+ const minVOIPercent = ((voiRange.min - range.min) / (range.max - range.min)) * 100;
+ const maxVoiPercent = (1 - (range.max - voiRange.max) / (range.max - range.min)) * 100;
+ const background =
+ colormap.Name !== 'Grayscale'
+ ? undefined
+ : `linear-gradient(to right, #000 0%, #000 ${minVOIPercent}%, #fff ${maxVoiPercent}%, #fff 100%)`;
+
+ useLayoutEffect(() => {
+ const container = containerRef.current;
+ const canvas = canvasRef.current;
+ if (!container || !canvas) return;
+
+ canvas.width = container.offsetWidth;
+ canvas.height = container.offsetHeight;
+ }, [containerRef, canvasRef]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const context = canvas.getContext('2d');
+ if (!context) return;
+
+ const fnIdentity = (x: number) => x;
+ const options = {
+ style,
+ fillColor,
+ lineColor,
+ scale: fnIdentity,
+ };
+
+ context.clearRect(0, 0, canvas.width, canvas.height);
+
+ drawBackground(canvas, range, voiRange, colormap);
+ drawHistogram(canvas, histogram, options);
+ }, [range, voiRange, histogram, colormap, style, fillColor, lineColor]);
+
+ return (
+
+
+
+ );
+};
+
+export default WindowLevelHistogram;
\ No newline at end of file
diff --git a/platform/ui-next/src/components/OHIFPanels/index.ts b/platform/ui-next/src/components/OHIFPanels/index.ts
new file mode 100644
index 000000000..b3f850149
--- /dev/null
+++ b/platform/ui-next/src/components/OHIFPanels/index.ts
@@ -0,0 +1,3 @@
+export { default as WindowLevel } from './WindowLevel';
+export { default as WindowLevelHistogram } from './WindowLevelHistogram';
+export * from './types';
\ No newline at end of file
diff --git a/platform/ui-next/src/components/OHIFPanels/types.ts b/platform/ui-next/src/components/OHIFPanels/types.ts
new file mode 100644
index 000000000..ad5d85fed
--- /dev/null
+++ b/platform/ui-next/src/components/OHIFPanels/types.ts
@@ -0,0 +1,41 @@
+// Window-level related types
+
+export interface Range {
+ min: number;
+ max: number;
+}
+
+export interface VOIRange {
+ min: number;
+ max: number;
+}
+
+export interface VOI {
+ windowWidth: number;
+ windowCenter: number;
+}
+
+export interface Histogram {
+ bins: number[] | Int32Array;
+ numBins: number;
+ maxBin: number;
+ maxBinValue: number;
+ range: Range;
+}
+
+export interface Colormap {
+ Name: string;
+ RGBPoints: number[];
+}
+
+export interface WindowLevelData {
+ volumeId: string;
+ viewportId: string;
+ modality: string;
+ voi: VOI;
+ step: number;
+ histogram: Histogram;
+ showOpacitySlider: boolean;
+ opacity?: number;
+ colormap?: Colormap;
+}
\ No newline at end of file
diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts
index 83bbe812b..4accaf0bc 100644
--- a/platform/ui-next/src/components/index.ts
+++ b/platform/ui-next/src/components/index.ts
@@ -35,6 +35,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
import { Toggle, toggleVariants } from './Toggle';
import { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
import { Input } from './Input';
+import { InputNumber } from './InputNumber';
import { Label } from './Label';
import { Switch } from './Switch';
import { Checkbox } from './Checkbox';
@@ -59,6 +60,7 @@ import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
import Modal from './Modal/Modal';
import { FooterAction } from './FooterAction';
import { InputFilter } from './InputFilter';
+import { WindowLevel, WindowLevelHistogram } from './OHIFPanels';
import {
DropdownMenu,
@@ -154,11 +156,12 @@ export {
Calendar,
DatePickerWithRange,
Input,
+ InputNumber,
Label,
Tabs,
+ TabsContent,
TabsList,
TabsTrigger,
- TabsContent,
Separator,
Switch,
Checkbox,
@@ -245,4 +248,6 @@ export {
FooterAction,
ToolSettings,
InputFilter,
+ WindowLevel,
+ WindowLevelHistogram,
};
diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts
index cf07d6bff..73c4652e9 100644
--- a/platform/ui-next/src/index.ts
+++ b/platform/ui-next/src/index.ts
@@ -1,6 +1,5 @@
+import * as utils from './utils';
export * from './components';
export * from './contextProviders';
-import * as utils from './utils';
-
export { utils };