fix(viewport-sync): remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync (#3849)

This commit is contained in:
Alireza 2024-01-22 09:44:35 -05:00 committed by GitHub
parent 2460d28afc
commit e4a116b074
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1110 additions and 1031 deletions

View File

@ -168,7 +168,7 @@ function OHIFCornerstoneRTViewport(props) {
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => {
// I'm not sure what is this, since in RT we support Overlapping segmnets
// I'm not sure what is this, since in RT we support Overlapping segments
// via contours
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,

View File

@ -175,6 +175,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
}
@ -197,13 +204,18 @@ const OHIFCornerstoneViewport = React.memo(props => {
return;
}
cleanUpServices(viewportInfo);
cornerstoneViewportService.storePresentation({ viewportId });
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
@ -547,6 +559,58 @@ function _jumpToMeasurement(
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
const { id, sourceViewports, targetViewports } = synchronizerObj;
const synchronizer = syncGroupService.getSynchronizer(id);
if (!synchronizer) {
return;
}
const sourceViewportInfo = sourceViewports.find(
sourceViewport => sourceViewport.viewportId === viewportId
);
const targetViewportInfo = targetViewports.find(
targetViewport => targetViewport.viewportId === viewportId
);
const isSourceViewportInSynchronizer = synchronizer
.getSourceViewports()
.find(sourceViewport => sourceViewport.viewportId === viewportId);
const isTargetViewportInSynchronizer = synchronizer
.getTargetViewports()
.find(targetViewport => targetViewport.viewportId === viewportId);
// if the viewport was previously a source viewport, add it again
if (sourceViewportInfo && !isSourceViewportInSynchronizer) {
synchronizer.addSource({
viewportId: sourceViewportInfo.viewportId,
renderingEngineId: sourceViewportInfo.renderingEngineId,
});
}
// if the viewport was previously a target viewport, add it again
if (targetViewportInfo && !isTargetViewportInSynchronizer) {
synchronizer.addTarget({
viewportId: targetViewportInfo.viewportId,
renderingEngineId: targetViewportInfo.renderingEngineId,
});
}
});
}
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';

View File

@ -17,7 +17,7 @@ import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
@ -566,8 +566,8 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
},
toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({
toggleImageSliceSync: ({ toggledState }) => {
toggleImageSliceSync({
servicesManager,
toggledState,
});
@ -726,8 +726,8 @@ function commandsModule({
setViewportColormap: {
commandFn: actions.setViewportColormap,
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
toggleImageSliceSync: {
commandFn: actions.toggleImageSliceSync,
},
setSourceViewportForReferenceLinesTool: {
commandFn: actions.setSourceViewportForReferenceLinesTool,

View File

@ -10,6 +10,7 @@ import {
metaData,
volumeLoader,
imageLoadPoolManager,
getEnabledElement,
Settings,
utilities as csUtilities,
Enums as csEnums,
@ -97,6 +98,7 @@ export default async function init({
toolbarService,
viewportGridService,
stateSyncService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
window.services = servicesManager.services;
@ -124,6 +126,9 @@ export default async function init({
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
// Stores synchronizers state to be restored
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
// Stores a map from `positionPresentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same position
stateSyncService.register('positionPresentationStore', {

View File

@ -45,7 +45,7 @@ export default class SyncGroupService {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
};
constructor(serviceManager: ServicesManager) {
@ -74,6 +74,10 @@ export default class SyncGroupService {
this.synchronizerCreators[type.toLowerCase()] = creator;
}
public getSynchronizer(id: string): Synchronizer | void {
return SynchronizerManager.getSynchronizer(id);
}
protected _getOrCreateSynchronizer(
type: string,
id: string,
@ -121,6 +125,17 @@ export default class SyncGroupService {
SynchronizerManager.destroy();
}
public getSynchronizersForViewport(
viewportId: string,
renderingEngineId: string
): Synchronizer[] {
return SynchronizerManager.getAllSynchronizers().filter(
s =>
s.hasSourceViewport(renderingEngineId, viewportId) ||
s.hasTargetViewport(renderingEngineId, viewportId)
);
}
public removeViewportFromSyncGroup(
viewportId: string,
renderingEngineId: string,

View File

@ -178,7 +178,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
public storePresentation({ viewportId }) {
const stateSyncService = this.servicesManager.services.stateSyncService;
const { stateSyncService, syncGroupService } = this.servicesManager.services;
let presentation;
try {
presentation = this.getPresentation(viewportId);
@ -189,7 +189,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
if (!presentation || !presentation.presentationIds) {
return;
}
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const synchronizers = syncGroupService.getSynchronizersForViewport(
viewportId,
this.renderingEngine.id
);
const { lutPresentationStore, positionPresentationStore, synchronizersStore } =
stateSyncService.getState();
const { presentationIds } = presentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
@ -205,6 +212,22 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
[positionPresentationId]: presentation,
};
}
if (synchronizers?.length) {
storeState.synchronizersStore = {
...synchronizersStore,
[viewportId]: synchronizers.map(synchronizer => {
const sourceViewports = synchronizer.getSourceViewports();
const targetViewports = synchronizer.getTargetViewports();
return {
id: synchronizer.id,
sourceViewports: [...sourceViewports],
targetViewports: [...targetViewports],
};
}),
};
}
stateSyncService.store(storeState);
}

View File

@ -1,12 +1,12 @@
const STACK_SYNC_NAME = 'stackImageSync';
const IMAGE_SLICE_SYNC_NAME = 'IMAGE_SLICE_SYNC';
export default function toggleStackImageSync({
export default function toggleImageSliceSync({
toggledState,
servicesManager,
viewports: providedViewports,
}) {
if (!toggledState) {
return disableSync(STACK_SYNC_NAME, servicesManager);
return disableSync(IMAGE_SLICE_SYNC_NAME, servicesManager);
}
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
@ -24,7 +24,7 @@ export default function toggleStackImageSync({
}
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
type: 'stackimage',
id: STACK_SYNC_NAME,
id: IMAGE_SLICE_SYNC_NAME,
source: true,
target: true,
});

View File

@ -79,19 +79,19 @@ const moreTools = [
'Flip Horizontally'
),
ToolbarService._createToggleButton(
'StackImageSync',
'ImageSliceSync',
'link',
'Stack Image Sync',
'Image Slice Sync',
[
{
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},

View File

@ -83,19 +83,19 @@ const moreTools = [
'Flip Horizontal'
),
ToolbarService._createToggleButton(
'StackImageSync',
'ImageSliceSync',
'link',
'Stack Image Sync',
'Image Slice Sync',
[
{
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},

View File

@ -57,19 +57,19 @@ const moreToolsMpr = [
'Reset'
),
ToolbarService._createToggleButton(
'StackImageSync',
'ImageSliceSync',
'link',
'Stack Image Sync',
'Image Slice Sync',
[
{
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},

View File

@ -261,9 +261,9 @@ const toolbarButtons = [
],
'Flip Horizontal'
),
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [
_createToggleButton('ImageSliceSync', 'link', 'Stack Image Sync', [
{
commandName: 'toggleStackImageSync',
commandName: 'toggleImageSliceSync',
commandOptions: {},
context: 'CORNERSTONE',
},

View File

@ -427,7 +427,7 @@ describe('OHIF Cornerstone Toolbar', () => {
it('checks if stack sync is preserved on new display set and uses FOR', () => {
// Active stack image sync and reference lines
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="StackImageSync"]').click();
cy.get('[data-cy="ImageSliceSync"]').click();
// Add reference lines as that sometimes throws an exception
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="ReferenceLines"]').click();

View File

@ -65,9 +65,9 @@ describe('OHIF MPR', () => {
const viewports = cornerstone.getRenderingEngines()[0].getViewports();
// The stack viewport still exists after the changes to viewportId and inde
const imageData1 = viewports[1].getImageData();
const imageData2 = viewports[2].getImageData();
const imageData3 = viewports[3].getImageData();
const imageData1 = viewports[0].getImageData();
const imageData2 = viewports[1].getImageData();
const imageData3 = viewports[2].getImageData();
// for some reason map doesn't work here
cy.wrap(imageData1).should('not.be', undefined);

1970
yarn.lock

File diff suppressed because it is too large Load Diff