diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index d702d7b7b2e..0c5c3e523dc 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -40,6 +40,7 @@ import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport'; import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { DisplaySet } from 'platform/core/src/types'; const { DefaultHistoryMemo } = csUtils.HistoryMemo; const toggleSyncFunctions = { @@ -117,9 +118,13 @@ function commandsModule({ hangingProtocolService, syncGroupService, segmentationService, + userAuthenticationService, displaySetService, } = servicesManager.services as AppTypes.Services; + let _protocolViewportDataInitSubscription: { unsubscribe: () => void } | null = null; + let _protocolViewportDataChangedSubscription: { unsubscribe: () => void } | null = null; + function _getActiveViewportEnabledElement() { return getActiveViewportEnabledElement(viewportGridService); } @@ -145,7 +150,99 @@ function commandsModule({ }; } + /** + * Retrieves derived segmentations (SEG/RTSTRUCT) that are not yet hydrated + * for the given display set UID. + */ + function getDerivedData(modalities: string[], displaySetUID: string): DisplaySet[] { + const currentDisplaySet = displaySetService.getDisplaySetByUID(displaySetUID); + if (!currentDisplaySet) { + return []; + } + + const displaySetCache = displaySetService.getDisplaySetCache(); + const allDisplaySets = Array.from(displaySetCache.values()); + + return allDisplaySets.filter((ds): ds is DisplaySet => { + const isValidModality = modalities.includes(ds.Modality); + if (!isValidModality) { + return false; + } + + /** Check if this derived display set references the current display set */ + const referencesDisplaySet = + ds.referencedDisplaySetInstanceUID === displaySetUID || + ds.SeriesInstanceUID === currentDisplaySet.SeriesInstanceUID; + + return referencesDisplaySet; + }); + } + + const loadDerivedDisplaySetsForActiveViewport = async ( + modalities: string[], + onLoadComplete: (displaySet: any, activeViewportId: string) => Promise | void + ): Promise => { + const activeViewportId = viewportGridService.getActiveViewportId(); + if (!activeViewportId) { + console.warn('No active viewport found'); + return false; + } + + const displaySetInstanceUIDs = + viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId); + if (!displaySetInstanceUIDs?.length) { + console.warn('No display sets found for active viewport'); + return false; + } + + const primaryDisplaySetUID = displaySetInstanceUIDs[0]; + const derivedDisplaySets = getDerivedData(modalities, primaryDisplaySetUID); + if (!derivedDisplaySets.length) { + console.warn('No derived data found for active viewport!'); + return false; + } + + const headers = userAuthenticationService.getAuthorizationHeader(); + + const loadPromises = derivedDisplaySets.map(async displaySet => { + try { + await displaySet.load({ headers }); + await onLoadComplete(displaySet, activeViewportId); + } catch (error) { + console.error(`Failed to load segmentation ${displaySet.displaySetInstanceUID}:`, error); + } + }); + + await Promise.all(loadPromises); + return true; + }; + const actions = { + loadSegmentationsForActiveViewport: async () => { + console.info('Loading segmentations for active viewport...'); + + const loaded = await loadDerivedDisplaySetsForActiveViewport( + ['SEG', 'RTSTRUCT'], + async (displaySet, activeViewportId) => { + const representationType = + displaySet.Modality === 'SEG' + ? Enums.SegmentationRepresentations.Labelmap + : Enums.SegmentationRepresentations.Contour; + + segmentationService.addSegmentationRepresentation(activeViewportId, { + segmentationId: displaySet.displaySetInstanceUID, + type: representationType, + }); + } + ); + + if (!loaded) { + console.warn('No derived segmentations found for active viewport'); + return; + } + + console.info('Segmentations loaded for active viewport.'); + }, jumpToMeasurementViewport: ({ annotationUID, measurement }) => { cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true); const { metadata } = measurement; @@ -1306,16 +1403,62 @@ function commandsModule({ const command = protocol.callbacks.onViewportDataInitialized; const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1; let numPanesWithData = 0; - const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => { + + actions.detachProtocolViewportDataListener?.(); + + const subscription = cornerstoneViewportService.subscribe(EVENT, () => { numPanesWithData++; if (numPanesWithData === numPanes) { - commandsManager.run(...command); + commandsManager.run(command); // Unsubscribe from the event - unsubscribe(EVENT); + subscription.unsubscribe(); + _protocolViewportDataInitSubscription = null; } }); + + _protocolViewportDataInitSubscription = subscription; + }, + + detachProtocolViewportDataListener: () => { + if (_protocolViewportDataInitSubscription) { + _protocolViewportDataInitSubscription.unsubscribe(); + _protocolViewportDataInitSubscription = null; + } + }, + + attachProtocolViewportDataChangedListener: ({ protocol, stageIndex }) => { + const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; + const command = protocol.callbacks.onViewportDataChanged; + + actions.detachProtocolViewportDataChangedListener?.(); + + const subscription = cornerstoneViewportService.subscribe( + EVENT, + (evt: { viewportId?: string; viewportData?: unknown }) => { + const viewportId = evt?.viewportId; + if (!viewportId) { + return; + } + + commandsManager.run(command, { + viewportId, + viewportData: evt.viewportData, + protocol, + stageIndex, + }); + } + ); + + _protocolViewportDataChangedSubscription = subscription; + }, + + detachProtocolViewportDataChangedListener: () => { + if (_protocolViewportDataChangedSubscription) { + _protocolViewportDataChangedSubscription.unsubscribe(); + _protocolViewportDataChangedSubscription = null; + } }, setViewportPreset: ({ viewportId, preset }) => { @@ -2211,6 +2354,9 @@ function commandsModule({ }; const definitions = { + loadSegmentationsForActiveViewport: { + commandFn: actions.loadSegmentationsForActiveViewport, + }, // The command here is to show the viewer context menu, as being the // context menu showCornerstoneContextMenu: { @@ -2370,6 +2516,15 @@ function commandsModule({ attachProtocolViewportDataListener: { commandFn: actions.attachProtocolViewportDataListener, }, + detachProtocolViewportDataListener: { + commandFn: actions.detachProtocolViewportDataListener, + }, + attachProtocolViewportDataChangedListener: { + commandFn: actions.attachProtocolViewportDataChangedListener, + }, + detachProtocolViewportDataChangedListener: { + commandFn: actions.detachProtocolViewportDataChangedListener, + }, setViewportPreset: { commandFn: actions.setViewportPreset, }, diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index 4ccd6e271a3..132eb18833f 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -14,6 +14,9 @@ const defaultProtocol = { modifiedDate: '2023-04-01', availableTo: {}, editableBy: {}, + callbacks: { + onViewportDataChanged: ['loadSegmentationsForActiveViewport'], + }, protocolMatchingRules: [], toolGroupIds: ['default'], // -1 would be used to indicate active only, whereas other values are diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index fd85749acf1..474ee9f51ba 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -430,7 +430,10 @@ export default class HangingProtocolService extends PubSubService { this.customAttributeRetrievalCallbacks ); - // Resets the full protocol status here. + this._commandsManager.run('detachProtocolViewportDataListener'); + this._commandsManager.run('detachProtocolViewportDataChangedListener'); + + // Resets the full protocol status here this.protocol = null; if (protocolId && typeof protocolId === 'string') { @@ -1032,6 +1035,10 @@ export default class HangingProtocolService extends PubSubService { try { if (!this.protocol || this.protocol.id !== protocol.id) { + + this._commandsManager.run('detachProtocolViewportDataListener'); + this._commandsManager.run('detachProtocolViewportDataChangedListener'); + this.stageIndex = options?.stageIndex || 0; //Reset load performed to false to re-fire loading strategy at new study opening this.customImageLoadPerformed = false; @@ -1212,8 +1219,18 @@ export default class HangingProtocolService extends PubSubService { const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps; + this._commandsManager.run('detachProtocolViewportDataListener'); + this._commandsManager.run('detachProtocolViewportDataChangedListener'); + if (this.protocol?.callbacks?.onViewportDataInitialized) { - this._commandsManager.runCommand('attachProtocolViewportDataListener', { + this._commandsManager.run('attachProtocolViewportDataListener', { + protocol: this.protocol, + stageIndex: this.stageIndex, + }); + } + + if (this.protocol?.callbacks?.onViewportDataChanged) { + this._commandsManager.run('attachProtocolViewportDataChangedListener', { protocol: this.protocol, stageIndex: this.stageIndex, }); diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts index 7eee952ec94..c5ec90038a7 100644 --- a/platform/core/src/types/DisplaySet.ts +++ b/platform/core/src/types/DisplaySet.ts @@ -51,6 +51,13 @@ export type DisplaySet = { isHydrated?: boolean; isRehydratable?: boolean; + + /** + * Loads the display set. + * @param headers - The headers to use for the request. + * @returns A promise that resolves when the display set is loaded. + */ + load: ({ headers }: { headers?: unknown }) => Promise; }; export type DisplaySetSeriesMetadataInvalidatedEvent = { diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index 189fe0dcc08..c3e825e0bf9 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -293,6 +293,15 @@ export type ProtocolNotifications = { // and all viewport data includes a designated display set. This command // will run on every stage's initial layout. onViewportDataInitialized?: Command[]; + // This set of commands is executed whenever viewport data has changed for a + // given viewport. + // + // Commands will receive an options object containing: + // - viewportId + // - viewportData + // - protocol + // - stageIndex + onViewportDataChanged?: Command[]; // This set of commands is executed before the stage change is applied onStageChange?: Command[]; }; diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index 1fb26c3a6cb..3fb3f8a9ab1 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -600,6 +600,17 @@ The `onLayoutChange` callback is executed before the layout change is started. Y The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start. +### `onViewportDataChanged` + +The `onViewportDataChanged` callback is executed whenever a viewport's data changes (for example, when a different display set is assigned to a viewport). This is useful for reacting to runtime changes like drag/drop swaps, layout tools that reassign data, etc. + +Commands in `onViewportDataChanged` will receive an options object including: + +- `viewportId` +- `viewportData` +- `protocol` +- `stageIndex` + Here is an example of how you can add these callbacks to your hanging protocol configuration: ```javascript @@ -620,7 +631,11 @@ const protocol = { onViewportDataInitialized: [ // Array of commands or actions to execute on viewport data initialization ], + onViewportDataChanged: [ + // Array of commands or actions to execute when viewport data changes + ], }, // protocolMatchingRules // the rest }; +``` diff --git a/platform/docs/versioned_docs/version-3.10/platform/extensions/modules/hpModule.md b/platform/docs/versioned_docs/version-3.10/platform/extensions/modules/hpModule.md index 8920ccc255a..99dbe1f0c34 100644 --- a/platform/docs/versioned_docs/version-3.10/platform/extensions/modules/hpModule.md +++ b/platform/docs/versioned_docs/version-3.10/platform/extensions/modules/hpModule.md @@ -622,3 +622,4 @@ const protocol = { // protocolMatchingRules // the rest }; +```