ui(components): Adds InputNumber component and 4d panel updates (#4874)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukas 2025-04-01 09:30:32 -04:00 committed by GitHub
parent c4045251fb
commit b7419fb6a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1620 additions and 304 deletions

281
bun.lock
View File

@ -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=="],

View File

@ -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 }) => (
<span>
<Icons.ByName
name="info-link"
className="text-primary-active h-[14px] w-[14px]"
className="text-primary h-3 w-3"
/>
</span>
</TooltipTrigger>
<TooltipContent
sideOffset={4}
className="bg-primary-dark max-w-xs p-2 text-white"
className="max-w-xs"
>
<div>{tooltip}</div>
</TooltipContent>
</Tooltip>
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wide">{title}</span>
</div>
);
@ -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 (
<div className="flex select-none flex-col">
<PanelSection
title="Controls"
childrenClassName="space-y-4 pb-5 px-5"
>
<div className="mt-2">
<Header
title="View"
tooltip={
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
}
/>
<ButtonGroup className="mt-2 w-full">
<button
className="w-1/2"
onClick={() => {
setComputedView(false);
onDynamicClick?.();
<PanelSection defaultOpen={true}>
<PanelSection.Header>Controls</PanelSection.Header>
<PanelSection.Content className="bg-muted space-y-4 px-5 pt-2 pb-4">
<div className="mb-4">
<Header
title="View"
tooltip={
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
}
/>
<Tabs
value={computedView ? 'computed' : '4d'}
onValueChange={value => {
const isComputed = value === 'computed';
setComputedView(isComputed);
if (!isComputed && typeof onDynamicClick === 'function') {
onDynamicClick();
}
}}
className="my-2 w-full"
>
4D
</button>
<button
className="w-1/2"
onClick={() => {
setComputedView(true);
}}
>
Computed
</button>
</ButtonGroup>
</div>
<div>
<DimensionGroupControls
onPlayPauseChange={onPlayPauseChange}
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<ButtonGroup
className="mt-2 w-full"
separated={true}
>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
>
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
>
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
>
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups}
values={rangeValues}
onChange={onDoubleRangeChange}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
<TabsList className="w-full">
<TabsTrigger
value="4d"
className="w-1/2"
>
4D
</TabsTrigger>
<TabsTrigger
value="computed"
className="w-1/2"
>
Computed
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Button
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
onClick={() => {
onGenerate(computeViewMode);
}}
>
Generate
</Button>
</div>
<div>
<DimensionGroupControls
onPlayPauseChange={onPlayPauseChange}
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-3 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to
be applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<Tabs
value={String(computeViewMode)}
onValueChange={value => {
setComputeViewMode(value);
}}
className="mt-2 w-full"
>
<TabsList className="w-full gap-1">
{' '}
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUM)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUM)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.AVERAGE)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.AVERAGE)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUBTRACT)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUBTRACT)}
</TabsTrigger>
</TabsList>
</Tabs>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups || 1}
values={rangeValues || [1, numDimensionGroups || 1]}
onChange={onDoubleRangeChange || (() => {})}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</div>
<Button
variant="default"
size="sm"
className="mt-2 h-[26px] w-[115px] self-start p-0"
onClick={handleGenerate}
>
Generate
</Button>
</div>
</PanelSection.Content>
</PanelSection>
</div>
);
@ -179,7 +217,7 @@ function DimensionGroupControls({
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
return (
<div className={computedView && 'ohif-disabled'}>
<div className={computedView ? 'ohif-disabled' : ''}>
<Header
title="4D Controls"
tooltip={
@ -192,32 +230,56 @@ function DimensionGroupControls({
}
/>
<div className="mt-3 flex justify-between">
<IconButton
<Button
id="play-pause-button"
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
onClick={() => onPlayPauseChange(!isPlaying)}
variant="secondary"
size="default"
className="w-[58px]"
onClick={() => {
if (typeof onPlayPauseChange === 'function') {
onPlayPauseChange(!isPlaying);
}
}}
>
<Icons.ByName
name={getPlayPauseIconName()}
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
className="text-foreground h-[24px] w-[24px]"
/>
</IconButton>
<InputNumber
value={currentDimensionGroupNumber}
onChange={onDimensionGroupChange}
minValue={1}
maxValue={numDimensionGroups}
label="Group"
{...controlClassNames}
/>
<InputNumber
value={fps}
onChange={onFpsChange}
minValue={minFps}
maxValue={maxFps}
{...controlClassNames}
label="FPS"
/>
</Button>
<Numeric.Container
mode="stepper"
value={currentDimensionGroupNumber || 1}
onChange={onDimensionGroupChange || (() => {})}
min={1}
max={numDimensionGroups || 1}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="h-[28px] w-[58px]"
direction="horizontal"
/>
<Numeric.Label className="text-muted-foreground mt-1 text-sm">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
value={fps || 1}
onChange={onFpsChange || (() => {})}
min={minFps || 1}
max={maxFps || 30}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="h-[28px] w-[58px]"
direction="horizontal"
/>
<Numeric.Label className="text-muted-foreground mt-1 text-sm">FPS</Numeric.Label>
</div>
</Numeric.Container>
</div>
</div>
);

View File

@ -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<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager });
@ -200,29 +200,41 @@ const ViewportWindowLevel = ({
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return (
<PanelSection title="Window Level">
{windowLevels.map((windowLevel, i) => {
if (!windowLevel.histogram) {
return null;
}
<PanelSection defaultOpen={true}>
<PanelSection.Header>Window Level</PanelSection.Header>
<PanelSection.Content className="bg-muted py-1">
{windowLevels.map((windowLevel, i) => {
if (!windowLevel.histogram) {
return null;
}
return (
<WindowLevel
key={windowLevel.volumeId}
title={`${windowLevel.modality}`}
histogram={windowLevel.histogram}
voi={windowLevel.voi}
step={windowLevel.step}
showOpacitySlider={windowLevel.showOpacitySlider}
colormap={windowLevel.colormap}
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
opacity={windowLevel.opacity}
onOpacityChange={opacity =>
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
}
/>
);
})}
return (
<WindowLevel
key={windowLevel.volumeId}
histogram={windowLevel.histogram}
voi={windowLevel.voi}
step={windowLevel.step}
showOpacitySlider={windowLevel.showOpacitySlider}
colormap={windowLevel.colormap}
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
opacity={windowLevel.opacity}
onOpacityChange={opacity =>
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
}
/>
);
})}
{windowLevels.length === 0 && !isLoading && (
<div className="text-muted-foreground py-2 text-center text-sm">
No window level data available
</div>
)}
{isLoading && (
<div className="text-muted-foreground py-2 text-center text-sm">
Loading window level data...
</div>
)}
</PanelSection.Content>
</PanelSection>
);
};

View File

@ -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",

View File

@ -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 (
<div className="space-y-8">
@ -87,6 +88,108 @@ export default function NumericShowcase() {
</div>
</ShowcaseRow>
{/* Stepper Controls */}
<ShowcaseRow
title="Numeric - Stepper Controls"
description="Number input with increment/decrement controls in horizontal or vertical layout"
code={`
// For controlled component
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
<Numeric.Container
mode="stepper"
value={dimensionGroupNumber}
onChange={val => setDimensionGroupNumber(val as number)}
min={1}
max={5}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
<Numeric.Label className="mt-1">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={0}
max={100}
step={1}
defaultValue={50}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Opacity</Numeric.Label>
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-10}
max={10}
step={0.1}
defaultValue={0}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Zoom:</Numeric.Label>
<Numeric.NumberStepper className="w-[65px]" direction="vertical"/>
</div>
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="stepper"
value={dimensionGroupNumber}
onChange={val => setDimensionGroupNumber(val as number)}
min={1}
max={5}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="flex w-[78px]"
direction="horizontal"
>
<span className="text-muted-foreground text-xs">FPS</span>
</Numeric.NumberStepper>
<Numeric.Label className="mt-1">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={0}
max={100}
step={1}
defaultValue={50}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Opacity</Numeric.Label>
<Numeric.NumberStepper
className="w-[58px]"
direction="horizontal"
/>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-10}
max={10}
step={0.1}
defaultValue={0}
>
<div className="flex items-center space-x-2">
<Numeric.NumberStepper
className="w-[65px]"
direction="vertical"
/>
<Numeric.Label>Zoom</Numeric.Label>
</div>
</Numeric.Container>
</div>
</ShowcaseRow>
{/* Single Range Slider */}
<ShowcaseRow
title="Numeric - Single Range"
@ -268,6 +371,20 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
<Numeric.NumberInput />
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-5}
max={5}
step={0.5}
defaultValue={0}
>
<div className="flex items-center justify-between">
<Numeric.Label>Offset</Numeric.Label>
<Numeric.NumberStepper className="w-[58px]">
</Numeric.NumberStepper>
</div>
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={0}
@ -302,6 +419,22 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
<Numeric.NumberInput />
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-5}
max={5}
step={0.5}
defaultValue={0}
>
<div className="flex items-center justify-between">
<Numeric.Label>Offset</Numeric.Label>
<Numeric.NumberStepper
className="w-[58px]"
direction="horizontal"
/>
</div>
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={0}

View File

@ -2527,6 +2527,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-arrow@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-checkbox@^1.1.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32"
@ -2611,6 +2618,17 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dismissable-layer@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz#5e1f453296dd9ae99224a26c36851832d26cf507"
@ -2638,6 +2656,21 @@
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-hover-card@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5"
integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.5"
"@radix-ui/react-popper" "1.2.2"
"@radix-ui/react-portal" "1.1.4"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-icons@^1.3.0":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441"
@ -2718,6 +2751,22 @@
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-popper@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029"
integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
@ -2726,6 +2775,14 @@
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8"
integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
@ -2741,6 +2798,13 @@
dependencies:
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-primitive@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==
dependencies:
"@radix-ui/react-slot" "1.1.2"
"@radix-ui/react-roving-focus@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520"
@ -2829,6 +2893,13 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-slot@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-switch@^1.1.0":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec"

View File

@ -0,0 +1,405 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
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;
};
// Enhanced context type - includes shared constraints
type InputNumberContextType = {
value: number | string;
setValue: React.Dispatch<React.SetStateAction<number | string>>;
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<InputNumberContextType | undefined>(undefined);
// Hook to use context
const useInputNumber = () => {
const context = React.useContext(InputNumberContext);
if (!context) {
throw new Error('useInputNumber must be used within an <InputNumber> 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<HTMLDivElement, InputNumberProps>(
({ value, onChange, className, children, ...props }, ref) => {
const [inputValue, setInputValue] = React.useState<number | string>(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 (
<InputNumberContext.Provider value={contextValue}>
<div
ref={ref}
className={cn('flex', className)}
{...props}
>
{children}
</div>
</InputNumberContext.Provider>
);
}
);
InputNumber.displayName = 'InputNumber';
// Input component with its own constraints
export interface InputNumberInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof Input>, 'onChange'> {
min?: number;
max?: number;
step?: number;
}
const InputNumberInput = React.forwardRef<HTMLInputElement, InputNumberInputProps>(
({ 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<HTMLInputElement>) => {
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 (
<Input
ref={ref}
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className={cn(
'h-6 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none',
className
)}
{...props}
/>
);
}
);
InputNumberInput.displayName = 'InputNumber.Input';
// Label component with position prop
export interface InputNumberLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
position?: 'left' | 'right' | 'top' | 'bottom';
}
const InputNumberLabel = React.forwardRef<HTMLLabelElement, InputNumberLabelProps>(
({ className, position = 'left', children, ...props }, ref) => {
const positionClasses = {
left: 'mr-2',
right: 'ml-2',
top: 'mb-1',
bottom: 'mt-1',
};
return (
<label
ref={ref}
className={cn('text-muted-foreground text-xs', positionClasses[position], className)}
{...props}
>
{children}
</label>
);
}
);
InputNumberLabel.displayName = 'InputNumber.Label';
// Container for horizontal button layout with constraints that are shared via context
export interface InputNumberHorizontalControlsProps extends React.HTMLAttributes<HTMLDivElement> {
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 (
<div
ref={ref}
className={cn('flex flex-row items-center', className)}
{...props}
>
<Button
variant="ghost"
size="icon"
onClick={decrement}
disabled={disabled}
className="text-primary h-6 w-4 cursor-pointer p-0"
>
<ChevronLeft className="h-4 w-4" />
</Button>
{children}
<Button
variant="ghost"
size="icon"
onClick={increment}
disabled={disabled}
className="text-primary h-6 w-4 cursor-pointer p-0"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
});
InputNumberHorizontalControls.displayName = 'InputNumber.HorizontalControls';
// Vertical controls with its own constraints
export interface InputNumberVerticalControlsProps extends React.HTMLAttributes<HTMLDivElement> {
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 (
<div
ref={ref}
className={cn('flex items-center', className)}
{...props}
>
{children}
<div className="ml-1 flex flex-col">
<Button
variant="ghost"
size="icon"
onClick={increment}
disabled={disabled}
className="text-primary h-3 w-5 pr-px"
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={decrement}
disabled={disabled}
className="text-primary h-3 w-5 pr-px"
>
<ChevronDown className="h-3 w-3" />
</Button>
</div>
</div>
);
});
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<HTMLDivElement> {
size?: 'sm' | 'md' | 'lg';
sizeClassName?: string;
}
const InputNumberContainer = React.forwardRef<HTMLDivElement, InputNumberContainerProps>(
({ className, size = 'md', sizeClassName, children, ...props }, ref) => {
const sizeToUse = sizeClassName || sizesClasses[size];
return (
<div
ref={ref}
className={cn(
'bg-background border-input flex items-center rounded-md border',
sizeToUse,
className
)}
{...props}
>
{children}
</div>
);
}
);
InputNumberContainer.displayName = 'InputNumber.Container';
// Attach subcomponents
Object.assign(InputNumber, {
Input: InputNumberInput,
Label: InputNumberLabel,
Container: InputNumberContainer,
HorizontalControls: InputNumberHorizontalControls,
VerticalControls: InputNumberVerticalControls,
});
export { InputNumber };

View File

@ -0,0 +1,3 @@
import { InputNumber } from './InputNumber';
export { InputNumber };
export type { InputNumberProps } from './InputNumber';

View File

@ -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<NumericMetaContextValue | null>(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 <Numeric.Container>.');
}
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<HTMLInputElement>) => {
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 (
<div
className={cn(
'bg-background border-input flex h-[28px] items-center overflow-hidden rounded-md border',
className
)}
>
<LeftControl
min={min}
step={step}
value={singleValue}
setValue={setSingleValue}
/>
<Input
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className="h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none"
/>
{children}
<RightControl
max={max}
step={step}
value={singleValue}
setValue={setSingleValue}
/>
</div>
);
}
return (
<div
className={cn(
'bg-background border-input flex items-center overflow-hidden rounded-md border',
className
)}
>
<Input
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className={cn(
'h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none'
)}
/>
<div className="ml-1 flex flex-col">
<Button
variant="ghost"
size="icon"
onClick={() => setSingleValue(singleValue + step)}
disabled={singleValue >= max}
className="text-primary h-3 w-5 pr-px"
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setSingleValue(singleValue - step)}
disabled={singleValue <= min}
className="text-primary h-3 w-5 pr-px"
>
<ChevronDown className="h-3 w-3" />
</Button>
</div>
</div>
);
}
// 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 (
<Button
variant="ghost"
size="icon"
onClick={decrement}
className="text-primary h-full w-4 cursor-pointer p-0"
>
<ChevronLeft className="h-4 w-4" />
</Button>
);
}
function RightControl({ max, step, value, setValue }) {
const increment = useCallback(() => {
const newValue = Math.min(value + step, max);
setValue(newValue);
}, [value, max, step, setValue]);
return (
<Button
variant="ghost"
size="icon"
onClick={increment}
className="text-primary h-full w-4 cursor-pointer p-0"
>
<ChevronRight className="h-4 w-4" />
</Button>
);
}
export const Numeric = {
Container: NumericMetaContainer,
Label: NumericMetaLabel,
SingleRange,
DoubleRange,
NumberInput,
NumberStepper,
};
export default Numeric;
export default Numeric;

View File

@ -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<VOIRange>(
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 (
<div className={`text-foreground max-w-sm p-1 ${containerClassName || ''}`}>
{title && <div className="text-muted-foreground mb-1 text-sm">{title}</div>}
<div className="space-y-2 px-2">
{/* Range Labels */}
<div className="text-muted-foreground flex h-4 text-sm">
<div className="relative h-fit grow">
<span className="absolute left-0 bottom-px leading-3">{range.min}</span>
</div>
<div className="relative h-fit grow text-right">
<span className="absolute right-0 bottom-px leading-3">{range.max}</span>
</div>
</div>
{/* Histogram Visualization */}
<div className="bg-foreground h-11">
<WindowLevelHistogram
range={range}
voiRange={voiRange}
histogram={histogram}
colormap={colormap}
style={style}
fillColor={fillColor}
lineColor={lineColor}
/>
</div>
{/* Window Level Range Slider */}
<Numeric.Container
mode="doubleRange"
min={range.min}
max={range.max}
values={[voiRange.min, voiRange.max]}
step={step}
onChange={values => handleVOIRangeChange(values as [number, number])}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
{/* Opacity Slider (optional) */}
{showOpacitySlider && (
<div className="mt-2">
<div className="flex items-center justify-between gap-2 text-base">
<div className="text-xs text-white">Opacity</div>
<div className="flex-1">
<Numeric.Container
mode="singleRange"
min={0}
max={1}
step={0.05}
value={opacityProp}
onChange={value => handleOpacityChange(value as number)}
>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default WindowLevel;

View File

@ -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<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(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 (
<div
ref={containerRef}
className="h-full"
style={{ background }}
>
<canvas
ref={canvasRef}
width="1"
height="1"
></canvas>
</div>
);
};
export default WindowLevelHistogram;

View File

@ -0,0 +1,3 @@
export { default as WindowLevel } from './WindowLevel';
export { default as WindowLevelHistogram } from './WindowLevelHistogram';
export * from './types';

View File

@ -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;
}

View File

@ -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,
};

View File

@ -1,6 +1,5 @@
import * as utils from './utils';
export * from './components';
export * from './contextProviders';
import * as utils from './utils';
export { utils };