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": {
|
"addOns/externals/devDependencies": {
|
||||||
"name": "@externals/devDependencies",
|
"name": "@externals/devDependencies",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@kitware/vtk.js": "32.12.0",
|
"@kitware/vtk.js": "32.12.0",
|
||||||
@ -121,14 +121,14 @@
|
|||||||
},
|
},
|
||||||
"addOns/externals/dicom-microscopy-viewer": {
|
"addOns/externals/dicom-microscopy-viewer": {
|
||||||
"name": "@externals/dicom-microscopy-viewer",
|
"name": "@externals/dicom-microscopy-viewer",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dicom-microscopy-viewer": "^0.46.1",
|
"dicom-microscopy-viewer": "^0.46.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"extensions/cornerstone": {
|
"extensions/cornerstone": {
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.7.17",
|
"@cornerstonejs/adapters": "^3.7.17",
|
||||||
@ -153,8 +153,8 @@
|
|||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||||
"@cornerstonejs/dicom-image-loader": "^3.7.17",
|
"@cornerstonejs/dicom-image-loader": "^3.7.17",
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -166,7 +166,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-pmap": {
|
"extensions/cornerstone-dicom-pmap": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.7.17",
|
"@cornerstonejs/adapters": "^3.7.17",
|
||||||
@ -175,10 +175,10 @@
|
|||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -189,16 +189,16 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-rt": {
|
"extensions/cornerstone-dicom-rt": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -209,7 +209,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-seg": {
|
"extensions/cornerstone-dicom-seg": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.7.17",
|
"@cornerstonejs/adapters": "^3.7.17",
|
||||||
@ -218,10 +218,10 @@
|
|||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -232,7 +232,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dicom-sr": {
|
"extensions/cornerstone-dicom-sr": {
|
||||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^3.7.17",
|
"@cornerstonejs/adapters": "^3.7.17",
|
||||||
@ -241,10 +241,10 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.140",
|
"@ohif/extension-measurement-tracking": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -254,7 +254,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/cornerstone-dynamic-volume": {
|
"extensions/cornerstone-dynamic-volume": {
|
||||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/core": "^3.7.17",
|
"@cornerstonejs/core": "^3.7.17",
|
||||||
@ -262,11 +262,11 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -276,7 +276,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/default": {
|
"extensions/default": {
|
||||||
"name": "@ohif/extension-default",
|
"name": "@ohif/extension-default",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/calculate-suv": "^1.1.0",
|
"@cornerstonejs/calculate-suv": "^1.1.0",
|
||||||
@ -284,8 +284,8 @@
|
|||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicomweb-client": "^0.10.4",
|
"dicomweb-client": "^0.10.4",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -299,7 +299,7 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-microscopy": {
|
"extensions/dicom-microscopy": {
|
||||||
"name": "@ohif/extension-dicom-microscopy",
|
"name": "@ohif/extension-dicom-microscopy",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||||
@ -310,10 +310,10 @@
|
|||||||
"mathjs": "^12.4.2",
|
"mathjs": "^12.4.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -324,14 +324,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-pdf": {
|
"extensions/dicom-pdf": {
|
||||||
"name": "@ohif/extension-dicom-pdf",
|
"name": "@ohif/extension-dicom-pdf",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -341,14 +341,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/dicom-video": {
|
"extensions/dicom-video": {
|
||||||
"name": "@ohif/extension-dicom-video",
|
"name": "@ohif/extension-dicom-video",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -358,20 +358,20 @@
|
|||||||
},
|
},
|
||||||
"extensions/measurement-tracking": {
|
"extensions/measurement-tracking": {
|
||||||
"name": "@ohif/extension-measurement-tracking",
|
"name": "@ohif/extension-measurement-tracking",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@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/react": "^3.2.2",
|
||||||
"xstate": "^4.10.0",
|
"xstate": "^4.10.0",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cornerstonejs/core": "^3.7.17",
|
"@cornerstonejs/core": "^3.7.17",
|
||||||
"@cornerstonejs/tools": "^3.7.17",
|
"@cornerstonejs/tools": "^3.7.17",
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
@ -384,14 +384,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/test-extension": {
|
"extensions/test-extension": {
|
||||||
"name": "@ohif/extension-test",
|
"name": "@ohif/extension-test",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "0.38.0",
|
"dcmjs": "0.38.0",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -401,14 +401,14 @@
|
|||||||
},
|
},
|
||||||
"extensions/tmtv": {
|
"extensions/tmtv": {
|
||||||
"name": "@ohif/extension-tmtv",
|
"name": "@ohif/extension-tmtv",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -418,7 +418,7 @@
|
|||||||
},
|
},
|
||||||
"modes/basic-dev-mode": {
|
"modes/basic-dev-mode": {
|
||||||
"name": "@ohif/mode-basic-dev-mode",
|
"name": "@ohif/mode-basic-dev-mode",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -428,17 +428,17 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/basic-test-mode": {
|
"modes/basic-test-mode": {
|
||||||
"name": "@ohif/mode-test",
|
"name": "@ohif/mode-test",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -448,19 +448,19 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.140",
|
"@ohif/extension-measurement-tracking": "3.10.0-beta.142",
|
||||||
"@ohif/extension-test": "3.10.0-beta.140",
|
"@ohif/extension-test": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/longitudinal": {
|
"modes/longitudinal": {
|
||||||
"name": "@ohif/mode-longitudinal",
|
"name": "@ohif/mode-longitudinal",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -470,32 +470,32 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.140",
|
"@ohif/extension-measurement-tracking": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/microscopy": {
|
"modes/microscopy": {
|
||||||
"name": "@ohif/mode-microscopy",
|
"name": "@ohif/mode-microscopy",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-microscopy": "3.10.0-beta.140",
|
"@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/preclinical-4d": {
|
"modes/preclinical-4d": {
|
||||||
"name": "@ohif/mode-preclinical-4d",
|
"name": "@ohif/mode-preclinical-4d",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
},
|
},
|
||||||
@ -504,17 +504,17 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-tmtv": "3.10.0-beta.140",
|
"@ohif/extension-tmtv": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/segmentation": {
|
"modes/segmentation": {
|
||||||
"name": "@ohif/mode-segmentation",
|
"name": "@ohif/mode-segmentation",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -546,19 +546,19 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"modes/tmtv": {
|
"modes/tmtv": {
|
||||||
"name": "@ohif/mode-tmtv",
|
"name": "@ohif/mode-tmtv",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
@ -568,18 +568,18 @@
|
|||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.140",
|
"@ohif/extension-measurement-tracking": "3.10.0-beta.142",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"platform/app": {
|
"platform/app": {
|
||||||
"name": "@ohif/app",
|
"name": "@ohif/app",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||||
@ -588,23 +588,23 @@
|
|||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||||
"@cornerstonejs/dicom-image-loader": "^3.7.17",
|
"@cornerstonejs/dicom-image-loader": "^3.7.17",
|
||||||
"@emotion/serialize": "^1.1.3",
|
"@emotion/serialize": "^1.1.3",
|
||||||
"@ohif/core": "3.10.0-beta.140",
|
"@ohif/core": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140",
|
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
|
||||||
"@ohif/extension-default": "3.10.0-beta.140",
|
"@ohif/extension-default": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-microscopy": "3.10.0-beta.140",
|
"@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-pdf": "3.10.0-beta.140",
|
"@ohif/extension-dicom-pdf": "3.10.0-beta.142",
|
||||||
"@ohif/extension-dicom-video": "3.10.0-beta.140",
|
"@ohif/extension-dicom-video": "3.10.0-beta.142",
|
||||||
"@ohif/extension-test": "3.10.0-beta.140",
|
"@ohif/extension-test": "3.10.0-beta.142",
|
||||||
"@ohif/i18n": "3.10.0-beta.140",
|
"@ohif/i18n": "3.10.0-beta.142",
|
||||||
"@ohif/mode-basic-dev-mode": "3.10.0-beta.140",
|
"@ohif/mode-basic-dev-mode": "3.10.0-beta.142",
|
||||||
"@ohif/mode-longitudinal": "3.10.0-beta.140",
|
"@ohif/mode-longitudinal": "3.10.0-beta.142",
|
||||||
"@ohif/mode-microscopy": "3.10.0-beta.140",
|
"@ohif/mode-microscopy": "3.10.0-beta.142",
|
||||||
"@ohif/mode-test": "3.10.0-beta.140",
|
"@ohif/mode-test": "3.10.0-beta.142",
|
||||||
"@ohif/ui": "3.10.0-beta.140",
|
"@ohif/ui": "3.10.0-beta.142",
|
||||||
"@ohif/ui-next": "3.10.0-beta.140",
|
"@ohif/ui-next": "3.10.0-beta.142",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@ -645,7 +645,7 @@
|
|||||||
},
|
},
|
||||||
"platform/cli": {
|
"platform/cli": {
|
||||||
"name": "@ohif/cli",
|
"name": "@ohif/cli",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ohif-cli": "src/index.js",
|
"ohif-cli": "src/index.js",
|
||||||
},
|
},
|
||||||
@ -669,7 +669,7 @@
|
|||||||
},
|
},
|
||||||
"platform/core": {
|
"platform/core": {
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"dcmjs": "*",
|
"dcmjs": "*",
|
||||||
@ -695,14 +695,14 @@
|
|||||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||||
"@cornerstonejs/dicom-image-loader": "^3.7.17",
|
"@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",
|
"cornerstone-math": "0.1.9",
|
||||||
"dicom-parser": "^1.8.21",
|
"dicom-parser": "^1.8.21",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"platform/i18n": {
|
"platform/i18n": {
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"i18next-locize-backend": "^2.0.0",
|
"i18next-locize-backend": "^2.0.0",
|
||||||
@ -727,7 +727,7 @@
|
|||||||
},
|
},
|
||||||
"platform/ui": {
|
"platform/ui": {
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/react": "^13.1.0",
|
"@testing-library/react": "^13.1.0",
|
||||||
"browser-detect": "^0.2.28",
|
"browser-detect": "^0.2.28",
|
||||||
@ -787,7 +787,7 @@
|
|||||||
},
|
},
|
||||||
"platform/ui-next": {
|
"platform/ui-next": {
|
||||||
"name": "@ohif/ui-next",
|
"name": "@ohif/ui-next",
|
||||||
"version": "3.10.0-beta.140",
|
"version": "3.10.0-beta.142",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
@ -830,13 +830,16 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"overrides": {
|
"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",
|
"commander": "8.3.0",
|
||||||
"body-parser": "1.20.3",
|
"path-to-regexp": "0.1.12",
|
||||||
"nth-check": "^2.1.1",
|
"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": {
|
"packages": {
|
||||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="],
|
"@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="],
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button, PanelSection, ButtonGroup, IconButton, InputNumber } from '@ohif/ui';
|
import {
|
||||||
import { Icons, Tooltip, TooltipTrigger, TooltipContent, Numeric } from '@ohif/ui-next';
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
Button,
|
||||||
|
PanelSection,
|
||||||
|
Icons,
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
Numeric,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
const controlClassNames = {
|
// Helper function to safely convert any value to uppercase string
|
||||||
sizeClassName: 'w-[58px] h-[28px]',
|
const toUpperCaseString = value => {
|
||||||
arrowsDirection: 'horizontal',
|
if (value === null || value === undefined) {
|
||||||
labelPosition: 'bottom',
|
return '';
|
||||||
|
}
|
||||||
|
return String(value).toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = ({ title, tooltip }) => (
|
const Header = ({ title, tooltip }) => (
|
||||||
@ -16,18 +28,18 @@ const Header = ({ title, tooltip }) => (
|
|||||||
<span>
|
<span>
|
||||||
<Icons.ByName
|
<Icons.ByName
|
||||||
name="info-link"
|
name="info-link"
|
||||||
className="text-primary-active h-[14px] w-[14px]"
|
className="text-primary h-3 w-3"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
className="bg-primary-dark max-w-xs p-2 text-white"
|
className="max-w-xs"
|
||||||
>
|
>
|
||||||
<div>{tooltip}</div>
|
<div>{tooltip}</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
<span className="text-muted-foreground text-xs uppercase tracking-wide">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -51,112 +63,138 @@ const DynamicVolumeControls = ({
|
|||||||
const [computedView, setComputedView] = useState(false);
|
const [computedView, setComputedView] = useState(false);
|
||||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
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 (
|
return (
|
||||||
<div className="flex select-none flex-col">
|
<div className="flex select-none flex-col">
|
||||||
<PanelSection
|
<PanelSection defaultOpen={true}>
|
||||||
title="Controls"
|
<PanelSection.Header>Controls</PanelSection.Header>
|
||||||
childrenClassName="space-y-4 pb-5 px-5"
|
<PanelSection.Content className="bg-muted space-y-4 px-5 pt-2 pb-4">
|
||||||
>
|
<div className="mb-4">
|
||||||
<div className="mt-2">
|
<Header
|
||||||
<Header
|
title="View"
|
||||||
title="View"
|
tooltip={
|
||||||
tooltip={
|
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Tabs
|
||||||
<ButtonGroup className="mt-2 w-full">
|
value={computedView ? 'computed' : '4d'}
|
||||||
<button
|
onValueChange={value => {
|
||||||
className="w-1/2"
|
const isComputed = value === 'computed';
|
||||||
onClick={() => {
|
setComputedView(isComputed);
|
||||||
setComputedView(false);
|
if (!isComputed && typeof onDynamicClick === 'function') {
|
||||||
onDynamicClick?.();
|
onDynamicClick();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
className="my-2 w-full"
|
||||||
>
|
>
|
||||||
4D
|
<TabsList className="w-full">
|
||||||
</button>
|
<TabsTrigger
|
||||||
<button
|
value="4d"
|
||||||
className="w-1/2"
|
className="w-1/2"
|
||||||
onClick={() => {
|
>
|
||||||
setComputedView(true);
|
4D
|
||||||
}}
|
</TabsTrigger>
|
||||||
>
|
<TabsTrigger
|
||||||
Computed
|
value="computed"
|
||||||
</button>
|
className="w-1/2"
|
||||||
</ButtonGroup>
|
>
|
||||||
</div>
|
Computed
|
||||||
<div>
|
</TabsTrigger>
|
||||||
<DimensionGroupControls
|
</TabsList>
|
||||||
onPlayPauseChange={onPlayPauseChange}
|
</Tabs>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div>
|
||||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
<DimensionGroupControls
|
||||||
onClick={() => {
|
onPlayPauseChange={onPlayPauseChange}
|
||||||
onGenerate(computeViewMode);
|
isPlaying={isPlaying}
|
||||||
}}
|
computedView={computedView}
|
||||||
>
|
// fps
|
||||||
Generate
|
fps={fps}
|
||||||
</Button>
|
onFpsChange={onFpsChange}
|
||||||
</div>
|
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>
|
</PanelSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -179,7 +217,7 @@ function DimensionGroupControls({
|
|||||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={computedView && 'ohif-disabled'}>
|
<div className={computedView ? 'ohif-disabled' : ''}>
|
||||||
<Header
|
<Header
|
||||||
title="4D Controls"
|
title="4D Controls"
|
||||||
tooltip={
|
tooltip={
|
||||||
@ -192,32 +230,56 @@ function DimensionGroupControls({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex justify-between">
|
<div className="mt-3 flex justify-between">
|
||||||
<IconButton
|
<Button
|
||||||
id="play-pause-button"
|
id="play-pause-button"
|
||||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
variant="secondary"
|
||||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
size="default"
|
||||||
|
className="w-[58px]"
|
||||||
|
onClick={() => {
|
||||||
|
if (typeof onPlayPauseChange === 'function') {
|
||||||
|
onPlayPauseChange(!isPlaying);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icons.ByName
|
<Icons.ByName
|
||||||
name={getPlayPauseIconName()}
|
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>
|
</Button>
|
||||||
<InputNumber
|
|
||||||
value={currentDimensionGroupNumber}
|
<Numeric.Container
|
||||||
onChange={onDimensionGroupChange}
|
mode="stepper"
|
||||||
minValue={1}
|
value={currentDimensionGroupNumber || 1}
|
||||||
maxValue={numDimensionGroups}
|
onChange={onDimensionGroupChange || (() => {})}
|
||||||
label="Group"
|
min={1}
|
||||||
{...controlClassNames}
|
max={numDimensionGroups || 1}
|
||||||
/>
|
step={1}
|
||||||
<InputNumber
|
>
|
||||||
value={fps}
|
<div className="flex flex-col items-center">
|
||||||
onChange={onFpsChange}
|
<Numeric.NumberStepper
|
||||||
minValue={minFps}
|
className="h-[28px] w-[58px]"
|
||||||
maxValue={maxFps}
|
direction="horizontal"
|
||||||
{...controlClassNames}
|
/>
|
||||||
label="FPS"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
|
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import debounce from 'lodash.debounce';
|
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 { Enums, eventTarget } from '@cornerstonejs/core';
|
||||||
import { useActiveViewportDisplaySets } from '@ohif/core';
|
import { useActiveViewportDisplaySets } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
@ -20,7 +20,7 @@ const ViewportWindowLevel = ({
|
|||||||
viewportId: string;
|
viewportId: string;
|
||||||
}>): ReactElement => {
|
}>): ReactElement => {
|
||||||
const { cornerstoneViewportService } = servicesManager.services;
|
const { cornerstoneViewportService } = servicesManager.services;
|
||||||
const [windowLevels, setWindowLevels] = useState([]);
|
const [windowLevels, setWindowLevels] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const displaySets = useActiveViewportDisplaySets({ servicesManager });
|
const displaySets = useActiveViewportDisplaySets({ servicesManager });
|
||||||
|
|
||||||
@ -200,29 +200,41 @@ const ViewportWindowLevel = ({
|
|||||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
|
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection title="Window Level">
|
<PanelSection defaultOpen={true}>
|
||||||
{windowLevels.map((windowLevel, i) => {
|
<PanelSection.Header>Window Level</PanelSection.Header>
|
||||||
if (!windowLevel.histogram) {
|
<PanelSection.Content className="bg-muted py-1">
|
||||||
return null;
|
{windowLevels.map((windowLevel, i) => {
|
||||||
}
|
if (!windowLevel.histogram) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WindowLevel
|
<WindowLevel
|
||||||
key={windowLevel.volumeId}
|
key={windowLevel.volumeId}
|
||||||
title={`${windowLevel.modality}`}
|
histogram={windowLevel.histogram}
|
||||||
histogram={windowLevel.histogram}
|
voi={windowLevel.voi}
|
||||||
voi={windowLevel.voi}
|
step={windowLevel.step}
|
||||||
step={windowLevel.step}
|
showOpacitySlider={windowLevel.showOpacitySlider}
|
||||||
showOpacitySlider={windowLevel.showOpacitySlider}
|
colormap={windowLevel.colormap}
|
||||||
colormap={windowLevel.colormap}
|
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
|
||||||
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
|
opacity={windowLevel.opacity}
|
||||||
opacity={windowLevel.opacity}
|
onOpacityChange={opacity =>
|
||||||
onOpacityChange={opacity =>
|
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, 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>
|
</PanelSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@radix-ui/react-hover-card":"^1.1.6",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@types/react": "^18.2.29",
|
"@types/react": "^18.2.29",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import ShowcaseRow from './ShowcaseRow';
|
|||||||
export default function NumericShowcase() {
|
export default function NumericShowcase() {
|
||||||
const [controlledValue, setControlledValue] = useState(0);
|
const [controlledValue, setControlledValue] = useState(0);
|
||||||
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
|
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
|
||||||
|
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@ -87,6 +88,108 @@ export default function NumericShowcase() {
|
|||||||
</div>
|
</div>
|
||||||
</ShowcaseRow>
|
</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 */}
|
{/* Single Range Slider */}
|
||||||
<ShowcaseRow
|
<ShowcaseRow
|
||||||
title="Numeric - Single Range"
|
title="Numeric - Single Range"
|
||||||
@ -268,6 +371,20 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
|
|||||||
<Numeric.NumberInput />
|
<Numeric.NumberInput />
|
||||||
</Numeric.Container>
|
</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
|
<Numeric.Container
|
||||||
mode="singleRange"
|
mode="singleRange"
|
||||||
min={0}
|
min={0}
|
||||||
@ -302,6 +419,22 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
|
|||||||
<Numeric.NumberInput />
|
<Numeric.NumberInput />
|
||||||
</Numeric.Container>
|
</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
|
<Numeric.Container
|
||||||
mode="singleRange"
|
mode="singleRange"
|
||||||
min={0}
|
min={0}
|
||||||
|
|||||||
@ -2527,6 +2527,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-primitive" "2.0.1"
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
|
||||||
|
integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
|
||||||
"@radix-ui/react-checkbox@^1.1.1":
|
"@radix-ui/react-checkbox@^1.1.1":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32"
|
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-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer@1.1.5":
|
||||||
|
version "1.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
|
||||||
|
integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@^2.1.1":
|
"@radix-ui/react-dropdown-menu@^2.1.1":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz#5e1f453296dd9ae99224a26c36851832d26cf507"
|
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-primitive" "2.0.1"
|
||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card@^1.1.6":
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5"
|
||||||
|
integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-context" "1.1.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.5"
|
||||||
|
"@radix-ui/react-popper" "1.2.2"
|
||||||
|
"@radix-ui/react-portal" "1.1.4"
|
||||||
|
"@radix-ui/react-presence" "1.1.2"
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-icons@^1.3.0":
|
"@radix-ui/react-icons@^1.3.0":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441"
|
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/react-use-size" "1.1.0"
|
||||||
"@radix-ui/rect" "1.1.0"
|
"@radix-ui/rect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-popper@1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029"
|
||||||
|
integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.0.0"
|
||||||
|
"@radix-ui/react-arrow" "1.1.2"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-context" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-rect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-size" "1.1.0"
|
||||||
|
"@radix-ui/rect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-portal@1.1.3":
|
"@radix-ui/react-portal@1.1.3":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
|
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-primitive" "2.0.1"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8"
|
||||||
|
integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.1.2":
|
"@radix-ui/react-presence@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
||||||
@ -2741,6 +2798,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-slot" "1.1.1"
|
"@radix-ui/react-slot" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@2.0.2":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
|
||||||
|
integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.1.1":
|
"@radix-ui/react-roving-focus@1.1.1":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520"
|
||||||
@ -2829,6 +2893,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-compose-refs" "1.1.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-slot@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
|
||||||
|
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-switch@^1.1.0":
|
"@radix-ui/react-switch@^1.1.0":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec"
|
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 { Input } from '../Input/Input';
|
||||||
import { Slider } from '../Slider/Slider';
|
import { Slider } from '../Slider/Slider';
|
||||||
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
|
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 {
|
interface NumericMetaContextValue {
|
||||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
|
||||||
singleValue: number;
|
singleValue: number;
|
||||||
doubleValue: [number, number];
|
doubleValue: [number, number];
|
||||||
setSingleValue: (val: number) => void;
|
setSingleValue: (val: number) => void;
|
||||||
@ -23,7 +37,7 @@ const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
|||||||
1) Container
|
1) Container
|
||||||
---------------------------------------------------------------------------*/
|
---------------------------------------------------------------------------*/
|
||||||
interface NumericMetaContainerProps {
|
interface NumericMetaContainerProps {
|
||||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
|
||||||
value?: number; // for controlled single-value usage from parent
|
value?: number; // for controlled single-value usage from parent
|
||||||
defaultValue?: number; // for uncontrolled single-value usage
|
defaultValue?: number; // for uncontrolled single-value usage
|
||||||
values?: [number, number]; // for controlled double-range usage from parent
|
values?: [number, number]; // for controlled double-range usage from parent
|
||||||
@ -57,10 +71,10 @@ function NumericMetaContainer({
|
|||||||
|
|
||||||
// Use useControllableState for both single and double values
|
// Use useControllableState for both single and double values
|
||||||
const [internalSingleValue, setInternalSingleValue] = useControllableState({
|
const [internalSingleValue, setInternalSingleValue] = useControllableState({
|
||||||
prop: mode === 'number' || mode === 'singleRange' ? value : undefined,
|
prop: mode === 'number' || mode === 'singleRange' || mode === 'stepper' ? value : undefined,
|
||||||
defaultProp: calculatedDefaultValue,
|
defaultProp: calculatedDefaultValue,
|
||||||
onChange: newVal => {
|
onChange: newVal => {
|
||||||
if (mode === 'number' || mode === 'singleRange') {
|
if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
|
||||||
onChange?.(newVal);
|
onChange?.(newVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -127,7 +141,7 @@ function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelPr
|
|||||||
|
|
||||||
let displayedValue = '';
|
let displayedValue = '';
|
||||||
let valueClasses = '';
|
let valueClasses = '';
|
||||||
if (mode === 'number' || mode === 'singleRange') {
|
if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
|
||||||
displayedValue = singleValue.toString();
|
displayedValue = singleValue.toString();
|
||||||
valueClasses = 'w-10';
|
valueClasses = 'w-10';
|
||||||
} else if (mode === 'doubleRange') {
|
} 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 = {
|
export const Numeric = {
|
||||||
Container: NumericMetaContainer,
|
Container: NumericMetaContainer,
|
||||||
Label: NumericMetaLabel,
|
Label: NumericMetaLabel,
|
||||||
SingleRange,
|
SingleRange,
|
||||||
DoubleRange,
|
DoubleRange,
|
||||||
NumberInput,
|
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 { Toggle, toggleVariants } from './Toggle';
|
||||||
import { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
|
import { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
|
import { InputNumber } from './InputNumber';
|
||||||
import { Label } from './Label';
|
import { Label } from './Label';
|
||||||
import { Switch } from './Switch';
|
import { Switch } from './Switch';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
@ -59,6 +60,7 @@ import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
|
|||||||
import Modal from './Modal/Modal';
|
import Modal from './Modal/Modal';
|
||||||
import { FooterAction } from './FooterAction';
|
import { FooterAction } from './FooterAction';
|
||||||
import { InputFilter } from './InputFilter';
|
import { InputFilter } from './InputFilter';
|
||||||
|
import { WindowLevel, WindowLevelHistogram } from './OHIFPanels';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -154,11 +156,12 @@ export {
|
|||||||
Calendar,
|
Calendar,
|
||||||
DatePickerWithRange,
|
DatePickerWithRange,
|
||||||
Input,
|
Input,
|
||||||
|
InputNumber,
|
||||||
Label,
|
Label,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
TabsContent,
|
|
||||||
Separator,
|
Separator,
|
||||||
Switch,
|
Switch,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@ -245,4 +248,6 @@ export {
|
|||||||
FooterAction,
|
FooterAction,
|
||||||
ToolSettings,
|
ToolSettings,
|
||||||
InputFilter,
|
InputFilter,
|
||||||
|
WindowLevel,
|
||||||
|
WindowLevelHistogram,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
|
import * as utils from './utils';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './contextProviders';
|
export * from './contextProviders';
|
||||||
|
|
||||||
import * as utils from './utils';
|
|
||||||
|
|
||||||
export { utils };
|
export { utils };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user