fix: prevent annotation from appearing in active viewport when switching series with different Frame of Reference UID (#5630)

* fix: prevent annotation from appearing in active viewport when switching series with different Frame of Reference UID

* refactor: extract viewport update strategies

* Update version and use nearest orientation viewport

---------

Co-authored-by: trenser-belbin <belbin.kunjumon@prenuvo.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-12-18 12:02:52 -05:00
1 parent 0af4c2ae47
commit 658994c854
13 files changed
+322 -261

No files matched your search

@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.14.4",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/adapters": "4.14.5",
"@cornerstonejs/core": "4.14.5",
"@kitware/vtk.js": "34.15.1",
"react-color": "2.19.3"
}
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.14.4",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/adapters": "4.14.5",
"@cornerstonejs/core": "4.14.5",
"@kitware/vtk.js": "34.15.1",
"react-color": "2.19.3"
}
+3 -3
View File
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.14.4",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/tools": "4.14.4",
"@cornerstonejs/adapters": "4.14.5",
"@cornerstonejs/core": "4.14.5",
"@cornerstonejs/tools": "4.14.5",
"classnames": "2.5.1"
}
}
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/tools": "4.14.4",
"@cornerstonejs/core": "4.14.5",
"@cornerstonejs/tools": "4.14.5",
"classnames": "2.5.1"
}
}
+7 -7
View File
@@ -40,7 +40,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.14.4",
"@cornerstonejs/dicom-image-loader": "4.14.5",
"@ohif/core": "3.12.0-beta.109",
"@ohif/ui": "3.12.0-beta.109",
"dcmjs": "0.45.0",
@@ -53,12 +53,12 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.14.4",
"@cornerstonejs/ai": "4.14.4",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/labelmap-interpolation": "4.14.4",
"@cornerstonejs/polymorphic-segmentation": "4.14.4",
"@cornerstonejs/tools": "4.14.4",
"@cornerstonejs/adapters": "4.14.5",
"@cornerstonejs/ai": "4.14.5",
"@cornerstonejs/core": "4.14.5",
"@cornerstonejs/labelmap-interpolation": "4.14.5",
"@cornerstonejs/polymorphic-segmentation": "4.14.5",
"@cornerstonejs/tools": "4.14.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "34.15.1",
"html2canvas": "1.4.1",
@@ -37,6 +37,7 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import getClosestOrientationFromIOP from '../../utils/isReferenceViewable';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -634,67 +635,118 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
/**
* Figures out which viewport to update when the viewport type needs to change.
* This may not be the active viewport if there is already a viewport showing
* the display set, but in the wrong orientation.
*
* The viewport will need to update the viewport type and/or display set to
* display the resulting data.
*
* The first choice will be a viewport already showing the correct display set,
* but showing it as a stack.
*
* Second choice is to see if there is a viewport already showing the right
* orientation for the image, but the wrong display set. This fixes the
* case where the user is in MPR and a viewport other than active should be
* the one to change to display the iamge.
*
* Final choice is to use the provide activeViewportId. This will cover
* changes to/from video and wsi viewports and other cases where no
* viewport is really even close to being able to display the measurement.
* Orchestrates the search strategies in order of preference.
*/
public findUpdateableViewportConfiguration(activeViewportId: string, measurement) {
const { metadata, displaySetInstanceUID } = measurement;
const { volumeId, referencedImageId } = metadata;
const { displaySetService, viewportGridService } = this.servicesManager.services;
const { displaySetService } = this.servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// 1. Determine the target Viewport Type (Stack vs Volume)
const viewportType = this.determineTargetViewportType(displaySet, metadata);
// 2. Strategy: Find viewport already showing this volume
const volumeMatch = this.findViewportShowingVolume(
metadata,
displaySetInstanceUID,
viewportType
);
if (volumeMatch) {
return volumeMatch;
}
// 3. Strategy: Find viewport with compatible orientation (even if different display set)
const compatibleMatch = this.findViewportConvertibleToVolume(
metadata,
displaySetInstanceUID,
viewportType
);
if (compatibleMatch) {
return compatibleMatch;
}
// 4. Strategy: Find viewport with matching orientation via IOP
const orientationMatch = this.findViewportWithMatchingOrientation(
displaySetInstanceUID,
viewportType
);
if (orientationMatch) {
return orientationMatch;
}
// 5. Fallback: Use the active viewport
return {
viewportId: activeViewportId,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
/**
* Determines if the viewport should be what is specified in
* the viewportType of the display set, or stack if the display
* set isn't reconstructable and there is a referenced image id, otherwise
* volume.
*
* Expect there to be more rules in the future for different types of annotations/settings
* such as 3d annotations.
*/
public determineTargetViewportType(displaySet, metadata): string {
let { viewportType } = displaySet;
if (!viewportType) {
if (referencedImageId && !displaySet.isReconstructable) {
if (metadata.referencedImageId && !displaySet.isReconstructable) {
viewportType = csEnums.ViewportType.STACK;
} else if (volumeId) {
} else if (metadata.volumeId) {
viewportType = 'volume';
}
}
return viewportType;
}
// Find viewports that could be updated to be volumes to show this view
// That prefers a viewport already showing the right display set.
if (volumeId) {
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
if (viewport?.isReferenceViewable(metadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
/**
* Find viewports that could be updated to be volumes to show this view.
* Prefers a viewport already showing the right display set.
*/
public findViewportShowingVolume(metadata, displaySetInstanceUID, viewportType) {
if (!metadata.volumeId) {
return null;
}
// Find a viewport in the correct orientation showing a different display set
// which could be used to display the annotation.
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
if (viewport?.isReferenceViewable(metadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
return null;
}
/**
* Find a viewport that could be converted to a volume to show this annotation,
* already showing the right display set.
*/
public findViewportConvertibleToVolume(metadata, displaySetInstanceUID, viewportType) {
const { viewportGridService } = this.servicesManager.services;
const altMetadata = { ...metadata, volumeId: null, referencedImageId: null };
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
const viewportDisplaySetUID = viewportGridService.getDisplaySetsUIDsForViewport(id)?.[0];
if (!viewportDisplaySetUID || !viewport) {
continue;
}
if (volumeId) {
if (metadata.volumeId) {
altMetadata.volumeId = viewportDisplaySetUID;
}
altMetadata.FrameOfReferenceUID = this._getFrameOfReferenceUID(viewportDisplaySetUID);
if (viewport.isReferenceViewable(altMetadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
@@ -703,13 +755,22 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
};
}
}
return null;
}
// Just display in the active viewport
return {
viewportId: activeViewportId,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
/**
* Find a viewport with the closest orientation but on a different display set.
*/
public findViewportWithMatchingOrientation(metadata, displaySetInstanceUID, viewportType) {
const viewportAlignmentData = this.getViewportAlignmentData(metadata);
if (viewportAlignmentData?.length) {
return {
...viewportAlignmentData[0],
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
return null;
}
/**
+2 -2
View File
@@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/tools": "4.14.4",
"@cornerstonejs/core": "4.14.5",
"@cornerstonejs/tools": "4.14.5",
"@ohif/core": "3.12.0-beta.109",
"@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.109",
"@ohif/extension-default": "3.12.0-beta.109",
+2 -2
View File
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.14.4",
"@cornerstonejs/tools": "4.14.4",
"@cornerstonejs/core": "4.14.5",
"@cornerstonejs/tools": "4.14.5",
"@ohif/core": "3.12.0-beta.109",
"@ohif/extension-cornerstone": "3.12.0-beta.109",
"@ohif/extension-default": "3.12.0-beta.109",