diff --git a/README.md b/README.md index 2ab0a29db..e755af294 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -

OHIF Medical Imaging Viewer

The OHIF Viewer is a zero-footprint medical image viewer diff --git a/bun.lock b/bun.lock index 62ec8dc96..a70f55f9c 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,7 @@ }, "addOns/externals/devDependencies": { "name": "@externals/devDependencies", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "@kitware/vtk.js": "32.12.0", @@ -121,22 +121,22 @@ }, "addOns/externals/dicom-microscopy-viewer": { "name": "@externals/dicom-microscopy-viewer", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "dicom-microscopy-viewer": "^0.46.1", }, }, "extensions/cornerstone": { "name": "@ohif/extension-cornerstone", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/ai": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/labelmap-interpolation": "^3.8.2", - "@cornerstonejs/polymorphic-segmentation": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/ai": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/labelmap-interpolation": "^3.8.4", + "@cornerstonejs/polymorphic-segmentation": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "^1.4.1", @@ -153,9 +153,9 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", - "@ohif/core": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@cornerstonejs/dicom-image-loader": "^3.8.4", + "@ohif/core": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -167,19 +167,19 @@ }, "extensions/cornerstone-dicom-pmap": { "name": "@ohif/extension-cornerstone-dicom-pmap", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -190,16 +190,16 @@ }, "extensions/cornerstone-dicom-rt": { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -210,19 +210,19 @@ }, "extensions/cornerstone-dicom-seg": { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -233,19 +233,19 @@ }, "extensions/cornerstone-dicom-sr": { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-measurement-tracking": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-measurement-tracking": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -255,19 +255,19 @@ }, "extensions/cornerstone-dynamic-volume": { "name": "@ohif/extension-cornerstone-dynamic-volume", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -277,7 +277,7 @@ }, "extensions/default": { "name": "@ohif/extension-default", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/calculate-suv": "^1.1.0", @@ -285,8 +285,8 @@ "lodash.uniqby": "^4.7.0", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", "dcmjs": "*", "dicomweb-client": "^0.10.4", "prop-types": "^15.6.2", @@ -300,7 +300,7 @@ }, "extensions/dicom-microscopy": { "name": "@ohif/extension-dicom-microscopy", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", @@ -311,10 +311,10 @@ "mathjs": "^12.4.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -325,14 +325,14 @@ }, "extensions/dicom-pdf": { "name": "@ohif/extension-dicom-pdf", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -342,14 +342,14 @@ }, "extensions/dicom-video": { "name": "@ohif/extension-dicom-video", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -359,20 +359,20 @@ }, "extensions/measurement-tracking": { "name": "@ohif/extension-measurement-tracking", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/ui": "3.10.0-beta.144", "@xstate/react": "^3.2.2", "xstate": "^4.10.0", }, "peerDependencies": { - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "classnames": "^2.3.2", "dcmjs": "*", "lodash.debounce": "^4.0.8", @@ -385,14 +385,14 @@ }, "extensions/test-extension": { "name": "@ohif/extension-test", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", "dcmjs": "0.38.0", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -402,7 +402,7 @@ }, "extensions/tmtv": { "name": "@ohif/extension-tmtv", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", @@ -419,7 +419,7 @@ }, "modes/basic-dev-mode": { "name": "@ohif/mode-basic-dev-mode", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -429,17 +429,17 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/extension-dicom-pdf": "3.10.0-beta.143", - "@ohif/extension-dicom-video": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/extension-dicom-pdf": "3.10.0-beta.144", + "@ohif/extension-dicom-video": "3.10.0-beta.144", }, }, "modes/basic-test-mode": { "name": "@ohif/mode-test", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -449,19 +449,19 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/extension-dicom-pdf": "3.10.0-beta.143", - "@ohif/extension-dicom-video": "3.10.0-beta.143", - "@ohif/extension-measurement-tracking": "3.10.0-beta.143", - "@ohif/extension-test": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/extension-dicom-pdf": "3.10.0-beta.144", + "@ohif/extension-dicom-video": "3.10.0-beta.144", + "@ohif/extension-measurement-tracking": "3.10.0-beta.144", + "@ohif/extension-test": "3.10.0-beta.144", }, }, "modes/longitudinal": { "name": "@ohif/mode-longitudinal", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -471,32 +471,32 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/extension-dicom-pdf": "3.10.0-beta.143", - "@ohif/extension-dicom-video": "3.10.0-beta.143", - "@ohif/extension-measurement-tracking": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/extension-dicom-pdf": "3.10.0-beta.144", + "@ohif/extension-dicom-video": "3.10.0-beta.144", + "@ohif/extension-measurement-tracking": "3.10.0-beta.144", }, }, "modes/microscopy": { "name": "@ohif/mode-microscopy", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.144", }, }, "modes/preclinical-4d": { "name": "@ohif/mode-preclinical-4d", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", }, @@ -515,7 +515,7 @@ }, "modes/segmentation": { "name": "@ohif/mode-segmentation", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -559,7 +559,7 @@ }, "modes/tmtv": { "name": "@ohif/mode-tmtv", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -580,32 +580,32 @@ }, "platform/app": { "name": "@ohif/app", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", + "@cornerstonejs/dicom-image-loader": "^3.8.4", "@emotion/serialize": "^1.1.3", - "@ohif/core": "3.10.0-beta.143", - "@ohif/extension-cornerstone": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.143", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.143", - "@ohif/extension-default": "3.10.0-beta.143", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.143", - "@ohif/extension-dicom-pdf": "3.10.0-beta.143", - "@ohif/extension-dicom-video": "3.10.0-beta.143", - "@ohif/extension-test": "3.10.0-beta.143", - "@ohif/i18n": "3.10.0-beta.143", - "@ohif/mode-basic-dev-mode": "3.10.0-beta.143", - "@ohif/mode-longitudinal": "3.10.0-beta.143", - "@ohif/mode-microscopy": "3.10.0-beta.143", - "@ohif/mode-test": "3.10.0-beta.143", - "@ohif/ui": "3.10.0-beta.143", - "@ohif/ui-next": "3.10.0-beta.143", + "@ohif/core": "3.10.0-beta.144", + "@ohif/extension-cornerstone": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.144", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", + "@ohif/extension-default": "3.10.0-beta.144", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.144", + "@ohif/extension-dicom-pdf": "3.10.0-beta.144", + "@ohif/extension-dicom-video": "3.10.0-beta.144", + "@ohif/extension-test": "3.10.0-beta.144", + "@ohif/i18n": "3.10.0-beta.144", + "@ohif/mode-basic-dev-mode": "3.10.0-beta.144", + "@ohif/mode-longitudinal": "3.10.0-beta.144", + "@ohif/mode-microscopy": "3.10.0-beta.144", + "@ohif/mode-test": "3.10.0-beta.144", + "@ohif/ui": "3.10.0-beta.144", + "@ohif/ui-next": "3.10.0-beta.144", "@svgr/webpack": "^8.1.0", "@types/react": "^18.3.3", "classnames": "^2.3.2", @@ -646,7 +646,7 @@ }, "platform/cli": { "name": "@ohif/cli", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "bin": { "ohif-cli": "src/index.js", }, @@ -670,7 +670,7 @@ }, "platform/core": { "name": "@ohif/core", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "dcmjs": "*", @@ -695,15 +695,15 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", - "@ohif/ui": "3.10.0-beta.143", + "@cornerstonejs/dicom-image-loader": "^3.8.4", + "@ohif/ui": "3.10.0-beta.144", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21", }, }, "platform/i18n": { "name": "@ohif/i18n", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@babel/runtime": "^7.20.13", "i18next-locize-backend": "^2.0.0", @@ -728,7 +728,7 @@ }, "platform/ui": { "name": "@ohif/ui", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@testing-library/react": "^13.1.0", "browser-detect": "^0.2.28", @@ -788,7 +788,7 @@ }, "platform/ui-next": { "name": "@ohif/ui-next", - "version": "3.10.0-beta.143", + "version": "3.10.0-beta.144", "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", @@ -831,16 +831,16 @@ }, }, "overrides": { - "commander": "8.3.0", - "path-to-regexp": "0.1.12", - "nth-check": "^2.1.1", - "trim-newlines": "^5.0.0", - "glob-parent": "^6.0.2", - "trim": "^1.0.0", - "package-json": "^8.1.0", - "sharp": "^0.32.6", - "rollup": "2.79.2", "body-parser": "1.20.3", + "commander": "8.3.0", + "glob-parent": "^6.0.2", + "nth-check": "^2.1.1", + "package-json": "^8.1.0", + "path-to-regexp": "0.1.12", + "rollup": "2.79.2", + "sharp": "^0.32.6", + "trim": "^1.0.0", + "trim-newlines": "^5.0.0", }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.2", "", {}, "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="], @@ -1099,9 +1099,9 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.8.2", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2", "@cornerstonejs/tools": "^3.8.2" } }, "sha512-6bxIa1BBvWD/X06MbB4QBOScTYmA1eTdLwg9jL+dHw5od8/CQhR22pfa0ZWsa8lkfFAMAdC1cU8BbiPNadG/0Q=="], + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.8.4", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4", "@cornerstonejs/tools": "^3.8.4" } }, "sha512-TFTvNZQBW54rEcyjDfyHZYYvIPHVGZoUCtjW3isHfpoXkjHz2cliwC55e0kcNJ0zbyI6H41c4VsYH8h2MTYOdA=="], - "@cornerstonejs/ai": ["@cornerstonejs/ai@3.8.2", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2", "@cornerstonejs/tools": "^3.8.2" } }, "sha512-B9Mr3BPWOQT+GKhG79v9xsuEfgh8a/YCK/qaseFfUGahxD/7QEcu49MuzIdigbA1rbRA3OELL0ZjXxVx3ON27A=="], + "@cornerstonejs/ai": ["@cornerstonejs/ai@3.8.4", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "lodash.clonedeep": "^4.5.0", "ndarray": "^1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4", "@cornerstonejs/tools": "^3.8.4" } }, "sha512-scc2srdL1BqNTSFEro4y7UlNBsPtBVSF31eT4SC1psxZQcr/iyaCKuOQg/hglk1yobFkdIKvjRAvvqvYmGduQw=="], "@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="], @@ -1113,15 +1113,15 @@ "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], - "@cornerstonejs/core": ["@cornerstonejs/core@3.8.2", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-zhtCXXXZMGooVYCmsfL7sC/o7wSFxXAfFm4rOp92t5m8n8UFbtmcLAB167/EEp/csHiRuv0PR/dOgd6y9tLb0A=="], + "@cornerstonejs/core": ["@cornerstonejs/core@3.8.4", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-8spQvERZRn4I/0EhzTNZ5ruPBrVn5F4msb/q0ttBtiVEa9JaVwoWh3yEcO4q5NpK7t1BhpfSPMKwU98Dy02ugQ=="], - "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.8.2", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2" } }, "sha512-K0z5N+X6GpuRTfZhyuqpLIl+Y4JePyybQof/ZZ4aqwEIUm9NjXAYEUiYZEQQGtLItkPw+Vl7TbWA0FeI5GAC3Q=="], + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.8.4", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4" } }, "sha512-N2D4U8JDhen+dCY1HrLOoK8W3CD2AAf/Cd+GiJMEGjtrihIuQ+Ha+hZTVQzaDMc7v2Ruvx2DpJ7i9HUJXoyWHw=="], - "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.8.2", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2", "@cornerstonejs/tools": "^3.8.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-nvMHY4X6crsTL8EXKsKxXfVP/T04cCxCU90tsg7ceDi5mZ9qh6qnE0r8XOTclx/SsQ/YrwEaOIaflrfLKft19g=="], + "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.8.4", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4", "@cornerstonejs/tools": "^3.8.4", "@kitware/vtk.js": "32.12.1" } }, "sha512-KKDTeVJg1s0trplkaHglBm0foQ3XL9BhsTfatFCTzgIOCT0wolrKbr+xAKIeZuBQifiFazDWpYaeBHRbkWEEMA=="], - "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.8.2", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2", "@cornerstonejs/tools": "^3.8.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-svlT/loFoB6t0r9dt31bMOkin82LftTF1J8ZWjJTOrbky2HxSyM4lnMbjQZbdhZDkhdQq0imQEOVIYNyhUAWBg=="], + "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.8.4", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4", "@cornerstonejs/tools": "^3.8.4", "@kitware/vtk.js": "32.12.1" } }, "sha512-+3o280NOniUkG9GcWTeTi6x3qy8Rm7WpYFycWpxXIufJzVudJi0zAR+EIWDq/uvTQ3DH1SrBDe4wzYNqD+yirw=="], - "@cornerstonejs/tools": ["@cornerstonejs/tools@3.8.2", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.2", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-da8pJGv7b+sjAGIomH5CyM8+xHOvjhLipY4i/gp7Z6f5Wr5b/gYWc/l0CTD9G0aVFkyP06scGaBjwv0Ei1mqMQ=="], + "@cornerstonejs/tools": ["@cornerstonejs/tools@3.8.4", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.8.4", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-yr8RLtnxGh9BsPOFpX+zwT++UGJDqWMo7noWGzxkuISDriLr+HGZI8gL910Mw62obaF8r9qpeOSvSOzEHN3nSw=="], "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@1.1.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA=="], diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index 118691b94..7d02058c9 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index dcb93f82b..4ac0f33d0 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index e7d14050d..562ca7335 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -1,22 +1,13 @@ import dcmjs from 'dcmjs'; -import { Types } from '@ohif/core'; +import { classes, Types } from '@ohif/core'; import { cache, metaData } from '@cornerstonejs/core'; -import { - segmentation as cornerstoneToolsSegmentation, - Enums as cornerstoneToolsEnums, - utilities, -} from '@cornerstonejs/tools'; +import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools'; import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters'; import { createReportDialogPrompt } from '@ohif/extension-default'; -import { classes, DicomMetadataStore } from '@ohif/core'; +import { DicomMetadataStore } from '@ohif/core'; -import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares'; -import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; -import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES'; -const { segmentation: segmentationUtils } = utilities; - const { datasetToBlob } = dcmjs.data; const getTargetViewport = ({ viewportId, viewportGridService }) => { @@ -46,14 +37,8 @@ const commandsModule = ({ servicesManager, extensionManager, }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { - const { - segmentationService, - uiDialogService, - displaySetService, - viewportGridService, - toolGroupService, - customizationService, - } = servicesManager.services as AppTypes.Services; + const { segmentationService, displaySetService, viewportGridService, toolGroupService } = + servicesManager.services as AppTypes.Services; const actions = { /** @@ -287,21 +272,24 @@ const commandsModule = ({ * converts dataset to downloadable blob. * */ - downloadRTSS: ({ segmentationId }) => { + downloadRTSS: async ({ segmentationId }) => { const segmentations = segmentationService.getSegmentation(segmentationId); - const vtkUtils = { - vtkImageMarchingSquares, - vtkDataArray, - vtkImageData, - }; - const RTSS = generateRTSSFromSegmentations( + // inject colors to the segmentIndex + const firstRepresentation = + segmentationService.getRepresentationsForSegmentation(segmentationId)[0]; + Object.entries(segmentations.segments).forEach(([segmentIndex, segment]) => { + segment.color = segmentationService.getSegmentColor( + firstRepresentation.viewportId, + segmentationId, + segmentIndex + ); + }); + + const RTSS = await generateRTSSFromSegmentations( segmentations, classes.MetadataProvider, - DicomMetadataStore, - cache, - cornerstoneToolsEnums, - vtkUtils + DicomMetadataStore ); try { @@ -314,56 +302,12 @@ const commandsModule = ({ console.warn(e); } }, - setBrushSize: ({ value, toolNames }) => { - const brushSize = Number(value); - - toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { - if (toolNames?.length === 0) { - segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize); - } else { - toolNames?.forEach(toolName => { - segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName); - }); - } - }); - }, - setThresholdRange: ({ - value, - toolNames = [ - 'ThresholdCircularBrush', - 'ThresholdSphereBrush', - 'ThresholdCircularBrushDynamic', - 'ThresholdSphereBrushDynamic', - ], - }) => { - const toolGroupIds = toolGroupService.getToolGroupIds(); - if (!toolGroupIds?.length) { - return; - } - - for (const toolGroupId of toolGroupIds) { - const toolGroup = toolGroupService.getToolGroup(toolGroupId); - toolNames?.forEach(toolName => { - toolGroup.setToolConfiguration(toolName, { - threshold: { - range: value, - }, - }); - }); - } - }, }; const definitions = { - /** - * Obsolete? - */ loadSegmentationDisplaySetsForViewport: { commandFn: actions.loadSegmentationDisplaySetsForViewport, }, - /** - * Obsolete? - */ loadSegmentationsForViewport: { commandFn: actions.loadSegmentationsForViewport, }, @@ -380,12 +324,6 @@ const commandsModule = ({ downloadRTSS: { commandFn: actions.downloadRTSS, }, - setBrushSize: { - commandFn: actions.setBrushSize, - }, - setThresholdRange: { - commandFn: actions.setThresholdRange, - }, }; return { diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index e134ce011..a100d08a9 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 38cef7810..2759c6c31 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index a1306b9d2..1430d93ab 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,7 +38,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", + "@cornerstonejs/dicom-image-loader": "^3.8.4", "@ohif/core": "3.10.0-beta.144", "@ohif/ui": "3.10.0-beta.144", "dcmjs": "*", @@ -51,12 +51,12 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.8.2", - "@cornerstonejs/ai": "^3.8.2", - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/labelmap-interpolation": "^3.8.2", - "@cornerstonejs/polymorphic-segmentation": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/adapters": "^3.8.4", + "@cornerstonejs/ai": "^3.8.4", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/labelmap-interpolation": "^3.8.4", + "@cornerstonejs/polymorphic-segmentation": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "^1.4.1", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 639e86f3c..20fdc8d9f 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -10,7 +10,6 @@ import { ToolGroupManager, Enums, utilities as cstUtils, - ReferenceLinesTool, annotation, Types as ToolTypes, } from '@cornerstonejs/tools'; @@ -43,6 +42,8 @@ const toggleSyncFunctions = { voi: toggleVOISliceSync, }; +const { segmentation: segmentationUtils } = cstUtils; + const getLabelmapTools = ({ toolGroupService }) => { const labelmapTools = []; const toolGroupIds = toolGroupService.getToolGroupIds(); @@ -155,6 +156,8 @@ function commandsModule({ } ); + measurement.annotationUID = annotation.annotationUID; + // Update segmentation stats const updatedSegmentation = updateSegmentBidirectionalStats({ segmentationId: targetId, @@ -172,6 +175,14 @@ function commandsModule({ }); } }); + + // get the active segmentIndex bidirectional annotation and jump to it + const activeBidirectional = bidirectionalData.find( + measurement => measurement.segmentIndex === targetIndex + ); + commandsManager.run('jumpToMeasurement', { + uid: activeBidirectional.annotationUID, + }); }, interpolateLabelmap: () => { const { segmentationId, segmentIndex } = _getActiveSegmentationInfo(); @@ -1630,6 +1641,72 @@ function commandsModule({ segmentAI.initViewport(viewport); } }, + setBrushSize: ({ value, toolNames }) => { + const brushSize = Number(value); + + toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { + if (toolNames?.length === 0) { + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize); + } else { + toolNames?.forEach(toolName => { + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName); + }); + } + }); + }, + setThresholdRange: ({ + value, + toolNames = [ + 'ThresholdCircularBrush', + 'ThresholdSphereBrush', + 'ThresholdCircularBrushDynamic', + 'ThresholdSphereBrushDynamic', + ], + }) => { + const toolGroupIds = toolGroupService.getToolGroupIds(); + if (!toolGroupIds?.length) { + return; + } + + for (const toolGroupId of toolGroupIds) { + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + toolNames?.forEach(toolName => { + toolGroup.setToolConfiguration(toolName, { + threshold: { + range: value, + }, + }); + }); + } + }, + increaseBrushSize: () => { + const toolGroupIds = toolGroupService.getToolGroupIds(); + if (!toolGroupIds?.length) { + return; + } + + for (const toolGroupId of toolGroupIds) { + const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId); + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize + 3); + } + }, + decreaseBrushSize: () => { + const toolGroupIds = toolGroupService.getToolGroupIds(); + if (!toolGroupIds?.length) { + return; + } + + for (const toolGroupId of toolGroupIds) { + const brushSize = segmentationUtils.getBrushSizeForToolGroup(toolGroupId); + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize - 3); + } + }, + addNewSegment: () => { + const { segmentationService } = servicesManager.services; + const { activeViewportId } = viewportGridService.getState(); + const activeSegmentation = segmentationService.getActiveSegmentation(activeViewportId); + segmentationService.addSegment(activeSegmentation.segmentationId); + }, }; const definitions = { @@ -1903,6 +1980,11 @@ function commandsModule({ toggleLabelmapAssist: actions.toggleLabelmapAssist, interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap, clearMarkersForMarkerLabelmap: actions.clearMarkersForMarkerLabelmap, + setBrushSize: actions.setBrushSize, + setThresholdRange: actions.setThresholdRange, + increaseBrushSize: actions.increaseBrushSize, + decreaseBrushSize: actions.decreaseBrushSize, + addNewSegment: actions.addNewSegment, }; return { diff --git a/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx index 56cad0f24..d83e1d59f 100644 --- a/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx +++ b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx @@ -95,24 +95,53 @@ export const CustomDropdownMenuContent = () => { - {t('Export & Download')} + {t('Download & Export')} - actions.downloadCSVSegmentationReport(segmentationId)}> - {t('Download CSV Report')} + + + {t('Download')} + + { + e.preventDefault(); + actions.downloadCSVSegmentationReport(segmentationId); + }} + > + {t('CSV Report')} actions.storeSegmentation(segmentationId)} + onClick={e => { + e.preventDefault(); + actions.onSegmentationDownload(segmentationId); + }} disabled={!allowExport} > - {t('Export DICOM SEG')} + {t('DICOM SEG')} actions.onSegmentationDownload(segmentationId)} + onClick={e => { + e.preventDefault(); + actions.onSegmentationDownloadRTSS(segmentationId); + }} disabled={!allowExport} > - {t('Download DICOM SEG')} + {t('DICOM RTSS')} + + + + + {t('Export')} + + { + e.preventDefault(); + actions.storeSegmentation(segmentationId); + }} + disabled={!allowExport} + > + {t('DICOM SEG')} diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index 7535b5afe..d372c0651 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -39,7 +39,10 @@ import { useLutPresentationStore } from './stores/useLutPresentationStore'; import { usePositionPresentationStore } from './stores/usePositionPresentationStore'; import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore'; import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities'; -import { updateSegmentationStats } from './utils/updateSegmentationStats'; +import { + setupSegmentationDataModifiedHandler, + setupSegmentationModifiedHandler, +} from './utils/segmentationHandlers'; const { registerColormap } = csUtilities.colormap; @@ -189,44 +192,16 @@ export default async function init({ }); }); - segmentationService.subscribeDebounced( - segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, - async ({ segmentationId }) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - const readableText = customizationService.getCustomization('panelSegmentation.readableText'); - const updatedSegmentation = await updateSegmentationStats({ - segmentation, - segmentationId, - readableText, - }); + // Setup segmentation event handlers + setupSegmentationDataModifiedHandler({ + segmentationService, + customizationService, + commandsManager, + }); - if (updatedSegmentation) { - segmentationService.addOrUpdateSegmentation({ - segmentationId, - segments: updatedSegmentation.segments, - }); - } - - // Check for segments with bidirectional measurements and update them - if (segmentation) { - const segmentIndices = Object.keys(segmentation.segments) - .map(index => parseInt(index)) - .filter(index => index > 0); - - for (const segmentIndex of segmentIndices) { - const segment = segmentation.segments[segmentIndex]; - if (segment?.cachedStats?.namedStats?.bidirectional) { - // Run the command to update the bidirectional measurement - commandsManager.runCommand('runSegmentBidirectional', { - segmentationId, - segmentIndex, - }); - } - } - } - }, - 1000 - ); + setupSegmentationModifiedHandler({ + segmentationService, + }); // When a custom image load is performed, update the relevant viewports hangingProtocolService.subscribe( diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 7c1a03e2f..2a7fa9257 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -602,10 +602,16 @@ class SegmentationService extends PubSubService { const segments: { [segmentIndex: string]: cstTypes.Segment } = {}; let segmentsCachedStats = {}; + // Create colorLUT array for RT structures + const colorLUT = [[0, 0, 0, 0]]; // First entry is transparent for index 0 + // Process each segment similarly to the SEG function for (const rtStructData of allRTStructData) { const { data, id, color, segmentIndex, geometryId } = rtStructData; + // Add the color to the colorLUT array + colorLUT.push(color); + try { const geometry = await geometryLoader.createAndCacheGeometry(geometryId, { geometryData: { @@ -647,6 +653,11 @@ class SegmentationService extends PubSubService { } } + // Create and register the colorLUT + const colorLUTIndex = getNextColorLUTIndex(); + addColorLUT(colorLUT, colorLUTIndex); + this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); + // Assign processed segments to segmentation config segmentation.config.segments = segments; diff --git a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts index 51bb8c404..f72c7f89f 100644 --- a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts +++ b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts @@ -53,7 +53,6 @@ const segmentationRepresentationModifiedCallback = async ( const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID(); if (!targetFrameOfReferenceUID) { - console.debug('No frame of reference UID found for the target viewport'); return; } diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index b63c9a044..4952da070 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -846,13 +846,17 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi if (addOverlayFn) { addOverlayFn(); } + viewport.render(); volumesProperties.forEach(({ properties, volumeId }) => { - viewport.setProperties(properties, volumeId); + setTimeout(() => { + // seems like a hack but we need the actor to be ready first before + // we set the properties + viewport.setProperties(properties, volumeId); + viewport.render(); + }, 0); }); - viewport.render(); - this.setPresentations(viewport.id, presentations, viewportInfo); if (!presentations.positionPresentation) { @@ -865,8 +869,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } } - viewport.render(); - this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, { viewportInfo, }); diff --git a/extensions/cornerstone/src/utils/colormaps.js b/extensions/cornerstone/src/utils/colormaps.js index a21df96ac..ea3f8ab3f 100644 --- a/extensions/cornerstone/src/utils/colormaps.js +++ b/extensions/cornerstone/src/utils/colormaps.js @@ -2,7 +2,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'Grayscale', - name: 'Grayscale', NanColor: [1, 0, 0], RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1], description: 'Grayscale', @@ -10,7 +9,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'X Ray', - name: 'X Ray', NanColor: [1, 0, 0], RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0], description: 'X Ray', @@ -18,7 +16,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'hsv', - name: 'hsv', RGBPoints: [ -1, 1, 0, 0, -0.666666, 1, 0, 1, -0.333333, 0, 0, 1, 0, 0, 1, 1, 0.33333, 0, 1, 0, 0.66666, 1, 1, 0, 1, 1, 0, 0, @@ -28,7 +25,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'hot_iron', - name: 'hot_iron', RGBPoints: [ 0.0, 0.0039215686, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0039215686, 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0039215686, 0.0039215686, 0.031372549, @@ -224,7 +220,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'red_hot', - name: 'red_hot', RGBPoints: [ 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0, 0.0, 0.0, 0.00784313725490196, 0.0, 0.0, 0.0, 0.011764705882352941, 0.0, 0.0, 0.0, 0.01568627450980392, 0.0039215686, 0.0039215686, @@ -359,7 +354,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 's_pet', - name: 's_pet', RGBPoints: [ 0.0, 0.0156862745, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0156862745, 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0274509804, 0.0039215686, 0.031372549, @@ -553,7 +547,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'perfusion', - name: 'perfusion', RGBPoints: [ 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0078431373, 0.0235294118, 0.0235294118, 0.00784313725490196, 0.0078431373, 0.031372549, 0.0470588235, 0.011764705882352941, @@ -733,7 +726,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'rainbow_2', - name: 'rainbow_2', RGBPoints: [ 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0156862745, 0.0, 0.0117647059, 0.00784313725490196, 0.0352941176, 0.0, 0.0274509804, 0.011764705882352941, 0.0509803922, 0.0, 0.0392156863, @@ -872,7 +864,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'suv', - name: 'suv', RGBPoints: [ 0.0, 1.0, 1.0, 1.0, 0.00392156862745098, 1.0, 1.0, 1.0, 0.00784313725490196, 1.0, 1.0, 1.0, 0.011764705882352941, 1.0, 1.0, 1.0, 0.01568627450980392, 1.0, 1.0, 1.0, 0.0196078431372549, @@ -1019,7 +1010,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'ge_256', - name: 'ge_256', RGBPoints: [ 0.0, 0.0039215686, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0039215686, 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0039215686, 0.0078431373, 0.0117647059, @@ -1211,7 +1201,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'ge', - name: 'ge', RGBPoints: [ 0.0, 0.0078431373, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0078431373, 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0078431373, 0.0078431373, 0.0078431373, @@ -1404,7 +1393,6 @@ const colormaps = [ { ColorSpace: 'RGB', Name: 'siemens', - name: 'siemens', RGBPoints: [ 0.0, 0.0078431373, 0.0039215686, 0.1254901961, 0.00392156862745098, 0.0078431373, 0.0039215686, 0.1254901961, 0.00784313725490196, 0.0078431373, 0.0039215686, 0.1882352941, diff --git a/extensions/cornerstone/src/utils/index.ts b/extensions/cornerstone/src/utils/index.ts index 14de717e0..3717385f3 100644 --- a/extensions/cornerstone/src/utils/index.ts +++ b/extensions/cornerstone/src/utils/index.ts @@ -1,9 +1,15 @@ import { handleSegmentChange } from './segmentUtils'; import { isReferenceViewable } from './isReferenceViewable'; +import { + setupSegmentationDataModifiedHandler, + setupSegmentationModifiedHandler, +} from './segmentationHandlers'; const utils = { handleSegmentChange, isReferenceViewable, + setupSegmentationDataModifiedHandler, + setupSegmentationModifiedHandler, }; export default utils; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts index 241848b43..c4ae6b53f 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts @@ -84,7 +84,7 @@ const Bidirectional = { function getMappedAnnotations(annotation, displaySetService) { const { metadata, data } = annotation; const { cachedStats } = data; - const { referencedImageId, referencedSeriesInstanceUID } = metadata; + const { referencedImageId } = metadata; const targets = Object.keys(cachedStats); if (!targets.length) { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts index b9971f7ab..754a62340 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts @@ -93,6 +93,7 @@ function getMappedAnnotations(annotation, displaySetService) { } const annotations = []; + const addedModalities = new Set(); Object.keys(cachedStats).forEach(targetId => { const targetStats = cachedStats[targetId]; @@ -108,6 +109,12 @@ function getMappedAnnotations(annotation, displaySetService) { const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit, perimeter, radiusUnit } = targetStats; + if (Modality && addedModalities.has(Modality)) { + return; + } + + addedModalities.add(Modality); + annotations.push({ SeriesInstanceUID, SOPInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts index 7cdbfceb1..c81a49d41 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts @@ -93,6 +93,8 @@ function getMappedAnnotations(annotation, displaySetService) { } const annotations = []; + const addedModalities = new Set(); + Object.keys(cachedStats).forEach(targetId => { const targetStats = cachedStats[targetId]; @@ -107,6 +109,16 @@ function getMappedAnnotations(annotation, displaySetService) { const { SeriesNumber } = displaySet; const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + // Skip if we've already added this modality + if (Modality && addedModalities.has(Modality)) { + return; + } + + // Add modality to the set if it exists + if (Modality) { + addedModalities.add(Modality); + } + annotations.push({ SeriesInstanceUID, SOPInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts index c35636b10..40c1bd793 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -92,6 +92,8 @@ function getMappedAnnotations(annotation, displaySetService) { } const annotations = []; + + const addedModalities = new Set(); Object.keys(cachedStats).forEach(targetId => { const targetStats = cachedStats[targetId]; @@ -106,6 +108,16 @@ function getMappedAnnotations(annotation, displaySetService) { const { SeriesNumber } = displaySet; const { mean, stdDev, max, area, Modality, modalityUnit, areaUnit } = targetStats; + // Skip if we've already added this modality + if (Modality && addedModalities.has(Modality)) { + return; + } + + // Add modality to the set if it exists + if (Modality) { + addedModalities.add(Modality); + } + annotations.push({ SeriesInstanceUID, SOPInstanceUID, diff --git a/extensions/cornerstone/src/utils/segmentationHandlers.ts b/extensions/cornerstone/src/utils/segmentationHandlers.ts new file mode 100644 index 000000000..f7e541115 --- /dev/null +++ b/extensions/cornerstone/src/utils/segmentationHandlers.ts @@ -0,0 +1,93 @@ +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { updateSegmentationStats } from './updateSegmentationStats'; + +/** + * Sets up the handler for segmentation data modification events + */ +export function setupSegmentationDataModifiedHandler({ + segmentationService, + customizationService, + commandsManager, +}) { + segmentationService.subscribeDebounced( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + async ({ segmentationId }) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + const readableText = customizationService.getCustomization('panelSegmentation.readableText'); + + // Check for segments with bidirectional measurements and update them + const segmentIndices = Object.keys(segmentation.segments) + .map(index => parseInt(index)) + .filter(index => index > 0); + + for (const segmentIndex of segmentIndices) { + const segment = segmentation.segments[segmentIndex]; + if (segment?.cachedStats?.namedStats?.bidirectional) { + // Run the command to update the bidirectional measurement + commandsManager.runCommand('runSegmentBidirectional', { + segmentationId, + segmentIndex, + }); + } + } + + const updatedSegmentation = await updateSegmentationStats({ + segmentation, + segmentationId, + readableText, + }); + + if (updatedSegmentation) { + segmentationService.addOrUpdateSegmentation({ + segmentationId, + segments: updatedSegmentation.segments, + }); + } + }, + 1000 + ); +} + +/** + * Sets up the handler for segmentation modification events + */ +export function setupSegmentationModifiedHandler({ segmentationService }) { + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + async ({ segmentationId }) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + + const annotationState = cornerstoneTools.annotation.state.getAllAnnotations(); + const bidirectionalAnnotations = annotationState.filter( + annotation => + annotation.metadata.toolName === cornerstoneTools.SegmentBidirectionalTool.toolName + ); + + let toRemoveUIDs = []; + if (!segmentation) { + toRemoveUIDs = bidirectionalAnnotations.map( + annotation => annotation.metadata.segmentationId === segmentationId + ); + return; + } else { + const segmentIndices = Object.keys(segmentation.segments) + .map(index => parseInt(index)) + .filter(index => index > 0); + + // check if there is a bidirectional data that exists but the segment + // does not exists anymore we need to remove the bidirectional data + const bidirectionalAnnotationsToRemove = bidirectionalAnnotations.filter( + annotation => + annotation.metadata.segmentationId === segmentationId && + !segmentIndices.includes(annotation.metadata.segmentIndex) + ); + + toRemoveUIDs = bidirectionalAnnotationsToRemove.map(annotation => annotation.annotationUID); + } + + toRemoveUIDs.forEach(uid => { + cornerstoneTools.annotation.state.removeAnnotation(uid); + }); + } + ); +} diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 7421f233b..a885f6f8c 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -12,33 +12,48 @@ import { CallbackCustomization } from 'platform/core/src/types'; const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils; +const thumbnailNoImageModalities = [ + 'SR', + 'SEG', + 'SM', + 'RTSTRUCT', + 'RTPLAN', + 'RTDOSE', + 'DOC', + 'OT', + 'PMAP', +]; + /** - * - * @param {*} param0 + * Study Browser component that displays and manages studies and their display sets */ function PanelStudyBrowser({ getImageSrc, getStudiesForPatientByMRN, requestDisplaySetCreationForStudy, dataSource, + customMapDisplaySets, + onClickUntrack, }) { const { servicesManager, commandsManager } = useSystem(); const { displaySetService, customizationService } = servicesManager.services; const navigate = useNavigate(); + const studyMode = customizationService.getCustomization('studyBrowser.studyMode') || 'all'; + + const internalImageViewer = useImageViewer(); + const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs; - // Normally you nest the components so the tree isn't so deep, and the data - // doesn't have to have such an intense shape. This works well enough for now. - // Tabs --> Studies --> DisplaySets --> Thumbnails - const { StudyInstanceUIDs } = useImageViewer(); const [{ activeViewportId, viewports, isHangingProtocolLayout }] = useViewportGrid(); - const [activeTabName, setActiveTabName] = useState('all'); + const [activeTabName, setActiveTabName] = useState(studyMode); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ ...StudyInstanceUIDs, ]); const [hasLoadedViewports, setHasLoadedViewports] = useState(false); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); + const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({}); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); const [viewPresets, setViewPresets] = useState( customizationService.getCustomization('studyBrowser.viewPresets') @@ -65,6 +80,8 @@ function PanelStudyBrowser({ setViewPresets(newViewPresets); }; + const mapDisplaySetsWithState = customMapDisplaySets || _mapDisplaySets; + const onDoubleClickThumbnailHandler = useCallback( async displaySetInstanceUID => { const customHandler = customizationService.getCustomization( @@ -121,7 +138,7 @@ function PanelStudyBrowser({ const actuallyMappedStudies = mappedStudies.map(qidoStudy => { return { studyInstanceUid: qidoStudy.StudyInstanceUID, - date: formatDate(qidoStudy.StudyDate), + date: formatDate(qidoStudy.StudyDate) || '', description: qidoStudy.StudyDescription, modalities: qidoStudy.ModalitiesInStudy, numInstances: Number(qidoStudy.NumInstances), @@ -142,7 +159,7 @@ function PanelStudyBrowser({ StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); - // // ~~ Initial Thumbnails + // ~~ Initial Thumbnails useEffect(() => { if (!hasLoadedViewports) { if (activeViewportId) { @@ -150,19 +167,35 @@ function PanelStudyBrowser({ // so wait a bit of time to allow the viewports preferential loading // which improves user experience of responsiveness significantly on slower // systems. - window.setTimeout(() => setHasLoadedViewports(true), 250); + const delayMs = 250 + displaySetService.getActiveDisplaySets().length * 10; + window.setTimeout(() => setHasLoadedViewports(true), delayMs); } return; } - const currentDisplaySets = displaySetService.activeDisplaySets; + let currentDisplaySets = displaySetService.activeDisplaySets; + // filter non based on the list of modalities that are supported by cornerstone + currentDisplaySets = currentDisplaySets.filter( + ds => !thumbnailNoImageModalities.includes(ds.Modality) + ); + + if (!currentDisplaySets.length) { + return; + } + currentDisplaySets.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - const imageId = imageIds[Math.floor(imageIds.length / 2)]; + const imageId = getImageIdForThumbnail(displaySet, imageIds); + + // TODO: Is it okay that imageIds are not returned here for SR displaySets? + if (!imageId || displaySet?.unsupported) { + return; + } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap let { thumbnailSrc } = displaySet; if (!thumbnailSrc && displaySet.getThumbnailSrc) { thumbnailSrc = await displaySet.getThumbnailSrc(); @@ -172,39 +205,40 @@ function PanelStudyBrowser({ displaySet.thumbnailSrc = thumbnailSrc; } newImageSrcEntry[dSet.displaySetInstanceUID] = thumbnailSrc; - // TODO: Is it okay that imageIds are not returned here for SR displaySets? - if (!imageId || displaySet?.unsupported) { - return; - } - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - try { - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); - } catch (e) { - // This can happen for thumbnails and generate huge log sets if logged. - return; - } + setThumbnailImageSrcMap(prevState => { return { ...prevState, ...newImageSrcEntry }; }); }); - }, [ - StudyInstanceUIDs, - dataSource, - displaySetService, - getImageSrc, - hasLoadedViewports, - activeViewportId, - ]); + }, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]); // ~~ displaySets useEffect(() => { - // TODO: Are we sure `activeDisplaySets` will always be accurate? const currentDisplaySets = displaySetService.activeDisplaySets; - const mappedDisplaySets = _mapDisplaySets(currentDisplaySets, thumbnailImageSrcMap); - sortStudyInstances(mappedDisplaySets); + + if (!currentDisplaySets.length) { + return; + } + + const mappedDisplaySets = mapDisplaySetsWithState( + currentDisplaySets, + displaySetsLoadingState, + thumbnailImageSrcMap, + viewports + ); + + if (!customMapDisplaySets) { + sortStudyInstances(mappedDisplaySets); + } setDisplaySets(mappedDisplaySets); - }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + }, [ + displaySetService.activeDisplaySets, + displaySetsLoadingState, + viewports, + thumbnailImageSrcMap, + customMapDisplaySets, + ]); // ~~ subscriptions --> displaySets useEffect(() => { @@ -212,30 +246,41 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { - // for some reason this breaks thumbnail loading - // if (!hasLoadedViewports) { - // return; - // } - const { displaySetsAdded } = data; + if (!hasLoadedViewports) { + return; + } + const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { + const displaySetInstanceUID = dSet.displaySetInstanceUID; + const newImageSrcEntry = {}; - const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); if (displaySet?.unsupported) { return; } + if (options?.madeInClient) { + setJumpToDisplaySet(displaySetInstanceUID); + } + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - const imageId = imageIds[Math.floor(imageIds.length / 2)]; + const imageId = getImageIdForThumbnail(displaySet, imageIds); // TODO: Is it okay that imageIds are not returned here for SR displaysets? if (!imageId) { return; } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc( - imageId, - dSet.initialViewport - ); + let { thumbnailSrc } = displaySet; + if (!thumbnailSrc && displaySet.getThumbnailSrc) { + thumbnailSrc = await displaySet.getThumbnailSrc(); + } + if (!thumbnailSrc) { + thumbnailSrc = await getImageSrc(imageId); + displaySet.thumbnailSrc = thumbnailSrc; + } + newImageSrcEntry[displaySetInstanceUID] = thumbnailSrc; setThumbnailImageSrcMap(prevState => { return { ...prevState, ...newImageSrcEntry }; @@ -247,7 +292,7 @@ function PanelStudyBrowser({ return () => { SubscriptionDisplaySetsAdded.unsubscribe(); }; - }, [getImageSrc, dataSource, displaySetService]); + }, [displaySetService, dataSource, getImageSrc, hasLoadedViewports]); useEffect(() => { // TODO: Will this always hold _all_ the displaySets we care about? @@ -255,7 +300,17 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetsChanged = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_CHANGED, changedDisplaySets => { - const mappedDisplaySets = _mapDisplaySets(changedDisplaySets, thumbnailImageSrcMap); + const mappedDisplaySets = mapDisplaySetsWithState( + changedDisplaySets, + displaySetsLoadingState, + thumbnailImageSrcMap, + viewports + ); + + if (!customMapDisplaySets) { + sortStudyInstances(mappedDisplaySets); + } + setDisplaySets(mappedDisplaySets); } ); @@ -263,11 +318,17 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, () => { - const mappedDisplaySets = _mapDisplaySets( + const mappedDisplaySets = mapDisplaySetsWithState( displaySetService.getActiveDisplaySets(), - thumbnailImageSrcMap + displaySetsLoadingState, + thumbnailImageSrcMap, + viewports ); + if (!customMapDisplaySets) { + sortStudyInstances(mappedDisplaySets); + } + setDisplaySets(mappedDisplaySets); } ); @@ -276,7 +337,13 @@ function PanelStudyBrowser({ SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; - }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + }, [ + displaySetsLoadingState, + thumbnailImageSrcMap, + viewports, + displaySetService, + customMapDisplaySets, + ]); const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); @@ -284,8 +351,7 @@ function PanelStudyBrowser({ function _handleStudyClick(StudyInstanceUID) { const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy - ? // eslint-disable-next-line prettier/prettier - [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + ? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -296,6 +362,43 @@ function PanelStudyBrowser({ } } + useEffect(() => { + if (jumpToDisplaySet) { + // Get element by displaySetInstanceUID + const displaySetInstanceUID = jumpToDisplaySet; + const element = document.getElementById(`thumbnail-${displaySetInstanceUID}`); + + if (element && typeof element.scrollIntoView === 'function') { + // TODO: Any way to support IE here? + element.scrollIntoView({ behavior: 'smooth' }); + + setJumpToDisplaySet(null); + } + } + }, [jumpToDisplaySet, expandedStudyInstanceUIDs, activeTabName]); + + useEffect(() => { + if (!jumpToDisplaySet) { + return; + } + + const displaySetInstanceUID = jumpToDisplaySet; + // Set the activeTabName and expand the study + const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs); + if (!thumbnailLocation) { + console.warn('jumpToThumbnail: displaySet thumbnail not found.'); + + return; + } + const { tabName, StudyInstanceUID } = thumbnailLocation; + setActiveTabName(tabName); + const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + if (!studyExpanded) { + const updatedExpandedStudyInstanceUIDs = [...expandedStudyInstanceUIDs, StudyInstanceUID]; + setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); + } + }, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]); + const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs; return ( @@ -323,10 +426,11 @@ function PanelStudyBrowser({ onClickTab={clickedTabName => { setActiveTabName(clickedTabName); }} + onClickUntrack={onClickUntrack} onClickThumbnail={() => {}} onDoubleClickThumbnail={onDoubleClickThumbnailHandler} activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs} - showSettings={actionIcons.find(icon => icon.id === 'settings').value} + showSettings={actionIcons.find(icon => icon.id === 'settings')?.value} viewPresets={viewPresets} ThumbnailMenuItems={MoreDropdownMenu({ commandsManager, @@ -367,35 +471,36 @@ function _mapDataSourceStudies(studies) { }); } -function _mapDisplaySets(displaySets, thumbnailImageSrcMap) { +function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcMap, viewports) { const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; - displaySets .filter(ds => !ds.excludeFromThumbnailBrowser) .forEach(ds => { - const { thumbnailSrc, displaySetInstanceUID } = ds; // thumbnailImageSrcMap[ds.displaySetInstanceUID]; + const { thumbnailSrc, displaySetInstanceUID } = ds; const componentType = _getComponentType(ds); const array = componentType === 'thumbnail' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; + const loadingProgress = displaySetLoadingState?.[displaySetInstanceUID]; + array.push({ - displaySetInstanceUID: ds.displaySetInstanceUID, + displaySetInstanceUID, description: ds.SeriesDescription || '', seriesNumber: ds.SeriesNumber, modality: ds.Modality, - seriesDate: ds.SeriesDate, - seriesTime: ds.SeriesTime, - numInstances: parseInt(ds.numImageFrames), + seriesDate: formatDate(ds.SeriesDate), + numInstances: ds.numImageFrames, + loadingProgress, countIcon: ds.countIcon, - StudyInstanceUID: ds.StudyInstanceUID, messages: ds.messages, + StudyInstanceUID: ds.StudyInstanceUID, componentType, imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], dragData: { type: 'displayset', - displaySetInstanceUID: ds.displaySetInstanceUID, + displaySetInstanceUID, // .. Any other data to pass }, isHydratedForDerivedDisplaySet: ds.isHydrated, @@ -405,13 +510,44 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) { return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; } -const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; - function _getComponentType(ds) { if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { - // TODO probably others. return 'thumbnailNoImage'; } return 'thumbnail'; } + +function getImageIdForThumbnail(displaySet, imageIds) { + let imageId; + if (displaySet.isDynamicVolume) { + const timePoints = displaySet.dynamicVolumeInfo.timePoints; + const middleIndex = Math.floor(timePoints.length / 2); + const middleTimePointImageIds = timePoints[middleIndex]; + imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)]; + } else { + imageId = imageIds[Math.floor(imageIds.length / 2)]; + } + return imageId; +} + +function _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs) { + for (let t = 0; t < tabs.length; t++) { + const { studies } = tabs[t]; + + for (let s = 0; s < studies.length; s++) { + const { displaySets } = studies[s]; + + for (let d = 0; d < displaySets.length; d++) { + const displaySet = displaySets[d]; + + if (displaySet.displaySetInstanceUID === displaySetInstanceUID) { + return { + tabName: tabs[t].name, + StudyInstanceUID: studies[s].studyInstanceUid, + }; + } + } + } + } +} diff --git a/extensions/default/src/Panels/getStudiesForPatientByMRN.js b/extensions/default/src/Panels/getStudiesForPatientByMRN.js index be4ac157f..21a8bc6fd 100644 --- a/extensions/default/src/Panels/getStudiesForPatientByMRN.js +++ b/extensions/default/src/Panels/getStudiesForPatientByMRN.js @@ -1,12 +1,19 @@ async function getStudiesForPatientByMRN(dataSource, qidoForStudyUID) { - if (qidoForStudyUID && qidoForStudyUID.length && qidoForStudyUID[0].mrn) { - return dataSource.query.studies.search({ - patientId: qidoForStudyUID[0].mrn, - disableWildcard: true, - }); + if (!qidoForStudyUID?.length) { + return []; } - console.log('No mrn found for', qidoForStudyUID); - return qidoForStudyUID; + + const mrn = qidoForStudyUID[0].mrn; + + // if not defined or empty, return the original qidoForStudyUID + if (!mrn) { + return qidoForStudyUID; + } + + return dataSource.query.studies.search({ + patientId: mrn, + disableWildcard: true, + }); } export default getStudiesForPatientByMRN; diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index 1de7df574..e8c5aeb62 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -55,16 +55,8 @@ function ViewerLayout({ setRightPanelClosed ); - const leftPanelRef = useRef(null); - const rightPanelRef = useRef(null); - const handleMouseEnter = () => { - if ( - leftPanelRef.current?.contains(document.activeElement) || - rightPanelRef.current?.contains(document.activeElement) - ) { - (document.activeElement as HTMLElement)?.blur(); - } + (document.activeElement as HTMLElement)?.blur(); }; const LoadingIndicatorProgress = customizationService.getCustomization( @@ -79,6 +71,7 @@ function ViewerLayout({ useEffect(() => { document.body.classList.add('bg-black'); document.body.classList.add('overflow-hidden'); + return () => { document.body.classList.remove('bg-black'); document.body.classList.remove('overflow-hidden'); @@ -165,14 +158,12 @@ function ViewerLayout({ {hasLeftPanels ? ( <> -

- -
+ -
- -
+
) : null} diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 95dade7ce..1d19e0c2c 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^3.8.2", - "@cornerstonejs/tools": "^3.8.2", + "@cornerstonejs/core": "^3.8.4", + "@cornerstonejs/tools": "^3.8.4", "@ohif/core": "3.10.0-beta.144", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.144", "@ohif/extension-default": "3.10.0-beta.144", diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index ce0e95a80..dd236181f 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -1,23 +1,9 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; +import React from 'react'; import PropTypes from 'prop-types'; -import { useSystem, utils } from '@ohif/core'; -import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui'; -import { useViewportGrid } from '@ohif/ui-next'; -import { StudyBrowser } from '@ohif/ui-next'; - -import { useTrackedMeasurements } from '../../getContextModule'; -import { Separator } from '@ohif/ui-next'; -import { MoreDropdownMenu, PanelStudyBrowserHeader } from '@ohif/extension-default'; -import { defaultActionIcons } from './constants'; +import { useSystem } from '@ohif/core'; +import PanelStudyBrowser from '@ohif/extension-default/src/Panels/StudyBrowser/PanelStudyBrowser'; import { UntrackSeriesModal } from './untrackSeriesModal'; -const { formatDate, createStudyBrowserTabs } = utils; - -const DIALOG_ID = { - UNTRACK_SERIES: 'untrack-series', - REJECT_REPORT: 'ds-reject-sr', -}; +import { useTrackedMeasurements } from '../../getContextModule'; const thumbnailNoImageModalities = [ 'SR', @@ -32,8 +18,7 @@ const thumbnailNoImageModalities = [ ]; /** - * - * @param {*} param0 + * Panel component for the Study Browser with tracking capabilities */ export default function PanelStudyBrowserTracking({ getImageSrc, @@ -41,404 +26,11 @@ export default function PanelStudyBrowserTracking({ requestDisplaySetCreationForStudy, dataSource, }) { - const { servicesManager, commandsManager } = useSystem(); - const { - displaySetService, - uiDialogService, - hangingProtocolService, - uiNotificationService, - measurementService, - studyPrefetcherService, - customizationService, - uiModalService, - } = servicesManager.services; - const navigate = useNavigate(); - const studyMode = customizationService.getCustomization('studyBrowser.studyMode'); - - const { t } = useTranslation('Common'); - - // Normally you nest the components so the tree isn't so deep, and the data - // doesn't have to have such an intense shape. This works well enough for now. - // Tabs --> Studies --> DisplaySets --> Thumbnails - const { StudyInstanceUIDs } = useImageViewer(); - const [{ activeViewportId, viewports, isHangingProtocolLayout }, viewportGridService] = - useViewportGrid(); + const { servicesManager } = useSystem(); + const { displaySetService, uiModalService, measurementService } = servicesManager.services; const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); - - const [activeTabName, setActiveTabName] = useState(studyMode); - const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ - ...StudyInstanceUIDs, - ]); - const [studyDisplayList, setStudyDisplayList] = useState([]); - const [hasLoadedViewports, setHasLoadedViewports] = useState(false); - const [displaySets, setDisplaySets] = useState([]); - const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({}); - const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); - const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); - - const [viewPresets, setViewPresets] = useState( - customizationService.getCustomization('studyBrowser.viewPresets') - ); - - const [actionIcons, setActionIcons] = useState(defaultActionIcons); - - const updateActionIconValue = actionIcon => { - actionIcon.value = !actionIcon.value; - const newActionIcons = [...actionIcons]; - setActionIcons(newActionIcons); - }; - - const updateViewPresetValue = viewPreset => { - if (!viewPreset) { - return; - } - const newViewPresets = viewPresets.map(preset => { - preset.selected = preset.id === viewPreset.id; - return preset; - }); - setViewPresets(newViewPresets); - }; - - const onDoubleClickThumbnailHandler = displaySetInstanceUID => { - let updatedViewports = []; - const viewportId = activeViewportId; - try { - updatedViewports = hangingProtocolService.getViewportsRequireUpdate( - viewportId, - displaySetInstanceUID, - isHangingProtocolLayout - ); - } catch (error) { - console.warn(error); - uiNotificationService.show({ - title: 'Thumbnail Double Click', - message: - 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', - type: 'error', - duration: 3000, - }); - } - - viewportGridService.setDisplaySetsForViewports(updatedViewports); - }; - - const activeViewportDisplaySetInstanceUIDs = - viewports.get(activeViewportId)?.displaySetInstanceUIDs; - const { trackedSeries } = trackedMeasurements.context; - useEffect(() => { - setActiveTabName(studyMode); - }, [studyMode]); - - // ~~ studyDisplayList - useEffect(() => { - // Fetch all studies for the patient in each primary study - async function fetchStudiesForPatient(StudyInstanceUID) { - // current study qido - const qidoForStudyUID = await dataSource.query.studies.search({ - studyInstanceUid: StudyInstanceUID, - }); - - if (!qidoForStudyUID?.length) { - navigate('/notfoundstudy', '_self'); - throw new Error('Invalid study URL'); - } - - let qidoStudiesForPatient = qidoForStudyUID; - - // try to fetch the prior studies based on the patientID if the - // server can respond. - try { - qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID); - } catch (error) { - console.warn(error); - } - - const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); - const actuallyMappedStudies = mappedStudies.map(qidoStudy => { - return { - studyInstanceUid: qidoStudy.StudyInstanceUID, - date: formatDate(qidoStudy.StudyDate) || t('NoStudyDate'), - description: qidoStudy.StudyDescription, - modalities: qidoStudy.ModalitiesInStudy, - numInstances: qidoStudy.NumInstances, - }; - }); - - setStudyDisplayList(prevArray => { - const ret = [...prevArray]; - for (const study of actuallyMappedStudies) { - if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) { - ret.push(study); - } - } - return ret; - }); - } - - StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [StudyInstanceUIDs, getStudiesForPatientByMRN]); - - // ~~ Initial Thumbnails - useEffect(() => { - if (!hasLoadedViewports) { - if (activeViewportId) { - // Once there is an active viewport id, it means the layout is ready - // so wait a bit of time to allow the viewports preferential loading - // which improves user experience of responsiveness significantly on slower - // systems. - const delayMs = 250 + displaySetService.getActiveDisplaySets().length * 10; - window.setTimeout(() => setHasLoadedViewports(true), delayMs); - } - - return; - } - - let currentDisplaySets = displaySetService.activeDisplaySets; - // filter non based on the list of modalities that are supported by cornerstone - currentDisplaySets = currentDisplaySets.filter( - ds => !thumbnailNoImageModalities.includes(ds.Modality) - ); - - if (!currentDisplaySets.length) { - return; - } - - currentDisplaySets.forEach(async dSet => { - const newImageSrcEntry = {}; - const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - - const imageId = getImageIdForThumbnail(displaySet, imageIds); - - // TODO: Is it okay that imageIds are not returned here for SR displaySets? - if (!imageId || displaySet?.unsupported) { - return; - } - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - let { thumbnailSrc } = displaySet; - if (!thumbnailSrc && displaySet.getThumbnailSrc) { - thumbnailSrc = await displaySet.getThumbnailSrc(); - } - if (!thumbnailSrc) { - const thumbnailSrc = await getImageSrc(imageId); - displaySet.thumbnailSrc = thumbnailSrc; - } - newImageSrcEntry[dSet.displaySetInstanceUID] = thumbnailSrc; - - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); - }); - }, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]); - - // ~~ displaySets - useEffect(() => { - const currentDisplaySets = displaySetService.activeDisplaySets; - - if (!currentDisplaySets.length) { - return; - } - - const mappedDisplaySets = _mapDisplaySets( - currentDisplaySets, - displaySetsLoadingState, - thumbnailImageSrcMap, - trackedSeries, - viewports, - viewportGridService, - dataSource, - displaySetService, - uiDialogService, - uiNotificationService - ); - - setDisplaySets(mappedDisplaySets); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - displaySetService.activeDisplaySets, - displaySetsLoadingState, - trackedSeries, - viewports, - dataSource, - thumbnailImageSrcMap, - ]); - - // -- displaySetsLoadingState - useEffect(() => { - const { unsubscribe } = studyPrefetcherService.subscribe( - studyPrefetcherService.EVENTS.DISPLAYSET_LOAD_PROGRESS, - updatedDisplaySetLoadingState => { - const { displaySetInstanceUID, loadingProgress } = updatedDisplaySetLoadingState; - - setDisplaySetsLoadingState(prevState => ({ - ...prevState, - [displaySetInstanceUID]: loadingProgress, - })); - } - ); - - return () => unsubscribe(); - }, [studyPrefetcherService]); - - // ~~ subscriptions --> displaySets - useEffect(() => { - // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added - const SubscriptionDisplaySetsAdded = displaySetService.subscribe( - displaySetService.EVENTS.DISPLAY_SETS_ADDED, - data => { - if (!hasLoadedViewports) { - return; - } - const { displaySetsAdded, options } = data; - displaySetsAdded.forEach(async dSet => { - const displaySetInstanceUID = dSet.displaySetInstanceUID; - - const newImageSrcEntry = {}; - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - if (displaySet?.unsupported) { - return; - } - - if (options.madeInClient) { - setJumpToDisplaySet(displaySetInstanceUID); - } - - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - const imageId = getImageIdForThumbnail(displaySet, imageIds); - - // TODO: Is it okay that imageIds are not returned here for SR displaysets? - if (!imageId) { - return; - } - - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId); - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); - }); - } - ); - - return () => { - SubscriptionDisplaySetsAdded.unsubscribe(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]); - - useEffect(() => { - // TODO: Will this always hold _all_ the displaySets we care about? - // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` - const SubscriptionDisplaySetsChanged = displaySetService.subscribe( - displaySetService.EVENTS.DISPLAY_SETS_CHANGED, - changedDisplaySets => { - const mappedDisplaySets = _mapDisplaySets( - changedDisplaySets, - displaySetsLoadingState, - thumbnailImageSrcMap, - trackedSeries, - viewports, - viewportGridService, - dataSource, - displaySetService, - uiDialogService, - uiNotificationService - ); - - setDisplaySets(mappedDisplaySets); - } - ); - - const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( - displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, - () => { - const mappedDisplaySets = _mapDisplaySets( - displaySetService.getActiveDisplaySets(), - displaySetsLoadingState, - thumbnailImageSrcMap, - trackedSeries, - viewports, - viewportGridService, - dataSource, - displaySetService, - uiDialogService, - uiNotificationService - ); - - setDisplaySets(mappedDisplaySets); - } - ); - - return () => { - SubscriptionDisplaySetsChanged.unsubscribe(); - SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); - }; - }, [ - displaySetsLoadingState, - thumbnailImageSrcMap, - trackedSeries, - viewports, - dataSource, - displaySetService, - ]); - - const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); - - // TODO: Should not fire this on "close" - function _handleStudyClick(StudyInstanceUID) { - const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); - const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy - ? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] - : [...expandedStudyInstanceUIDs, StudyInstanceUID]; - - setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); - - if (!shouldCollapseStudy) { - const madeInClient = true; - requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); - } - } - - useEffect(() => { - if (jumpToDisplaySet) { - // Get element by displaySetInstanceUID - const displaySetInstanceUID = jumpToDisplaySet; - const element = document.getElementById(`thumbnail-${displaySetInstanceUID}`); - - if (element && typeof element.scrollIntoView === 'function') { - // TODO: Any way to support IE here? - element.scrollIntoView({ behavior: 'smooth' }); - - setJumpToDisplaySet(null); - } - } - }, [jumpToDisplaySet, expandedStudyInstanceUIDs, activeTabName]); - - useEffect(() => { - if (!jumpToDisplaySet) { - return; - } - - const displaySetInstanceUID = jumpToDisplaySet; - // Set the activeTabName and expand the study - const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs); - if (!thumbnailLocation) { - console.warn('jumpToThumbnail: displaySet thumbnail not found.'); - - return; - } - const { tabName, StudyInstanceUID } = thumbnailLocation; - setActiveTabName(tabName); - const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID); - if (!studyExpanded) { - const updatedExpandedStudyInstanceUIDs = [...expandedStudyInstanceUIDs, StudyInstanceUID]; - setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); - } - }, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]); - const onClickUntrack = displaySetInstanceUID => { const onConfirm = () => { const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); @@ -462,51 +54,68 @@ export default function PanelStudyBrowserTracking({ }); }; - return ( - <> - <> - - - + // Custom mapping function to add tracking data to display sets + const mapDisplaySetsWithTracking = ( + displaySets, + displaySetLoadingState, + thumbnailImageSrcMap, + viewports + ) => { + const thumbnailDisplaySets = []; + const thumbnailNoImageDisplaySets = []; + displaySets + .filter(ds => !ds.excludeFromThumbnailBrowser) + .forEach(ds => { + const { thumbnailSrc, displaySetInstanceUID } = ds; + const componentType = getComponentType(ds); - { - setActiveTabName(clickedTabName); - }} - onClickUntrack={displaySetInstanceUID => { - onClickUntrack(displaySetInstanceUID); - }} - onClickThumbnail={() => {}} - onDoubleClickThumbnail={onDoubleClickThumbnailHandler} - activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs} - showSettings={actionIcons.find(icon => icon.id === 'settings').value} - viewPresets={viewPresets} - ThumbnailMenuItems={MoreDropdownMenu({ - commandsManager, - servicesManager, - menuItemsKey: 'studyBrowser.thumbnailMenuItems', - })} - StudyMenuItems={MoreDropdownMenu({ - commandsManager, - servicesManager, - menuItemsKey: 'studyBrowser.studyMenuItems', - })} - /> - + const array = + componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; + + const loadingProgress = displaySetLoadingState?.[displaySetInstanceUID]; + + array.push({ + displaySetInstanceUID, + description: ds.SeriesDescription || '', + seriesNumber: ds.SeriesNumber, + modality: ds.Modality, + seriesDate: ds.SeriesDate ? new Date(ds.SeriesDate).toLocaleDateString() : '', + numInstances: ds.numImageFrames, + loadingProgress, + countIcon: ds.countIcon, + messages: ds.messages, + StudyInstanceUID: ds.StudyInstanceUID, + componentType, + imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], + dragData: { + type: 'displayset', + displaySetInstanceUID, + }, + isTracked: trackedSeries.includes(ds.SeriesInstanceUID), + isHydratedForDerivedDisplaySet: ds.isHydrated, + }); + }); + + return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; + }; + + // Override component type to use tracking specific components + const getComponentType = ds => { + if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { + return 'thumbnailNoImage'; + } + return 'thumbnailTracked'; + }; + + return ( + ); } @@ -518,120 +127,3 @@ PanelStudyBrowserTracking.propTypes = { getStudiesForPatientByMRN: PropTypes.func.isRequired, requestDisplaySetCreationForStudy: PropTypes.func.isRequired, }; - -function getImageIdForThumbnail(displaySet: any, imageIds: any) { - let imageId; - if (displaySet.isDynamicVolume) { - const timePoints = displaySet.dynamicVolumeInfo.timePoints; - const middleIndex = Math.floor(timePoints.length / 2); - const middleTimePointImageIds = timePoints[middleIndex]; - imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)]; - } else { - imageId = imageIds[Math.floor(imageIds.length / 2)]; - } - return imageId; -} - -/** - * Maps from the DataSource's format to a naturalized object - * - * @param {*} studies - */ -function _mapDataSourceStudies(studies) { - return studies.map(study => { - // TODO: Why does the data source return in this format? - return { - AccessionNumber: study.accession, - StudyDate: study.date, - StudyDescription: study.description, - NumInstances: study.instances, - ModalitiesInStudy: study.modalities, - PatientID: study.mrn, - PatientName: study.patientName, - StudyInstanceUID: study.studyInstanceUid, - StudyTime: study.time, - }; - }); -} - -function _mapDisplaySets( - displaySets, - displaySetLoadingState, - thumbnailImageSrcMap, - trackedSeriesInstanceUIDs, - viewports, // TODO: make array of `displaySetInstanceUIDs`? - viewportGridService, - dataSource, - displaySetService, - uiDialogService, - uiNotificationService -) { - const thumbnailDisplaySets = []; - const thumbnailNoImageDisplaySets = []; - displaySets - .filter(ds => !ds.excludeFromThumbnailBrowser) - .forEach(ds => { - const { thumbnailSrc, displaySetInstanceUID } = ds; // thumbnailImageSrcMap[ds.displaySetInstanceUID]; - const componentType = _getComponentType(ds); - - const array = - componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; - - const loadingProgress = displaySetLoadingState?.[displaySetInstanceUID]; - - const thumbnailProps = { - displaySetInstanceUID, - description: ds.SeriesDescription, - seriesNumber: ds.SeriesNumber, - modality: ds.Modality, - seriesDate: formatDate(ds.SeriesDate), - numInstances: ds.numImageFrames, - loadingProgress, - countIcon: ds.countIcon, - messages: ds.messages, - StudyInstanceUID: ds.StudyInstanceUID, - componentType, - imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], - dragData: { - type: 'displayset', - displaySetInstanceUID, - // .. Any other data to pass - }, - isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID), - isHydratedForDerivedDisplaySet: ds.isHydrated, - }; - - array.push(thumbnailProps); - }); - - return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; -} - -function _getComponentType(ds) { - if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { - return 'thumbnailNoImage'; - } - - return 'thumbnailTracked'; -} - -function _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs) { - for (let t = 0; t < tabs.length; t++) { - const { studies } = tabs[t]; - - for (let s = 0; s < studies.length; s++) { - const { displaySets } = studies[s]; - - for (let d = 0; d < displaySets.length; d++) { - const displaySet = displaySets[d]; - - if (displaySet.displaySetInstanceUID === displaySetInstanceUID) { - return { - tabName: tabs[t].name, - StudyInstanceUID: studies[s].studyInstanceUid, - }; - } - } - } - } -} diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index 85755a47e..b24e1f250 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -82,7 +82,8 @@ function modeFactory({ modeConfiguration }) { * Lifecycle hooks */ onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { - const { measurementService, toolbarService, toolGroupService } = servicesManager.services; + const { measurementService, toolbarService, toolGroupService, customizationService } = + servicesManager.services; measurementService.clearMeasurements(); @@ -135,6 +136,12 @@ function modeFactory({ modeConfiguration }) { 'WindowLevelRegion', ]); + customizationService.setCustomizations({ + 'panelSegmentation.disableEditing': { + $set: true, + }, + }); + // // ActivatePanel event trigger for when a segmentation or measurement is added. // // Do not force activation so as to respect the state the user may have left the UI in. // _activatePanelTriggersSubscriptions = [ diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index b2273b222..c0a4ada98 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -192,7 +192,7 @@ function modeFactory({ modeConfiguration }) { /** HangingProtocol used by the mode */ // Commented out to just use the most applicable registered hanging protocol // The example is used for a grid layout to specify that as a preferred layout - // hangingProtocol: ['@ohif/mnGrid'], + hangingProtocol: ['@ohif/mnGrid'], /** SopClassHandlers used by the mode */ sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler], }; diff --git a/platform/app/package.json b/platform/app/package.json index 48628ecca..90dd90f73 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -54,7 +54,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", + "@cornerstonejs/dicom-image-loader": "^3.8.4", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.10.0-beta.144", "@ohif/extension-cornerstone": "3.10.0-beta.144", diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 58d0ba81b..e595a4fe0 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -275,6 +275,20 @@ function ViewerViewportGrid(props: withAppTypes) { // Store previous isReferenceViewable values to avoid infinite loops const prevReferenceViewableMap = useRef(new Map()); + // Track viewports that need isReferenceViewable updates + const viewportsToUpdate = useRef(new Map()); + + // Apply isReferenceViewable updates in an effect, not during render + useEffect(() => { + const updates = viewportsToUpdate.current; + if (updates.size > 0) { + updates.forEach((isReferenceViewable, viewportId) => { + viewportGridService.setIsReferenceViewable(viewportId, isReferenceViewable); + prevReferenceViewableMap.current.set(viewportId, isReferenceViewable); + }); + viewportsToUpdate.current.clear(); + } + }); const getViewportPanes = useCallback(() => { const viewportPanes = []; @@ -313,7 +327,7 @@ function ViewerViewportGrid(props: withAppTypes) { uiNotificationService ); - // Only update isReferenceViewable if it's changed to avoid render loops + // Only queue isReferenceViewable updates if it's changed to avoid render loops // We need to handle both function and non-function values if (viewportId) { const prevValue = prevReferenceViewableMap.current.get(viewportId); @@ -323,8 +337,8 @@ function ViewerViewportGrid(props: withAppTypes) { // For non-functions, compare directly. For functions, we treat them as always different // (this is conservative but safe) if (!isSameFunction && prevValue !== isReferenceViewable) { - viewportGridService.setIsReferenceViewable(viewportId, isReferenceViewable); - prevReferenceViewableMap.current.set(viewportId, isReferenceViewable); + // Queue the update instead of doing it during render + viewportsToUpdate.current.set(viewportId, isReferenceViewable); } } diff --git a/platform/core/package.json b/platform/core/package.json index 7e710798c..728d92d73 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,7 +37,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^3.8.2", + "@cornerstonejs/dicom-image-loader": "^3.8.4", "@ohif/ui": "3.10.0-beta.144", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/defaults/hotkeyBindings.ts b/platform/core/src/defaults/hotkeyBindings.ts index fd62fc551..87d0b6750 100644 --- a/platform/core/src/defaults/hotkeyBindings.ts +++ b/platform/core/src/defaults/hotkeyBindings.ts @@ -195,6 +195,38 @@ const bindings = [ keys: ['n'], isEditable: true, }, + { + commandName: 'increaseBrushSize', + label: 'Increase Brush Size', + keys: [']'], + isEditable: true, + }, + { + commandName: 'decreaseBrushSize', + label: 'Decrease Brush Size', + keys: ['['], + isEditable: true, + }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'CircularEraser' }, + label: 'Eraser', + keys: ['e'], + isEditable: true, + }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'CircularBrush' }, + label: 'Brush', + keys: ['b'], + isEditable: true, + }, + { + commandName: 'addNewSegment', + label: 'Add New Segment', + keys: ['a'], + isEditable: true, + }, ]; export default bindings; diff --git a/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx b/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx index 026307262..0ad5c2bfd 100644 --- a/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx +++ b/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx @@ -2,18 +2,111 @@ import React, { useState, useEffect } from 'react'; import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from '../Dialog/Dialog'; +import { Dialog, DialogContent } from '../Dialog/Dialog'; import { ScrollArea } from '../ScrollArea/ScrollArea'; -import { Icons } from '../Icons'; +import { Button } from '../Button/Button'; const isProduction = process.env.NODE_ENV === 'production'; +/** + * Parses an error stack trace to extract important information + * Extracts the first function name from the stack trace + */ +const parseErrorStack = (error: ErrorBoundaryError) => { + if (!error.stack) { + return { filePath: null, errorTitle: null, code: null, firstFilename: null }; + } + + const stack = error.stack; + const stackLines = stack.split('\n'); + + // Extract error message from first line + const errorMessage = stackLines[0].trim(); + + // Extract first function name from the stack trace + let firstFilename = null; + + // Find the first stack line (starts with " at ") + for (let i = 1; i < stackLines.length; i++) { + const line = stackLines[i].trim(); + if (line.startsWith('at ')) { + // Extract function name pattern + const match = line.match(/at\s+([^\s(]+)[\s(]/); + if (match && match[1]) { + firstFilename = match[1]; + break; + } + } + } + + // Sanitize stack trace for display - safer approach to avoid ReDoS + const sanitizedStack = stackLines + .map(line => { + // Limit line length to prevent excessive processing + const limitedLine = line.substring(0, 2000); + + // Process each part separately to avoid complex regex patterns + if (limitedLine.includes('(')) { + // Extract filename from paths in parentheses + const openParenIndex = limitedLine.indexOf('('); + const closeParenIndex = limitedLine.indexOf(')', openParenIndex); + + if (openParenIndex >= 0 && closeParenIndex > openParenIndex) { + const pathInParens = limitedLine.substring(openParenIndex + 1, closeParenIndex); + + // Find the last segment after slash or backslash + const lastSlashIndex = Math.max( + pathInParens.lastIndexOf('/'), + pathInParens.lastIndexOf('\\') + ); + + if (lastSlashIndex >= 0) { + const filename = pathInParens.substring(lastSlashIndex + 1); + return ( + limitedLine.substring(0, openParenIndex + 1) + + filename + + limitedLine.substring(closeParenIndex) + ); + } + } + } + + // Handle the "at Function path:line:column" format + if (limitedLine.includes(' at ')) { + const atIndex = limitedLine.indexOf(' at '); + const afterAt = limitedLine.substring(atIndex + 4).trim(); + + // Split by whitespace to separate function and path + const spaceAfterFunc = afterAt.indexOf(' '); + + if (spaceAfterFunc > 0) { + const funcName = afterAt.substring(0, spaceAfterFunc); + const path = afterAt.substring(spaceAfterFunc + 1); + + // Check if this is a path with line/column numbers + if (path.includes(':') && /.*:[0-9]+:[0-9]+/.test(path)) { + // Find the last segment after slash or backslash + const lastSlashIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + + if (lastSlashIndex >= 0) { + const filename = path.substring(lastSlashIndex + 1); + return limitedLine.substring(0, atIndex + 4) + funcName + ' ' + filename; + } + } + } + } + + return limitedLine; + }) + .join('\n'); + + return { + errorTitle: errorMessage, + code: sanitizedStack, + firstFilename: firstFilename, + }; +}; + interface ErrorBoundaryError extends Error { message: string; stack?: string; @@ -45,14 +138,13 @@ const DefaultFallback = ({ const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`; const subtitle = t('Sorry, something went wrong there. Try again.'); - const copyErrorDetails = () => { - const errorDetails = ` -Context: ${context} -Error Message: ${error.message} -Stack: ${error.stack} - `; - navigator.clipboard.writeText(errorDetails); - toast.success(t('Copied to clipboard')); + const { errorTitle, code, firstFilename } = parseErrorStack(error); + + const copyErrorToClipboard = () => { + if (code) { + navigator.clipboard.writeText(code); + toast.success(t('Error copied to clipboard')); + } }; useEffect(() => { @@ -64,55 +156,76 @@ Stack: ${error.stack} }, duration: 0, }); - }, [error]); + }, [error, subtitle, t, title]); if (isProduction) { return null; } return ( - <> - + e.preventDefault()} > - - - -
{title}
- -
+
+
+

+ {errorTitle || error.message || title} +

+
+
- {subtitle} -
- - -
-
-

- {t('Context')}: {context} -

-

- {t('Error Message')}: {error.message} -

-
-                  Stack: {error.stack}
-                
+ {/* Code block */} + {code && ( + <> + +
+
+ {firstFilename || 'Error Stack'} +
+
-
- - -
- +
+ {code.split('\n').map((line, index) => ( +
+ {line} +
+ ))} +
+ + + )} + + {/* Footer */} +
+ +
+ + ); }; @@ -164,9 +277,9 @@ const ErrorBoundary = ({ }; }, []); - const onErrorHandler = (error: ErrorBoundaryError, componentStack: string) => { + const onErrorHandler = (error: ErrorBoundaryError, componentStack: string | null) => { console.debug(`${context} Error Boundary`, error, componentStack, context); - onError(error, componentStack, context); + onError(error, componentStack || '', context); }; return ( @@ -179,7 +292,7 @@ const ErrorBoundary = ({ /> )} onReset={onResetHandler} - onError={onErrorHandler} + onError={(error, info) => onErrorHandler(error as ErrorBoundaryError, info.componentStack)} > <> {children} diff --git a/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx b/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx index c5a9fb943..783335850 100644 --- a/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx +++ b/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx @@ -124,24 +124,32 @@ const Trigger = ({ }: TriggerProps) => { const { isOpen } = useLayoutSelector(); - // Style constants matching ToolButton - const baseClasses = '!rounded-lg inline-flex items-center justify-center'; - const defaultClasses = - 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight'; - const activeClasses = 'bg-background text-foreground/80'; - const disabledClasses = - 'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed'; - const buttonSizeClass = 'w-10 h-10'; - const iconSizeClass = 'h-7 w-7'; - - const buttonClasses = cn( - baseClasses, - buttonSizeClass, - disabled ? disabledClasses : isOpen ? activeClasses : defaultClasses - ); - const hasTooltip = tooltip || (disabled && disabledText); + const button = ( + + ); + + // If user passed children (custom button), just wrap it directly if (children) { return ( - - + if (!isOpen && hasTooltip) { + return ( + + - + {button} - - - {hasTooltip && ( + {tooltip &&
{tooltip}
} {disabled && disabledText &&
{disabledText}
}
- )} - + + ); + } + + return ( + + {button} + ); }; diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx index c385e3b3a..5ab8d23d2 100644 --- a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx @@ -47,7 +47,7 @@ export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ childr return (
-
+
{allowAddSegment ? (
- {t('Add segment')} + {t('Add segment')}
{segmentation && ( diff --git a/yarn.lock b/yarn.lock index a80cb6692..23960f045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1393,10 +1393,10 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.8.2.tgz#44faeeb1cf3a384254630cf17c015e7427853feb" - integrity sha512-6bxIa1BBvWD/X06MbB4QBOScTYmA1eTdLwg9jL+dHw5od8/CQhR22pfa0ZWsa8lkfFAMAdC1cU8BbiPNadG/0Q== +"@cornerstonejs/adapters@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.8.4.tgz#14e22bef3abaf14cde52ecf8b05b43a687b0dece" + integrity sha512-TFTvNZQBW54rEcyjDfyHZYYvIPHVGZoUCtjW3isHfpoXkjHz2cliwC55e0kcNJ0zbyI6H41c4VsYH8h2MTYOdA== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1404,10 +1404,10 @@ gl-matrix "^3.4.3" ndarray "^1.0.19" -"@cornerstonejs/ai@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.8.2.tgz#889d2b138e0f2d76a5355fc37675f915527c5d54" - integrity sha512-B9Mr3BPWOQT+GKhG79v9xsuEfgh8a/YCK/qaseFfUGahxD/7QEcu49MuzIdigbA1rbRA3OELL0ZjXxVx3ON27A== +"@cornerstonejs/ai@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.8.4.tgz#c998cdfbd1cae01e3ea9ed61810c657d91112f75" + integrity sha512-scc2srdL1BqNTSFEro4y7UlNBsPtBVSF31eT4SC1psxZQcr/iyaCKuOQg/hglk1yobFkdIKvjRAvvqvYmGduQw== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1443,20 +1443,20 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.8.2.tgz#27f7bc10603f3ce6fd41057ec4ef52b09e3a62d5" - integrity sha512-zhtCXXXZMGooVYCmsfL7sC/o7wSFxXAfFm4rOp92t5m8n8UFbtmcLAB167/EEp/csHiRuv0PR/dOgd6y9tLb0A== +"@cornerstonejs/core@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.8.4.tgz#bbc6863cb3f2e78f5f4c7a1a5a9287c091287228" + integrity sha512-8spQvERZRn4I/0EhzTNZ5ruPBrVn5F4msb/q0ttBtiVEa9JaVwoWh3yEcO4q5NpK7t1BhpfSPMKwU98Dy02ugQ== dependencies: "@kitware/vtk.js" "32.12.1" comlink "^4.4.1" gl-matrix "^3.4.3" loglevel "^1.9.2" -"@cornerstonejs/dicom-image-loader@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.8.2.tgz#dcfefe7fb77b63286e84c5bfbdaecd0647ef4e3f" - integrity sha512-K0z5N+X6GpuRTfZhyuqpLIl+Y4JePyybQof/ZZ4aqwEIUm9NjXAYEUiYZEQQGtLItkPw+Vl7TbWA0FeI5GAC3Q== +"@cornerstonejs/dicom-image-loader@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.8.4.tgz#c4f7af80a40a1f70deff37ddfc50447cc42422e7" + integrity sha512-N2D4U8JDhen+dCY1HrLOoK8W3CD2AAf/Cd+GiJMEGjtrihIuQ+Ha+hZTVQzaDMc7v2Ruvx2DpJ7i9HUJXoyWHw== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" @@ -1468,25 +1468,25 @@ pako "^2.0.4" uuid "^9.0.0" -"@cornerstonejs/labelmap-interpolation@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.8.2.tgz#2fe31e711d16366a7d591e414612205d0eeb7a38" - integrity sha512-nvMHY4X6crsTL8EXKsKxXfVP/T04cCxCU90tsg7ceDi5mZ9qh6qnE0r8XOTclx/SsQ/YrwEaOIaflrfLKft19g== +"@cornerstonejs/labelmap-interpolation@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.8.4.tgz#5c18761a7bbb8e3e76bfbf22b2205c1c5e8bee6e" + integrity sha512-KKDTeVJg1s0trplkaHglBm0foQ3XL9BhsTfatFCTzgIOCT0wolrKbr+xAKIeZuBQifiFazDWpYaeBHRbkWEEMA== dependencies: "@itk-wasm/morphological-contour-interpolation" "1.1.0" itk-wasm "1.0.0-b.165" -"@cornerstonejs/polymorphic-segmentation@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.8.2.tgz#faae193c34e7624d8fc55dabe3bd75b6d4b4e1db" - integrity sha512-svlT/loFoB6t0r9dt31bMOkin82LftTF1J8ZWjJTOrbky2HxSyM4lnMbjQZbdhZDkhdQq0imQEOVIYNyhUAWBg== +"@cornerstonejs/polymorphic-segmentation@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.8.4.tgz#5c7578eedd4e7ac78250f06ccecb3950ddf5ba28" + integrity sha512-+3o280NOniUkG9GcWTeTi6x3qy8Rm7WpYFycWpxXIufJzVudJi0zAR+EIWDq/uvTQ3DH1SrBDe4wzYNqD+yirw== dependencies: "@icr/polyseg-wasm" "0.4.0" -"@cornerstonejs/tools@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.8.2.tgz#661cd986a19e5e3f49fffedea365aa290c0646fb" - integrity sha512-da8pJGv7b+sjAGIomH5CyM8+xHOvjhLipY4i/gp7Z6f5Wr5b/gYWc/l0CTD9G0aVFkyP06scGaBjwv0Ei1mqMQ== +"@cornerstonejs/tools@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.8.4.tgz#4676ba9d7fcae22f236bab2314118b5d06cc973e" + integrity sha512-yr8RLtnxGh9BsPOFpX+zwT++UGJDqWMo7noWGzxkuISDriLr+HGZI8gL910Mw62obaF8r9qpeOSvSOzEHN3nSw== dependencies: "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1"