diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index e6bd1d624..ad6376803 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -36,6 +36,7 @@ function OHIFCornerstoneSEGViewport(props) { DisplaySetService, ToolGroupService, SegmentationService, + UINotificationService, } = servicesManager.services; const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`; @@ -105,7 +106,6 @@ function OHIFCornerstoneSEGViewport(props) { } = referencedDisplaySetRef.current; // Todo: jump to the center of the first segment - return ( { } }, [elementRef]); + const cleanUpServices = useCallback(() => { + const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( + viewportIndex + ); + + if (!viewportInfo) { + return; + } + + const viewportId = viewportInfo.getViewportId(); + const renderingEngineId = viewportInfo.getRenderingEngineId(); + const syncGroups = viewportInfo.getSyncGroups(); + + ToolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId); + + SyncGroupService.removeViewportFromSyncGroup( + viewportId, + renderingEngineId, + syncGroups + ); + }, [viewportIndex, viewportOptions.viewportId]); + const elementEnabledHandler = useCallback( evt => { // check this is this element reference and return early if doesn't match @@ -188,28 +216,15 @@ const OHIFCornerstoneViewport = React.memo(props => { setImageScrollBarHeight(); return () => { - const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( - viewportIndex - ); - - const viewportId = viewportInfo.getViewportId(); - const renderingEngineId = viewportInfo.getRenderingEngineId(); - const syncGroups = viewportInfo.getSyncGroups(); - - ToolGroupService.removeViewportFromToolGroup( - viewportId, - renderingEngineId - ); - - SyncGroupService.removeViewportFromSyncGroup( - viewportId, - renderingEngineId, - syncGroups - ); + cleanUpServices(); CornerstoneViewportService.disableElement(viewportIndex); if (onElementDisabled) { + const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( + viewportIndex + ); + onElementDisabled(viewportInfo); } @@ -326,8 +341,8 @@ const OHIFCornerstoneViewport = React.memo(props => { handleWidth handleHeight skipOnMount={true} // Todo: make these configurable - refreshMode={'throttle'} - refreshRate={100} + refreshMode={'debounce'} + refreshRate={200} // transition amount in side panel onResize={onResize} targetRef={elementRef.current} /> @@ -355,7 +370,7 @@ function _subscribeToJumpToMeasurementEvents( elementRef, viewportIndex, displaySets, - viewportGridService, + ViewportGridService, CornerstoneViewportService ) { const displaysUIDs = displaySets.map( diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 243161108..c7839706b 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -7,34 +7,20 @@ import { ToolGroupManager, Enums, utilities as cstUtils, - segmentation as cstSegmentation, ReferenceLinesTool, - synchronizers as cstSynchronizers, } from '@cornerstonejs/tools'; -import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; - import { getEnabledElement as OHIFgetEnabledElement } from './state'; +import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import callInputDialog from './utils/callInputDialog'; import { setColormap } from './utils/colormap/transferFunctionHelpers'; -import getProtocolViewportStructureFromGridViewports from './utils/getProtocolViewportStructureFromGridViewports'; -import removeToolGroupSegmentationRepresentations from './utils/removeToolGroupSegmentationRepresentations'; -import calculateViewportRegistrations from './utils/calculateViewportRegistrations'; - -const MPR_TOOLGROUP_ID = 'mpr'; - -// [ { -// synchronizerId: string, -// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...] -// ]} -let STACK_IMAGE_SYNC_GROUPS_INFO = []; +import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol'; +import toggleStackImageSync from './utils/stackSync/toggleStackImageSync'; const commandsModule = ({ servicesManager }) => { const { ViewportGridService, ToolGroupService, - DisplaySetService, - SyncGroupService, CineService, ToolBarService, UIDialogService, @@ -160,48 +146,51 @@ const commandsModule = ({ servicesManager }) => { } } - const toolGroup = _getToolGroup(toolGroupId); - - if (!toolGroup) { - console.warn('No tool group found for toolGroupId:', toolGroupId); - return; - } - // Todo: we need to check if the viewports of the toolGroup is actually - // parts of the ViewportGrid's viewports, if not we return - const { viewports } = ViewportGridService.getState() || { viewports: [], }; - // iterate over all viewports and set the tool active for the - // viewports that belong to the toolGroup - for (let index = 0; index < viewports.length; index++) { - const ohifEnabledElement = OHIFgetEnabledElement(index); - - if (!ohifEnabledElement) { - continue; - } - - const viewport = getEnabledElement(ohifEnabledElement.element); - - if (!viewport) { - continue; - } - - // Find the current active tool and set it to be passive - const activeTool = toolGroup.getActivePrimaryMouseButtonTool(); - - if (activeTool) { - toolGroup.setToolPassive(activeTool); - } - - // Set the new toolName to be active - toolGroup.setToolActive(toolName, { - bindings: [{ mouseButton: Enums.MouseBindings.Primary }], - }); + const toolGroup = _getToolGroup(toolGroupId); + const toolGroupViewportIds = toolGroup.getViewportIds(); + // if toolGroup has been destroyed, or its viewports have been removed + if (!toolGroupViewportIds || !toolGroupViewportIds.length) { return; } + + const filteredViewports = viewports.filter(viewport => { + if (!viewport.viewportOptions) { + return false; + } + + return toolGroupViewportIds.includes( + viewport.viewportOptions.viewportId + ); + }); + + if (!filteredViewports.length) { + return; + } + + const activeToolName = toolGroup.getActivePrimaryMouseButtonTool(); + + if (activeToolName) { + // Todo: this is a hack to prevent the crosshairs to stick around + // after another tool is selected. We should find a better way to do this + if (activeToolName === 'Crosshairs') { + toolGroup.setToolDisabled(activeToolName); + } else { + toolGroup.setToolPassive(activeToolName); + } + } + // Set the new toolName to be active + toolGroup.setToolActive(toolName, { + bindings: [ + { + mouseButton: Enums.MouseBindings.Primary, + }, + ], + }); }, showDownloadViewportModal: () => { const { activeViewportIndex } = ViewportGridService.getState(); @@ -375,229 +364,17 @@ const commandsModule = ({ servicesManager }) => { HangingProtocolService.setProtocol(protocolId); }, toggleMPR: ({ toggledState }) => { - const { activeViewportIndex, viewports } = ViewportGridService.getState(); - const viewportDisplaySetInstanceUIDs = - viewports[activeViewportIndex].displaySetInstanceUIDs; - - const errorCallback = error => { - UINotificationService.show({ - title: 'Multiplanar reconstruction (MPR) ', - message: - 'Cannot create MPR for this DisplaySet since it is not reconstructable.', - type: 'info', - duration: 3000, - }); - }; - - const cacheId = 'beforeMPR'; - if (toggledState) { - ViewportGridService.setCachedLayout({ - cacheId, - cachedLayout: ViewportGridService.getState(), - }); - - const matchDetails = { - displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs, - }; - - HangingProtocolService.setProtocol( - MPR_TOOLGROUP_ID, - matchDetails, - errorCallback - ); - return; - } - - const { cachedLayout } = ViewportGridService.getState(); - - if (!cachedLayout || !cachedLayout[cacheId]) { - return; - } - - const { viewports: cachedViewports, numRows, numCols } = cachedLayout[ - cacheId - ]; - - // Todo: The following assumes that when turning off MPR we are applying the default - // protocol which might not be the one that was used before MPR was turned on - // In order to properly implement this logic, we should modify the hanging protocol - // upon layout change with layout selector, and cache and restore it when turning - // MPR on and off - const viewportStructure = getProtocolViewportStructureFromGridViewports({ - viewports: cachedViewports, - numRows, - numCols, + toggleMPRHangingProtocol({ + toggledState, + servicesManager, + getToolGroup: _getToolGroup, }); - - const viewportSpecificMatch = cachedViewports.reduce( - (acc, viewport, index) => { - const { - displaySetInstanceUIDs, - viewportOptions, - displaySetOptions, - } = viewport; - - acc[index] = { - displaySetInstanceUIDs, - viewportOptions, - displaySetOptions, - }; - - return acc; - }, - {} - ); - - const defaultProtocol = HangingProtocolService.getProtocolById('default'); - - // Todo: this assumes there is only one stage in the default protocol - const defaultProtocolStage = defaultProtocol.stages[0]; - defaultProtocolStage.viewportStructure = viewportStructure; - - const { primaryToolId } = ToolBarService.state; - const mprToolGroup = _getToolGroup(MPR_TOOLGROUP_ID); - // turn off crosshairs if it is on - if ( - primaryToolId === 'Crosshairs' || - mprToolGroup.getToolInstance('Crosshairs')?.mode === - Enums.ToolModes.Active - ) { - const toolGroup = _getToolGroup(MPR_TOOLGROUP_ID); - toolGroup.setToolDisabled('Crosshairs'); - ToolBarService.recordInteraction({ - groupId: 'WindowLevel', - itemId: 'WindowLevel', - interactionType: 'tool', - commands: [ - { - commandName: 'setToolActive', - commandOptions: { - toolName: 'WindowLevel', - }, - context: 'CORNERSTONE', - }, - ], - }); - } - - // clear segmentations if they exist - removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID); - - HangingProtocolService.setProtocol( - 'default', - viewportSpecificMatch, - error => { - UINotificationService.show({ - title: 'Multiplanar reconstruction (MPR) ', - message: - 'Something went wrong while trying to restore the previous layout.', - type: 'info', - duration: 3000, - }); - } - ); }, toggleStackImageSync: ({ toggledState }) => { - if (!toggledState) { - STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => { - const { viewports, synchronizerId } = syncGroupInfo; - - viewports.forEach(({ viewportId, renderingEngineId }) => { - SyncGroupService.removeViewportFromSyncGroup( - viewportId, - renderingEngineId, - synchronizerId - ); - }); - }); - - return; - } - - STACK_IMAGE_SYNC_GROUPS_INFO = []; - - // create synchronization groups and add viewports - let { viewports } = ViewportGridService.getState(); - - // filter empty viewports - viewports = viewports.filter( - viewport => - viewport.displaySetInstanceUIDs && - viewport.displaySetInstanceUIDs.length - ); - - // filter reconstructable viewports - viewports = viewports.filter(viewport => { - const { displaySetInstanceUIDs } = viewport; - - for (const displaySetInstanceUID of displaySetInstanceUIDs) { - const displaySet = DisplaySetService.getDisplaySetByUID( - displaySetInstanceUID - ); - - if (displaySet && displaySet.isReconstructable) { - return true; - } - - return false; - } - }); - - const viewportsByOrientation = viewports.reduce((acc, viewport) => { - const { viewportId, viewportType } = viewport.viewportOptions; - - if (viewportType !== 'stack') { - console.warn('Viewport is not a stack, cannot sync images yet'); - return acc; - } - - const { element } = CornerstoneViewportService.getViewportInfo( - viewportId - ); - const { viewport: csViewport, renderingEngineId } = getEnabledElement( - element - ); - const { viewPlaneNormal } = csViewport.getCamera(); - - // Should we round here? I guess so, but not sure how much precision we need - const orientation = viewPlaneNormal.map(v => Math.round(v)).join(','); - - if (!acc[orientation]) { - acc[orientation] = []; - } - - acc[orientation].push({ viewportId, renderingEngineId }); - - return acc; - }, {}); - - // create synchronizer for each group - Object.values(viewportsByOrientation).map(viewports => { - let synchronizerId = viewports - .map(({ viewportId }) => viewportId) - .join(','); - - synchronizerId = `imageSync_${synchronizerId}`; - - calculateViewportRegistrations(viewports); - - viewports.forEach(({ viewportId, renderingEngineId }) => { - SyncGroupService.addViewportToSyncGroup( - viewportId, - renderingEngineId, - { - type: 'stackimage', - id: synchronizerId, - source: true, - target: true, - } - ); - }); - - STACK_IMAGE_SYNC_GROUPS_INFO.push({ - synchronizerId, - viewports, - }); + toggleStackImageSync({ + getEnabledElement, + servicesManager, + toggledState, }); }, toggleReferenceLines: ({ toggledState }) => { @@ -635,11 +412,6 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, - toggleCrosshairs: { - commandFn: actions.toggleCrosshairs, - storeContexts: [], - options: {}, - }, rotateViewportCW: { commandFn: actions.rotateViewport, storeContexts: [], diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index b4319383c..93f51d768 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -362,13 +362,13 @@ export default async function init({ const viewportId = `viewport-${viewportIndex}`; const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId); - if (!toolGroup) { + if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) { return; } // check if reference lines are active const referenceLinesEnabled = - toolGroup._toolInstances?.['ReferenceLines'].mode === + toolGroup._toolInstances['ReferenceLines'].mode === Enums.ToolModes.Enabled; if (!referenceLinesEnabled) { diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 478e0195b..559d0f069 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -590,6 +590,8 @@ class SegmentationService { // Note: ideally we could use the TypedArray set method, but since each // slice can have multiple segments, we need to loop over each slice and // set the segment value for each segment. + let overlappingSegments = false; + const _segmentInfoUpdate = (segmentInfo, segmentIndex) => { const { pixelData: segPixelData } = segmentInfo; @@ -636,6 +638,10 @@ class SegmentationService { i++, j++ ) { if (functionGroupPixelData[j] !== 0) { + if (derivedVolumeScalarData[i] !== 0) { + overlappingSegments = true; + } + derivedVolumeScalarData[i] = segmentIndex; // centroid calculations @@ -716,6 +722,7 @@ class SegmentationService { this._broadcastEvent(EVENTS.SEGMENTATION_PIXEL_DATA_CREATED, { segmentationId, segDisplaySet, + overlappingSegments, }); return this.addOrUpdateSegmentation(segmentationSchema, suppressEvents); diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 6b25b43fe..6736d9003 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -77,17 +77,14 @@ class CornerstoneViewportService implements IViewportService { viewportOptions: PublicViewportOptions, elementRef: HTMLDivElement ) { - const viewportId = - viewportOptions.viewportId || this.getViewportId(viewportIndex); - const viewportInfo = new ViewportInfo(viewportIndex, viewportId); + const viewportInfo = new ViewportInfo( + viewportIndex, + viewportOptions.viewportId + ); viewportInfo.setElement(elementRef); this.viewportsInfo.set(viewportIndex, viewportInfo); } - public getViewportId(viewportIndex: number): string { - return `viewport-${viewportIndex}`; - } - public getViewportIds(): string[] { const viewportIds = []; @@ -176,6 +173,22 @@ class CornerstoneViewportService implements IViewportService { ): void { const renderingEngine = this.getRenderingEngine(); const viewportInfo = this.viewportsInfo.get(viewportIndex); + let viewportId = viewportInfo.getViewportId(); + + // if currently there is a viewport with the viewportId, but it is not the same + // as the one we are trying to set, we need to disable the old one + // and enable the new one, we could ideally change the name of the viewportId + // but the viewportId is an integral part in renderers map, tools svg cache + // etc. which would require a lot of refactoring, for now we will just disable + // the old one and enable the new one at the end of this function + let newViewportId = null; + if (publicViewportOptions?.viewportId !== viewportId) { + newViewportId = publicViewportOptions.viewportId; + viewportInfo.setViewportId(newViewportId); + + renderingEngine.disableElement(viewportId); + } + viewportInfo.setRenderingEngineId(renderingEngine.id); const { @@ -196,7 +209,7 @@ class CornerstoneViewportService implements IViewportService { viewportIndex, }); - const viewportId = viewportInfo.getViewportId(); + viewportId = viewportInfo.getViewportId(); const element = viewportInfo.getElement(); const type = viewportInfo.getViewportType(); const background = viewportInfo.getBackground(); @@ -218,7 +231,8 @@ class CornerstoneViewportService implements IViewportService { // ENABLED_ELEMENT again and again, which will run onEnableElement callbacks renderingEngine.enableElement(viewportInput); - this._setDisplaySets(viewportId, viewportData, viewportInfo); + const viewport = renderingEngine.getViewport(viewportId); + this._setDisplaySets(viewport, viewportData, viewportInfo); } public getCornerstoneViewport( @@ -616,12 +630,10 @@ class CornerstoneViewportService implements IViewportService { } _setDisplaySets( - viewportId: string, + viewport: StackViewport | VolumeViewport, viewportData: StackViewportData | VolumeViewportData, viewportInfo: ViewportInfo ): void { - const viewport = this.getCornerstoneViewport(viewportId); - if (viewport instanceof StackViewport) { this._setStackViewport( viewport, @@ -687,10 +699,6 @@ class CornerstoneViewportService implements IViewportService { } { const viewportIndex = viewportInfo.getViewportIndex(); - if (!publicViewportOptions.viewportId) { - publicViewportOptions.viewportId = this.getViewportId(viewportIndex); - } - // Creating a temporary viewportInfo to handle defaults const newViewportInfo = new ViewportInfo( viewportIndex, diff --git a/extensions/cornerstone/src/utils/getProtocolViewportStructureFromGridViewports.ts b/extensions/cornerstone/src/utils/mpr/getProtocolViewportStructureFromGridViewports.ts similarity index 100% rename from extensions/cornerstone/src/utils/getProtocolViewportStructureFromGridViewports.ts rename to extensions/cornerstone/src/utils/mpr/getProtocolViewportStructureFromGridViewports.ts diff --git a/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts b/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts new file mode 100644 index 000000000..cce8869a3 --- /dev/null +++ b/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts @@ -0,0 +1,285 @@ +import { Enums } from '@cornerstonejs/tools'; +import removeToolGroupSegmentationRepresentations from '../removeToolGroupSegmentationRepresentations'; + +const MPR_TOOLGROUP_ID = 'mpr'; + +const cachedState = { + protocol: null, + stage: null, + viewportMatchDetails: null, + viewportStructure: null, + toolOptions: null, +}; + +const setCachedState = ( + protocol, + stage, + viewportMatchDetails, + viewportStructure, + toolOptions +) => { + cachedState.protocol = protocol; + cachedState.stage = stage; + cachedState.viewportMatchDetails = viewportMatchDetails; + cachedState.viewportStructure = viewportStructure; + cachedState.toolOptions = JSON.parse(JSON.stringify(toolOptions)); +}; + +const resetCachedState = () => { + cachedState.protocol = null; + cachedState.stage = null; + cachedState.viewportMatchDetails = null; + cachedState.viewportStructure = null; + cachedState.toolOptions = null; +}; + +export default function toggleMPRHangingProtocol({ + toggledState, + servicesManager, + getToolGroup, +}) { + const { + UINotificationService, + HangingProtocolService, + ViewportGridService, + ToolBarService, + } = servicesManager.services; + + const { + activeViewportIndex, + viewports, + numRows, + numCols, + } = ViewportGridService.getState(); + const viewportDisplaySetInstanceUIDs = + viewports[activeViewportIndex].displaySetInstanceUIDs; + + const errorCallback = error => { + UINotificationService.show({ + title: 'Multiplanar reconstruction (MPR) ', + message: + 'Cannot create MPR for this DisplaySet since it is not reconstructable.', + type: 'info', + duration: 3000, + }); + }; + + // What is the current active protocol and stage number to restore later + const { protocol, stage } = HangingProtocolService.getActiveProtocol(); + + if (toggledState) { + resetCachedState(); + + const { + viewportMatchDetails, + viewportStructure, + toolOptions, + } = _getViewportsInfo({ + protocol, + stage, + viewports, + servicesManager, + }); + + setCachedState( + protocol, + stage, + viewportMatchDetails, + viewportStructure, + toolOptions + ); + + const matchDetails = { + displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs, + }; + + _disableCrosshairs( + toolOptions.map(({ toolGroupId }) => toolGroupId), + getToolGroup + ); + + HangingProtocolService.setProtocol( + MPR_TOOLGROUP_ID, + matchDetails, + errorCallback + ); + return; + } + + const restoreErrorCallback = error => { + UINotificationService.show({ + title: 'Multiplanar reconstruction (MPR) ', + message: + 'Something went wrong while trying to restore the previous layout.', + type: 'info', + duration: 3000, + }); + }; + + _disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup); + + const { layoutType, properties } = cachedState.viewportStructure; + const { viewportMatchDetails } = cachedState; + + // The reason we split the flow here is that we don't allow viewport grid + // change in the non default hanging protocol, so we can just apply the + // cached protocol and stage. However, for the default protocol, we need + // to also apply the layout type and properties. + if (cachedState.protocol.id !== 'default') { + HangingProtocolService.setProtocol( + cachedState.protocol.id, + viewportMatchDetails, + restoreErrorCallback + ); + + return; + } + + HangingProtocolService.setProtocol( + 'default', + viewportMatchDetails, + restoreErrorCallback + ); + + if (numRows !== properties.rows || numCols !== properties.columns) { + ViewportGridService.setLayout({ + numRows: properties.rows, + numCols: properties.columns, + layoutType, + layoutOptions: properties.layoutOptions, + }); + } + + const numViewports = + properties.layoutOptions.length || properties.rows * properties.columns; + + // loop inside viewportMatchDetails map + // and set the viewportOptions for each viewport + [...Array(numViewports).keys()].forEach(viewportIndex => { + const viewportMatchDetailsForViewport = viewportMatchDetails.get( + viewportIndex + ); + + if (viewportMatchDetailsForViewport) { + const { + viewportOptions, + displaySetsInfo, + } = viewportMatchDetailsForViewport; + ViewportGridService.setDisplaySetsForViewport({ + viewportIndex, + displaySetInstanceUIDs: displaySetsInfo.map( + displaySetInfo => displaySetInfo.displaySetInstanceUID + ), + viewportOptions, + }); + } else { + ViewportGridService.setDisplaySetsForViewport({ + viewportIndex, + displaySetInstanceUIDs: [], + viewportOptions: {}, + }); + } + }); + + ToolBarService.recordInteraction({ + groupId: 'WindowLevel', + itemId: 'WindowLevel', + interactionType: 'tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'WindowLevel', + }, + context: 'CORNERSTONE', + }, + ], + }); + + //clear segmentations if they exist + removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID); +} + +function _disableCrosshairs(toolGroupIds, getToolGroup) { + toolGroupIds.forEach(toolGroupId => { + const toolGroup = getToolGroup(toolGroupId); + if ( + toolGroup.getToolInstance('Crosshairs')?.mode === Enums.ToolModes.Active + ) { + toolGroup.setToolDisabled('Crosshairs'); + } + }); +} + +function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) { + // here we need to use the viewports and try to map it into the + // viewportMatchDetails and displaySetMatch that HangingProtocolService + // expects + const { + ViewportGridService, + HangingProtocolService, + ToolGroupService, + } = servicesManager.services; + + const { numRows, numCols } = ViewportGridService.getState(); + + let viewportMatchDetails = new Map(); + + const viewportStructure = { + layoutType: 'grid', + properties: { + rows: numRows, + columns: numCols, + layoutOptions: [], + }, + }; + + viewports.forEach((viewport, viewportIndex) => { + viewportStructure.properties.layoutOptions.push({ + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + }); + }); + + if (protocol.id === 'default') { + viewports.forEach((viewport, viewportIndex) => { + if (viewport.displaySetInstanceUIDs) { + viewportMatchDetails.set(viewportIndex, { + displaySetsInfo: viewport.displaySetInstanceUIDs.map( + displaySetInstanceUID => { + return { displaySetInstanceUID }; + } + ), + viewportOptions: viewport.viewportOptions, + }); + } + }); + } else { + ({ viewportMatchDetails } = HangingProtocolService.getMatchDetails()); + } + + // get the toolGroup state for viewports + let toolOptions = []; + const viewportIds = viewports + .map( + viewport => + viewport.displaySetInstanceUIDs && + viewport.displaySetInstanceUIDs.length > 0 && + viewport.viewportOptions?.viewportId + ) + .filter(Boolean); + + if (viewportIds.length) { + toolOptions = viewportIds.map(viewportId => { + const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId); + return { + toolGroupId: toolGroup.id, + toolOptions: toolGroup.toolOptions, + }; + }); + } + + return { viewportMatchDetails, viewportStructure, toolOptions }; +} diff --git a/extensions/cornerstone/src/utils/calculateViewportRegistrations.ts b/extensions/cornerstone/src/utils/stackSync/calculateViewportRegistrations.ts similarity index 100% rename from extensions/cornerstone/src/utils/calculateViewportRegistrations.ts rename to extensions/cornerstone/src/utils/stackSync/calculateViewportRegistrations.ts diff --git a/extensions/cornerstone/src/utils/stackSync/toggleStackImageSync.ts b/extensions/cornerstone/src/utils/stackSync/toggleStackImageSync.ts new file mode 100644 index 000000000..3ff32cdbe --- /dev/null +++ b/extensions/cornerstone/src/utils/stackSync/toggleStackImageSync.ts @@ -0,0 +1,115 @@ +import calculateViewportRegistrations from './calculateViewportRegistrations'; + +// [ { +// synchronizerId: string, +// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...] +// ]} +let STACK_IMAGE_SYNC_GROUPS_INFO = []; + +export default function toggleStackImageSync({ + toggledState, + servicesManager, + getEnabledElement, +}) { + const { + SyncGroupService, + ViewportGridService, + DisplaySetService, + CornerstoneViewportService, + } = servicesManager.services; + + if (!toggledState) { + STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => { + const { viewports, synchronizerId } = syncGroupInfo; + + viewports.forEach(({ viewportId, renderingEngineId }) => { + SyncGroupService.removeViewportFromSyncGroup( + viewportId, + renderingEngineId, + synchronizerId + ); + }); + }); + + return; + } + + STACK_IMAGE_SYNC_GROUPS_INFO = []; + + // create synchronization groups and add viewports + let { viewports } = ViewportGridService.getState(); + + // filter empty viewports + viewports = viewports.filter( + viewport => + viewport.displaySetInstanceUIDs && viewport.displaySetInstanceUIDs.length + ); + + // filter reconstructable viewports + viewports = viewports.filter(viewport => { + const { displaySetInstanceUIDs } = viewport; + + for (const displaySetInstanceUID of displaySetInstanceUIDs) { + const displaySet = DisplaySetService.getDisplaySetByUID( + displaySetInstanceUID + ); + + if (displaySet && displaySet.isReconstructable) { + return true; + } + + return false; + } + }); + + const viewportsByOrientation = viewports.reduce((acc, viewport) => { + const { viewportId, viewportType } = viewport.viewportOptions; + + if (viewportType !== 'stack') { + console.warn('Viewport is not a stack, cannot sync images yet'); + return acc; + } + + const { element } = CornerstoneViewportService.getViewportInfo(viewportId); + const { viewport: csViewport, renderingEngineId } = getEnabledElement( + element + ); + const { viewPlaneNormal } = csViewport.getCamera(); + + // Should we round here? I guess so, but not sure how much precision we need + const orientation = viewPlaneNormal.map(v => Math.round(v)).join(','); + + if (!acc[orientation]) { + acc[orientation] = []; + } + + acc[orientation].push({ viewportId, renderingEngineId }); + + return acc; + }, {}); + + // create synchronizer for each group + Object.values(viewportsByOrientation).map(viewports => { + let synchronizerId = viewports + .map(({ viewportId }) => viewportId) + .join(','); + + synchronizerId = `imageSync_${synchronizerId}`; + + calculateViewportRegistrations(viewports); + + viewports.forEach(({ viewportId, renderingEngineId }) => { + SyncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, { + type: 'stackimage', + id: synchronizerId, + source: true, + target: true, + }); + }); + + STACK_IMAGE_SYNC_GROUPS_INFO.push({ + synchronizerId, + viewports, + }); + }); +} diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 6f7eb76f9..36c6cc1da 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -92,7 +92,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); const StudyInstanceUIDs = - queryStudyInstanceUIDs.length && queryStudyInstanceUIDs || + (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || paramsStudyInstanceUIDs; const StudyInstanceUIDsAsArray = StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) diff --git a/extensions/default/src/commandsModule.js b/extensions/default/src/commandsModule.js index c66a0c788..eb576784d 100644 --- a/extensions/default/src/commandsModule.js +++ b/extensions/default/src/commandsModule.js @@ -7,7 +7,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => { HangingProtocolService, UINotificationService, ViewportGridService, - DisplaySetService + DisplaySetService, } = servicesManager.services; const actions = { @@ -30,16 +30,13 @@ const commandsModule = ({ servicesManager, commandsManager }) => { }, openDICOMTagViewer() { const { activeViewportIndex, viewports } = ViewportGridService.getState(); - const activeViewportSpecificData = - viewports[activeViewportIndex]; - const { - displaySetInstanceUIDs, - } = activeViewportSpecificData; + const activeViewportSpecificData = viewports[activeViewportIndex]; + const { displaySetInstanceUIDs } = activeViewportSpecificData; const displaySets = DisplaySetService.activeDisplaySets; const { UIModalService } = servicesManager.services; - const displaySetInstanceUID = displaySetInstanceUIDs[0] + const displaySetInstanceUID = displaySetInstanceUIDs[0]; UIModalService.show({ content: DicomTagBrowser, contentProps: { @@ -47,9 +44,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => { displaySetInstanceUID, onClose: UIModalService.hide, }, - title: 'DICOM Tag Browser' + title: 'DICOM Tag Browser', }); - } + }, }; const definitions = { @@ -75,7 +72,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => { }, openDICOMTagViewer: { commandFn: actions.openDICOMTagViewer, - } + }, }; return { diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index f556dab8a..6660e95ce 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.2.6", - "@cornerstonejs/core": "^0.21.0", - "@cornerstonejs/tools": "^0.29.2", + "@cornerstonejs/core": "^0.21.4", + "@cornerstonejs/tools": "^0.29.6", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", "dcmjs": "^0.28.3", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 309320185..c987d92f9 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -29,6 +29,7 @@ function TrackedCornerstoneViewport(props) { servicesManager, extensionManager, commandsManager, + viewportOptions, } = props; const { t } = useTranslation('TrackedViewport'); @@ -50,8 +51,7 @@ function TrackedCornerstoneViewport(props) { const [element, setElement] = useState(null); const { trackedSeries } = trackedMeasurements.context; - - const viewportId = CornerstoneViewportService.getViewportId(viewportIndex); + const viewportId = viewportOptions.viewportId; const { Modality, @@ -86,8 +86,6 @@ function TrackedCornerstoneViewport(props) { cineService.playClip(element, { framesPerSecond: validFrameRate, }); - } else { - cineService.stopClip(element); } }; diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index 1a9ae5eb7..fdbf697ed 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -91,8 +91,6 @@ function modeFactory({ modeConfiguration }) { }); }; - // Since we only have one viewport for the basic cs3d mode and it has - // only one hanging protocol, we can just use the first viewport const { unsubscribe } = ToolGroupService.subscribe( ToolGroupService.EVENTS.VIEWPORT_ADDED, () => { diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js index 56656af65..3167d7528 100644 --- a/modes/tmtv/src/initToolGroups.js +++ b/modes/tmtv/src/initToolGroups.js @@ -4,9 +4,10 @@ export const toolGroupIds = { Fusion: 'fusionToolGroup', MIP: 'mipToolGroup', default: 'default', + // MPR: 'mpr', }; -function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { +function _initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { const tools = { active: [ { @@ -43,7 +44,7 @@ function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { [toolNames.Crosshairs]: { viewportIndicators: false, autoPan: { - enabled: true, + enabled: false, panSize: 10, }, }, @@ -122,4 +123,70 @@ function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { ); } +function initMPRToolGroup(toolNames, Enums, ToolGroupService, commandsManager) { + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + ], + passive: [ + { toolName: toolNames.Length }, + { toolName: toolNames.ArrowAnnotate }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.SegmentationDisplay }, + ], + disabled: [{ toolName: toolNames.Crosshairs }], + + // enabled + // disabled + }; + + const toolsConfig = { + [toolNames.Crosshairs]: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + }, + [toolNames.ArrowAnnotate]: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }; + + ToolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig); +} + +function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { + _initToolGroups(toolNames, Enums, ToolGroupService, commandsManager); + // initMPRToolGroup(toolNames, Enums, ToolGroupService, commandsManager); +} + export default initToolGroups; diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js index d52dbb1e8..890ff0416 100644 --- a/modes/tmtv/src/toolbarButtons.js +++ b/modes/tmtv/src/toolbarButtons.js @@ -105,6 +105,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Length' @@ -125,6 +126,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Length Tool' @@ -138,6 +140,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Bidirectional Tool' @@ -151,6 +154,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Arrow Annotate' @@ -164,6 +168,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Ellipse Tool' @@ -184,10 +189,27 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], }, }, + { + id: 'MPR', + type: 'ohif.action', + props: { + type: 'toggle', + icon: 'icon-mpr', + label: 'MPR', + commands: [ + { + commandName: 'toggleMPR', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, + }, // Window Level + Presets... { id: 'WindowLevel', @@ -203,6 +225,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], 'Window Level' @@ -236,6 +259,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], }, @@ -253,6 +277,7 @@ const toolbarButtons = [ toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion, + // toolGroupIds.MPR, ]), ], }, diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 34c67f674..216bf80bd 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -122,6 +122,13 @@ class HangingProtocolService { this.displaySetMatchDetails = new Map(); } + public getActiveProtocol(): { + protocol: HangingProtocol.Protocol; + stage: number; + } { + return { protocol: this.protocol, stage: this.stage }; + } + public getDefaultProtocol(): HangingProtocol.Protocol { return this.getProtocolById('default'); } diff --git a/platform/core/src/utils/sortInstancesByPosition.ts b/platform/core/src/utils/sortInstancesByPosition.ts index 6267363a2..a441796fb 100644 --- a/platform/core/src/utils/sortInstancesByPosition.ts +++ b/platform/core/src/utils/sortInstancesByPosition.ts @@ -20,6 +20,10 @@ export default function sortInstances(instances: Array) { ImageOrientationPatient, } = instances[Math.floor(instances.length / 2)]; // this prevents getting scout image as test image + if (!referenceImagePositionPatient || !ImageOrientationPatient) { + return instances; + } + const rowCosineVec = vec3.fromValues( ImageOrientationPatient[0], ImageOrientationPatient[1], diff --git a/platform/docs/docs/platform/extensions/modules/utility.md b/platform/docs/docs/platform/extensions/modules/utility.md index d538a51d5..fbb975d1a 100644 --- a/platform/docs/docs/platform/extensions/modules/utility.md +++ b/platform/docs/docs/platform/extensions/modules/utility.md @@ -52,5 +52,4 @@ const utilityModule = extensionManager.getModuleEntry( ); const { CornerstoneViewportService } = utilityModule.exports; -const viewportId = CornerstoneViewportService.getViewportId(viewportIndex); ``` diff --git a/platform/ui/src/components/IconButton/IconButton.tsx b/platform/ui/src/components/IconButton/IconButton.tsx index 6d11d7ef2..93bff34c5 100644 --- a/platform/ui/src/components/IconButton/IconButton.tsx +++ b/platform/ui/src/components/IconButton/IconButton.tsx @@ -33,27 +33,47 @@ const variantClasses = { }, outlined: { default: - 'border bg-trasparent border-primary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + 'border border-primary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80', primary: - 'border bg-transparent border-primary-main text-primary-main hover:opacity-80 active:opacity-100 focus:opacity-80', + 'border border-primary-main text-primary-main hover:opacity-80 active:opacity-100 focus:opacity-80', secondary: - 'border bg-transparent border-secondary-light text-secondary-light hover:opacity-80 active:opacity-100 focus:opacity-80', + 'border border-secondary-light text-secondary-light hover:opacity-80 active:opacity-100 focus:opacity-80', white: - 'border bg-transparent border-white text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + 'border border-white text-white hover:opacity-80 active:opacity-100 focus:opacity-80', black: - 'border bg-black border-primary-main text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black', + 'border border-primary-main text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black', }, contained: { - default: - 'bg-primary-light text-black hover:opacity-80 active:opacity-100 focus:opacity-80', - primary: - 'bg-primary-main text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + default: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80', + primary: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80', secondary: - 'bg-secondary-light text-white hover:opacity-80 active:opacity-100 focus:opacity-80', - white: - 'bg-white text-black hover:opacity-80 active:opacity-100 focus:opacity-80', - black: - 'bg-black text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + white: 'text-black hover:opacity-80 active:opacity-100 focus:opacity-80', + black: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80', + }, +}; + +const backgroundClasses = { + text: { + default: '', + primary: '', + secondary: '', + white: '', + black: '', + }, + outlined: { + default: 'bg-transparent', + primary: 'bg-transparent', + secondary: 'bg-transparent', + white: 'bg-transparent', + black: 'bg-black', + }, + contained: { + default: 'bg-primary-light', + primary: 'bg-primary-main', + secondary: 'bg-secondary-light', + white: 'bg-white', + black: 'bg-black', }, }; @@ -90,6 +110,7 @@ const IconButton = ({ className, name, id, + bgColor, ...rest }) => { const buttonElement = useRef(null); @@ -98,6 +119,9 @@ const IconButton = ({ buttonElement.current.blur(); onClick(e); }; + + const bgColorToUse = bgColor ? bgColor : backgroundClasses[variant][color]; + return (