From ec79b5523e262efd775149cd86b87b870d05d6bc Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 17 Mar 2025 22:34:34 -0400 Subject: [PATCH] feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865) --- bun.lock | 402 +++++++++++------- .../cornerstone-dicom-pmap/package.json | 4 +- extensions/cornerstone-dicom-seg/package.json | 4 +- .../src/getToolbarModule.ts | 7 + extensions/cornerstone-dicom-sr/package.json | 6 +- .../cornerstone-dynamic-volume/package.json | 4 +- extensions/cornerstone/package.json | 11 +- extensions/cornerstone/src/commandsModule.ts | 125 +++++- .../CustomDropdownMenuContent.tsx | 127 ++++++ .../CustomSegmentStatisticsHeader.tsx | 138 ++++++ .../segmentationPanelCustomization.tsx | 99 +---- .../cornerstone/src/getToolbarModule.tsx | 13 +- extensions/cornerstone/src/init.tsx | 97 +++-- .../cornerstone/src/initCornerstoneTools.js | 8 + .../cornerstone/src/initMeasurementService.ts | 9 + .../src/panels/PanelSegmentation.tsx | 316 +++++++------- .../SegmentationService.ts | 30 ++ .../utils/generateSegmentationCSVReport.ts | 139 ++++++ .../SegmentBidirectional.ts | 185 ++++++++ .../constants/supportedTools.js | 1 + .../measurementServiceMappingsFactory.ts | 23 + .../src/utils/updateSegmentationStats.ts | 185 ++++++++ .../src/Panels/WrappedPanelStudyBrowser.tsx | 1 - .../default/src/Toolbar/ToolBoxWrapper.tsx | 4 +- extensions/measurement-tracking/package.json | 4 +- .../PanelStudyBrowserTracking/index.tsx | 1 - .../PanelROIThresholdExport.tsx | 14 +- extensions/tmtv/src/commandsModule.ts | 192 +-------- .../tmtv/src/utils/calculateSUVPeakWorker.js | 209 --------- extensions/tmtv/src/utils/calculateTMTV.ts | 42 -- .../tmtv/src/utils/handleROIThresholding.ts | 78 +--- modes/basic-test-mode/src/toolbarButtons.ts | 2 +- modes/segmentation/src/index.tsx | 23 +- modes/segmentation/src/initToolGroups.ts | 3 + modes/segmentation/src/toolbarButtons.ts | 36 +- platform/app/package.json | 2 +- platform/core/package.json | 2 +- .../services/ToolBarService/ToolbarService.ts | 10 +- .../_shared/pubSubServiceInterface.ts | 38 +- .../UI/Migration-3p10-SegmentationTable.md | 83 ++++ ...3p10-Tourss.md => Migration-3p10-Tours.md} | 0 .../pages/patterns/patterns-segmentation.tsx | 262 +++++++++++- platform/ui-next/assets/data/index.ts | 294 +++++++++++++ platform/ui-next/package.json | 1 + .../src/components/DataRow/DataRow.tsx | 17 +- .../src/components/HoverCard/HoverCard.tsx | 27 ++ .../ui-next/src/components/HoverCard/index.ts | 1 + .../ui-next/src/components/Icons/Icons.tsx | 6 + .../components/Icons/Sources/JumpToSlice.tsx | 69 +++ .../src/components/Icons/Sources/Redo.tsx | 51 +++ .../src/components/Icons/Sources/Undo.tsx | 51 +++ .../SegmentationTable/AddSegmentRow.tsx | 55 ++- .../SegmentationTable/AddSegmentationRow.tsx | 4 +- .../SegmentationTable/SegmentStatistics.tsx | 104 +++++ .../SegmentationCollapsed.tsx | 162 ++++++- .../SegmentationExpanded.tsx | 165 +++++-- .../SegmentationTable/SegmentationHeader.tsx | 28 +- .../SegmentationSegments.tsx | 134 ++++-- .../SegmentationSelectorHeader.tsx | 108 ----- .../SegmentationTable/SegmentationTable.tsx | 141 ++++-- .../SegmentationTableConfig.tsx | 261 ++++++------ .../SegmentationTableContext.ts | 80 ---- .../contexts/SegmentStatisticsContext.tsx | 45 ++ .../contexts/SegmentationExpandedContext.tsx | 40 ++ .../contexts/SegmentationTableContext.tsx | 129 ++++++ .../SegmentationTable/contexts/index.ts | 15 + .../src/components/SegmentationTable/index.ts | 12 +- .../src/components/Viewport/ViewportPane.tsx | 2 +- platform/ui-next/src/components/index.ts | 11 +- platform/ui-next/src/index.ts | 6 +- platform/ui-next/src/utils/index.ts | 3 +- platform/ui-next/src/utils/roundNumber.ts | 43 ++ yarn.lock | 162 ++++++- 73 files changed, 3603 insertions(+), 1563 deletions(-) create mode 100644 extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx create mode 100644 extensions/cornerstone/src/customizations/CustomSegmentStatisticsHeader.tsx create mode 100644 extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts create mode 100644 extensions/cornerstone/src/utils/measurementServiceMappings/SegmentBidirectional.ts create mode 100644 extensions/cornerstone/src/utils/updateSegmentationStats.ts delete mode 100644 extensions/tmtv/src/utils/calculateSUVPeakWorker.js delete mode 100644 extensions/tmtv/src/utils/calculateTMTV.ts create mode 100644 platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-SegmentationTable.md rename platform/docs/docs/migration-guide/3p9-to-3p10/UI/{Migration-3p10-Tourss.md => Migration-3p10-Tours.md} (100%) create mode 100644 platform/ui-next/src/components/HoverCard/HoverCard.tsx create mode 100644 platform/ui-next/src/components/HoverCard/index.ts create mode 100644 platform/ui-next/src/components/Icons/Sources/JumpToSlice.tsx create mode 100644 platform/ui-next/src/components/Icons/Sources/Redo.tsx create mode 100644 platform/ui-next/src/components/Icons/Sources/Undo.tsx create mode 100644 platform/ui-next/src/components/SegmentationTable/SegmentStatistics.tsx delete mode 100644 platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx delete mode 100644 platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts create mode 100644 platform/ui-next/src/components/SegmentationTable/contexts/SegmentStatisticsContext.tsx create mode 100644 platform/ui-next/src/components/SegmentationTable/contexts/SegmentationExpandedContext.tsx create mode 100644 platform/ui-next/src/components/SegmentationTable/contexts/SegmentationTableContext.tsx create mode 100644 platform/ui-next/src/components/SegmentationTable/contexts/index.ts create mode 100644 platform/ui-next/src/utils/roundNumber.ts diff --git a/bun.lock b/bun.lock index c26f809ba..a5532c046 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,7 @@ }, "addOns/externals/devDependencies": { "name": "@externals/devDependencies", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "@kitware/vtk.js": "32.12.0", @@ -121,20 +121,21 @@ }, "addOns/externals/dicom-microscopy-viewer": { "name": "@externals/dicom-microscopy-viewer", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "dicom-microscopy-viewer": "^0.46.1", }, }, "extensions/cornerstone": { "name": "@ohif/extension-cornerstone", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.5.0", - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/polymorphic-segmentation": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/labelmap-interpolation": "^3.7.0", + "@cornerstonejs/polymorphic-segmentation": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "^1.4.1", @@ -151,9 +152,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.5.0", - "@ohif/core": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@cornerstonejs/dicom-image-loader": "^3.7.0", + "@ohif/core": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -165,19 +166,19 @@ }, "extensions/cornerstone-dicom-pmap": { "name": "@ohif/extension-cornerstone-dicom-pmap", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.5.0", - "@cornerstonejs/core": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -188,16 +189,16 @@ }, "extensions/cornerstone-dicom-rt": { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -208,19 +209,19 @@ }, "extensions/cornerstone-dicom-seg": { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.5.0", - "@cornerstonejs/core": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -231,19 +232,19 @@ }, "extensions/cornerstone-dicom-sr": { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.5.0", - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-measurement-tracking": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-measurement-tracking": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -253,19 +254,19 @@ }, "extensions/cornerstone-dynamic-volume": { "name": "@ohif/extension-cornerstone-dynamic-volume", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -275,7 +276,7 @@ }, "extensions/default": { "name": "@ohif/extension-default", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/calculate-suv": "^1.1.0", @@ -283,8 +284,8 @@ "lodash.uniqby": "^4.7.0", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", "dcmjs": "*", "dicomweb-client": "^0.10.4", "prop-types": "^15.6.2", @@ -298,7 +299,7 @@ }, "extensions/dicom-microscopy": { "name": "@ohif/extension-dicom-microscopy", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/codec-charls": "^1.2.3", @@ -309,10 +310,10 @@ "mathjs": "^12.4.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "prop-types": "^15.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -323,14 +324,14 @@ }, "extensions/dicom-pdf": { "name": "@ohif/extension-dicom-pdf", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -340,14 +341,14 @@ }, "extensions/dicom-video": { "name": "@ohif/extension-dicom-video", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -357,20 +358,20 @@ }, "extensions/measurement-tracking": { "name": "@ohif/extension-measurement-tracking", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/ui": "3.10.0-beta.125", "@xstate/react": "^3.2.2", "xstate": "^4.10.0", }, "peerDependencies": { - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "classnames": "^2.3.2", "dcmjs": "*", "lodash.debounce": "^4.0.8", @@ -383,14 +384,14 @@ }, "extensions/test-extension": { "name": "@ohif/extension-test", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "0.38.0", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -400,14 +401,14 @@ }, "extensions/tmtv": { "name": "@ohif/extension-tmtv", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -417,7 +418,7 @@ }, "modes/basic-dev-mode": { "name": "@ohif/mode-basic-dev-mode", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -427,17 +428,17 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", }, }, "modes/basic-test-mode": { "name": "@ohif/mode-test", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -447,19 +448,19 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", - "@ohif/extension-measurement-tracking": "3.10.0-beta.121", - "@ohif/extension-test": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", + "@ohif/extension-measurement-tracking": "3.10.0-beta.125", + "@ohif/extension-test": "3.10.0-beta.125", }, }, "modes/longitudinal": { "name": "@ohif/mode-longitudinal", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -469,32 +470,32 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", - "@ohif/extension-measurement-tracking": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", + "@ohif/extension-measurement-tracking": "3.10.0-beta.125", }, }, "modes/microscopy": { "name": "@ohif/mode-microscopy", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.125", }, }, "modes/preclinical-4d": { "name": "@ohif/mode-preclinical-4d", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", }, @@ -503,17 +504,17 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-tmtv": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-tmtv": "3.10.0-beta.125", }, }, "modes/segmentation": { "name": "@ohif/mode-segmentation", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -545,19 +546,19 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", }, }, "modes/tmtv": { "name": "@ohif/mode-tmtv", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next": "^17.0.3", @@ -567,43 +568,43 @@ "webpack-merge": "^5.7.3", }, "peerDependencies": { - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", - "@ohif/extension-measurement-tracking": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", + "@ohif/extension-measurement-tracking": "3.10.0-beta.125", }, }, "platform/app": { "name": "@ohif/app", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "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.5.0", + "@cornerstonejs/dicom-image-loader": "^3.7.0", "@emotion/serialize": "^1.1.3", - "@ohif/core": "3.10.0-beta.121", - "@ohif/extension-cornerstone": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.121", - "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.121", - "@ohif/extension-default": "3.10.0-beta.121", - "@ohif/extension-dicom-microscopy": "3.10.0-beta.121", - "@ohif/extension-dicom-pdf": "3.10.0-beta.121", - "@ohif/extension-dicom-video": "3.10.0-beta.121", - "@ohif/extension-test": "3.10.0-beta.121", - "@ohif/i18n": "3.10.0-beta.121", - "@ohif/mode-basic-dev-mode": "3.10.0-beta.121", - "@ohif/mode-longitudinal": "3.10.0-beta.121", - "@ohif/mode-microscopy": "3.10.0-beta.121", - "@ohif/mode-test": "3.10.0-beta.121", - "@ohif/ui": "3.10.0-beta.121", - "@ohif/ui-next": "3.10.0-beta.121", + "@ohif/core": "3.10.0-beta.125", + "@ohif/extension-cornerstone": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.125", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", + "@ohif/extension-default": "3.10.0-beta.125", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.125", + "@ohif/extension-dicom-pdf": "3.10.0-beta.125", + "@ohif/extension-dicom-video": "3.10.0-beta.125", + "@ohif/extension-test": "3.10.0-beta.125", + "@ohif/i18n": "3.10.0-beta.125", + "@ohif/mode-basic-dev-mode": "3.10.0-beta.125", + "@ohif/mode-longitudinal": "3.10.0-beta.125", + "@ohif/mode-microscopy": "3.10.0-beta.125", + "@ohif/mode-test": "3.10.0-beta.125", + "@ohif/ui": "3.10.0-beta.125", + "@ohif/ui-next": "3.10.0-beta.125", "@svgr/webpack": "^8.1.0", "@types/react": "^18.3.3", "classnames": "^2.3.2", @@ -644,7 +645,7 @@ }, "platform/cli": { "name": "@ohif/cli", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "bin": { "ohif-cli": "src/index.js", }, @@ -668,7 +669,7 @@ }, "platform/core": { "name": "@ohif/core", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "dcmjs": "*", @@ -693,15 +694,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.5.0", - "@ohif/ui": "3.10.0-beta.121", + "@cornerstonejs/dicom-image-loader": "^3.7.0", + "@ohif/ui": "3.10.0-beta.125", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21", }, }, "platform/i18n": { "name": "@ohif/i18n", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@babel/runtime": "^7.20.13", "i18next-locize-backend": "^2.0.0", @@ -726,7 +727,7 @@ }, "platform/ui": { "name": "@ohif/ui", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@testing-library/react": "^13.1.0", "browser-detect": "^0.2.28", @@ -786,13 +787,14 @@ }, "platform/ui-next": { "name": "@ohif/ui-next", - "version": "3.10.0-beta.121", + "version": "3.10.0-beta.125", "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.0.7", @@ -1093,7 +1095,7 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.5.0", "", { "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.5.0", "@cornerstonejs/tools": "^3.5.0" } }, "sha512-xkixGbCDS2wwAHmggx1mW0ewEjWGntbE5L1svdkmRpGBvTtEkQJbNkNG6dUiHXWZ9mICxwahyiMc7YVDdH3tfA=="], + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@3.7.0", "", { "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.7.0", "@cornerstonejs/tools": "^3.7.0" } }, "sha512-7ROFACNjT4W8dukUoKwggHhl+tjg0rdDpYDq8rM6b0c0gO2MgYKbU55t9MoT682yIuebt81jCsAbFSRGVzf45Q=="], "@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="], @@ -1105,13 +1107,15 @@ "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], - "@cornerstonejs/core": ["@cornerstonejs/core@3.5.0", "", { "dependencies": { "@kitware/vtk.js": "32.12.0", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-Ai4dyxZameDz+cp3nBST6KfK1WK/Ic5R6DoTFrBlph4DuK7IUradAx2N9uEydhUJTuF6606mF3VeaAdPG9Hfig=="], + "@cornerstonejs/core": ["@cornerstonejs/core@3.7.0", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", "gl-matrix": "^3.4.3", "loglevel": "^1.9.2" } }, "sha512-eRCovUYO3JvFV3Dzuwc6EWn9bpm+fvp/x6SXvr3MwiO+LRw1o5bmZfeh0+m4+5xzvQJPbjNYiXPPEpLYGQIOqQ=="], - "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.5.0", "", { "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", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "dicom-parser": "^1.8.9" } }, "sha512-Jel/an3IHe68sebtRLYB3/aJ1Dggw8D11cWBiBUqzXfxGTlg1SUd3KnvU5OrgNJ+ZpNUEdwCrR3OoiU65hL6UA=="], + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@3.7.0", "", { "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.7.0" } }, "sha512-MSv5A7ZcAp7uVm1ofrTaQppPm1DyOAXN0BdX8a2/fMZ2Ud4OmEtk0aoUlaJ52/hQtUvfdCqfYObBos2oBIMQ1w=="], - "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.5.0", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "@cornerstonejs/tools": "^3.5.0", "@kitware/vtk.js": "32.12.0" } }, "sha512-S2zqXNKlFURPCq99fPj5CAnBO7chuVk7bIRWmBOBYQL21TnL4rzyvY+Td5pvHBal4sZ8uTs6PW72rrwGCospAQ=="], + "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@3.7.0", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@cornerstonejs/tools": "^3.7.0", "@kitware/vtk.js": "32.12.1" } }, "sha512-/1l1XsKFYeov+hrMKindJ4n6l4H2zcXFwcO+wtqE4kmfoFhXRN6RSlIXbCRzEZtFCmh0hafg7SUUcXxr0QoClQ=="], - "@cornerstonejs/tools": ["@cornerstonejs/tools@3.5.0", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.5.0", "@kitware/vtk.js": "32.12.0", "@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-wyT7runGpPvY9dMJOUBAtxDBSqabk6Ks/SNjbTGXAIUemAYuonP+33pN4zK/4n/zFfa0gpKFP7LFc461xFK16g=="], + "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@3.7.0", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@cornerstonejs/tools": "^3.7.0", "@kitware/vtk.js": "32.12.1" } }, "sha512-MYb8aXE2EwfsyPmhvhTmNdTkCq5pIlMJuVRmT6GuQ4nf2dIhx5AeZ+Ya/TRaNNumV2Fx9/9PetI8+Z8XJLAILw=="], + + "@cornerstonejs/tools": ["@cornerstonejs/tools@3.7.0", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^3.7.0", "@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-72lbmaAm/dnOANAUNc0OK+9aL2pFklaWBLR+kOKn8OeSh1i9RRTSEaP8zWzStaSNAcdhwQ+JOQzcMR40Eo8oNQ=="], "@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=="], @@ -1539,7 +1543,7 @@ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw=="], @@ -1565,6 +1569,8 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ=="], + "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], @@ -1575,7 +1581,7 @@ "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], @@ -1593,7 +1599,7 @@ "@radix-ui/react-slider": ["@radix-ui/react-slider@1.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g=="], @@ -3555,7 +3561,7 @@ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - "itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="], + "itk-wasm": ["itk-wasm@1.0.0-b.165", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-Rq0+AL2BuRqcFh9r3h69pB3nt84tw/gTxnI6GjIOYw5Bfah9qO+C8hRJpD8aWNBa5o3wNHRAMVlp0pt/L4K8gg=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -5381,6 +5387,14 @@ "@cornerstonejs/adapters/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="], + "@cornerstonejs/core/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="], + + "@cornerstonejs/labelmap-interpolation/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="], + + "@cornerstonejs/polymorphic-segmentation/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="], + + "@cornerstonejs/tools/@kitware/vtk.js": ["@kitware/vtk.js@32.12.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg=="], + "@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@cypress/xvfb/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -5411,6 +5425,8 @@ "@itk-wasm/dam/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="], + "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -5527,6 +5543,38 @@ "@ohif/ui/postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="], + + "@radix-ui/react-hover-card/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], + + "@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + + "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@rollup/plugin-node-resolve/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], @@ -5985,8 +6033,6 @@ "istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "itk-wasm/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], - "itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], "itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], @@ -6629,6 +6675,14 @@ "@babel/register/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "@cornerstonejs/core/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@cornerstonejs/labelmap-interpolation/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@cornerstonejs/polymorphic-segmentation/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@cornerstonejs/tools/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -6645,6 +6699,12 @@ "@itk-wasm/dam/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -6719,6 +6779,14 @@ "@ohif/ui/postcss-loader/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + + "@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + + "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + + "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "@storybook/builder-webpack5/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@storybook/builder-webpack5/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], @@ -7051,8 +7119,6 @@ "ipfs-unixfs-exporter/p-queue/p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], - "itk-wasm/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "jake/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -7465,6 +7531,10 @@ "@itk-wasm/dam/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "@lerna/child-process/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "@lerna/create/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -7729,6 +7799,8 @@ "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "@itk-wasm/morphological-contour-interpolation/itk-wasm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], "@ohif/ui/postcss-loader/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index 69b5835bb..c4a2be5f3 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.5.0", - "@cornerstonejs/core": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", "@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 c14f7bf17..1126c98ff 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.5.0", - "@cornerstonejs/core": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", "@kitware/vtk.js": "32.12.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts index fdd32c1c7..681a5fd95 100644 --- a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -42,6 +42,13 @@ export function getToolbarModule({ servicesManager }: withAppTypes) { }; } + if (!toolNames) { + return { + disabled: false, + // isActive: true, + }; + } + const toolName = toolbarService.getToolNameForButton(button); if (!toolGroup.hasTool(toolName) && !toolNames) { diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index f5a4a0752..88b321f6b 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.5.0", - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index f7de52eb4..e7bf2fbb9 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.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index bfe325d5d..03b5db339 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.5.0", + "@cornerstonejs/dicom-image-loader": "^3.7.0", "@ohif/core": "3.10.0-beta.125", "@ohif/ui": "3.10.0-beta.125", "dcmjs": "*", @@ -51,10 +51,11 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^3.5.0", - "@cornerstonejs/core": "^3.5.0", - "@cornerstonejs/polymorphic-segmentation": "^3.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/adapters": "^3.7.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/polymorphic-segmentation": "^3.7.0", + "@cornerstonejs/labelmap-interpolation": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "@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 df6d9b51c..849335130 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -4,6 +4,7 @@ import { VolumeViewport, utilities as csUtils, Types as CoreTypes, + cache, BaseVolumeViewport, } from '@cornerstonejs/core'; import { @@ -14,6 +15,7 @@ import { annotation, } from '@cornerstonejs/tools'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation'; import { Types as OhifTypes, utils } from '@ohif/core'; import i18n from '@ohif/i18n'; @@ -31,6 +33,8 @@ import toggleVOISliceSync from './utils/toggleVOISliceSync'; import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores'; import { toolNames } from './initCornerstoneTools'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; +import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats'; +import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport'; const { DefaultHistoryMemo } = csUtils.HistoryMemo; const toggleSyncFunctions = { imageSlice: toggleImageSliceSync, @@ -39,7 +43,6 @@ const toggleSyncFunctions = { function commandsModule({ servicesManager, - extensionManager, commandsManager, }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { const { @@ -54,7 +57,9 @@ function commandsModule({ colorbarService, hangingProtocolService, syncGroupService, - } = servicesManager.services; + segmentationService, + displaySetService, + } = servicesManager.services as AppTypes.Services; const { measurementServiceSource } = this; @@ -67,7 +72,75 @@ function commandsModule({ return toolGroupService.getToolGroupForViewport(viewport.id); } + function _getActiveSegmentationInfo() { + const viewportId = viewportGridService.getActiveViewportId(); + const activeSegmentation = segmentationService.getActiveSegmentation(viewportId); + const segmentationId = activeSegmentation?.segmentationId; + const activeSegmentIndex = segmentationService.getActiveSegment(viewportId).segmentIndex; + + return { + segmentationId, + segmentIndex: activeSegmentIndex, + }; + } + const actions = { + runSegmentBidirectional: async ({ segmentationId, segmentIndex } = {}) => { + // Get active segmentation if not specified + const targetSegmentation = + segmentationId && segmentIndex + ? { segmentationId, segmentIndex } + : _getActiveSegmentationInfo(); + + const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation; + + // Get bidirectional measurement data + const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({ + segmentationId: targetId, + segmentIndices: [targetIndex], + }); + + const activeViewportId = viewportGridService.getActiveViewportId(); + + // Process each bidirectional measurement + bidirectionalData.forEach(measurement => { + const { segmentIndex, majorAxis, minorAxis } = measurement; + + // Create annotation + const annotation = cornerstoneTools.SegmentBidirectionalTool.hydrate( + activeViewportId, + [majorAxis, minorAxis], + { + segmentIndex, + segmentationId: targetId, + } + ); + + // Update segmentation stats + const updatedSegmentation = updateSegmentBidirectionalStats({ + segmentationId: targetId, + segmentIndex: targetIndex, + bidirectionalData: measurement, + segmentationService, + annotation, + }); + + // Save changes if needed + if (updatedSegmentation) { + segmentationService.addOrUpdateSegmentation({ + segmentationId: targetId, + segments: updatedSegmentation.segments, + }); + } + }); + }, + interpolateLabelmap: () => { + const { segmentationId, segmentIndex } = _getActiveSegmentationInfo(); + labelmapInterpolation.interpolate({ + segmentationId, + segmentIndex, + }); + }, /** * Generates the selector props for the context menu, specific to * the cornerstone viewport, and then runs the context menu. @@ -373,6 +446,44 @@ function commandsModule({ utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); }, + downloadCSVSegmentationReport: ({ segmentationId }) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + const cachedStats = segmentation.cachedStats; + + const { representationData } = segmentation; + const { Labelmap } = representationData; + const { referencedImageIds } = Labelmap; + + const firstImageId = referencedImageIds[0]; + + // find displaySet for firstImageId + const displaySet = displaySetService + .getActiveDisplaySets() + .find(ds => ds.imageIds?.some(i => i === firstImageId)); + + const { + SeriesNumber, + SeriesInstanceUID, + StudyInstanceUID, + SeriesDate, + SeriesTime, + SeriesDescription, + } = displaySet; + + const additionalInfo = { + reference: { + SeriesNumber, + SeriesInstanceUID, + StudyInstanceUID, + SeriesDate, + SeriesTime, + SeriesDescription, + }, + }; + + generateSegmentationCSVReport(segmentation, additionalInfo); + }, + // Retrieve value commands getActiveViewportEnabledElement: _getActiveViewportEnabledElement, @@ -1094,7 +1205,12 @@ function commandsModule({ * @param props.segmentIndex - The index of the segment to activate */ setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => { - const { segmentationService } = servicesManager.services; + const { segmentationService, viewportGridService } = servicesManager.services; + // set both active segmentation and active segment + segmentationService.setActiveSegmentation( + viewportGridService.getActiveViewportId(), + segmentationId + ); segmentationService.setActiveSegment(segmentationId, segmentIndex); segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex); }, @@ -1636,6 +1752,9 @@ function commandsModule({ }, undo: actions.undo, redo: actions.redo, + interpolateLabelmap: actions.interpolateLabelmap, + runSegmentBidirectional: actions.runSegmentBidirectional, + downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport, }; return { diff --git a/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx new file mode 100644 index 000000000..56cad0f24 --- /dev/null +++ b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + Icons, + useSegmentationTableContext, + useSegmentationExpanded, +} from '@ohif/ui-next'; +import { useTranslation } from 'react-i18next'; +import { useSystem } from '@ohif/core/src'; + +/** + * Custom dropdown menu component for segmentation panel that uses context for data + */ +export const CustomDropdownMenuContent = () => { + const { commandsManager } = useSystem(); + const { t } = useTranslation('SegmentationTable'); + const { + onSegmentationAdd, + onSegmentationRemoveFromViewport, + onSegmentationEdit, + onSegmentationDelete, + exportOptions, + activeSegmentation, + activeSegmentationId, + } = useSegmentationTableContext('CustomDropdownMenu'); + + // Try to get segmentation data from expanded context first, fall back to table context + let segmentation; + let segmentationId; + let allowExport = false; + + try { + // Try to get from expanded context + const context = useSegmentationExpanded(); + segmentation = context.segmentation; + segmentationId = segmentation.segmentationId; + } catch (e) { + // If not in expanded context, fallback to active segmentation from table context + segmentation = activeSegmentation; + segmentationId = activeSegmentationId; + } + + // Determine if export is allowed for this segmentation + if (exportOptions && segmentationId) { + const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId); + allowExport = exportOption?.isExportable || false; + } + + if (!segmentation || !segmentationId) { + return null; + } + + const actions = { + storeSegmentation: async segmentationId => { + commandsManager.run({ + commandName: 'storeSegmentation', + commandOptions: { segmentationId }, + context: 'CORNERSTONE', + }); + }, + onSegmentationDownloadRTSS: segmentationId => { + commandsManager.run('downloadRTSS', { segmentationId }); + }, + onSegmentationDownload: segmentationId => { + commandsManager.run('downloadSegmentation', { segmentationId }); + }, + downloadCSVSegmentationReport: segmentationId => { + commandsManager.run('downloadCSVSegmentationReport', { segmentationId }); + }, + }; + + return ( + + onSegmentationAdd(segmentationId)}> + + {t('Create New Segmentation')} + + + {t('Manage Current Segmentation')} + onSegmentationRemoveFromViewport(segmentationId)}> + + {t('Remove from Viewport')} + + onSegmentationEdit(segmentationId)}> + + {t('Rename')} + + + + + {t('Export & Download')} + + + + actions.downloadCSVSegmentationReport(segmentationId)}> + {t('Download CSV Report')} + + actions.storeSegmentation(segmentationId)} + disabled={!allowExport} + > + {t('Export DICOM SEG')} + + actions.onSegmentationDownload(segmentationId)} + disabled={!allowExport} + > + {t('Download DICOM SEG')} + + + + + + onSegmentationDelete(segmentationId)}> + + {t('Delete')} + + + ); +}; diff --git a/extensions/cornerstone/src/customizations/CustomSegmentStatisticsHeader.tsx b/extensions/cornerstone/src/customizations/CustomSegmentStatisticsHeader.tsx new file mode 100644 index 000000000..9c089e26d --- /dev/null +++ b/extensions/cornerstone/src/customizations/CustomSegmentStatisticsHeader.tsx @@ -0,0 +1,138 @@ +import * as cornerstoneTools from '@cornerstonejs/tools'; +import React from 'react'; +import { Separator, Button, Tooltip, TooltipTrigger, TooltipContent, Icons } from '@ohif/ui-next'; +import { useTranslation } from 'react-i18next'; +import { roundNumber } from '@ohif/core/src/utils'; +import { useSystem } from '@ohif/core/src'; + +interface CustomSegmentStatisticsHeaderProps { + segmentationId: string; + segmentIndex: number; +} + +/** + * Custom header component for segment statistics + */ +export const CustomSegmentStatisticsHeader = ({ + segmentationId, + segmentIndex, +}: CustomSegmentStatisticsHeaderProps) => { + const { servicesManager, commandsManager } = useSystem(); + const { segmentationService } = servicesManager.services; + const { t } = useTranslation('SegmentationTable'); + + const segmentation = segmentationService.getSegmentation(segmentationId); + const segment = segmentation.segments[segmentIndex]; + const cachedStats = segment.cachedStats; + const namedStats = cachedStats.namedStats; + + if (!namedStats) { + return null; + } + + const bidirectional = namedStats.bidirectional; + + if (!bidirectional) { + return ( +
+
+ + + + + {t('Add bidirectional measurement')} + +
+ +
+ ); + } + + const { value, unit } = bidirectional; + const maxMajor = value.maxMajor; + const maxMinor = value.maxMinor; + + const max = Math.max(maxMajor, maxMinor); + const min = Math.min(maxMajor, maxMinor); + + const isVisible = cornerstoneTools.annotation.visibility.isAnnotationVisible( + bidirectional.annotationUID + ); + + return ( +
+
+
+
+ L: {roundNumber(max)} {unit} +
+
+ W: {roundNumber(min)} {unit} +
+
+
+ + + + + {t('Toggle visibility')} + + + + + + {t('Jump to measurement')} + +
+
+ +
+ ); +}; diff --git a/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx b/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx index d0649f57f..23236f701 100644 --- a/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx +++ b/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx @@ -1,77 +1,10 @@ -import React from 'react'; -import { - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - Icons, -} from '@ohif/ui-next'; +import { CustomDropdownMenuContent } from './CustomDropdownMenuContent'; +import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader'; export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) { return { - 'panelSegmentation.customDropdownMenuContent': ({ - activeSegmentation, - onSegmentationAdd, - onSegmentationRemoveFromViewport, - onSegmentationEdit, - onSegmentationDelete, - allowExport, - storeSegmentation, - onSegmentationDownload, - onSegmentationDownloadRTSS, - t, - }) => ( - - onSegmentationAdd(activeSegmentation.segmentationId)}> - - {t('Create New Segmentation')} - - - {t('Manage Current Segmentation')} - onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)} - > - - {t('Remove from Viewport')} - - onSegmentationEdit(activeSegmentation.segmentationId)}> - - {t('Rename')} - - - - - {t('Export & Download')} - - - - storeSegmentation(activeSegmentation.segmentationId)} - > - {t('Export DICOM SEG')} - - onSegmentationDownload(activeSegmentation.segmentationId)} - > - {t('Download DICOM SEG')} - - - - - - onSegmentationDelete(activeSegmentation.segmentationId)}> - - {t('Delete')} - - - ), + 'panelSegmentation.customDropdownMenuContent': CustomDropdownMenuContent, + 'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader, 'panelSegmentation.disableEditing': false, 'panelSegmentation.showAddSegment': true, 'panelSegmentation.onSegmentationAdd': () => { @@ -81,15 +14,21 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser }, 'panelSegmentation.tableMode': 'collapsed', 'panelSegmentation.readableText': { - lesionStats: 'Lesion Statistics', - minValue: 'Minimum Value', - maxValue: 'Maximum Value', - meanValue: 'Mean Value', - volume: 'Volume (ml)', - suvPeak: 'SUV Peak', - suvMax: 'Maximum SUV', - suvMaxIJK: 'SUV Max IJK', - lesionGlyoclysisStats: 'Lesion Glycolysis', + // the values will appear in this order + min: 'Min Value', + minLPS: 'Min Coord', + max: 'Max Value', + maxLPS: 'Max Coord', + mean: 'Mean Value', + stdDev: 'Standard Deviation', + count: 'Voxel Count', + median: 'Median', + skewness: 'Skewness', + kurtosis: 'Kurtosis', + peakValue: 'Peak Value', + peakLPS: 'Peak Coord', + volume: 'Volume', + lesionGlycolysis: 'Lesion Glycolysis', }, }; } diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx index 95fac6c71..7279bbfee 100644 --- a/extensions/cornerstone/src/getToolbarModule.tsx +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -190,16 +190,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w }, }, { - name: 'evaluate.mpr', + name: 'evaluate.displaySetIsReconstructable', evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => { - const { protocol } = hangingProtocolService.getActiveProtocol(); + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); - - if (!displaySetUIDs?.length) { + if (!viewport) { return; } + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID); const areReconstructable = displaySets.every(displaySet => { @@ -210,11 +210,8 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w return getDisabledState(disabledText); } - const isMpr = protocol?.id === 'mpr'; - return { disabled: false, - className: utils.getToggledClassName(isMpr), }; }, }, diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index 565844169..706dfe536 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -39,6 +39,7 @@ 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'; const { registerColormap } = csUtilities.colormap; @@ -90,6 +91,7 @@ export default async function init({ cornerstoneViewportService, hangingProtocolService, viewportGridService, + segmentationService, } = servicesManager.services; window.services = servicesManager.services; @@ -150,8 +152,6 @@ export default async function init({ ); }); - // ... existing code ... - // add metadata providers metaData.addProvider( csUtilities.calibratedPixelSpacingMetadataProvider.get.bind( @@ -177,6 +177,45 @@ export default async function init({ initCineService(servicesManager); initStudyPrefetcherService(servicesManager); + 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, + }); + + 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 + ); + // When a custom image load is performed, update the relevant viewports hangingProtocolService.subscribe( hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, @@ -278,35 +317,35 @@ export default async function init({ function initializeWebWorkerProgressHandler(uiNotificationService) { const activeToasts = new Map(); - eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => { - const { progress, type, id } = detail; + // eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => { + // const { progress, type, id } = detail; - const cacheKey = `${type}-${id}`; - if (progress === 0 && !activeToasts.has(cacheKey)) { - const progressPromise = new Promise((resolve, reject) => { - activeToasts.set(cacheKey, { resolve, reject }); - }); + // const cacheKey = `${type}-${id}`; + // if (progress === 0 && !activeToasts.has(cacheKey)) { + // const progressPromise = new Promise((resolve, reject) => { + // activeToasts.set(cacheKey, { resolve, reject }); + // }); - uiNotificationService.show({ - id: cacheKey, - title: `${type}`, - message: `${type}: ${progress}%`, - autoClose: false, - promise: progressPromise, - promiseMessages: { - loading: `Computing...`, - success: `Completed successfully`, - error: 'Web Worker failed', - }, - }); - } else { - if (progress === 100) { - const { resolve } = activeToasts.get(cacheKey); - resolve({ progress, type }); - activeToasts.delete(cacheKey); - } - } - }); + // uiNotificationService.show({ + // id: cacheKey, + // title: `${type}`, + // message: `${type}: ${progress}%`, + // autoClose: false, + // promise: progressPromise, + // promiseMessages: { + // loading: `Computing...`, + // success: `Completed successfully`, + // error: 'Web Worker failed', + // }, + // }); + // } else { + // if (progress === 100) { + // const { resolve } = activeToasts.get(cacheKey); + // resolve({ progress, type }); + // activeToasts.delete(cacheKey); + // } + // } + // }); } /** diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js index 423bbf9d9..cd632559e 100644 --- a/extensions/cornerstone/src/initCornerstoneTools.js +++ b/extensions/cornerstone/src/initCornerstoneTools.js @@ -1,6 +1,7 @@ import { PanTool, WindowLevelTool, + SegmentBidirectionalTool, StackScrollTool, VolumeRotateTool, ZoomTool, @@ -52,8 +53,14 @@ export default function initCornerstoneTools(configuration = {}) { addons: { polySeg, }, + computeWorker: { + autoTerminateOnIdle: { + enabled: false, + }, + }, }); addTool(PanTool); + addTool(SegmentBidirectionalTool); addTool(WindowLevelTool); addTool(StackScrollTool); addTool(VolumeRotateTool); @@ -142,6 +149,7 @@ const toolNames = { OrientationMarker: OrientationMarkerTool.toolName, WindowLevelRegion: WindowLevelRegionTool.toolName, PlanarFreehandContourSegmentation: PlanarFreehandContourSegmentationTool.toolName, + SegmentBidirectional: SegmentBidirectionalTool.toolName, }; export { toolNames }; diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index f0d863363..156f1e705 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -34,6 +34,7 @@ const initMeasurementService = ( LivewireContour, Probe, UltrasoundDirectional, + SegmentBidirectional, } = measurementServiceMappingsFactory( measurementService, displaySetService, @@ -172,6 +173,14 @@ const initMeasurementService = ( UltrasoundDirectional.toMeasurement ); + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SegmentBidirectional', + SegmentBidirectional.matchingCriteria, + SegmentBidirectional.toAnnotation, + SegmentBidirectional.toMeasurement + ); + return csTools3DVer1MeasurementSource; }; diff --git a/extensions/cornerstone/src/panels/PanelSegmentation.tsx b/extensions/cornerstone/src/panels/PanelSegmentation.tsx index bb5def037..cee8e5485 100644 --- a/extensions/cornerstone/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone/src/panels/PanelSegmentation.tsx @@ -15,217 +15,191 @@ export default function PanelSegmentation({ servicesManager, }); - const handlers = { - onSegmentationClick: (segmentationId: string) => { - commandsManager.run('setActiveSegmentation', { segmentationId }); - }, - - onSegmentAdd: segmentationId => { - commandsManager.run('addSegment', { segmentationId }); - }, - - onSegmentClick: (segmentationId, segmentIndex) => { - commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex }); - }, - - onSegmentEdit: (segmentationId, segmentIndex) => { - commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex }); - }, - - onSegmentationEdit: segmentationId => { - commandsManager.run('editSegmentationLabel', { segmentationId }); - }, - - onSegmentColorClick: (segmentationId, segmentIndex) => { - commandsManager.run('editSegmentColor', { segmentationId, segmentIndex }); - }, - - onSegmentDelete: (segmentationId, segmentIndex) => { - commandsManager.run('deleteSegment', { segmentationId, segmentIndex }); - }, - - onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => { - commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type }); - }, - - onToggleSegmentLock: (segmentationId, segmentIndex) => { - commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex }); - }, - - onToggleSegmentationRepresentationVisibility: (segmentationId, type) => { - commandsManager.run('toggleSegmentationVisibility', { segmentationId, type }); - }, - - onSegmentationDownload: segmentationId => { - commandsManager.run('downloadSegmentation', { segmentationId }); - }, - - storeSegmentation: async segmentationId => { - commandsManager.run({ - commandName: 'storeSegmentation', - commandOptions: { segmentationId }, - context: 'CORNERSTONE', - }); - }, - - onSegmentationDownloadRTSS: segmentationId => { - commandsManager.run('downloadRTSS', { segmentationId }); - }, - - setStyle: (segmentationId, type, key, value) => { - commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value }); - }, - - toggleRenderInactiveSegmentations: () => { - commandsManager.run('toggleRenderInactiveSegmentations'); - }, - - onSegmentationRemoveFromViewport: segmentationId => { - commandsManager.run('removeSegmentationFromViewport', { segmentationId }); - }, - - onSegmentationDelete: segmentationId => { - commandsManager.run('deleteSegmentation', { segmentationId }); - }, - - setFillAlpha: ({ type }, value) => { - commandsManager.run('setFillAlpha', { type, value }); - }, - - setOutlineWidth: ({ type }, value) => { - commandsManager.run('setOutlineWidth', { type, value }); - }, - - setRenderFill: ({ type }, value) => { - commandsManager.run('setRenderFill', { type, value }); - }, - - setRenderOutline: ({ type }, value) => { - commandsManager.run('setRenderOutline', { type, value }); - }, - - setFillAlphaInactive: ({ type }, value) => { - commandsManager.run('setFillAlphaInactive', { type, value }); - }, - - getRenderInactiveSegmentations: () => { - return commandsManager.run('getRenderInactiveSegmentations'); - }, - }; - + // Extract customization options const segmentationTableMode = customizationService.getCustomization( 'panelSegmentation.tableMode' - ); - - // custom onSegmentationAdd if provided + ) as unknown as string; const onSegmentationAdd = customizationService.getCustomization( 'panelSegmentation.onSegmentationAdd' ); - const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing'); const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment'); const CustomDropdownMenuContent = customizationService.getCustomization( 'panelSegmentation.customDropdownMenuContent' ); + const CustomSegmentStatisticsHeader = customizationService.getCustomization( + 'panelSegmentation.customSegmentStatisticsHeader' + ); + + // Create handlers object for all command runs + const handlers = { + onSegmentationClick: (segmentationId: string) => { + commandsManager.run('setActiveSegmentation', { segmentationId }); + }, + onSegmentAdd: segmentationId => { + commandsManager.run('addSegment', { segmentationId }); + }, + onSegmentClick: (segmentationId, segmentIndex) => { + commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex }); + }, + onSegmentEdit: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex }); + }, + onSegmentationEdit: segmentationId => { + commandsManager.run('editSegmentationLabel', { segmentationId }); + }, + onSegmentColorClick: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentColor', { segmentationId, segmentIndex }); + }, + onSegmentDelete: (segmentationId, segmentIndex) => { + commandsManager.run('deleteSegment', { segmentationId, segmentIndex }); + }, + onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => { + commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type }); + }, + onToggleSegmentLock: (segmentationId, segmentIndex) => { + commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex }); + }, + onToggleSegmentationRepresentationVisibility: (segmentationId, type) => { + commandsManager.run('toggleSegmentationVisibility', { segmentationId, type }); + }, + onSegmentationDownload: segmentationId => { + commandsManager.run('downloadSegmentation', { segmentationId }); + }, + setStyle: (segmentationId, type, key, value) => { + commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value }); + }, + toggleRenderInactiveSegmentations: () => { + commandsManager.run('toggleRenderInactiveSegmentations'); + }, + onSegmentationRemoveFromViewport: segmentationId => { + commandsManager.run('removeSegmentationFromViewport', { segmentationId }); + }, + onSegmentationDelete: segmentationId => { + commandsManager.run('deleteSegmentation', { segmentationId }); + }, + setFillAlpha: ({ type }, value) => { + commandsManager.run('setFillAlpha', { type, value }); + }, + setOutlineWidth: ({ type }, value) => { + commandsManager.run('setOutlineWidth', { type, value }); + }, + setRenderFill: ({ type }, value) => { + commandsManager.run('setRenderFill', { type, value }); + }, + setRenderOutline: ({ type }, value) => { + commandsManager.run('setRenderOutline', { type, value }); + }, + setFillAlphaInactive: ({ type }, value) => { + commandsManager.run('setFillAlphaInactive', { type, value }); + }, + getRenderInactiveSegmentations: () => { + return commandsManager.run('getRenderInactiveSegmentations'); + }, + }; + + // Generate export options const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => { const { representationData, segmentationId } = segmentation; const { Labelmap } = representationData; if (!Labelmap) { - return { - segmentationId, - isExportable: true, - }; + return { segmentationId, isExportable: true }; } const referencedImageIds = Labelmap.referencedImageIds; const firstImageId = referencedImageIds[0]; - const instance = metaData.get('instance', firstImageId); if (!instance) { - return { - segmentationId, - isExportable: false, - }; + return { segmentationId, isExportable: false }; } const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; const SeriesInstanceUID = instance.SeriesInstanceUID; - const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, SeriesInstanceUID ); - const isExportable = displaySet?.isReconstructable; - return { segmentationId, - isExportable, + isExportable: displaySet?.isReconstructable, }; }); - return ( - <> - - {children} - - + // Common props for SegmentationTable + const tableProps = { + disabled, + data: segmentationsWithRepresentations, + mode: segmentationTableMode, + title: 'Segmentations', + exportOptions, + disableEditing, + onSegmentationAdd, + showAddSegment, + renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(), + ...handlers, + }; - {segmentationTableMode === 'collapsed' ? ( - - + const renderSegments = () => { + return ( + + + + + + + ); + }; + + // Render content based on mode + const renderModeContent = () => { + if (tableProps.mode === 'collapsed') { + return ( + + + - + + + + + - - - ) : ( - - + {renderSegments()} + + + ); + } + + return ( + <> + + + - - {/* */} - - - )} - - + + + + + + + + {renderSegments()} + + + + ); + }; + + return ( + + {children} + + + {renderModeContent()} + ); } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 0541e6c7c..8cac0c542 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -92,6 +92,7 @@ class SegmentationService extends PubSubService { }; private _segmentationIdToColorLUTIndexMap: Map; + private _segmentationGroupStatsMap: Map; readonly servicesManager: AppTypes.ServicesManager; highlightIntervalId = null; readonly EVENTS = EVENTS; @@ -102,6 +103,8 @@ class SegmentationService extends PubSubService { this._segmentationIdToColorLUTIndexMap = new Map(); this.servicesManager = servicesManager; + + this._segmentationGroupStatsMap = new Map(); } public onModeEnter(): void { @@ -1058,6 +1061,33 @@ class SegmentationService extends PubSubService { public getRenderInactiveSegmentations(viewportId: string): boolean { return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId); } + /** + * Sets statistics for a group of segmentations + * @param segmentationIds - Array of segmentation IDs that form the group + * @param stats - Statistics object containing metrics for the segmentation group + * + * @remarks + * This method stores statistical data for a group of related segmentations. + * The stats are stored using a composite key created from the sorted and joined + */ + public setSegmentationGroupStats(segmentationIds: string[], stats: any) { + const groupId = this.getGroupId(segmentationIds); + this._segmentationGroupStatsMap.set(groupId, stats); + } + + /** + * Gets statistics for a group of segmentations + * @param segmentationIds - Array of segmentation IDs that form the group + * @returns The stored statistics object for the segmentation group if found, undefined otherwise + */ + public getSegmentationGroupStats(segmentationIds: string[]) { + const groupId = this.getGroupId(segmentationIds); + return this._segmentationGroupStatsMap.get(groupId); + } + + private getGroupId(segmentationIds: string[]) { + return segmentationIds.sort().join(','); + } /** * Toggles the visibility of a segmentation in the state, and broadcasts the event. diff --git a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts new file mode 100644 index 000000000..023f46df6 --- /dev/null +++ b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts @@ -0,0 +1,139 @@ +export function generateSegmentationCSVReport( + segmentationData, + info: { + reference: { + SeriesNumber: string; + SeriesInstanceUID: string; + StudyInstanceUID: string; + SeriesDate: string; + SeriesTime: string; + SeriesDescription: string; + }; + } +) { + // Initialize the rows for our CSV + const csvRows = []; + + // Add segmentation-level information + csvRows.push(['Segmentation ID', segmentationData.segmentationId || '']); + csvRows.push(['Segmentation Label', segmentationData.label || '']); + + csvRows.push([]); + + const additionalInfo = info.reference; + // Add reference information + const referenceKeys = [ + ['Series Number', additionalInfo.SeriesNumber], + ['Series Instance UID', additionalInfo.SeriesInstanceUID], + ['Study Instance UID', additionalInfo.StudyInstanceUID], + ['Series Date', additionalInfo.SeriesDate], + ['Series Time', additionalInfo.SeriesTime], + ['Series Description', additionalInfo.SeriesDescription], + ]; + + referenceKeys.forEach(([key, value]) => { + if (value) { + csvRows.push([`reference ${key}`, value]); + } + }); + + // Add a blank row for separation + csvRows.push([]); + + csvRows.push(['Segments Statistics']); + + // Add segment information in columns + if (segmentationData.segments) { + // First row: Segment headers + const segmentHeaderRow = ['Label']; + for (const segmentId in segmentationData.segments) { + const segment = segmentationData.segments[segmentId]; + segmentHeaderRow.push(`${segment.label || ''}`); + } + csvRows.push(segmentHeaderRow); + + // Add segment properties + csvRows.push([ + 'Segment Index', + ...Object.values(segmentationData.segments).map(s => s.segmentIndex || ''), + ]); + csvRows.push([ + 'Locked', + ...Object.values(segmentationData.segments).map(s => (s.locked ? 'Yes' : 'No')), + ]); + csvRows.push([ + 'Active', + ...Object.values(segmentationData.segments).map(s => (s.active ? 'Yes' : 'No')), + ]); + + // Add segment statistics + // First, collect all unique statistics across all segments + const allStats = new Set(); + for (const segment of Object.values(segmentationData.segments)) { + if (segment.cachedStats && segment.cachedStats.namedStats) { + for (const statKey in segment.cachedStats.namedStats) { + const stat = segment.cachedStats.namedStats[statKey]; + const statLabel = stat.label || stat.name; + const statUnit = stat.unit ? ` (${stat.unit})` : ''; + allStats.add(`${statLabel}${statUnit}`); + } + } + } + + // Then create a row for each statistic + for (const statName of allStats) { + const statRow = [statName]; + + for (const segment of Object.values(segmentationData.segments)) { + let statValue = ''; + + if (segment.cachedStats && segment.cachedStats.namedStats) { + for (const statKey in segment.cachedStats.namedStats) { + const stat = segment.cachedStats.namedStats[statKey]; + const currentStatName = `${stat.label || stat.name}${stat.unit ? ` (${stat.unit})` : ''}`; + + if (currentStatName === statName) { + statValue = stat.value !== undefined ? stat.value : ''; + break; + } + } + } + + statRow.push(statValue); + } + + csvRows.push(statRow); + } + } + + // Convert to CSV string + let csvString = ''; + for (const row of csvRows) { + const formattedRow = row.map(cell => { + // Handle values that need to be quoted (contain commas, quotes, or newlines) + const cellValue = cell !== undefined && cell !== null ? cell.toString() : ''; + if (cellValue.includes(',') || cellValue.includes('"') || cellValue.includes('\n')) { + // Escape quotes and wrap in quotes + return '"' + cellValue.replace(/"/g, '""') + '"'; + } + return cellValue; + }); + csvString += formattedRow.join(',') + '\n'; + } + + // Create a download link and trigger the download + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.setAttribute('href', url); + link.setAttribute( + 'download', + `${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv` + ); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/SegmentBidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/SegmentBidirectional.ts new file mode 100644 index 000000000..312b5e793 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/SegmentBidirectional.ts @@ -0,0 +1,185 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; + +const SegmentBidirectional = { + toAnnotation: measurement => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + debugger; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.debug('SegmentBidirectional tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { length, width, unit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + length, + width, + }); + }); + + return annotations; +} + +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:SegmentBidirectional'); + + mappedAnnotations.forEach(annotation => { + const { length, width, unit } = annotation; + columns.push(`Length`, `Width`, 'Unit'); + values.push(length, width, unit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const roundedLength = utils.roundNumber(length, 2); + const roundedWidth = utils.roundNumber(width, 2); + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`); + displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +export default SegmentBidirectional; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js index a3f9c832c..6f6c19232 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -13,6 +13,7 @@ const supportedTools = [ 'LivewireContour', 'UltrasoundDirectionalTool', 'SCOORD3DPoint', + 'SegmentBidirectional', ]; export default supportedTools; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts index 041c767b6..ff944ab64 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts @@ -12,6 +12,7 @@ import SplineROI from './SplineROI'; import LivewireContour from './LivewireContour'; import Probe from './Probe'; import UltrasoundDirectional from './UltrasoundDirectional'; +import SegmentBidirectional from './SegmentBidirectional'; const measurementServiceMappingsFactory = ( measurementService: MeasurementService, @@ -48,6 +49,7 @@ const measurementServiceMappingsFactory = ( LivewireContour: POLYLINE, Probe: POINT, UltrasoundDirectional: POLYLINE, + SegmentBidirectional: BIDIRECTIONAL, }; return TOOL_TYPE_TO_VALUE_TYPE[toolType]; @@ -94,6 +96,27 @@ const measurementServiceMappingsFactory = ( }, ], }, + SegmentBidirectional: { + toAnnotation: SegmentBidirectional.toAnnotation, + toMeasurement: csToolsAnnotation => + SegmentBidirectional.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, EllipticalROI: { toAnnotation: EllipticalROI.toAnnotation, toMeasurement: csToolsAnnotation => diff --git a/extensions/cornerstone/src/utils/updateSegmentationStats.ts b/extensions/cornerstone/src/utils/updateSegmentationStats.ts new file mode 100644 index 000000000..936c822b4 --- /dev/null +++ b/extensions/cornerstone/src/utils/updateSegmentationStats.ts @@ -0,0 +1,185 @@ +import * as cornerstoneTools from '@cornerstonejs/tools'; + +interface BidirectionalAxis { + length: number; + // Add other axis properties as needed +} + +interface BidirectionalData { + majorAxis: BidirectionalAxis; + minorAxis: BidirectionalAxis; +} + +/** + * Updates the statistics for a segmentation by calculating stats for each segment + * and storing them in the segment's cachedStats property + * + * @param segmentation - The segmentation object containing segments to update stats for + * @param segmentationId - The ID of the segmentation + * @returns The updated segmentation object with new stats, or null if no updates were made + */ +export async function updateSegmentationStats({ + segmentation, + segmentationId, + readableText, +}: { + segmentation: any; + segmentationId: string; + readableText: any; +}): Promise { + if (!segmentation) { + console.debug('No segmentation found for id:', segmentationId); + return null; + } + + const segmentIndices = Object.keys(segmentation.segments) + .map(index => parseInt(index)) + .filter(index => index > 0); // Filter out segment 0 which is typically background + + if (segmentIndices.length === 0) { + console.debug('No segments found in segmentation:', segmentationId); + return null; + } + + const stats = await cornerstoneTools.utilities.segmentation.getStatistics({ + segmentationId, + segmentIndices, + mode: 'individual', + }); + + if (!stats) { + return null; + } + + const updatedSegmentation = { ...segmentation }; + let hasUpdates = false; + + // Loop through each segment's stats + Object.entries(stats).forEach(([segmentIndex, segmentStats]) => { + const index = parseInt(segmentIndex); + + if (!updatedSegmentation.segments[index].cachedStats) { + updatedSegmentation.segments[index].cachedStats = {}; + hasUpdates = true; + } + + // Get existing namedStats or initialize if not present + const namedStats = updatedSegmentation.segments[index].cachedStats.namedStats || {}; + + if (segmentStats.array) { + segmentStats.array.forEach(stat => { + // only gather stats that are in the readableText + if (!readableText[stat.name]) { + return; + } + + if (stat && stat.name) { + namedStats[stat.name] = { + name: stat.name, + label: readableText[stat.name], + value: stat.value, + unit: stat.unit, + order: Object.keys(readableText).indexOf(stat.name), + }; + } + }); + + if (readableText.volume) { + // Add volume if it exists but isn't in the array + if (segmentStats.volume && !namedStats.volume) { + namedStats.volume = { + name: 'volume', + label: 'Volume', + value: segmentStats.volume.value, + unit: segmentStats.volume.unit, + order: Object.keys(readableText).indexOf('volume'), + }; + } + } + + // Update the segment's cachedStats with namedStats + updatedSegmentation.segments[index].cachedStats.namedStats = namedStats; + hasUpdates = true; + } + }); + + return hasUpdates ? updatedSegmentation : null; +} + +/** + * Updates a segment's statistics with bidirectional measurement data + * + * @param segmentationId - The ID of the segmentation + * @param segmentIndex - The index of the segment to update + * @param bidirectionalData - The bidirectional measurement data to add + * @param segmentationService - The segmentation service to use for updating the segment + * @returns Whether the update was successful + */ +export function updateSegmentBidirectionalStats({ + segmentationId, + segmentIndex, + bidirectionalData, + segmentationService, + annotation, +}: { + segmentationId: string; + segmentIndex: number; + bidirectionalData: BidirectionalData; + segmentationService: AppTypes.SegmentationService; + annotation: any; +}) { + if (!segmentationId || segmentIndex === undefined || !bidirectionalData) { + console.debug('Missing required data for bidirectional stats update'); + return null; + } + + const segmentation = segmentationService.getSegmentation(segmentationId); + if (!segmentation || !segmentation.segments[segmentIndex]) { + console.debug('Segment not found:', segmentIndex, 'in segmentation:', segmentationId); + return null; + } + + const updatedSegmentation = { ...segmentation }; + const segment = updatedSegmentation.segments[segmentIndex]; + + if (!segment.cachedStats) { + segment.cachedStats = { namedStats: {} }; + } + + if (!segment.cachedStats.namedStats) { + segment.cachedStats.namedStats = {}; + } + + const { majorAxis, minorAxis, maxMajor, maxMinor } = bidirectionalData; + if (!majorAxis || !minorAxis) { + console.debug('Missing major or minor axis data'); + return null; + } + + let hasUpdates = false; + const namedStats = segment.cachedStats.namedStats; + + // Only calculate and update if we have valid measurements + if (maxMajor > 0 && maxMinor > 0) { + namedStats.bidirectional = { + name: 'bidirectional', + label: 'Bidirectional', + annotationUID: annotation.annotationUID, + value: { + maxMajor, + maxMinor, + majorAxis, + minorAxis, + }, + unit: 'mm', + }; + + hasUpdates = true; + } + + if (hasUpdates) { + return updatedSegmentation; + } + + return null; +} diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx index 35b32ec9a..fb4c32034 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -import PropTypes from 'prop-types'; // import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser'; import getImageSrcFromImageId from './getImageSrcFromImageId'; diff --git a/extensions/default/src/Toolbar/ToolBoxWrapper.tsx b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx index 7a292299c..abc51051b 100644 --- a/extensions/default/src/Toolbar/ToolBoxWrapper.tsx +++ b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx @@ -26,7 +26,7 @@ export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props }) {...item} key={item.id} size="small" - className={props.disabled && 'text-primary'} + className={item.disabled && 'text-foreground/70'} onInteraction={event => { onInteraction?.({ event, @@ -49,7 +49,7 @@ export function ToolBoxButtonWrapper({ onInteraction, options, ...props }) { {...props} id={props.id} size="small" - className={classNames(props.disabled && 'text-primary')} + className={classNames(props.disabled && 'text-foreground/70')} onInteraction={event => { onInteraction?.({ event, diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index df54ba8f1..f7622ac56 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.5.0", - "@cornerstonejs/tools": "^3.5.0", + "@cornerstonejs/core": "^3.7.0", + "@cornerstonejs/tools": "^3.7.0", "@ohif/core": "3.10.0-beta.125", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.125", "@ohif/extension-default": "3.10.0-beta.125", diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx index 962384ae1..afadb3e56 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -import PropTypes from 'prop-types'; // import PanelStudyBrowserTracking from './PanelStudyBrowserTracking'; import getImageSrcFromImageId from './getImageSrcFromImageId'; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx index 315cbef83..9457adba6 100644 --- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx @@ -11,11 +11,9 @@ export default function PanelRoiThresholdSegmentation({ const { segmentationsWithRepresentations: segmentationsInfo } = useActiveViewportSegmentationRepresentations({ servicesManager }); - useEffect(() => { - const segmentationIds = segmentationsInfo.map( - segmentationInfo => segmentationInfo.segmentation.segmentationId - ); + const segmentationIds = segmentationsInfo?.map(info => info.segmentation.segmentationId) || []; + useEffect(() => { const initialRun = async () => { for (const segmentationId of segmentationIds) { await handleROIThresholding({ @@ -54,10 +52,9 @@ export default function PanelRoiThresholdSegmentation({ }, [commandsManager, segmentationService]); // Find the first segmentation with a TMTV value since all of them have the same value - const tmtvSegmentation = segmentationsInfo.find( - info => info.segmentation.cachedStats?.tmtv !== undefined - ); - const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv; + const stats = segmentationService.getSegmentationGroupStats(segmentationIds); + const tmtvValue = stats?.tmtv; + const tlgValue = stats?.tlg; return (
@@ -68,6 +65,7 @@ export default function PanelRoiThresholdSegmentation({ {'TMTV:'}
{`${tmtvValue?.toFixed(3)} mL`}
+
{`${tlgValue?.toFixed(3)} mL`}
) : null} diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts index 430111ef7..f8555e802 100644 --- a/extensions/tmtv/src/commandsModule.ts +++ b/extensions/tmtv/src/commandsModule.ts @@ -24,20 +24,7 @@ const ROI_THRESHOLD_MANUAL_TOOL_IDS = [ const workerManager = getWebWorkerManager(); -const options = { - maxWorkerInstances: 1, - autoTerminateOnIdle: { - enabled: true, - idleTimeThreshold: 3000, - }, -}; - // Register the task -const workerFn = () => { - return new Worker(new URL('./utils/calculateSUVPeakWorker.js', import.meta.url), { - name: 'suv-peak-worker', // name used by the browser to name the worker - }); -}; function getVolumesFromSegmentation(segmentationId) { const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId); @@ -262,153 +249,22 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: { overwrite: true, segmentIndex, segmentationId } ); }, - calculateSuvPeak: async ({ segmentationId, segmentIndex }) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - const { representationData } = segmentation; - const { volumeId, referencedVolumeId } = representationData[ - SegmentationRepresentations.Labelmap - ] as csTools.Types.LabelmapToolOperationDataVolume; - - const labelmap = cs.cache.getVolume(volumeId); - const referencedVolume = cs.cache.getVolume(referencedVolumeId); - - // if we put it in the top, it will appear in other modes - workerManager.registerWorker('suv-peak-worker', workerFn, options); - - const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS); - - const annotations = annotationUIDs.map(annotationUID => - csTools.annotation.state.getAnnotation(annotationUID) - ); - - const labelmapProps = { - dimensions: labelmap.dimensions, - origin: labelmap.origin, - direction: labelmap.direction, - spacing: labelmap.spacing, - metadata: labelmap.metadata, - scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), - }; - - const referenceVolumeProps = { - dimensions: referencedVolume.dimensions, - origin: referencedVolume.origin, - direction: referencedVolume.direction, - spacing: referencedVolume.spacing, - metadata: referencedVolume.metadata, - scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(), - }; - - // metadata in annotations has enabledElement which is not serializable - // we need to remove it - // Todo: we should probably have a sanitization function for this - const annotationsToSend = annotations.map(annotation => { - return { - ...annotation, - metadata: { - ...annotation.metadata, - enabledElement: { - ...annotation.metadata.enabledElement, - viewport: null, - renderingEngine: null, - element: null, - }, - }, - }; - }); - - const suvPeak = - (await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', { - labelmapProps, - referenceVolumeProps, - annotations: annotationsToSend, - segmentIndex, - })) || {}; - - return { - suvPeak: suvPeak.mean, - suvMax: suvPeak.max, - suvMaxIJK: suvPeak.maxIJK, - suvMaxLPS: suvPeak.maxLPS, - }; - }, - getLesionStats: ({ segmentationId, segmentIndex = 1 }) => { - const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId); - const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume; - const { voxelManager: refVoxelManager } = referencedVolume; - - let segmentationMax = -Infinity; - let segmentationMin = Infinity; - const segmentationValues = []; - let voxelCount = 0; - - const callback = ({ value, index }) => { - if (value === segmentIndex) { - const refValue = refVoxelManager.getAtIndex(index) as number; - segmentationValues.push(refValue); - if (refValue > segmentationMax) { - segmentationMax = refValue; - } - if (refValue < segmentationMin) { - segmentationMin = refValue; - } - voxelCount++; - } - }; - - segVoxelManager.forEach(callback, { imageData }); - const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount; - const stats = { - minValue: segmentationMin, - maxValue: segmentationMax, - meanValue: mean, - stdValue: Math.sqrt( - segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) / - voxelCount - ), - volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3, - }; - - return stats; - }, - calculateLesionGlycolysis: ({ lesionStats }) => { - const { meanValue, volume } = lesionStats; - - return { - lesionGlyoclysisStats: volume * meanValue, - }; - }, calculateTMTV: async ({ segmentations }) => { - const labelmapProps = segmentations.map(segmentation => { - const labelmap = getLabelmapVolumeFromSegmentation(segmentation); - return { - dimensions: labelmap.dimensions, - spacing: labelmap.spacing, - scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), - origin: labelmap.origin, - direction: labelmap.direction, - }; + const segmentationIds = segmentations.map(segmentation => segmentation.segmentationId); + + const stats = await csTools.utilities.segmentation.computeMetabolicStats({ + segmentationIds, + segmentIndex: 1, }); - if (!labelmapProps.length) { - return; - } - - const tmtv = await workerManager.executeTask( - 'suv-peak-worker', - 'calculateTMTV', - labelmapProps - ); - - return tmtv; + segmentationService.setSegmentationGroupStats(segmentationIds, stats); + return stats; }, exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => { const segReport = commandsManager.runCommand('getSegmentationCSVReport', { segmentations, }); - const tlg = await actions.getTotalLesionGlycolysis({ segmentations }); const additionalReportRows = [ { key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } }, { key: 'Threshold Configuration', value: { ...config } }, @@ -423,35 +279,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: createAndDownloadTMTVReport(segReport, additionalReportRows, options); }, - getTotalLesionGlycolysis: async ({ segmentations }) => { - const labelmapProps = segmentations.map(segmentation => { - const labelmap = getLabelmapVolumeFromSegmentation(segmentation); - return { - dimensions: labelmap.dimensions, - spacing: labelmap.spacing, - scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), - origin: labelmap.origin, - direction: labelmap.direction, - }; - }); - const { referencedVolume: ptVolume } = getVolumesFromSegmentation( - segmentations[0].segmentationId - ); - - const ptVolumeProps = { - dimensions: ptVolume.dimensions, - spacing: ptVolume.spacing, - scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(), - origin: ptVolume.origin, - direction: ptVolume.direction, - }; - - return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', { - labelmapProps, - referenceVolumeProps: ptVolumeProps, - }); - }, setStartSliceForROIThresholdTool: () => { const { viewport } = _getActiveViewportsEnabledElement(); const { focalPoint } = viewport.getCamera(); @@ -651,12 +479,6 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: getTotalLesionGlycolysis: { commandFn: actions.getTotalLesionGlycolysis, }, - calculateSuvPeak: { - commandFn: actions.calculateSuvPeak, - }, - getLesionStats: { - commandFn: actions.getLesionStats, - }, calculateTMTV: { commandFn: actions.calculateTMTV, }, diff --git a/extensions/tmtv/src/utils/calculateSUVPeakWorker.js b/extensions/tmtv/src/utils/calculateSUVPeakWorker.js deleted file mode 100644 index dbfdea32d..000000000 --- a/extensions/tmtv/src/utils/calculateSUVPeakWorker.js +++ /dev/null @@ -1,209 +0,0 @@ -import { utilities } from '@cornerstonejs/core'; -import { utilities as cstUtils } from '@cornerstonejs/tools'; -import { vec3 } from 'gl-matrix'; -import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; -import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; -import { expose } from 'comlink'; - -const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => { - const imageData = vtkImageData.newInstance(); - imageData.setDimensions(dimensions); - imageData.setOrigin(origin); - imageData.setDirection(direction); - imageData.setSpacing(spacing); - - const scalarArray = vtkDataArray.newInstance({ - name: 'Pixels', - numberOfComponents: 1, - values: scalarData, - }); - - imageData.getPointData().setScalars(scalarArray); - - imageData.modified(); - - const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({ - scalarData, - dimensions, - numberOfComponents: 1, - }); - return { - imageData, - spacing, - origin, - direction, - metadata, - voxelManager, - }; -}; - -/** - * This method calculates the SUV peak on a segmented ROI from a reference PET - * volume. If a rectangle annotation is provided, the peak is calculated within that - * rectangle. Otherwise, the calculation is performed on the entire volume which - * will be slower but same result. - * @param viewport Viewport to use for the calculation - * @param labelmap Labelmap from which the mask is taken - * @param referenceVolume PET volume to use for SUV calculation - * @param toolData [Optional] list of toolData to use for SUV calculation - * @param segmentIndex The index of the segment to use for masking - * @returns - */ -function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) { - const labelmapInfo = createVolume(labelmapProps); - const referenceInfo = createVolume(referenceVolumeProps); - - if (referenceInfo.metadata.Modality !== 'PT') { - return; - } - - const { dimensions, imageData: labelmapImageData } = labelmapInfo; - const { imageData: referenceVolumeImageData } = referenceInfo; - - let boundsIJK; - // Todo: using the first annotation for now - if (annotations?.length && annotations[0].data?.cachedStats) { - const { projectionPoints } = annotations[0].data.cachedStats; - const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now - - const rectangleCornersIJK = pointsToUse.map(world => { - const ijk = vec3.fromValues(0, 0, 0); - referenceVolumeImageData.worldToIndex(world, ijk); - return ijk; - }); - - boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); - } - - let max = 0; - let maxIJK = [0, 0, 0]; - let maxLPS = [0, 0, 0]; - - const callback = ({ pointIJK, pointLPS }) => { - const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK); - - if (value !== segmentIndex) { - return; - } - - const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK); - - if (referenceValue > max) { - max = referenceValue; - maxIJK = pointIJK; - maxLPS = pointLPS; - } - }; - - labelmapInfo.voxelManager.forEach(callback, { - boundsIJK, - imageData: labelmapImageData, - isInObject: () => true, - returnPoints: true, - }); - - const direction = labelmapImageData.getDirection().slice(0, 3); - - /** - * 2. Find the bottom and top of the great circle for the second sphere (1cc sphere) - * V = (4/3)Ï€r3 - */ - const radius = Math.pow(1 / ((4 / 3) * Math.PI), 1 / 3) * 10; - const diameter = radius * 2; - - const secondaryCircleWorld = vec3.create(); - const bottomWorld = vec3.create(); - const topWorld = vec3.create(); - referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld); - vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2); - vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2); - const suvPeakCirclePoints = [bottomWorld, topWorld]; - - /** - * 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous - * sphere - */ - let count = 0; - let acc = 0; - const suvPeakMeanCallback = ({ value }) => { - acc += value; - count += 1; - }; - - cstUtils.pointInSurroundingSphereCallback( - referenceVolumeImageData, - suvPeakCirclePoints, - suvPeakMeanCallback - ); - - const mean = acc / count; - - return { - max, - maxIJK, - maxLPS, - mean, - }; -} - -function calculateTMTV(labelmapProps, segmentIndex = 1) { - const labelmaps = labelmapProps.map(props => createVolume(props)); - - const mergedLabelmap = - labelmaps.length === 1 - ? labelmaps[0] - : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); - - const { imageData, spacing } = mergedLabelmap; - const values = imageData.getPointData().getScalars().getData(); - - // count non-zero values inside the outputData, this would - // consider the overlapping regions to be only counted once - const numVoxels = values.reduce((acc, curr) => { - if (curr > 0) { - return acc + 1; - } - return acc; - }, 0); - - return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; -} - -function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) { - const labelmaps = labelmapProps.map(props => createVolume(props)); - - const mergedLabelmap = - labelmaps.length === 1 - ? labelmaps[0] - : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); - - // grabbing the first labelmap referenceVolume since it will be the same for all - const { spacing } = labelmaps[0]; - - const ptVolume = createVolume(referenceVolumeProps); - - let suv = 0; - let totalLesionVoxelCount = 0; - const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength(); - for (let i = 0; i < scalarDataLength; i++) { - // if not background - if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) { - suv += ptVolume.voxelManager.getAtIndex(i); - totalLesionVoxelCount += 1; - } - } - - // Average SUV for the merged labelmap - const averageSuv = suv / totalLesionVoxelCount; - - // total Lesion Glycolysis [suv * ml] - return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3; -} - -const obj = { - calculateSuvPeak, - calculateTMTV, - getTotalLesionGlycolysis, -}; - -expose(obj); diff --git a/extensions/tmtv/src/utils/calculateTMTV.ts b/extensions/tmtv/src/utils/calculateTMTV.ts deleted file mode 100644 index 0fcc680ae..000000000 --- a/extensions/tmtv/src/utils/calculateTMTV.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Types } from '@cornerstonejs/core'; -import { utilities } from '@cornerstonejs/tools'; - -/** - * Given a list of labelmaps (with the possibility of overlapping regions), - * and a referenceVolume, it calculates the total metabolic tumor volume (TMTV) - * by flattening and rasterizing each segment into a single labelmap and summing - * the total number of volume voxels. It should be noted that for this calculation - * we do not double count voxels that are part of multiple labelmaps. - * @param {} labelmaps - * @param {number} segmentIndex - * @returns {number} TMTV in ml - */ -function calculateTMTV(labelmaps: Array, segmentIndex = 1): number { - const volumeId = 'mergedLabelmap'; - - const mergedLabelmap = utilities.segmentation.createMergedLabelmapForIndex( - labelmaps, - segmentIndex, - volumeId - ); - - const { imageData, spacing, voxelManager } = mergedLabelmap; - - // count non-zero values inside the outputData, this would - // consider the overlapping regions to be only counted once - let numVoxels = 0; - const callback = ({ value }) => { - if (value > 0) { - numVoxels += 1; - } - }; - - voxelManager.forEach(callback, { - imageData, - isInObject: () => true, - }); - - return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; -} - -export default calculateTMTV; diff --git a/extensions/tmtv/src/utils/handleROIThresholding.ts b/extensions/tmtv/src/utils/handleROIThresholding.ts index bb5ae0c55..9d001b0c9 100644 --- a/extensions/tmtv/src/utils/handleROIThresholding.ts +++ b/extensions/tmtv/src/utils/handleROIThresholding.ts @@ -1,72 +1,12 @@ -import { Segment, Segmentation } from '@cornerstonejs/tools/types'; -import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core'; - export const handleROIThresholding = async ({ - segmentationId, commandsManager, segmentationService, }: withAppTypes<{ segmentationId: string; }>) => { - const segmentation = segmentationService.getSegmentation(segmentationId); - - triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { - progress: 0, - type: 'Calculate Lesion Stats', - id: segmentationId, - }); - - // re-calculating the cached stats for the active segmentation - const updatedPerSegmentCachedStats = {}; - for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) { - if (!segment) { - continue; - } - - const numericSegmentIndex = Number(segmentIndex); - - const lesionStats = await commandsManager.run('getLesionStats', { - segmentationId, - segmentIndex: numericSegmentIndex, - }); - - const suvPeak = await commandsManager.run('calculateSuvPeak', { - segmentationId, - segmentIndex: numericSegmentIndex, - }); - - const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; - - // update segDetails with the suv peak for the active segmentation - const cachedStats = { - lesionStats, - suvPeak, - lesionGlyoclysisStats, - }; - - const updatedSegment: Segment = { - ...segment, - cachedStats: { - ...segment.cachedStats, - ...cachedStats, - }, - }; - - updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats; - - segmentation.segments[segmentIndex] = updatedSegment; - } - - // all available segmentations const segmentations = segmentationService.getSegmentations(); const tmtv = await commandsManager.run('calculateTMTV', { segmentations }); - triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { - progress: 100, - type: 'Calculate Lesion Stats', - id: segmentationId, - }); - // add the tmtv to all the segment cachedStats, although it is a global // value but we don't have any other way to display it for now // Update all segmentations with the calculated TMTV @@ -76,22 +16,6 @@ export const handleROIThresholding = async ({ tmtv, }; - // Update each segment within the segmentation - Object.keys(segmentation.segments).forEach(segmentIndex => { - segmentation.segments[segmentIndex].cachedStats = { - ...segmentation.segments[segmentIndex].cachedStats, - tmtv, - }; - }); - - // Update the segmentation object - const updatedSegmentation: Segmentation = { - ...segmentation, - segments: { - ...segmentation.segments, - }, - }; - - segmentationService.addOrUpdateSegmentation(updatedSegmentation); + segmentationService.addOrUpdateSegmentation(segmentation); }); }; diff --git a/modes/basic-test-mode/src/toolbarButtons.ts b/modes/basic-test-mode/src/toolbarButtons.ts index 7569e45de..f63af8291 100644 --- a/modes/basic-test-mode/src/toolbarButtons.ts +++ b/modes/basic-test-mode/src/toolbarButtons.ts @@ -214,7 +214,7 @@ const toolbarButtons: Button[] = [ protocolId: 'mpr', }, }, - evaluate: 'evaluate.mpr', + evaluate: 'evaluate.displaySetIsReconstructable', }, }, { diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 212945c49..7b8325d10 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -12,6 +12,7 @@ const ohif = { const cornerstone = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', + measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', }; const segmentation = { @@ -19,6 +20,10 @@ const segmentation = { viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; +const dicomRT = { + viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt', + sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', +}; /** * Just two dependencies to be able to render a viewport with panels in order * to make sure that the mode is working. @@ -27,6 +32,7 @@ const extensionDependencies = { '@ohif/extension-default': '^3.0.0', '@ohif/extension-cornerstone': '^3.0.0', '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', + '@ohif/extension-cornerstone-dicom-rt': '^3.0.0', }; function modeFactory({ modeConfiguration }) { @@ -81,7 +87,14 @@ function modeFactory({ modeConfiguration }) { 'TagBrowser', ]); - toolbarService.createButtonSection('segmentationToolbox', ['SegmentationTools']); + toolbarService.createButtonSection('segmentationToolbox', [ + 'SegmentationUtilities', + 'SegmentationTools', + ]); + toolbarService.createButtonSection('segmentationToolboxUtilitySection', [ + 'InterpolateLabelmap', + 'SegmentBidirectional', + ]); toolbarService.createButtonSection('segmentationToolboxToolsSection', [ 'BrushTools', 'Shapes', @@ -149,7 +162,7 @@ function modeFactory({ modeConfiguration }) { props: { leftPanels: [ohif.leftPanel], leftPanelResizable: true, - rightPanels: [cornerstone.panelTool], + rightPanels: [cornerstone.panelTool, cornerstone.measurements], rightPanelResizable: true, // leftPanelClosed: true, viewports: [ @@ -161,6 +174,10 @@ function modeFactory({ modeConfiguration }) { namespace: segmentation.viewport, displaySetsToDisplay: [segmentation.sopClassHandler], }, + { + namespace: dicomRT.viewport, + displaySetsToDisplay: [dicomRT.sopClassHandler], + }, ], }, }; @@ -174,7 +191,7 @@ function modeFactory({ modeConfiguration }) { // The example is used for a grid layout to specify that as a preferred layout // hangingProtocol: ['@ohif/mnGrid'], /** SopClassHandlers used by the mode */ - sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler], + sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler], }; } diff --git a/modes/segmentation/src/initToolGroups.ts b/modes/segmentation/src/initToolGroups.ts index 6335563e2..1f4de6ee4 100644 --- a/modes/segmentation/src/initToolGroups.ts +++ b/modes/segmentation/src/initToolGroups.ts @@ -76,6 +76,9 @@ function createTools(utilityModule) { }, }, }, + { + toolName: toolNames.SegmentBidirectional, + }, { toolName: 'ThresholdSphereBrushDynamic', parentTool: 'Brush', diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts index a9d81b3b8..42afe1b9e 100644 --- a/modes/segmentation/src/toolbarButtons.ts +++ b/modes/segmentation/src/toolbarButtons.ts @@ -255,7 +255,7 @@ const toolbarButtons: Button[] = [ { id: 'Brush', - uiType: 'ohif.toolButton', + uiType: 'ohif.toolBoxButton', props: { icon: 'icon-tool-brush', label: 'Brush', @@ -292,9 +292,39 @@ const toolbarButtons: Button[] = [ ], }, }, + { + id: 'InterpolateLabelmap', + uiType: 'ohif.toolBoxButton', + props: { + icon: 'icon-tool-interpolation', + label: 'Interpolate Labelmap', + evaluate: [ + 'evaluate.cornerstone.segmentation', + { + name: 'evaluate.displaySetIsReconstructable', + disabledText: 'The current viewport cannot handle interpolation.', + }, + ], + commands: 'interpolateLabelmap', + }, + }, + { + id: 'SegmentBidirectional', + uiType: 'ohif.toolBoxButton', + props: { + icon: 'icon-tool-bidirectional-segment', + label: 'Segment Bidirectional', + tooltip: 'Finding the largest bidirectional measurement per lesion', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + disabledText: 'Create new segmentation to enable this tool.', + }, + commands: 'runSegmentBidirectional', + }, + }, { id: 'Eraser', - uiType: 'ohif.toolButton', + uiType: 'ohif.toolBoxButton', props: { icon: 'icon-tool-eraser', label: 'Eraser', @@ -332,7 +362,7 @@ const toolbarButtons: Button[] = [ }, { id: 'Threshold', - uiType: 'ohif.toolButton', + uiType: 'ohif.toolBoxButton', props: { icon: 'icon-tool-threshold', label: 'Threshold Tool', diff --git a/platform/app/package.json b/platform/app/package.json index feb81fa63..edf2acf9c 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.5.0", + "@cornerstonejs/dicom-image-loader": "^3.7.0", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.10.0-beta.125", "@ohif/extension-cornerstone": "3.10.0-beta.125", diff --git a/platform/core/package.json b/platform/core/package.json index b2a741651..85dc03e50 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.5.0", + "@cornerstonejs/dicom-image-loader": "^3.7.0", "@ohif/ui": "3.10.0-beta.125", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index b38e8efa0..90334c13a 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -585,7 +585,11 @@ export default class ToolbarService extends PubSubService { }); props.evaluate = args => { - const results = evaluators.map(evaluator => evaluator(args)); + const results = evaluators.map(evaluator => evaluator(args)).filter(Boolean); + + // had at least one disabled button, so we need to disable the button + const hasDisabledButton = results?.some(result => result.disabled); + const mergedResult = results.reduce((acc, result) => { return { ...acc, @@ -593,6 +597,10 @@ export default class ToolbarService extends PubSubService { }; }, {}); + if (hasDisabledButton) { + mergedResult.disabled = true; + } + return mergedResult; }; diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.ts b/platform/core/src/services/_shared/pubSubServiceInterface.ts index a286b9755..59c41e74f 100644 --- a/platform/core/src/services/_shared/pubSubServiceInterface.ts +++ b/platform/core/src/services/_shared/pubSubServiceInterface.ts @@ -1,4 +1,5 @@ import guid from '../../utils/guid'; +import debounce from '../../utils/debounce'; /** * Consumer must implement: @@ -92,14 +93,14 @@ function _broadcastEvent(eventName, callbackProps) { /** Export a PubSubService class to be used instead of the individual items */ export class PubSubService { - EVENTS: any; - subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any }; - _broadcastEvent: (eventName: string, callbackProps: any) => void; + EVENTS: Record; + subscribe: (eventName: string, callback: (data: unknown) => void) => { unsubscribe: () => void }; + _broadcastEvent: (eventName: string, callbackProps: unknown) => void; _unsubscribe: (eventName: string, listenerId: string) => void; _isValidEvent: (eventName: string) => boolean; - listeners: {}; - unsubscriptions: any[]; - constructor(EVENTS) { + listeners: Record void }> | undefined>; + unsubscriptions: Array<() => void>; + constructor(EVENTS: Record) { this.EVENTS = EVENTS; this.subscribe = subscribe; this._broadcastEvent = _broadcastEvent; @@ -109,6 +110,27 @@ export class PubSubService { this.unsubscriptions = []; } + /** + * Subscribe to updates with debouncing to limit callback execution frequency + * @param eventName - The name of the event + * @param callback - Events callback + * @param wait - Debounce wait time in milliseconds + * @param immediate - If true, trigger on the leading edge instead of trailing + */ + subscribeDebounced( + eventName: string, + callback: (data: unknown) => void, + wait = 300, + immediate = false + ) { + if (this._isValidEvent(eventName)) { + const debouncedCallback = debounce(callback, wait, immediate); + return this.subscribe(eventName, debouncedCallback); + } else { + throw new Error(`Event ${eventName} not supported.`); + } + } + reset() { this.unsubscriptions.forEach(unsub => unsub()); this.unsubscriptions = []; @@ -120,7 +142,9 @@ export class PubSubService { * Check eventData.isConsumed to see if it is consumed or not. * @param props - to include in the event */ - protected createConsumableEvent(props) { + protected createConsumableEvent>( + props: T + ): T & { isConsumed: boolean; consume: () => void } { return { ...props, isConsumed: false, diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-SegmentationTable.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-SegmentationTable.md new file mode 100644 index 000000000..713f44ec3 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-SegmentationTable.md @@ -0,0 +1,83 @@ +--- +title: Segmentation Table +--- + + +# SegmentationTable Migration Guide + +This guide will help you migrate your code to use the refactored SegmentationTable component. + +## Key Changes + +* **Context System Refactoring**: The context system has been completely redesigned with dedicated providers for different aspects of the segmentation UI +* **Compound Component Pattern**: Components now follow a more structured compound component pattern with clearer parent-child relationships +* **Header Components Changed**: The `SegmentationTable.SelectorHeader` has been replaced with specialized header components +* **New Features**: Added segment statistics, hover cards, and better customization options +* **Better Customization**: Added support for custom dropdown menus and segment statistics headers + +## Migration Steps + + + +### 1. Migrate from SelectorHeader to New Header Components + +The `SegmentationTable.SelectorHeader` has been removed. Use the new Collapsed pattern instead: + +```diff +- +- +- + ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ {/* Content here */} ++ ++ +``` + +### 2. Update Component Hierarchy for Expanded View + +The expanded view structure has also changed: + +```diff +- +- +- +- +- +- + ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +``` + +### 3. Using the New Segment Statistics Component + +The new `SegmentStatistics` component provides a way to display segment statistics: + +```diff ++ ++ ++ ++ ++ ++ +``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tours.md similarity index 100% rename from platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md rename to platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tours.md diff --git a/platform/docs/src/pages/patterns/patterns-segmentation.tsx b/platform/docs/src/pages/patterns/patterns-segmentation.tsx index c78b6c226..3e9681aa5 100644 --- a/platform/docs/src/pages/patterns/patterns-segmentation.tsx +++ b/platform/docs/src/pages/patterns/patterns-segmentation.tsx @@ -37,6 +37,11 @@ import { Input } from '../../../../ui-next/src/components/Input'; import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; +import { + HoverCard, + HoverCardTrigger, + HoverCardContent, +} from '../../../../ui-next/src/components/HoverCard'; interface DataItem { id: number; @@ -44,8 +49,29 @@ interface DataItem { description: string; optionalField?: string; colorHex?: string; - details?: string; + details?: { primary: string[]; secondary: string[] }; series?: string; + statistics?: { + centroidX: string; + centroidY: string; + centroidZ: string; + frameDuration: string; + kurtosis: string; + max: string; + maxSlice: string; + mean: string; + median: string; + min: string; + regions: string; + skewness: string; + sphereDiameter: string; + standardDeviation: string; + suvPeak: string; + total: string; + glycolysis: string; + volume: string; + voxelCount: string; + }; } interface ListGroup { @@ -67,13 +93,189 @@ export default function SegmentationPanel() { }; const organSegmentationGroup = dataList.find( - (listGroup: ListGroup) => listGroup.type === 'Organ Segmentation' - ); + (listGroup: any) => listGroup.type === 'Organ Segmentation' + ) as unknown as ListGroup; if (!organSegmentationGroup) { return
Organ Segmentation data not found.
; } + // Create a state to track which item's statistics to show + + // Function to render statistics panel + const renderStatisticsPanel = (item: DataItem) => { + if (!item.statistics) { + return null; + } + + const stats = item.statistics; + return ( +
+
+
+

{item.title}

+
+ +
+
+
Centroid X
+
+ {stats.centroidX.value}{' '} + {stats.centroidX.unit} +
+
+ +
+
Centroid Y
+
+ {stats.centroidY.value}{' '} + {stats.centroidY.unit} +
+
+ +
+
Centroid Z
+
+ {stats.centroidZ.value}{' '} + {stats.centroidZ.unit} +
+
+ +
+
Frame Duration
+
+ {stats.frameDuration.value}{' '} + {stats.frameDuration.unit} +
+
+ +
+
Kurtosis
+
+ {stats.kurtosis.value}{' '} + {stats.kurtosis.unit} +
+
+ +
+
Max
+
+ {stats.max.value}{' '} + {stats.max.unit} +
+
+ +
+
Max Slice
+
+ {stats.maxSlice.value}{' '} + {stats.maxSlice.unit} +
+
+ +
+
Mean
+
+ {stats.mean.value}{' '} + {stats.mean.unit} +
+
+ +
+
Median
+
+ {stats.median.value}{' '} + {stats.median.unit} +
+
+ +
+
Min
+
+ {stats.min.value}{' '} + {stats.min.unit} +
+
+ +
+
Regions
+
+ {stats.regions.value}{' '} + {stats.regions.unit} +
+
+ +
+
Skewness
+
+ {stats.skewness.value}{' '} + {stats.skewness.unit} +
+
+ +
+
Sphere Diameter
+
+ {stats.sphereDiameter.value}{' '} + {stats.sphereDiameter.unit} +
+
+ +
+
Standard Deviation
+
+ {stats.standardDeviation.value}{' '} + {stats.standardDeviation.unit} +
+
+ +
+
SUV Peak
+
+ {stats.suvPeak.value}{' '} + {stats.suvPeak.unit} +
+
+ +
+
Total
+
+ {stats.total.value}{' '} + {stats.total.unit} +
+
+ +
+
Glycolysis
+
+ {stats.glycolysis.value}{' '} + {stats.glycolysis.unit} +
+
+ +
+
Volume
+
+ {stats.volume.value}{' '} + {stats.volume.unit} +
+
+ +
+
Voxel Count
+
+ {stats.voxelCount.value}{' '} + {stats.voxelCount.unit} +
+
+
+
+ ); + }; + return (
@@ -284,20 +486,46 @@ export default function SegmentationPanel() { {organSegmentationGroup.items.map((item, index) => { const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID return ( - handleAction(compositeId, action)} - isSelected={selectedRowId === compositeId} - onSelect={() => handleRowSelect(compositeId)} - /> + + +
+ handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + isVisible={true} + isLocked={false} + onToggleVisibility={() => console.debug('Toggle visibility')} + onToggleLocked={() => console.debug('Toggle locked')} + onRename={() => console.debug('Rename')} + onDelete={() => console.debug('Delete')} + onColor={() => console.debug('Color')} + disableEditing={false} + /> +
+
+ + {renderStatisticsPanel(item)} + +
); })}
diff --git a/platform/ui-next/assets/data/index.ts b/platform/ui-next/assets/data/index.ts index 8d63a4e7e..8eac4ec80 100644 --- a/platform/ui-next/assets/data/index.ts +++ b/platform/ui-next/assets/data/index.ts @@ -118,84 +118,378 @@ const dataList = [ title: 'Spleen', description: 'Description for Spleen.', colorHex: '#6B8E23', + statistics: { + centroidX: { value: -3.45, unit: 'cm' }, + centroidY: { value: 4.78, unit: 'cm' }, + centroidZ: { value: 118.32, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.22, unit: '' }, + max: { value: 14.92, unit: 'SUV' }, + maxSlice: { value: 78, unit: '' }, + mean: { value: 7.14, unit: 'SUV' }, + median: { value: 6.85, unit: 'SUV' }, + min: { value: 3.8, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.76, unit: '' }, + sphereDiameter: { value: 7.12, unit: 'cm' }, + standardDeviation: { value: 2.31, unit: 'SUV' }, + suvPeak: { value: 12.95, unit: 'SUV' }, + total: { value: 18245.63, unit: 'SUV' }, + glycolysis: { value: 1245.78, unit: 'SUV ml' }, + volume: { value: 178.34, unit: 'ml' }, + voxelCount: { value: 2834, unit: '' }, + }, }, { id: 16, title: 'Kidney', description: 'Description for Kidney.', colorHex: '#4682B4', + statistics: { + centroidX: { value: 2.18, unit: 'cm' }, + centroidY: { value: 6.32, unit: 'cm' }, + centroidZ: { value: 125.47, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.18, unit: '' }, + max: { value: 16.23, unit: 'SUV' }, + maxSlice: { value: 92, unit: '' }, + mean: { value: 8.05, unit: 'SUV' }, + median: { value: 7.92, unit: 'SUV' }, + min: { value: 4.2, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.91, unit: '' }, + sphereDiameter: { value: 8.45, unit: 'cm' }, + standardDeviation: { value: 2.67, unit: 'SUV' }, + suvPeak: { value: 14.32, unit: 'SUV' }, + total: { value: 22567.89, unit: 'SUV' }, + glycolysis: { value: 1532.45, unit: 'SUV ml' }, + volume: { value: 215.67, unit: 'ml' }, + voxelCount: { value: 3245, unit: '' }, + }, }, { id: 17, title: 'Kidney very long title name lorem ipsum dolor sit amet segmentation', description: 'Description for Kidney.', colorHex: '#9ACD32', + statistics: { + centroidX: { value: -2.34, unit: 'cm' }, + centroidY: { value: 5.87, unit: 'cm' }, + centroidZ: { value: 124.65, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.25, unit: '' }, + max: { value: 15.45, unit: 'SUV' }, + maxSlice: { value: 88, unit: '' }, + mean: { value: 7.82, unit: 'SUV' }, + median: { value: 7.45, unit: 'SUV' }, + min: { value: 4.1, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.85, unit: '' }, + sphereDiameter: { value: 7.95, unit: 'cm' }, + standardDeviation: { value: 2.54, unit: 'SUV' }, + suvPeak: { value: 13.78, unit: 'SUV' }, + total: { value: 21345.67, unit: 'SUV' }, + glycolysis: { value: 1423.56, unit: 'SUV ml' }, + volume: { value: 198.45, unit: 'ml' }, + voxelCount: { value: 3012, unit: '' }, + }, }, { id: 18, title: 'Gallbladder', description: 'Description for Gallbladder.', colorHex: '#20B2AA', + statistics: { + centroidX: { value: -1.87, unit: 'cm' }, + centroidY: { value: 4.23, unit: 'cm' }, + centroidZ: { value: 119.78, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.14, unit: '' }, + max: { value: 12.67, unit: 'SUV' }, + maxSlice: { value: 72, unit: '' }, + mean: { value: 6.34, unit: 'SUV' }, + median: { value: 6.12, unit: 'SUV' }, + min: { value: 3.5, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.72, unit: '' }, + sphereDiameter: { value: 5.23, unit: 'cm' }, + standardDeviation: { value: 2.12, unit: 'SUV' }, + suvPeak: { value: 11.45, unit: 'SUV' }, + total: { value: 9876.54, unit: 'SUV' }, + glycolysis: { value: 876.32, unit: 'SUV ml' }, + volume: { value: 98.45, unit: 'ml' }, + voxelCount: { value: 1567, unit: '' }, + }, }, { id: 19, title: 'Esophagus', description: 'Description for Esophagus.', colorHex: '#DAA520', + statistics: { + centroidX: { value: 0.45, unit: 'cm' }, + centroidY: { value: 7.65, unit: 'cm' }, + centroidZ: { value: 132.45, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.12, unit: '' }, + max: { value: 11.34, unit: 'SUV' }, + maxSlice: { value: 65, unit: '' }, + mean: { value: 5.87, unit: 'SUV' }, + median: { value: 5.65, unit: 'SUV' }, + min: { value: 3.2, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.68, unit: '' }, + sphereDiameter: { value: 4.56, unit: 'cm' }, + standardDeviation: { value: 1.98, unit: 'SUV' }, + suvPeak: { value: 10.23, unit: 'SUV' }, + total: { value: 7654.32, unit: 'SUV' }, + glycolysis: { value: 765.43, unit: 'SUV ml' }, + volume: { value: 87.65, unit: 'ml' }, + voxelCount: { value: 1345, unit: '' }, + }, }, { id: 20, title: 'Liver', description: 'Description for Liver.', colorHex: '#CD5C5C', + statistics: { + centroidX: { value: -1.23, unit: 'cm' }, + centroidY: { value: 5.43, unit: 'cm' }, + centroidZ: { value: 121.34, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.19, unit: '' }, + max: { value: 17.56, unit: 'SUV' }, + maxSlice: { value: 95, unit: '' }, + mean: { value: 8.76, unit: 'SUV' }, + median: { value: 8.45, unit: 'SUV' }, + min: { value: 4.5, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.95, unit: '' }, + sphereDiameter: { value: 9.87, unit: 'cm' }, + standardDeviation: { value: 2.87, unit: 'SUV' }, + suvPeak: { value: 15.67, unit: 'SUV' }, + total: { value: 32456.78, unit: 'SUV' }, + glycolysis: { value: 2345.67, unit: 'SUV ml' }, + volume: { value: 345.67, unit: 'ml' }, + voxelCount: { value: 5678, unit: '' }, + }, }, { id: 21, title: 'Stomach', description: 'Description for Stomach.', colorHex: '#778899', + statistics: { + centroidX: { value: -0.87, unit: 'cm' }, + centroidY: { value: 4.98, unit: 'cm' }, + centroidZ: { value: 120.56, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.15, unit: '' }, + max: { value: 13.45, unit: 'SUV' }, + maxSlice: { value: 82, unit: '' }, + mean: { value: 6.78, unit: 'SUV' }, + median: { value: 6.54, unit: 'SUV' }, + min: { value: 3.7, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.79, unit: '' }, + sphereDiameter: { value: 6.78, unit: 'cm' }, + standardDeviation: { value: 2.23, unit: 'SUV' }, + suvPeak: { value: 12.34, unit: 'SUV' }, + total: { value: 15678.9, unit: 'SUV' }, + glycolysis: { value: 1123.45, unit: 'SUV ml' }, + volume: { value: 156.78, unit: 'ml' }, + voxelCount: { value: 2456, unit: '' }, + }, }, { id: 22, title: 'Abdominal aorta', description: 'Description for Abdominal Aorta.', colorHex: '#B8860B', + statistics: { + centroidX: { value: 0.12, unit: 'cm' }, + centroidY: { value: 6.54, unit: 'cm' }, + centroidZ: { value: 126.78, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.11, unit: '' }, + max: { value: 10.23, unit: 'SUV' }, + maxSlice: { value: 68, unit: '' }, + mean: { value: 5.43, unit: 'SUV' }, + median: { value: 5.21, unit: 'SUV' }, + min: { value: 3.1, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.65, unit: '' }, + sphereDiameter: { value: 3.45, unit: 'cm' }, + standardDeviation: { value: 1.87, unit: 'SUV' }, + suvPeak: { value: 9.45, unit: 'SUV' }, + total: { value: 5432.1, unit: 'SUV' }, + glycolysis: { value: 654.32, unit: 'SUV ml' }, + volume: { value: 67.89, unit: 'ml' }, + voxelCount: { value: 1123, unit: '' }, + }, }, { id: 23, title: 'Inferior vena cava', description: 'Description for Inferior Vena Cava.', colorHex: '#556B2F', + statistics: { + centroidX: { value: 0.34, unit: 'cm' }, + centroidY: { value: 6.12, unit: 'cm' }, + centroidZ: { value: 125.43, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.13, unit: '' }, + max: { value: 9.87, unit: 'SUV' }, + maxSlice: { value: 64, unit: '' }, + mean: { value: 5.12, unit: 'SUV' }, + median: { value: 4.98, unit: 'SUV' }, + min: { value: 2.9, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.62, unit: '' }, + sphereDiameter: { value: 3.21, unit: 'cm' }, + standardDeviation: { value: 1.76, unit: 'SUV' }, + suvPeak: { value: 8.98, unit: 'SUV' }, + total: { value: 4567.89, unit: 'SUV' }, + glycolysis: { value: 598.76, unit: 'SUV ml' }, + volume: { value: 59.87, unit: 'ml' }, + voxelCount: { value: 987, unit: '' }, + }, }, { id: 24, title: 'Portal vein', description: 'Description for Portal Vein.', colorHex: '#8B4513', + statistics: { + centroidX: { value: -0.45, unit: 'cm' }, + centroidY: { value: 5.67, unit: 'cm' }, + centroidZ: { value: 123.45, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.14, unit: '' }, + max: { value: 10.56, unit: 'SUV' }, + maxSlice: { value: 71, unit: '' }, + mean: { value: 5.67, unit: 'SUV' }, + median: { value: 5.45, unit: 'SUV' }, + min: { value: 3.2, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.69, unit: '' }, + sphereDiameter: { value: 3.78, unit: 'cm' }, + standardDeviation: { value: 1.92, unit: 'SUV' }, + suvPeak: { value: 9.67, unit: 'SUV' }, + total: { value: 6789.01, unit: 'SUV' }, + glycolysis: { value: 723.45, unit: 'SUV ml' }, + volume: { value: 76.54, unit: 'ml' }, + voxelCount: { value: 1234, unit: '' }, + }, }, { id: 25, title: 'Pancreas', description: 'Description for Pancreas.', colorHex: '#2F4F4F', + statistics: { + centroidX: { value: -1.56, unit: 'cm' }, + centroidY: { value: 5.23, unit: 'cm' }, + centroidZ: { value: 120.87, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.17, unit: '' }, + max: { value: 14.23, unit: 'SUV' }, + maxSlice: { value: 79, unit: '' }, + mean: { value: 7.23, unit: 'SUV' }, + median: { value: 6.98, unit: 'SUV' }, + min: { value: 3.9, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.81, unit: '' }, + sphereDiameter: { value: 6.34, unit: 'cm' }, + standardDeviation: { value: 2.34, unit: 'SUV' }, + suvPeak: { value: 12.67, unit: 'SUV' }, + total: { value: 14567.89, unit: 'SUV' }, + glycolysis: { value: 1087.65, unit: 'SUV ml' }, + volume: { value: 134.56, unit: 'ml' }, + voxelCount: { value: 2134, unit: '' }, + }, }, { id: 26, title: 'Adrenal gland', description: 'Description for Adrenal Gland.', colorHex: '#708090', + statistics: { + centroidX: { value: -2.12, unit: 'cm' }, + centroidY: { value: 5.76, unit: 'cm' }, + centroidZ: { value: 122.34, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.16, unit: '' }, + max: { value: 12.34, unit: 'SUV' }, + maxSlice: { value: 74, unit: '' }, + mean: { value: 6.45, unit: 'SUV' }, + median: { value: 6.23, unit: 'SUV' }, + min: { value: 3.6, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.74, unit: '' }, + sphereDiameter: { value: 5.67, unit: 'cm' }, + standardDeviation: { value: 2.21, unit: 'SUV' }, + suvPeak: { value: 11.23, unit: 'SUV' }, + total: { value: 10987.65, unit: 'SUV' }, + glycolysis: { value: 945.67, unit: 'SUV ml' }, + volume: { value: 112.34, unit: 'ml' }, + voxelCount: { value: 1789, unit: '' }, + }, }, { id: 27, title: 'Adrenal gland', description: 'Description for Adrenal Gland.', colorHex: '#6A5ACD', + statistics: { + centroidX: { value: 2.34, unit: 'cm' }, + centroidY: { value: 5.89, unit: 'cm' }, + centroidZ: { value: 123.56, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.15, unit: '' }, + max: { value: 11.98, unit: 'SUV' }, + maxSlice: { value: 73, unit: '' }, + mean: { value: 6.32, unit: 'SUV' }, + median: { value: 6.12, unit: 'SUV' }, + min: { value: 3.5, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.73, unit: '' }, + sphereDiameter: { value: 5.45, unit: 'cm' }, + standardDeviation: { value: 2.18, unit: 'SUV' }, + suvPeak: { value: 10.87, unit: 'SUV' }, + total: { value: 10456.78, unit: 'SUV' }, + glycolysis: { value: 923.45, unit: 'SUV ml' }, + volume: { value: 109.87, unit: 'ml' }, + voxelCount: { value: 1756, unit: '' }, + }, }, { id: 28, title: 'New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test ', description: 'Description for New Seg Test.', colorHex: '#4682B4', + statistics: { + centroidX: { value: -2.76, unit: 'cm' }, + centroidY: { value: 4.98, unit: 'cm' }, + centroidZ: { value: 121.23, unit: 'cm' }, + frameDuration: { value: 'N/A', unit: 'ms' }, + kurtosis: { value: -0.21, unit: '' }, + max: { value: 15.12, unit: 'SUV' }, + maxSlice: { value: 83, unit: '' }, + mean: { value: 7.45, unit: 'SUV' }, + median: { value: 7.12, unit: 'SUV' }, + min: { value: 4.1, unit: 'SUV' }, + regions: { value: 1, unit: '' }, + skewness: { value: 0.82, unit: '' }, + sphereDiameter: { value: 7.23, unit: 'cm' }, + standardDeviation: { value: 2.45, unit: 'SUV' }, + suvPeak: { value: 13.45, unit: 'SUV' }, + total: { value: 19876.54, unit: 'SUV' }, + glycolysis: { value: 1345.67, unit: 'SUV ml' }, + volume: { value: 187.65, unit: 'ml' }, + voxelCount: { value: 2987, unit: '' }, + }, }, ], }, diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json index 27460481d..31453b502 100644 --- a/platform/ui-next/package.json +++ b/platform/ui-next/package.json @@ -44,6 +44,7 @@ "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@radix-ui/react-hover-card": "^1.1.6", "class-variance-authority": "^0.7.0", "clsx": "*", "cmdk": "^1.0.0", diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx index b50630e50..7d084e8a8 100644 --- a/platform/ui-next/src/components/DataRow/DataRow.tsx +++ b/platform/ui-next/src/components/DataRow/DataRow.tsx @@ -8,6 +8,7 @@ import { } from '../../components/DropdownMenu'; import { Icons } from '../../components/Icons/Icons'; import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip'; +import { cn } from '../../lib/utils'; /** * DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data. @@ -77,6 +78,7 @@ interface DataRowProps { // colorHex?: string; onColor: () => void; + className?: string; } const DataRow: React.FC = ({ @@ -94,16 +96,19 @@ const DataRow: React.FC = ({ isSelected = false, isVisible = true, disableEditing = false, + className, }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const isTitleLong = title?.length > 25; const rowRef = useRef(null); - useEffect(() => { - if (isSelected && rowRef.current) { - rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }, [isSelected]); + // useEffect(() => { + // if (isSelected && rowRef.current) { + // setTimeout(() => { + // rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // }, 200); + // } + // }, [isSelected]); const handleAction = (action: string, e: React.MouseEvent) => { e.stopPropagation(); @@ -190,7 +195,7 @@ const DataRow: React.FC = ({ return (
, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/platform/ui-next/src/components/HoverCard/index.ts b/platform/ui-next/src/components/HoverCard/index.ts new file mode 100644 index 000000000..476d28765 --- /dev/null +++ b/platform/ui-next/src/components/HoverCard/index.ts @@ -0,0 +1 @@ +export * from './HoverCard'; diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx index e311f4779..01c2a01ad 100644 --- a/platform/ui-next/src/components/Icons/Icons.tsx +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -24,6 +24,7 @@ import IconMPR from './Sources/IconMPR'; import Info from './Sources/Info'; import InfoLink from './Sources/InfoLink'; import InfoSeries from './Sources/InfoSeries'; +import JumpToSlice from './Sources/JumpToSlice'; import ListView from './Sources/ListView'; import LoadingSpinner from './Sources/LoadingSpinner'; import Lock from './Sources/Lock'; @@ -38,6 +39,7 @@ import Pin from './Sources/Pin'; import PinFill from './Sources/PinFill'; import Plus from './Sources/Plus'; import PowerOff from './Sources/PowerOff'; +import Redo from './Sources/Redo'; import Refresh from './Sources/Refresh'; import Rename from './Sources/Rename'; import Series from './Sources/Series'; @@ -195,6 +197,7 @@ import NotificationWarning from './Sources/NotificationWarning'; import ArrowRight from './Sources/ArrowRight'; import ChevronLeft from './Sources/ChevronLeft'; import StatusAlert from './Sources/StatusAlert'; +import Undo from './Sources/Undo'; // // type IconProps = React.HTMLAttributes; @@ -717,6 +720,9 @@ export const Icons = { 'tool-point': (props: IconProps) => ToolCircle(props), 'tool-freehand-line': (props: IconProps) => ToolFreehand(props), clipboard: (props: IconProps) => Clipboard(props), + Undo, + Redo, + JumpToSlice, /** Adds an icon to the set of icons */ addIcon: (name: string, icon) => { diff --git a/platform/ui-next/src/components/Icons/Sources/JumpToSlice.tsx b/platform/ui-next/src/components/Icons/Sources/JumpToSlice.tsx new file mode 100644 index 000000000..fcd10707d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/JumpToSlice.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const JumpToSlice = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default JumpToSlice; diff --git a/platform/ui-next/src/components/Icons/Sources/Redo.tsx b/platform/ui-next/src/components/Icons/Sources/Redo.tsx new file mode 100644 index 000000000..ca0c2d6b7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Redo.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Redo = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default Redo; diff --git a/platform/ui-next/src/components/Icons/Sources/Undo.tsx b/platform/ui-next/src/components/Icons/Sources/Undo.tsx new file mode 100644 index 000000000..6fc9e1f1a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Undo.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Undo = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default Undo; diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx index 3da4e253f..c385e3b3a 100644 --- a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx @@ -1,26 +1,8 @@ import React from 'react'; import { Button, Icons } from '@ohif/ui-next'; -import { useSegmentationTableContext } from './SegmentationTableContext'; +import { useSegmentationTableContext, useSegmentationExpanded } from './contexts'; -/** - * Props interface for the AddSegmentRow component - */ -interface AddSegmentRowProps { - /** Optional child elements to render within the row */ - children?: React.ReactNode; - /** Optional segmentation object to override the active segmentation */ - segmentation?: unknown; -} - -/** - * A component that renders a row with controls for adding segments and toggling visibility - * in the segmentation table. - * - * @param props - Component properties - * @param props.children - Optional child elements to render within the row - * @param props.segmentation - Optional segmentation object to override the active segmentation - */ -export const AddSegmentRow: React.FC = ({ children = null, segmentation }) => { +export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ children = null }) => { const { activeRepresentation, disableEditing, @@ -29,18 +11,32 @@ export const AddSegmentRow: React.FC = ({ children = null, s onToggleSegmentationRepresentationVisibility, data, showAddSegment, - } = useSegmentationTableContext('SegmentationTable'); + } = useSegmentationTableContext('AddSegmentRow'); - const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every( - segment => segment?.visible !== false - ); + // Try to get from expanded context first, then fall back to active segmentation + let segmentationId = activeSegmentationId; + let representation = activeRepresentation; - const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId; + try { + const expandedContext = useSegmentationExpanded('AddSegmentRow'); + if (expandedContext.isActive) { + segmentationId = expandedContext.segmentation.segmentationId; + representation = expandedContext.representation; + } + } catch (e) { + // Use the default values from table context + } + // If no segmentations, don't render if (!data?.length) { return null; } + // Check if all segments are visible + const allSegmentsVisible = Object.values(representation?.segments || {}).every( + segment => segment?.visible !== false + ); + const Icon = allSegmentsVisible ? ( ) : ( @@ -50,14 +46,14 @@ export const AddSegmentRow: React.FC = ({ children = null, s const allowAddSegment = showAddSegment && !disableEditing; return ( -
+
{allowAddSegment ? ( + + {children} + + ); +}; + +// Selector component - for the segmentation selection dropdown +const SegmentationCollapsedSelector = () => { + const { t } = useTranslation('SegmentationTable.HeaderCollapsed'); + const { data, activeSegmentationId, onSegmentationClick } = useSegmentationTableContext( + 'SegmentationCollapsedSelector' + ); + + if (!data?.length) { + return null; + } + + const segmentations = data.map(seg => ({ + id: seg.segmentation.segmentationId, + label: seg.segmentation.label, + })); + + return ( + + ); +}; + +// Info component - for displaying the info tooltip +const SegmentationCollapsedInfo = () => { + const { data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsedInfo'); + + const activeSegmentationObj = data.find( + seg => seg.segmentation.segmentationId === activeSegmentationId + ); + + const info = activeSegmentationObj?.segmentation.cachedStats?.info; + + return ( + + + + + + {info} + + + ); +}; + +// Content component - for the main collapsed view content +const SegmentationCollapsedContent = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +// Main compound component +const SegmentationCollapsedRoot: React.FC<{ children?: React.ReactNode }> = ({ children = null, }) => { - return
{children}
; + const { mode, data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsed'); + + // Check if we should render based on mode + if (mode !== 'collapsed' || !data || data.length === 0) { + return null; + } + + // Find active segmentation + const activeSegmentationInfo = data.find( + info => info.segmentation.segmentationId === activeSegmentationId + ); + + if (!activeSegmentationInfo) { + return null; + } + + return ( +
+ + {}} // No-op since it's already the active one + > + {children} + + +
+ ); }; + +// Add the subcomponents to the main component +const SegmentationCollapsed = Object.assign(SegmentationCollapsedRoot, { + Header: SegmentationCollapsedHeader, + DropdownMenu: SegmentationCollapsedDropdownMenu, + Selector: SegmentationCollapsedSelector, + Info: SegmentationCollapsedInfo, + Content: SegmentationCollapsedContent, +}); + +// Export the component +export { SegmentationCollapsed }; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx index fa1499291..bc988602f 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx @@ -1,47 +1,132 @@ import React from 'react'; -import { useSegmentationTableContext } from './SegmentationTableContext'; import { PanelSection } from '../PanelSection'; -import { SegmentationHeader } from './SegmentationHeader'; -import { SegmentationTable } from './SegmentationTable'; +import { + useSegmentationTableContext, + SegmentationExpandedProvider, + useSegmentationExpanded, +} from './contexts'; +import { Button } from '../Button'; +import { Icons } from '../Icons/Icons'; +import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip'; -export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => { - const { data } = useSegmentationTableContext('SegmentationExpanded'); - - // Separate the Header component from other children - const headerComponent = React.Children.toArray(children).find( - child => React.isValidElement(child) && child.type === SegmentationTable.Header - ); - const otherChildren = React.Children.toArray(children).filter( - child => !(React.isValidElement(child) && child.type === SegmentationTable.Header) - ); +// The Header container component +const SegmentationExpandedHeader = ({ children }: { children: React.ReactNode }) => { + const { segmentation, isActive } = useSegmentationExpanded('SegmentationExpandedHeader'); + const { onSegmentationClick } = useSegmentationTableContext('SegmentationExpandedHeader'); return ( - <> - {data.map(segmentationInfo => ( - - - {headerComponent ? ( - React.cloneElement(headerComponent as React.ReactElement, { - segmentation: segmentationInfo.segmentation, - representation: segmentationInfo.representation, - }) - ) : ( - - )} - - -
- {React.Children.map(otherChildren, child => - React.isValidElement(child) - ? React.cloneElement(child, { - segmentation: segmentationInfo.segmentation, - }) - : child - )} -
-
-
- ))} - + { + e.stopPropagation(); + onSegmentationClick(segmentation.segmentationId); + }} + > +
{children}
+
); }; + +// Dropdown menu component - specifically for dropdown menu content +const SegmentationExpandedDropdownMenu = ({ children }: { children: React.ReactNode }) => { + return ( + + + + + {children} + + ); +}; + +// Label component - for displaying the segmentation label +const SegmentationExpandedLabel = () => { + const { segmentation } = useSegmentationExpanded('SegmentationExpandedLabel'); + + return
{segmentation.label}
; +}; + +// Info component - for the info tooltip +const SegmentationExpandedInfo = () => { + const { segmentation } = useSegmentationExpanded('SegmentationExpandedInfo'); + + return ( +
+ + + + + +

{segmentation.cachedStats?.info}

+
+
+
+ ); +}; + +// Content component +const SegmentationExpandedContent = ({ children }: { children: React.ReactNode }) => { + return ( + +
{children}
+
+ ); +}; + +// Main compound component +const SegmentationExpandedRoot = ({ children }) => { + const { data, activeSegmentationId, onSegmentationClick, mode } = + useSegmentationTableContext('SegmentationExpanded'); + + // Check if we should render based on mode + if (mode !== 'expanded' || !data || data.length === 0) { + return null; + } + + return ( +
+ {data.map(segmentationInfo => { + const isActive = segmentationInfo.segmentation.segmentationId === activeSegmentationId; + + return ( + + + {children} + + + ); + })} +
+ ); +}; + +const SegmentationExpanded = Object.assign(SegmentationExpandedRoot, { + Header: SegmentationExpandedHeader, + DropdownMenu: SegmentationExpandedDropdownMenu, + Label: SegmentationExpandedLabel, + Info: SegmentationExpandedInfo, + Content: SegmentationExpandedContent, +}); + +export { SegmentationExpanded }; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx index 97663d05b..5d009de43 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx @@ -3,29 +3,23 @@ import { Button } from '../Button'; import { Icons } from '../Icons/Icons'; import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu'; import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip'; -import { useSegmentationTableContext } from './SegmentationTableContext'; -import { useTranslation } from 'react-i18next'; +import { useSegmentationExpanded, useSegmentationTableContext } from './contexts'; +// Then use it in your component: export const SegmentationHeader: React.FC<{ children?: React.ReactNode; - segmentation?: any; -}> = ({ children, segmentation }) => { - const { t } = useTranslation('SegmentationTable'); - const { ...contextProps } = useSegmentationTableContext('SegmentationHeader'); +}> = ({ children }) => { + // Always call both hooks unconditionally at the top level + const expandedContext = useSegmentationExpanded('SegmentationHeader'); + const tableContext = useSegmentationTableContext('SegmentationHeader'); + + // Determine which segmentation to use + const segmentation = expandedContext?.segmentation || tableContext.activeSegmentation; if (!segmentation) { return null; } - const childrenWithProps = React.Children.map(children, child => - React.isValidElement(child) - ? React.cloneElement(child, { - t, - ...contextProps, - }) - : child - ); - return (
@@ -40,7 +34,7 @@ export const SegmentationHeader: React.FC<{ - {childrenWithProps} + {children}
{segmentation.label}
@@ -55,7 +49,7 @@ export const SegmentationHeader: React.FC<{ -

{segmentation.cachedStats.info}

+

{segmentation.cachedStats?.info}

diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx index 9d2c5f676..1742ccd61 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { ScrollArea, DataRow } from '../../components'; -import { useSegmentationTableContext } from './SegmentationTableContext'; +import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard'; +import { useSegmentationTableContext, useSegmentationExpanded } from './contexts'; +import { SegmentStatistics } from './SegmentStatistics'; -export const SegmentationSegments: React.FC<{ - segmentation?: unknown; - representation?: unknown; -}> = ({ segmentation, representation }) => { +export const SegmentationSegments = ({ children = null }: { children?: React.ReactNode }) => { const { activeSegmentationId, disableEditing, @@ -13,37 +12,51 @@ export const SegmentationSegments: React.FC<{ onToggleSegmentVisibility, onToggleSegmentLock, onSegmentClick, - mode, onSegmentEdit, onSegmentDelete, data, - } = useSegmentationTableContext('SegmentationTable.Segments'); + } = useSegmentationTableContext('SegmentationSegments'); - let segmentationToUse = segmentation; - let representationToUse = representation; - let segmentationIdToUse = activeSegmentationId; - if (!segmentationToUse || !representationToUse) { - const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId); - segmentationToUse = entry?.segmentation; - representationToUse = entry?.representation; - segmentationIdToUse = entry?.segmentation.segmentationId; + // Try to get segmentation data from expanded context first, then fall back to table context + let segmentation; + let representation; + + try { + // Try to use the SegmentationExpanded context if available + const segmentationInfo = useSegmentationExpanded('SegmentationSegments'); + segmentation = segmentationInfo.segmentation; + representation = segmentationInfo.representation; + } catch (e) { + // Not within SegmentationExpanded context, get from active segmentation + const segmentationInfo = data.find( + entry => entry.segmentation.segmentationId === activeSegmentationId + ); + segmentation = segmentationInfo?.segmentation; + representation = segmentationInfo?.representation; } - if (!representationToUse || !segmentationToUse) { + if (!representation || !segmentation) { return null; } + const segments = Object.values(representation.segments); + const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId; + return ( - {Object.values(representationToUse.segments).map(segment => { + {segments.map(segment => { if (!segment) { return null; } - const { segmentIndex, color, visible } = segment; - const segmentFromSegmentation = segmentationToUse.segments[segmentIndex]; + const { segmentIndex, color, visible } = segment as { + segmentIndex: number; + color: number[]; + visible: boolean; + }; + const segmentFromSegmentation = segmentation.segments[segmentIndex]; if (!segmentFromSegmentation) { return null; @@ -53,27 +66,70 @@ export const SegmentationSegments: React.FC<{ const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; return ( - onSegmentColorClick(segmentationIdToUse, segmentIndex)} - onToggleVisibility={() => - onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type) - } - onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)} - onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)} - onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)} - onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)} - /> + + + {/* The div here is not random, it is needed for making it work with the hover card */} +
+ onSegmentColorClick(segmentation.segmentationId, segmentIndex)} + onToggleVisibility={() => + onToggleSegmentVisibility( + segmentation.segmentationId, + segmentIndex, + representation.type + ) + } + onToggleLocked={() => + onToggleSegmentLock(segmentation.segmentationId, segmentIndex) + } + onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)} + onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)} + onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)} + /> +
+
+ +
+
+

{label}

+
+ + + {children} + +
+
); })}
); }; + +SegmentationSegments.displayName = 'SegmentationTable.Segments'; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx deleted file mode 100644 index 1d8ca93e3..000000000 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { - Icons, - Button, - DropdownMenu, - DropdownMenuTrigger, - Tooltip, - TooltipTrigger, - TooltipContent, -} from '../../components'; - -import { useTranslation } from 'react-i18next'; -import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next'; -import { useSegmentationTableContext } from './SegmentationTableContext'; - -export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({ - children = null, -}) => { - const { t } = useTranslation('SegmentationTable.HeaderCollapsed'); - - const { data, activeSegmentationId, mode, onSegmentationClick, exportOptions, ...contextProps } = - useSegmentationTableContext('SegmentationTable.HeaderCollapsed'); - - if (mode !== 'collapsed' || !data?.length) { - return null; - } - - const activeSegmentationObj = data.find( - seg => seg.segmentation.segmentationId === activeSegmentationId - ); - - const activeSegmentation = { - id: activeSegmentationObj?.segmentation.segmentationId, - label: activeSegmentationObj?.segmentation.label, - info: activeSegmentationObj?.segmentation.cachedStats?.info, - }; - - const segmentations = data.map(seg => ({ - id: seg.segmentation.segmentationId, - label: seg.segmentation.label, - info: seg.segmentation.cachedStats?.info, - })); - - const allowExport = exportOptions?.find( - ({ segmentationId }) => segmentationId === activeSegmentation.id - )?.isExportable; - - const childrenWithProps = React.Children.map(children, child => - React.isValidElement(child) - ? React.cloneElement(child, { - activeSegmentation, - allowExport, - t, - ...contextProps, - }) - : child - ); - - return ( -
- - - - - {childrenWithProps} - - - - - - - - {activeSegmentation.info} - - -
- ); -}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx index 4859cfbe2..9a6e6b2ba 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx @@ -1,20 +1,26 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useState, Children, isValidElement } from 'react'; import { useTranslation } from 'react-i18next'; import { PanelSection } from '../PanelSection'; -import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext'; +import { + SegmentationTableProvider, + SegmentationTableContextType, + ViewportSegmentationInfo, +} from './contexts'; import { SegmentationSegments } from './SegmentationSegments'; +import { SegmentStatistics } from './SegmentStatistics'; import { SegmentationTableConfig } from './SegmentationTableConfig'; import { AddSegmentRow } from './AddSegmentRow'; import { AddSegmentationRow } from './AddSegmentationRow'; -import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; import { SegmentationHeader } from './SegmentationHeader'; import { SegmentationCollapsed } from './SegmentationCollapsed'; import { SegmentationExpanded } from './SegmentationExpanded'; +import Icons from '../Icons'; -interface SegmentationTableProps extends SegmentationTableContext { - disabled?: boolean; +// Only include props that aren't part of the context +interface SegmentationTableProps extends Omit { title?: string; children?: ReactNode; + setShowConfig?: (value: boolean) => void; } interface SegmentationTableComponent extends React.FC { @@ -22,55 +28,118 @@ interface SegmentationTableComponent extends React.FC { Config: typeof SegmentationTableConfig; AddSegmentRow: typeof AddSegmentRow; AddSegmentationRow: typeof AddSegmentationRow; - SelectorHeader: typeof SegmentationSelectorHeader; Header: typeof SegmentationHeader; Collapsed: typeof SegmentationCollapsed; Expanded: typeof SegmentationExpanded; + SegmentStatistics: typeof SegmentStatistics; } -export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => { +export const SegmentationTableRoot = (props: SegmentationTableProps) => { const { t } = useTranslation('SegmentationTable'); - const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props; + const { + data = [], + mode, + title, + disableEditing = false, + disabled = false, + children, + showConfig: externalShowConfig, + ...contextProps + } = props; + const [internalShowConfig, setInternalShowConfig] = useState(false); + const showConfig = externalShowConfig !== undefined ? externalShowConfig : internalShowConfig; + + // Find the active segmentation info based on which representation is active const activeSegmentationInfo = data.find(info => info.representation?.active); - const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId; - const activeRepresentation = activeSegmentationInfo?.representation; - const activeSegmentation = activeSegmentationInfo?.segmentation; - const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } = - activeRepresentation?.styles ?? {}; + // Get the active segmentation ID + const activeSegmentationId = + props.activeSegmentationId || activeSegmentationInfo?.segmentation?.segmentationId; + const activeRepresentation = props.activeRepresentation || activeSegmentationInfo?.representation; + const activeSegmentation = props.activeSegmentation || activeSegmentationInfo?.segmentation; + + // Extract style properties or use defaults + const { + fillAlpha = props.fillAlpha || 0.5, + fillAlphaInactive = props.fillAlphaInactive || 0.2, + outlineWidth = props.outlineWidth || 1, + renderFill = props.renderFill !== undefined ? props.renderFill : true, + renderOutline = props.renderOutline !== undefined ? props.renderOutline : true, + } = activeRepresentation?.styles ?? {}; + + // Check if SegmentationTableConfig is present in children + const hasConfigComponent = Children.toArray(children).some( + child => isValidElement(child) && child.type === SegmentationTableConfig + ); + + // Process children to conditionally render the config component based on showConfig + const processedChildren = Children.map(children, child => { + if (isValidElement(child) && child.type === SegmentationTableConfig) { + // Only render the Config component if showConfig is true + return showConfig ? child : null; + } + return child; + }); + + const toggleShowConfig = () => { + if (props.setShowConfig) { + props.setShowConfig(!showConfig); + } else { + setInternalShowConfig(!internalShowConfig); + } + }; return ( - + {t(title)} + {hasConfigComponent && ( +
+ { + e.stopPropagation(); + toggleShowConfig(); + }} + /> +
+ )}
- {children} + {processedChildren}
); }; -SegmentationTable.Segments = SegmentationSegments; -SegmentationTable.Config = SegmentationTableConfig; -SegmentationTable.AddSegmentRow = AddSegmentRow; -SegmentationTable.AddSegmentationRow = AddSegmentationRow; -SegmentationTable.SelectorHeader = SegmentationSelectorHeader; -SegmentationTable.Header = SegmentationHeader; -SegmentationTable.Collapsed = SegmentationCollapsed; -SegmentationTable.Expanded = SegmentationExpanded; +const SegmentationTable = Object.assign(SegmentationTableRoot, { + Segments: SegmentationSegments, + Config: SegmentationTableConfig, + AddSegmentRow: AddSegmentRow, + AddSegmentationRow: AddSegmentationRow, + Collapsed: SegmentationCollapsed, + Expanded: SegmentationExpanded, + SegmentStatistics: SegmentStatistics, + Header: SegmentationHeader, +}) as SegmentationTableComponent; + +export { SegmentationTable }; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx index 2f5fc220e..2c9bb5b77 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { PanelSection } from '../PanelSection'; import { Tabs, TabsList, TabsTrigger } from '../Tabs'; import { Slider } from '../Slider'; import { Icons } from '../Icons'; import { Switch } from '../Switch'; import { Label } from '../Label'; import { Input } from '../Input'; -import { useSegmentationTableContext } from './SegmentationTableContext'; +import { useSegmentationTableContext } from './contexts'; export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const { t } = useTranslation('SegmentationTable.AppearanceSettings'); @@ -26,152 +25,134 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = renderInactiveSegmentations, toggleRenderInactiveSegmentations, data, - } = useSegmentationTableContext('styles'); + } = useSegmentationTableContext('SegmentationTableConfig'); if (!data?.length) { return null; } return ( - - -
- - {t('Appearance Settings')} +
+
+ + {t('Show')}:{' '} + {renderFill && renderOutline + ? t('Fill & Outline') + : renderOutline + ? t('Outline Only') + : t('Fill Only')} + + { + if (value === 'fill-and-outline') { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, true); + } else if (value === 'outline') { + setRenderFill({ type: activeRepresentation.type }, false); + setRenderOutline({ type: activeRepresentation.type }, true); + } else { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, false); + } + }} + > + + + + + + + + + + + + +
+ +
+
+ + setFillAlpha({ type: activeRepresentation.type }, value)} + max={1} + min={0} + step={0.1} + /> + + setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value)) + } + />
- - -
-
- - {t('Show')}:{' '} - {renderFill && renderOutline - ? t('Fill & Outline') - : renderOutline - ? t('Outline Only') - : t('Fill Only')} - - { - if (value === 'fill-and-outline') { - setRenderFill({ type: activeRepresentation.type }, true); - setRenderOutline({ type: activeRepresentation.type }, true); - } else if (value === 'outline') { - setRenderFill({ type: activeRepresentation.type }, false); - setRenderOutline({ type: activeRepresentation.type }, true); - } else { - setRenderFill({ type: activeRepresentation.type }, true); - setRenderOutline({ type: activeRepresentation.type }, false); - } - }} - > - - - - - - - - - - - - -
-
-
- - - setFillAlpha({ type: activeRepresentation.type }, value) - } - max={1} - min={0} - step={0.1} - /> - - setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value)) - } - /> -
- -
- - - setOutlineWidth({ type: activeRepresentation.type }, value) - } - max={10} - min={0} - step={0.1} - className="mx-1 flex-1" - /> - - setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value)) - } - className="mx-1 w-10 flex-none text-center" - /> -
-
- -
- -
- - -
- {renderInactiveSegmentations && ( -
- - - setFillAlphaInactive({ type: activeRepresentation.type }, value) - } - max={1} - min={0} - step={0.1} - /> - - setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value)) - } - /> -
- )} +
+ + setOutlineWidth({ type: activeRepresentation.type }, value)} + max={10} + min={0} + step={0.1} + className="mx-1 flex-1" + /> + + setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value)) + } + className="mx-1 w-10 flex-none text-center" + />
- {children} - - +
+ +
+ +
+ + +
+ {renderInactiveSegmentations && ( +
+ + + setFillAlphaInactive({ type: activeRepresentation.type }, value) + } + max={1} + min={0} + step={0.1} + /> + + setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value)) + } + /> +
+ )} + {children} +
); }; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts b/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts deleted file mode 100644 index f299b9e05..000000000 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { createContext } from '../../lib/createContext'; - -interface Segmentation { - segmentationId: string; - label: string; - cachedStats: { - info: string; - }; -} - -interface Representation { - active: boolean; - visible: boolean; - type: string; - styles: { - fillAlpha: number; - fillAlphaInactive: number; - outlineWidth: number; - renderFill: boolean; - renderOutline: boolean; - }; -} - -interface ViewportSegmentationInfo { - segmentation: Segmentation; - representation: Representation; -} - -interface SegmentationTableContext { - data: ViewportSegmentationInfo[]; - disabled: boolean; - mode: 'collapsed' | 'expanded'; - fillAlpha: number; - exportOptions: { - segmentationId: string; - isExportable: boolean; - }[]; - fillAlphaInactive: number; - outlineWidth: number; - renderFill: boolean; - renderOutline: boolean; - activeSegmentationId: string; - activeRepresentation: Representation; - activeSegmentation: Segmentation; - setRenderFill: ({ type }, value: boolean) => void; - setRenderOutline: ({ type }, value: boolean) => void; - setOutlineWidth: ({ type }, value: number) => void; - setFillAlpha: ({ type }, value: number) => void; - setFillAlphaInactive: ({ type }, value: number) => void; - renderInactiveSegmentations: boolean; - toggleRenderInactiveSegmentations: () => void; - onSegmentationAdd: (segmentationId: string) => void; - onSegmentationClick: (segmentationId: string) => void; - onSegmentationDelete: (segmentationId: string) => void; - onSegmentAdd: (segmentationId: string) => void; - onSegmentClick: (segmentationId: string, segmentIndex: number) => void; - onSegmentEdit: (segmentationId: string, segmentIndex: number) => void; - onSegmentationEdit: (segmentationId: string) => void; - onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void; - onSegmentDelete: (segmentationId: string, segmentIndex: number) => void; - onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void; - onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void; - onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void; - onSegmentationDownload: (segmentationId: string) => void; - storeSegmentation: (segmentationId: string) => void; - onSegmentationDownloadRTSS: (segmentationId: string) => void; - setStyle: ( - segmentationId: string, - representationType: string, - styleKey: string, - value: unknown - ) => void; - onSegmentationRemoveFromViewport: (segmentationId: string) => void; - disableEditing: boolean; -} - -const [SegmentationTableProvider, useSegmentationTableContext] = - createContext('SegmentationTable'); - -export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext }; diff --git a/platform/ui-next/src/components/SegmentationTable/contexts/SegmentStatisticsContext.tsx b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentStatisticsContext.tsx new file mode 100644 index 000000000..72fa5cd85 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentStatisticsContext.tsx @@ -0,0 +1,45 @@ +import React, { createContext, useContext } from 'react'; + +interface SegmentStatisticsContextType { + segment: { + segmentIndex: number; + cachedStats: any; + }; + namedStats: Record | undefined; + segmentationId: string; +} + +// Create a named context +const SegmentStatisticsContext = createContext(undefined); +SegmentStatisticsContext.displayName = 'SegmentStatisticsContext'; + +export const useSegmentStatistics = (componentName?: string) => { + const context = useContext(SegmentStatisticsContext); + + if (context === undefined) { + throw new Error( + `useSegmentStatistics must be used within a SegmentStatisticsProvider` + + (componentName ? ` (called from ${componentName})` : '') + ); + } + + return context; +}; + +export const SegmentStatisticsProvider: React.FC<{ + segment: { + segmentIndex: number; + cachedStats: any; + }; + segmentationId: string; + children: React.ReactNode; +}> = ({ segment, segmentationId, children }) => { + const { cachedStats } = segment || {}; + const { namedStats } = cachedStats || {}; + + return ( + + {children} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationExpandedContext.tsx b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationExpandedContext.tsx new file mode 100644 index 000000000..b128008d4 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationExpandedContext.tsx @@ -0,0 +1,40 @@ +import React, { createContext, useContext } from 'react'; + +// Context for expanded segmentation panel +interface SegmentationExpandedContextType { + segmentation: any; + representation: any; + isActive: boolean; +} + +// Create a named context +const SegmentationExpandedContext = createContext( + undefined +); +SegmentationExpandedContext.displayName = 'SegmentationExpandedContext'; + +export const useSegmentationExpanded = (componentName?: string) => { + const context = useContext(SegmentationExpandedContext); + + if (context === undefined) { + throw new Error( + `useSegmentationExpanded must be used within a SegmentationExpandedProvider` + + (componentName ? ` (called from ${componentName})` : '') + ); + } + + return context; +}; + +export const SegmentationExpandedProvider: React.FC<{ + segmentation: any; + representation: any; + isActive: boolean; + children: React.ReactNode; +}> = ({ segmentation, representation, isActive, children }) => { + return ( + + {children} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationTableContext.tsx b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationTableContext.tsx new file mode 100644 index 000000000..98b3d4620 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/contexts/SegmentationTableContext.tsx @@ -0,0 +1,129 @@ +import React, { createContext, useContext } from 'react'; + +export interface SegmentStats { + info: string; + namedStats?: Record; +} + +export interface Segment { + color?: string; + label: string; + segmentIndex: number; + visible?: boolean; + locked?: boolean; +} + +export interface Segmentation { + segmentationId: string; + label: string; + cachedStats: SegmentStats; + segments: Record; +} + +export interface RepresentationStyles { + fillAlpha: number; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; +} + +export interface Representation { + active: boolean; + visible: boolean; + type: string; + segments: Record; + styles: RepresentationStyles; +} + +export interface ViewportSegmentationInfo { + segmentation: Segmentation; + representation: Representation; +} + +export interface ExportOption { + segmentationId: string; + isExportable: boolean; +} + +export interface SegmentationTableContextType { + data: ViewportSegmentationInfo[]; + disabled: boolean; + mode: 'collapsed' | 'expanded'; + fillAlpha: number; + exportOptions?: ExportOption[]; + showConfig: boolean; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; + activeSegmentationId?: string; + activeRepresentation?: Representation; + activeSegmentation?: Segmentation; + disableEditing: boolean; + showAddSegment?: boolean; + renderInactiveSegmentations?: boolean; + + // Function handlers + setShowConfig?: (show: boolean) => void; + setRenderFill?: ({ type }: { type: string }, value: boolean) => void; + setRenderOutline?: ({ type }: { type: string }, value: boolean) => void; + setOutlineWidth?: ({ type }: { type: string }, value: number) => void; + setFillAlpha?: ({ type }: { type: string }, value: number) => void; + setFillAlphaInactive?: ({ type }: { type: string }, value: number) => void; + toggleRenderInactiveSegmentations?: () => void; + onSegmentationAdd?: (segmentationId: string) => void; + onSegmentationClick?: (segmentationId: string) => void; + onSegmentationDelete?: (segmentationId: string) => void; + onSegmentAdd?: (segmentationId: string) => void; + onSegmentClick?: (segmentationId: string, segmentIndex: number) => void; + onSegmentEdit?: (segmentationId: string, segmentIndex: number) => void; + onSegmentationEdit?: (segmentationId: string) => void; + onSegmentColorClick?: (segmentationId: string, segmentIndex: number) => void; + onSegmentDelete?: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentVisibility?: ( + segmentationId: string, + segmentIndex: number, + representationType?: string + ) => void; + onToggleSegmentLock?: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentationRepresentationVisibility?: (segmentationId: string, type: string) => void; + onSegmentationDownload?: (segmentationId: string) => void; + storeSegmentation?: (segmentationId: string) => void; + onSegmentationDownloadRTSS?: (segmentationId: string) => void; + setStyle?: ( + segmentationId: string, + representationType: string, + styleKey: string, + value: unknown + ) => void; + onSegmentationRemoveFromViewport?: (segmentationId: string) => void; +} + +const SegmentationTableContext = createContext(undefined); +SegmentationTableContext.displayName = 'SegmentationTableContext'; + +export const SegmentationTableProvider = ({ + value, + children, +}: { + value: SegmentationTableContextType; + children: React.ReactNode; +}) => { + return ( + {children} + ); +}; + +export const useSegmentationTableContext = (componentName?: string) => { + const context = useContext(SegmentationTableContext); + + if (context === undefined) { + throw new Error( + `useSegmentationTableContext must be used within a SegmentationTableProvider` + + (componentName ? ` (called from ${componentName})` : '') + ); + } + + return context; +}; diff --git a/platform/ui-next/src/components/SegmentationTable/contexts/index.ts b/platform/ui-next/src/components/SegmentationTable/contexts/index.ts new file mode 100644 index 000000000..fc21a1828 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/contexts/index.ts @@ -0,0 +1,15 @@ +import { useSegmentationTableContext, SegmentationTableProvider } from './SegmentationTableContext'; +import { + useSegmentationExpanded, + SegmentationExpandedProvider, +} from './SegmentationExpandedContext'; +import { useSegmentStatistics, SegmentStatisticsProvider } from './SegmentStatisticsContext'; + +export { + useSegmentationTableContext, + useSegmentationExpanded, + useSegmentStatistics, + SegmentationTableProvider, + SegmentationExpandedProvider, + SegmentStatisticsProvider, +}; diff --git a/platform/ui-next/src/components/SegmentationTable/index.ts b/platform/ui-next/src/components/SegmentationTable/index.ts index b6704de80..da199f88b 100644 --- a/platform/ui-next/src/components/SegmentationTable/index.ts +++ b/platform/ui-next/src/components/SegmentationTable/index.ts @@ -3,20 +3,22 @@ import { SegmentationTableConfig } from './SegmentationTableConfig'; import { AddSegmentationRow } from './AddSegmentationRow'; import { AddSegmentRow } from './AddSegmentRow'; import { SegmentationSegments } from './SegmentationSegments'; -import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; -import { SegmentationHeader } from './SegmentationHeader'; -import { useSegmentationTableContext } from './SegmentationTableContext'; +import { + useSegmentationTableContext, + useSegmentStatistics, + useSegmentationExpanded, +} from './contexts'; SegmentationTable.Segments = SegmentationSegments; SegmentationTable.Config = SegmentationTableConfig; SegmentationTable.AddSegmentRow = AddSegmentRow; SegmentationTable.AddSegmentationRow = AddSegmentationRow; -SegmentationTable.SelectorHeader = SegmentationSelectorHeader; -SegmentationTable.Header = SegmentationHeader; export { // context useSegmentationTableContext, + useSegmentStatistics, + useSegmentationExpanded, // components SegmentationTable, }; diff --git a/platform/ui-next/src/components/Viewport/ViewportPane.tsx b/platform/ui-next/src/components/Viewport/ViewportPane.tsx index 84139547a..4a1e32769 100644 --- a/platform/ui-next/src/components/Viewport/ViewportPane.tsx +++ b/platform/ui-next/src/components/Viewport/ViewportPane.tsx @@ -58,7 +58,7 @@ function ViewportPane({ onScroll={onInteractionHandler} onWheel={onInteractionHandler} className={classNames( - 'group relative h-full w-full overflow-hidden transition duration-300', + 'group/pane relative h-full w-full overflow-hidden transition duration-300', className )} style={customStyle} diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index 84310f7e7..c438de444 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -81,7 +81,12 @@ import { Onboarding } from './Onboarding'; import { DoubleSlider } from './DoubleSlider'; import { DataRow } from './DataRow'; import { MeasurementTable } from './MeasurementTable'; -import { SegmentationTable, useSegmentationTableContext } from './SegmentationTable'; +import { + SegmentationTable, + useSegmentationTableContext, + useSegmentationExpanded, + useSegmentStatistics, +} from './SegmentationTable'; import { Toaster, toast } from './Sonner'; import { StudySummary } from './StudySummary'; import { ErrorBoundary } from './Errorboundary'; @@ -108,6 +113,9 @@ import { } from './ToolButton'; import { ToolSettings } from './OHIFToolSettings'; +// Segmentation Context Exports +export { useSegmentationTableContext, useSegmentationExpanded, useSegmentStatistics }; + export { Numeric, ErrorBoundary, @@ -205,7 +213,6 @@ export { Toaster, toast, SegmentationTable, - useSegmentationTableContext, StudySummary, Header, Card, diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index 37388deb0..a73446759 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -77,6 +77,8 @@ import { MeasurementTable, SegmentationTable, useSegmentationTableContext, + useSegmentationExpanded, + useSegmentStatistics, TooltipProvider, Tooltip, TooltipTrigger, @@ -210,6 +212,7 @@ export { MeasurementTable, SegmentationTable, useSegmentationTableContext, + useSegmentationExpanded, TooltipProvider, Tooltip, TooltipTrigger, @@ -246,5 +249,6 @@ export { useDialog, ManagedDialog, ToolSettings, + useSegmentStatistics, InputFilter, -}; \ No newline at end of file +}; diff --git a/platform/ui-next/src/utils/index.ts b/platform/ui-next/src/utils/index.ts index b842eb1f3..8876fc121 100644 --- a/platform/ui-next/src/utils/index.ts +++ b/platform/ui-next/src/utils/index.ts @@ -1,3 +1,4 @@ import { getToggledClassName } from './getToggledClassName'; +import roundNumber from './roundNumber'; -export { getToggledClassName }; +export { getToggledClassName, roundNumber }; diff --git a/platform/ui-next/src/utils/roundNumber.ts b/platform/ui-next/src/utils/roundNumber.ts new file mode 100644 index 000000000..3747e5159 --- /dev/null +++ b/platform/ui-next/src/utils/roundNumber.ts @@ -0,0 +1,43 @@ +/** + * Truncates decimal points to that there is at least 1+precision significant + * digits. + * + * For example, with the default precision 2 (3 significant digits) + * * Values larger than 100 show no information after the decimal point + * * Values between 10 and 99 show 1 decimal point + * * Values between 1 and 9 show 2 decimal points + * + * @param value - to return a fixed measurement value from + * @param precision - defining how many digits after 1..9 are desired + */ + +function roundNumber(value, precision = 2) { + if (Array.isArray(value)) { + return value.map(v => roundNumber(v, precision)).join(', '); + } + if (value === undefined || value === null || value === '') { + return 'NaN'; + } + value = Number(value); + const absValue = Math.abs(value); + if (absValue < 0.0001) { + return `${value}`; + } + const fixedPrecision = + absValue >= 100 + ? precision - 2 + : absValue >= 10 + ? precision - 1 + : absValue >= 1 + ? precision + : absValue >= 0.1 + ? precision + 1 + : absValue >= 0.01 + ? precision + 2 + : absValue >= 0.001 + ? precision + 3 + : precision + 4; + return value.toFixed(fixedPrecision); +} + +export default roundNumber; diff --git a/yarn.lock b/yarn.lock index eeac77803..292589f79 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.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.5.0.tgz#8c719bc39f4dc78466cfb1fefbb3cbf3c82666d3" - integrity sha512-xkixGbCDS2wwAHmggx1mW0ewEjWGntbE5L1svdkmRpGBvTtEkQJbNkNG6dUiHXWZ9mICxwahyiMc7YVDdH3tfA== +"@cornerstonejs/adapters@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.7.0.tgz#7e16d609e1e5035585e40d7a7d919534b03ae094" + integrity sha512-7ROFACNjT4W8dukUoKwggHhl+tjg0rdDpYDq8rM6b0c0gO2MgYKbU55t9MoT682yIuebt81jCsAbFSRGVzf45Q== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1429,41 +1429,50 @@ 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.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.5.0.tgz#8e57064c347f32c8ecd28c5e2f46e95aecb5c00c" - integrity sha512-Ai4dyxZameDz+cp3nBST6KfK1WK/Ic5R6DoTFrBlph4DuK7IUradAx2N9uEydhUJTuF6606mF3VeaAdPG9Hfig== +"@cornerstonejs/core@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.7.0.tgz#d31cafc237ccc7ae621d1a41e3979a721b2fdcff" + integrity sha512-eRCovUYO3JvFV3Dzuwc6EWn9bpm+fvp/x6SXvr3MwiO+LRw1o5bmZfeh0+m4+5xzvQJPbjNYiXPPEpLYGQIOqQ== dependencies: - "@kitware/vtk.js" "32.12.0" + "@kitware/vtk.js" "32.12.1" comlink "^4.4.1" gl-matrix "^3.4.3" loglevel "^1.9.2" -"@cornerstonejs/dicom-image-loader@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.5.0.tgz#78a01ad3587d547da7ca708713b42ced75451851" - integrity sha512-Jel/an3IHe68sebtRLYB3/aJ1Dggw8D11cWBiBUqzXfxGTlg1SUd3KnvU5OrgNJ+ZpNUEdwCrR3OoiU65hL6UA== +"@cornerstonejs/dicom-image-loader@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.7.0.tgz#6446bc6f9011674b5d000b275b6b0c34efd95c2c" + integrity sha512-MSv5A7ZcAp7uVm1ofrTaQppPm1DyOAXN0BdX8a2/fMZ2Ud4OmEtk0aoUlaJ52/hQtUvfdCqfYObBos2oBIMQ1w== 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" -"@cornerstonejs/polymorphic-segmentation@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.5.0.tgz#b97218b3140ad895b567b3e2dc8678ebf03be531" - integrity sha512-S2zqXNKlFURPCq99fPj5CAnBO7chuVk7bIRWmBOBYQL21TnL4rzyvY+Td5pvHBal4sZ8uTs6PW72rrwGCospAQ== +"@cornerstonejs/labelmap-interpolation@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.7.0.tgz#00d7bbd056fd292b2ea53681259efd0b8f6e5f5f" + integrity sha512-/1l1XsKFYeov+hrMKindJ4n6l4H2zcXFwcO+wtqE4kmfoFhXRN6RSlIXbCRzEZtFCmh0hafg7SUUcXxr0QoClQ== + dependencies: + "@itk-wasm/morphological-contour-interpolation" "1.1.0" + itk-wasm "1.0.0-b.165" + +"@cornerstonejs/polymorphic-segmentation@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.7.0.tgz#6bc0c71451fc8bacfcc4b2ac311ba5efda43c51e" + integrity sha512-MYb8aXE2EwfsyPmhvhTmNdTkCq5pIlMJuVRmT6GuQ4nf2dIhx5AeZ+Ya/TRaNNumV2Fx9/9PetI8+Z8XJLAILw== dependencies: "@icr/polyseg-wasm" "0.4.0" -"@cornerstonejs/tools@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.5.0.tgz#cfb046cfe6095c2295e03cb66da74e675f9d60c8" - integrity sha512-wyT7runGpPvY9dMJOUBAtxDBSqabk6Ks/SNjbTGXAIUemAYuonP+33pN4zK/4n/zFfa0gpKFP7LFc461xFK16g== +"@cornerstonejs/tools@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.7.0.tgz#0df08e1e9e47ab5da103592488a3a29d567537c8" + integrity sha512-72lbmaAm/dnOANAUNc0OK+9aL2pFklaWBLR+kOKn8OeSh1i9RRTSEaP8zWzStaSNAcdhwQ+JOQzcMR40Eo8oNQ== dependencies: "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1" @@ -2299,6 +2308,29 @@ worker-loader "3.0.8" xmlbuilder2 "3.0.2" +"@kitware/vtk.js@32.12.1": + version "32.12.1" + resolved "https://registry.yarnpkg.com/@kitware/vtk.js/-/vtk.js-32.12.1.tgz#426fe65112c36c4bf312044dee0aeb641616d09b" + integrity sha512-IF8WK7WDu84035ws3HZXz8wys8O81mmeFayYFbdBEfKjgZ4k0EGCK25YCGc6Q47Zxq1iutjs3Zs2xAgGjTgGQg== + dependencies: + "@babel/runtime" "7.22.11" + "@types/webxr" "^0.5.5" + commander "9.2.0" + d3-scale "4.0.2" + fast-deep-equal "^3.1.3" + fflate "0.7.3" + gl-matrix "3.4.3" + globalthis "1.0.3" + seedrandom "3.0.5" + shader-loader "1.3.1" + shelljs "0.8.5" + spark-md5 "3.0.2" + stream-browserify "3.0.0" + utif "3.1.0" + webworker-promise "0.5.0" + worker-loader "3.0.8" + xmlbuilder2 "3.0.2" + "@lerna/child-process@7.4.2": version "7.4.2" resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.2.tgz#a2fd013ac2150dc288270d3e0d0b850c06bec511" @@ -3020,6 +3052,13 @@ dependencies: "@radix-ui/react-primitive" "2.0.1" +"@radix-ui/react-arrow@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab" + integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg== + dependencies: + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-checkbox@^1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" @@ -3224,6 +3263,17 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0" +"@radix-ui/react-dismissable-layer@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774" + integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/react-dropdown-menu@^2.1.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" @@ -3287,6 +3337,21 @@ "@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-hover-card@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5" + integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.5" + "@radix-ui/react-popper" "1.2.2" + "@radix-ui/react-portal" "1.1.4" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-icons@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" @@ -3432,6 +3497,22 @@ "@radix-ui/react-use-size" "1.1.0" "@radix-ui/rect" "1.1.0" +"@radix-ui/react-popper@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029" + integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + "@radix-ui/react-portal@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" @@ -3464,6 +3545,14 @@ "@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-portal@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8" + integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA== + dependencies: + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -3511,6 +3600,13 @@ dependencies: "@radix-ui/react-slot" "1.1.1" +"@radix-ui/react-primitive@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef" + integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w== + dependencies: + "@radix-ui/react-slot" "1.1.2" + "@radix-ui/react-roving-focus@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" @@ -3657,6 +3753,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.1" +"@radix-ui/react-slot@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6" + integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-switch@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a" @@ -12875,6 +12978,23 @@ iterator.prototype@^1.1.3: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +itk-wasm@1.0.0-b.165: + version "1.0.0-b.165" + resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.165.tgz#a5ba3c6aec94fe371ac297ecd3b22a4f85cc6566" + integrity sha512-Rq0+AL2BuRqcFh9r3h69pB3nt84tw/gTxnI6GjIOYw5Bfah9qO+C8hRJpD8aWNBa5o3wNHRAMVlp0pt/L4K8gg== + dependencies: + "@itk-wasm/dam" "^1.1.1" + "@thewtex/zstddec" "^0.2.0" + "@types/emscripten" "^1.39.10" + axios "^1.6.2" + comlink "^4.4.1" + commander "^11.1.0" + fs-extra "^11.2.0" + glob "^8.1.0" + markdown-table "^3.0.3" + mime-types "^2.1.35" + wasm-feature-detect "^1.6.1" + itk-wasm@1.0.0-b.173: version "1.0.0-b.173" resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.173.tgz#e484e1765f4205a5704f8fac205d2ecf74e726a2"