diff --git a/packages/core/src/RenderingEngine/BaseRenderingEngine.ts b/packages/core/src/RenderingEngine/BaseRenderingEngine.ts index 5ecae8e570..c7fc461182 100644 --- a/packages/core/src/RenderingEngine/BaseRenderingEngine.ts +++ b/packages/core/src/RenderingEngine/BaseRenderingEngine.ts @@ -16,6 +16,7 @@ import { import type IStackViewport from '../types/IStackViewport'; import type IVolumeViewport from '../types/IVolumeViewport'; import viewportTypeToViewportClass from './helpers/viewportTypeToViewportClass'; +import ViewportStatus from '../enums/ViewportStatus'; import type * as EventTypes from '../types/EventTypes'; import type { @@ -689,6 +690,10 @@ abstract class BaseRenderingEngine { // Add the viewports to the set of flagged viewports viewportIds.forEach((viewportId) => { this._needsRender.add(viewportId); + const viewport = this._viewports.get(viewportId); + if (viewport) { + viewport.viewportStatus = ViewportStatus.NEEDS_RENDER; + } }); // Render any flagged viewports diff --git a/packages/core/src/RenderingEngine/ECGViewport.ts b/packages/core/src/RenderingEngine/ECGViewport.ts index f0317e5125..1ee99f2bd6 100644 --- a/packages/core/src/RenderingEngine/ECGViewport.ts +++ b/packages/core/src/RenderingEngine/ECGViewport.ts @@ -634,6 +634,7 @@ class ECGViewport extends Viewport { this.drawLabels(ctx, layouts); ctx.resetTransform(); + this.setRendered(); triggerEvent(this.element, EVENTS.IMAGE_RENDERED, { element: this.element, diff --git a/packages/core/src/RenderingEngine/VideoViewport.ts b/packages/core/src/RenderingEngine/VideoViewport.ts index 385bb63a2e..639c05ade1 100644 --- a/packages/core/src/RenderingEngine/VideoViewport.ts +++ b/packages/core/src/RenderingEngine/VideoViewport.ts @@ -1185,6 +1185,7 @@ class VideoViewport extends Viewport { (actor.actor as ICanvasActor).render(this, this.canvasContext); } this.canvasContext.resetTransform(); + this.setRendered(); // This is stack new image to agree with stack/non-volume viewports triggerEvent(this.element, EVENTS.STACK_NEW_IMAGE, { diff --git a/packages/core/src/RenderingEngine/Viewport.ts b/packages/core/src/RenderingEngine/Viewport.ts index 1fedee84f0..d9de46796e 100644 --- a/packages/core/src/RenderingEngine/Viewport.ts +++ b/packages/core/src/RenderingEngine/Viewport.ts @@ -250,6 +250,14 @@ class Viewport { this.viewportStatus = ViewportStatus.RENDERED; } + /** + * Mark the viewport as needing a render pass (e.g. after external display-set / segmentation updates). + * Does not queue a render; the rendering engine’s normal RAF cycle will pick this up. + */ + public setNeedsRender(): void { + this.viewportStatus = ViewportStatus.NEEDS_RENDER; + } + /** * This applies a color transform as an svg filter to the output image. */ diff --git a/packages/core/src/enums/ViewportStatus.ts b/packages/core/src/enums/ViewportStatus.ts index 9af71df641..ca92ff51bc 100644 --- a/packages/core/src/enums/ViewportStatus.ts +++ b/packages/core/src/enums/ViewportStatus.ts @@ -5,6 +5,8 @@ enum ViewportStatus { LOADING = 'loading', /** Ready to be rendered */ PRE_RENDER = 'preRender', + /** Render has been requested and is pending in RAF queue */ + NEEDS_RENDER = 'needsRender', /** In the midst of a resize */ RESIZE = 'resize', /** Rendered image data */ diff --git a/packages/tools/src/utilities/segmentation/utilsForWorker.ts b/packages/tools/src/utilities/segmentation/utilsForWorker.ts index 2726c0d635..f9f2467550 100644 --- a/packages/tools/src/utilities/segmentation/utilsForWorker.ts +++ b/packages/tools/src/utilities/segmentation/utilsForWorker.ts @@ -39,6 +39,14 @@ export const getSegmentationDataForWorker = ( segmentIndices ) => { const segmentation = getSegmentation(segmentationId); + if (!segmentation?.representationData) { + console.debug( + 'getSegmentationDataForWorker: segmentation missing or not ready', + segmentationId + ); + return null; + } + const { representationData } = segmentation; const { Labelmap } = representationData; diff --git a/tests/labelmapsegmentationtools.spec.ts b/tests/labelmapsegmentationtools.spec.ts index 7189c32367..468a1eb6a6 100644 --- a/tests/labelmapsegmentationtools.spec.ts +++ b/tests/labelmapsegmentationtools.spec.ts @@ -4,10 +4,11 @@ import { checkForScreenshot, screenShotPaths, simulateClicksOnElement, + waitForViewportsRendered, } from './utils/index'; const delayBetweenClicks = async (page: any) => { - await page.waitForTimeout(1500); + await waitForViewportsRendered(page); }; test.beforeEach(async ({ page }) => { @@ -621,7 +622,7 @@ test.describe('Basic manual labelmap Segmentation tools', async () => { ], }); - await page.waitForTimeout(3000); + await waitForViewportsRendered(page); await checkForScreenshot( page, screenshotLocator, @@ -682,7 +683,7 @@ test.describe('Basic manual labelmap Segmentation tools', async () => { ], }); - await page.waitForTimeout(3000); + await waitForViewportsRendered(page); await checkForScreenshot( page, screenshotLocator, diff --git a/tests/utils/index.ts b/tests/utils/index.ts index bb02070d26..d8551e5f98 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -6,3 +6,4 @@ export { simulateClicksOnElement } from './simulateClicksOnElement'; export { simulateDrawPath } from './simulateDrawPath'; export { reduceViewportsSize } from './reduceViewportsSize'; export { attemptAction } from './attemptAction'; +export { waitForViewportsRendered } from './waitForViewportsRendered'; diff --git a/tests/utils/waitForViewportsRendered.ts b/tests/utils/waitForViewportsRendered.ts new file mode 100644 index 0000000000..4086c1bf7a --- /dev/null +++ b/tests/utils/waitForViewportsRendered.ts @@ -0,0 +1,94 @@ +import type { Page } from '@playwright/test'; + +type WaitForViewportsRenderedOptions = { + timeout?: number; + /** + * If true (default), also waits for any volume actors referenced by the + * viewports to report loaded. + */ + waitVolumeLoad?: boolean; +}; + +/** + * Stabilize tests by waiting for a short tick, network idle, then viewport render completion. + * To use this method safely, you may need to make changes to OHIF and/or CS3D + * methods handling clicks (SHOULD be commands modules only). These should set the + * state to needs render synchronously so that this method can safely wait for the render to complete. + * Examples such as changing the hanging protocol currently don't set such a state + * and thus can't be rendered without a delay. + * + * If options.waitVolumeLoad is not false, then this method will wait for all volumes + * associated with viewports to be loaded. + */ +export const waitForViewportsRendered = async ( + page: Page, + options: WaitForViewportsRenderedOptions = {} +) => { + const { timeout = 15000, waitVolumeLoad = true } = options; + + await page.waitForFunction( + ({ waitVolumeLoad }) => { + const cornerstone = (window as any).cornerstone; + if (!cornerstone?.getRenderingEngines) { + return false; + } + + const renderingEngines = cornerstone.getRenderingEngines(); + const viewports = renderingEngines.flatMap((engine) => + engine.getViewports ? engine.getViewports() : [] + ); + + if (!viewports.length) { + return false; + } + + const allRendered = viewports.every( + (viewport) => viewport?.viewportStatus === 'rendered' + ); + + if (!allRendered) { + return false; + } + + if (!waitVolumeLoad) { + return true; + } + + const cache = cornerstone.cache; + if (!cache?.getVolume) { + return true; + } + + const actorEntries = viewports.flatMap((viewport) => + viewport?.getActors ? viewport.getActors() : [] + ); + + for (const actorEntry of actorEntries) { + const id = actorEntry?.referencedId || actorEntry?.uid; + if (!id) { + continue; + } + + let volume: any; + try { + volume = cache.getVolume(id); + } catch { + continue; + } + + const loaded = + volume?.loadStatus && typeof volume.loadStatus.loaded === 'boolean' + ? volume.loadStatus.loaded + : true; + + if (!loaded) { + return false; + } + } + + return true; + }, + { waitVolumeLoad }, + { timeout } + ); +};