diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index b3c754902c6..ca75fa39d7c 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@babel/runtime": "^7.5.5", + "gl-matrix": "^3.3.0", "react-select": "^3.0.8" } } diff --git a/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js b/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js index e3dd32c041c..9d405e8e95e 100644 --- a/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js +++ b/extensions/dicom-segmentation/src/components/SegmentItem/SegmentItem.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { TableListItem, Icon } from '@ohif/ui'; @@ -19,6 +19,11 @@ ColoredCircle.propTypes = { const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => { const [isVisible, setIsVisible] = useState(visible); + + useEffect(() => { + setIsVisible(visible); + }, [visible]); + return (
{ * @param {Array} props.viewports - Viewports data (viewportSpecificData) * @param {number} props.activeIndex - Active viewport index * @param {boolean} props.isOpen - Boolean that indicates if the panel is expanded - * @param {Function} props.onSegItemClick - Segment click handler + * @param {Function} props.onSegmentItemClick - Segment click handler + * @param {Function} props.onSegmentVisibilityChange - Segment visibiliy change handler + * @param {Function} props.onConfigurationChange - Configuration change handler + * @param {Function} props.activeContexts - List of active application contexts + * @param {Function} props.contexts - List of available application contexts * @returns component */ const SegmentationPanel = ({ @@ -40,25 +44,40 @@ const SegmentationPanel = ({ viewports, activeIndex, isOpen, - onSegItemClick, - UINotificationService, + onSegmentItemClick, + onSegmentVisibilityChange, + onConfigurationChange, + onDisplaySetLoadFailure, + onSelectedSegmentationChange, + activeContexts, + contexts, }) => { + const isVTK = () => activeContexts.includes(contexts.VTK); + const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE); + /* * TODO: wrap get/set interactions with the cornerstoneTools * store with context to make these kind of things less blurry. */ const { configuration } = cornerstoneTools.getModule('segmentation'); const DEFAULT_BRUSH_RADIUS = configuration.radius || 10; + + /* + * TODO: We shouldn't hardcode brushColor color, in the future + * the SEG may set the colorLUT to whatever it wants. + */ const [state, setState] = useState({ brushRadius: DEFAULT_BRUSH_RADIUS, brushColor: - 'rgba(221, 85, 85, 1)' /* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */, + 'rgba(221, 85, 85, 1)', selectedSegment: null, selectedSegmentation: null, - showSegSettings: false, + showSegmentationSettings: false, brushStackState: null, labelmapList: [], segmentList: [], + cachedSegmentsProperties: [], + isLoading: false }); useEffect(() => { @@ -75,6 +94,25 @@ const SegmentationPanel = ({ updateState('brushStackState', module.state.series[firstImageId]); }; + /* + * TODO: Improve the way we notify parts of the app that depends on segs to be loaded. + * + * Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel + * or other components that could rely on loaded segmentations that + * the segments were loaded so that e.g. when the user opens the panel + * before the segments are fully loaded, the panel can subscribe to this custom event + * and update itself with the new segments. + * + * This limitation is due to the fact that the cs segmentation module is an object (which will be + * updated after the segments are loaded) that React its not aware of its changes + * because the module object its not passed in to the panel component as prop but accessed externally. + * + * Improving this event approach to something reactive that can be tracked inside the react lifecycle, + * allows us to easily watch the module or the segmentations loading process in any other component + * without subscribing to external events. + */ + document.addEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations); + /* * These are specific to each element; * Need to iterate cornerstone-tools tracked enabled elements? @@ -88,6 +126,7 @@ const SegmentationPanel = ({ ); return () => { + document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations); cornerstoneTools.store.state.enabledElements.forEach(enabledElement => enabledElement.removeEventListener( 'cornerstonetoolslabelmapmodified', @@ -95,9 +134,9 @@ const SegmentationPanel = ({ ) ); }; - }); + }, [activeIndex, viewports]); - useEffect(() => { + const refreshSegmentations = useCallback(() => { const module = cornerstoneTools.getModule('segmentation'); const activeViewport = viewports[activeIndex]; const studyMetadata = studyMetadataManager.get( @@ -107,7 +146,6 @@ const SegmentationPanel = ({ activeViewport.displaySetInstanceUID ); const brushStackState = module.state.series[firstImageId]; - if (brushStackState) { const labelmap3D = brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex]; @@ -136,19 +174,20 @@ const SegmentationPanel = ({ })); } }, [ - studies, viewports, activeIndex, - getLabelmapList, - getSegmentList, - state.selectedSegmentation, + state.isLoading ]); + useEffect(() => { + refreshSegmentations(); + }, [viewports, activeIndex, state.selectedSegmentation, activeContexts, state.isLoading]); + /* Handle open/closed panel behaviour */ useEffect(() => { setState(state => ({ ...state, - showSegSettings: state.showSegSettings && !isOpen, + showSegmentationSettings: state.showSegmentationSettings && !isOpen, })); }, [isOpen]); @@ -183,7 +222,8 @@ const SegmentationPanel = ({ displaySet, firstImageId, brushStackState.activeLabelmapIndex, - UINotificationService + () => onSelectedSegmentationChange(), + onDisplaySetLoadFailure ); updateState('selectedSegmentation', activatedLabelmapIndex); }, @@ -262,65 +302,104 @@ const SegmentationPanel = ({ : prev; }); - const enabledElements = cornerstone.getEnabledElements(); - const element = enabledElements[activeIndex].element; - const toolState = cornerstoneTools.getToolState(element, 'stack'); + if (isCornerstone()) { + const enabledElements = cornerstone.getEnabledElements(); + const element = enabledElements[activeIndex].element; + const toolState = cornerstoneTools.getToolState(element, 'stack'); + + if (!toolState) { + return; + } + + const imageIds = toolState.data[0].imageIds; + const imageId = imageIds[closest]; + const frameIndex = imageIds.indexOf(imageId); + + const SOPInstanceUID = cornerstone.metaData.get( + 'SOPInstanceUID', + imageId + ); + const StudyInstanceUID = cornerstone.metaData.get( + 'StudyInstanceUID', + imageId + ); - if (!toolState) { - return; + onSegmentItemClick({ + StudyInstanceUID, + SOPInstanceUID, + frameIndex, + activeViewportIndex: activeIndex, + }); } - const imageIds = toolState.data[0].imageIds; - const imageId = imageIds[closest]; - const frameIndex = imageIds.indexOf(imageId); + if (isVTK()) { + const activeViewport = viewports[activeIndex]; + const studyMetadata = studyMetadataManager.get( + activeViewport.StudyInstanceUID + ); + const allDisplaySets = studyMetadata.getDisplaySets(); + const currentDisplaySet = allDisplaySets.find( + displaySet => + displaySet.displaySetInstanceUID === + activeViewport.displaySetInstanceUID + ); - const SOPInstanceUID = cornerstone.metaData.get( - 'SOPInstanceUID', - imageId - ); - const StudyInstanceUID = cornerstone.metaData.get( - 'StudyInstanceUID', - imageId - ); + const frame = labelmap3D.labelmaps2D[closest]; - onSegItemClick({ - StudyInstanceUID, - SOPInstanceUID, - frameIndex, - activeViewportIndex: activeIndex, - }); + onSegmentItemClick({ + studies, + StudyInstanceUID: currentDisplaySet.StudyInstanceUID, + displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID, + SOPClassUID: viewports[activeIndex].sopClassUIDs[0], + SOPInstanceUID: currentDisplaySet.SOPInstanceUID, + segmentNumber, + frameIndex: closest, + frame, + }); + } }; - const enabledElements = cornerstone.getEnabledElements(); - const enabledElementViewport = enabledElements[activeIndex]; + const isSegmentVisible = () => { + return !labelmap3D.segmentsHidden[segmentIndex]; + }; - let isVisible = true; - if (enabledElementViewport) { - const element = enabledElementViewport.element; - const module = cornerstoneTools.getModule('segmentation'); - isVisible = module.getters.isSegmentVisible( - element, - segmentNumber, - brushStackState.activeLabelmapIndex - ); + const toggleSegmentVisibility = () => { + const segmentsHidden = labelmap3D.segmentsHidden; + segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex]; + return !segmentsHidden[segmentIndex]; + }; + + const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber]; + let visible = isSegmentVisible(); + if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) { + toggleSegmentVisibility(); } segmentList.push( setCurrentSelectedSegment()} label={segmentLabel} index={segmentNumber} color={color} - visible={isVisible} - onVisibilityChange={() => { - const element = enabledElements[activeIndex].element; - module.setters.toggleSegmentVisibility( - element, - segmentNumber, - brushStackState.activeLabelmapIndex - ); + visible={visible} + onVisibilityChange={newVisibility => { + if (isCornerstone()) { + const enabledElements = cornerstone.getEnabledElements(); + const element = enabledElements[activeIndex].element; + module.setters.toggleSegmentVisibility( + element, + segmentNumber, + brushStackState.activeLabelmapIndex + ); + } + + if (isVTK()) { + onSegmentVisibilityChange(segmentNumber, newVisibility); + } + + updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility }); refreshViewport(); }} /> @@ -336,9 +415,25 @@ const SegmentationPanel = ({ * Show default name */ }, - [activeIndex, onSegItemClick, state.selectedSegment] + [activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading] ); + const updateCachedSegmentsProperties = (segmentNumber, properties) => { + const segmentsProperties = state.cachedSegmentsProperties; + const segmentProperties = state.cachedSegmentsProperties[segmentNumber]; + + segmentsProperties[segmentNumber] = + segmentProperties ? + { ...segmentProperties, ...properties } : + properties; + + updateState('cachedSegmentsProperties', segmentsProperties); + }; + + useEffect(() => { + updateState('cachedSegmentsProperties', []); + }, [activeContexts]); + const updateState = (field, value) => { setState(state => ({ ...state, [field]: value })); }; @@ -387,7 +482,6 @@ const SegmentationPanel = ({ }; const updateConfiguration = newConfiguration => { - /* Supported configuration */ configuration.renderFill = newConfiguration.renderFill; configuration.renderOutline = newConfiguration.renderOutline; configuration.shouldRenderInactiveLabelmaps = @@ -397,14 +491,17 @@ const SegmentationPanel = ({ configuration.outlineWidth = newConfiguration.outlineWidth; configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive; configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive; + onConfigurationChange(newConfiguration); refreshViewport(); }; - if (state.showSegSettings) { + const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps']; + if (state.showSegmentationSettings) { return ( updateState('showSegSettings', false)} + onBack={() => updateState('showSegmentationSettings', false)} onChange={updateConfiguration} /> ); @@ -416,7 +513,7 @@ const SegmentationPanel = ({ name="cog" width="25px" height="25px" - onClick={() => updateState('showSegSettings', true)} + onClick={() => updateState('showSegmentationSettings', true)} /> {false && (
@@ -525,7 +622,8 @@ const _setActiveLabelmap = async ( displaySet, firstImageId, activeLabelmapIndex, - UINotificationService + callback = () => { }, + onDisplaySetLoadFailure ) => { if (displaySet.labelmapIndex === activeLabelmapIndex) { log.warn(`${activeLabelmapIndex} is already the active labelmap`); @@ -539,12 +637,7 @@ const _setActiveLabelmap = async ( const loadPromise = displaySet.load(viewportSpecificData, studies); loadPromise.catch(error => { - UINotificationService.show({ - title: 'DICOM Segmentation Loader', - message: error.message, - type: 'error', - autoClose: false, - }); + onDisplaySetLoadFailure(error); // Return old index. return activeLabelmapIndex; @@ -559,6 +652,8 @@ const _setActiveLabelmap = async ( refreshViewport(); + callback(); + return displaySet.labelmapIndex; }; diff --git a/extensions/dicom-segmentation/src/components/SegmentationSettings/SegmentationSettings.js b/extensions/dicom-segmentation/src/components/SegmentationSettings/SegmentationSettings.js index f700dfd7572..3d637b80fae 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationSettings/SegmentationSettings.js +++ b/extensions/dicom-segmentation/src/components/SegmentationSettings/SegmentationSettings.js @@ -4,7 +4,7 @@ import { Range } from '@ohif/ui'; import './SegmentationSettings.css'; -const SegmentationSettings = ({ configuration, onBack, onChange }) => { +const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields = [] }) => { const [state, setState] = useState({ renderFill: configuration.renderFill, renderOutline: configuration.renderOutline, @@ -70,28 +70,32 @@ const SegmentationSettings = ({ configuration, onBack, onChange }) => { /> {state.renderOutline && ( <> - save('outlineAlpha', toFloat(event.target.value))} - /> - save('outlineWidth', parseInt(event.target.value))} - /> + {!disabledFields.includes('outlineAlpha') && ( + save('outlineAlpha', toFloat(event.target.value))} + /> + )} + {!disabledFields.includes('outlineWidth') && ( + save('outlineWidth', parseInt(event.target.value))} + /> + )} )}
- {(state.renderFill || state.renderOutline) && ( + {(state.renderFill || state.renderOutline) && !disabledFields.includes('shouldRenderInactiveLabelmaps') && (
{ /> {state.shouldRenderInactiveLabelmaps && ( <> - {state.renderFill && ( + {state.renderFill && !disabledFields.includes('fillAlphaInactive') && ( { onChange={event => save('fillAlphaInactive', toFloat(event.target.value))} /> )} - {state.renderOutline && ( + {state.renderOutline && !disabledFields.includes('outlineAlphaInactive') && ( { - const segItemClickHandler = segData => { - commandsManager.runCommand('jumpToImage', segData); + const { activeContexts } = api.hooks.useAppContext(); + + const onDisplaySetLoadFailureHandler = error => { + UINotificationService.show({ + title: 'DICOM Segmentation Loader', + message: error.message, + type: 'error', + autoClose: false, + }); }; - const { UINotificationService } = servicesManager.services; + const segmentItemClickHandler = data => { + commandsManager.runCommand('jumpToImage', data); + commandsManager.runCommand('jumpToSlice', data); + }; + + const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => { + commandsManager.runCommand('setSegmentConfiguration', { + segmentNumber, + visible + }); + }; + + const onConfigurationChangeHandler = configuration => { + commandsManager.runCommand('setSegmentationConfiguration', { + globalOpacity: configuration.fillAlpha, + outlineThickness: configuration.outlineWidth, + renderOutline: configuration.renderOutline, + visible: configuration.renderFill + }); + }; + + const onSelectedSegmentationChangeHandler = () => { + commandsManager.runCommand('requestNewSegmentation'); + }; return ( ); }; diff --git a/extensions/dicom-segmentation/src/loadSegmentation.js b/extensions/dicom-segmentation/src/loadSegmentation.js index 63cbef08e11..388d562b6c7 100644 --- a/extensions/dicom-segmentation/src/loadSegmentation.js +++ b/extensions/dicom-segmentation/src/loadSegmentation.js @@ -63,6 +63,27 @@ export default async function loadSegmentation( segDisplaySet.labelmapIndex = labelmapIndex; + /* + * TODO: Improve the way we notify parts of the app that depends on segs to be loaded. + * + * Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel + * or other components that could rely on loaded segmentations that + * the segments were loaded so that e.g. when the user opens the panel + * before the segments are fully loaded, the panel can subscribe to this custom event + * and update itself with the new segments. + * + * This limitation is due to the fact that the cs segmentation module is an object (which will be + * updated after the segments are loaded) that React its not aware of its changes + * because the module object its not passed in to the panel component as prop but accessed externally. + * + * Improving this event approach to something reactive that can be tracked inside the react lifecycle, + * allows us to easily watch the module or the segmentations loading process in any other component + * without subscribing to external events. + */ + console.log('Segmentation loaded.'); + const event = new CustomEvent('extensiondicomsegmentationsegloaded'); + document.dispatchEvent(event); + resolve(labelmapIndex); }); } diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 6d1c4ecd234..b9c7643e67f 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -50,7 +50,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "lodash.throttle": "^4.1.1", - "react-vtkjs-viewport": "^0.8.3" + "react-vtkjs-viewport": "^0.9.0" }, "devDependencies": { "@ohif/core": "^2.9.3", diff --git a/extensions/vtk/src/LoadingIndicator.js b/extensions/vtk/src/LoadingIndicator.js index 7cb16399ef7..953c5e45f81 100644 --- a/extensions/vtk/src/LoadingIndicator.js +++ b/extensions/vtk/src/LoadingIndicator.js @@ -32,16 +32,16 @@ class LoadingIndicator extends PureComponent {
) : ( -
-
-

- {this.props.t('Reformatting')}... - - {percComplete} -

+
+
+

+ {this.props.t('Loading...')} + + {percComplete} +

+
-
- )} + )} ); } diff --git a/extensions/vtk/src/OHIFVTKViewport.js b/extensions/vtk/src/OHIFVTKViewport.js index 69c89c1648e..5f864ba2b80 100644 --- a/extensions/vtk/src/OHIFVTKViewport.js +++ b/extensions/vtk/src/OHIFVTKViewport.js @@ -49,7 +49,7 @@ class OHIFVTKViewport extends Component { state = { volumes: null, paintFilterLabelMapImageData: null, - paintFilterBackgroundImageData: null, + paintFilterBackgroundImageData: null }; static propTypes = { @@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component { }; static defaultProps = { - onScroll: () => {}, + onScroll: () => { }, }; static id = 'OHIFVTKViewport'; @@ -156,6 +156,10 @@ class OHIFVTKViewport extends Component { const { activeLabelmapIndex } = brushStackState; const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex]; + this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(isHidden => { + return { visible: !isHidden }; + }); + const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`; if (labelmapCache[vtkLabelmapID]) { @@ -339,13 +343,13 @@ class OHIFVTKViewport extends Component { this.setStateFromProps(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { displaySet } = this.props.viewportData; const prevDisplaySet = prevProps.viewportData.displaySet; if ( displaySet.displaySetInstanceUID !== - prevDisplaySet.displaySetInstanceUID || + prevDisplaySet.displaySetInstanceUID || displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID || displaySet.frameIndex !== prevDisplaySet.frameIndex ) { @@ -405,10 +409,6 @@ class OHIFVTKViewport extends Component { const style = { width: '100%', height: '100%', position: 'relative' }; - const visible = configuration.renderFill || configuration.renderOutline; - const opacity = configuration.fillAlpha; - const outlineThickness = configuration.outlineThickness; - return ( <>
@@ -428,10 +428,14 @@ class OHIFVTKViewport extends Component { dataDetails={this.state.dataDetails} labelmapRenderingOptions={{ colorLUT: this.state.labelmapColorLUT, - globalOpacity: opacity, - visible, - outlineThickness, - renderOutline: true, + globalOpacity: configuration.fillAlpha, + visible: configuration.renderFill, + outlineThickness: configuration.outlineWidth, + renderOutline: configuration.renderOutline, + segmentsDefaultProperties: this.segmentsDefaultProperties, + onNewSegmentationRequested: () => { + this.setStateFromProps(); + } }} onScroll={this.props.onScroll} /> diff --git a/extensions/vtk/src/commandsModule.js b/extensions/vtk/src/commandsModule.js index 7702b565634..e2b110c426e 100644 --- a/extensions/vtk/src/commandsModule.js +++ b/extensions/vtk/src/commandsModule.js @@ -5,10 +5,13 @@ import { vtkInteractorStyleMPRRotate, vtkSVGCrosshairsWidget, } from 'react-vtkjs-viewport'; - +import { getImageData } from 'react-vtkjs-viewport'; +import { vec3 } from 'gl-matrix'; import setMPRLayout from './utils/setMPRLayout.js'; import setViewportToVTK from './utils/setViewportToVTK.js'; import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js'; +import OHIFVTKViewport from './OHIFVTKViewport'; +import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate'; const { BlendMode } = Constants; @@ -34,7 +37,6 @@ const commandsModule = ({ commandsManager }) => { } const displaySet = viewportSpecificData[activeViewportIndex]; - let api; if (!api) { try { @@ -99,6 +101,20 @@ const commandsModule = ({ commandsManager }) => { }); } + const _convertModelToWorldSpace = (position, vtkImageData) => { + const indexToWorld = vtkImageData.getIndexToWorld(); + const pos = vec3.create(); + + position[0] += 0.5; /* Move to the centre of the voxel. */ + position[1] += 0.5; /* Move to the centre of the voxel. */ + position[2] += 0.5; /* Move to the centre of the voxel. */ + + vec3.set(pos, position[0], position[1], position[2]); + vec3.transformMat4(pos, pos, indexToWorld); + + return pos; + }; + const actions = { getVtkApis: ({ index }) => { return apis[index]; @@ -124,6 +140,119 @@ const commandsModule = ({ commandsManager }) => { _setView(api, [0, 1, 0], [0, 0, 1]); }, + requestNewSegmentation: async ({ viewports }) => { + const allViewports = Object.values(viewports.viewportSpecificData); + const promises = allViewports.map(async (viewport, viewportIndex) => { + let api = apis[viewportIndex]; + + if (!api) { + api = await _getActiveViewportVTKApi(viewports); + apis[viewportIndex] = api; + } + + api.requestNewSegmentation(); + api.updateImage(); + }); + await Promise.all(promises); + }, + jumpToSlice: async ({ + viewports, + studies, + StudyInstanceUID, + displaySetInstanceUID, + SOPClassUID, + SOPInstanceUID, + segmentNumber, + frameIndex, + frame, + done = () => { } + }) => { + let api = apis[viewports.activeViewportIndex]; + + if (!api) { + api = await _getActiveViewportVTKApi(viewports); + apis[viewports.activeViewportIndex] = api; + } + + const stack = OHIFVTKViewport.getCornerstoneStack( + studies, + StudyInstanceUID, + displaySetInstanceUID, + SOPClassUID, + SOPInstanceUID, + frameIndex, + ); + + const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID); + + let pixelIndex = 0; + let x = 0; + let y = 0; + let count = 0; + + const rows = imageDataObject.dimensions[1]; + const cols = imageDataObject.dimensions[0]; + + for (let j = 0; j < rows; j++) { + for (let i = 0; i < cols; i++) { + // [i, j] = + const pixel = frame.pixelData[pixelIndex]; + if (pixel === segmentNumber) { + x += i; + y += j; + count++; + } + pixelIndex++; + } + } + x /= count; + y /= count; + + const position = [x, y, frameIndex]; + const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData); + + api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis); + done(); + }, + setSegmentationConfiguration: async ({ + viewports, + globalOpacity, + visible, + renderOutline, + outlineThickness, + }) => { + const allViewports = Object.values(viewports.viewportSpecificData); + const promises = allViewports.map(async (viewport, viewportIndex) => { + let api = apis[viewportIndex]; + + if (!api) { + api = await _getActiveViewportVTKApi(viewports); + apis[viewportIndex] = api; + } + + api.setGlobalOpacity(globalOpacity); + api.setVisibility(visible); + api.setOutlineThickness(outlineThickness); + api.setOutlineRendering(renderOutline); + api.updateImage(); + }); + await Promise.all(promises); + }, + setSegmentConfiguration: async ({ viewports, visible, segmentNumber }) => { + const allViewports = Object.values(viewports.viewportSpecificData); + const promises = allViewports.map(async (viewport, viewportIndex) => { + let api = apis[viewportIndex]; + + if (!api) { + api = await _getActiveViewportVTKApi(viewports); + apis[viewportIndex] = api; + } + + api.setSegmentVisibility(segmentNumber, visible); + api.updateImage(); + }); + await Promise.all(promises); + }, enableRotateTool: () => { apis.forEach(api => { const istyle = vtkInteractorStyleMPRRotate.newInstance(); @@ -280,6 +409,26 @@ const commandsModule = ({ commandsManager }) => { window.vtkActions = actions; const definitions = { + requestNewSegmentation: { + commandFn: actions.requestNewSegmentation, + storeContexts: ['viewports'], + options: {}, + }, + jumpToSlice: { + commandFn: actions.jumpToSlice, + storeContexts: ['viewports'], + options: {}, + }, + setSegmentationConfiguration: { + commandFn: actions.setSegmentationConfiguration, + storeContexts: ['viewports'], + options: {}, + }, + setSegmentConfiguration: { + commandFn: actions.setSegmentConfiguration, + storeContexts: ['viewports'], + options: {}, + }, axial: { commandFn: actions.axial, storeContexts: ['viewports'], diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 911427f6778..a33c3dfe5ab 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -79,7 +79,7 @@ export class StudyMetadata extends Metadata { Object.defineProperty(this, 'studyInstanceUID', { configurable: false, enumerable: false, - get: function() { + get: function () { return this.getStudyInstanceUID(); }, }); diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 7bb5a30b299..bb32ab553f3 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -2,7 +2,7 @@ import MODULE_TYPES from './MODULE_TYPES.js'; import log from './../log.js'; export default class ExtensionManager { - constructor({ commandsManager, servicesManager, appConfig = {} }) { + constructor({ commandsManager, servicesManager, api, appConfig = {} }) { this.modules = {}; this.registeredExtensionIds = []; this.moduleTypeNames = Object.values(MODULE_TYPES); @@ -10,6 +10,7 @@ export default class ExtensionManager { this._commandsManager = commandsManager; this._servicesManager = servicesManager; this._appConfig = appConfig; + this._api = api; this.moduleTypeNames.forEach(moduleType => { this.modules[moduleType] = []; @@ -119,6 +120,7 @@ export default class ExtensionManager { commandsManager: this._commandsManager, appConfig: this._appConfig, configuration, + api: this._api }); if (!extensionModule) { diff --git a/platform/viewer/cypress/integration/pwa/OHIFExtensionVTK.spec.js b/platform/viewer/cypress/integration/pwa/OHIFExtensionVTK.spec.js index edf7b028376..935f5175987 100644 --- a/platform/viewer/cypress/integration/pwa/OHIFExtensionVTK.spec.js +++ b/platform/viewer/cypress/integration/pwa/OHIFExtensionVTK.spec.js @@ -21,8 +21,8 @@ describe('OHIF VTK Extension', () => { //Select 2D MPR button cy.get('[data-cy="2d mpr"]').click(); - //Wait Reformatting Images - cy.waitVTKReformatting(); + //Wait waitVTKLoading Images + cy.waitVTKLoading(); }); beforeEach(() => { diff --git a/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFExtensionVTK.spec.js b/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFExtensionVTK.spec.js index 231e817e32c..b40d30b3dad 100644 --- a/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFExtensionVTK.spec.js +++ b/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFExtensionVTK.spec.js @@ -18,8 +18,8 @@ describe('Visual Regression - OHIF VTK Extension', () => { //Select 2D MPR button cy.get('[data-cy="2d mpr"]').click(); - //Wait Reformatting Images - cy.waitVTKReformatting(); + //Wait waitVTKLoading Images + cy.waitVTKwaitVTKLoading(); }); beforeEach(() => { diff --git a/platform/viewer/cypress/support/commands.js b/platform/viewer/cypress/support/commands.js index 487b56f2df6..68e9eb873fb 100644 --- a/platform/viewer/cypress/support/commands.js +++ b/platform/viewer/cypress/support/commands.js @@ -107,15 +107,15 @@ Cypress.Commands.add('waitStudyList', () => { }); }); -Cypress.Commands.add('waitVTKReformatting', () => { - // Wait for start reformatting +Cypress.Commands.add('waitVTKLoading', () => { + // Wait for start loading cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => { - expect($grid).to.contain.text('Reform'); + expect($grid).to.contain.text('Loading'); }); - // Wait for finish reformatting + // Wait for finish loading cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => { - expect($grid).not.to.contain.text('Reform'); + expect($grid).not.to.contain.text('Loading'); }); }); diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js index f41ff394bc0..4c33735008c 100644 --- a/platform/viewer/src/App.js +++ b/platform/viewer/src/App.js @@ -53,7 +53,7 @@ import store from './store'; /** Contexts */ import WhiteLabelingContext from './context/WhiteLabelingContext'; import UserManagerContext from './context/UserManagerContext'; -import AppContext from './context/AppContext'; +import { AppProvider, useAppContext, CONTEXTS } from './context/AppContext'; /** ~~~~~~~~~~~~~ Application Setup */ const commandsManagerConfig = { @@ -159,8 +159,8 @@ class App extends Component { if (this._userManager) { return ( - - + + @@ -183,14 +183,14 @@ class App extends Component { - - + + ); } return ( - - + + @@ -204,8 +204,8 @@ class App extends Component { - - + + ); } @@ -255,6 +255,12 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) { commandsManager, servicesManager, appConfig, + api: { + contexts: CONTEXTS, + hooks: { + useAppContext + } + } }); const requiredExtensions = [ diff --git a/platform/viewer/src/OHIFStandaloneViewer.js b/platform/viewer/src/OHIFStandaloneViewer.js index 9d0aedac113..baad5d759f2 100644 --- a/platform/viewer/src/OHIFStandaloneViewer.js +++ b/platform/viewer/src/OHIFStandaloneViewer.js @@ -191,8 +191,8 @@ class OHIFStandaloneViewer extends Component { {match === null ? ( <> ) : ( - - )} + + )} )} diff --git a/platform/viewer/src/connectedComponents/ConnectedToolbarRow.js b/platform/viewer/src/connectedComponents/ConnectedToolbarRow.js deleted file mode 100644 index 3df035c3ebd..00000000000 --- a/platform/viewer/src/connectedComponents/ConnectedToolbarRow.js +++ /dev/null @@ -1,15 +0,0 @@ -// TODO: REPLACE THIS WITH A CONTEXT PROVIDER -// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT -import ToolbarRow from './ToolbarRow'; -import { connect } from 'react-redux'; -import { getActiveContexts } from './../store/layout/selectors.js'; - -const mapStateToProps = state => { - return { - activeContexts: getActiveContexts(state), - }; -}; - -const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow); - -export default ConnectedToolbarRow; diff --git a/platform/viewer/src/connectedComponents/ToolbarRow.js b/platform/viewer/src/connectedComponents/ToolbarRow.js index a1729a4fc2a..b03f778bc8c 100644 --- a/platform/viewer/src/connectedComponents/ToolbarRow.js +++ b/platform/viewer/src/connectedComponents/ToolbarRow.js @@ -16,6 +16,7 @@ import { commandsManager, extensionManager } from './../App.js'; import ConnectedCineDialog from './ConnectedCineDialog'; import ConnectedLayoutButton from './ConnectedLayoutButton'; +import { withAppContext } from '../context/AppContext'; class ToolbarRow extends Component { // TODO: Simplify these? isOpen can be computed if we say "any" value for selected, @@ -381,5 +382,5 @@ function _handleBuiltIn(button) { } export default withTranslation(['Common', 'ViewportDownloadForm'])( - withModal(withDialog(ToolbarRow)) + withModal(withDialog(withAppContext(ToolbarRow))) ); diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index 851d9462fb1..f2acea44c9e 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -7,7 +7,7 @@ import OHIF, { DICOMSR } from '@ohif/core'; import { withDialog } from '@ohif/ui'; import moment from 'moment'; import ConnectedHeader from './ConnectedHeader.js'; -import ConnectedToolbarRow from './ConnectedToolbarRow.js'; +import ToolbarRow from './ToolbarRow.js'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedViewerMain from './ConnectedViewerMain.js'; import SidePanel from './../components/SidePanel.js'; @@ -256,7 +256,7 @@ class Viewer extends Component { {/* TOOLBAR */} - ) : ( - - )} + + )} {/* MAIN */} @@ -349,7 +349,7 @@ export default withDialog(Viewer); * @param {Study[]} studies * @param {DisplaySet[]} studies[].displaySets */ -const _mapStudiesToThumbnails = function(studies) { +const _mapStudiesToThumbnails = function (studies) { return studies.map(study => { const { StudyInstanceUID } = study; diff --git a/platform/viewer/src/context/AppContext.js b/platform/viewer/src/context/AppContext.js index 82bec40c40e..1bd8e817952 100644 --- a/platform/viewer/src/context/AppContext.js +++ b/platform/viewer/src/context/AppContext.js @@ -1,5 +1,33 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { getActiveContexts } from '../store/layout/selectors.js'; let AppContext = React.createContext({}); +export const CONTEXTS = { + CORNERSTONE: 'ACTIVE_VIEWPORT::CORNERSTONE', + VTK: 'ACTIVE_VIEWPORT::VTK' +}; + +export const useAppContext = () => useContext(AppContext); + +export const AppProvider = ({ children, config }) => { + const activeContexts = useSelector(state => getActiveContexts(state)); + + return ( + + {children} + + ); +}; + +export const withAppContext = Component => { + return function WrappedComponent(props) { + const { appConfig, activeContexts } = useAppContext(); + return ( + + ); + }; +}; + export default AppContext; diff --git a/yarn.lock b/yarn.lock index 4a06d84e476..ed51294ddd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9110,6 +9110,11 @@ gl-matrix@^3.1.0: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0" integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A== +gl-matrix@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" + integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== + gl-preserve-state@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198" @@ -15598,7 +15603,7 @@ react-codemirror2@^6.0.0: resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525" integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag== -react-cornerstone-viewport@^2.3.8: +react-cornerstone-viewport@2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.8.tgz#7af8360f29bca986ae4e36b4e503269b88ddc52f" integrity sha512-aiG2uVNrDY6SQx4t/HBxIA3zsMsCwT+6TpcXK9qSSoXhs+X6OTmYEKncWUqL0jtxU1yfh6JTUz8ARTg03gtF+A== @@ -16024,10 +16029,10 @@ react-transition-group@^4.1.1: loose-envify "^1.4.0" prop-types "^15.6.2" -react-vtkjs-viewport@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.8.4.tgz#6bd9837d7762b845e77f053fb8fbf4469428d594" - integrity sha512-b90ENJzmStiahxVox+7mMwZij3NVUu//jE07PV2Qtp6E7q/eLXn6ZUwcEt9w5YHN36xLZVcx9b53ztLcXVgh/Q== +react-vtkjs-viewport@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.9.0.tgz#5a7890643e511946f960db6b05142318be2dfe80" + integrity sha512-kjpNr1n+eW0nU736HH07IsSYQXphxfn/RBvKtpBer9tNsl+FyLH8h+uY8KWdv8kVBHkEoaH6/srxB0JBa8l+jA== dependencies: date-fns "^2.2.1" gl-matrix "^3.1.0"