fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set #5900
Conversation
…f reference as viewport - Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering - Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch - Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
…segmentation-menu-FOR
✅ Deploy Preview for ohif-dev canceled.
|
| useEffect(() => { | ||
| setOptimisticOverlayDisplaySets(overlayDisplaySets); | ||
| }, [backgroundDisplaySet?.displaySetInstanceUID]); |
There was a problem hiding this comment.
Missing overlayDisplaySets dependency
overlayDisplaySets is referenced inside the useEffect callback but is not listed in the dependency array. This means the effect captures a stale closure value if overlayDisplaySets changes between renders without backgroundDisplaySet?.displaySetInstanceUID also changing. For instance, if a segmentation representation is added or removed while the background stays the same, and then the user switches the background, the effect would reset to a stale overlayDisplaySets snapshot from the render when the background last changed — not the current one.
Since React re-runs the effect by comparing dependency values, adding overlayDisplaySets ensures the optimistic state stays synchronized. If the intent is to only reset on background changes, consider using a ref to hold the latest overlayDisplaySets value instead.
| useEffect(() => { | |
| setOptimisticOverlayDisplaySets(overlayDisplaySets); | |
| }, [backgroundDisplaySet?.displaySetInstanceUID]); | |
| useEffect(() => { | |
| setOptimisticOverlayDisplaySets(overlayDisplaySets); | |
| }, [backgroundDisplaySet?.displaySetInstanceUID, overlayDisplaySets]); |
There was a problem hiding this comment.
@greptileai is correct here. Please add the missing dependency.
There was a problem hiding this comment.
| useEffect(() => { | |
| setOptimisticOverlayDisplaySets(overlayDisplaySets); | |
| }, [backgroundDisplaySet?.displaySetInstanceUID]); | |
| useEffect(() => { | |
| setOptimisticOverlayDisplaySets(overlayDisplaySets); | |
| }, [backgroundDisplaySet?.displaySetInstanceUID, overlayDisplaySets]); |
tests/pages/DataOverlayPageObject.ts
Outdated
| return labels; | ||
| } | ||
|
|
||
| getActiveSegmentationLocator(label: string) { |
There was a problem hiding this comment.
I am not crazy about the word Locator in this method name. Maybe ask AI for a better name?
There was a problem hiding this comment.
I changed the name here :)
| page, | ||
| viewportPageObject, | ||
| }) => { | ||
| const viewportId = await viewportPageObject.getIdOfNth(0); |
There was a problem hiding this comment.
I am not crazy about this getIdOfNth method. Could we not simply use await viewportPageObject.getNth(0) and then get the overlay object for it?
| return this.viewportPageObjectFactory(viewport); | ||
| } | ||
|
|
||
| async getIdOfNth(index: number): Promise<string> { |
There was a problem hiding this comment.
See my comment in the spec about possibly removing this metody. In general Playwright strongly suggests to NOT assert and/or get attributes, text or data of any kind... https://playwright.dev/docs/api/class-locator#locator-get-attribute
| await page.waitForTimeout(2000); | ||
|
|
||
| const segmentationLabels = await dataOverlay.getDropdownOptionLabels(); | ||
| expect(segmentationLabels.sort()).toEqual([...segmentationLabelsFOR1].sort()); |
There was a problem hiding this comment.
Are we sorting these because at present there is a bug whereby the labels might be in different order?
| await this.page.getByText('SELECT A SEGMENTATION').click(); | ||
| } | ||
|
|
||
| async getDropdownOptionLabels(): Promise<string[]> { |
There was a problem hiding this comment.
There are two concerns I have about this method:
- It goes against Playwright's principle to not get data and assert it. So instead this should be replaced by a utility that asserts the labels
- It assumes that the drop down menu is open. Instead whether we keep this method or replace it with a utility, this function/method itself should open and close the menu to get and/or assert its data.
|
|
||
| await dataOverlay.toggle(viewportId); | ||
| await dataOverlay.openAddSegmentationDropdown(viewportId); | ||
| await page.waitForTimeout(2000); |
There was a problem hiding this comment.
No need to wait, especially if we replace getDropdownOptionLabels to be a utility that does an expect which will do the waiting for us.
| await dataOverlay.toggle(viewportId); | ||
| await dataOverlay.changeBackgroundDisplaySet('CT Std (FoR 2)-CT', viewportId); | ||
| await dataOverlay.openAddSegmentationDropdown(viewportId); | ||
| await page.waitForTimeout(2000); |
There was a problem hiding this comment.
See comment above about waiting.
| await dataOverlay.addSegmentation(segmentationFOR1Label, FOR1ViewportIdA); | ||
| await expect(dataOverlay.getOverlaySegmentationRow(segmentationFOR1Label)).toBeVisible(); | ||
| await dataOverlay.closeMenu(FOR1ViewportIdA); | ||
| await page.waitForTimeout(2000); |
| } | ||
|
|
||
| async closeMenu(viewportId: string = 'default') { | ||
| await this.toggle(viewportId); |
There was a problem hiding this comment.
I think it is also best to wait for the menu to no longer be visible before returning. Again these are the principles of GUI testing.
jbocce
left a comment
There was a problem hiding this comment.
Thank you for this. See my comments.
Context
Fixes #5890
This PR fixes a bug where the segmentation overlay menu allowed users to select segmentations that did not share the same Frame of Reference (FOR) as the underlying viewport display set.
Also, in segmentation mode, the hydrate segmentation synchronizer added segmentations to all viewports regardless of their Frame of Reference, causing segmentations to appear immediately in viewports with a different FOR.
Root Cause
SEGandRTSTRUCTdisplay sets had noFrameOfReferenceUIDso the FOR filter ingetEnhancedDisplaySetssilently passed every SEG/RT and mark them asisOverlayable: truesince the the check never gets true, the check always (undefined && ...)inextensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/utils.tsThe hydrate segmentation synchronizer did not check whether the target viewport matched the source viewport's FOR. So the segmentations were added to all viewports with different FOR.
Changes & Results
FrameOfReferenceUIDto theDisplaySetand ensuredSEGandRTSTRUCTSop class handlers set it (from instance and/or referenced display set).In
getEnhancedDisplaySets, overlay display sets (SEG/RTSTRUCT) whoseFrameOfReferenceUIDdoes not match the viewport background’s are marked non-overlayable, so they no longer appear as selectable in the overlay segmentation menu.After
FOR-fix.mov
Testing
Open a study with multiple frames of reference in a segmentation mode
(e.g., StudyInstanceUIDs=1.3.6.1.4.1.12201.1091.126683095609223531686845324113579088978)
Open the overlay menu on a viewport
Click on add segmentation
Verify only segmentations matching that viewport’s background FOR should be listed.
Change the viewport background display set
Verify the segmentation display set will reset
Select a segmentation
Open another Viewport overlay menu
Verify that the selected segmentation (from previous viewport) was add only to the matching viewport display set FOR
Checklist
PR
semantic-release format and guidelines.
Code
etc.)
Public Documentation Updates
additions or removals.
Tested Environment
Greptile Summary
This PR fixes a bug where the segmentation overlay menu allowed selecting segmentations with a different Frame of Reference (FOR) than the viewport's background display set, and where the hydrate segmentation synchronizer propagated segmentations to viewports with mismatched FORs.
FrameOfReferenceUIDpropagation: Both the SEG and RTSTRUCT SOP class handlers now populateFrameOfReferenceUIDon their display sets — from the instance metadata directly and from the referenced display set as a fallback. This enables the existing FOR check ingetEnhancedDisplaySets(utils.ts) to properly filter non-matching overlays.useEffectinViewportDataOverlayMenuresets the optimistic overlay display set list when the background display set changes, keeping the UI in sync. However,overlayDisplaySetsis missing from the effect's dependency array.createHydrateSegmentationSynchronizernow compares source and target viewport FORs before propagating segmentation representations, preventing cross-FOR hydration.FrameOfReferenceUIDadded as an optional property to the coreDisplaySettype.Confidence Score: 4/5
extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenu.tsx— theuseEffectdependency array is incomplete.Important Files Changed
overlayDisplaySetsdependency is missing from the effect's dependency array, which could cause stale state.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[SEG/RTSTRUCT Instance Loaded] --> B[SOP Class Handler] B --> C[Set FrameOfReferenceUID on DisplaySet] C --> D{Referenced DisplaySet Available?} D -->|Yes| E[Copy FOR from Referenced DisplaySet] D -->|No| F[Subscribe for DISPLAY_SETS_ADDED event] F --> G[On event: Copy FOR from added DisplaySet] E --> H[getEnhancedDisplaySets] G --> H H --> I{displaySet.FrameOfReferenceUID matches background?} I -->|Yes or undefined| J[isOverlayable: true] I -->|No| K[isOverlayable: false] J --> L[Shown in Overlay Menu] K --> M[Hidden from Overlay Menu] N[Segmentation Hydration Sync] --> O{Shared DisplaySet exists?} O -->|Yes| P[Add segmentation to target viewport] O -->|No| Q{Source FOR == Target FOR?} Q -->|Yes| P Q -->|No| R[Skip - different Frame of Reference]Last reviewed commit: 9e5ae01