diff --git a/extensions/cornerstone/src/ConnectedCornerstoneViewport.js b/extensions/cornerstone/src/ConnectedCornerstoneViewport.js index 73a5a05ae..2ea40f70a 100644 --- a/extensions/cornerstone/src/ConnectedCornerstoneViewport.js +++ b/extensions/cornerstone/src/ConnectedCornerstoneViewport.js @@ -2,6 +2,7 @@ import CornerstoneViewport from 'react-cornerstone-viewport'; import OHIF from '@ohif/core'; import { connect } from 'react-redux'; import throttle from 'lodash.throttle'; +import { setEnabledElement } from './state'; const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions; const { @@ -80,11 +81,11 @@ const mapDispatchToProps = (dispatch, ownProps) => { */ onElementEnabled: event => { const enabledElement = event.detail.element; + setEnabledElement(viewportIndex, enabledElement); dispatch( setViewportSpecificData(viewportIndex, { // TODO: Hack to make sure our plugin info is available from the outset plugin: 'cornerstone', - dom: enabledElement, }) ); }, diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index 17011a766..5b71edf1a 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -1,14 +1,12 @@ import cornerstone from 'cornerstone-core'; import cornerstoneTools from 'cornerstone-tools'; import OHIF from '@ohif/core'; +import { getEnabledElement } from './state'; const scroll = cornerstoneTools.import('util/scroll'); const actions = { rotateViewport: ({ viewports, rotation }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const enabledElement = getEnabledElement(viewports.activeViewportIndex); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -17,10 +15,7 @@ const actions = { } }, flipViewportHorizontal: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const enabledElement = getEnabledElement(viewports.activeViewportIndex); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -29,10 +24,7 @@ const actions = { } }, flipViewportVertical: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const enabledElement = getEnabledElement(viewports.activeViewportIndex); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -40,11 +32,8 @@ const actions = { cornerstone.setViewport(enabledElement, viewport); } }, - scaleViewport: ({ viewports, direction }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + scaleViewport: ({ direction, viewports }) => { + const enabledElement = getEnabledElement(viewports.activeViewportIndex); const step = direction * 0.15; if (enabledElement) { @@ -58,20 +47,14 @@ const actions = { } }, resetViewport: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const enabledElement = getEnabledElement(viewports.activeViewportIndex); if (enabledElement) { cornerstone.reset(enabledElement); } }, invertViewport: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const enabledElement = getEnabledElement(viewports.activeViewportIndex); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -92,10 +75,7 @@ const actions = { console.warn('updateDisplaySet: ', direction); }, clearAnnotations: ({ viewports }) => { - const element = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); + const element = getEnabledElement(viewports.activeViewportIndex); if (!element) { return; } @@ -147,24 +127,25 @@ const actions = { }); }, nextImage: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); - + const enabledElement = getEnabledElement(viewports.activeViewportIndex); scroll(enabledElement, 1); }, previousImage: ({ viewports }) => { - const enabledElement = _getActiveViewportEnabledElement( - viewports.viewportSpecificData, - viewports.activeViewportIndex - ); - + const enabledElement = getEnabledElement(viewports.activeViewportIndex); scroll(enabledElement, -1); }, + getActiveViewportEnabledElement: ({ viewports }) => { + const enabledElement = getEnabledElement(viewports.activeViewportIndex); + return enabledElement; + }, }; const definitions = { + getActiveViewportEnabledElement: { + commandFn: actions.getActiveViewportEnabledElement, + storeContexts: ['viewports'], + options: {}, + }, rotateViewportCW: { commandFn: actions.rotateViewport, storeContexts: ['viewports'], @@ -245,15 +226,6 @@ const definitions = { }, }; -/** - * Grabs `dom` reference for the enabledElement of - * the active viewport - */ -function _getActiveViewportEnabledElement(viewports, activeIndex) { - const activeViewport = viewports[activeIndex] || {}; - return activeViewport.dom; -} - export default { actions, definitions, diff --git a/extensions/cornerstone/src/state.js b/extensions/cornerstone/src/state.js new file mode 100644 index 000000000..c7139a00b --- /dev/null +++ b/extensions/cornerstone/src/state.js @@ -0,0 +1,20 @@ +const state = { + enabledElements: {}, +}; + +/** + * Sets the enabled element `dom` reference for an active viewport. + * @param {HTMLElement} dom Active viewport element. + * @return void + */ +const setEnabledElement = (viewportIndex, element) => + (state.enabledElements[viewportIndex] = element); + +/** + * Grabs the enabled element `dom` reference of an active viewport. + * + * @return {HTMLElement} Active viewport element. + */ +const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex]; + +export { setEnabledElement, getEnabledElement }; diff --git a/extensions/dicom-html/src/ConnectedDicomHtmlViewport.js b/extensions/dicom-html/src/ConnectedDicomHtmlViewport.js index 9e66b0b74..81832d505 100644 --- a/extensions/dicom-html/src/ConnectedDicomHtmlViewport.js +++ b/extensions/dicom-html/src/ConnectedDicomHtmlViewport.js @@ -6,7 +6,7 @@ const { setViewportActive } = OHIF.redux.actions; const mapStateToProps = (state, ownProps) => { const { viewportIndex, byteArray } = ownProps; - const activeViewportIndex = state.viewports; + const { activeViewportIndex } = state.viewports; return { viewportIndex, diff --git a/extensions/dicom-html/src/DicomHtmlViewport.js b/extensions/dicom-html/src/DicomHtmlViewport.js index 1008d92cd..7dca015d8 100644 --- a/extensions/dicom-html/src/DicomHtmlViewport.js +++ b/extensions/dicom-html/src/DicomHtmlViewport.js @@ -238,7 +238,7 @@ class DicomHtmlViewport extends Component { } = this.props; if (viewportIndex !== activeViewportIndex) { - setViewportActive(); + setViewportActive(viewportIndex); } }; diff --git a/extensions/vtk/src/commandsModule.js b/extensions/vtk/src/commandsModule.js index d9dff5d99..c0904fc39 100644 --- a/extensions/vtk/src/commandsModule.js +++ b/extensions/vtk/src/commandsModule.js @@ -1,3 +1,4 @@ +import throttle from 'lodash.throttle'; import { vtkInteractorStyleMPRCrosshairs, vtkInteractorStyleMPRWindowLevel, @@ -8,354 +9,358 @@ import { import setMPRLayout from './utils/setMPRLayout.js'; import setViewportToVTK from './utils/setViewportToVTK.js'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js'; -import throttle from 'lodash.throttle'; const { BlendMode } = Constants; -// TODO: Put this somewhere else -let apis = {}; +const commandsModule = ({ commandsManager }) => { + // TODO: Put this somewhere else + let apis = {}; -async function _getActiveViewportVTKApi(viewports) { - const { - numRows, - numColumns, - layout, - viewportSpecificData, - activeViewportIndex, - } = viewports; + async function _getActiveViewportVTKApi(viewports) { + const { + numRows, + numColumns, + layout, + viewportSpecificData, + activeViewportIndex, + } = viewports; - const currentData = layout.viewports[activeViewportIndex]; - if (currentData && currentData.plugin === 'vtk') { - // TODO: I was storing/pulling this from Redux but ran into weird issues - if (apis[activeViewportIndex]) { - return apis[activeViewportIndex]; + const currentData = layout.viewports[activeViewportIndex]; + if (currentData && currentData.plugin === 'vtk') { + // TODO: I was storing/pulling this from Redux but ran into weird issues + if (apis[activeViewportIndex]) { + return apis[activeViewportIndex]; + } } + + const displaySet = viewportSpecificData[activeViewportIndex]; + + let api; + if (!api) { + try { + api = await setViewportToVTK( + displaySet, + activeViewportIndex, + numRows, + numColumns, + layout, + viewportSpecificData + ); + } catch (error) { + throw new Error(error); + } + } + + return api; } - const displaySet = viewportSpecificData[activeViewportIndex]; + function _setView(api, sliceNormal, viewUp) { + const renderWindow = api.genericRenderWindow.getRenderWindow(); + const istyle = renderWindow.getInteractor().getInteractorStyle(); + istyle.setSliceNormal(...sliceNormal); + istyle.setViewUp(...viewUp); - let api; - if (!api) { - try { - api = await setViewportToVTK( - displaySet, - activeViewportIndex, - numRows, - numColumns, - layout, - viewportSpecificData + renderWindow.render(); + } + + function getVOIFromCornerstoneViewport() { + const dom = commandsManager.runCommand('getActiveViewportEnabledElement'); + const cornerstoneElement = cornerstone.getEnabledElement(dom); + + if (cornerstoneElement) { + const imageId = cornerstoneElement.image.imageId; + + const { modality } = cornerstone.metaData.get( + 'generalSeriesModule', + imageId ); - } catch (error) { - throw new Error(error); + + if (modality !== 'PT') { + const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi; + + return { + windowWidth, + windowCenter, + }; + } } } - return api; -} + function setVOI(voi) { + const { windowWidth, windowCenter } = voi; + const lower = windowCenter - windowWidth / 2.0; + const upper = windowCenter + windowWidth / 2.0; -function _setView(api, sliceNormal, viewUp) { - const renderWindow = api.genericRenderWindow.getRenderWindow(); - const istyle = renderWindow.getInteractor().getInteractorStyle(); - istyle.setSliceNormal(...sliceNormal); - istyle.setViewUp(...viewUp); + const rgbTransferFunction = apis[0].volumes[0] + .getProperty() + .getRGBTransferFunction(0); - renderWindow.render(); -} + rgbTransferFunction.setRange(lower, upper); -const actions = { - axial: async ({ viewports }) => { - const api = await _getActiveViewportVTKApi(viewports); - - apis[viewports.activeViewportIndex] = api; - - _setView(api, [0, 0, 1], [0, -1, 0]); - }, - sagittal: async ({ viewports }) => { - const api = await _getActiveViewportVTKApi(viewports); - - apis[viewports.activeViewportIndex] = api; - - _setView(api, [1, 0, 0], [0, 0, 1]); - }, - coronal: async ({ viewports }) => { - const api = await _getActiveViewportVTKApi(viewports); - - apis[viewports.activeViewportIndex] = api; - - _setView(api, [0, 1, 0], [0, 0, 1]); - }, - enableRotateTool: () => { apis.forEach(api => { - const istyle = vtkInteractorStyleMPRRotate.newInstance(); - - api.setInteractorStyle({ istyle }); + api.updateVOI(windowWidth, windowCenter); }); - }, - enableCrosshairsTool: () => { - apis.forEach((api, apiIndex) => { - const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); + } - api.setInteractorStyle({ - istyle, - configuration: { apis, apiIndex }, - }); - }); - }, - enableLevelTool: () => { - function updateVOI(apis, windowWidth, windowCenter) { + const actions = { + axial: async ({ viewports }) => { + const api = await _getActiveViewportVTKApi(viewports); + + apis[viewports.activeViewportIndex] = api; + + _setView(api, [0, 0, 1], [0, -1, 0]); + }, + sagittal: async ({ viewports }) => { + const api = await _getActiveViewportVTKApi(viewports); + + apis[viewports.activeViewportIndex] = api; + + _setView(api, [1, 0, 0], [0, 0, 1]); + }, + coronal: async ({ viewports }) => { + const api = await _getActiveViewportVTKApi(viewports); + + apis[viewports.activeViewportIndex] = api; + + _setView(api, [0, 1, 0], [0, 0, 1]); + }, + enableRotateTool: () => { apis.forEach(api => { - api.updateVOI(windowWidth, windowCenter); + const istyle = vtkInteractorStyleMPRRotate.newInstance(); + + api.setInteractorStyle({ istyle }); }); - } + }, + enableCrosshairsTool: () => { + apis.forEach((api, apiIndex) => { + const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); - const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps - - const callbacks = { - setOnLevelsChanged: ({ windowCenter, windowWidth }) => { - apis.forEach(api => { - const renderWindow = api.genericRenderWindow.getRenderWindow(); - - renderWindow.render(); + api.setInteractorStyle({ + istyle, + configuration: { apis, apiIndex }, }); - - throttledUpdateVOIs(apis, windowWidth, windowCenter); - }, - }; - - apis.forEach(api => { - const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); - - api.setInteractorStyle({ istyle, callbacks }); - }); - }, - setSlabThickness: ({ slabThickness }) => { - apis.forEach(api => { - api.setSlabThickness(slabThickness); - }); - }, - changeSlabThickness: ({ change }) => { - apis.forEach(api => { - const slabThickness = Math.max(api.getSlabThickness() + change, 0.1); - - api.setSlabThickness(slabThickness); - }); - }, - setBlendModeToComposite: () => { - apis.forEach(api => { - const renderWindow = api.genericRenderWindow.getRenderWindow(); - const istyle = renderWindow.getInteractor().getInteractorStyle(); - - const slabThickness = api.getSlabThickness(); - - const mapper = api.volumes[0].getMapper(); - if (mapper.setBlendModeToComposite) { - mapper.setBlendModeToComposite(); - } - - if (istyle.setSlabThickness) { - istyle.setSlabThickness(slabThickness); - } - renderWindow.render(); - }); - }, - setBlendModeToMaximumIntensity: () => { - apis.forEach(api => { - const renderWindow = api.genericRenderWindow.getRenderWindow(); - const mapper = api.volumes[0].getMapper(); - if (mapper.setBlendModeToMaximumIntensity) { - mapper.setBlendModeToMaximumIntensity(); - } - renderWindow.render(); - }); - }, - setBlendMode: ({ blendMode }) => { - apis.forEach(api => { - const renderWindow = api.genericRenderWindow.getRenderWindow(); - - api.volumes[0].getMapper().setBlendMode(blendMode); - - renderWindow.render(); - }); - }, - mpr2d: async ({ viewports }) => { - // TODO push a lot of this backdoor logic lower down to the library level. - const displaySet = - viewports.viewportSpecificData[viewports.activeViewportIndex]; - - // Get current VOI if cornerstone viewport. - const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet); - - const viewportProps = [ - { - //Axial - orientation: { - sliceNormal: [0, 0, 1], - viewUp: [0, -1, 0], - }, - }, - { - // Sagital - orientation: { - sliceNormal: [1, 0, 0], - viewUp: [0, 0, 1], - }, - }, - { - // Coronal - orientation: { - sliceNormal: [0, 1, 0], - viewUp: [0, 0, 1], - }, - }, - ]; - - try { - apis = await setMPRLayout(displaySet, viewportProps, 1, 3); - } catch (error) { - throw new Error(error); - } - - if (cornerstoneVOI) { - setVOI(cornerstoneVOI); - } - - // Add widgets and set default interactorStyle of each viewport. - apis.forEach((api, apiIndex) => { - api.addSVGWidget( - vtkSVGCrosshairsWidget.newInstance(), - 'crosshairsWidget' - ); - - const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); - - api.setInteractorStyle({ - istyle, - configuration: { apis, apiIndex }, }); - }); - }, -}; - -window.vtkActions = actions; - -const definitions = { - axial: { - commandFn: actions.axial, - storeContexts: ['viewports'], - options: {}, - }, - coronal: { - commandFn: actions.coronal, - storeContexts: ['viewports'], - options: {}, - }, - sagittal: { - commandFn: actions.sagittal, - storeContexts: ['viewports'], - options: {}, - }, - enableRotateTool: { - commandFn: actions.enableRotateTool, - storeContexts: [], - options: {}, - }, - enableCrosshairsTool: { - commandFn: actions.enableCrosshairsTool, - storeContexts: [], - options: {}, - }, - enableLevelTool: { - commandFn: actions.enableLevelTool, - storeContexts: [], - options: {}, - }, - setBlendModeToComposite: { - commandFn: actions.setBlendModeToComposite, - storeContexts: [], - options: { blendMode: BlendMode.COMPOSITE_BLEND }, - }, - setBlendModeToMaximumIntensity: { - commandFn: actions.setBlendModeToMaximumIntensity, - storeContexts: [], - options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND }, - }, - setBlendModeToMinimumIntensity: { - commandFn: actions.setBlendMode, - storeContexts: [], - options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND }, - }, - setBlendModeToAverageIntensity: { - commandFn: actions.setBlendMode, - storeContexts: [], - options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND }, - }, - setSlabThickness: { - // TODO: How do we pass in a function argument? - commandFn: actions.setSlabThickness, - storeContexts: [], - options: {}, - }, - increaseSlabThickness: { - commandFn: actions.changeSlabThickness, - storeContexts: [], - options: { - change: 3, }, - }, - decreaseSlabThickness: { - commandFn: actions.changeSlabThickness, - storeContexts: [], - options: { - change: -3, - }, - }, - mpr2d: { - commandFn: actions.mpr2d, - storeContexts: ['viewports'], - options: {}, - context: 'VIEWER', - }, -}; + enableLevelTool: () => { + function updateVOI(apis, windowWidth, windowCenter) { + apis.forEach(api => { + api.updateVOI(windowWidth, windowCenter); + }); + } -export default { - definitions, - defaultContext: 'ACTIVE_VIEWPORT::VTK', -}; + const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps -function getVOIFromCornerstoneViewport(displaySet) { - const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom); + const callbacks = { + setOnLevelsChanged: ({ windowCenter, windowWidth }) => { + apis.forEach(api => { + const renderWindow = api.genericRenderWindow.getRenderWindow(); - if (cornerstoneElement) { - const imageId = cornerstoneElement.image.imageId; + renderWindow.render(); + }); - const { modality } = cornerstone.metaData.get( - 'generalSeriesModule', - imageId - ); - - if (modality !== 'PT') { - const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi; - - return { - windowWidth, - windowCenter, + throttledUpdateVOIs(apis, windowWidth, windowCenter); + }, }; - } - } -} -function setVOI(voi) { - const { windowWidth, windowCenter } = voi; - const lower = windowCenter - windowWidth / 2.0; - const upper = windowCenter + windowWidth / 2.0; + apis.forEach(api => { + const istyle = vtkInteractorStyleMPRWindowLevel.newInstance(); - const rgbTransferFunction = apis[0].volumes[0] - .getProperty() - .getRGBTransferFunction(0); + api.setInteractorStyle({ istyle, callbacks }); + }); + }, + setSlabThickness: ({ slabThickness }) => { + apis.forEach(api => { + api.setSlabThickness(slabThickness); + }); + }, + changeSlabThickness: ({ change }) => { + apis.forEach(api => { + const slabThickness = Math.max(api.getSlabThickness() + change, 0.1); - rgbTransferFunction.setRange(lower, upper); + api.setSlabThickness(slabThickness); + }); + }, + setBlendModeToComposite: () => { + apis.forEach(api => { + const renderWindow = api.genericRenderWindow.getRenderWindow(); + const istyle = renderWindow.getInteractor().getInteractorStyle(); - apis.forEach(api => { - api.updateVOI(windowWidth, windowCenter); - }); -} + const slabThickness = api.getSlabThickness(); + + const mapper = api.volumes[0].getMapper(); + if (mapper.setBlendModeToComposite) { + mapper.setBlendModeToComposite(); + } + + if (istyle.setSlabThickness) { + istyle.setSlabThickness(slabThickness); + } + renderWindow.render(); + }); + }, + setBlendModeToMaximumIntensity: () => { + apis.forEach(api => { + const renderWindow = api.genericRenderWindow.getRenderWindow(); + const mapper = api.volumes[0].getMapper(); + if (mapper.setBlendModeToMaximumIntensity) { + mapper.setBlendModeToMaximumIntensity(); + } + renderWindow.render(); + }); + }, + setBlendMode: ({ blendMode }) => { + apis.forEach(api => { + const renderWindow = api.genericRenderWindow.getRenderWindow(); + + api.volumes[0].getMapper().setBlendMode(blendMode); + + renderWindow.render(); + }); + }, + mpr2d: async ({ viewports }) => { + // TODO push a lot of this backdoor logic lower down to the library level. + const displaySet = + viewports.viewportSpecificData[viewports.activeViewportIndex]; + + // Get current VOI if cornerstone viewport. + const cornerstoneVOI = getVOIFromCornerstoneViewport(); + + const viewportProps = [ + { + //Axial + orientation: { + sliceNormal: [0, 0, 1], + viewUp: [0, -1, 0], + }, + }, + { + // Sagital + orientation: { + sliceNormal: [1, 0, 0], + viewUp: [0, 0, 1], + }, + }, + { + // Coronal + orientation: { + sliceNormal: [0, 1, 0], + viewUp: [0, 0, 1], + }, + }, + ]; + + try { + apis = await setMPRLayout(displaySet, viewportProps, 1, 3); + } catch (error) { + throw new Error(error); + } + + if (cornerstoneVOI) { + setVOI(cornerstoneVOI); + } + + // Add widgets and set default interactorStyle of each viewport. + apis.forEach((api, apiIndex) => { + api.addSVGWidget( + vtkSVGCrosshairsWidget.newInstance(), + 'crosshairsWidget' + ); + + const istyle = vtkInteractorStyleMPRCrosshairs.newInstance(); + + api.setInteractorStyle({ + istyle, + configuration: { apis, apiIndex }, + }); + }); + }, + }; + + window.vtkActions = actions; + + const definitions = { + axial: { + commandFn: actions.axial, + storeContexts: ['viewports'], + options: {}, + }, + coronal: { + commandFn: actions.coronal, + storeContexts: ['viewports'], + options: {}, + }, + sagittal: { + commandFn: actions.sagittal, + storeContexts: ['viewports'], + options: {}, + }, + enableRotateTool: { + commandFn: actions.enableRotateTool, + storeContexts: [], + options: {}, + }, + enableCrosshairsTool: { + commandFn: actions.enableCrosshairsTool, + storeContexts: [], + options: {}, + }, + enableLevelTool: { + commandFn: actions.enableLevelTool, + storeContexts: [], + options: {}, + }, + setBlendModeToComposite: { + commandFn: actions.setBlendModeToComposite, + storeContexts: [], + options: { blendMode: BlendMode.COMPOSITE_BLEND }, + }, + setBlendModeToMaximumIntensity: { + commandFn: actions.setBlendModeToMaximumIntensity, + storeContexts: [], + options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND }, + }, + setBlendModeToMinimumIntensity: { + commandFn: actions.setBlendMode, + storeContexts: [], + options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND }, + }, + setBlendModeToAverageIntensity: { + commandFn: actions.setBlendMode, + storeContexts: [], + options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND }, + }, + setSlabThickness: { + // TODO: How do we pass in a function argument? + commandFn: actions.setSlabThickness, + storeContexts: [], + options: {}, + }, + increaseSlabThickness: { + commandFn: actions.changeSlabThickness, + storeContexts: [], + options: { + change: 3, + }, + }, + decreaseSlabThickness: { + commandFn: actions.changeSlabThickness, + storeContexts: [], + options: { + change: -3, + }, + }, + mpr2d: { + commandFn: actions.mpr2d, + storeContexts: ['viewports'], + options: {}, + context: 'VIEWER', + }, + }; + + return { + definitions, + defaultContext: 'ACTIVE_VIEWPORT::VTK', + }; +}; + +export default commandsModule; diff --git a/extensions/vtk/src/index.js b/extensions/vtk/src/index.js index 6f2cfc5ff..dbc53f6e0 100644 --- a/extensions/vtk/src/index.js +++ b/extensions/vtk/src/index.js @@ -20,8 +20,8 @@ const vtkExtension = { getToolbarModule() { return toolbarModule; }, - getCommandsModule() { - return commandsModule; + getCommandsModule({ commandsManager }) { + return commandsModule({ commandsManager }); }, }; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index e5adbb85f..01fb88a6f 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -69,6 +69,7 @@ export default class ExtensionManager { if (extension.preRegistration) { extension.preRegistration({ servicesManager: this._servicesManager, + commandsManager: this._commandsManager, configuration, }); } @@ -112,6 +113,7 @@ export default class ExtensionManager { try { const extensionModule = getModuleFn({ servicesManager: this._servicesManager, + commandsManager: this._commandsManager, }); if (!extensionModule) { diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js index 37b396e52..503e4ccdb 100644 --- a/platform/core/src/extensions/ExtensionManager.test.js +++ b/platform/core/src/extensions/ExtensionManager.test.js @@ -49,7 +49,7 @@ describe('ExtensionManager.js', () => { expect(fakeExtension.preRegistration.mock.calls.length).toBe(1); }); - it('calls preRegistration() passing configuration and servicesManager instance for extension', () => { + it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => { const configuration = { config: 'Some configuration' }; extensionManager._servicesManager = { services: { TestService: {} } }; @@ -60,6 +60,7 @@ describe('ExtensionManager.js', () => { // Assert expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({ servicesManager: extensionManager._servicesManager, + commandsManager: extensionManager._commandsManager, configuration, }); }); @@ -134,7 +135,7 @@ describe('ExtensionManager.js', () => { ); }); - it('successfully passes a servicesManager instance to each module', () => { + it('successfully passes a servicesManager and commandsManager instances to each module', () => { extensionManager._servicesManager = { services: { TestService: {} } }; const extension = { @@ -150,6 +151,7 @@ describe('ExtensionManager.js', () => { expect(extension.getViewportModule.mock.calls[0][0]).toEqual({ servicesManager: extensionManager._servicesManager, + commandsManager: extensionManager._commandsManager, }); }); diff --git a/platform/core/src/redux/actions.js b/platform/core/src/redux/actions.js index 5b4b008d3..4ac0c14b7 100644 --- a/platform/core/src/redux/actions.js +++ b/platform/core/src/redux/actions.js @@ -12,13 +12,25 @@ import { SET_VIEWPORT_LAYOUT_AND_DATA, } from './constants/ActionTypes.js'; +/** + * The definition of a viewport layout. + * + * @typedef {Object} ViewportLayout + * @property {number} numRows - + * @property {number} numColumns - + * @property {array} viewports - + */ + /** * VIEWPORT */ -export const setViewportSpecificData = (viewportIndex, data) => ({ +export const setViewportSpecificData = ( + viewportIndex, + viewportSpecificData +) => ({ type: SET_VIEWPORT, viewportIndex, - data, + viewportSpecificData, }); export const setViewportActive = viewportIndex => ({ @@ -27,10 +39,7 @@ export const setViewportActive = viewportIndex => ({ }); /** - * @param {object} layout - * @param {number} layout.numRows - * @param {number} layout.numColumns - * @param {array} layout.viewports + * @param {ViewportLayout} layout */ export const setLayout = ({ numRows, numColumns, viewports }) => ({ type: SET_VIEWPORT_LAYOUT, @@ -40,10 +49,9 @@ export const setLayout = ({ numRows, numColumns, viewports }) => ({ }); /** - * @param {object} layout * @param {number} layout.numRows * @param {number} layout.numColumns - * @param {array} layout.viewports + * @param {array} viewports */ export const setViewportLayoutAndData = ( { numRows, numColumns, viewports }, @@ -61,9 +69,9 @@ export const clearViewportSpecificData = viewportIndex => ({ viewportIndex, }); -export const setActiveViewportSpecificData = data => ({ +export const setActiveViewportSpecificData = viewportSpecificData => ({ type: SET_ACTIVE_SPECIFIC_DATA, - data, + viewportSpecificData, }); /** @@ -113,13 +121,18 @@ export const setServers = servers => ({ }); const actions = { - // VIEWPORT + /** + * VIEWPORT + */ setViewportActive, setViewportSpecificData, setViewportLayoutAndData, setLayout, clearViewportSpecificData, setActiveViewportSpecificData, + /** + * NOT-VIEWPORT + */ setStudyLoadingProgress, clearStudyLoadingProgress, setUserPreferences, diff --git a/platform/core/src/redux/actions.test.js b/platform/core/src/redux/actions.test.js index 581d5cdc8..79a47a11a 100644 --- a/platform/core/src/redux/actions.test.js +++ b/platform/core/src/redux/actions.test.js @@ -28,7 +28,7 @@ describe('actions', () => { describe('viewport action creators', () => { it('should create an action to set the viewport specific data', () => { - const data = { + const viewportSpecificData = { displaySetInstanceUid: 'ef859a23-4631-93ab-d26b-7940a822c699', seriesDate: '20151026', seriesTime: '082611.370000', @@ -65,12 +65,12 @@ describe('actions', () => { const expectedAction = { type: types.SET_ACTIVE_SPECIFIC_DATA, - data, + viewportSpecificData, }; - expect(actions.setActiveViewportSpecificData(data)).toEqual( - expectedAction - ); + expect( + actions.setActiveViewportSpecificData(viewportSpecificData) + ).toEqual(expectedAction); }); it('should create an action to clear clearViewportSpecificData', () => { diff --git a/platform/core/src/redux/reducers/viewports.js b/platform/core/src/redux/reducers/viewports.js index 3bc0e740b..2a6d66ee0 100644 --- a/platform/core/src/redux/reducers/viewports.js +++ b/platform/core/src/redux/reducers/viewports.js @@ -1,3 +1,6 @@ +import cloneDeep from 'lodash.clonedeep'; +import merge from 'lodash.merge'; + import { CLEAR_VIEWPORT, SET_ACTIVE_SPECIFIC_DATA, @@ -8,10 +11,7 @@ import { SET_VIEWPORT_LAYOUT_AND_DATA, } from './../constants/ActionTypes.js'; -import cloneDeep from 'lodash.clonedeep'; -import merge from 'lodash.merge'; - -const defaultState = { +const DEFAULT_STATE = { numRows: 1, numColumns: 1, activeViewportIndex: 0, @@ -26,94 +26,137 @@ const defaultState = { }; /** - * @param {Object} [state=defaultState] - * @param {Object} action - * @param {string} [action.type] - * @param {number} [action.viewportIndex] - * @param {Object} [action.layout] - * @param {Object} [action.viewportSpecificData] + * The definition of a viewport action. + * + * @typedef {Object} ViewportAction + * @property {string} type - + * @property {Object} data - + * @property {Object} layout - + * @property {number} viewportIndex - + * @property {Object} viewportSpecificData - */ -const viewports = (state = defaultState, action) => { - let viewportSpecificData; + +/** + * @param {Object} [state=DEFAULT_STATE] The current viewport state. + * @param {ViewportAction} action A viewport action. + */ +const viewports = (state = DEFAULT_STATE, action) => { let useActiveViewport = false; + switch (action.type) { - case SET_VIEWPORT_ACTIVE: - return Object.assign({}, state, { - activeViewportIndex: action.viewportIndex, - }); + /** + * Sets the active viewport index. + * + * @return {Object} New state. + */ + case SET_VIEWPORT_ACTIVE: { + return { ...state, activeViewportIndex: action.viewportIndex }; + } + + /** + * Sets viewport layout. + * + * @return {Object} New state. + */ case SET_VIEWPORT_LAYOUT: { - const { numRows, numColumns, viewports } = action; - const layout = { - viewports: [...viewports], + return { + ...state, + numRows: action.numRows, + numColumns: action.numColumns, + layout: { viewports: [...action.viewports] }, }; - - return Object.assign({}, state, { numRows, numColumns, layout }); } + + /** + * Sets viewport layout and data. + * + * @return {Object} New state. + */ case SET_VIEWPORT_LAYOUT_AND_DATA: { - const { numRows, numColumns, viewports, viewportSpecificData } = action; - const layout = { - viewports: [...viewports], + return { + ...state, + numRows: action.numRows, + numColumns: action.numColumns, + layout: { viewports: [...action.viewports] }, + viewportSpecificData: cloneDeep(action.viewportSpecificData), }; - - return Object.assign({}, state, { - numRows, - numColumns, - layout, - viewportSpecificData: cloneDeep(viewportSpecificData), - }); } + + /** + * Sets viewport specific data of active viewport. + * + * @return {Object} New state. + */ case SET_VIEWPORT: { const layout = cloneDeep(state.layout); - const hasPlugin = action.data && action.data.plugin; - viewportSpecificData = cloneDeep(state.viewportSpecificData); + let viewportSpecificData = cloneDeep(state.viewportSpecificData); viewportSpecificData[action.viewportIndex] = merge( {}, viewportSpecificData[action.viewportIndex], - action.data + action.viewportSpecificData ); - if (hasPlugin) { - layout.viewports[action.viewportIndex].plugin = action.data.plugin; + if (action.viewportSpecificData && action.viewportSpecificData.plugin) { + layout.viewports[action.viewportIndex].plugin = + action.viewportSpecificData.plugin; } - return Object.assign({}, state, { layout, viewportSpecificData }); + return { ...state, layout, viewportSpecificData }; } + + /** + * Sets viewport specific data of active/any viewport. + * + * @return {Object} New state. + */ case SET_ACTIVE_SPECIFIC_DATA: useActiveViewport = true; // Allow fall-through // eslint-disable-next-line case SET_SPECIFIC_DATA: { const layout = cloneDeep(state.layout); - const hasPlugin = action.data && action.data.plugin; const viewportIndex = useActiveViewport ? state.activeViewportIndex : action.viewportIndex; - const { dom } = state.viewportSpecificData[viewportIndex]; - viewportSpecificData = cloneDeep(state.viewportSpecificData); + let viewportSpecificData = cloneDeep(state.viewportSpecificData); viewportSpecificData[viewportIndex] = { - dom, - ...action.data, + ...action.viewportSpecificData, }; - if (hasPlugin) { - layout.viewports[viewportIndex].plugin = action.data.plugin; + if (action.viewportSpecificData && action.viewportSpecificData.plugin) { + layout.viewports[viewportIndex].plugin = + action.viewportSpecificData.plugin; } - return Object.assign({}, state, { layout, viewportSpecificData }); + return { ...state, layout, viewportSpecificData }; } - case CLEAR_VIEWPORT: - viewportSpecificData = cloneDeep(state.viewportSpecificData); + + /** + * Clears viewport specific data of any viewport. + * + * @return {Object} New state. + */ + case CLEAR_VIEWPORT: { + let viewportSpecificData = cloneDeep(state.viewportSpecificData); + if (action.viewportIndex) { viewportSpecificData[action.viewportIndex] = {}; - return Object.assign({}, state, { viewportSpecificData }); + return { ...state, viewportSpecificData }; } else { - return defaultState; + return DEFAULT_STATE; } + } - default: + /** + * Returns the current application state. + * + * @return {Object} The current state. + */ + default: { return state; + } } }; diff --git a/platform/core/src/redux/reducers/viewports.test.js b/platform/core/src/redux/reducers/viewports.test.js index 57ad1687c..879fabc23 100644 --- a/platform/core/src/redux/reducers/viewports.test.js +++ b/platform/core/src/redux/reducers/viewports.test.js @@ -60,7 +60,7 @@ describe('viewports reducer', () => { const setViewportAction = { type: types.SET_VIEWPORT, viewportIndex: viewportToSet, - data: { + viewportSpecificData: { hello: 'this is that data for the viewport', world: 'that will be set for the viewportIndex', }, @@ -69,7 +69,7 @@ describe('viewports reducer', () => { const updatedState = reducer(undefined, setViewportAction); const updatedViewport = updatedState.viewportSpecificData[viewportToSet]; - expect(updatedViewport).toEqual(setViewportAction.data); + expect(updatedViewport).toEqual(setViewportAction.viewportSpecificData); }); it('should handle CLEAR_VIEWPORT', () => { diff --git a/platform/viewer/src/connectedComponents/ConnectedCineDialog.js b/platform/viewer/src/connectedComponents/ConnectedCineDialog.js index 39070a96f..82270fb18 100644 --- a/platform/viewer/src/connectedComponents/ConnectedCineDialog.js +++ b/platform/viewer/src/connectedComponents/ConnectedCineDialog.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { CineDialog } from '@ohif/ui'; import OHIF from '@ohif/core'; import csTools from 'cornerstone-tools'; +import { commandsManager } from './../App.js'; // Our target output kills the `as` and "import" throws a keyword error // import { import as toolImport, getToolState } from 'cornerstone-tools'; import cloneDeep from 'lodash.clonedeep'; @@ -16,7 +17,8 @@ const { setViewportSpecificData } = OHIF.redux.actions; const mapStateToProps = state => { // Get activeViewport's `cine` and `stack` const { viewportSpecificData, activeViewportIndex } = state.viewports; - const { cine, dom } = viewportSpecificData[activeViewportIndex] || {}; + const { cine } = viewportSpecificData[activeViewportIndex] || {}; + const dom = commandsManager.runCommand('getActiveViewportEnabledElement'); const cineData = cine || { isPlaying: false, diff --git a/platform/viewer/src/connectedComponents/ConnectedViewportDownloadForm.js b/platform/viewer/src/connectedComponents/ConnectedViewportDownloadForm.js index e0aabc7b9..bebab6b8c 100644 --- a/platform/viewer/src/connectedComponents/ConnectedViewportDownloadForm.js +++ b/platform/viewer/src/connectedComponents/ConnectedViewportDownloadForm.js @@ -3,15 +3,16 @@ import { ViewportDownloadForm } from '@ohif/ui'; import { utils } from '@ohif/core'; import cornerstone from 'cornerstone-core'; import cornerstoneTools from 'cornerstone-tools'; +import { commandsManager } from './../App.js'; const MINIMUM_SIZE = 100; const DEFAULT_SIZE = 512; const MAX_TEXTURE_SIZE = 10000; const mapStateToProps = (state, ownProps) => { - const { viewportSpecificData, activeViewportIndex } = state.viewports; - const { dom: activeEnabledElement } = - viewportSpecificData[activeViewportIndex] || {}; + const activeEnabledElement = commandsManager.runCommand( + 'getActiveViewportEnabledElement' + ); return { onClose: ownProps.hide,