fix(segmentation): Added support for unhydrated RTSTRUCT in MPR. Added support for overlaying segmentations over display sets the segmentations do not reference. (#5168)
1
.vscode/settings.json
vendored
@ -82,6 +82,7 @@
|
|||||||
"Reslices",
|
"Reslices",
|
||||||
"roadmap",
|
"roadmap",
|
||||||
"ROADMAPS",
|
"ROADMAPS",
|
||||||
|
"rtstruct",
|
||||||
"Segmentations",
|
"Segmentations",
|
||||||
"semibold",
|
"semibold",
|
||||||
"sitk",
|
"sitk",
|
||||||
|
|||||||
@ -169,7 +169,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
|||||||
{...props}
|
{...props}
|
||||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
viewportType: 'stack',
|
viewportType: viewportOptions.viewportType,
|
||||||
toolGroupId: toolGroupId,
|
toolGroupId: toolGroupId,
|
||||||
orientation: viewportOptions.orientation,
|
orientation: viewportOptions.orientation,
|
||||||
viewportId: viewportOptions.viewportId,
|
viewportId: viewportOptions.viewportId,
|
||||||
|
|||||||
@ -67,9 +67,21 @@ export function configureViewportForLayerAddition(params: {
|
|||||||
|
|
||||||
// If a viewport type was already set do not reset it.
|
// If a viewport type was already set do not reset it.
|
||||||
if (!viewport.viewportOptions.viewportType) {
|
if (!viewport.viewportOptions.viewportType) {
|
||||||
// Do not force volume for SEG and RTSTRUCT if there is only one display set
|
// Special handling for overlay display sets
|
||||||
if (requestedLayerDisplaySet.isOverlayDisplaySet && currentDisplaySetUIDs.length === 1) {
|
if (requestedLayerDisplaySet.isOverlayDisplaySet) {
|
||||||
viewport.viewportOptions.viewportType = 'stack';
|
// Do not force volume for SEG and RTSTRUCT if it and all the current display sets are for the same display set
|
||||||
|
const isSameDisplaySet = currentDisplaySetUIDs.every(uid => {
|
||||||
|
const currentDisplaySet = displaySetService.getDisplaySetByUID(uid);
|
||||||
|
return currentDisplaySet.isOverlayDisplaySet
|
||||||
|
? currentDisplaySet.referencedDisplaySetInstanceUID ===
|
||||||
|
requestedLayerDisplaySet.referencedDisplaySetInstanceUID
|
||||||
|
: uid === requestedLayerDisplaySet.referencedDisplaySetInstanceUID;
|
||||||
|
});
|
||||||
|
if (isSameDisplaySet) {
|
||||||
|
viewport.viewportOptions.viewportType = 'stack';
|
||||||
|
} else {
|
||||||
|
viewport.viewportOptions.viewportType = 'volume';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
viewport.viewportOptions.viewportType = 'volume';
|
viewport.viewportOptions.viewportType = 'volume';
|
||||||
}
|
}
|
||||||
|
|||||||
41
tests/MPRThenRTOverlayNoHydration.spec.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay menu', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('side-panel-header-right').click();
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('MPR').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.mprThenRTOverlayNoHydration.mprPreRTOverlayNoHydration
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hover over the middle/sagittal viewport so that the data overlay menu is available.
|
||||||
|
await page.locator('css=div[data-viewportid="mpr-sagittal"]').hover();
|
||||||
|
await page.getByTestId('dataOverlayMenu-mpr-sagittal-btn').click();
|
||||||
|
await page.getByTestId('AddSegmentationDataOverlay-mpr-sagittal').click();
|
||||||
|
await page.getByText('SELECT A SEGMENTATION').click();
|
||||||
|
await page.getByTestId('ARIA RadOnc Structure Sets').click();
|
||||||
|
|
||||||
|
// Hide the overlay menu.
|
||||||
|
await page.getByTestId('dataOverlayMenu-mpr-sagittal-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.mprThenRTOverlayNoHydration.mprPostRTOverlayNoHydration
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
import { press } from './utils/keyboardUtils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
|
||||||
|
const mode = 'segmentation';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should overlay an unhydrated RTSTRUCT over a display set that the RTSTRUCT does NOT reference', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
await page.getByTestId('AddSegmentationDataOverlay-default').click();
|
||||||
|
await page.getByText('SELECT A SEGMENTATION').click();
|
||||||
|
await page.getByTestId('Contours on PET').click();
|
||||||
|
|
||||||
|
// Hide the overlay menu.
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtDataOverlayForUnreferencedDisplaySetNoHydration.overlayFirstImage
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to the middle image of the default viewport.
|
||||||
|
await press({ page, key: 'ArrowDown', nTimes: 23 });
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtDataOverlayForUnreferencedDisplaySetNoHydration.overlayMiddleImage
|
||||||
|
);
|
||||||
|
});
|
||||||
38
tests/RTDataOverlayNoHydrationThenMPR.spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay menu', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('side-panel-header-right').click();
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
await page.getByTestId('AddSegmentationDataOverlay-default').click();
|
||||||
|
await page.getByText('SELECT A SEGMENTATION').click();
|
||||||
|
await page.getByTestId('ARIA RadOnc Structure Sets').click();
|
||||||
|
|
||||||
|
// Hide the overlay menu.
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtDataOverlayNoHydrationThenMPR.rtDataOverlayNoHydrationPreMPR
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('MPR').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtDataOverlayNoHydrationThenMPR.rtDataOverlayNoHydrationPostMPR
|
||||||
|
);
|
||||||
|
});
|
||||||
42
tests/RTHydrationFromMPR.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hydrate an RTSTRUCT from MPR', async ({ page }) => {
|
||||||
|
await page.getByTestId('side-panel-header-right').click();
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('MPR').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT);
|
||||||
|
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRT);
|
||||||
|
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRTHydrated);
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('Axial Primary').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtHydrationFromMPR.mprAfterRTHydratedAfterLayoutChange
|
||||||
|
);
|
||||||
|
});
|
||||||
28
tests/RTHydrationThenMPR.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hydrate an RTSTRUCT and then launch MPR', async ({ page }) => {
|
||||||
|
await page.getByTestId('side-panel-header-right').click();
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
|
||||||
|
await page.getByTestId('yes-hydrate-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtHydrationThenMPR.rtPostHydration);
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('Axial Primary').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.rtHydrationThenMPR.rtPostHydrationMPRAxialPrimary
|
||||||
|
);
|
||||||
|
});
|
||||||
22
tests/RTNoHydrationThenMPR.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should launch MPR with unhydrated RTSTRUCT', async ({ page }) => {
|
||||||
|
await page.getByTestId('side-panel-header-right').click();
|
||||||
|
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPreMPR);
|
||||||
|
|
||||||
|
await page.getByTestId('Layout').click();
|
||||||
|
await page.getByTestId('MPR').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPostMPR);
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { test } from 'playwright-test-coverage';
|
||||||
|
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||||
|
import { press } from './utils/keyboardUtils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||||
|
const mode = 'segmentation';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should overlay an unhydrated SEG over a display set that the SEG does NOT reference', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
await page.getByTestId('AddSegmentationDataOverlay-default').click();
|
||||||
|
await page.getByText('SELECT A SEGMENTATION').click();
|
||||||
|
await page.getByTestId('Segmentation').click();
|
||||||
|
|
||||||
|
// Hide the overlay menu.
|
||||||
|
await page.getByTestId('dataOverlayMenu-default-btn').click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.segDataOverlayForUnreferencedDisplaySetNoHydration.overlayFirstImage
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to the middle image of the default viewport.
|
||||||
|
await press({ page, key: 'ArrowDown', nTimes: 9 });
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await checkForScreenshot(
|
||||||
|
page,
|
||||||
|
page,
|
||||||
|
screenShotPaths.segDataOverlayForUnreferencedDisplaySetNoHydration.overlayMiddleImage
|
||||||
|
);
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 211 KiB |
26
tests/utils/keyboardUtils.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PressParams defines the parameters for the press function.
|
||||||
|
* @property {Page} page - The Playwright page on which to perform the key press.
|
||||||
|
* @property {string} key - The key to be pressed.
|
||||||
|
* @property {number} [nTimes=1] - The number of times to press the key (default is 1).
|
||||||
|
* @property {unknown} [options={}] - Additional options for the key press.
|
||||||
|
*/
|
||||||
|
type PressParams = {
|
||||||
|
page: Page;
|
||||||
|
key: string;
|
||||||
|
nTimes?: number;
|
||||||
|
options?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to press a key multiple times on a Playwright page.
|
||||||
|
*/
|
||||||
|
export const press = async (params: PressParams) => {
|
||||||
|
const { page, key, nTimes = 1, options = {} } = params;
|
||||||
|
|
||||||
|
for (let i = 0; i < nTimes; i += 1) {
|
||||||
|
await page.keyboard.press(key, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -69,6 +69,28 @@ const screenShotPaths = {
|
|||||||
reset: {
|
reset: {
|
||||||
resetDisplayedCorrectly: 'resetDisplayedCorrectly.png',
|
resetDisplayedCorrectly: 'resetDisplayedCorrectly.png',
|
||||||
},
|
},
|
||||||
|
rtDataOverlayForUnreferencedDisplaySetNoHydration: {
|
||||||
|
overlayFirstImage: 'overlayFirstImage.png',
|
||||||
|
overlayMiddleImage: 'overlayMiddleImage.png',
|
||||||
|
},
|
||||||
|
rtDataOverlayNoHydrationThenMPR: {
|
||||||
|
rtDataOverlayNoHydrationPreMPR: 'rtDataOverlayNoHydrationPreMpr.png',
|
||||||
|
rtDataOverlayNoHydrationPostMPR: 'rtDataOverlayNoHydrationPostMpr.png',
|
||||||
|
},
|
||||||
|
rtHydrationFromMPR: {
|
||||||
|
mprBeforeRT: 'mprBeforeRT.png',
|
||||||
|
mprAfterRT: 'mprAfterRT.png',
|
||||||
|
mprAfterRTHydrated: 'mprAfterRTHydrated.png',
|
||||||
|
mprAfterRTHydratedAfterLayoutChange: 'mprAfterRTHydratedAfterLayoutChange.png',
|
||||||
|
},
|
||||||
|
rtHydrationThenMPR: {
|
||||||
|
rtPostHydration: 'rtPostHydration.png',
|
||||||
|
rtPostHydrationMPRAxialPrimary: 'rtPostHydrationMPRAxialPrimary.png',
|
||||||
|
},
|
||||||
|
rtNoHydrationThenMPR: {
|
||||||
|
rtNoHydrationPreMPR: 'rtNoHydrationPreMpr.png',
|
||||||
|
rtNoHydrationPostMPR: 'rtNoHydrationPostMpr.png',
|
||||||
|
},
|
||||||
srHydration: {
|
srHydration: {
|
||||||
srPostHydration: 'srPostHydration.png',
|
srPostHydration: 'srPostHydration.png',
|
||||||
srPreHydration: 'srPreHydration.png',
|
srPreHydration: 'srPreHydration.png',
|
||||||
@ -93,10 +115,18 @@ const screenShotPaths = {
|
|||||||
segNoHydrationPreMPR: 'segNoHydrationPreMpr.png',
|
segNoHydrationPreMPR: 'segNoHydrationPreMpr.png',
|
||||||
segNoHydrationPostMPR: 'segNoHydrationPostMpr.png',
|
segNoHydrationPostMPR: 'segNoHydrationPostMpr.png',
|
||||||
},
|
},
|
||||||
|
segDataOverlayForUnreferencedDisplaySetNoHydration: {
|
||||||
|
overlayFirstImage: 'overlayFirstImage.png',
|
||||||
|
overlayMiddleImage: 'overlayMiddleImage.png',
|
||||||
|
},
|
||||||
segDataOverlayNoHydrationThenMPR: {
|
segDataOverlayNoHydrationThenMPR: {
|
||||||
segDataOverlayNoHydrationPreMPR: 'segDataOverlayNoHydrationPreMpr.png',
|
segDataOverlayNoHydrationPreMPR: 'segDataOverlayNoHydrationPreMpr.png',
|
||||||
segDataOverlayNoHydrationPostMPR: 'segDataOverlayNoHydrationPostMpr.png',
|
segDataOverlayNoHydrationPostMPR: 'segDataOverlayNoHydrationPostMpr.png',
|
||||||
},
|
},
|
||||||
|
mprThenRTOverlayNoHydration: {
|
||||||
|
mprPreRTOverlayNoHydration: 'mprPreRTOverlayNoHydration.png',
|
||||||
|
mprPostRTOverlayNoHydration: 'mprPostRTOverlayNoHydration.png',
|
||||||
|
},
|
||||||
mprThenSEGOverlayNoHydration: {
|
mprThenSEGOverlayNoHydration: {
|
||||||
mprPreSEGOverlayNoHydration: 'mprPreSEGOverlayNoHydration.png',
|
mprPreSEGOverlayNoHydration: 'mprPreSEGOverlayNoHydration.png',
|
||||||
mprPostSEGOverlayNoHydration: 'mprPostSEGOverlayNoHydration.png',
|
mprPostSEGOverlayNoHydration: 'mprPostSEGOverlayNoHydration.png',
|
||||||
|
|||||||