fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set (#5900)

- 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.
- fix segmentation overlay order reversal on viewport re-render
This commit is contained in:
Ghadeer Albattarni authored and GitHub committed 2026-04-02 08:52:23 -04:00
1 parent a875d7de1f
commit b9029ef6f8
15 files changed
+223 -56

No files matched your search

@@ -0,0 +1,98 @@
import { test, visitStudy, expect } from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.12201.1091.126683095609223531686845324113579088978';
const mode = 'segmentation';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test('should only show segmentations matching the background Frame of Reference', async ({
page,
viewportPageObject,
}) => {
const segmentationLabelsFOR1 = ['Segmentation FOR1SEG', 'FoR 1 RTstructRTSTRUCT'];
const segmentationLabelsFOR2 = ['Segmentation FOR2SEG', 'FoR 2 RTstructRTSTRUCT'];
const dataOverlay = (await viewportPageObject.getNth(0)).overlayMenu.dataOverlay;
await dataOverlay.toggle();
await dataOverlay.openAddSegmentationDropdown();
const segmentationLabels = await dataOverlay.getDropdownOptionLabels();
expect(segmentationLabels.sort()).toEqual([...segmentationLabelsFOR1].sort());
await page.keyboard.press('Escape');
await dataOverlay.changeBackgroundDisplaySet('CT Std (FoR 2)-CT');
await dataOverlay.clickSelectSegmentation();
const segmentationLabelsAfterBgChange = await dataOverlay.getDropdownOptionLabels();
expect(segmentationLabelsAfterBgChange).not.toEqual(segmentationLabelsFOR1);
expect(segmentationLabelsAfterBgChange.sort()).toEqual([...segmentationLabelsFOR2].sort());
});
test('should only sync segmentations to viewports with same Frame of Reference', async ({
viewportPageObject,
}) => {
const segmentationFOR1Label = 'Segmentation FOR1';
const viewportA = await viewportPageObject.getNth(0);
const viewportB = await viewportPageObject.getNth(1);
const viewportC = await viewportPageObject.getNth(2);
const dataOverlayA = viewportA.overlayMenu.dataOverlay;
const dataOverlayB = viewportB.overlayMenu.dataOverlay;
const dataOverlayC = viewportC.overlayMenu.dataOverlay;
// In FOR1 viewport A: add a segmentation FOR1 overlay
await viewportA.pane.hover();
await dataOverlayA.toggle();
await dataOverlayA.addSegmentation(segmentationFOR1Label);
await expect(dataOverlayA.getOverlaySegmentationRow(segmentationFOR1Label)).toBeVisible();
await dataOverlayA.toggle();
await expect(dataOverlayA.menu).not.toBeVisible();
// In another FOR1 viewport B: segmentation FOR1 should be visible (synced)
await viewportB.pane.hover();
await dataOverlayB.toggle();
const activeSegmentationViewportB = dataOverlayB.getOverlaySegmentationRow(segmentationFOR1Label);
await expect(activeSegmentationViewportB).toBeVisible();
await expect(activeSegmentationViewportB).toHaveText(segmentationFOR1Label.toUpperCase());
await dataOverlayB.toggle();
await expect(dataOverlayB.menu).not.toBeVisible();
// In FOR2 viewport C: segmentation FOR1 should NOT be visible
await viewportC.pane.hover();
await dataOverlayC.toggle();
await expect(dataOverlayC.getOverlaySegmentationRow(segmentationFOR1Label)).not.toBeVisible();
await dataOverlayC.toggle();
await expect(dataOverlayC.menu).not.toBeVisible();
});
test('should preserve segmentation order after menu close and reopen', async ({
viewportPageObject,
}) => {
const segmentation1Label = 'Segmentation FOR1';
const segmentation2Label = 'FoR 1 RTstruct';
const dataOverlay = (await viewportPageObject.getNth(0)).overlayMenu.dataOverlay;
await dataOverlay.toggle();
await dataOverlay.addSegmentation(segmentation1Label);
await expect(dataOverlay.getOverlaySegmentationRow(segmentation1Label)).toBeVisible();
await dataOverlay.addSegmentation(segmentation2Label);
await expect(dataOverlay.getOverlaySegmentationRow(segmentation2Label)).toBeVisible();
await dataOverlay.toggle();
await expect(dataOverlay.menu).not.toBeVisible();
// Reopen menu and verify segmentation order is preserved
await dataOverlay.toggle();
await expect(dataOverlay.overlaySegmentationRows).toHaveCount(2);
await expect(dataOverlay.overlaySegmentationRows.nth(0)).toHaveText(
segmentation1Label.toUpperCase()
);
await expect(dataOverlay.overlaySegmentationRows.nth(1)).toHaveText(
segmentation2Label.toUpperCase()
);
});
+33 -2
View File
@@ -9,13 +9,16 @@ export class DataOverlayPageObject {
this.viewportId = viewportId;
}
get menu() {
return this.page.getByTestId(`viewport-data-overlay-menu-${this.viewportId}`);
}
async toggle() {
await this.page.getByTestId(`dataOverlayMenu-${this.viewportId}-btn`).click();
}
async addSegmentation(segmentation: string) {
await this.page.getByTestId(`AddSegmentationDataOverlay-${this.viewportId}`).click();
await this.page.getByText('SELECT A SEGMENTATION').click();
await this.openAddSegmentationDropdown();
await this.page.getByTestId(segmentation).click();
}
@@ -28,4 +31,32 @@ export class DataOverlayPageObject {
await this.page.getByTestId(`overlay-ds-more-button-${segmentation}`).click();
await this.page.getByTestId(`overlay-ds-remove-button-${segmentation}`).click();
}
async openAddSegmentationDropdown() {
await this.page.getByTestId(`AddSegmentationDataOverlay-${this.viewportId}`).click();
await this.clickSelectSegmentation();
}
async clickSelectSegmentation() {
await this.page.getByText('SELECT A SEGMENTATION').click();
}
async getDropdownOptionLabels(): Promise<string[]> {
const options = this.page.getByRole('option');
const labels = (await options.allTextContents()).map(text => text.trim()).filter(Boolean);
return labels;
}
getOverlaySegmentationRow(label: string) {
return this.menu.getByTestId(`overlay-ds-select-value-${label.toUpperCase()}`);
}
get overlaySegmentationRows() {
return this.menu.locator('[data-cy^="overlay-ds-select-value-"]');
}
async changeBackgroundDisplaySet(backgroundLabel: string) {
await this.page.getByTestId(`overlay-background-ds-select-${this.viewportId}`).click();
await this.page.getByTestId(backgroundLabel).click();
}
}