From 0b34d255e801d16ed8e74406e60ee8fabe9da681 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso Date: Tue, 21 Mar 2023 12:15:21 -0400 Subject: [PATCH 1/4] feat(DoubleClick): double click a viewport to one up and back Added a toggleOneUp command that puts the active viewport into a 1x1 grid layout and it toggles out of 'one-up' by restoring its saved 'toggleOneUpViewportGridStore' from the StateSyncService. Added double click customization for the Cornerstone extension with the default double click handling being the toggleOneUp command. Added a cypress test for the double click functionality. --- extensions/cornerstone/src/init.tsx | 12 ++ extensions/cornerstone/src/initDoubleClick.ts | 59 +++++++++ extensions/default/src/commandsModule.ts | 121 +++++++++++++++++- .../ViewportGridService.ts | 21 ++- platform/core/src/types/Command.ts | 2 +- platform/core/src/utils/index.js | 2 + platform/core/src/utils/index.test.js | 1 + .../subscribeToNextViewportGridChange.ts | 37 ++++++ .../contextProviders/ViewportGridProvider.tsx | 20 ++- .../customization/OHIFDoubleClick.spec.js | 52 ++++++++ platform/viewer/public/config/e2e.js | 1 + 11 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 extensions/cornerstone/src/initDoubleClick.ts create mode 100644 platform/core/src/utils/subscribeToNextViewportGridChange.ts create mode 100644 platform/viewer/cypress/integration/customization/OHIFDoubleClick.spec.js diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index c44ec812d8..ed8474fbdf 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -25,6 +25,7 @@ import interleaveCenterLoader from './utils/interleaveCenterLoader'; import nthLoader from './utils/nthLoader'; import interleaveTopToBottom from './utils/interleaveTopToBottom'; import initContextMenu from './initContextMenu'; +import initDoubleClick from './initDoubleClick'; // TODO: Cypress tests are currently grabbing this from the window? window.cornerstone = cornerstone; @@ -104,6 +105,12 @@ export default async function init({ clearOnModeExit: true, }); + // Stores the entire ViewportGridService getState when toggling to one up + // (e.g. via a double click) so that it can be restored when toggling back. + stateSyncService.register('toggleOneUpViewportGridStore', { + clearOnModeExit: true, + }); + const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap; @@ -181,6 +188,11 @@ export default async function init({ commandsManager, }); + initDoubleClick({ + customizationService, + commandsManager, + }); + const newStackCallback = evt => { const { element } = evt.detail; utilities.stackPrefetch.enable(element); diff --git a/extensions/cornerstone/src/initDoubleClick.ts b/extensions/cornerstone/src/initDoubleClick.ts new file mode 100644 index 0000000000..12ddde514d --- /dev/null +++ b/extensions/cornerstone/src/initDoubleClick.ts @@ -0,0 +1,59 @@ +import { eventTarget, EVENTS } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; +import { CommandsManager, CustomizationService, Types } from '@ohif/core'; + +const cs3DToolsEvents = Enums.Events; + +const DEFAULT_DOUBLE_CLICK_COMMAND: Types.Command = { + commandName: 'toggleOneUp', + commandOptions: {}, +}; + +export type initDoubleClickArgs = { + customizationService: CustomizationService; + commandsManager: CommandsManager; +}; + +function initDoubleClick({ + customizationService, + commandsManager, +}: initDoubleClickArgs): void { + const cornerstoneViewportHandleDoubleClick = _ => { + const customizations: Types.Command | Types.CommandCustomization = + (customizationService.get( + 'cornerstoneViewportDoubleClickCommands' + ) as Types.CommandCustomization) || DEFAULT_DOUBLE_CLICK_COMMAND; + + commandsManager.run(customizations); + }; + + function elementEnabledHandler(evt: CustomEvent) { + const { element } = evt.detail; + + element.addEventListener( + cs3DToolsEvents.MOUSE_DOUBLE_CLICK, + cornerstoneViewportHandleDoubleClick + ); + } + + function elementDisabledHandler(evt: CustomEvent) { + const { element } = evt.detail; + + element.removeEventListener( + cs3DToolsEvents.MOUSE_DOUBLE_CLICK, + cornerstoneViewportHandleDoubleClick + ); + } + + eventTarget.addEventListener( + EVENTS.ELEMENT_ENABLED, + elementEnabledHandler.bind(null) + ); + + eventTarget.addEventListener( + EVENTS.ELEMENT_DISABLED, + elementDisabledHandler.bind(null) + ); +} + +export default initDoubleClick; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index e44651101f..a9e7b275e1 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -1,4 +1,4 @@ -import { ServicesManager, Types } from '@ohif/core'; +import { ServicesManager, utils } from '@ohif/core'; import { ContextMenuController, @@ -12,6 +12,8 @@ import findViewportsByPosition, { import { ContextMenuProps } from './CustomizeableContextMenu/types'; +const { subscribeToNextViewportGridChange } = utils; + export type HangingProtocolParams = { protocolId?: string; stageIndex?: number; @@ -165,6 +167,7 @@ const commandsModule = ({ protocolId, stageId, stageIndex, + reset = false, }: HangingProtocolParams): boolean => { try { // Stores in the state the reuseID to displaySetUID mapping @@ -210,10 +213,11 @@ const commandsModule = ({ hangingProtocolService.setActiveStudyUID(activeStudyUID); } - const storedHanging = `${hangingProtocolService.getState().activeStudyUID + const storedHanging = `${ + hangingProtocolService.getState().activeStudyUID }:${protocolId}:${useStageIdx || 0}`; - const restoreProtocol = !!viewportGridStore[storedHanging]; + const restoreProtocol = !reset && viewportGridStore[storedHanging]; if ( protocolId === hpInfo.protocolId && @@ -273,8 +277,9 @@ const commandsModule = ({ activeStudy, } = hangingProtocolService.getActiveProtocol(); const { toggleHangingProtocol } = stateSyncService.getState(); - const storedHanging = `${activeStudy.StudyInstanceUID - }:${protocolId}:${stageIndex | 0}`; + const storedHanging = `${ + activeStudy.StudyInstanceUID + }:${protocolId}:${stageIndex | 0}`; if ( protocol.id === protocolId && (stageIndex === undefined || stageIndex === desiredStageIndex) @@ -294,7 +299,11 @@ const commandsModule = ({ }, }, }); - return actions.setHangingProtocol({ protocolId, stageIndex }); + return actions.setHangingProtocol({ + protocolId, + stageIndex, + reset: true, + }); } }, @@ -365,6 +374,101 @@ const commandsModule = ({ window.setTimeout(completeLayout, 0); }, + toggleOneUp() { + const viewportGridState = viewportGridService.getState(); + const { activeViewportIndex, viewports, layout } = viewportGridState; + const { + displaySetInstanceUIDs, + displaySetOptions, + viewportOptions, + } = viewports[activeViewportIndex]; + + if (layout.numCols === 1 && layout.numRows === 1) { + // The viewer is in one-up. Check if there is a state to restore/toggle back to. + const { toggleOneUpViewportGridStore } = stateSyncService.getState(); + + if (toggleOneUpViewportGridStore.layout) { + // There is a state to toggle back to. The viewport that was + // originally toggled to one up was the former active viewport. + const viewportIndexToUpdate = + toggleOneUpViewportGridStore.activeViewportIndex; + + // Determine which viewports need to be updated. This is particularly + // important for MPR. + const updatedViewports = displaySetInstanceUIDs + .map(displaySetInstanceUID => + hangingProtocolService.getViewportsRequireUpdate( + viewportIndexToUpdate, + displaySetInstanceUID + ) + ) + .flat(); + + // This findOrCreateViewport returns either one of the updatedViewports + // returned from the HP service OR if there is not one from the HP service then + // simply returns what was in the previous state. + const findOrCreateViewport = (viewportIndex: number) => { + const viewport = updatedViewports.find( + viewport => viewport.viewportIndex === viewportIndex + ); + + return viewport + ? { viewportOptions, displaySetOptions, ...viewport } + : toggleOneUpViewportGridStore.viewports[viewportIndex]; + // } + }; + + // Restore the previous layout including the active viewport. + viewportGridService.setLayout({ + numRows: toggleOneUpViewportGridStore.layout.numRows, + numCols: toggleOneUpViewportGridStore.layout.numCols, + activeViewportIndex: viewportIndexToUpdate, + findOrCreateViewport, + }); + } + } else { + // We are not in one-up, so toggle to one up. + + // This findOrCreateViewport only return one viewport - the active + // one being toggled to one up. + const findOrCreateViewport = () => { + return { + displaySetInstanceUIDs, + displaySetOptions, + viewportOptions, + }; + }; + + // Set the layout to be 1x1/one-up. + viewportGridService.setLayout({ + numRows: 1, + numCols: 1, + findOrCreateViewport, + }); + + // Store the current viewport grid state so we can toggle it back later. + stateSyncService.store({ + toggleOneUpViewportGridStore: viewportGridState, + }); + + // Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that + // any grid layout state to toggle to from one up is cleared. This is performed on + // a timeout to avoid clearing the state for the actual to one up change. + // Whenever the next layout change event is fired, the subscriptions are unsubscribed. + const clearToggleOneUpViewportGridStore = () => { + const toggleOneUpViewportGridStore = {}; + stateSyncService.store({ + toggleOneUpViewportGridStore, + }); + }; + + subscribeToNextViewportGridChange( + viewportGridService, + clearToggleOneUpViewportGridStore + ); + } + }, + openDICOMTagViewer() { const { activeViewportIndex, viewports } = viewportGridService.getState(); const activeViewportSpecificData = viewports[activeViewportIndex]; @@ -440,6 +544,11 @@ const commandsModule = ({ storeContexts: [], options: {}, }, + toggleOneUp: { + commandFn: actions.toggleOneUp, + storeContexts: [], + options: {}, + }, openDICOMTagViewer: { commandFn: actions.openDICOMTagViewer, }, diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index cfa5a5bc52..915ff0fe67 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -2,6 +2,8 @@ import { PubSubService } from '../_shared/pubSubServiceInterface'; const EVENTS = { ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged', + LAYOUT_CHANGED: 'event::layoutChanged', + GRID_STATE_CHANGED: 'event::gridStateChanged', }; class ViewportGridService extends PubSubService { @@ -101,12 +103,26 @@ class ViewportGridService extends PubSubService { * options that is initially provided as {} (eg to store intermediate state) * The function returns a viewport object to use at the given position. */ - public setLayout({ numCols, numRows, findOrCreateViewport = undefined }) { + public setLayout({ + numCols, + numRows, + layoutOptions, + layoutType = 'grid', + activeViewportIndex = undefined, + findOrCreateViewport = undefined, + }) { this.serviceImplementation._setLayout({ numCols, numRows, + layoutOptions, + layoutType, + activeViewportIndex, findOrCreateViewport, }); + this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, { + numCols, + numRows, + }); } public reset() { @@ -125,6 +141,9 @@ class ViewportGridService extends PubSubService { public set(state) { this.serviceImplementation._set(state); + this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, { + state, + }); } public getNumViewportPanes() { diff --git a/platform/core/src/types/Command.ts b/platform/core/src/types/Command.ts index 0b6129d0c0..837c835907 100644 --- a/platform/core/src/types/Command.ts +++ b/platform/core/src/types/Command.ts @@ -6,5 +6,5 @@ export interface Command { /** A set of commands, typically contained in a tool item or other configuration */ export interface Commands { - commands: Commands[]; + commands: Command[]; } diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index d428a66480..4c54c32efe 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -32,6 +32,7 @@ import { sortingCriteria, seriesSortCriteria, } from './sortStudy'; +import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange'; // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -69,6 +70,7 @@ const utils = { debounce, roundNumber, downloadCSVReport, + subscribeToNextViewportGridChange, }; export { diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 9b83f8db5a..cae7691913 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -35,6 +35,7 @@ describe('Top level exports', () => { 'resolveObjectPath', 'hierarchicalListUtils', 'progressTrackingUtils', + 'subscribeToNextViewportGridChange', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/core/src/utils/subscribeToNextViewportGridChange.ts b/platform/core/src/utils/subscribeToNextViewportGridChange.ts new file mode 100644 index 0000000000..e6c72d0485 --- /dev/null +++ b/platform/core/src/utils/subscribeToNextViewportGridChange.ts @@ -0,0 +1,37 @@ +import { ViewportGridService } from '../services'; + +/** + * Subscribes to the very next LAYOUT_CHANGED or GRID_STATE_CHANGED event that + * is not currently on the event queue. The subscriptions are made on a 'zero' + * timeout so as to avoid responding to any of those events currently on the event queue. + * The subscription persists only for a single invocation of either event. + * Once either event is fired, the subscriptions are unsubscribed. + * @param viewportGridService the viewport grid service to subscribe to + * @param gridChangeCallback the callback + */ +function subscribeToNextViewportGridChange( + viewportGridService: ViewportGridService, + gridChangeCallback: (arg: unknown) => void +): void { + const subscriber = () => { + const callback = (callbackProps: unknown) => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + gridChangeCallback(callbackProps); + }; + + const subscriptions = [ + viewportGridService.subscribe( + viewportGridService.EVENTS.LAYOUT_CHANGED, + callback + ), + viewportGridService.subscribe( + viewportGridService.EVENTS.GRID_STATE_CHANGED, + callback + ), + ]; + }; + + window.setTimeout(subscriber, 0); +} + +export { subscribeToNextViewportGridChange }; diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx index c3bff44dfd..d74a6d7158 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx @@ -146,6 +146,7 @@ export function ViewportGridProvider({ children, service }) { numRows, layoutOptions, layoutType = 'grid', + activeViewportIndex, findOrCreateViewport, } = action.payload; @@ -160,7 +161,7 @@ export function ViewportGridProvider({ children, service }) { // haven't been viewed yet, and add them in the appropriate order. const options = {}; - let activeViewportIndex; + let activeViewportIndexToSet = activeViewportIndex; for (let row = 0; row < numRows; row++) { for (let col = 0; col < numCols; col++) { const pos = col + row * numCols; @@ -170,10 +171,11 @@ export function ViewportGridProvider({ children, service }) { continue; } if ( - !activeViewportIndex || - state.viewports[pos]?.positionId === positionId + activeViewportIndexToSet == null && + state.viewports[state.activeViewportIndex]?.positionId === + positionId ) { - activeViewportIndex = pos; + activeViewportIndexToSet = pos; } const viewport = findOrCreateViewport(pos, positionId, options); if (!viewport) continue; @@ -199,6 +201,8 @@ export function ViewportGridProvider({ children, service }) { } } + activeViewportIndexToSet = activeViewportIndexToSet ?? 0; + const viewportIdSet = {}; for ( let viewportIndex = 0; @@ -223,7 +227,7 @@ export function ViewportGridProvider({ children, service }) { const ret = { ...state, - activeViewportIndex, + activeViewportIndex: activeViewportIndexToSet, layout: { ...state.layout, numCols, @@ -300,6 +304,7 @@ export function ViewportGridProvider({ children, service }) { numRows, numCols, layoutOptions = [], + activeViewportIndex, findOrCreateViewport, }) => dispatch({ @@ -309,6 +314,7 @@ export function ViewportGridProvider({ children, service }) { numRows, numCols, layoutOptions, + activeViewportIndex, findOrCreateViewport, }, }), @@ -375,9 +381,9 @@ export function ViewportGridProvider({ children, service }) { setActiveViewportIndex: index => service.setActiveViewportIndex(index), // run it through the service itself since we want to publish events setDisplaySetsForViewport, setDisplaySetsForViewports, - setLayout, + setLayout: layout => service.setLayout(layout), // run it through the service itself since we want to publish events reset, - set, + set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events getNumViewportPanes, }; diff --git a/platform/viewer/cypress/integration/customization/OHIFDoubleClick.spec.js b/platform/viewer/cypress/integration/customization/OHIFDoubleClick.spec.js new file mode 100644 index 0000000000..d60c1be911 --- /dev/null +++ b/platform/viewer/cypress/integration/customization/OHIFDoubleClick.spec.js @@ -0,0 +1,52 @@ +describe('OHIF Double Click', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '&hangingProtocolId=@ohif/hp-extension.mn' + ); + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + }); + + it('Should double click each viewport to one up and back', () => { + const numExpectedViewports = 3; + cy.get('[data-cy="viewport-pane"]') + .its('length') + .should('be.eq', numExpectedViewports); + + for (let i = 0; i < numExpectedViewports; i += 1) { + // For whatever reason, with Cypress tests, we have to activate the + // viewport we are double clicking first. + cy.get('[data-cy="viewport-pane"]') + .eq(i) + .trigger('mousedown', 'center', { force: true }) + .trigger('mouseup', 'center', { force: true }); + + // Wait for the viewport to be 'active'. + // TODO Is there a better way to do this? + cy.get('[data-cy="viewport-pane"]') + .eq(i) + .parent() + .find('[data-cy="viewport-pane"]') + .not('.pointer-events-none'); + + // The actual double click. + cy.get('[data-cy="viewport-pane"]') + .eq(i) + .trigger('dblclick', 'center'); + + cy.get('[data-cy="viewport-pane"]') + .its('length') + .should('be.eq', 1); + + cy.get('[data-cy="viewport-pane"]') + .eq(0) + .trigger('dblclick', 'center'); + + cy.get('[data-cy="viewport-pane"]') + .its('length') + .should('be.eq', numExpectedViewports); + } + }); +}); diff --git a/platform/viewer/public/config/e2e.js b/platform/viewer/public/config/e2e.js index 2c090b9e77..462c07e3fa 100644 --- a/platform/viewer/public/config/e2e.js +++ b/platform/viewer/public/config/e2e.js @@ -122,6 +122,7 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + { commandName: 'toggleOneUp', label: 'Toggle One Up', keys: ['w'] }, // { // commandName: 'previousViewportDisplaySet', // label: 'Previous Series', From cc43b8be3aeb2c88315e0e452fc4161623c62994 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso Date: Tue, 28 Mar 2023 16:34:40 -0400 Subject: [PATCH 2/4] PR feedback: - tracked viewport measurements no longer show as dashed when toggling one up - disallowed double clicking near a measurement - updated cornerstone3D dependencies to fix double click of TMTV and volume viewport 3D - created ViewportGridService.getLayoutOptionsFromState --- extensions/cornerstone-dicom-sr/package.json | 4 +- extensions/cornerstone/package.json | 4 +- extensions/cornerstone/src/initContextMenu.ts | 25 +---- extensions/cornerstone/src/initDoubleClick.ts | 10 +- .../src/utils/findNearbyToolData.ts | 21 ++++ extensions/default/src/commandsModule.ts | 97 +++++++++++-------- extensions/measurement-tracking/package.json | 4 +- .../viewports/TrackedCornerstoneViewport.tsx | 2 +- .../ViewportGridService.ts | 11 +++ platform/viewer/public/config/e2e.js | 1 - yarn.lock | 26 ++--- 11 files changed, 113 insertions(+), 92 deletions(-) create mode 100644 extensions/cornerstone/src/utils/findNearbyToolData.ts diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index e302b690c3..66e4a2aad2 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,7 +46,7 @@ "@babel/runtime": "^7.20.13", "classnames": "^2.3.2", "@cornerstonejs/adapters": "^0.4.1", - "@cornerstonejs/core": "^0.36.5", - "@cornerstonejs/tools": "^0.55.1" + "@cornerstonejs/core": "^0.37.0", + "@cornerstonejs/tools": "^0.57.1" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 4aceecc654..2d4767d058 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -44,9 +44,9 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@cornerstonejs/adapters": "^0.4.1", - "@cornerstonejs/core": "^0.36.5", + "@cornerstonejs/core": "^0.37.0", "@cornerstonejs/streaming-image-volume-loader": "^0.15.1", - "@cornerstonejs/tools": "^0.55.1", + "@cornerstonejs/tools": "^0.57.1", "@kitware/vtk.js": "26.5.6", "html2canvas": "^1.4.1", "lodash.debounce": "4.0.8", diff --git a/extensions/cornerstone/src/initContextMenu.ts b/extensions/cornerstone/src/initContextMenu.ts index a9f69db51e..c46e5053e9 100644 --- a/extensions/cornerstone/src/initContextMenu.ts +++ b/extensions/cornerstone/src/initContextMenu.ts @@ -1,6 +1,7 @@ import { eventTarget, EVENTS } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/tools'; import { setEnabledElement } from './state'; +import { findNearbyToolData } from './utils/findNearbyToolData'; const cs3DToolsEvents = Enums.Events; @@ -47,28 +48,6 @@ function initContextMenu({ customizationService, commandsManager, }): void { - /** - * Finds tool nearby event position triggered. - * - * @param {Object} commandsManager mannager of commands - * @param {Object} event that has being triggered - * @returns cs toolData or undefined if not found. - */ - const findNearbyToolData = evt => { - if (!evt?.detail) { - return; - } - const { element, currentPoints } = evt.detail; - return commandsManager.runCommand( - 'getNearbyToolData', - { - element, - canvasCoordinates: currentPoints?.canvas, - }, - 'CORNERSTONE' - ); - }; - /* * Run the commands associated with the given button press, * defaults on button1 and button2 @@ -80,7 +59,7 @@ function initContextMenu({ const toRun = customizations[name]; console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun); const options = { - nearbyToolData: findNearbyToolData(evt), + nearbyToolData: findNearbyToolData(commandsManager, evt), event: evt, }; commandsManager.run(toRun, options); diff --git a/extensions/cornerstone/src/initDoubleClick.ts b/extensions/cornerstone/src/initDoubleClick.ts index 12ddde514d..bd1f7eeeda 100644 --- a/extensions/cornerstone/src/initDoubleClick.ts +++ b/extensions/cornerstone/src/initDoubleClick.ts @@ -1,6 +1,7 @@ import { eventTarget, EVENTS } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/tools'; import { CommandsManager, CustomizationService, Types } from '@ohif/core'; +import { findNearbyToolData } from './utils/findNearbyToolData'; const cs3DToolsEvents = Enums.Events; @@ -18,7 +19,14 @@ function initDoubleClick({ customizationService, commandsManager, }: initDoubleClickArgs): void { - const cornerstoneViewportHandleDoubleClick = _ => { + const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => { + // Do not allow double click on a tool. + const nearbyToolData = findNearbyToolData(commandsManager, evt); + if (nearbyToolData) { + return; + } + + // Allows for the customization of the double click on a viewport. const customizations: Types.Command | Types.CommandCustomization = (customizationService.get( 'cornerstoneViewportDoubleClickCommands' diff --git a/extensions/cornerstone/src/utils/findNearbyToolData.ts b/extensions/cornerstone/src/utils/findNearbyToolData.ts new file mode 100644 index 0000000000..ab57a67d25 --- /dev/null +++ b/extensions/cornerstone/src/utils/findNearbyToolData.ts @@ -0,0 +1,21 @@ +/** + * Finds tool nearby event position triggered. + * + * @param {Object} commandsManager mannager of commands + * @param {Object} event that has being triggered + * @returns cs toolData or undefined if not found. + */ +export const findNearbyToolData = (commandsManager, evt) => { + if (!evt?.detail) { + return; + } + const { element, currentPoints } = evt.detail; + return commandsManager.runCommand( + 'getNearbyToolData', + { + element, + canvasCoordinates: currentPoints?.canvas, + }, + 'CORNERSTONE' + ); +}; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index a9e7b275e1..b2a80cbfe8 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -161,6 +161,7 @@ const commandsModule = ({ * @param options.protocolId - the protocol ID to change to * @param options.stageId - the stageId to apply * @param options.stageIndex - the index of the stage to go to. + * @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state */ setHangingProtocol: ({ activeStudyUID = '', @@ -387,48 +388,63 @@ const commandsModule = ({ // The viewer is in one-up. Check if there is a state to restore/toggle back to. const { toggleOneUpViewportGridStore } = stateSyncService.getState(); - if (toggleOneUpViewportGridStore.layout) { - // There is a state to toggle back to. The viewport that was - // originally toggled to one up was the former active viewport. - const viewportIndexToUpdate = - toggleOneUpViewportGridStore.activeViewportIndex; - - // Determine which viewports need to be updated. This is particularly - // important for MPR. - const updatedViewports = displaySetInstanceUIDs - .map(displaySetInstanceUID => - hangingProtocolService.getViewportsRequireUpdate( - viewportIndexToUpdate, - displaySetInstanceUID - ) - ) - .flat(); - - // This findOrCreateViewport returns either one of the updatedViewports - // returned from the HP service OR if there is not one from the HP service then - // simply returns what was in the previous state. - const findOrCreateViewport = (viewportIndex: number) => { - const viewport = updatedViewports.find( - viewport => viewport.viewportIndex === viewportIndex - ); - - return viewport - ? { viewportOptions, displaySetOptions, ...viewport } - : toggleOneUpViewportGridStore.viewports[viewportIndex]; - // } - }; - - // Restore the previous layout including the active viewport. - viewportGridService.setLayout({ - numRows: toggleOneUpViewportGridStore.layout.numRows, - numCols: toggleOneUpViewportGridStore.layout.numCols, - activeViewportIndex: viewportIndexToUpdate, - findOrCreateViewport, - }); + if (!toggleOneUpViewportGridStore.layout) { + return; } + // There is a state to toggle back to. The viewport that was + // originally toggled to one up was the former active viewport. + const viewportIndexToUpdate = + toggleOneUpViewportGridStore.activeViewportIndex; + + // Determine which viewports need to be updated. This is particularly + // important when MPR is toggled to one up and a different reconstructable + // is swapped in. Note that currently HangingProtocolService.getViewportsRequireUpdate + // does not support viewport with multiple display sets. + const updatedViewports = + displaySetInstanceUIDs.length > 1 + ? [] + : displaySetInstanceUIDs + .map(displaySetInstanceUID => + hangingProtocolService.getViewportsRequireUpdate( + viewportIndexToUpdate, + displaySetInstanceUID + ) + ) + .flat(); + + // This findOrCreateViewport returns either one of the updatedViewports + // returned from the HP service OR if there is not one from the HP service then + // simply returns what was in the previous state. + const findOrCreateViewport = (viewportIndex: number) => { + const viewport = updatedViewports.find( + viewport => viewport.viewportIndex === viewportIndex + ); + + return viewport + ? { viewportOptions, displaySetOptions, ...viewport } + : toggleOneUpViewportGridStore.viewports[viewportIndex]; + }; + + const layoutOptions = viewportGridService.getLayoutOptionsFromState( + toggleOneUpViewportGridStore + ); + + // Restore the previous layout including the active viewport. + viewportGridService.setLayout({ + numRows: toggleOneUpViewportGridStore.layout.numRows, + numCols: toggleOneUpViewportGridStore.layout.numCols, + activeViewportIndex: viewportIndexToUpdate, + layoutOptions, + findOrCreateViewport, + }); } else { // We are not in one-up, so toggle to one up. + // Store the current viewport grid state so we can toggle it back later. + stateSyncService.store({ + toggleOneUpViewportGridStore: viewportGridState, + }); + // This findOrCreateViewport only return one viewport - the active // one being toggled to one up. const findOrCreateViewport = () => { @@ -446,11 +462,6 @@ const commandsModule = ({ findOrCreateViewport, }); - // Store the current viewport grid state so we can toggle it back later. - stateSyncService.store({ - toggleOneUpViewportGridStore: viewportGridState, - }); - // Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that // any grid layout state to toggle to from one up is cleared. This is performed on // a timeout to avoid clearing the state for the actual to one up change. diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 1582d8784a..3241180f97 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "classnames": "^2.3.2", - "@cornerstonejs/core": "^0.36.5", - "@cornerstonejs/tools": "^0.55.1", + "@cornerstonejs/core": "^0.37.0", + "@cornerstonejs/tools": "^0.57.1", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", "dcmjs": "^0.29.4", "lodash.debounce": "^4.17.21", diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 97ac7affca..5e5393f53e 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -80,7 +80,7 @@ function TrackedCornerstoneViewport(props) { return; } - annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, { + annotation.config.style.setViewportToolStyles(viewportId, { global: { lineDash: '4,4', }, diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index 915ff0fe67..be98756701 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -149,6 +149,17 @@ class ViewportGridService extends PubSubService { public getNumViewportPanes() { return this.serviceImplementation._getNumViewportPanes(); } + + public getLayoutOptionsFromState(state) { + return state.viewports.map(viewport => { + return { + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + }; + }); + } } export default ViewportGridService; diff --git a/platform/viewer/public/config/e2e.js b/platform/viewer/public/config/e2e.js index 462c07e3fa..2c090b9e77 100644 --- a/platform/viewer/public/config/e2e.js +++ b/platform/viewer/public/config/e2e.js @@ -122,7 +122,6 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { commandName: 'toggleOneUp', label: 'Toggle One Up', keys: ['w'] }, // { // commandName: 'previousViewportDisplaySet', // label: 'Previous Series', diff --git a/yarn.lock b/yarn.lock index ca74f1038a..c72dc8e0ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1451,18 +1451,10 @@ detect-gpu "^4.0.45" lodash.clonedeep "4.5.0" -"@cornerstonejs/core@^0.36.2": - version "0.36.2" - resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.36.2.tgz#205573bfd75a273fa6bca587662b4dd1f5e46167" - integrity sha512-jJYawOjLGop18O426YxyBepkiaYT/xHMa3mqhToIO5KRxK/UYSGHt0RS8wSHeFR8pNkCAHepcdfhyIVkAln66g== - dependencies: - detect-gpu "^4.0.45" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/core@^0.36.5": - version "0.36.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.36.5.tgz#2e8c2fc2f9d00c2b5a0f1666aa575ab5024f3616" - integrity sha512-5Z4GEjpWaYbEbSpbgNc92oiwTcD8TUpVw1TUQ5UC4OkzxBMN4THmkGifRq5kJpxJTahGVsoU4W1S7of+YsULlg== +"@cornerstonejs/core@^0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.37.0.tgz#d0cc59116d5393c995e01f9cb58b7d96acd46fa8" + integrity sha512-EPKUsEFp9X7ZOhDS5EoNpuJDs4KjlCodDTYYU8ITU9pzW9gJj5j3IivJy5mMI2tDq/rpdxCoL0TSn3KVsJvp5Q== dependencies: detect-gpu "^4.0.45" lodash.clonedeep "4.5.0" @@ -1475,12 +1467,12 @@ "@cornerstonejs/core" "^0.35.1" cornerstone-wado-image-loader "^4.10.0" -"@cornerstonejs/tools@^0.55.1": - version "0.55.1" - resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.55.1.tgz#cde00881c35a43f2d10c634350e46f45e8bf9a7b" - integrity sha512-fYRNqnS9WXWBrkOd++nocKqjlD6p3BvFtPCJeaA8M2bdgEHEHK+KbM999GG5YTmWV88xLW4mjSl+wJvbdWUwEQ== +"@cornerstonejs/tools@^0.57.1": + version "0.57.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.57.1.tgz#687e097e0ee8884b0e65ec4b0e8a7afed3f1a5de" + integrity sha512-CEBVi+LvYs9vDhItgz1q6KUzQRYb7SX0sPMIXNBMl0Ia5iCx3YOvKnX/ryNVXHZsenNdG3356cGjyexFzPkrSw== dependencies: - "@cornerstonejs/core" "^0.36.2" + "@cornerstonejs/core" "^0.37.0" lodash.clonedeep "4.5.0" lodash.get "^4.4.2" From 6f94c7f3aab6f14e00d64c8860843ecb7d4af87c Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso Date: Wed, 29 Mar 2023 09:16:20 -0400 Subject: [PATCH 3/4] Updated the ViewportGridService docs. --- .../platform/services/ui/viewport-grid-service.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/platform/docs/docs/platform/services/ui/viewport-grid-service.md b/platform/docs/docs/platform/services/ui/viewport-grid-service.md index b6806343e2..d523a7415a 100644 --- a/platform/docs/docs/platform/services/ui/viewport-grid-service.md +++ b/platform/docs/docs/platform/services/ui/viewport-grid-service.md @@ -9,6 +9,15 @@ sidebar_label: Viewport Grid Service This is a new UI service, that handles the grid layout of the viewer. +## Events + +There are seven events that get publish in `ViewportGridService `: + +| Event | Description | +| ----------------------------- | --------------------------------------------------| +| ACTIVE_VIEWPORT_INDEX_CHANGED | Fires the index of the active viewport is changed | +| LAYOUT_CHANGED | Fires the layout is changed | +| GRID_STATE_CHANGED | Fires when the entire grid state is changed | ## Interface For a more detailed look on the options and return values each of these methods @@ -19,9 +28,10 @@ is expected to support, [check out it's interface in `@ohif/core`][interface] | `setActiveViewportIndex(index)` | Sets the active viewport index in the app | | `getState()` | Gets the states of the viewport (see below) | | `setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id | -| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | +| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | | `reset()` | Resets the default states | | `getNumViewportPanes()` | Gets the number of visible viewport panes | +| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state| ## Implementations From 2c0fac0f9d3b001ba2dc5005bc9dbfb6c69391fd Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso Date: Wed, 29 Mar 2023 10:31:48 -0400 Subject: [PATCH 4/4] Switched to using 'cornerstoneViewportClickCommands' and consistency with the context menu clicks. --- extensions/cornerstone/src/initDoubleClick.ts | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/extensions/cornerstone/src/initDoubleClick.ts b/extensions/cornerstone/src/initDoubleClick.ts index bd1f7eeeda..da8fb8fa49 100644 --- a/extensions/cornerstone/src/initDoubleClick.ts +++ b/extensions/cornerstone/src/initDoubleClick.ts @@ -5,11 +5,29 @@ import { findNearbyToolData } from './utils/findNearbyToolData'; const cs3DToolsEvents = Enums.Events; -const DEFAULT_DOUBLE_CLICK_COMMAND: Types.Command = { - commandName: 'toggleOneUp', - commandOptions: {}, +const DEFAULT_DOUBLE_CLICK = { + doubleClick: { + commandName: 'toggleOneUp', + commandOptions: {}, + }, }; +/** + * Generates a double click event name, consisting of: + * * alt when the alt key is down + * * ctrl when the cctrl key is down + * * shift when the shift key is down + * * 'doubleClick' + */ +function getDoubleClickEventName(evt: CustomEvent) { + const nameArr = []; + if (evt.detail.event.altKey) nameArr.push('alt'); + if (evt.detail.event.ctrlKey) nameArr.push('ctrl'); + if (evt.detail.event.shiftKey) nameArr.push('shift'); + nameArr.push('doubleClick'); + return nameArr.join(''); +} + export type initDoubleClickArgs = { customizationService: CustomizationService; commandsManager: CommandsManager; @@ -26,13 +44,20 @@ function initDoubleClick({ return; } + const eventName = getDoubleClickEventName(evt); + // Allows for the customization of the double click on a viewport. - const customizations: Types.Command | Types.CommandCustomization = - (customizationService.get( - 'cornerstoneViewportDoubleClickCommands' - ) as Types.CommandCustomization) || DEFAULT_DOUBLE_CLICK_COMMAND; + const customizations = + customizationService.get('cornerstoneViewportClickCommands') || + DEFAULT_DOUBLE_CLICK; + + const toRun = customizations[eventName]; + + if (!toRun) { + return; + } - commandsManager.run(customizations); + commandsManager.run(toRun); }; function elementEnabledHandler(evt: CustomEvent) {