fix(segmentation overlay): update viewport ds list upon seg delete - OHIF-2425 (#5729)

- add segmentationExists check to getSopClassHandlerModule
- now firing SEGMENTATION_REMOVED and SEGMENTATION_REPRESENTATION_REMOVED events
- centralized segmentation removal in a listener
- when a segmentation is deleted (completely), remove it from all viewports it overlays
- when a segmentation is removed from a viewport, remove it as overlay from the viewport

---------

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
dxlin 2026-02-18 20:31:43 -05:00 committed by GitHub
parent c75cbc2a89
commit b35354120a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 174 additions and 48 deletions

View File

@ -1,5 +1,6 @@
import { utils, Types as OhifTypes } from '@ohif/core'; import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
import loadRTStruct from './loadRTStruct'; import loadRTStruct from './loadRTStruct';
@ -8,8 +9,6 @@ const { sopClassDictionary } = utils;
const sopClassUids = [sopClassDictionary.RTStructureSetStorage]; const sopClassUids = [sopClassDictionary.RTStructureSetStorage];
const cachedRTStructsSEG = new Set<string>();
const loadPromises = {}; const loadPromises = {};
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(
@ -143,23 +142,13 @@ function _load(
if ( if (
(rtDisplaySet.loading || rtDisplaySet.isLoaded) && (rtDisplaySet.loading || rtDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] && loadPromises[SOPInstanceUID] &&
cachedRTStructsSEG.has(rtDisplaySet.displaySetInstanceUID) _segmentationExists(rtDisplaySet)
) { ) {
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
rtDisplaySet.loading = true; rtDisplaySet.loading = true;
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
(evt: { rtDisplaySet: { displaySetInstanceUID: string } }) => {
if (evt.rtDisplaySet?.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
cachedRTStructsSEG.add(rtDisplaySet.displaySetInstanceUID);
unsubscribe();
}
}
);
// We don't want to fire multiple loads, so we'll wait for the first to finish // We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers. // and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise<void>(async (resolve, reject) => { loadPromises[SOPInstanceUID] = new Promise<void>(async (resolve, reject) => {
@ -219,6 +208,10 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence; return ReferencedSeriesSequence;
} }
function _segmentationExists(segDisplaySet) {
return !!cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params; const { servicesManager, extensionManager } = params;

View File

@ -168,7 +168,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId); segmentationService.removeRepresentationsFromViewport(viewportId);
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
toolGroupService.destroyToolGroup(toolGroupId); toolGroupService.destroyToolGroup(toolGroupId);
}; };

View File

@ -1735,12 +1735,14 @@ function commandsModule({
* Removes a segmentation from the viewport * Removes a segmentation from the viewport
* @param props.segmentationId - The ID of the segmentation to remove * @param props.segmentationId - The ID of the segmentation to remove
*/ */
removeSegmentationFromViewportCommand: ({ segmentationId }) => { removeSegmentationFromViewportCommand: ({ segmentationId: displaySetInstanceUID }) => {
const { segmentationService, viewportGridService } = servicesManager.services; const { viewportGridService } = servicesManager.services;
segmentationService.removeSegmentationRepresentations( const viewportId = viewportGridService.getActiveViewportId();
viewportGridService.getActiveViewportId(),
{ segmentationId } commandsManager.runCommand('removeDisplaySetLayer', {
); viewportId,
displaySetInstanceUID,
});
}, },
/** /**

View File

@ -2567,12 +2567,12 @@ describe('SegmentationService', () => {
describe('clearSegmentationRepresentations', () => { describe('clearSegmentationRepresentations', () => {
it('should clear the segmentation representations', () => { it('should clear the segmentation representations', () => {
const viewportId = 'viewportId'; const viewportId = 'viewportId';
jest.spyOn(service, 'removeSegmentationRepresentations').mockReturnValue(undefined); jest.spyOn(service, 'removeRepresentationsFromViewport').mockReturnValue(undefined);
service.clearSegmentationRepresentations(viewportId); service.clearSegmentationRepresentations(viewportId);
expect(service.removeSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(service.removeRepresentationsFromViewport).toHaveBeenCalledTimes(1);
expect(service.removeSegmentationRepresentations).toHaveBeenCalledWith(viewportId); expect(service.removeRepresentationsFromViewport).toHaveBeenCalledWith(viewportId);
}); });
}); });
@ -2600,7 +2600,7 @@ describe('SegmentationService', () => {
}); });
}); });
describe('removeSegmentationRepresentations', () => { describe('removeRepresentationsFromViewport', () => {
it('should remove the segmentation representations', () => { it('should remove the segmentation representations', () => {
const viewportId = 'viewportId'; const viewportId = 'viewportId';
const specifier = { const specifier = {
@ -2609,7 +2609,7 @@ describe('SegmentationService', () => {
}; };
jest.spyOn(cstSegmentation, 'removeSegmentationRepresentations').mockReturnValue(undefined); jest.spyOn(cstSegmentation, 'removeSegmentationRepresentations').mockReturnValue(undefined);
service.removeSegmentationRepresentations(viewportId, specifier); service.removeRepresentationsFromViewport(viewportId, specifier);
expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledTimes(1);
expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledWith( expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledWith(

View File

@ -247,7 +247,7 @@ class SegmentationService extends PubSubService {
eventTarget.removeEventListener( eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REMOVED, csToolsEnums.Events.SEGMENTATION_REMOVED,
this._onSegmentationModifiedFromSource this._onSegmentationRemovedFromSource
); );
eventTarget.removeEventListener( eventTarget.removeEventListener(
@ -267,7 +267,7 @@ class SegmentationService extends PubSubService {
eventTarget.removeEventListener( eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED,
this._onSegmentationRepresentationModifiedFromSource this._onSegmentationRepresentationRemovedFromSource
); );
eventTarget.removeEventListener( eventTarget.removeEventListener(
@ -1284,14 +1284,14 @@ class SegmentationService extends PubSubService {
/** /**
* Clears segmentation representations from the viewport. * Clears segmentation representations from the viewport.
* Unlike removeSegmentationRepresentations, this doesn't update * Unlike removeRepresentationsFromViewport, this doesn't update
* removed display set and representation maps. * removed display set and representation maps.
* We track removed segmentations manually to avoid re-adding them * We track removed segmentations manually to avoid re-adding them
* when the display set is added again. * when the display set is added again.
* @param viewportId - The viewport ID to clear segmentation representations from. * @param viewportId - The viewport ID to clear segmentation representations from.
*/ */
public clearSegmentationRepresentations(viewportId: string): void { public clearSegmentationRepresentations(viewportId: string): void {
this.removeSegmentationRepresentations(viewportId); this.removeRepresentationsFromViewport(viewportId);
} }
/** /**
@ -1307,7 +1307,7 @@ class SegmentationService extends PubSubService {
} }
/** /**
* It removes the segmentation representations from the viewport. * Removes segmentation representations from the viewport.
* @param viewportId - The viewport id to remove the segmentation representations from. * @param viewportId - The viewport id to remove the segmentation representations from.
* @param specifier - The specifier to remove the segmentation representations. * @param specifier - The specifier to remove the segmentation representations.
* *
@ -1317,7 +1317,7 @@ class SegmentationService extends PubSubService {
* If a type specifier is provided, only the segmentation representation with the specified type are removed. * If a type specifier is provided, only the segmentation representation with the specified type are removed.
* If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed. * If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed.
*/ */
public removeSegmentationRepresentations( public removeRepresentationsFromViewport(
viewportId: string, viewportId: string,
specifier: { specifier: {
segmentationId?: string; segmentationId?: string;
@ -1861,7 +1861,7 @@ class SegmentationService extends PubSubService {
eventTarget.addEventListener( eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REMOVED, csToolsEnums.Events.SEGMENTATION_REMOVED,
this._onSegmentationModifiedFromSource this._onSegmentationRemovedFromSource
); );
eventTarget.addEventListener( eventTarget.addEventListener(
@ -1881,7 +1881,7 @@ class SegmentationService extends PubSubService {
eventTarget.addEventListener( eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED,
this._onSegmentationRepresentationModifiedFromSource this._onSegmentationRepresentationRemovedFromSource
); );
eventTarget.addEventListener( eventTarget.addEventListener(
@ -2120,6 +2120,14 @@ class SegmentationService extends PubSubService {
}); });
}; };
private _onSegmentationRepresentationRemovedFromSource = evt => {
const { segmentationId, viewportId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_REMOVED, {
segmentationId,
viewportId,
});
};
private _onSegmentationModifiedFromSource = ( private _onSegmentationModifiedFromSource = (
evt: cstTypes.EventTypes.SegmentationModifiedEventType evt: cstTypes.EventTypes.SegmentationModifiedEventType
) => { ) => {
@ -2140,6 +2148,16 @@ class SegmentationService extends PubSubService {
}); });
}; };
private _onSegmentationRemovedFromSource = (
evt: cstTypes.EventTypes.SegmentationRemovedEventType
) => {
const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
segmentationId,
});
};
private _onAnnotationCutMergeProcessCompletedFromSource = evt => { private _onAnnotationCutMergeProcessCompletedFromSource = evt => {
const { segmentationId } = evt.detail; const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, { this._broadcastEvent(this.EVENTS.SEGMENTATION_ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {

View File

@ -15,6 +15,7 @@ describe('setUpSegmentationEventHandlers', () => {
const mockSegmentationService = { const mockSegmentationService = {
EVENTS: { EVENTS: {
SEGMENTATION_ADDED: 'SEGMENTATION_ADDED', SEGMENTATION_ADDED: 'SEGMENTATION_ADDED',
SEGMENTATION_REMOVED: 'SEGMENTATION_REMOVED',
}, },
subscribe: jest.fn(), subscribe: jest.fn(),
getSegmentation: jest.fn(), getSegmentation: jest.fn(),
@ -40,6 +41,7 @@ describe('setUpSegmentationEventHandlers', () => {
const mockUnsubscribeDataModified = jest.fn(); const mockUnsubscribeDataModified = jest.fn();
const mockUnsubscribeModified = jest.fn(); const mockUnsubscribeModified = jest.fn();
const mockUnsubscribeCreated = jest.fn(); const mockUnsubscribeCreated = jest.fn();
const mockUnsubscribeRemoved = jest.fn();
const mockUnsubscribeSelectedSegmentationsForViewportEvents = [jest.fn(), jest.fn()]; const mockUnsubscribeSelectedSegmentationsForViewportEvents = [jest.fn(), jest.fn()];
const defaultParameters = { const defaultParameters = {
@ -59,8 +61,14 @@ describe('setUpSegmentationEventHandlers', () => {
unsubscribeSelectedSegmentationsForViewportEvents: unsubscribeSelectedSegmentationsForViewportEvents:
mockUnsubscribeSelectedSegmentationsForViewportEvents, mockUnsubscribeSelectedSegmentationsForViewportEvents,
}); });
mockSegmentationService.subscribe.mockReturnValue({ mockSegmentationService.subscribe.mockImplementation((eventName: string) => {
unsubscribe: mockUnsubscribeCreated, if (eventName === mockSegmentationService.EVENTS.SEGMENTATION_ADDED) {
return { unsubscribe: mockUnsubscribeCreated };
}
if (eventName === mockSegmentationService.EVENTS.SEGMENTATION_REMOVED) {
return { unsubscribe: mockUnsubscribeRemoved };
}
return { unsubscribe: jest.fn() };
}); });
}); });
@ -99,6 +107,7 @@ describe('setUpSegmentationEventHandlers', () => {
mockUnsubscribeDataModified, mockUnsubscribeDataModified,
mockUnsubscribeModified, mockUnsubscribeModified,
mockUnsubscribeCreated, mockUnsubscribeCreated,
mockUnsubscribeRemoved,
...mockUnsubscribeSelectedSegmentationsForViewportEvents, ...mockUnsubscribeSelectedSegmentationsForViewportEvents,
], ],
}); });
@ -358,5 +367,6 @@ describe('setUpSegmentationEventHandlers', () => {
expect(mockUnsubscribeDataModified).toHaveBeenCalled(); expect(mockUnsubscribeDataModified).toHaveBeenCalled();
expect(mockUnsubscribeModified).toHaveBeenCalled(); expect(mockUnsubscribeModified).toHaveBeenCalled();
expect(mockUnsubscribeCreated).toHaveBeenCalled(); expect(mockUnsubscribeCreated).toHaveBeenCalled();
expect(mockUnsubscribeRemoved).toHaveBeenCalled();
}); });
}); });

View File

@ -7,7 +7,8 @@ import {
} from './segmentationHandlers'; } from './segmentationHandlers';
export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManager }) => { export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManager }) => {
const { segmentationService, customizationService, displaySetService } = servicesManager.services; const { segmentationService, customizationService, displaySetService, viewportGridService } =
servicesManager.services;
const { unsubscribe: unsubscribeSegmentationDataModifiedHandler } = const { unsubscribe: unsubscribeSegmentationDataModifiedHandler } =
setupSegmentationDataModifiedHandler({ setupSegmentationDataModifiedHandler({
@ -57,6 +58,36 @@ export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManage
} }
); );
const { unsubscribe: unsubscribeSegmentationRemoved } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
({ segmentationId }) => {
const displaySet = displaySetService.getDisplaySetByUID(segmentationId);
// Remove the display set layer from all viewports that have it
if (displaySet) {
const state = viewportGridService.getState();
const viewports = state.viewports;
// Find all viewports that contain this segmentation's display set as a layer
for (const [viewportId, viewport] of viewports.entries()) {
const displaySetInstanceUIDs = viewport.displaySetInstanceUIDs || [];
if (displaySetInstanceUIDs.includes(segmentationId)) {
// Remove the display set layer from this viewport
commandsManager.runCommand('removeDisplaySetLayer', {
viewportId,
displaySetInstanceUID: segmentationId,
});
}
}
// Delete the display set from the service if it was made in client
if (displaySet.madeInClient) {
displaySetService.deleteDisplaySet(segmentationId);
}
}
}
);
const { unsubscribeSelectedSegmentationsForViewportEvents } = const { unsubscribeSelectedSegmentationsForViewportEvents } =
setUpSelectedSegmentationsForViewportHandler({ setUpSelectedSegmentationsForViewportHandler({
segmentationService, segmentationService,
@ -66,6 +97,7 @@ export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManage
unsubscribeSegmentationDataModifiedHandler, unsubscribeSegmentationDataModifiedHandler,
unsubscribeSegmentationModifiedHandler, unsubscribeSegmentationModifiedHandler,
unsubscribeSegmentationCreated, unsubscribeSegmentationCreated,
unsubscribeSegmentationRemoved,
...unsubscribeSelectedSegmentationsForViewportEvents, ...unsubscribeSelectedSegmentationsForViewportEvents,
]; ];

View File

@ -66,8 +66,8 @@ const commandsModule = ({
*/ */
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => { addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
if (!viewportId) { if (!viewportId) {
const { activeViewportId } = servicesManager.services.viewportGridService.getState(); const { activeViewportId } = servicesManager.services.viewportGridService.getState();
viewportId = activeViewportId; viewportId = activeViewportId;
} }
if (!viewportId || !displaySetInstanceUID) { if (!viewportId || !displaySetInstanceUID) {
@ -151,6 +151,19 @@ const commandsModule = ({
return; return;
} }
// Check if it's a segmentation and handle accordingly.
// Note that for the sake of hydrated segmentations, we remove the
// segmentation before checking if the display set is indeed in the viewport.
// This is because hydrated segmentations are not in the viewport per se
// {i.e. they are not layered) but are simply referenced by the display
// set in the viewport.
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
if (isSegmentation) {
segmentationService.removeRepresentationsFromViewport(viewportId, {
segmentationId: displaySetInstanceUID,
});
}
// Get current display sets for the viewport // Get current display sets for the viewport
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
@ -159,14 +172,6 @@ const commandsModule = ({
return; return;
} }
// Check if it's a segmentation and handle accordingly
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
if (isSegmentation) {
segmentationService.removeSegmentationRepresentations(viewportId, {
segmentationId: displaySetInstanceUID,
});
}
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId, viewportId,
displaySetInstanceUID displaySetInstanceUID

View File

@ -1,11 +1,11 @@
--- ---
sidebar_position: 1 sidebar_position: 1
sidebar_label: 3.11 -> 3.12 beta sidebar_label: 3.11 -> 3.12
--- ---
# Migration Guide # Migration Guide
This guide provides information about migrating from OHIF version 3.11 to version 3.12 beta This guide provides information about migrating from OHIF version 3.11 to version 3.12
## Optional: Migrate modes to extend `modes/basic` ## Optional: Migrate modes to extend `modes/basic`

View File

@ -0,0 +1,15 @@
---
id: index
sidebar_position: 1
sidebar_label: 3.12 -> 3.13
title: 3.12 to 3.13 Migration Guide
---
import DocCardList from '@theme/DocCardList';
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
# 3.12 to 3.13 Migration Guide
This guide covers changes when upgrading from OHIF version 3.12 to version 3.13.
<DocCardList items={useCurrentSidebarCategory().items.filter(item => item.docId !== 'migration-guide/3p12-to-3p13/index')} />

View File

@ -0,0 +1,51 @@
---
sidebar_position: 2
sidebar_label: SegmentationService
title: SegmentationService API change
---
# SegmentationService method rename
The `SegmentationService` method for removing segmentation representations from a viewport has been renamed to better reflect its behavior.
## Change
| 3.12 (old) | 3.13 (new) |
|------------------------------------|-----------------------------------|
| `removeSegmentationRepresentations` | `removeRepresentationsFromViewport` |
The signature is unchanged:
```ts
removeRepresentationsFromViewport(
viewportId: string,
specifier?: {
segmentationId?: string;
type?: SegmentationRepresentations;
}
): void
```
## Migration
Replace calls to `removeSegmentationRepresentations` with `removeRepresentationsFromViewport`.
**Before (3.12):**
```ts
segmentationService.removeSegmentationRepresentations(viewportId);
// or with a specifier:
segmentationService.removeSegmentationRepresentations(viewportId, { segmentationId });
```
**After (3.13):**
```ts
segmentationService.removeRepresentationsFromViewport(viewportId);
// or with a specifier:
segmentationService.removeRepresentationsFromViewport(viewportId, { segmentationId });
```
## Reason
The new name makes it clear that the method removes representations from a specific viewport, rather than removing segmentations globally.