ui(components): Adds InputNumber component and 4d panel updates (#4874)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
parent
c4045251fb
commit
b7419fb6a1
281
bun.lock
281
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=="],
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
405
platform/ui-next/src/components/InputNumber/InputNumber.tsx
Normal file
405
platform/ui-next/src/components/InputNumber/InputNumber.tsx
Normal 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 };
|
||||
3
platform/ui-next/src/components/InputNumber/index.ts
Normal file
3
platform/ui-next/src/components/InputNumber/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { InputNumber } from './InputNumber';
|
||||
export { InputNumber };
|
||||
export type { InputNumberProps } from './InputNumber';
|
||||
@ -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;
|
||||
162
platform/ui-next/src/components/OHIFPanels/WindowLevel.tsx
Normal file
162
platform/ui-next/src/components/OHIFPanels/WindowLevel.tsx
Normal 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;
|
||||
@ -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;
|
||||
3
platform/ui-next/src/components/OHIFPanels/index.ts
Normal file
3
platform/ui-next/src/components/OHIFPanels/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as WindowLevel } from './WindowLevel';
|
||||
export { default as WindowLevelHistogram } from './WindowLevelHistogram';
|
||||
export * from './types';
|
||||
41
platform/ui-next/src/components/OHIFPanels/types.ts
Normal file
41
platform/ui-next/src/components/OHIFPanels/types.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import * as utils from './utils';
|
||||
export * from './components';
|
||||
export * from './contextProviders';
|
||||
|
||||
import * as utils from './utils';
|
||||
|
||||
export { utils };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user