diff --git a/.circleci/config.yml b/.circleci/config.yml index 6bf4b7692b..3130f457e4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,18 +13,18 @@ version: 2.1 ## orbs: codecov: codecov/codecov@1.0.5 - cypress: cypress-io/cypress@1.26.0 -executors: - # Custom executor to override Cypress config - deploy-to-prod-executor: - docker: - - image: cimg/node:16.14 - environment: - CYPRESS_BASE_URL: https://ohif-staging.netlify.com/ - chrome-and-pacs: - docker: - # Primary container image where all steps run. - - image: 'cypress/browsers:node16.14.2-slim-chrome103-ff102' + cypress: cypress-io/cypress@3 +# executors: +# # Custom executor to override Cypress config +# deploy-to-prod-executor: +# docker: +# - image: cimg/node:16.14 +# environment: +# CYPRESS_BASE_URL: https://ohif-staging.netlify.com/ +# chrome-and-pacs: +# docker: +# # Primary container image where all steps run. +# - image: 'cypress/browsers:node18.12.0-chrome106-ff106' defaults: &defaults docker: @@ -363,32 +363,20 @@ jobs: fi workflows: - version: 2 - PR_CHECKS: jobs: - UNIT_TESTS - # E2E: PWA - cypress/run: name: 'E2E: PWA' - executor: chrome-and-pacs - browser: chrome - pre-steps: - - run: | - # Clear yarn cache; use yarn from image (update image to update yarn) - rm -rf ~/.yarn - yarn -v - yarn: true - record: true - store_artifacts: true - working_directory: platform/app - build: yarn test:data - start: yarn run test:e2e:serve - spec: 'cypress/integration/**/*' - wait-on: 'http://localhost:3000' - cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' - no-workspace: true # Don't persist workspace + start-command: yarn run test:data && yarn run test:e2e:serve + install-browsers: true + cypress-command: + 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run + --record --browser chrome --parallel' + package-manager: 'yarn' + cypress-cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' + cypress-cache-path: '~/.cache/Cypress' post-steps: - store_artifacts: path: platform/app/cypress/screenshots @@ -399,34 +387,34 @@ workflows: requires: - UNIT_TESTS - PR_OPTIONAL_VISUAL_TESTS: - jobs: - - AWAIT_APPROVAL: - type: approval - # Update hub.docker.org - - cypress/run: - name: 'Generate Percy Snapshots' - executor: cypress/browsers-chrome76 - browser: chrome - pre-steps: - - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on' - yarn: true - store_artifacts: false - working_directory: platform/app - build: - yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js - yarn run build - # start server --> verify running --> percy + chrome + cypress - command: yarn run test:e2e:dist - cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' - no-workspace: true # Don't persist workspace - post-steps: - - store_artifacts: - path: platform/app/cypress/screenshots - - store_artifacts: - path: platform/app/cypress/videos - requires: - - AWAIT_APPROVAL + # PR_OPTIONAL_VISUAL_TESTS: + # jobs: + # - AWAIT_APPROVAL: + # type: approval + # # Update hub.docker.org + # - cypress/run: + # name: 'Generate Percy Snapshots' + # executor: cypress/browsers-chrome76 + # browser: chrome + # pre-steps: + # - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on' + # yarn: true + # store_artifacts: false + # working_directory: platform/app + # build: + # yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js + # yarn run build + # # start server --> verify running --> percy + chrome + cypress + # command: yarn run test:e2e:dist + # cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' + # no-workspace: true # Don't persist workspace + # post-steps: + # - store_artifacts: + # path: platform/app/cypress/screenshots + # - store_artifacts: + # path: platform/app/cypress/videos + # requires: + # - AWAIT_APPROVAL # Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is # deployed from the release branch which is more stable and less frequently updated. diff --git a/CHANGELOG.md b/CHANGELOG.md index f46788ef3b..11428cd4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Bug Fixes + +* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a)) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) diff --git a/commit.txt b/commit.txt index da38aca77b..c102f17f92 100644 --- a/commit.txt +++ b/commit.txt @@ -1 +1 @@ -221dedde5dd4df086276406a9fa2da1cc23b4eb1 \ No newline at end of file +04ca10d8779dd15454920002f3d48afa8830de8a \ No newline at end of file diff --git a/extensions/cornerstone-dicom-rt/CHANGELOG.md b/extensions/cornerstone-dicom-rt/CHANGELOG.md index dcae7d6a22..1e79655af7 100644 --- a/extensions/cornerstone-dicom-rt/CHANGELOG.md +++ b/extensions/cornerstone-dicom-rt/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt diff --git a/extensions/cornerstone-dicom-rt/package.json b/extensions/cornerstone-dicom-rt/package.json index da59e6fb17..11cec414b2 100644 --- a/extensions/cornerstone-dicom-rt/package.json +++ b/extensions/cornerstone-dicom-rt/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "DICOM RT read workflow", "author": "OHIF", "license": "MIT", @@ -31,10 +31,10 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/i18n": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/i18n": "3.7.0-beta.80", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/cornerstone-dicom-rt/src/index.tsx b/extensions/cornerstone-dicom-rt/src/index.tsx index a8541a303b..953c9a7c7d 100644 --- a/extensions/cornerstone-dicom-rt/src/index.tsx +++ b/extensions/cornerstone-dicom-rt/src/index.tsx @@ -2,7 +2,6 @@ import { id } from './id'; import React from 'react'; import { Types } from '@ohif/core'; import getSopClassHandlerModule from './getSopClassHandlerModule'; -import hydrateRTDisplaySet from './utils/_hydrateRT'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'); @@ -60,4 +59,3 @@ const extension: Types.Extensions.Extension = { }; export default extension; -export { hydrateRTDisplaySet }; diff --git a/extensions/cornerstone-dicom-rt/src/utils/_hydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/_hydrateRT.ts deleted file mode 100644 index d61e9e1865..0000000000 --- a/extensions/cornerstone-dicom-rt/src/utils/_hydrateRT.ts +++ /dev/null @@ -1,70 +0,0 @@ -async function _hydrateRTDisplaySet({ rtDisplaySet, viewportId, servicesManager }) { - const { segmentationService, hangingProtocolService, viewportGridService } = - servicesManager.services; - - const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID; - - let segmentationId = null; - - // We need the hydration to notify panels about the new segmentation added - const suppressEvents = false; - - segmentationId = await segmentationService.createSegmentationForRTDisplaySet( - rtDisplaySet, - segmentationId, - suppressEvents - ); - - segmentationService.hydrateSegmentation(rtDisplaySet.displaySetInstanceUID); - - const { viewports } = viewportGridService.getState(); - - const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( - viewportId, - displaySetInstanceUID - ); - - viewportGridService.setDisplaySetsForViewports(updatedViewports); - - // Todo: fix this after we have a better way for stack viewport segmentations - - // check every viewport in the viewports to see if the displaySetInstanceUID - // is being displayed, if so we need to update the viewport to use volume viewport - // (if already is not using it) since Cornerstone3D currently only supports - // volume viewport for segmentation - viewports.forEach(viewport => { - if (viewport.viewportId === viewportId) { - return; - } - - const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( - viewport.displaySetInstanceUIDs, - rtDisplaySet.displaySetInstanceUID - ); - - if (shouldDisplaySeg) { - updatedViewports.push({ - viewportId: viewport.viewportId, - displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, - viewportOptions: { - // Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component - // Used for segmentation hydration right now, since the logic to decide whether - // a viewport needs to render a segmentation lives inside the CornerstoneViewportService - // so we need to re-render (force update via change of the needsRerendering) so that React - // does the diffing and decides we should render this again (although the id and element has not changed) - // so that the CornerstoneViewportService can decide whether to render the segmentation or not. - needsRerendering: true, - initialImageOptions: { - preset: 'middle', - }, - }, - }); - } - }); - - // Do the entire update at once - viewportGridService.setDisplaySetsForViewports(updatedViewports); - return true; -} - -export default _hydrateRTDisplaySet; diff --git a/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts b/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts index aaa27c28f8..f47f0089d8 100644 --- a/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts +++ b/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts @@ -1,7 +1,7 @@ function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) { const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {}; - return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); + return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } export default createRTToolGroupAndAddTools; diff --git a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts index c6069abc15..91492cbfbe 100644 --- a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts +++ b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts @@ -1,5 +1,4 @@ import { ButtonEnums } from '@ohif/ui'; -import hydrateRTDisplaySet from './_hydrateRT'; const RESPONSE = { NO_NEVER: -1, @@ -13,6 +12,7 @@ function promptHydrateRT({ viewportId, toolGroupId = 'default', preHydrateCallbacks, + hydrateRTDisplaySet, }) { const { uiViewportDialogService } = servicesManager.services; diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 3dd9588d7d..e9c7450c69 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -3,11 +3,9 @@ import PropTypes from 'prop-types'; import OHIF, { utils } from '@ohif/core'; import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui'; -import _hydrateRTdisplaySet from '../utils/_hydrateRT'; import promptHydrateRT from '../utils/promptHydrateRT'; import _getStatusComponent from './_getStatusComponent'; import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; -import _hydrateRTDisplaySet from '../utils/_hydrateRT'; const { formatDate } = utils; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; @@ -95,6 +93,13 @@ function OHIFCornerstoneRTViewport(props) { }); }, [viewportGrid]); + const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => { + commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { + displaySets: [rtDisplaySet], + viewportId, + }); + }; + const getCornerstoneViewport = useCallback(() => { const { component: Component } = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.viewportModule.cornerstone' @@ -154,6 +159,7 @@ function OHIFCornerstoneRTViewport(props) { viewportId, rtDisplaySet, preHydrateCallbacks: [storePresentationState], + hydrateRTDisplaySet, }).then(isHydrated => { if (isHydrated) { setIsHydrated(true); @@ -295,10 +301,9 @@ function OHIFCornerstoneRTViewport(props) { // presentation state (w/l and invert) and then opens the RT. If we don't store // the presentation state, the viewport will be reset to the default presentation storePresentationState(); - const isHydrated = await _hydrateRTDisplaySet({ + const isHydrated = await hydrateRTDisplaySet({ rtDisplaySet, viewportId, - servicesManager, }); setIsHydrated(isHydrated); diff --git a/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js index 6b6a4f71da..3f6eb4b69e 100644 --- a/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js +++ b/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js @@ -45,10 +45,10 @@ module.exports = (env, argv) => { new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), - // new MiniCssExtractPlugin({ - // filename: `./dist/${outputName}.css`, - // chunkFilename: `./dist/${outputName}.css`, - // }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), ], }); }; diff --git a/extensions/cornerstone-dicom-seg/CHANGELOG.md b/extensions/cornerstone-dicom-seg/CHANGELOG.md index 025a3c7a0a..810b04c53d 100644 --- a/extensions/cornerstone-dicom-seg/CHANGELOG.md +++ b/extensions/cornerstone-dicom-seg/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index e8d0419de8..a1e858a517 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "DICOM SEG read workflow", "author": "OHIF", "license": "MIT", @@ -31,10 +31,10 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/i18n": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/i18n": "3.7.0-beta.80", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -44,6 +44,7 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", + "@cornerstonejs/tools": "^1.16.4", "react-color": "^2.19.3" } } diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts new file mode 100644 index 0000000000..7cffb83f76 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -0,0 +1,383 @@ +import dcmjs from 'dcmjs'; +import { createReportDialogPrompt } from '@ohif/extension-default'; +import { ServicesManager, Types } from '@ohif/core'; +import { cache, metaData } from '@cornerstonejs/core'; +import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools'; +import { adaptersSEG, helpers } from '@cornerstonejs/adapters'; +import { DicomMetadataStore } from '@ohif/core'; + +import { + updateViewportsForSegmentationRendering, + getUpdatedViewportsForSegmentation, + getTargetViewport, +} from './utils/hydrationUtils'; + +const { + Cornerstone3D: { + Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation }, + }, +} = adaptersSEG; + +const { downloadDICOMData } = helpers; + +const commandsModule = ({ + servicesManager, + extensionManager, +}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { + const { + uiNotificationService, + segmentationService, + uiDialogService, + displaySetService, + viewportGridService, + } = (servicesManager as ServicesManager).services; + + const actions = { + /** + * Retrieves a list of viewports that require updates in preparation for segmentation rendering. + * This function evaluates viewports based on their compatibility with the provided segmentation's + * frame of reference UID and appends them to the updated list if they should render the segmentation. + * + * @param {Object} params - Parameters for the function. + * @param params.viewportId - the ID of the viewport to be updated. + * @param params.servicesManager - The services manager + * @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance. + * + * @returns {Array} Returns an array of viewports that require updates for segmentation rendering. + */ + getUpdatedViewportsForSegmentation, + /** + * Creates an empty segmentation for a specified viewport. + * It first checks if the display set associated with the viewport is reconstructable. + * If not, it raises a notification error. Otherwise, it creates a new segmentation + * for the display set after handling the necessary steps for making the viewport + * a volume viewport first + * + * @param {Object} params - Parameters for the function. + * @param params.viewportId - the target viewport ID. + * + */ + createEmptySegmentationForViewport: async ({ viewportId }) => { + const viewport = getTargetViewport({ viewportId, viewportGridService }); + // Todo: add support for multiple display sets + const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; + + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + if (!displaySet.isReconstructable) { + uiNotificationService.show({ + title: 'Segmentation', + message: 'Segmentation is not supported for non-reconstructible displaysets yet', + type: 'error', + }); + return; + } + + updateViewportsForSegmentationRendering({ + viewportId, + servicesManager, + loadFn: async () => { + const currentSegmentations = segmentationService.getSegmentations(); + const segmentationId = await segmentationService.createSegmentationForDisplaySet( + displaySetInstanceUID, + { label: `Segmentation ${currentSegmentations.length + 1}` } + ); + + const toolGroupId = viewport.viewportOptions.toolGroupId; + + await segmentationService.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId + ); + + // Add only one segment for now + segmentationService.addSegment(segmentationId, { + toolGroupId, + segmentIndex: 1, + properties: { + label: 'Segment 1', + }, + }); + + return segmentationId; + }, + }); + }, + /** + * Loads segmentations for a specified viewport. + * The function prepares the viewport for rendering, then loads the segmentation details. + * Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentations - Array of segmentations to be loaded. + * @param params.viewportId - the target viewport ID. + * + */ + loadSegmentationsForViewport: async ({ segmentations, viewportId }) => { + updateViewportsForSegmentationRendering({ + viewportId, + servicesManager, + loadFn: async () => { + // Todo: handle adding more than one segmentation + const viewport = getTargetViewport({ viewportId, viewportGridService }); + const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; + + const segmentation = segmentations[0]; + const segmentationId = segmentation.id; + const label = segmentation.label; + const segments = segmentation.segments; + + delete segmentation.segments; + + await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, { + segmentationId, + label, + }); + + if (segmentation.scalarData) { + const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId); + labelmapVolume.scalarData.set(segmentation.scalarData); + } + + segmentationService.addOrUpdateSegmentation(segmentation); + + const toolGroupId = viewport.viewportOptions.toolGroupId; + await segmentationService.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId + ); + + segments.forEach(segment => { + if (segment === null) { + return; + } + segmentationService.addSegment(segmentationId, { + segmentIndex: segment.segmentIndex, + toolGroupId, + properties: { + color: segment.color, + label: segment.label, + opacity: segment.opacity, + isLocked: segment.isLocked, + visibility: segment.isVisible, + active: segmentation.activeSegmentIndex === segment.segmentIndex, + }, + }); + }); + + if (segmentation.centroidsIJK) { + segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK); + } + + return segmentationId; + }, + }); + }, + /** + * Loads segmentation display sets for a specified viewport. + * Depending on the modality of the display set (SEG or RTSTRUCT), + * it chooses the appropriate service function to create + * the segmentation for the display set. + * The function then prepares the viewport for rendering segmentation. + * + * @param {Object} params - Parameters for the function. + * @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded. + * @param params.displaySets - Array of display sets to be loaded for segmentation. + * + */ + loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => { + // Todo: handle adding more than one segmentation + const displaySet = displaySets[0]; + + updateViewportsForSegmentationRendering({ + viewportId, + servicesManager, + referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID, + loadFn: async () => { + const segDisplaySet = displaySet; + const suppressEvents = false; + const serviceFunction = + segDisplaySet.Modality === 'SEG' + ? 'createSegmentationForSEGDisplaySet' + : 'createSegmentationForRTDisplaySet'; + + const boundFn = segmentationService[serviceFunction].bind(segmentationService); + const segmentationId = await boundFn(segDisplaySet, null, suppressEvents); + + return segmentationId; + }, + }); + }, + /** + * Generates a segmentation from a given segmentation ID. + * This function retrieves the associated segmentation and + * its referenced volume, extracts label maps from the + * segmentation volume, and produces segmentation data + * alongside associated metadata. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be generated. + * @param params.options - Optional configuration for the generation process. + * + * @returns Returns the generated segmentation data. + */ + generateSegmentation: ({ segmentationId, options = {} }) => { + const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); + + const { referencedVolumeId } = segmentation.representationData.LABELMAP; + + const segmentationVolume = cache.getVolume(segmentationId); + const referencedVolume = cache.getVolume(referencedVolumeId); + const referencedImages = referencedVolume.getCornerstoneImages(); + + const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + + // Generate fake metadata as an example + labelmapObj.metadata = []; + + const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); + labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => { + // segmentation service already has a color for each segment + const segment = segmentationInOHIF?.segments[segmentIndex]; + const { label, color } = segment; + + const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( + color.slice(0, 3).map(value => value / 255) + ).map(value => Math.round(value)); + + const segmentMetadata = { + SegmentNumber: segmentIndex.toString(), + SegmentLabel: label, + SegmentAlgorithmType: 'MANUAL', + SegmentAlgorithmName: 'OHIF Brush', + RecommendedDisplayCIELabValue, + SegmentedPropertyCategoryCodeSequence: { + CodeValue: 'T-D0050', + CodingSchemeDesignator: 'SRT', + CodeMeaning: 'Tissue', + }, + SegmentedPropertyTypeCodeSequence: { + CodeValue: 'T-D0050', + CodingSchemeDesignator: 'SRT', + CodeMeaning: 'Tissue', + }, + }; + labelmapObj.metadata[segmentIndex] = segmentMetadata; + }); + + const generatedSegmentation = generateSegmentation( + referencedImages, + labelmapObj, + metaData, + options + ); + + return generatedSegmentation; + }, + /** + * Downloads a segmentation based on the provided segmentation ID. + * This function retrieves the associated segmentation and + * uses it to generate the corresponding DICOM dataset, which + * is then downloaded with an appropriate filename. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be downloaded. + * + */ + downloadSegmentation: ({ segmentationId }) => { + const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); + const generatedSegmentation = actions.generateSegmentation({ + segmentationId, + }); + + downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`); + }, + /** + * Stores a segmentation based on the provided segmentationId into a specified data source. + * The SeriesDescription is derived from user input or defaults to the segmentation label, + * and in its absence, defaults to 'Research Derived Series'. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be stored. + * @param params.dataSource - Data source where the generated segmentation will be stored. + * + * @returns {Object|void} Returns the naturalized report if successfully stored, + * otherwise throws an error. + */ + storeSegmentation: async ({ segmentationId, dataSource }) => { + const promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); + + if (promptResult.action !== 1 && promptResult.value) { + return; + } + + const segmentation = segmentationService.getSegmentation(segmentationId); + + if (!segmentation) { + throw new Error('No segmentation found'); + } + + const { label } = segmentation; + const SeriesDescription = promptResult.value || label || 'Research Derived Series'; + + const generatedData = actions.generateSegmentation({ + segmentationId, + options: { + SeriesDescription, + }, + }); + + if (!generatedData || !generatedData.dataset) { + throw new Error('Error during segmentation generation'); + } + + const { dataset: naturalizedReport } = generatedData; + + await dataSource.store.dicom(naturalizedReport); + + // The "Mode" route listens for DicomMetadataStore changes + // When a new instance is added, it listens and + // automatically calls makeDisplaySets + + // add the information for where we stored it to the instance as well + naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot; + + DicomMetadataStore.addInstances([naturalizedReport], true); + + return naturalizedReport; + }, + }; + + const definitions = { + getUpdatedViewportsForSegmentation: { + commandFn: actions.getUpdatedViewportsForSegmentation, + }, + loadSegmentationDisplaySetsForViewport: { + commandFn: actions.loadSegmentationDisplaySetsForViewport, + }, + loadSegmentationsForViewport: { + commandFn: actions.loadSegmentationsForViewport, + }, + createEmptySegmentationForViewport: { + commandFn: actions.createEmptySegmentationForViewport, + }, + generateSegmentation: { + commandFn: actions.generateSegmentation, + }, + downloadSegmentation: { + commandFn: actions.downloadSegmentation, + }, + storeSegmentation: { + commandFn: actions.storeSegmentation, + }, + }; + + return { + actions, + definitions, + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx b/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx new file mode 100644 index 0000000000..e626c87d7b --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import { useAppConfig } from '@state'; +import PanelSegmentation from './panels/PanelSegmentation'; +import SegmentationToolbox from './panels/SegmentationToolbox'; + +const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => { + const { customizationService } = servicesManager.services; + + const wrappedPanelSegmentation = configuration => { + const [appConfig] = useAppConfig(); + + const disableEditingForMode = customizationService.get('segmentation.disableEditing'); + + return ( + + ); + }; + + const wrappedPanelSegmentationWithTools = configuration => { + const [appConfig] = useAppConfig(); + return ( + <> + + + + ); + }; + + return [ + { + name: 'panelSegmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentation, + }, + { + name: 'panelSegmentationWithTools', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentationWithTools, + }, + ]; +}; + +export default getPanelModule; diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index 33a4b523a4..6f7f06e4bd 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -60,7 +60,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) throw new Error('ReferencedSeriesSequence is missing for the SEG'); } - const referencedSeries = referencedSeriesSequence[0]; + const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence; displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; diff --git a/extensions/cornerstone-dicom-seg/src/index.tsx b/extensions/cornerstone-dicom-seg/src/index.tsx index 48d75202d8..bb6a6d4b11 100644 --- a/extensions/cornerstone-dicom-seg/src/index.tsx +++ b/extensions/cornerstone-dicom-seg/src/index.tsx @@ -1,12 +1,11 @@ import { id } from './id'; import React from 'react'; -import { Types } from '@ohif/core'; - import getSopClassHandlerModule from './getSopClassHandlerModule'; -import PanelSegmentation from './panels/PanelSegmentation'; import getHangingProtocolModule from './getHangingProtocolModule'; -import hydrateSEGDisplaySet from './utils/_hydrateSEG'; +import getPanelModule from './getPanelModule'; +import getCommandsModule from './commandsModule'; +import preRegistration from './init'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'); @@ -29,6 +28,7 @@ const extension = { * You ID can be anything you want, but it should be unique. */ id, + preRegistration, /** * PanelModule should provide a list of panels that will be available in OHIF @@ -36,27 +36,8 @@ const extension = { * iconName, iconLabel, label, component} object. Example of a panel module * is the StudyBrowserPanel that is provided by the default extension in OHIF. */ - getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => { - const wrappedPanelSegmentation = () => { - return ( - - ); - }; - - return [ - { - name: 'panelSegmentation', - iconName: 'tab-segmentation', - iconLabel: 'Segmentation', - label: 'Segmentation', - component: wrappedPanelSegmentation, - }, - ]; - }, + getPanelModule, + getCommandsModule, getViewportModule({ servicesManager, extensionManager }) { const ExtendedOHIFCornerstoneSEGViewport = props => { @@ -83,4 +64,3 @@ const extension = { }; export default extension; -export { hydrateSEGDisplaySet }; diff --git a/extensions/cornerstone-dicom-seg/src/init.ts b/extensions/cornerstone-dicom-seg/src/init.ts new file mode 100644 index 0000000000..9702aa570b --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/init.ts @@ -0,0 +1,5 @@ +import { addTool, BrushTool } from '@cornerstonejs/tools'; + +export default function init({ configuration = {} }): void { + addTool(BrushTool); +} diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 7f6e96b705..d17c0397b9 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -1,16 +1,22 @@ +import { createReportAsync } from '@ohif/extension-default'; import React, { useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { SegmentationGroupTable } from '@ohif/ui'; + import callInputDialog from './callInputDialog'; -import { useAppConfig } from '@state'; +import callColorPickerDialog from './colorPickerDialog'; import { useTranslation } from 'react-i18next'; -export default function PanelSegmentation({ servicesManager, commandsManager }) { - const { segmentationService, uiDialogService } = servicesManager.services; - const [appConfig] = useAppConfig(); - const disableEditing = appConfig?.disableEditing; +export default function PanelSegmentation({ + servicesManager, + commandsManager, + extensionManager, + configuration, +}) { + const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services; const { t } = useTranslation('PanelSegmentation'); + const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); const [segmentationConfiguration, setSegmentationConfiguration] = useState( segmentationService.getConfiguration() @@ -18,29 +24,6 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); - const [isMinimized, setIsMinimized] = useState({}); - - const onToggleMinimizeSegmentation = useCallback( - id => { - setIsMinimized(prevState => ({ - ...prevState, - [id]: !prevState[id], - })); - }, - [setIsMinimized] - ); - - // Only expand the last segmentation added to the list and collapse the rest - useEffect(() => { - const lastSegmentationId = segmentations[segmentations.length - 1]?.id; - if (lastSegmentationId) { - setIsMinimized(prevState => ({ - ...prevState, - [lastSegmentationId]: false, - })); - } - }, [segmentations, setIsMinimized]); - useEffect(() => { // ~~ Subscription const added = segmentationService.EVENTS.SEGMENTATION_ADDED; @@ -64,6 +47,16 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) }; }, []); + const getToolGroupIds = segmentationId => { + const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); + + return toolGroupIds; + }; + + const onSegmentationAdd = async () => { + commandsManager.runCommand('createEmptySegmentationForViewport'); + }; + const onSegmentationClick = (segmentationId: string) => { segmentationService.setActiveSegmentationForToolGroup(segmentationId); }; @@ -72,14 +65,12 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) segmentationService.remove(segmentationId); }; - const getToolGroupIds = segmentationId => { - const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); - - return toolGroupIds; + const onSegmentAdd = segmentationId => { + segmentationService.addSegment(segmentationId); }; const onSegmentClick = (segmentationId, segmentIndex) => { - segmentationService.setActiveSegmentForSegmentation(segmentationId, segmentIndex); + segmentationService.setActiveSegment(segmentationId, segmentIndex); const toolGroupIds = getToolGroupIds(segmentationId); @@ -101,7 +92,7 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) return; } - segmentationService.setSegmentLabelForSegmentation(segmentationId, segmentIndex, label); + segmentationService.setSegmentLabel(segmentationId, segmentIndex, label); }); }; @@ -126,16 +117,34 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) }; const onSegmentColorClick = (segmentationId, segmentIndex) => { - // Todo: Implement color picker later - return; + const segmentation = segmentationService.getSegmentation(segmentationId); + + const segment = segmentation.segments[segmentIndex]; + const { color, opacity } = segment; + + const rgbaColor = { + r: color[0], + g: color[1], + b: color[2], + a: opacity / 255.0, + }; + + callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { + if (actionId === 'cancel') { + return; + } + + segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [ + newRgbaColor.r, + newRgbaColor.g, + newRgbaColor.b, + newRgbaColor.a * 255.0, + ]); + }); }; const onSegmentDelete = (segmentationId, segmentIndex) => { - // segmentationService.removeSegmentFromSegmentation( - // segmentationId, - // segmentIndex - // ); - console.warn('not implemented yet'); + segmentationService.removeSegment(segmentationId, segmentIndex); }; const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { @@ -155,6 +164,10 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) }); }; + const onToggleSegmentLock = (segmentationId, segmentIndex) => { + segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); + }; + const onToggleSegmentationVisibility = segmentationId => { segmentationService.toggleSegmentationVisibility(segmentationId); }; @@ -169,27 +182,62 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) [segmentationService] ); + const onSegmentationDownload = segmentationId => { + commandsManager.runCommand('downloadSegmentation', { + segmentationId, + }); + }; + + const storeSegmentation = async segmentationId => { + const datasources = extensionManager.getActiveDataSource(); + + const displaySetInstanceUIDs = await createReportAsync({ + servicesManager, + getReport: () => + commandsManager.runCommand('storeSegmentation', { + segmentationId, + dataSource: datasources[0], + }), + reportType: 'Segmentation', + }); + + // Show the exported report in the active viewport as read only (similar to SR) + if (displaySetInstanceUIDs) { + // clear the segmentation that we exported, similar to the storeMeasurement + // where we remove the measurements and prompt again the user if they would like + // to re-read the measurements in a SR read only viewport + segmentationService.remove(segmentationId); + + viewportGridService.setDisplaySetsForViewport({ + viewportId: viewportGridService.getActiveViewportId(), + displaySetInstanceUIDs, + }); + } + }; + return ( -
- {/* show segmentation table */} - {segmentations?.length ? ( + <> +
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value) @@ -217,8 +265,8 @@ export default function PanelSegmentation({ servicesManager, commandsManager }) _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) } /> - ) : null} -
+
+ ); } diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx new file mode 100644 index 0000000000..674ae3d2e6 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -0,0 +1,405 @@ +import React, { useCallback, useEffect, useState, useReducer } from 'react'; +import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui'; +import { Types } from '@ohif/extension-cornerstone'; +import { utilities } from '@cornerstonejs/tools'; + +const { segmentation: segmentationUtils } = utilities; + +const TOOL_TYPES = { + CIRCULAR_BRUSH: 'CircularBrush', + SPHERE_BRUSH: 'SphereBrush', + CIRCULAR_ERASER: 'CircularEraser', + SPHERE_ERASER: 'SphereEraser', + CIRCLE_SCISSOR: 'CircleScissor', + RECTANGLE_SCISSOR: 'RectangleScissor', + SPHERE_SCISSOR: 'SphereScissor', + THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush', + THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush', +}; + +const ACTIONS = { + SET_TOOL_CONFIG: 'SET_TOOL_CONFIG', + SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL', +}; + +const initialState = { + Brush: { + brushSize: 15, + mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush' + }, + Eraser: { + brushSize: 15, + mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser' + }, + Scissors: { + brushSize: 15, + mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor' + }, + ThresholdBrush: { + brushSize: 15, + thresholdRange: [-500, 500], + }, + activeTool: null, +}; + +function toolboxReducer(state, action) { + switch (action.type) { + case ACTIONS.SET_TOOL_CONFIG: + const { tool, config } = action.payload; + return { + ...state, + [tool]: { + ...state[tool], + ...config, + }, + }; + case ACTIONS.SET_ACTIVE_TOOL: + return { ...state, activeTool: action.payload }; + default: + return state; + } +} + +function SegmentationToolbox({ servicesManager, extensionManager }) { + const { toolbarService, segmentationService, toolGroupService } = + servicesManager.services as Types.CornerstoneServices; + + const [viewportGrid] = useViewportGrid(); + const { viewports, activeViewportId } = viewportGrid; + + const [toolsEnabled, setToolsEnabled] = useState(false); + const [state, dispatch] = useReducer(toolboxReducer, initialState); + + const updateActiveTool = useCallback(() => { + if (!viewports?.size || activeViewportId === undefined) { + return; + } + const viewport = viewports.get(activeViewportId); + + if (!viewport) { + return; + } + + dispatch({ + type: ACTIONS.SET_ACTIVE_TOOL, + payload: toolGroupService.getActiveToolForViewport(viewport.viewportId), + }); + }, [activeViewportId, viewports, toolGroupService, dispatch]); + + const setToolActive = useCallback( + toolName => { + toolbarService.recordInteraction({ + interactionType: 'tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName, + }, + }, + ], + }); + + dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName }); + }, + [toolbarService, dispatch] + ); + + /** + * sets the tools enabled IF there are segmentations + */ + useEffect(() => { + const events = [ + segmentationService.EVENTS.SEGMENTATION_ADDED, + segmentationService.EVENTS.SEGMENTATION_UPDATED, + segmentationService.EVENTS.SEGMENTATION_REMOVED, + ]; + + const unsubscriptions = []; + + events.forEach(event => { + const { unsubscribe } = segmentationService.subscribe(event, () => { + const segmentations = segmentationService.getSegmentations(); + + const activeSegmentation = segmentations?.find(seg => seg.isActive); + + setToolsEnabled(activeSegmentation?.segmentCount > 0); + }); + + unsubscriptions.push(unsubscribe); + }); + + updateActiveTool(); + + return () => { + unsubscriptions.forEach(unsubscribe => unsubscribe()); + }; + }, [activeViewportId, viewports, segmentationService, updateActiveTool]); + + /** + * Update the active tool when the toolbar state changes + */ + useEffect(() => { + const { unsubscribe } = toolbarService.subscribe( + toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED, + () => { + updateActiveTool(); + } + ); + + return () => { + unsubscribe(); + }; + }, [toolbarService, updateActiveTool]); + + useEffect(() => { + // if the active tool is not a brush tool then do nothing + if (!Object.values(TOOL_TYPES).includes(state.activeTool)) { + return; + } + + // if the tool is Segmentation and it is enabled then do nothing + if (toolsEnabled) { + return; + } + + // if the tool is Segmentation and it is disabled, then switch + // back to the window level tool to not confuse the user when no + // segmentation is active or when there is no segment in the segmentation + setToolActive('WindowLevel'); + }, [toolsEnabled, state.activeTool, setToolActive]); + + const updateBrushSize = useCallback( + (toolName, brushSize) => { + toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName); + }); + }, + [toolGroupService] + ); + + const onBrushSizeChange = useCallback( + (valueAsStringOrNumber, toolCategory) => { + const value = Number(valueAsStringOrNumber); + + _getToolNamesFromCategory(toolCategory).forEach(toolName => { + updateBrushSize(toolName, value); + }); + + dispatch({ + type: ACTIONS.SET_TOOL_CONFIG, + payload: { + tool: toolCategory, + config: { brushSize: value }, + }, + }); + }, + [toolGroupService, dispatch] + ); + + const handleRangeChange = useCallback( + newRange => { + if ( + newRange[0] === state.ThresholdBrush.thresholdRange[0] && + newRange[1] === state.ThresholdBrush.thresholdRange[1] + ) { + return; + } + + const toolNames = _getToolNamesFromCategory('ThresholdBrush'); + + toolNames.forEach(toolName => { + toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + toolGroup.setToolConfiguration(toolName, { + strategySpecificConfiguration: { + THRESHOLD_INSIDE_CIRCLE: { + threshold: newRange, + }, + }, + }); + }); + }); + + dispatch({ + type: ACTIONS.SET_TOOL_CONFIG, + payload: { + tool: 'ThresholdBrush', + config: { thresholdRange: newRange }, + }, + }); + }, + [toolGroupService, dispatch, state.ThresholdBrush.thresholdRange] + ); + + return ( + setToolActive(TOOL_TYPES.CIRCULAR_BRUSH), + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.01, + max: 100, + value: state.Brush.brushSize, + step: 0.5, + onChange: value => onBrushSizeChange(value, 'Brush'), + }, + { + name: 'Mode', + type: 'radio', + id: 'brush-mode', + value: state.Brush.mode, + values: [ + { value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' }, + { value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' }, + ], + onChange: value => setToolActive(value), + }, + ], + }, + { + name: 'Eraser', + icon: 'icon-tool-eraser', + disabled: !toolsEnabled, + active: + state.activeTool === TOOL_TYPES.CIRCULAR_ERASER || + state.activeTool === TOOL_TYPES.SPHERE_ERASER, + onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER), + options: [ + { + name: 'Radius (mm)', + type: 'range', + id: 'eraser-radius', + min: 0.01, + max: 100, + value: state.Eraser.brushSize, + step: 0.5, + onChange: value => onBrushSizeChange(value, 'Eraser'), + }, + { + name: 'Mode', + type: 'radio', + id: 'eraser-mode', + value: state.Eraser.mode, + values: [ + { value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' }, + { value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' }, + ], + onChange: value => setToolActive(value), + }, + ], + }, + { + name: 'Scissor', + icon: 'icon-tool-scissor', + disabled: !toolsEnabled, + active: + state.activeTool === TOOL_TYPES.CIRCLE_SCISSOR || + state.activeTool === TOOL_TYPES.RECTANGLE_SCISSOR || + state.activeTool === TOOL_TYPES.SPHERE_SCISSOR, + onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SCISSOR), + options: [ + { + name: 'Mode', + type: 'radio', + value: state.Scissors.mode, + id: 'scissor-mode', + values: [ + { value: TOOL_TYPES.CIRCLE_SCISSOR, label: 'Circle' }, + { value: TOOL_TYPES.RECTANGLE_SCISSOR, label: 'Rectangle' }, + { value: TOOL_TYPES.SPHERE_SCISSOR, label: 'Sphere' }, + ], + onChange: value => setToolActive(value), + }, + ], + }, + { + name: 'Threshold Tool', + icon: 'icon-tool-threshold', + disabled: !toolsEnabled, + active: + state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH || + state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, + onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH), + options: [ + { + name: 'Radius (mm)', + id: 'threshold-radius', + type: 'range', + min: 0.01, + max: 100, + value: state.ThresholdBrush.brushSize, + step: 0.5, + onChange: value => onBrushSizeChange(value, 'ThresholdBrush'), + }, + { + name: 'Mode', + type: 'radio', + id: 'threshold-mode', + value: state.activeTool, + values: [ + { value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' }, + { value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' }, + ], + onChange: value => setToolActive(value), + }, + { + type: 'custom', + id: 'segmentation-threshold-range', + children: () => { + return ( +
+
+
Threshold
+ +
+ ); + }, + }, + ], + }, + ]} + /> + ); +} + +function _getToolNamesFromCategory(category) { + let toolNames = []; + switch (category) { + case 'Brush': + toolNames = ['CircularBrush', 'SphereBrush']; + break; + case 'Eraser': + toolNames = ['CircularEraser', 'SphereEraser']; + break; + case 'ThresholdBrush': + toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush']; + break; + default: + break; + } + + return toolNames; +} + +export default SegmentationToolbox; diff --git a/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.css b/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.css new file mode 100644 index 0000000000..1c6bb20670 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.css @@ -0,0 +1,3 @@ +.chrome-picker { + background: #090c29 !important; +} diff --git a/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx b/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx new file mode 100644 index 0000000000..38e85efb29 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/panels/colorPickerDialog.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Dialog } from '@ohif/ui'; +import { ChromePicker } from 'react-color'; + +import './colorPickerDialog.css'; + +function callColorPickerDialog(uiDialogService, rgbaColor, callback) { + const dialogId = 'pick-color'; + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': + callback(value.rgbaColor, action.id); + break; + case 'cancel': + callback('', action.id); + break; + } + uiDialogService.dismiss({ id: dialogId }); + }; + + if (uiDialogService) { + uiDialogService.create({ + id: dialogId, + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Segment Color', + value: { rgbaColor }, + noCloseButton: true, + onClose: () => uiDialogService.dismiss({ id: dialogId }), + actions: [ + { id: 'cancel', text: 'Cancel', type: 'primary' }, + { id: 'save', text: 'Save', type: 'secondary' }, + ], + onSubmit: onSubmitHandler, + body: ({ value, setValue }) => { + const handleChange = color => { + setValue({ rgbaColor: color.rgb }); + }; + + return ( + + ); + }, + }, + }); + } +} + +export default callColorPickerDialog; diff --git a/extensions/cornerstone-dicom-seg/src/utils/_hydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/_hydrateSEG.ts deleted file mode 100644 index 84d075f97c..0000000000 --- a/extensions/cornerstone-dicom-seg/src/utils/_hydrateSEG.ts +++ /dev/null @@ -1,73 +0,0 @@ -async function _hydrateSEGDisplaySet({ - segDisplaySet, - viewportId: targetViewportId, - servicesManager, -}) { - const { segmentationService, hangingProtocolService, viewportGridService } = - servicesManager.services; - - const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; - - let segmentationId = null; - - // We need the hydration to notify panels about the new segmentation added - const suppressEvents = false; - - segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( - segDisplaySet, - segmentationId, - suppressEvents - ); - - segmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID); - - const { viewports } = viewportGridService.getState(); - - const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( - targetViewportId, - displaySetInstanceUID - ); - - // Todo: fix this after we have a better way for stack viewport segmentations - - // check every viewport in the viewports to see if the displaySetInstanceUID - // is being displayed, if so we need to update the viewport to use volume viewport - // (if already is not using it) since Cornerstone3D currently only supports - // volume viewport for segmentation - viewports.forEach((viewport, viewportId) => { - if (targetViewportId === viewportId) { - return; - } - - const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( - viewport.displaySetInstanceUIDs, - segDisplaySet.displaySetInstanceUID - ); - - if (shouldDisplaySeg) { - updatedViewports.push({ - viewportId, - displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, - viewportOptions: { - // Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component - // Used for segmentation hydration right now, since the logic to decide whether - // a viewport needs to render a segmentation lives inside the CornerstoneViewportService - // so we need to re-render (force update via change of the needsRerendering) so that React - // does the diffing and decides we should render this again (although the id and element has not changed) - // so that the CornerstoneViewportService can decide whether to render the segmentation or not. - needsRerendering: true, - initialImageOptions: { - preset: 'middle', - }, - }, - }); - } - }); - - // Do the entire update at once - viewportGridService.setDisplaySetsForViewports(updatedViewports); - - return true; -} - -export default _hydrateSEGDisplaySet; diff --git a/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts b/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts new file mode 100644 index 0000000000..fa3c6d47c8 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts @@ -0,0 +1,190 @@ +import { Enums, cache } from '@cornerstonejs/core'; + +/** + * Updates the viewports in preparation for rendering segmentations. + * Evaluates each viewport to determine which need modifications, + * then for those viewports, changes them to a volume type and ensures + * they are ready for segmentation rendering. + * + * @param {Object} params - Parameters for the function. + * @param params.viewportId - ID of the viewport to be updated. + * @param params.loadFn - Function to load the segmentation data. + * @param params.servicesManager - The services manager. + * @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance. + * + * @returns Returns true upon successful update of viewports for segmentation rendering. + */ +async function updateViewportsForSegmentationRendering({ + viewportId, + loadFn, + servicesManager, + referencedDisplaySetInstanceUID, +}: { + viewportId: string; + loadFn: () => Promise; + servicesManager: any; + referencedDisplaySetInstanceUID?: string; +}) { + const { cornerstoneViewportService, segmentationService, viewportGridService } = + servicesManager.services; + + const viewport = getTargetViewport({ viewportId, viewportGridService }); + const targetViewportId = viewport.viewportOptions.viewportId; + + referencedDisplaySetInstanceUID = + referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0]; + + const updatedViewports = getUpdatedViewportsForSegmentation({ + servicesManager, + viewportId, + referencedDisplaySetInstanceUID, + }); + + // create Segmentation callback which needs to be waited until + // the volume is created (if coming from stack) + const createSegmentationForVolume = async () => { + const segmentationId = await loadFn(); + segmentationService.hydrateSegmentation(segmentationId); + }; + + // the reference volume that is used to draw the segmentation. so check if the + // volume exists in the cache (the target Viewport is already a volume viewport) + const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId => + volumeId.includes(referencedDisplaySetInstanceUID) + ); + + updatedViewports.forEach(async viewport => { + viewport.viewportOptions = { + ...viewport.viewportOptions, + viewportType: 'volume', + needsRerendering: true, + }; + const viewportId = viewport.viewportId; + + const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const prevCamera = csViewport.getCamera(); + + // only run the createSegmentationForVolume for the targetViewportId + // since the rest will get handled by cornerstoneViewportService + if (volumeExists && viewportId === targetViewportId) { + await createSegmentationForVolume(); + return; + } + + const createNewSegmentationWhenVolumeMounts = async evt => { + const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac => + ac.uid.includes(referencedDisplaySetInstanceUID) + ); + + // Note: make sure to re-grab the viewport since it might have changed + // during the time it took for the volume to be mounted, for instance + // the stack viewport has been changed to a volume viewport + const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + volumeViewport.setCamera(prevCamera); + + volumeViewport.element.removeEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + createNewSegmentationWhenVolumeMounts + ); + + if (!isTheActiveViewportVolumeMounted) { + // it means it is one of those other updated viewports so just update the camera + return; + } + + if (viewportId === targetViewportId) { + await createSegmentationForVolume(); + } + }; + + csViewport.element.addEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + createNewSegmentationWhenVolumeMounts + ); + }); + + // Set the displaySets for the viewports that require to be updated + viewportGridService.setDisplaySetsForViewports(updatedViewports); + + return true; +} + +const getTargetViewport = ({ viewportId, viewportGridService }) => { + const { viewports, activeViewportId } = viewportGridService.getState(); + const targetViewportId = viewportId || activeViewportId; + + const viewport = viewports.get(targetViewportId); + + return viewport; +}; + +/** + * Retrieves a list of viewports that require updates in preparation for segmentation rendering. + * This function evaluates viewports based on their compatibility with the provided segmentation's + * frame of reference UID and appends them to the updated list if they should render the segmentation. + * + * @param {Object} params - Parameters for the function. + * @param params.viewportId - the ID of the viewport to be updated. + * @param params.servicesManager - The services manager + * @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance. + * + * @returns {Array} Returns an array of viewports that require updates for segmentation rendering. + */ +function getUpdatedViewportsForSegmentation({ + viewportId, + servicesManager, + referencedDisplaySetInstanceUID, +}) { + const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } = + servicesManager.services; + + const { viewports } = viewportGridService.getState(); + + const viewport = getTargetViewport({ viewportId, viewportGridService }); + const targetViewportId = viewport.viewportOptions.viewportId; + + const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs; + + const referenceDisplaySetInstanceUID = + referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0]; + + const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID); + const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID; + + const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + targetViewportId, + referenceDisplaySetInstanceUID + ); + + viewports.forEach((viewport, viewportId) => { + if ( + targetViewportId === viewportId || + updatedViewports.find(v => v.viewportId === viewportId) + ) { + return; + } + + const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( + viewport.displaySetInstanceUIDs, + segmentationFrameOfReferenceUID + ); + + if (shouldDisplaySeg) { + updatedViewports.push({ + viewportId, + displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, + viewportOptions: { + viewportType: 'volume', + needsRerendering: true, + }, + }); + } + }); + return updatedViewports; +} + +export { + updateViewportsForSegmentationRendering, + getUpdatedViewportsForSegmentation, + getTargetViewport, +}; diff --git a/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts b/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts index f4fe36ca7e..8ddc088c6c 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts @@ -1,7 +1,7 @@ function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) { const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {}; - return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); + return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } export default createSEGToolGroupAndAddTools; diff --git a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts index 65f92c7435..9f8c1dddf7 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts @@ -1,5 +1,4 @@ import { ButtonEnums } from '@ohif/ui'; -import hydrateSEGDisplaySet from './_hydrateSEG'; const RESPONSE = { NO_NEVER: -1, @@ -7,7 +6,13 @@ const RESPONSE = { HYDRATE_SEG: 5, }; -function promptHydrateSEG({ servicesManager, segDisplaySet, viewportId, preHydrateCallbacks }) { +function promptHydrateSEG({ + servicesManager, + segDisplaySet, + viewportId, + preHydrateCallbacks, + hydrateSEGDisplaySet, +}) { const { uiViewportDialogService } = servicesManager.services; return new Promise(async function (resolve, reject) { @@ -21,7 +26,6 @@ function promptHydrateSEG({ servicesManager, segDisplaySet, viewportId, preHydra const isHydrated = await hydrateSEGDisplaySet({ segDisplaySet, viewportId, - servicesManager, }); resolve(isHydrated); diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 7e4d397786..fb39f8c36f 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -5,7 +5,6 @@ import OHIF, { utils } from '@ohif/core'; import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import promptHydrateSEG from '../utils/promptHydrateSEG'; -import hydrateSEGDisplaySet from '../utils/_hydrateSEG'; import _getStatusComponent from './_getStatusComponent'; const { formatDate } = utils; @@ -158,6 +157,7 @@ function OHIFCornerstoneSEGViewport(props) { viewportId, segDisplaySet, preHydrateCallbacks: [storePresentationState], + hydrateSEGDisplaySet, }).then(isHydrated => { if (isHydrated) { setIsHydrated(true); @@ -291,6 +291,13 @@ function OHIFCornerstoneSEGViewport(props) { SpacingBetweenSlices, } = referencedDisplaySetRef.current.metadata; + const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => { + commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { + displaySets: [segDisplaySet], + viewportId, + }); + }; + const onStatusClick = async () => { // Before hydrating a SEG and make it added to all viewports in the grid // that share the same frameOfReferenceUID, we need to store the viewport grid @@ -302,7 +309,6 @@ function OHIFCornerstoneSEGViewport(props) { const isHydrated = await hydrateSEGDisplaySet({ segDisplaySet, viewportId, - servicesManager, }); setIsHydrated(isHydrated); @@ -369,12 +375,18 @@ OHIFCornerstoneSEGViewport.defaultProps = { }; function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) { - const { - SharedFunctionalGroupsSequence: [SharedFunctionalGroup], - } = segDisplaySet.instance; - const { - PixelMeasuresSequence: [PixelMeasures], - } = SharedFunctionalGroup; + const { SharedFunctionalGroupsSequence } = segDisplaySet.instance; + + const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence) + ? SharedFunctionalGroupsSequence[0] + : SharedFunctionalGroupsSequence; + + const { PixelMeasuresSequence } = SharedFunctionalGroup; + + const PixelMeasures = Array.isArray(PixelMeasuresSequence) + ? PixelMeasuresSequence[0] + : PixelMeasuresSequence; + const { SpacingBetweenSlices, SliceThickness } = PixelMeasures; const image0 = referencedDisplaySet.images[0]; diff --git a/extensions/cornerstone-dicom-sr/CHANGELOG.md b/extensions/cornerstone-dicom-sr/CHANGELOG.md index 6040e1a167..044f65bc94 100644 --- a/extensions/cornerstone-dicom-sr/CHANGELOG.md +++ b/extensions/cornerstone-dicom-sr/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 9706a4ec5c..5dc80ccda0 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for an SR Cornerstone Viewport", "author": "OHIF", "license": "MIT", @@ -32,10 +32,10 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-measurement-tracking": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-measurement-tracking": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "^0.29.5", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", @@ -44,9 +44,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.13.2", - "@cornerstonejs/core": "^1.13.2", - "@cornerstonejs/tools": "^1.13.2", + "@cornerstonejs/adapters": "^1.16.5", + "@cornerstonejs/core": "^1.16.5", + "@cornerstonejs/tools": "^1.16.5", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 3089056b6a..a1e728bdda 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -240,7 +240,7 @@ function OHIFCornerstoneSRViewport(props) { const onDisplaySetsRemovedSubscription = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_REMOVED, ({ displaySetInstanceUIDs }) => { - const activeViewport = viewports[activeViewportId]; + const activeViewport = viewports.get(activeViewportId); if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { viewportGridService.setDisplaySetsForViewport({ viewportId: activeViewportId, diff --git a/extensions/cornerstone/CHANGELOG.md b/extensions/cornerstone/CHANGELOG.md index d7152c0e1a..7318ceca59 100644 --- a/extensions/cornerstone/CHANGELOG.md +++ b/extensions/cornerstone/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-cornerstone diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index e77e1bca5d..dc84a2be75 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for Cornerstone", "author": "OHIF", "license": "MIT", @@ -36,9 +36,9 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.13.2", - "@ohif/core": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@cornerstonejs/dicom-image-loader": "^1.16.5", + "@ohif/core": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "^0.29.6", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", @@ -52,10 +52,10 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.13.2", - "@cornerstonejs/core": "^1.13.2", - "@cornerstonejs/streaming-image-volume-loader": "^1.13.2", - "@cornerstonejs/tools": "^1.13.2", + "@cornerstonejs/adapters": "^1.16.5", + "@cornerstonejs/core": "^1.16.5", + "@cornerstonejs/streaming-image-volume-loader": "^1.16.5", + "@cornerstonejs/tools": "^1.16.5", "@kitware/vtk.js": "27.3.1", "html2canvas": "^1.4.1", "lodash.debounce": "4.0.8", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 8f9e928e64..a5b12709f9 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -18,7 +18,6 @@ import toggleStackImageSync from './utils/stackSync/toggleStackImageSync'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import { CornerstoneServices } from './types'; -import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; function commandsModule({ servicesManager, @@ -266,7 +265,6 @@ function commandsModule({ toolbarServiceRecordInteraction: props => { toolbarService.recordInteraction(props); }, - setToolActive: ({ toolName, toolGroupId = null, toggledState }) => { if (toolName === 'Crosshairs') { const activeViewportToolGroup = toolGroupService.getToolGroup(null); diff --git a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx index e4e080422e..92bb151032 100644 --- a/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx +++ b/extensions/cornerstone/src/components/CinePlayer/CinePlayer.tsx @@ -13,12 +13,12 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) { const handleCineClose = () => { toolbarService.recordInteraction({ groupId: 'MoreTools', - itemId: 'cine', interactionType: 'toggle', commands: [ { commandName: 'toggleCine', commandOptions: {}, + toolName: 'cine', context: 'CORNERSTONE', }, ], diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index d766776e63..10b4ce026b 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -137,7 +137,7 @@ const cornerstoneExtension: Types.Extensions.Extension = { export type { PublicViewportOptions }; export { measurementMappingUtils, - CornerstoneExtensionTypes, + CornerstoneExtensionTypes as Types, toolNames, getActiveViewportEnabledElement, }; diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index 6be18b3637..f052b6e019 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -12,6 +12,7 @@ import { imageLoadPoolManager, Settings, utilities as csUtilities, + Enums as csEnums, } from '@cornerstonejs/core'; import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools'; import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader'; @@ -41,10 +42,28 @@ export default async function init({ configuration, appConfig, }: Types.Extensions.ExtensionParams): Promise { - await cs3DInit(); + + await cs3DInit({ + rendering: { + preferSizeOverAccuracy: Boolean(appConfig.use16BitDataType), + useNorm16Texture: Boolean(appConfig.use16BitDataType), + }, + }); // For debugging e2e tests that are failing on CI cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering)); + + switch (appConfig.useSharedArrayBuffer) { + case 'AUTO': + cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.AUTO); + break; + case 'FALSE': + cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.FALSE); + break; + default: + cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.TRUE); + } + cornerstone.setConfiguration({ ...cornerstone.getConfiguration(), rendering: { diff --git a/extensions/cornerstone/src/initContextMenu.ts b/extensions/cornerstone/src/initContextMenu.ts index 60e1668242..488fdf8587 100644 --- a/extensions/cornerstone/src/initContextMenu.ts +++ b/extensions/cornerstone/src/initContextMenu.ts @@ -18,6 +18,7 @@ const DEFAULT_CONTEXT_MENU_CLICKS = { { commandName: 'showCornerstoneContextMenu', commandOptions: { + requireNearbyToolData: true, menuId: 'measurementsContextMenu', }, }, @@ -62,8 +63,20 @@ function initContextMenu({ const customizations = customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_CONTEXT_MENU_CLICKS; const toRun = customizations[name]; + + if (!toRun) { + return; + } + + // only find nearbyToolData if required, for the click (which closes the context menu + // we don't need to find nearbyToolData) + let nearbyToolData = null; + if (toRun.commands.some(command => command.commandOptions?.requireNearbyToolData)) { + nearbyToolData = findNearbyToolData(commandsManager, evt); + } + const options = { - nearbyToolData: findNearbyToolData(commandsManager, evt), + nearbyToolData, event: evt, }; commandsManager.run(toRun, options); diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js index d6515a5088..767b607a77 100644 --- a/extensions/cornerstone/src/initCornerstoneTools.js +++ b/extensions/cornerstone/src/initCornerstoneTools.js @@ -25,6 +25,9 @@ import { annotation, ReferenceLinesTool, TrackballRotateTool, + CircleScissorsTool, + RectangleScissorsTool, + SphereScissorsTool, } from '@cornerstonejs/tools'; import CalibrationLineTool from './tools/CalibrationLineTool'; @@ -59,6 +62,9 @@ export default function initCornerstoneTools(configuration = {}) { addTool(ReferenceLinesTool); addTool(CalibrationLineTool); addTool(TrackballRotateTool); + addTool(CircleScissorsTool); + addTool(RectangleScissorsTool); + addTool(SphereScissorsTool); addTool(ImageOverlayViewerTool); // Modify annotation tools to use dashed lines on SR @@ -101,6 +107,9 @@ const toolNames = { ReferenceLines: ReferenceLinesTool.toolName, CalibrationLine: CalibrationLineTool.toolName, TrackballRotateTool: TrackballRotateTool.toolName, + CircleScissors: CircleScissorsTool.toolName, + RectangleScissors: RectangleScissorsTool.toolName, + SphereScissors: SphereScissorsTool.toolName, ImageOverlayViewer: ImageOverlayViewerTool.toolName, }; diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js index 1df82f1c93..1beda3885b 100644 --- a/extensions/cornerstone/src/initWADOImageLoader.js +++ b/extensions/cornerstone/src/initWADOImageLoader.js @@ -49,6 +49,7 @@ export default function initWADOImageLoader( // Until the default is set to true (which is the case for cornerstone3D), // we should set this flag to false. convertFloatPixelDataToInt: false, + use16BitDataType: Boolean(appConfig.use16BitDataType), }, beforeSend: function (xhr) { //TODO should be removed in the future and request emitted by DicomWebDataSource diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index 8d573f5641..a2fc897601 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -210,7 +210,7 @@ class CornerstoneCacheService { } private _shouldRenderSegmentation(displaySets) { - const { segmentationService } = this.servicesManager.services; + const { segmentationService, displaySetService } = this.servicesManager.services; const viewportDisplaySetInstanceUIDs = displaySets.map( ({ displaySetInstanceUID }) => displaySetInstanceUID @@ -222,10 +222,11 @@ class CornerstoneCacheService { for (const segmentation of segmentations) { const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID; + const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID); const shouldDisplaySeg = segmentationService.shouldRenderSegmentation( viewportDisplaySetInstanceUIDs, - segDisplaySetInstanceUID + segDisplaySet.instances[0].FrameOfReferenceUID ); if (shouldDisplaySeg) { diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 8ea071e5ab..7d61a1dc55 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -99,43 +99,51 @@ class SegmentationService extends PubSubService { }; /** - * It adds a segment to a segmentation, basically just setting the properties for - * the segment. - * @param segmentationId - The ID of the segmentation you want to add a - * segment to. - * @param segmentIndex - The index of the segment to add. - * @param properties - The properties of the segment to add including - * -- label: the label of the segment - * -- color: the color of the segment - * -- opacity: the opacity of the segment - * -- visibility: the visibility of the segment (boolean) - * -- isLocked: whether the segment is locked for editing - * -- active: whether the segment is currently the active segment to be edited + * Adds a new segment to the specified segmentation. + * @param segmentationId - The ID of the segmentation to add the segment to. + * @param config - An object containing the configuration options for the new segment. + * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used. + * - toolGroupId: (optional) The ID of the tool group to associate the new segment with. If not provided, the first available tool group will be used. + * - properties: (optional) An object containing the properties of the new segment. + * - label: (optional) The label of the new segment. If not provided, a default label will be used. + * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used. + * - opacity: (optional) The opacity of the new segment. If not provided, a default opacity will be used. + * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default. + * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default. + * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default. */ public addSegment( segmentationId: string, - segmentIndex: number, - toolGroupId?: string, - properties?: { - label?: string; - color?: ohifTypes.RGB; - opacity?: number; - visibility?: boolean; - isLocked?: boolean; - active?: boolean; - } + config: { + segmentIndex?: number; + toolGroupId?: string; + properties?: { + label?: string; + color?: ohifTypes.RGB; + opacity?: number; + visibility?: boolean; + isLocked?: boolean; + active?: boolean; + }; + } = {} ): void { - if (segmentIndex === 0) { + if (config?.segmentIndex === 0) { throw new Error('Segment index 0 is reserved for "no label"'); } - toolGroupId = toolGroupId ?? this._getFirstToolGroupId(); + const toolGroupId = config.toolGroupId ?? this._getFirstToolGroupId(); const { segmentationRepresentationUID, segmentation } = this._getSegmentationInfo( segmentationId, toolGroupId ); + let segmentIndex = config.segmentIndex; + if (!segmentIndex) { + // grab the next available segment index + segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length; + } + if (this._getSegmentInfo(segmentation, segmentIndex)) { throw new Error(`Segment ${segmentIndex} already exists`); } @@ -147,7 +155,7 @@ class SegmentationService extends PubSubService { ); segmentation.segments[segmentIndex] = { - label: properties.label, + label: config.properties?.label ?? `Segment ${segmentIndex}`, segmentIndex: segmentIndex, color: [rgbaColor[0], rgbaColor[1], rgbaColor[2]], opacity: rgbaColor[3], @@ -157,9 +165,12 @@ class SegmentationService extends PubSubService { segmentation.segmentCount++; + // make the newly added segment the active segment + this._setActiveSegment(segmentationId, segmentIndex); + const suppressEvents = true; - if (properties !== undefined) { - const { color: newColor, opacity, isLocked, visibility, active } = properties; + if (config.properties !== undefined) { + const { color: newColor, opacity, isLocked, visibility, active } = config.properties; if (newColor !== undefined) { this._setSegmentColor(segmentationId, segmentIndex, newColor, toolGroupId, suppressEvents); @@ -281,17 +292,21 @@ class SegmentationService extends PubSubService { ); } - public setSegmentLockedForSegmentation( - segmentationId: string, - segmentIndex: number, - isLocked: boolean - ): void { + public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void { const suppressEvents = false; this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents); } - public setSegmentLabel(segmentationId: string, segmentIndex: number, segmentLabel: string): void { - this._setSegmentLabel(segmentationId, segmentIndex, segmentLabel); + /** + * Toggles the locked state of a segment in a segmentation. + * @param segmentationId - The ID of the segmentation. + * @param segmentIndex - The index of the segment to toggle. + */ + public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void { + const segmentation = this.getSegmentation(segmentationId); + const segment = this._getSegmentInfo(segmentation, segmentIndex); + const isLocked = !segment.isLocked; + this._setSegmentLocked(segmentationId, segmentIndex, isLocked); } public setSegmentColor( @@ -353,7 +368,7 @@ class SegmentationService extends PubSubService { this._setActiveSegmentationForToolGroup(segmentationId, toolGroupId, suppressEvents); } - public setActiveSegmentForSegmentation(segmentationId: string, segmentIndex: number): void { + public setActiveSegment(segmentationId: string, segmentIndex: number): void { this._setActiveSegment(segmentationId, segmentIndex, false); } @@ -434,12 +449,14 @@ class SegmentationService extends PubSubService { }, ]); - // Define a new color LUT and associate it with this segmentation. - // Todo: need to be generalized to accept custom color LUTs - const newColorLUT = this.generateNewColorLUT(); - const newColorLUTIndex = this.getNextColorLUTIndex(); - - cstSegmentation.config.color.addColorLUT(newColorLUT, newColorLUTIndex); + // if first segmentation, we can use the default colorLUT, otherwise + // we need to generate a new one and use a new colorLUT + const colorLUTIndex = 0; + if (Object.keys(this.segmentations).length !== 0) { + const newColorLUT = this.generateNewColorLUT(); + const colorLUTIndex = this.getNextColorLUTIndex(); + cstSegmentation.config.color.addColorLUT(newColorLUT, colorLUTIndex); + } this.segmentations[segmentationId] = { ...segmentation, @@ -448,8 +465,8 @@ class SegmentationService extends PubSubService { activeSegmentIndex: segmentation.activeSegmentIndex ?? null, segmentCount: segmentation.segmentCount ?? 0, isActive: false, - colorLUTIndex: newColorLUTIndex, isVisible: true, + colorLUTIndex, }; cachedSegmentation = this.segmentations[segmentationId]; @@ -736,6 +753,97 @@ class SegmentationService extends PubSubService { return this.addOrUpdateSegmentation(segmentation, suppressEvents); } + // Todo: this should not run on the main thread + public calculateCentroids = ( + segmentationId: string, + segmentIndex?: number + ): Map => { + const segmentation = this.getSegmentation(segmentationId); + const volume = this.getLabelmapVolume(segmentationId); + const { dimensions, imageData } = volume; + const scalarData = volume.getScalarData(); + const [dimX, dimY, numFrames] = dimensions; + const frameLength = dimX * dimY; + + const segmentIndices = segmentIndex + ? [segmentIndex] + : segmentation.segments + .filter(segment => segment?.segmentIndex) + .map(segment => segment.segmentIndex); + + const segmentIndicesSet = new Set(segmentIndices); + + const centroids = new Map(); + for (const index of segmentIndicesSet) { + centroids.set(index, { x: 0, y: 0, z: 0, count: 0 }); + } + + let voxelIndex = 0; + for (let frame = 0; frame < numFrames; frame++) { + for (let p = 0; p < frameLength; p++) { + const segmentIndex = scalarData[voxelIndex++]; + if (segmentIndicesSet.has(segmentIndex)) { + const centroid = centroids.get(segmentIndex); + centroid.x += p % dimX; + centroid.y += (p / dimX) | 0; + centroid.z += frame; + centroid.count++; + } + } + } + + const result = new Map(); + for (const [index, centroid] of centroids) { + const count = centroid.count; + const normalizedCentroid = { + x: centroid.x / count, + y: centroid.y / count, + z: centroid.z / count, + }; + normalizedCentroid.world = imageData.indexToWorld([ + normalizedCentroid.x, + normalizedCentroid.y, + normalizedCentroid.z, + ]); + result.set(index, normalizedCentroid); + } + + this.setCentroids(segmentationId, result); + return result; + }; + + private setCentroids = ( + segmentationId: string, + centroids: Map + ): void => { + const segmentation = this.getSegmentation(segmentationId); + const imageData = this.getLabelmapVolume(segmentationId).imageData; // Assuming this method returns imageData + + if (!segmentation.cachedStats) { + segmentation.cachedStats = { segmentCenter: {} }; + } else if (!segmentation.cachedStats.segmentCenter) { + segmentation.cachedStats.segmentCenter = {}; + } + + for (const [segmentIndex, centroid] of centroids) { + let world = centroid.world; + + // If world coordinates are not provided, calculate them + if (!world || world.length === 0) { + world = imageData.indexToWorld(centroid.image); + } + + segmentation.cachedStats.segmentCenter[segmentIndex] = { + center: { + image: centroid.image, + world: world, + }, + }; + } + + this.addOrUpdateSegmentation(segmentation, true, true); + }; + public jumpToSegmentCenter( segmentationId: string, segmentIndex: number, @@ -749,6 +857,10 @@ class SegmentationService extends PubSubService { const { toolGroupService } = this.servicesManager.services; const center = this._getSegmentCenter(segmentationId, segmentIndex); + if (!center?.world) { + return; + } + const { world } = center; // todo: generalize @@ -831,6 +943,7 @@ class SegmentationService extends PubSubService { displaySetInstanceUID: string, options?: { segmentationId: string; + FrameOfReferenceUID: string; label: string; } ): Promise => { @@ -865,6 +978,8 @@ class SegmentationService extends PubSubService { // We should set it as active by default, as it created for display isActive: true, type: representationType, + FrameOfReferenceUID: + options?.FrameOfReferenceUID || displaySet.instances?.[0]?.FrameOfReferenceUID, representationData: { LABELMAP: { volumeId: segmentationId, @@ -892,7 +1007,8 @@ class SegmentationService extends PubSubService { toolGroupId: string, segmentationId: string, hydrateSegmentation = false, - representationType = csToolsEnums.SegmentationRepresentations.Labelmap + representationType = csToolsEnums.SegmentationRepresentations.Labelmap, + suppressEvents = false ): Promise => { const segmentation = this.getSegmentation(segmentationId); @@ -959,13 +1075,19 @@ class SegmentationService extends PubSubService { ); } - if (isLocked !== undefined) { + if (isLocked) { this._setSegmentLocked(segmentationId, segmentIndex, isLocked, suppressEvents); } } + + if (!suppressEvents) { + this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { + segmentation, + }); + } }; - public setSegmentRGBAColorForSegmentation = ( + public setSegmentRGBAColor = ( segmentationId: string, segmentIndex: number, rgbaColor, @@ -1008,11 +1130,11 @@ class SegmentationService extends PubSubService { if (!segmentation) { throw new Error(`Segmentation with segmentationId ${segmentationId} not found.`); } + segmentation.hydrated = true; + // Not all segmentations have dipslaysets, some of them are derived in the client this._setDisplaySetIsHydrated(segmentationId, true); - segmentation.hydrated = true; - if (!suppressEvents) { this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { segmentation, @@ -1021,8 +1143,13 @@ class SegmentationService extends PubSubService { }; private _setDisplaySetIsHydrated(displaySetUID: string, isHydrated: boolean): void { - const { DisplaySetService: displaySetService } = this.servicesManager.services; + const { displaySetService } = this.servicesManager.services; const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); + + if (!displaySet) { + return; + } + displaySet.isHydrated = isHydrated; displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false); } @@ -1167,7 +1294,6 @@ class SegmentationService extends PubSubService { } const { colorLUTIndex } = segmentation; - this._removeSegmentationFromCornerstone(segmentationId); // Delete associated colormap @@ -1181,8 +1307,12 @@ class SegmentationService extends PubSubService { if (wasActive) { const remainingSegmentations = this._getSegmentations(); - if (remainingSegmentations.length) { - const { id } = remainingSegmentations[0]; + const remainingHydratedSegmentations = remainingSegmentations.filter( + segmentation => segmentation.hydrated + ); + + if (remainingHydratedSegmentations.length) { + const { id } = remainingHydratedSegmentations[0]; this._setActiveSegmentationForToolGroup(id, this._getFirstToolGroupId(), false); } @@ -1312,15 +1442,11 @@ class SegmentationService extends PubSubService { return cstSegmentation.state.getSegmentationRepresentations(toolGroupId); }; - public setSegmentLabelForSegmentation( - segmentationId: string, - segmentIndex: number, - label: string - ) { - this._setSegmentLabelForSegmentation(segmentationId, segmentIndex, label); + public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) { + this._setSegmentLabel(segmentationId, segmentIndex, label); } - private _setSegmentLabelForSegmentation( + private _setSegmentLabel( segmentationId: string, segmentIndex: number, label: string, @@ -1348,8 +1474,8 @@ class SegmentationService extends PubSubService { } } - public shouldRenderSegmentation(viewportDisplaySetInstanceUIDs, segDisplaySetInstanceUID) { - if (!viewportDisplaySetInstanceUIDs || !viewportDisplaySetInstanceUIDs.length) { + public shouldRenderSegmentation(viewportDisplaySetInstanceUIDs, segmentationFrameOfReferenceUID) { + if (!viewportDisplaySetInstanceUIDs?.length) { return false; } @@ -1357,10 +1483,6 @@ class SegmentationService extends PubSubService { let shouldDisplaySeg = false; - const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID); - - const segFrameOfReferenceUID = this._getFrameOfReferenceUIDForSeg(segDisplaySet); - // check if the displaySet is sharing the same frameOfReferenceUID // with the new segmentation for (const displaySetInstanceUID of viewportDisplaySetInstanceUIDs) { @@ -1370,7 +1492,7 @@ class SegmentationService extends PubSubService { // don't want to show the segmentation for all the frames if ( displaySet.isReconstructable && - displaySet?.images?.[0]?.FrameOfReferenceUID === segFrameOfReferenceUID + displaySet?.images?.[0]?.FrameOfReferenceUID === segmentationFrameOfReferenceUID ) { shouldDisplaySeg = true; break; @@ -1717,7 +1839,7 @@ class SegmentationService extends PubSubService { const segmentationRepresentations = this.getSegmentationRepresentationsForToolGroup(toolGroupId); - if (segmentationRepresentations.length === 0) { + if (!segmentationRepresentations?.length) { return; } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts index e170197c36..d6d231fd0a 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts @@ -39,6 +39,8 @@ type Segmentation = { isActive: boolean; // if the segmentation is visible in the viewer isVisible: boolean; + // the frame of reference UID of the segmentation + FrameOfReferenceUID: string; // the label of the segmentation label: string; // the number of segments in the segmentation diff --git a/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts index fc3d6c86c2..3a85342b38 100644 --- a/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts +++ b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts @@ -97,9 +97,9 @@ export default class ToolGroupService { } public getActiveToolForViewport(viewportId: string): string { - const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId); + const toolGroup = this.getToolGroupForViewport(viewportId); if (!toolGroup) { - return null; + return; } return toolGroup.getActivePrimaryMouseButtonTool(); @@ -184,13 +184,9 @@ export default class ToolGroupService { this._setToolsMode(toolGroup, tools); } - public createToolGroupAndAddTools( - toolGroupId: string, - tools: Array, - configs: any = {} - ): Types.IToolGroup { + public createToolGroupAndAddTools(toolGroupId: string, tools: Array): Types.IToolGroup { const toolGroup = this.createToolGroup(toolGroupId); - this.addToolsToToolGroup(toolGroupId, tools, configs); + this.addToolsToToolGroup(toolGroupId, tools); return toolGroup; } @@ -239,34 +235,6 @@ export default class ToolGroupService { toolInstance.configuration = config; } - private _getToolNames(toolGroupTools: Tools): string[] { - const toolNames = []; - if (toolGroupTools.active) { - toolGroupTools.active.forEach(tool => { - toolNames.push(tool.toolName); - }); - } - if (toolGroupTools.passive) { - toolGroupTools.passive.forEach(tool => { - toolNames.push(tool.toolName); - }); - } - - if (toolGroupTools.enabled) { - toolGroupTools.enabled.forEach(tool => { - toolNames.push(tool.toolName); - }); - } - - if (toolGroupTools.disabled) { - toolGroupTools.disabled.forEach(tool => { - toolNames.push(tool.toolName); - }); - } - - return toolNames; - } - private _setToolsMode(toolGroup, tools) { const { active, passive, enabled, disabled } = tools; @@ -295,17 +263,33 @@ export default class ToolGroupService { } } - private _addTools(toolGroup, tools, configs) { - const toolNames = this._getToolNames(tools); - toolNames.forEach(toolName => { - // Initialize the toolConfig if no configuration is provided - const toolConfig = configs[toolName] ?? {}; + private _addTools(toolGroup, tools) { + const addTools = tools => { + tools.forEach(({ toolName, parentTool, configuration }) => { + if (parentTool) { + toolGroup.addToolInstance(toolName, parentTool, { + ...configuration, + }); + } else { + toolGroup.addTool(toolName, { ...configuration }); + } + }); + }; - // if (volumeUID) { - // toolConfig.volumeUID = volumeUID; - // } + if (tools.active) { + addTools(tools.active); + } - toolGroup.addTool(toolName, { ...toolConfig }); - }); + if (tools.passive) { + addTools(tools.passive); + } + + if (tools.enabled) { + addTools(tools.enabled); + } + + if (tools.disabled) { + addTools(tools.disabled); + } } } diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 29ffe0d39d..f6d0978972 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -15,11 +15,7 @@ import { import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools'; import { IViewportService } from './IViewportService'; import { RENDERING_ENGINE_ID } from './constants'; -import ViewportInfo, { - ViewportOptions, - DisplaySetOptions, - PublicViewportOptions, -} from './Viewport'; +import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { Presentation, Presentations } from '../../types/Presentation'; @@ -586,10 +582,23 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi continue; } - // otherwise, check if the hydrated segmentations are in the same FOR + // otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID // as the primary displaySet, if so add the representation (since it was not there) - const { id: segDisplaySetInstanceUID, type } = segmentation; - const segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID); + const { id: segDisplaySetInstanceUID } = segmentation; + let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID); + + if (!segFrameOfReferenceUID) { + // if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the + // segmentation itself maybe it has a FrameOfReferenceUID + const { FrameOfReferenceUID } = segmentation; + if (FrameOfReferenceUID) { + segFrameOfReferenceUID = FrameOfReferenceUID; + } + } + + if (!segFrameOfReferenceUID) { + return; + } let shouldDisplaySeg = false; diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx index 2d402df5fb..e3dead9851 100644 --- a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx +++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx @@ -1,5 +1,6 @@ -import { metaData } from '@cornerstonejs/core'; +import { VolumeViewport, metaData } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/core'; +import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types'; import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools'; import { guid } from '@ohif/core/src/utils'; @@ -46,6 +47,15 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { this._cachedOverlayMetadata = new Map(); }; + protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string { + if (viewport instanceof VolumeViewport) { + return; + } + + const targetId = this.getTargetId(viewport); + return targetId.split('imageId:')[1]; + } + renderAnnotation = (enabledElement, svgDrawingHelper) => { const { viewport } = enabledElement; diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js index cd52871e8a..79cb2053ec 100644 --- a/extensions/cornerstone/src/utils/dicomLoaderService.js +++ b/extensions/cornerstone/src/utils/dicomLoaderService.js @@ -97,7 +97,7 @@ class DicomLoaderService { if ( (!imageInstance && !nonImageInstance) || - !nonImageInstance.imageId.startsWith('dicomfile') + !nonImageInstance.imageId?.startsWith('dicomfile') ) { return; } diff --git a/extensions/default/CHANGELOG.md b/extensions/default/CHANGELOG.md index 4d16968669..f6e509f37b 100644 --- a/extensions/default/CHANGELOG.md +++ b/extensions/default/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-default diff --git a/extensions/default/package.json b/extensions/default/package.json index b53dbd07b1..1bea87696f 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-default", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Common/default features and functionality for basic image viewing", "author": "OHIF Core Team", "license": "MIT", @@ -30,8 +30,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/i18n": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/i18n": "3.7.0-beta.80", "dcmjs": "^0.29.5", "dicomweb-client": "^0.10.2", "prop-types": "^15.6.2", diff --git a/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx index af9520917c..d0f34a48a2 100644 --- a/extensions/default/src/Actions/createReportAsync.tsx +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -4,49 +4,31 @@ import { DicomMetadataStore } from '@ohif/core'; /** * * @param {*} servicesManager - * @param {*} dataSource - * @param {*} measurements - * @param {*} options - * @returns {string[]} displaySetInstanceUIDs */ -async function createReportAsync( - servicesManager, - commandsManager, - dataSource, - measurements, - options -) { +async function createReportAsync({ servicesManager, getReport, reportType = 'measurement' }) { const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services; const loadingDialogId = uiDialogService.create({ showOverlay: true, isDraggable: false, centralize: true, - // TODO: Create a loading indicator component + zeplin design? content: Loading, }); try { - const naturalizedReport = await commandsManager.runCommand( - 'storeMeasurements', - { - measurementData: measurements, - dataSource, - additionalFindingTypes: ['ArrowAnnotate'], - options, - }, - 'CORNERSTONE_STRUCTURED_REPORT' - ); + const naturalizedReport = await getReport(); // The "Mode" route listens for DicomMetadataStore changes // When a new instance is added, it listens and // automatically calls makeDisplaySets DicomMetadataStore.addInstances([naturalizedReport], true); - const displaySetInstanceUID = displaySetService.getMostRecentDisplaySet(); + const displaySet = displaySetService.getMostRecentDisplaySet(); + + const displaySetInstanceUID = displaySet.displaySetInstanceUID; uiNotificationService.show({ title: 'Create Report', - message: 'Measurements saved successfully', + message: `${reportType} saved successfully`, type: 'success', }); @@ -54,7 +36,7 @@ async function createReportAsync( } catch (error) { uiNotificationService.show({ title: 'Create Report', - message: error.message || 'Failed to store measurements', + message: error.message || `Failed to store ${reportType}`, type: 'error', }); } finally { diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx new file mode 100644 index 0000000000..23e9841b06 --- /dev/null +++ b/extensions/default/src/Components/SidePanelWithServices.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react'; +import { SidePanel } from '@ohif/ui'; +import { PanelService, ServicesManager } from '@ohif/core'; + +export type SidePanelWithServicesProps = { + servicesManager: ServicesManager; + side: 'left' | 'right'; + className: string; + activeTabIndex: number; + tabs: any; +}; + +const SidePanelWithServices = ({ + servicesManager, + side, + className, + activeTabIndex: activeTabIndexProp, + tabs, +}) => { + const panelService: PanelService = servicesManager?.services?.panelService; + + // Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM. + // Thus going to the Study List page and back to the viewer resets this flag for a SidePanel. + const [hasBeenOpened, setHasBeenOpened] = useState(false); + const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp); + + useEffect(() => { + if (panelService) { + const activatePanelSubscription = panelService.subscribe( + panelService.EVENTS.ACTIVATE_PANEL, + (activatePanelEvent: Types.ActivatePanelEvent) => { + if (!hasBeenOpened || activatePanelEvent.forceActive) { + const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId); + if (tabIndex !== -1) { + setActiveTabIndex(tabIndex); + } + } + } + ); + + return () => { + activatePanelSubscription.unsubscribe(); + }; + } + }, [tabs, hasBeenOpened, panelService]); + + return ( + { + setHasBeenOpened(true); + }} + > + ); +}; + +export default SidePanelWithServices; diff --git a/extensions/default/src/DicomJSONDataSource/index.js b/extensions/default/src/DicomJSONDataSource/index.js index d75ab67e5a..c4bfb75ce6 100644 --- a/extensions/default/src/DicomJSONDataSource/index.js +++ b/extensions/default/src/DicomJSONDataSource/index.js @@ -119,18 +119,18 @@ function createDicomJSONApi(dicomJsonConfig) { }); }, processResults: () => { - console.debug(' DICOMJson QUERY processResults'); + console.warn(' DICOMJson QUERY processResults not implemented'); }, }, series: { // mapParams: mapParams.bind(), search: () => { - console.debug(' DICOMJson QUERY SERIES SEARCH'); + console.warn(' DICOMJson QUERY SERIES SEARCH not implemented'); }, }, instances: { search: () => { - console.debug(' DICOMJson QUERY instances SEARCH'); + console.warn(' DICOMJson QUERY instances SEARCH not implemented'); }, }, }, @@ -211,7 +211,7 @@ function createDicomJSONApi(dicomJsonConfig) { }, store: { dicom: () => { - console.debug(' DICOMJson store dicom'); + console.warn(' DICOMJson store dicom not implemented'); }, }, getImageIdsForDisplaySet(displaySet) { diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index 712acd0578..a7628e80a9 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -85,7 +85,7 @@ function createDicomLocalApi(dicomLocalConfig) { }); }, processResults: () => { - console.debug(' DICOMLocal QUERY processResults'); + console.warn(' DICOMLocal QUERY processResults not implemented'); }, }, series: { @@ -107,7 +107,7 @@ function createDicomLocalApi(dicomLocalConfig) { }, instances: { search: () => { - console.debug(' DICOMLocal QUERY instances SEARCH'); + console.warn(' DICOMLocal QUERY instances SEARCH not implemented'); }, }, }, diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 4699051d52..f5e300647e 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -226,7 +226,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { await clientManager.getWadoClient(clientName).storeInstances(options); } else { const meta = { - FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value, + FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, MediaStorageSOPClassUID: dataset.SOPClassUID, MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, @@ -326,6 +326,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }); instance.imageId = imageId; + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; metadataProvider.addImageIdToUIDs(imageId, { StudyInstanceUID, diff --git a/extensions/default/src/Panels/ActionButtons.tsx b/extensions/default/src/Panels/ActionButtons.tsx index 9382a2451d..c21f8b6b65 100644 --- a/extensions/default/src/Panels/ActionButtons.tsx +++ b/extensions/default/src/Panels/ActionButtons.tsx @@ -2,18 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; -import { LegacyButton, ButtonGroup } from '@ohif/ui'; +import { LegacyButton, LegacyButtonGroup } from '@ohif/ui'; function ActionButtons({ onExportClick, onCreateReportClick }) { const { t } = useTranslation('MeasurementTable'); return ( - - {/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/} + {/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/} {t('Create Report')} - + ); } diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index 65b3701815..791d878aa2 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -103,13 +103,20 @@ export default function PanelMeasurementTable({ // creating too many series instances. const options = findSRWithSameSeriesDescription(SeriesDescription, displaySetService); - return createReportAsync( - servicesManager, - commandsManager, - dataSource, - trackedMeasurements, - options - ); + const getReport = async () => { + return commandsManager.runCommand( + 'storeMeasurements', + { + measurementData: trackedMeasurements, + dataSource, + additionalFindingTypes: ['ArrowAnnotate'], + options, + }, + 'CORNERSTONE_STRUCTURED_REPORT' + ); + }; + + return createReportAsync({ servicesManager, getReport }); } } diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index d868730d9f..7498dc3347 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -103,8 +103,7 @@ function PanelStudyBrowser({ } StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [StudyInstanceUIDs, getStudiesForPatientByMRN]); + }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); // // ~~ Initial Thumbnails useEffect(() => { @@ -116,21 +115,23 @@ function PanelStudyBrowser({ const imageId = imageIds[Math.floor(imageIds.length / 2)]; // TODO: Is it okay that imageIds are not returned here for SR displaySets? - if (imageId && !displaySet?.unsupported) { - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); - if (isMounted.current) { - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); - } + if (!imageId || displaySet?.unsupported) { + return; + } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); + if (!isMounted.current) { + return; } + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); }); return () => { isMounted.current = false; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]); // ~~ displaySets useEffect(() => { @@ -140,8 +141,7 @@ function PanelStudyBrowser({ sortStudyInstances(mappedDisplaySets); setDisplaySets(mappedDisplaySets); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [thumbnailImageSrcMap]); + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); // ~~ subscriptions --> displaySets useEffect(() => { @@ -149,32 +149,40 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { - const { displaySetsAdded } = data; + const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); - if (!displaySet?.unsupported) { - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - const imageId = imageIds[Math.floor(imageIds.length / 2)]; - - // TODO: Is it okay that imageIds are not returned here for SR displaysets? - if (imageId) { - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc( - imageId, - dSet.initialViewport - ); - if (isMounted.current) { - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); - } - } + if (displaySet?.unsupported) { + return; + } + + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + const imageId = imageIds[Math.floor(imageIds.length / 2)]; + + // TODO: Is it okay that imageIds are not returned here for SR displaysets? + if (!imageId) { + return; } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc( + imageId, + dSet.initialViewport + ); + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); }); } ); + return () => { + SubscriptionDisplaySetsAdded.unsubscribe(); + }; + }, [getImageSrc, dataSource, displaySetService]); + + useEffect(() => { // TODO: Will this always hold _all_ the displaySets we care about? // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` const SubscriptionDisplaySetsChanged = displaySetService.subscribe( @@ -198,12 +206,10 @@ function PanelStudyBrowser({ ); return () => { - SubscriptionDisplaySetsAdded.unsubscribe(); SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); const tabs = _createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); diff --git a/extensions/default/src/Panels/index.js b/extensions/default/src/Panels/index.js index a5a81473ca..e81fdc304a 100644 --- a/extensions/default/src/Panels/index.js +++ b/extensions/default/src/Panels/index.js @@ -1,5 +1,11 @@ import PanelStudyBrowser from './PanelStudyBrowser'; import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser'; import PanelMeasurementTable from './PanelMeasurementTable'; +import createReportDialogPrompt from './createReportDialogPrompt'; -export { PanelStudyBrowser, WrappedPanelStudyBrowser, PanelMeasurementTable }; +export { + PanelStudyBrowser, + WrappedPanelStudyBrowser, + PanelMeasurementTable, + createReportDialogPrompt, +}; diff --git a/extensions/default/src/Toolbar/Toolbar.tsx b/extensions/default/src/Toolbar/Toolbar.tsx index 51aa20f7cd..c714968a1d 100644 --- a/extensions/default/src/Toolbar/Toolbar.tsx +++ b/extensions/default/src/Toolbar/Toolbar.tsx @@ -1,51 +1,29 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import classnames from 'classnames'; export default function Toolbar({ servicesManager }) { const { toolbarService } = servicesManager.services; const [toolbarButtons, setToolbarButtons] = useState([]); - const [buttonState, setButtonState] = useState({ - primaryToolId: '', - toggles: {}, - groups: {}, - }); - // Could track buttons and state separately...? useEffect(() => { - const { unsubscribe: unsub1 } = toolbarService.subscribe( - toolbarService.EVENTS.TOOL_BAR_MODIFIED, - () => setToolbarButtons(toolbarService.getButtonSection('primary')) - ); - const { unsubscribe: unsub2 } = toolbarService.subscribe( - toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED, - () => setButtonState({ ...toolbarService.state }) + const { unsubscribe } = toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, () => + setToolbarButtons(toolbarService.getButtonSection('primary')) ); return () => { - unsub1(); - unsub2(); + unsubscribe(); }; }, [toolbarService]); + const onInteraction = useCallback( + args => toolbarService.recordInteraction(args), + [toolbarService] + ); + return ( <> {toolbarButtons.map(toolDef => { const { id, Component, componentProps } = toolDef; - // TODO: ... - - // isActive if: - // - id is primary? - // - id is in list of "toggled on"? - let isActive; - if (componentProps.type === 'toggle') { - isActive = buttonState.toggles[id]; - } - // Also need... to filter list for splitButton, and set primary based on most recently clicked - // Also need to kill the radioGroup button's magic logic - // Everything should be reactive off these props, so commands can inform ToolbarService - - // These can... Trigger toolbar events based on updates? - // Then sync using useEffect, or simply modify the state here? return ( // The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item. // This allows for the individual items to be included in other UI components where perhaps alternative margins are desired. @@ -56,9 +34,7 @@ export default function Toolbar({ servicesManager }) { toolbarService.recordInteraction(args)} + onInteraction={onInteraction} servicesManager={servicesManager} /> diff --git a/extensions/default/src/Toolbar/ToolbarButtonWithServices.tsx b/extensions/default/src/Toolbar/ToolbarButtonWithServices.tsx new file mode 100644 index 0000000000..24dda712b4 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarButtonWithServices.tsx @@ -0,0 +1,75 @@ +import { ToolbarButton } from '@ohif/ui'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +function ToolbarButtonWithServices({ + id, + type, + commands, + onInteraction, + servicesManager, + ...props +}) { + const { toolbarService } = servicesManager?.services || {}; + + const [buttonsState, setButtonState] = useState({ + primaryToolId: '', + toggles: {}, + groups: {}, + }); + const { primaryToolId } = buttonsState; + + const isActive = + (type === 'tool' && id === primaryToolId) || + (type === 'toggle' && buttonsState.toggles[id] === true); + + useEffect(() => { + const { unsubscribe } = toolbarService.subscribe( + toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED, + state => { + setButtonState({ ...state }); + } + ); + + return () => { + unsubscribe(); + }; + }, [toolbarService]); + + return ( + + ); +} + +ToolbarButtonWithServices.propTypes = { + id: PropTypes.string.isRequired, + type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired, + commands: PropTypes.arrayOf( + PropTypes.shape({ + commandName: PropTypes.string.isRequired, + context: PropTypes.string, + }) + ), + onInteraction: PropTypes.func.isRequired, + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + toolbarService: PropTypes.shape({ + subscribe: PropTypes.func.isRequired, + state: PropTypes.shape({ + primaryToolId: PropTypes.string, + toggles: PropTypes.objectOf(PropTypes.bool), + groups: PropTypes.objectOf(PropTypes.object), + }).isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; + +export default ToolbarButtonWithServices; diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx index 9db937fec2..430cd5abae 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx @@ -1,13 +1,37 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui'; - import { ServicesManager } from '@ohif/core'; -function LayoutSelector({ rows, columns, className, servicesManager, ...rest }) { - const [isOpen, setIsOpen] = useState(false); +function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) { + const { toolbarService } = servicesManager.services; + + const onSelection = useCallback( + props => { + toolbarService.recordInteraction({ + interactionType: 'action', + commands: [ + { + commandName: 'setViewportGridLayout', + commandOptions: { ...props }, + context: 'DEFAULT', + }, + ], + }); + }, + [toolbarService] + ); + + return ( + + ); +} - const { hangingProtocolService, toolbarService } = (servicesManager as ServicesManager).services; +function LayoutSelector({ rows, columns, className, onSelection, ...rest }) { + const [isOpen, setIsOpen] = useState(false); const closeOnOutsideClick = () => { if (isOpen) { @@ -15,19 +39,6 @@ function LayoutSelector({ rows, columns, className, servicesManager, ...rest }) } }; - useEffect(() => { - const { unsubscribe } = hangingProtocolService.subscribe( - hangingProtocolService.EVENTS.PROTOCOL_CHANGED, - evt => { - const { protocol } = evt; - } - ); - - return () => { - unsubscribe(); - }; - }, [hangingProtocolService]); - useEffect(() => { window.addEventListener('click', closeOnOutsideClick); return () => { @@ -38,19 +49,6 @@ function LayoutSelector({ rows, columns, className, servicesManager, ...rest }) const onInteractionHandler = () => setIsOpen(!isOpen); const DropdownContent = isOpen ? OHIFLayoutSelector : null; - const onSelectionHandler = props => { - toolbarService.recordInteraction({ - interactionType: 'action', - commands: [ - { - commandName: 'setViewportGridLayout', - commandOptions: { ...props }, - context: 'DEFAULT', - }, - ], - }); - }; - return ( ) } @@ -87,4 +85,4 @@ LayoutSelector.defaultProps = { onLayoutChange: () => {}, }; -export default LayoutSelector; +export default ToolbarLayoutSelectorWithServices; diff --git a/extensions/default/src/Toolbar/ToolbarSplitButton.tsx b/extensions/default/src/Toolbar/ToolbarSplitButton.tsx deleted file mode 100644 index 484e8f97dc..0000000000 --- a/extensions/default/src/Toolbar/ToolbarSplitButton.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { SplitButton } from '@ohif/ui'; - -export default SplitButton; diff --git a/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx b/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx new file mode 100644 index 0000000000..a8943b16fa --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx @@ -0,0 +1,186 @@ +import { SplitButton, Icon, ToolbarButton } from '@ohif/ui'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +function ToolbarSplitButtonWithServices({ + isRadio, + isAction, + groupId, + primary, + secondary, + items, + renderer, + onInteraction, + servicesManager, +}) { + const { toolbarService } = servicesManager?.services; + + const handleItemClick = (item, index) => { + const { id, type, commands } = item; + onInteraction({ + groupId, + itemId: id, + interactionType: type, + commands, + }); + + setState(state => ({ + ...state, + primary: !isAction && isRadio ? { ...item, index } : state.primary, + isExpanded: false, + items: getSplitButtonItems(items).filter(item => + isRadio && !isAction ? item.index !== index : true + ), + })); + }; + + /* Bubbles up individual item clicks */ + const getSplitButtonItems = items => + items.map((item, index) => ({ + ...item, + index, + onClick: () => handleItemClick(item, index), + })); + + const [buttonsState, setButtonState] = useState({ + primaryToolId: '', + toggles: {}, + groups: {}, + }); + + const [state, setState] = useState({ + primary, + items: getSplitButtonItems(items).filter(item => + isRadio && !isAction ? item.id !== primary.id : true + ), + }); + + const { primaryToolId, toggles } = buttonsState; + + const isPrimaryToggle = state.primary.type === 'toggle'; + + const isPrimaryActive = + (state.primary.type === 'tool' && primaryToolId === state.primary.id) || + (isPrimaryToggle && toggles[state.primary.id] === true); + + const PrimaryButtonComponent = + toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton; + + useEffect(() => { + const { unsubscribe } = toolbarService.subscribe( + toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED, + state => { + setButtonState({ ...state }); + } + ); + + return () => { + unsubscribe(); + }; + }, [toolbarService]); + + const updatedItems = state.items.map(item => { + const isActive = item.type === 'tool' && primaryToolId === item.id; + + // We could have added the + // item.type === 'toggle' && toggles[item.id] === true + // too but that makes the button active when the toggle is active under it + // which feels weird + return { + ...item, + isActive, + }; + }); + + const DefaultListItemRenderer = ({ type, icon, label, t, id }) => { + const isActive = type === 'toggle' && toggles[id] === true; + + return ( +
+ {icon && ( + + + + )} + {t(label)} +
+ ); + }; + + const listItemRenderer = renderer || DefaultListItemRenderer; + + return ( + item.isActive)} + isToggle={isPrimaryToggle} + onInteraction={onInteraction} + Component={props => ( + + )} + /> + ); +} + +ToolbarSplitButtonWithServices.propTypes = { + isRadio: PropTypes.bool, + isAction: PropTypes.bool, + groupId: PropTypes.string, + primary: PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired, + uiType: PropTypes.string, + }), + secondary: PropTypes.shape({ + id: PropTypes.string, + icon: PropTypes.string.isRequired, + label: PropTypes.string, + tooltip: PropTypes.string.isRequired, + isActive: PropTypes.bool, + }), + items: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired, + icon: PropTypes.string, + label: PropTypes.string, + tooltip: PropTypes.string, + }) + ), + renderer: PropTypes.func, + onInteraction: PropTypes.func.isRequired, + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + toolbarService: PropTypes.object, + }), + }), +}; + +ToolbarSplitButtonWithServices.defaultProps = { + isRadio: false, + isAction: false, +}; + +export default ToolbarSplitButtonWithServices; diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index d7e4335ca8..e045bca33c 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -5,6 +5,7 @@ import { SidePanel, ErrorBoundary, LoadingIndicatorProgress } from '@ohif/ui'; import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core'; import { useAppConfig } from '@state'; import ViewerHeader from './ViewerHeader'; +import SidePanelWithServices from '../Components/SidePanelWithServices'; function ViewerLayout({ // From Extension Module Params @@ -44,7 +45,7 @@ function ViewerLayout({ if (!entry) { throw new Error( - `${id} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.` + `${id} is not valid for an extension module. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file)` ); } @@ -119,7 +120,7 @@ function ViewerLayout({ {/* LEFT SIDEPANELS */} {leftPanelComponents.length ? ( - {rightPanelComponents.length ? ( - {}, }, { name: 'ohif.radioGroup', - defaultComponent: ToolbarButton, + defaultComponent: ToolbarButtonWithServices, clickHandler: () => {}, }, { name: 'ohif.splitButton', - defaultComponent: ToolbarSplitButton, + defaultComponent: ToolbarSplitButtonWithServices, clickHandler: () => {}, }, { name: 'ohif.layoutSelector', - defaultComponent: ToolbarLayoutSelector, + defaultComponent: ToolbarLayoutSelectorWithServices, clickHandler: (evt, clickedBtn, btnSectionName) => {}, }, { name: 'ohif.toggle', - defaultComponent: ToolbarButton, + defaultComponent: ToolbarButtonWithServices, clickHandler: () => {}, }, ]; diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index 25c37bc4bb..aa46624436 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -13,6 +13,8 @@ import { id } from './id.js'; import preRegistration from './init'; import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu'; import * as dicomWebUtils from './DicomWebDataSource/utils'; +import { createReportDialogPrompt } from './Panels'; +import createReportAsync from './Actions/createReportAsync'; const defaultExtension: Types.Extensions.Extension = { /** @@ -48,4 +50,6 @@ export { CustomizableContextMenuTypes, getStudiesForPatientByMRN, dicomWebUtils, + createReportDialogPrompt, + createReportAsync, }; diff --git a/extensions/dicom-microscopy/CHANGELOG.md b/extensions/dicom-microscopy/CHANGELOG.md index 86119eabb4..5b29c16cc2 100644 --- a/extensions/dicom-microscopy/CHANGELOG.md +++ b/extensions/dicom-microscopy/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-dicom-microscopy diff --git a/extensions/dicom-microscopy/package.json b/extensions/dicom-microscopy/package.json index f2bf69c962..53215d11b3 100644 --- a/extensions/dicom-microscopy/package.json +++ b/extensions/dicom-microscopy/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-dicom-microscopy", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for DICOM microscopy", "author": "Bill Wallace, md-prog", "license": "MIT", @@ -28,10 +28,10 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/i18n": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/i18n": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx index 51ed6c6999..c57df827a6 100644 --- a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx +++ b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx @@ -338,29 +338,6 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) { onEdit={onMeasurementItemEditHandler} /> -
- - {/* Let's hide the save button for now, as export SR for SM is a proof of concept */} - {/*{promptSave && ( - - )} */} - {/* */} - -
); } diff --git a/extensions/dicom-microscopy/src/utils/constructSR.ts b/extensions/dicom-microscopy/src/utils/constructSR.ts index 340e66d1ed..07e50943e6 100644 --- a/extensions/dicom-microscopy/src/utils/constructSR.ts +++ b/extensions/dicom-microscopy/src/utils/constructSR.ts @@ -60,10 +60,10 @@ export default function constructSR(metadata, { SeriesDescription, SeriesNumber const { roiGraphic: roi, label } = annotations[i]; let { measurements, evaluations, marker, presentationState } = roi.properties; - console.debug('[SR] storing marker...', marker); - console.debug('[SR] storing measurements...', measurements); - console.debug('[SR] storing evaluations...', evaluations); - console.debug('[SR] storing presentation state...', presentationState); + console.log('[SR] storing marker...', marker); + console.log('[SR] storing measurements...', measurements); + console.log('[SR] storing evaluations...', evaluations); + console.log('[SR] storing presentation state...', presentationState); if (presentationState) { presentationState.marker = marker; diff --git a/extensions/dicom-microscopy/src/utils/loadSR.js b/extensions/dicom-microscopy/src/utils/loadSR.js index cf9509b818..c300b6f5e8 100644 --- a/extensions/dicom-microscopy/src/utils/loadSR.js +++ b/extensions/dicom-microscopy/src/utils/loadSR.js @@ -149,12 +149,12 @@ async function _getROIsFromToolState(naturalizedDataset, FrameOfReferenceUID) { if (measurements && measurements.length) { properties.measurements = measurements; - console.debug('[SR] retrieving measurements...', measurements); + console.log('[SR] retrieving measurements...', measurements); } if (evaluations && evaluations.length) { properties.evaluations = evaluations; - console.debug('[SR] retrieving evaluations...', evaluations); + console.log('[SR] retrieving evaluations...', evaluations); } const roi = new DICOMMicroscopyViewer.roi.ROI({ scoord3d, properties }); diff --git a/extensions/dicom-pdf/CHANGELOG.md b/extensions/dicom-pdf/CHANGELOG.md index c5569ec709..7f15852b6d 100644 --- a/extensions/dicom-pdf/CHANGELOG.md +++ b/extensions/dicom-pdf/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-dicom-pdf diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 8fa2da4c74..e43fcdce86 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-dicom-pdf", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for PDF display", "author": "OHIF", "license": "MIT", @@ -28,8 +28,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "^0.29.5", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", diff --git a/extensions/dicom-video/CHANGELOG.md b/extensions/dicom-video/CHANGELOG.md index 8ab968ca5d..99314792b5 100644 --- a/extensions/dicom-video/CHANGELOG.md +++ b/extensions/dicom-video/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-dicom-video diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index 1a45d063ce..36a355d326 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-dicom-video", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for video display", "author": "OHIF", "license": "MIT", @@ -28,8 +28,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "^0.29.5", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", diff --git a/extensions/measurement-tracking/CHANGELOG.md b/extensions/measurement-tracking/CHANGELOG.md index 8bfa684319..d82e4f5cc3 100644 --- a/extensions/measurement-tracking/CHANGELOG.md +++ b/extensions/measurement-tracking/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-measurement-tracking diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index b4e01225e0..9c204247d1 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-measurement-tracking", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Tracking features and functionality for basic image viewing", "author": "OHIF Core Team", "license": "MIT", @@ -30,11 +30,11 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^1.13.2", - "@cornerstonejs/tools": "^1.13.2", - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@cornerstonejs/core": "^1.16.5", + "@cornerstonejs/tools": "^1.16.5", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "classnames": "^2.3.2", "dcmjs": "^0.29.5", "lodash.debounce": "^4.17.21", @@ -46,7 +46,7 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/ui": "3.7.0-beta.80", "@xstate/react": "^3.2.2", "xstate": "^4.10.0" } diff --git a/extensions/measurement-tracking/src/_shared/createReportAsync.tsx b/extensions/measurement-tracking/src/_shared/createReportAsync.tsx deleted file mode 100644 index af9520917c..0000000000 --- a/extensions/measurement-tracking/src/_shared/createReportAsync.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { DicomMetadataStore } from '@ohif/core'; - -/** - * - * @param {*} servicesManager - * @param {*} dataSource - * @param {*} measurements - * @param {*} options - * @returns {string[]} displaySetInstanceUIDs - */ -async function createReportAsync( - servicesManager, - commandsManager, - dataSource, - measurements, - options -) { - const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services; - const loadingDialogId = uiDialogService.create({ - showOverlay: true, - isDraggable: false, - centralize: true, - // TODO: Create a loading indicator component + zeplin design? - content: Loading, - }); - - try { - const naturalizedReport = await commandsManager.runCommand( - 'storeMeasurements', - { - measurementData: measurements, - dataSource, - additionalFindingTypes: ['ArrowAnnotate'], - options, - }, - 'CORNERSTONE_STRUCTURED_REPORT' - ); - - // The "Mode" route listens for DicomMetadataStore changes - // When a new instance is added, it listens and - // automatically calls makeDisplaySets - DicomMetadataStore.addInstances([naturalizedReport], true); - - const displaySetInstanceUID = displaySetService.getMostRecentDisplaySet(); - - uiNotificationService.show({ - title: 'Create Report', - message: 'Measurements saved successfully', - type: 'success', - }); - - return [displaySetInstanceUID]; - } catch (error) { - uiNotificationService.show({ - title: 'Create Report', - message: error.message || 'Failed to store measurements', - type: 'error', - }); - } finally { - uiDialogService.dismiss({ id: loadingDialogId }); - } -} - -function Loading() { - return
Loading...
; -} - -export default createReportAsync; diff --git a/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.tsx b/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.tsx deleted file mode 100644 index 33f13b2034..0000000000 --- a/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable react/display-name */ -import React from 'react'; -import { ButtonEnums, Dialog, Input } from '@ohif/ui'; -import RESPONSE from './PROMPT_RESPONSES'; - -export default function createReportDialogPrompt(uiDialogService) { - return new Promise(function (resolve, reject) { - let dialogId = undefined; - - const _handleClose = () => { - // Dismiss dialog - uiDialogService.dismiss({ id: dialogId }); - // Notify of cancel action - resolve({ action: RESPONSE.CANCEL, value: undefined }); - }; - - /** - * - * @param {string} param0.action - value of action performed - * @param {string} param0.value - value from input field - */ - const _handleFormSubmit = ({ action, value }) => { - switch (action.id) { - case 'save': - // Only save if description is not blank otherwise ignore - if (value.label && value.label.trim() !== '') { - resolve({ - action: RESPONSE.CREATE_REPORT, - value: value.label.trim(), - }); - uiDialogService.dismiss({ id: dialogId }); - } - break; - case 'cancel': - uiDialogService.dismiss({ id: dialogId }); - resolve({ action: RESPONSE.CANCEL, value: undefined }); - break; - } - }; - - dialogId = uiDialogService.create({ - centralize: true, - isDraggable: false, - content: Dialog, - useLastPosition: false, - showOverlay: true, - contentProps: { - title: 'Create Report', - value: { label: '' }, - noCloseButton: true, - onClose: _handleClose, - actions: [ - { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, - { id: 'save', text: 'Save', type: ButtonEnums.type.primary }, - ], - // TODO: Should be on button press... - onSubmit: _handleFormSubmit, - body: ({ value, setValue }) => { - const onChangeHandler = event => { - event.persist(); - setValue(value => ({ ...value, label: event.target.value })); - }; - const onKeyPressHandler = event => { - if (event.key === 'Enter') { - // Trigger form submit - _handleFormSubmit({ action: { id: 'save' }, value }); - } - }; - return ( -
- -
- ); - }, - }, - }); - }); -} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index d750909a80..bc83efa7db 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -35,7 +35,7 @@ function TrackedMeasurementsContextProvider( const machineOptions = Object.assign({}, defaultOptions); machineOptions.actions = Object.assign({}, machineOptions.actions, { jumpToFirstMeasurementInActiveViewport: (ctx, evt) => { - const { trackedStudy, trackedSeries } = ctx; + const { trackedStudy, trackedSeries, activeViewportId } = ctx; const measurements = measurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) @@ -43,7 +43,7 @@ function TrackedMeasurementsContextProvider( console.log( 'jumping to measurement reset viewport', - viewportGrid.activeViewportId, + activeViewportId, trackedMeasurements[0] ); @@ -71,7 +71,7 @@ function TrackedMeasurementsContextProvider( } viewportGridService.setDisplaySetsForViewport({ - viewportId: viewportGrid.activeViewportId, + viewportId: activeViewportId, displaySetInstanceUIDs: [referencedDisplaySetUID], viewportOptions: { initialImageOptions: { @@ -82,8 +82,7 @@ function TrackedMeasurementsContextProvider( }, showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { if (evt.data.createdDisplaySetInstanceUIDs.length > 0) { - const StructuredReportDisplaySetInstanceUID = - evt.data.createdDisplaySetInstanceUIDs[0].displaySetInstanceUID; + const StructuredReportDisplaySetInstanceUID = evt.data.createdDisplaySetInstanceUIDs[0]; viewportGridService.setDisplaySetsForViewport({ viewportId: evt.data.viewportId, @@ -160,6 +159,13 @@ function TrackedMeasurementsContextProvider( measurementTrackingMachine ); + useEffect(() => { + // Update the state machine with the active viewport ID + sendTrackedMeasurementsEvent('UPDATE_ACTIVE_VIEWPORT_ID', { + activeViewportId, + }); + }, [activeViewportId, sendTrackedMeasurementsEvent]); + // ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle useEffect(() => { if (viewports.size > 0) { diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 494a51d02b..2b0b65ca15 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -15,6 +15,7 @@ const machineConfiguration = { id: 'measurementTracking', initial: 'idle', context: { + activeViewportId: null, trackedStudy: '', trackedSeries: [], ignoredSeries: [], @@ -47,6 +48,11 @@ const machineConfiguration = { }, RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport', HYDRATE_SR: 'hydrateStructuredReport', + UPDATE_ACTIVE_VIEWPORT_ID: { + actions: assign({ + activeViewportId: (_, event) => event.activeViewportId, + }), + }, }, }, promptBeginTracking: { diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js index e247573760..1719c56dd3 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js @@ -1,5 +1,4 @@ -import createReportAsync from './../../_shared/createReportAsync'; -import createReportDialogPrompt from '../../_shared/createReportDialogPrompt'; +import { createReportAsync, createReportDialogPrompt } from '@ohif/extension-default'; import getNextSRSeriesNumber from '../../_shared/getNextSRSeriesNumber'; import RESPONSE from '../../_shared/PROMPT_RESPONSES'; @@ -15,7 +14,9 @@ function promptSaveReport({ servicesManager, commandsManager, extensionManager } return new Promise(async function (resolve, reject) { // TODO: Fallback if (uiDialogService) { - const promptResult = await createReportDialogPrompt(uiDialogService); + const promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); if (promptResult.action === RESPONSE.CREATE_REPORT) { const dataSources = extensionManager.getDataSources(); @@ -33,16 +34,25 @@ function promptSaveReport({ servicesManager, commandsManager, extensionManager } const SeriesNumber = getNextSRSeriesNumber(displaySetService); - displaySetInstanceUIDs = await createReportAsync( + const getReport = async () => { + return commandsManager.runCommand( + 'storeMeasurements', + { + measurementData: trackedMeasurements, + dataSource, + additionalFindingTypes: ['ArrowAnnotate'], + options: { + SeriesDescription, + SeriesNumber, + }, + }, + 'CORNERSTONE_STRUCTURED_REPORT' + ); + }; + displaySetInstanceUIDs = await createReportAsync({ servicesManager, - commandsManager, - dataSource, - trackedMeasurements, - { - SeriesDescription, - SeriesNumber, - } - ); + getReport, + }); } else if (promptResult.action === RESPONSE.CANCEL) { // Do nothing } diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx index 36fa7651c4..6d7a906333 100644 --- a/extensions/measurement-tracking/src/getPanelModule.tsx +++ b/extensions/measurement-tracking/src/getPanelModule.tsx @@ -9,7 +9,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }): return [ { name: 'seriesList', - iconName: 'group-layers', + iconName: 'tab-studies', iconLabel: 'Studies', label: 'Studies', component: PanelStudyBrowserTracking.bind(null, { diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 578ccdf9de..f40934e785 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -131,14 +131,16 @@ function PanelStudyBrowserTracking({ const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = imageIds[Math.floor(imageIds.length / 2)]; - // TODO: Is it okay that imageIds are not returned here for SR displaysets? - if (imageId && !displaySet?.unsupported) { - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); + // TODO: Is it okay that imageIds are not returned here for SR displaySets? + if (!imageId || displaySet?.unsupported) { + return; } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); }); }, [displaySetService, dataSource, getImageSrc]); @@ -184,27 +186,38 @@ function PanelStudyBrowserTracking({ const newImageSrcEntry = {}; const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - if (!displaySet?.unsupported) { - if (options.madeInClient) { - setJumpToDisplaySet(displaySetInstanceUID); - } - - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - const imageId = imageIds[Math.floor(imageIds.length / 2)]; - - // TODO: Is it okay that imageIds are not returned here for SR displaysets? - if (imageId) { - // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId); - setThumbnailImageSrcMap(prevState => { - return { ...prevState, ...newImageSrcEntry }; - }); - } + if (displaySet?.unsupported) { + return; + } + + if (options.madeInClient) { + setJumpToDisplaySet(displaySetInstanceUID); } + + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + const imageId = imageIds[Math.floor(imageIds.length / 2)]; + + // TODO: Is it okay that imageIds are not returned here for SR displaysets? + if (!imageId) { + return; + } + + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId); + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); }); } ); + return () => { + SubscriptionDisplaySetsAdded.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]); + + useEffect(() => { // TODO: Will this always hold _all_ the displaySets we care about? // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` const SubscriptionDisplaySetsChanged = displaySetService.subscribe( @@ -246,12 +259,10 @@ function PanelStudyBrowserTracking({ ); return () => { - SubscriptionDisplaySetsAdded.unsubscribe(); SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]); + }, [thumbnailImageSrcMap, trackedSeries, viewports, dataSource, displaySetService]); const tabs = _createStudyBrowserTabs( StudyInstanceUIDs, @@ -445,7 +456,7 @@ function _mapDisplaySets( body: () => (

Are you sure you want to delete this report?

-

This action cannot be undone.

+

This action cannot be undone.

), actions: [ diff --git a/extensions/test-extension/CHANGELOG.md b/extensions/test-extension/CHANGELOG.md index 6f81c1eda2..133eb14a55 100644 --- a/extensions/test-extension/CHANGELOG.md +++ b/extensions/test-extension/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-test diff --git a/extensions/test-extension/package.json b/extensions/test-extension/package.json index 46c4eba8ee..4effd8a272 100644 --- a/extensions/test-extension/package.json +++ b/extensions/test-extension/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-test", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension used inside e2e testing", "author": "OHIF", "license": "MIT", @@ -28,8 +28,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "0.29.4", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", diff --git a/extensions/tmtv/CHANGELOG.md b/extensions/tmtv/CHANGELOG.md index ce8e481df6..60d8cc2990 100644 --- a/extensions/tmtv/CHANGELOG.md +++ b/extensions/tmtv/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/extension-tmtv diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json index 26067f0f28..f2d77b03a1 100644 --- a/extensions/tmtv/package.json +++ b/extensions/tmtv/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-tmtv", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF extension for Total Metabolic Tumor Volume", "author": "OHIF", "license": "MIT", @@ -28,8 +28,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@ohif/core": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "dcmjs": "^0.29.5", "dicom-parser": "^1.8.9", "hammerjs": "^2.0.8", diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx index 1e85e9b9b8..628cb14eab 100644 --- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LegacyButton, ButtonGroup } from '@ohif/ui'; +import { LegacyButton, LegacyButtonGroup } from '@ohif/ui'; import { useTranslation } from 'react-i18next'; function ExportReports({ segmentations, tmtvValue, config, commandsManager }) { @@ -9,8 +9,8 @@ function ExportReports({ segmentations, tmtvValue, config, commandsManager }) { <> {segmentations?.length ? (
- {/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/} - @@ -27,8 +27,8 @@ function ExportReports({ segmentations, tmtvValue, config, commandsManager }) { > {t('Export CSV')} - - + @@ -41,7 +41,7 @@ function ExportReports({ segmentations, tmtvValue, config, commandsManager }) { > {t('Create RT Report')} - +
) : null} diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx index d30a50af9e..274ae720da 100644 --- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Input, Label, Select, LegacyButton, ButtonGroup } from '@ohif/ui'; +import { Input, Label, Select, LegacyButton, LegacyButtonGroup } from '@ohif/ui'; import { useTranslation } from 'react-i18next'; export const ROI_STAT = 'roi_stat'; @@ -35,8 +35,8 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) { />
- {/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/} - + {/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/} + {t('End')} - +
diff --git a/lerna.json b/lerna.json index 9a6e52e84a..6565f3c097 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "packages": ["extensions/*", "platform/*", "modes/*"], "npmClient": "yarn" } diff --git a/modes/basic-dev-mode/CHANGELOG.md b/modes/basic-dev-mode/CHANGELOG.md index a4137f2653..9d686f3233 100644 --- a/modes/basic-dev-mode/CHANGELOG.md +++ b/modes/basic-dev-mode/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/mode-basic-dev-mode diff --git a/modes/basic-dev-mode/package.json b/modes/basic-dev-mode/package.json index ea2ebf27cd..0085ee3451 100644 --- a/modes/basic-dev-mode/package.json +++ b/modes/basic-dev-mode/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-basic-dev-mode", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Basic OHIF Viewer Using Cornerstone", "author": "OHIF", "license": "MIT", @@ -29,12 +29,12 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/extension-dicom-pdf": "3.7.0-beta.78", - "@ohif/extension-dicom-video": "3.7.0-beta.78" + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/extension-dicom-pdf": "3.7.0-beta.80", + "@ohif/extension-dicom-video": "3.7.0-beta.80" }, "dependencies": { "@babel/runtime": "^7.20.13" diff --git a/modes/basic-dev-mode/src/index.js b/modes/basic-dev-mode/src/index.js index f1e250ea0b..411409e1f4 100644 --- a/modes/basic-dev-mode/src/index.js +++ b/modes/basic-dev-mode/src/index.js @@ -89,14 +89,13 @@ function modeFactory({ modeConfiguration }) { }; const toolGroupId = 'default'; - toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, configs); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); let unsubscribe; const activateTool = () => { toolbarService.recordInteraction({ groupId: 'WindowLevel', - itemId: 'WindowLevel', interactionType: 'tool', commands: [ { diff --git a/modes/basic-test-mode/CHANGELOG.md b/modes/basic-test-mode/CHANGELOG.md index fe8f04663b..9ed365e29e 100644 --- a/modes/basic-test-mode/CHANGELOG.md +++ b/modes/basic-test-mode/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/mode-test diff --git a/modes/basic-test-mode/package.json b/modes/basic-test-mode/package.json index 15fb711b68..0889c9357a 100644 --- a/modes/basic-test-mode/package.json +++ b/modes/basic-test-mode/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-test", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Basic mode for testing", "author": "OHIF", "license": "MIT", @@ -32,14 +32,14 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/extension-dicom-pdf": "3.7.0-beta.78", - "@ohif/extension-dicom-video": "3.7.0-beta.78", - "@ohif/extension-measurement-tracking": "3.7.0-beta.78", - "@ohif/extension-test": "3.7.0-beta.78" + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/extension-dicom-pdf": "3.7.0-beta.80", + "@ohif/extension-dicom-video": "3.7.0-beta.80", + "@ohif/extension-measurement-tracking": "3.7.0-beta.80", + "@ohif/extension-test": "3.7.0-beta.80" }, "dependencies": { "@babel/runtime": "^7.20.13" diff --git a/modes/basic-test-mode/src/index.js b/modes/basic-test-mode/src/index.js index 1c6645c4b0..ab68c94699 100644 --- a/modes/basic-test-mode/src/index.js +++ b/modes/basic-test-mode/src/index.js @@ -81,7 +81,6 @@ function modeFactory() { const activateTool = () => { toolbarService.recordInteraction({ groupId: 'WindowLevel', - itemId: 'WindowLevel', interactionType: 'tool', commands: [ { diff --git a/modes/basic-test-mode/src/initToolGroups.js b/modes/basic-test-mode/src/initToolGroups.js index 452d7ff15b..3110f24f88 100644 --- a/modes/basic-test-mode/src/initToolGroups.js +++ b/modes/basic-test-mode/src/initToolGroups.js @@ -23,7 +23,23 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage ], passive: [ { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, { toolName: toolNames.Bidirectional }, { toolName: toolNames.DragProbe }, { toolName: toolNames.EllipticalROI }, @@ -39,24 +55,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage disabled: [{ toolName: toolNames.ReferenceLines }], }; - const toolsConfig = { - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - - toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { @@ -117,25 +116,8 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { // disabled }; - const toolsConfig = { - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - const toolGroupId = 'SRToolGroup'; - toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { @@ -163,7 +145,23 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { ], passive: [ { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, { toolName: toolNames.Bidirectional }, { toolName: toolNames.DragProbe }, { toolName: toolNames.EllipticalROI }, @@ -173,37 +171,25 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: toolNames.Angle }, { toolName: toolNames.SegmentationDisplay }, ], - disabled: [{ toolName: toolNames.Crosshairs }, { toolName: toolNames.ReferenceLines }], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + }, + }, + { toolName: toolNames.ReferenceLines }, + ], // enabled // disabled }; - const toolsConfig = { - [toolNames.Crosshairs]: { - viewportIndicators: false, - autoPan: { - enabled: false, - panSize: 10, - }, - }, - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - - toolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools('mpr', tools); } function initToolGroups(extensionManager, toolGroupService, commandsManager) { diff --git a/modes/longitudinal/CHANGELOG.md b/modes/longitudinal/CHANGELOG.md index dd23aaaedb..faf3221274 100644 --- a/modes/longitudinal/CHANGELOG.md +++ b/modes/longitudinal/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/mode-longitudinal diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json index b2c323bb7d..ca9ea3dcd3 100644 --- a/modes/longitudinal/package.json +++ b/modes/longitudinal/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-longitudinal", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Longitudinal Workflow", "author": "OHIF", "license": "MIT", @@ -32,15 +32,15 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-seg": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/extension-dicom-pdf": "3.7.0-beta.78", - "@ohif/extension-dicom-video": "3.7.0-beta.78", - "@ohif/extension-measurement-tracking": "3.7.0-beta.78" + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-seg": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/extension-dicom-pdf": "3.7.0-beta.80", + "@ohif/extension-dicom-video": "3.7.0-beta.80", + "@ohif/extension-measurement-tracking": "3.7.0-beta.80" }, "dependencies": { "@babel/runtime": "^7.20.13" diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 2ed36a5b0a..288d0787c0 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -74,7 +74,7 @@ function modeFactory({ modeConfiguration }) { toolbarService, toolGroupService, panelService, - segmentationService, + customizationService, } = servicesManager.services; measurementService.clearMeasurements(); @@ -87,7 +87,6 @@ function modeFactory({ modeConfiguration }) { const activateTool = () => { toolbarService.recordInteraction({ groupId: 'WindowLevel', - itemId: 'WindowLevel', interactionType: 'tool', commands: [ { @@ -126,6 +125,13 @@ function modeFactory({ modeConfiguration }) { 'MoreTools', ]); + customizationService.addModeCustomizations([ + { + id: 'segmentation.disableEditing', + value: true, + }, + ]); + // // ActivatePanel event trigger for when a segmentation or measurement is added. // // Do not force activation so as to respect the state the user may have left the UI in. // _activatePanelTriggersSubscriptions = [ diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index cdc17c024f..e7ea816db3 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -23,7 +23,23 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage ], passive: [ { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, { toolName: toolNames.Bidirectional }, { toolName: toolNames.DragProbe }, { toolName: toolNames.EllipticalROI }, @@ -43,24 +59,7 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage disabled: [{ toolName: toolNames.ReferenceLines }], }; - const toolsConfig = { - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - - toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { @@ -68,6 +67,10 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' ); + if (!SRUtilityModule) { + return; + } + const CS3DUtilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.tools' ); @@ -121,25 +124,8 @@ function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { // disabled }; - const toolsConfig = { - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - const toolGroupId = 'SRToolGroup'; - toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); } function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { @@ -167,7 +153,23 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { ], passive: [ { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, { toolName: toolNames.Bidirectional }, { toolName: toolNames.DragProbe }, { toolName: toolNames.EllipticalROI }, @@ -179,37 +181,25 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { { toolName: toolNames.PlanarFreehandROI }, { toolName: toolNames.SegmentationDisplay }, ], - disabled: [{ toolName: toolNames.Crosshairs }, { toolName: toolNames.ReferenceLines }], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + }, + }, + { toolName: toolNames.ReferenceLines }, + ], // enabled // disabled }; - const toolsConfig = { - [toolNames.Crosshairs]: { - viewportIndicators: false, - autoPan: { - enabled: false, - panSize: 10, - }, - }, - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - - toolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools('mpr', tools); } function initVolume3DToolGroup(extensionManager, toolGroupService) { const utilityModule = extensionManager.getModuleEntry( diff --git a/modes/microscopy/CHANGELOG.md b/modes/microscopy/CHANGELOG.md index d12bf76b1d..e4da4ea460 100644 --- a/modes/microscopy/CHANGELOG.md +++ b/modes/microscopy/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/mode-microscopy diff --git a/modes/microscopy/package.json b/modes/microscopy/package.json index 839c3369c2..231cab4f0e 100644 --- a/modes/microscopy/package.json +++ b/modes/microscopy/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-microscopy", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "OHIF mode for DICOM microscopy", "author": "OHIF", "license": "MIT", @@ -33,8 +33,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-dicom-microscopy": "3.7.0-beta.78" + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-dicom-microscopy": "3.7.0-beta.80" }, "dependencies": { "@babel/runtime": "^7.20.13" diff --git a/modes/segmentation/.gitignore b/modes/segmentation/.gitignore new file mode 100644 index 0000000000..67045665db --- /dev/null +++ b/modes/segmentation/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/modes/segmentation/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js new file mode 100644 index 0000000000..163392a699 --- /dev/null +++ b/modes/segmentation/.webpack/webpack.prod.js @@ -0,0 +1,62 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the mode in addition to umd build +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/modes/segmentation/CHANGELOG.md b/modes/segmentation/CHANGELOG.md new file mode 100644 index 0000000000..2518e1aeba --- /dev/null +++ b/modes/segmentation/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) diff --git a/modes/segmentation/LICENSE b/modes/segmentation/LICENSE new file mode 100644 index 0000000000..c58f05915f --- /dev/null +++ b/modes/segmentation/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 @ohif-segmentation-mode (contact@ohif.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/modes/segmentation/README.md b/modes/segmentation/README.md new file mode 100644 index 0000000000..5bf905d927 --- /dev/null +++ b/modes/segmentation/README.md @@ -0,0 +1,7 @@ +# @ohif-segmentation-mode +## Description +OHIF segmentation mode which enables labelmap segmentation read/edit/export +## Author +@ohif +## License +MIT \ No newline at end of file diff --git a/modes/segmentation/babel.config.js b/modes/segmentation/babel.config.js new file mode 100644 index 0000000000..a38ddda212 --- /dev/null +++ b/modes/segmentation/babel.config.js @@ -0,0 +1,44 @@ +module.exports = { + plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/modes/segmentation/package.json b/modes/segmentation/package.json new file mode 100644 index 0000000000..73f6f9d58a --- /dev/null +++ b/modes/segmentation/package.json @@ -0,0 +1,64 @@ +{ + "name": "@ohif/mode-segmentation", + "version": "3.7.0-beta.80", + "description": "OHIF segmentation mode which enables labelmap segmentation read/edit/export", + "author": "@ohif", + "license": "MIT", + "main": "dist/umd/@ohif/mode-segmentation/index.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.7.0-beta.80" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "babel-eslint": "^8.0.3", + "babel-loader": "^8.0.0-beta.4", + "babel-plugin-inline-react-svg": "^2.0.1", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^5.0.1", + "eslint-loader": "^2.0.0", + "webpack": "^5.50.0", + "webpack-cli": "^4.7.2", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/segmentation/src/id.js b/modes/segmentation/src/id.js new file mode 100644 index 0000000000..ebe5acd98a --- /dev/null +++ b/modes/segmentation/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx new file mode 100644 index 0000000000..a6f552c5ae --- /dev/null +++ b/modes/segmentation/src/index.tsx @@ -0,0 +1,179 @@ +import { hotkeys } from '@ohif/core'; +import { id } from './id'; +import toolbarButtons from './toolbarButtons'; +import initToolGroups from './initToolGroups'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', + rightPanel: '@ohif/extension-default.panelModule.measure', +}; + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const segmentation = { + panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', + panelTool: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationWithTools', + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', +}; + +/** + * Just two dependencies to be able to render a viewport with panels in order + * to make sure that the mode is working. + */ +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', +}; + +function modeFactory({ modeConfiguration }) { + return { + /** + * Mode ID, which should be unique among modes used by the viewer. This ID + * is used to identify the mode in the viewer's state. + */ + id, + routeName: 'segmentation', + /** + * Mode name, which is displayed in the viewer's UI in the workList, for the + * user to select the mode. + */ + displayName: 'Segmentation', + /** + * Runs when the Mode Route is mounted to the DOM. Usually used to initialize + * Services and other resources. + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + const { measurementService, toolbarService, toolGroupService } = servicesManager.services; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + let unsubscribe; + + const activateTool = () => { + toolbarService.recordInteraction({ + groupId: 'WindowLevel', + interactionType: 'tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'WindowLevel', + }, + context: 'CORNERSTONE', + }, + ], + }); + + // We don't need to reset the active tool whenever a viewport is getting + // added to the toolGroup. + unsubscribe(); + }; + + // Since we only have one viewport for the basic cs3d mode and it has + // only one hanging protocol, we can just use the first viewport + ({ unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + activateTool + )); + + toolbarService.init(extensionManager); + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MPR', + 'Crosshairs', + 'MoreTools', + ]); + }, + onModeExit: ({ servicesManager }) => { + const { + toolGroupService, + syncGroupService, + toolbarService, + segmentationService, + cornerstoneViewportService, + } = servicesManager.services; + + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + /** */ + validationTags: { + study: [], + series: [], + }, + /** + * A boolean return value that indicates whether the mode is valid for the + * modalities of the selected studies. For instance a PET/CT mode should be + */ + isValidMode: ({ modalities }) => true, + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'template', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.leftPanel], + rightPanels: [segmentation.panelTool], + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + { + namespace: segmentation.viewport, + displaySetsToDisplay: [segmentation.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + /** List of extensions that are used by the mode */ + extensions: extensionDependencies, + /** HangingProtocol used by the mode */ + // hangingProtocol: [''], + /** SopClassHandlers used by the mode */ + sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler], + /** hotkeys for mode */ + hotkeys: [...hotkeys.defaults.hotkeyBindings], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/segmentation/src/initToolGroups.ts b/modes/segmentation/src/initToolGroups.ts new file mode 100644 index 0000000000..58204839ee --- /dev/null +++ b/modes/segmentation/src/initToolGroups.ts @@ -0,0 +1,82 @@ +const brushInstanceNames = { + CircularBrush: 'CircularBrush', + CircularEraser: 'CircularEraser', + SphereBrush: 'SphereBrush', + SphereEraser: 'SphereEraser', + ThresholdCircularBrush: 'ThresholdCircularBrush', + ThresholdSphereBrush: 'ThresholdSphereBrush', +}; + +const brushStrategies = { + CircularBrush: 'FILL_INSIDE_CIRCLE', + CircularEraser: 'ERASE_INSIDE_CIRCLE', + SphereBrush: 'FILL_INSIDE_SPHERE', + SphereEraser: 'ERASE_INSIDE_SPHERE', + ThresholdCircularBrush: 'THRESHOLD_INSIDE_CIRCLE', + ThresholdSphereBrush: 'THRESHOLD_INSIDE_SPHERE', +}; + +function createTools(utilityModule) { + const { toolNames, Enums } = utilityModule.exports; + return { + active: [ + { toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] }, + { toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] }, + { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }] }, + { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + ], + passive: Object.keys(brushInstanceNames) + .map(brushName => ({ + toolName: brushName, + parentTool: 'Brush', + configuration: { + activeStrategy: brushStrategies[brushName], + }, + })) + .concat([ + { toolName: toolNames.CircleScissors }, + { toolName: toolNames.RectangleScissors }, + { toolName: toolNames.SphereScissors }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Magnify }, + { toolName: toolNames.SegmentationDisplay }, + ]), + disabled: [{ toolName: toolNames.ReferenceLines }], + }; +} + +function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + const tools = createTools(utilityModule); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + const tools = createTools(utilityModule); + tools.disabled.push( + { + toolName: utilityModule.exports.toolNames.Crosshairs, + configuration: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + }, + }, + { toolName: utilityModule.exports.toolNames.ReferenceLines } + ); + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} + +function initToolGroups(extensionManager, toolGroupService, commandsManager) { + initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default'); + initMPRToolGroup(extensionManager, toolGroupService, commandsManager); +} + +export default initToolGroups; diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts new file mode 100644 index 0000000000..b17a33deae --- /dev/null +++ b/modes/segmentation/src/toolbarButtons.ts @@ -0,0 +1,356 @@ +import { + // ExpandableToolbarButton, + // ListMenu, + WindowLevelMenuItem, +} from '@ohif/ui'; +import { defaults } from '@ohif/core'; + +const { windowLevelPresets } = defaults; +/** + * + * @param {*} type - 'tool' | 'action' | 'toggle' + * @param {*} id + * @param {*} icon + * @param {*} label + */ +function _createButton(type, id, icon, label, commands, tooltip, uiType) { + return { + id, + icon, + label, + type, + commands, + tooltip, + uiType, + }; +} + +const _createActionButton = _createButton.bind(null, 'action'); +const _createToggleButton = _createButton.bind(null, 'toggle'); +const _createToolButton = _createButton.bind(null, 'tool'); + +/** + * + * @param {*} preset - preset number (from above import) + * @param {*} title + * @param {*} subtitle + */ +function _createWwwcPreset(preset, title, subtitle) { + return { + id: preset.toString(), + title, + subtitle, + type: 'action', + commands: [ + { + commandName: 'setWindowLevel', + commandOptions: { + ...windowLevelPresets[preset], + }, + context: 'CORNERSTONE', + }, + ], + }; +} + +const toolGroupIds = ['default', 'mpr', 'SRToolGroup']; + +/** + * Creates an array of 'setToolActive' commands for the given toolName - one for + * each toolGroupId specified in toolGroupIds. + * @param {string} toolName + * @returns {Array} an array of 'setToolActive' commands + */ +function _createSetToolActiveCommands(toolName) { + const temp = toolGroupIds.map(toolGroupId => ({ + commandName: 'setToolActive', + commandOptions: { + toolGroupId, + toolName, + }, + context: 'CORNERSTONE', + })); + return temp; +} + +const toolbarButtons = [ + // Zoom.. + { + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + }, + }, + // Window Level + Presets... + { + id: 'WindowLevel', + type: 'ohif.splitButton', + props: { + groupId: 'WindowLevel', + primary: _createToolButton( + 'WindowLevel', + 'tool-window-level', + 'Window Level', + [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'WindowLevel', + }, + context: 'CORNERSTONE', + }, + ], + 'Window Level' + ), + secondary: { + icon: 'chevron-down', + label: 'W/L Manual', + isActive: true, + tooltip: 'W/L Presets', + }, + isAction: true, // ? + renderer: WindowLevelMenuItem, + items: [ + _createWwwcPreset(1, 'Soft tissue', '400 / 40'), + _createWwwcPreset(2, 'Lung', '1500 / -600'), + _createWwwcPreset(3, 'Liver', '150 / 90'), + _createWwwcPreset(4, 'Bone', '2500 / 480'), + _createWwwcPreset(5, 'Brain', '80 / 40'), + ], + }, + }, + // Pan... + { + id: 'Pan', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: _createSetToolActiveCommands('Pan'), + }, + }, + { + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + type: 'action', + commands: [ + { + commandName: 'showDownloadViewportModal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, + }, + { + id: 'Layout', + type: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 3, + }, + }, + { + id: 'MPR', + type: 'ohif.action', + props: { + type: 'toggle', + icon: 'icon-mpr', + label: 'MPR', + commands: [ + { + commandName: 'toggleHangingProtocol', + commandOptions: { + protocolId: 'mpr', + }, + context: 'DEFAULT', + }, + ], + }, + }, + { + id: 'Crosshairs', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Crosshairs', + toolGroupId: 'mpr', + }, + context: 'CORNERSTONE', + }, + ], + }, + }, + // More... + { + id: 'MoreTools', + type: 'ohif.splitButton', + props: { + isRadio: true, // ? + groupId: 'MoreTools', + primary: _createActionButton( + 'Reset', + 'tool-reset', + 'Reset View', + [ + { + commandName: 'resetViewport', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Reset' + ), + secondary: { + icon: 'chevron-down', + label: '', + isActive: true, + tooltip: 'More Tools', + }, + items: [ + _createActionButton( + 'Reset', + 'tool-reset', + 'Reset View', + [ + { + commandName: 'resetViewport', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Reset' + ), + _createActionButton( + 'rotate-right', + 'tool-rotate-right', + 'Rotate Right', + [ + { + commandName: 'rotateViewportCW', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Rotate +90' + ), + _createActionButton( + 'flip-horizontal', + 'tool-flip-horizontal', + 'Flip Horizontally', + [ + { + commandName: 'flipViewportHorizontal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Flip Horizontal' + ), + _createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [ + { + commandName: 'toggleStackImageSync', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ]), + _createToggleButton( + 'ReferenceLines', + 'tool-referenceLines', // change this with the new icon + 'Reference Lines', + [ + { + commandName: 'toggleReferenceLines', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ] + ), + _createToolButton( + 'StackScroll', + 'tool-stack-scroll', + 'Stack Scroll', + [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'StackScroll', + }, + context: 'CORNERSTONE', + }, + ], + 'Stack Scroll' + ), + _createActionButton( + 'invert', + 'tool-invert', + 'Invert', + [ + { + commandName: 'invertViewport', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + 'Invert Colors' + ), + _createToggleButton( + 'cine', + 'tool-cine', + 'Cine', + [ + { + commandName: 'toggleCine', + context: 'CORNERSTONE', + }, + ], + 'Cine' + ), + _createToolButton( + 'Magnify', + 'tool-magnify', + 'Magnify', + [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Magnify', + }, + context: 'CORNERSTONE', + }, + ], + 'Magnify' + ), + _createActionButton( + 'TagBrowser', + 'list-bullets', + 'Dicom Tag Browser', + [ + { + commandName: 'openDICOMTagViewer', + commandOptions: {}, + context: 'DEFAULT', + }, + ], + 'Dicom Tag Browser' + ), + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/tmtv/CHANGELOG.md b/modes/tmtv/CHANGELOG.md index 81d28855e4..85d72389fe 100644 --- a/modes/tmtv/CHANGELOG.md +++ b/modes/tmtv/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/mode-tmtv diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json index 1099c7ca59..e609c080b1 100644 --- a/modes/tmtv/package.json +++ b/modes/tmtv/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-tmtv", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Total Metabolic Tumor Volume Workflow", "author": "OHIF", "license": "MIT", @@ -32,13 +32,13 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/extension-dicom-pdf": "3.7.0-beta.78", - "@ohif/extension-dicom-video": "3.7.0-beta.78", - "@ohif/extension-measurement-tracking": "3.7.0-beta.78" + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/extension-dicom-pdf": "3.7.0-beta.80", + "@ohif/extension-dicom-video": "3.7.0-beta.80", + "@ohif/extension-measurement-tracking": "3.7.0-beta.80" }, "dependencies": { "@babel/runtime": "^7.20.13" diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index 38e620a180..110fb0fb75 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -58,7 +58,6 @@ function modeFactory({ modeConfiguration }) { const setWindowLevelActive = () => { toolbarService.recordInteraction({ groupId: 'WindowLevel', - itemId: 'WindowLevel', interactionType: 'tool', commands: [ { diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js index 549a6052fe..a8f4fcf31b 100644 --- a/modes/tmtv/src/initToolGroups.js +++ b/modes/tmtv/src/initToolGroups.js @@ -26,7 +26,24 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { ], passive: [ { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }); + }, + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, { toolName: toolNames.Bidirectional }, { toolName: toolNames.DragProbe }, { toolName: toolNames.Probe }, @@ -38,138 +55,54 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { { toolName: toolNames.Magnify }, ], enabled: [{ toolName: toolNames.SegmentationDisplay }], - disabled: [{ toolName: toolNames.Crosshairs }], - }; - - const toolsConfig = { - [toolNames.Crosshairs]: { - viewportIndicators: false, - autoPan: { - enabled: false, - panSize: 10, - }, - }, - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => { - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }); + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + }, }, - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, + ], }; - toolGroupService.createToolGroupAndAddTools(toolGroupIds.CT, tools, toolsConfig); - toolGroupService.createToolGroupAndAddTools( - toolGroupIds.PT, - { - active: tools.active, - passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], - enabled: tools.enabled, - disabled: tools.disabled, - }, - toolsConfig - ); - toolGroupService.createToolGroupAndAddTools(toolGroupIds.Fusion, tools, toolsConfig); - toolGroupService.createToolGroupAndAddTools(toolGroupIds.default, tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.CT, tools); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.PT, { + active: tools.active, + passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], + enabled: tools.enabled, + disabled: tools.disabled, + }); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.Fusion, tools); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.default, tools); const mipTools = { active: [ { toolName: toolNames.VolumeRotateMouseWheel, + configuration: { + rotateIncrementDegrees: 0.1, + }, }, { toolName: toolNames.MipJumpToClick, + configuration: { + toolGroupId: toolGroupIds.PT, + }, bindings: [{ mouseButton: Enums.MouseBindings.Primary }], }, ], enabled: [{ toolName: toolNames.SegmentationDisplay }], }; - const mipToolsConfig = { - [toolNames.VolumeRotateMouseWheel]: { - rotateIncrementDegrees: 0.1, - }, - [toolNames.MipJumpToClick]: { - toolGroupId: toolGroupIds.PT, - }, - }; - - toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools, mipToolsConfig); -} - -function initMPRToolGroup(toolNames, Enums, toolGroupService, commandsManager) { - const tools = { - active: [ - { - toolName: toolNames.WindowLevel, - bindings: [{ mouseButton: Enums.MouseBindings.Primary }], - }, - { - toolName: toolNames.Pan, - bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], - }, - { - toolName: toolNames.Zoom, - bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], - }, - { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, - ], - passive: [ - { toolName: toolNames.Length }, - { toolName: toolNames.ArrowAnnotate }, - { toolName: toolNames.Bidirectional }, - { toolName: toolNames.DragProbe }, - { toolName: toolNames.EllipticalROI }, - { toolName: toolNames.RectangleROI }, - { toolName: toolNames.StackScroll }, - { toolName: toolNames.Angle }, - { toolName: toolNames.CobbAngle }, - { toolName: toolNames.SegmentationDisplay }, - ], - disabled: [{ toolName: toolNames.Crosshairs }], - - // enabled - // disabled - }; - - const toolsConfig = { - [toolNames.Crosshairs]: { - viewportIndicators: false, - autoPan: { - enabled: false, - panSize: 10, - }, - }, - [toolNames.ArrowAnnotate]: { - getTextCallback: (callback, eventDetails) => - commandsManager.runCommand('arrowTextCallback', { - callback, - eventDetails, - }), - - changeTextCallback: (data, eventDetails, callback) => - commandsManager.runCommand('arrowTextCallback', { - callback, - data, - eventDetails, - }), - }, - }; - - toolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools); } function initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { _initToolGroups(toolNames, Enums, toolGroupService, commandsManager); - // initMPRToolGroup(toolNames, Enums, toolGroupService, commandsManager); } export default initToolGroups; diff --git a/platform/app/CHANGELOG.md b/platform/app/CHANGELOG.md index 3d68cad680..34c41c7a19 100644 --- a/platform/app/CHANGELOG.md +++ b/platform/app/CHANGELOG.md @@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Bug Fixes + +* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a)) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/app diff --git a/platform/app/cypress.config.ts b/platform/app/cypress.config.ts index 7cf5ff77f9..c575e7c56c 100644 --- a/platform/app/cypress.config.ts +++ b/platform/app/cypress.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ responseTimeout: 10000, specPattern: 'cypress/integration/**/*.spec.[jt]s', projectId: '4oe38f', - video: false, + video: true, reporter: 'junit', reporterOptions: { mochaFile: 'cypress/results/test-output.xml', diff --git a/platform/app/cypress/integration/MultiStudy.spec.js b/platform/app/cypress/integration/MultiStudy.spec.js index 8a6bd81b01..1df0a98070 100644 --- a/platform/app/cypress/integration/MultiStudy.spec.js +++ b/platform/app/cypress/integration/MultiStudy.spec.js @@ -7,13 +7,18 @@ describe('OHIF Multi Study', () => { cy.expectMinimumThumbnails(4); cy.initCornerstoneToolsAliases(); cy.initCommonElementsAliases(); + cy.waitDicomImage(); }; it('Should display 2 comparison up', () => { beforeSetup(); - cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4); - cy.get('[data-cy="studyDate"]').should(studyDate => { + cy.get('[data-cy="viewport-pane"]').as('viewportPane'); + cy.get('@viewportPane').its('length').should('be.eq', 4); + + cy.get('[data-cy="studyDate"]').as('studyDate'); + + cy.get('@studyDate').should(studyDate => { expect(studyDate.length).to.be.eq(4); expect(studyDate.text()).to.contain('2014').contain('2001'); expect(studyDate.text().indexOf('2014')).to.be.lessThan(studyDate.text().indexOf('2001')); diff --git a/platform/app/cypress/integration/OHIFPdfDisplay.spec.js b/platform/app/cypress/integration/OHIFPdfDisplay.spec.js index 05be6e8de6..2ef2f020ef 100644 --- a/platform/app/cypress/integration/OHIFPdfDisplay.spec.js +++ b/platform/app/cypress/integration/OHIFPdfDisplay.spec.js @@ -1,8 +1,6 @@ describe('OHIF PDF Display', function () { beforeEach(function () { cy.openStudyInViewer('2.25.317377619501274872606137091638706705333'); - - cy.resetViewport().wait(50); }); it('checks if series thumbnails are being displayed', function () { diff --git a/platform/app/cypress/integration/OHIFVideoDisplay.spec.js b/platform/app/cypress/integration/OHIFVideoDisplay.spec.js index 1b3565b8af..ff877034af 100644 --- a/platform/app/cypress/integration/OHIFVideoDisplay.spec.js +++ b/platform/app/cypress/integration/OHIFVideoDisplay.spec.js @@ -1,7 +1,6 @@ describe('OHIF Video Display', function () { beforeEach(function () { cy.openStudyInViewer('2.25.96975534054447904995905761963464388233'); - cy.resetViewport().wait(50); }); it('checks if series thumbnails are being displayed', function () { diff --git a/platform/app/cypress/integration/customization/HangingProtocol.spec.js b/platform/app/cypress/integration/customization/HangingProtocol.spec.js index 87de457af0..4f55e620b5 100644 --- a/platform/app/cypress/integration/customization/HangingProtocol.spec.js +++ b/platform/app/cypress/integration/customization/HangingProtocol.spec.js @@ -1,5 +1,5 @@ describe('OHIF HP', () => { - const beforeSetup = () => { + beforeEach(() => { cy.checkStudyRouteInViewer( '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', '&hangingProtocolId=@ohif/mnGrid' @@ -7,17 +7,14 @@ describe('OHIF HP', () => { cy.expectMinimumThumbnails(3); cy.initCornerstoneToolsAliases(); cy.initCommonElementsAliases(); - }; + cy.waitDicomImage(); + }); it('Should display 3 up', () => { - beforeSetup(); - cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 3); }); it('Should navigate next/previous stage', () => { - beforeSetup(); - cy.get('body').type(','); cy.wait(250); cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js index a06201f90e..5f2ba6f5a2 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js @@ -5,7 +5,7 @@ describe('OHIF Context Menu', function () { cy.expectMinimumThumbnails(3); cy.initCommonElementsAliases(); cy.initCornerstoneToolsAliases(); - cy.resetViewport().wait(50); + cy.waitDicomImage(); }); it('checks context menu customization', function () { diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js index ea0a016b13..984b038bc0 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js @@ -13,10 +13,10 @@ describe('OHIF Cornerstone Hotkeys', () => { cy.expectMinimumThumbnails(3); cy.initCornerstoneToolsAliases(); cy.initCommonElementsAliases(); + cy.waitDicomImage(); }); it('checks if hotkeys "R" and "L" can rotate the image', () => { - // Hotkey R cy.get('body').type('R'); cy.get('@viewportInfoMidLeft').should('contains.text', 'P'); cy.get('@viewportInfoMidTop').should('contains.text', 'R'); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js index 829fd1d929..15fe14babc 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js @@ -9,6 +9,7 @@ describe('OHIF Cornerstone Toolbar', () => { //const expectedText = 'Ser: 1'; //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); + cy.waitDicomImage(); }); it('checks if all primary buttons are being displayed', () => { @@ -66,13 +67,12 @@ describe('OHIF Cornerstone Toolbar', () => { // }); it('checks if Levels tool will change the window width and center of an image', () => { - //Click on button and verify if icon is active on toolbar - cy.waitDicomImage(); - cy.get('@wwwcBtnPrimary') - .click() - .then($wwwcBtn => { - cy.wrap($wwwcBtn).should('have.class', 'active'); - }); + // Wait for the DICOM image to load + + // Assign an alias to the button element + cy.get('@wwwcBtnPrimary').as('wwwcButton'); + cy.get('@wwwcButton').click(); + cy.get('@wwwcButton').should('have.class', 'active'); //drags the mouse inside the viewport to be able to interact with series cy.get('@viewport') @@ -90,13 +90,16 @@ describe('OHIF Cornerstone Toolbar', () => { }); it('checks if Pan tool will move the image inside the viewport', () => { - //Click on button and verify if icon is active on toolbar - cy.get('@panBtn') - .click() - .then($panBtn => { - cy.wrap($panBtn).should('have.class', 'active'); - }); + // Assign an alias to the button element + cy.get('@panBtn').as('panButton'); + + // Click on the button + cy.get('@panButton').click(); + + // Assert that the button has the 'active' class + cy.get('@panButton').should('have.class', 'active'); + // Trigger the pan actions on the viewport cy.get('@viewport') .trigger('mousedown', 'center', { buttons: 1 }) .trigger('mousemove', 'bottom', { buttons: 1 }) diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js index 45ff0511a0..74b172f32d 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js @@ -5,7 +5,6 @@ describe('OHIF Measurement Panel', function () { cy.expectMinimumThumbnails(3); cy.initCommonElementsAliases(); cy.initCornerstoneToolsAliases(); - cy.resetViewport().wait(50); cy.waitDicomImage(); }); @@ -24,26 +23,28 @@ describe('OHIF Measurement Panel', function () { it('checks if measurement item can be Relabeled under Measurements panel', function () { // Add length measurement cy.addLengthMeasurement(); - cy.get('[data-cy="viewport-notification"]').should('exist'); - cy.get('[data-cy="viewport-notification"]').should('be.visible'); - cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click(); - cy.get('[data-cy="measurement-item"]').click(); + cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('exist'); + cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('be.visible'); - cy.get('[data-cy="measurement-item"]').find('svg').click(); + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn').click(); + + cy.get('[data-cy="measurement-item"]').as('measurementItem').click(); + + cy.get('[data-cy="measurement-item"]').find('svg').as('measurementItemSvg').click(); // enter Bone label cy.get('[data-cy="input-annotation"]').should('exist'); cy.get('[data-cy="input-annotation"]').should('be.visible'); cy.get('[data-cy="input-annotation"]').type('Bone{enter}'); - // Verify if 'Bone' label was added - cy.get('[data-cy="measurement-item"]').should('contain.text', 'Bone'); + cy.get('[data-cy="measurement-item"]').as('measurementItem').should('contain.text', 'Bone'); }); it('checks if image would jump when clicked on a measurement item', function () { // Add length measurement cy.addLengthMeasurement(); - cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').click(); + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('promptBeginTrackingYesBtn'); + cy.get('@promptBeginTrackingYesBtn').click(); cy.scrollToIndex(13); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js index 6bcd8e0f0f..e92e75e5c0 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js @@ -5,7 +5,6 @@ describe('OHIF Study Viewer Page', function () { cy.expectMinimumThumbnails(3); cy.initCommonElementsAliases(); cy.initCornerstoneToolsAliases(); - cy.resetViewport().wait(50); }); it('checks if series thumbnails are being displayed', function () { diff --git a/platform/app/cypress/support/commands.js b/platform/app/cypress/support/commands.js index 04e438c573..948df1a279 100644 --- a/platform/app/cypress/support/commands.js +++ b/platform/app/cypress/support/commands.js @@ -144,18 +144,22 @@ Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) => * @param {number[]} secondClick - Click position [x, y] */ Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => { - cy.get(viewport).then($viewport => { - const [x1, y1] = firstClick; - const [x2, y2] = secondClick; + const performClick = (alias, x, y) => { + cy.get(alias).click(x, y, { force: true, multiple: true }).wait(250); + }; - // The wait is necessary because of double click testing - cy.wrap($viewport) - .click(x1, y1) - .wait(250) - .trigger('mousemove', { clientX: x2, clientY: y2 }) - .click(x2, y2) - .wait(250); - }); + cy.get(viewport).as('viewportAlias'); + const [x1, y1] = firstClick; + const [x2, y2] = secondClick; + + // First click + performClick('@viewportAlias', x1, y1); + + // Move the mouse + cy.get('@viewportAlias').trigger('mousemove', { clientX: x2, clientY: y2 }).wait(250); + + // Second click + performClick('@viewportAlias', x2, y2); }); /** @@ -183,8 +187,10 @@ Cypress.Commands.add('addAngle', (viewport, firstClick, secondClick, thirdClick) }); Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => { - cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).should($itemList => { - expect($itemList.length >= seriesToWait).to.be.true; + cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).as('thumbnails'); + + cy.get('@thumbnails').should($itemList => { + expect($itemList.length).to.be.gte(seriesToWait); }); }); @@ -211,11 +217,13 @@ Cypress.Commands.add('waitDicomImage', (mode = '/basic-test', timeout = 50000) = //Command to reset and clear all the changes made to the viewport Cypress.Commands.add('resetViewport', () => { - //Click on More button + // Assign an alias to the More button cy.get('[data-cy="MoreTools-split-button-primary"]') .should('have.attr', 'data-tool', 'Reset') - .as('moreBtn') - .click(); + .as('moreBtn'); + + // Use the alias to click on the More button + cy.get('@moreBtn').click(); }); Cypress.Commands.add('imageZoomIn', () => { @@ -266,12 +274,13 @@ Cypress.Commands.add('initStudyListAliasesOnDesktop', () => { Cypress.Commands.add( 'addLengthMeasurement', (firstClick = [150, 100], secondClick = [130, 170]) => { - cy.get('@measurementToolsBtnPrimary') - .should('have.attr', 'data-tool', 'Length') - .click() - .then($lengthBtn => { - cy.wrap($lengthBtn).should('have.class', 'active'); - }); + // Assign an alias to the button element + cy.get('@measurementToolsBtnPrimary').as('lengthButton'); + + cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length'); + cy.get('@lengthButton').click(); + + cy.get('@lengthButton').should('have.class', 'active'); cy.addLine('.viewport-element', firstClick, secondClick); } @@ -463,7 +472,11 @@ Cypress.Commands.add('closePreferences', () => { Cypress.Commands.add('selectPreferencesTab', tabAlias => { cy.initPreferencesModalAliases(); - cy.get(tabAlias).click().should('have.class', 'active'); + + cy.get(tabAlias).as('selectedTab'); + cy.get('@selectedTab').click(); + cy.get('@selectedTab').should('have.class', 'active'); + initPreferencesModalFooterBtnAliases(); }); diff --git a/platform/app/package.json b/platform/app/package.json index b83dabb7d6..85a6e05f34 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/app", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "productVersion": "3.4.0", "description": "OHIF Viewer", "author": "OHIF Contributors", @@ -50,23 +50,23 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.13.2", - "@ohif/core": "3.7.0-beta.78", - "@ohif/extension-cornerstone": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-seg": "3.7.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.78", - "@ohif/extension-default": "3.7.0-beta.78", - "@ohif/extension-dicom-microscopy": "3.7.0-beta.78", - "@ohif/extension-dicom-pdf": "3.7.0-beta.78", - "@ohif/extension-dicom-video": "3.7.0-beta.78", - "@ohif/extension-test": "3.7.0-beta.78", - "@ohif/i18n": "3.7.0-beta.78", - "@ohif/mode-basic-dev-mode": "3.7.0-beta.78", - "@ohif/mode-longitudinal": "3.7.0-beta.78", - "@ohif/mode-microscopy": "3.7.0-beta.78", - "@ohif/mode-test": "3.7.0-beta.78", - "@ohif/ui": "3.7.0-beta.78", + "@cornerstonejs/dicom-image-loader": "^1.16.5", + "@ohif/core": "3.7.0-beta.80", + "@ohif/extension-cornerstone": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-seg": "3.7.0-beta.80", + "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.80", + "@ohif/extension-default": "3.7.0-beta.80", + "@ohif/extension-dicom-microscopy": "3.7.0-beta.80", + "@ohif/extension-dicom-pdf": "3.7.0-beta.80", + "@ohif/extension-dicom-video": "3.7.0-beta.80", + "@ohif/extension-test": "3.7.0-beta.80", + "@ohif/i18n": "3.7.0-beta.80", + "@ohif/mode-basic-dev-mode": "3.7.0-beta.80", + "@ohif/mode-longitudinal": "3.7.0-beta.80", + "@ohif/mode-microscopy": "3.7.0-beta.80", + "@ohif/mode-test": "3.7.0-beta.80", + "@ohif/ui": "3.7.0-beta.80", "@types/react": "^17.0.38", "classnames": "^2.3.2", "core-js": "^3.16.1", @@ -94,7 +94,7 @@ "devDependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@percy/cypress": "^3.1.1", - "cypress": "^12.6.0", + "cypress": "^13.2.0", "cypress-file-upload": "^3.5.3", "glob": "^8.0.3", "identity-obj-proxy": "3.0.x", diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index 2f892db6c8..08a42deb0f 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -57,6 +57,9 @@ { "packageName": "@ohif/mode-longitudinal" }, + { + "packageName": "@ohif/mode-segmentation" + }, { "packageName": "@ohif/mode-tmtv" }, diff --git a/platform/app/public/config/aws.js b/platform/app/public/config/aws.js index f09e37fc0b..f7dd9a0698 100644 --- a/platform/app/public/config/aws.js +++ b/platform/app/public/config/aws.js @@ -23,7 +23,6 @@ window.config = { qidoRoot: 'https://myserver.com/dicomweb', wadoRoot: 'https://myserver.com/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 09c0a233f0..00013529ea 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -3,10 +3,7 @@ window.config = { // whiteLabeling: {}, extensions: [], modes: [], - customizationService: { - // Shows a custom route -access via http://localhost:3000/custom - // helloPage: '@ohif/extension-default.customizationModule.helloPage', - }, + customizationService: {}, showStudyList: true, // some windows systems have issues with more than 3 web workers maxNumberOfWebWorkers: 3, @@ -45,7 +42,6 @@ window.config = { qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/public/config/default_16bit.js b/platform/app/public/config/default_16bit.js new file mode 100644 index 0000000000..20b14d6b9b --- /dev/null +++ b/platform/app/public/config/default_16bit.js @@ -0,0 +1,189 @@ +window.config = { + routerBasename: '/', + // whiteLabeling: {}, + extensions: [], + modes: [], + customizationService: { + // Shows a custom route -access via http://localhost:3000/custom + // helloPage: '@ohif/extension-default.customizationModule.helloPage', + }, + showStudyList: true, + // some windows systems have issues with more than 3 web workers + maxNumberOfWebWorkers: 3, + // below flag is for performance reasons, but it might not work for all servers + omitQuotationForMultipartRequest: true, + showWarningMessageForCrossOrigin: false, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + use16BitDataType: true, + useSharedArrayBuffer: 'AUTO', + maxNumRequests: { + interaction: 100, + thumbnail: 75, + // Prefetch number is dependent on the http protocol. For http 2 or + // above, the number of requests can be go a lot higher. + prefetch: 25, + }, + // filterQueryParam: false, + dataSources: [ + { + friendlyName: 'dcmjs DICOMWeb Server', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'aws', + // old server + // wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + // qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // new server + wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + }, + }, + { + friendlyName: 'dicom json', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + name: 'json', + }, + }, + { + friendlyName: 'dicom local', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: {}, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + // whiteLabeling: { + // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + // createLogoComponentFn: function (React) { + // return React.createElement( + // 'a', + // { + // target: '_self', + // rel: 'noopener noreferrer', + // className: 'text-purple-600 line-through', + // href: '/', + // }, + // React.createElement('img', + // { + // src: './customLogo.svg', + // className: 'w-8 h-8', + // } + // )) + // }, + // }, + defaultDataSourceName: 'dicomweb', + hotkeys: [ + { + commandName: 'incrementActiveViewport', + label: 'Next Viewport', + keys: ['right'], + }, + { + commandName: 'decrementActiveViewport', + label: 'Previous Viewport', + keys: ['left'], + }, + { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] }, + { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] }, + { commandName: 'invertViewport', label: 'Invert', keys: ['i'] }, + { + commandName: 'flipViewportHorizontal', + label: 'Flip Horizontally', + keys: ['h'], + }, + { + commandName: 'flipViewportVertical', + label: 'Flip Vertically', + keys: ['v'], + }, + { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] }, + { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] }, + { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] }, + { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, + { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, + { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + }, + // ~ Window level presets + { + commandName: 'windowLevelPreset1', + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'windowLevelPreset2', + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'windowLevelPreset3', + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'windowLevelPreset4', + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'windowLevelPreset5', + label: 'W/L Preset 5', + keys: ['5'], + }, + { + commandName: 'windowLevelPreset6', + label: 'W/L Preset 6', + keys: ['6'], + }, + { + commandName: 'windowLevelPreset7', + label: 'W/L Preset 7', + keys: ['7'], + }, + { + commandName: 'windowLevelPreset8', + label: 'W/L Preset 8', + keys: ['8'], + }, + { + commandName: 'windowLevelPreset9', + label: 'W/L Preset 9', + keys: ['9'], + }, + ], +}; diff --git a/platform/app/public/config/dicomweb_relative.js b/platform/app/public/config/dicomweb_relative.js index 476b2d55e0..bfa51c34ae 100644 --- a/platform/app/public/config/dicomweb_relative.js +++ b/platform/app/public/config/dicomweb_relative.js @@ -22,7 +22,6 @@ window.config = { qidoRoot: '/dicomweb', wadoRoot: '/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js index af48a84cbe..8b5f174a1e 100644 --- a/platform/app/public/config/e2e.js +++ b/platform/app/public/config/e2e.js @@ -23,7 +23,6 @@ window.config = { qidoRoot: '/viewer-testdata', wadoRoot: '/viewer-testdata', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, @@ -63,7 +62,6 @@ window.config = { qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/public/config/local_static.js b/platform/app/public/config/local_static.js index 8d1e4f6630..89f205d011 100644 --- a/platform/app/public/config/local_static.js +++ b/platform/app/public/config/local_static.js @@ -22,7 +22,6 @@ window.config = { qidoRoot: '/dicomweb', wadoRoot: '/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/public/config/multiple.js b/platform/app/public/config/multiple.js index 145b16ffc2..57684a9ef5 100644 --- a/platform/app/public/config/multiple.js +++ b/platform/app/public/config/multiple.js @@ -59,7 +59,6 @@ window.config = { qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, @@ -78,7 +77,6 @@ window.config = { qidoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb', wadoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, supportsStow: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', @@ -99,7 +97,6 @@ window.config = { qidoRoot: '/viewer-testdata', wadoRoot: '/viewer-testdata', qidoSupportsIncludeField: false, - supportsReject: false, supportsStow: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js index bbb24da90f..a729d1c8d5 100644 --- a/platform/app/public/config/netlify.js +++ b/platform/app/public/config/netlify.js @@ -22,7 +22,6 @@ window.config = { wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb', qidoSupportsIncludeField: false, - supportsReject: false, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 236add7e30..e6e1c23947 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -382,7 +382,7 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe console.log("Can't show displaySet", SOPClassHandlerId, displaySets[0]); uiNotificationService.show({ title: 'Viewport Not Supported Yet', - message: `Cannot display SOPClassId of ${displaySets[0].SOPClassUID} yet`, + message: `Cannot display SOPClassUID of ${displaySets[0].SOPClassUID} yet`, type: 'error', }); diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 1b1c048a95..865c6fbb9e 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -336,8 +336,6 @@ function WorkList({ >
{appConfig.loadedModes.map((mode, i) => { - const isFirst = i === 0; - const modalitiesToCheck = modalities.replaceAll('/', '\\'); const isValidMode = mode.isValidMode({ diff --git a/platform/cli/CHANGELOG.md b/platform/cli/CHANGELOG.md index 539753ce4b..73ec9c783f 100644 --- a/platform/cli/CHANGELOG.md +++ b/platform/cli/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/cli + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package @ohif/cli diff --git a/platform/cli/package.json b/platform/cli/package.json index 9fa6bfd9b5..146cbdbdbf 100644 --- a/platform/cli/package.json +++ b/platform/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/cli", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "A CLI to bootstrap new OHIF extension or mode", "type": "module", "main": "src/index.js", diff --git a/platform/cli/templates/mode/src/index.tsx b/platform/cli/templates/mode/src/index.tsx index 3a3f53b891..2d63d10618 100644 --- a/platform/cli/templates/mode/src/index.tsx +++ b/platform/cli/templates/mode/src/index.tsx @@ -53,7 +53,6 @@ function modeFactory({ modeConfiguration }) { const activateTool = () => { toolbarService.recordInteraction({ groupId: 'WindowLevel', - itemId: 'WindowLevel', interactionType: 'tool', commands: [ { diff --git a/platform/core/CHANGELOG.md b/platform/core/CHANGELOG.md index b16beb635d..f8d0f13ae5 100644 --- a/platform/core/CHANGELOG.md +++ b/platform/core/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) diff --git a/platform/core/package.json b/platform/core/package.json index 1352a5d64f..7fb355e0dd 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/core", - "version": "3.7.0-beta.78", + "version": "3.7.0-beta.80", "description": "Generic business logic for web-based medical imaging applications", "author": "OHIF Core Team", "license": "MIT", @@ -35,8 +35,8 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.13.2", - "@ohif/ui": "3.7.0-beta.78", + "@cornerstonejs/dicom-image-loader": "^1.16.5", + "@ohif/ui": "3.7.0-beta.80", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" }, diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts index d7312b81cf..8d01a28dca 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -178,8 +178,15 @@ export default class DisplaySetService extends PubSubService { * @param {string} displaySetInstanceUID * @returns {object} displaySet */ - public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet => - displaySetCache.get(displaySetInstanceUid); + public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet => { + if (typeof displaySetInstanceUid !== 'string') { + throw new Error( + `getDisplaySetByUID: displaySetInstanceUid must be a string, you passed ${displaySetInstanceUid}` + ); + } + + return displaySetCache.get(displaySetInstanceUid); + }; /** * diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index b6f4bc3b69..01fdd8f6ef 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -523,22 +523,32 @@ export default class HangingProtocolService extends PubSubService { stage.viewports.push({ viewportOptions: { ...defaultViewportOptions, - viewportId: uuidv4(), + // Use 'default' for the first viewport, and UUIDs for the rest. + viewportId: i === 0 ? 'default' : uuidv4(), }, displaySets: [], }); } } else { // Clone each viewport to ensure independent objects - stage.viewports = stage.viewports.map(viewport => ({ - ...viewport, - viewportOptions: { - ...(viewport.viewportOptions || defaultViewportOptions), - viewportId: viewport.viewportOptions?.viewportId || uuidv4(), - }, - displaySets: viewport.displaySets || [], - })); + stage.viewports = stage.viewports.map((viewport, index) => { + const existingViewportId = viewport.viewportOptions?.viewportId; + return { + ...viewport, + viewportOptions: { + ...(viewport.viewportOptions || defaultViewportOptions), + // use provided viewportId when available, otherwise use default for first viewport + // and uuid for the rest + viewportId: existingViewportId + ? existingViewportId + : index === 0 + ? 'default' + : uuidv4(), + }, + displaySets: viewport.displaySets || [], + }; + }); stage.viewports.forEach(viewport => { viewport.displaySets.forEach(displaySet => { displaySet.options = displaySet.options || {}; diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.js b/platform/core/src/services/HangingProtocolService/lib/validator.js index ef30551f80..72c0a9549c 100644 --- a/platform/core/src/services/HangingProtocolService/lib/validator.js +++ b/platform/core/src/services/HangingProtocolService/lib/validator.js @@ -77,7 +77,7 @@ validate.validators.doesNotEqual = function (value, options, key) { } } } else if (testValue === dicomArrayValue[0]) { - console.debug(dicomArrayValue, testValue); + console.log(dicomArrayValue, testValue); return `${key} must not equal to ${testValue}`; } }; diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts index e79808296f..919ee94635 100644 --- a/platform/core/src/services/ToolBarService/ToolbarService.ts +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -94,8 +94,10 @@ export default class ToolbarService extends PubSubService { commandsManager.runCommand(commandName, commandOptions, context); }); - // only set the primary tool if no error was thrown - this.state.primaryToolId = itemId; + // only set the primary tool if no error was thrown. + // if the itemId is not undefined use it; otherwise, set the first tool in + // the commands as the primary tool + this.state.primaryToolId = itemId || commands[0].commandOptions?.toolName; } catch (error) { console.warn(error); } @@ -164,7 +166,7 @@ export default class ToolbarService extends PubSubService { this.state.groups[groupId] = itemId; } - this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, {}); + this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, { ...this.state }); } getButtons() { @@ -293,6 +295,10 @@ export default class ToolbarService extends PubSubService { * @param {*} props - Props set by the Viewer layer */ _mapButtonToDisplay(btn, btnSection, metadata, props) { + if (!btn) { + return; + } + const { id, type, component } = btn; const buttonType = this._buttonTypes()[type]; diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index f6e3904efc..ee430ccacb 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -74,6 +74,11 @@ class ViewportGridService extends PubSubService { return this.serviceImplementation._getState(); } + public getActiveViewportId() { + const state = this.getState(); + return state.activeViewportId; + } + public setDisplaySetsForViewport(props) { // Just update a single viewport, but use the multi-viewport update for it. this.setDisplaySetsForViewports([props]); diff --git a/platform/docs/CHANGELOG.md b/platform/docs/CHANGELOG.md index a3bfd00c1d..d4fe0b9a4f 100644 --- a/platform/docs/CHANGELOG.md +++ b/platform/docs/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + # [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) **Note:** Version bump only for package ohif-docs diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md index 0da3cb1699..09d5d7d1e6 100644 --- a/platform/docs/docs/configuration/configurationFiles.md +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -108,8 +108,13 @@ window.config = ({ servicesManager } = {}) => { }; ``` + + + + ## Configuration Options + Here are a list of some options available: - `disableEditing`: If true, it disables editing in OHIF, hiding edit buttons in segmentation panel and locking already stored measurements. @@ -174,6 +179,10 @@ if auth headers are used, a preflight request is required. } ``` - `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies. +- `use16BitDataType`: (default to false), if set to true, it will use 16 bit data type for the image data wherever possible which has + significant impact on reducing the memory usage. However, the 16Bit textures require EXT_texture_norm16 extension in webGL 2.0 (you can check if you have it here https://webglreport.com/?v=2). In addition to the extension, there are reported problems for Intel Macs that might cause the viewer to crash. In summary, it is great a configuration if you have support for it. +- `useSharedArrayBuffer` (default to true), for volume loading we use sharedArrayBuffer to be able to + load the volume progressively as the data arrives (each webworker has the shared buffer and can write to it). However, there might be certain environments that do not support sharedArrayBuffer. In that case, you can set this flag to false and the viewer will use the regular arrayBuffer which might be slower for large volume loading. - `supportsWildcard`: (default to false), if set to true, the datasource will support wildcard matching for patient name and patient id. - `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
Points to consider while using `dangerouslyUseDynamicConfig`:
diff --git a/platform/docs/docs/deployment/build-for-production.md b/platform/docs/docs/deployment/build-for-production.md index 7c061a89eb..217f38da28 100644 --- a/platform/docs/docs/deployment/build-for-production.md +++ b/platform/docs/docs/deployment/build-for-production.md @@ -116,6 +116,26 @@ In the video below notice that there is `platform/viewer` which has been renamed
+### Build for non-root path + +If you would like to access the viewer from a non-root path (e.g., `/my-awesome-viewer` instead of `/`), +You can achieve so by using the `PUBLIC_URL` environment variable AND the `routerBasename` configuration option. + +1. use a config (e.g. config/myConfig.js) file that is using the `routerBasename` of your choice `/my-awesome-viewer` (note there is only one / - it is not /my-awesome-viewer/). +2. build the viewer with `PUBLIC_URL=/my-awesome-viewer/ APP_CONFIG=config/myConfig.js yarn build` (note there are two / - it is not /my-awesome-viewer). + + +:::tip +The PUBLIC_URL tells the application where to find the static assets and the routerBasename will tell the application how to handle the routes +::: + +:::tip +Testing, you can use `npx http-server` to serve the files in the generated `dist` folder and access the viewer from `http://localhost:8080/my-awesome-viewer`. To achieve +so, you should first rename the `dist` folder to `my-awesome-viewer` and then change the working directory +to the `platform/app` folder and run `npx http-server ./`. Then on the browser, you can access the viewer from `http://localhost:8080/my-awesome-viewer` +::: + + ### Automating Builds and Deployments If you found setting up your environment and running all of these steps to be a diff --git a/platform/docs/docs/deployment/iframe.md b/platform/docs/docs/deployment/iframe.md index 2deac3fdb8..c1c0fb8706 100644 --- a/platform/docs/docs/deployment/iframe.md +++ b/platform/docs/docs/deployment/iframe.md @@ -28,7 +28,7 @@ It is also required that the PUBLIC_URL environment variable is set to the same `