From fe1ecfe0cc5d1bcf82686d821f356da8936a8c4b Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:12:07 -0400 Subject: [PATCH] fix(window level): The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. (#5865) * fix(window level): Set up listener for viewport availability such that the initial window level can be read and displayed. * PR feedback. * PR feedback. --------- Co-authored-by: Bill Wallace --- .../Overlays/CustomizableViewportOverlay.tsx | 32 +-- .../viewportOverlayCustomization.tsx | 2 + .../src/hooks/useViewportRendering.tsx | 184 +++++++++--------- .../ColorbarService/ColorbarService.ts | 24 +-- .../src/utils/getDataIdForViewport.ts | 22 +++ tests/MeasurementPanel.spec.ts | 2 +- tests/SegmentationPanel.spec.ts | 2 +- tests/WindowLevelOverlayText.spec.ts | 40 ++++ tests/pages/ViewportPageObject.ts | 33 +++- 9 files changed, 188 insertions(+), 153 deletions(-) create mode 100644 extensions/cornerstone/src/utils/getDataIdForViewport.ts create mode 100644 tests/WindowLevelOverlayText.spec.ts diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index 5e76c2046..209d190a6 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -67,10 +67,9 @@ function CustomizableViewportOverlay({ }) { const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } = servicesManager.services; - const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [scale, setScale] = useState(1); const [annotationState, setAnnotationState] = useState(0); - const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId); + const { isViewportBackgroundLight: isLight, windowLevel: voi } = useViewportRendering(viewportId); const { imageIndex } = imageSliceData; // Historical usage defined the overlays as separate items due to lack of @@ -110,30 +109,6 @@ function CustomizableViewportOverlay({ }; }, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]); - /** - * Updating the VOI when the viewport changes its voi - */ - useEffect(() => { - const updateVOI = eventDetail => { - const { range } = eventDetail.detail; - - if (!range) { - return; - } - - const { lower, upper } = range; - const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper); - - setVOI({ windowCenter, windowWidth }); - }; - - element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI); - - return () => { - element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI); - }; - }, [viewportId, viewportData, voi, element]); - const annotationModified = useCallback(evt => { if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) { // Update the annotation state to trigger a re-render @@ -226,6 +201,7 @@ function CustomizableViewportOverlay({ scale, instanceNumber, annotationState, + isLight, ] ); @@ -402,6 +378,7 @@ function OverlayItem(props) { */ function VOIOverlayItem({ voi, customization }: OverlayItemProps) { const { windowWidth, windowCenter } = voi; + const { title } = customization; if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') { return null; } @@ -410,6 +387,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
W: {windowWidth.toFixed(0)} @@ -443,11 +421,13 @@ function InstanceNumberOverlayItem({ customization, }: OverlayItemProps) { const { imageIndex, numberOfSlices } = imageSliceData; + const { title } = customization; return (
{instanceNumber !== undefined && instanceNumber !== null ? ( diff --git a/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx index 9d3e2e1b2..a26cd6ada 100644 --- a/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx +++ b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx @@ -25,6 +25,7 @@ export default { { id: 'WindowLevel', inheritsFrom: 'ohif.overlayItem.windowLevel', + title: 'Window Level', }, { id: 'ZoomLevel', @@ -39,6 +40,7 @@ export default { { id: 'InstanceNumber', inheritsFrom: 'ohif.overlayItem.instanceNumber', + title: 'Instance Number', }, ], }; diff --git a/extensions/cornerstone/src/hooks/useViewportRendering.tsx b/extensions/cornerstone/src/hooks/useViewportRendering.tsx index 08538623e..a7a12f353 100644 --- a/extensions/cornerstone/src/hooks/useViewportRendering.tsx +++ b/extensions/cornerstone/src/hooks/useViewportRendering.tsx @@ -10,6 +10,7 @@ import { BaseVolumeViewport, cache, } from '@cornerstonejs/core'; +import { getDataIdForViewport } from '../utils/getDataIdForViewport'; import { WindowLevelPreset } from '../types/WindowLevel'; import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar'; import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig'; @@ -104,11 +105,6 @@ const opacityToLinear = (opacityValue: number): number => { return Math.pow(opacityValue, 1.0 / GAMMA); }; -const is3DViewport = ({ viewportId, cornerstoneViewportService }) => { - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - return viewport instanceof VolumeViewport3D; -}; - /** * Hook to access window level functionality for a specific viewport * @@ -124,15 +120,17 @@ export function useViewportRendering( const { cornerstoneViewportService, colorbarService, customizationService } = servicesManager.services; - const [is3DVolume, setIs3DVolume] = useState( - is3DViewport({ viewportId, cornerstoneViewportService }) - ); const [hasColorbar, setHasColorbar] = useState(colorbarService.hasColorbar(viewportId)); const [colorbarPosition, setColorbarPosition] = useState( options?.location ? getPosition(options.location) : 'bottom' ); const [voiRange, setVoiRange] = useState<{ lower: number; upper: number } | undefined>(); const voiRangeRef = React.useRef<{ lower: number; upper: number } | undefined>(); + // Viewport from service; kept in state so we can subscribe to VIEWPORT_DATA_CHANGED when null and re-run effects when it becomes available + const [viewport, setViewport] = useState(() => + viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null + ); + const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D); const [opacity, setOpacityState] = useState(); const [opacityLinear, setOpacityLinearState] = useState(); const [threshold, setThresholdState] = useState(); @@ -178,6 +176,34 @@ export function useViewportRendering( ); }, [viewportDisplaySets, presets]); + // Keep viewport in state; when not available, subscribe to VIEWPORT_DATA_CHANGED so we set it when the viewport is ready + useEffect(() => { + if (!viewportId) { + setViewport(null); + return; + } + const vp = cornerstoneViewportService.getCornerstoneViewport(viewportId); + setViewport(vp ?? null); + if (vp) { + return; + } + const { unsubscribe } = cornerstoneViewportService.subscribe( + cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, + ({ viewportId: eventViewportId }) => { + if (eventViewportId !== viewportId) { + return; + } + const next = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (next) { + setViewport(next); + } + } + ); + return () => { + unsubscribe(); + }; + }, [viewportId, cornerstoneViewportService]); + // Calculate pixel value range for the active display set useEffect(() => { if (!activeDisplaySetInstanceUID) { @@ -189,17 +215,15 @@ export function useViewportRendering( return; } - const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - - if (!csViewport) { + if (!viewport) { return; } - if (!(csViewport instanceof BaseVolumeViewport)) { + if (!(viewport instanceof BaseVolumeViewport)) { return; } - const volumeIds = csViewport.getAllVolumeIds(); + const volumeIds = viewport.getAllVolumeIds(); const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID)); if (!volumeId) { @@ -207,7 +231,7 @@ export function useViewportRendering( } // only handle volume viewports for now - const imageData = csViewport.getImageData(volumeId); + const imageData = viewport.getImageData(volumeId); if (!imageData) { return; @@ -220,7 +244,7 @@ export function useViewportRendering( const range = voxelManager.getRange(); setPixelValueRange({ min: range[0], max: range[1] }); - }, [activeDisplaySetInstanceUID, displaySetService, cornerstoneViewportService, viewportId]); + }, [activeDisplaySetInstanceUID, displaySetService, viewport]); // Get the presets specifically for the active display set const activeDisplaySetPresets = useMemo(() => { @@ -240,55 +264,47 @@ export function useViewportRendering( }, [allWindowLevelPresets, activeDisplaySetInstanceUID]); useEffect(() => { - setIs3DVolume(is3DViewport({ viewportId, cornerstoneViewportService })); + setIs3DVolume(viewport instanceof VolumeViewport3D); - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - - // Initialize the VOI range from the viewport - if (viewport && activeDisplaySetInstanceUID) { - try { - let properties; - - if (viewport instanceof StackViewport) { - properties = viewport.getProperties(); - if (properties.voiRange) { - setVoiRange(properties.voiRange); - voiRangeRef.current = properties.voiRange; - } - } else if (viewport instanceof BaseVolumeViewport) { - // For volume viewports, find the actor for the active display set - const volumeIds = viewport.getAllVolumeIds(); - const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID)); - - if (volumeId) { - properties = viewport.getProperties(volumeId); - if (properties?.voiRange) { - setVoiRange(properties.voiRange); - voiRangeRef.current = properties.voiRange; - } - - // Get opacity from colormap if available - if (properties?.colormap?.opacity !== undefined) { - const isArray = Array.isArray(properties.colormap.opacity); - const opacity = isArray - ? properties.colormap.opacity.reduce((max, current) => Math.max(max, current), 0) - : properties.colormap.opacity; - - setOpacityState(opacity); - setOpacityLinearState(opacityToLinear(opacity)); - } - - // Get threshold from colormap if available - if (properties?.colormap && properties.colormap.threshold !== undefined) { - setThresholdState(properties.colormap.threshold); - } - } - } - } catch (error) { - console.error('Error initializing VOI range:', error); - } + if (!viewport || !activeDisplaySetInstanceUID) { + return; } - }, [cornerstoneViewportService, viewportId, activeDisplaySetInstanceUID]); + try { + const dataId = getDataIdForViewport(viewport as unknown, activeDisplaySetInstanceUID); + + const properties = + dataId != null + ? (viewport as Types.IBaseVolumeViewport).getProperties(dataId) + : viewport.getProperties(); + + if (!properties) { + return; + } + + if (properties.voiRange) { + setVoiRange(properties.voiRange); + voiRangeRef.current = properties.voiRange; + } + + if (properties.colormap?.opacity !== undefined) { + const opacityVal = properties.colormap.opacity; + const opacity = Array.isArray(opacityVal) + ? (opacityVal as unknown as number[]).reduce( + (max, current) => Math.max(max, current), + 0 + ) + : opacityVal; + setOpacityState(opacity); + setOpacityLinearState(opacityToLinear(opacity)); + } + + if (properties.colormap?.threshold !== undefined) { + setThresholdState(properties.colormap.threshold); + } + } catch (error) { + console.error('Error initializing VOI range:', error); + } + }, [activeDisplaySetInstanceUID, viewport]); useEffect(() => { if (!viewportId) { @@ -313,19 +329,11 @@ export function useViewportRendering( }, [colorbarService, viewportId]); useEffect(() => { - if (!viewportId || !activeDisplaySetInstanceUID) { - return; - } - - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport) { + if (!activeDisplaySetInstanceUID || !viewport?.element) { return; } const element = viewport.element; - if (!element) { - return; - } const updateVOI = eventDetail => { const { range } = eventDetail.detail; @@ -369,7 +377,7 @@ export function useViewportRendering( element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI); element.removeEventListener(Enums.Events.COLORMAP_MODIFIED, updateColormap); }; - }, [viewportId, activeDisplaySetInstanceUID, cornerstoneViewportService, opacityToLinear]); + }, [activeDisplaySetInstanceUID, viewport, opacityToLinear]); const validateActiveDisplaySet = useCallback(() => { if (!activeDisplaySetInstanceUID) { @@ -560,16 +568,11 @@ export function useViewportRendering( context: 'CORNERSTONE', }); }, - [commandsManager, viewportId, validateActiveDisplaySet] + [commandsManager, validateActiveDisplaySet, viewport, viewportId] ); const setOpacity = useCallback( (opacityValue: number) => { - if (!viewportId) { - return; - } - - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (!viewport || !(viewport instanceof BaseVolumeViewport)) { return; } @@ -607,7 +610,7 @@ export function useViewportRendering( viewport.render(); }, - [cornerstoneViewportService, viewportId, validateActiveDisplaySet, opacityToLinear] + [validateActiveDisplaySet, opacityToLinear, viewport] ); const setOpacityLinear = useCallback( @@ -621,11 +624,6 @@ export function useViewportRendering( const setThreshold = useCallback( (thresholdValue: number) => { - if (!viewportId) { - return; - } - - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (!viewport || !(viewport instanceof BaseVolumeViewport)) { return; } @@ -653,18 +651,16 @@ export function useViewportRendering( viewport.render(); }, - [cornerstoneViewportService, viewportId, validateActiveDisplaySet] + [validateActiveDisplaySet, viewport] ); // Get the current colormap for the active display set const colormap = useMemo(() => { - if (!viewportId || !activeDisplaySetInstanceUID || !viewportDisplaySets?.length) { + if (!activeDisplaySetInstanceUID || !viewportDisplaySets?.length) { return null; } try { - const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport) { return null; } @@ -711,13 +707,7 @@ export function useViewportRendering( colorbarProperties?.colormaps?.[0] ); } - }, [ - cornerstoneViewportService, - viewportId, - activeDisplaySetInstanceUID, - viewportDisplaySets, - colorbarProperties?.colormaps, - ]); + }, [activeDisplaySetInstanceUID, viewportDisplaySets, colorbarProperties?.colormaps, viewport]); // 3D volume rendering functions const setVolumeRenderingPreset = useCallback( @@ -801,7 +791,9 @@ export function useViewportRendering( setWindowLevel, setVOIRange, voiRange, - windowLevel: utilities.windowLevel.toWindowLevel(voiRange?.lower, voiRange?.upper), + windowLevel: voiRange + ? utilities.windowLevel.toWindowLevel(voiRange?.lower, voiRange?.upper) + : { windowCenter: null, windowWidth: null }, // Colorbar functions hasColorbar, diff --git a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts index 638a7132f..7e1705631 100644 --- a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts +++ b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts @@ -1,6 +1,7 @@ import { PubSubService, Types as OhifTypes } from '@ohif/core'; import { RENDERING_ENGINE_ID } from '../ViewportService/constants'; import { getRenderingEngine } from '@cornerstonejs/core'; +import { getDataIdForViewport } from '../../utils/getDataIdForViewport'; import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar'; export default class ColorbarService extends PubSubService { @@ -38,25 +39,6 @@ export default class ColorbarService extends PubSubService { this.servicesManager = servicesManager; } - /** - * Gets the appropriate data ID for a viewport and display set - * @param viewport - The viewport instance - * @param displaySetInstanceUID - The display set instance UID to identify data - * @returns The appropriate data ID for the viewport type (volumeId for volume viewports, undefined for stack) - */ - private getDataIdForViewport(viewport, displaySetInstanceUID: string): string | undefined { - // For volume viewports, find the matching volumeId - if (viewport.getAllVolumeIds) { - const volumeIds = viewport.getAllVolumeIds() || []; - return volumeIds.length > 0 - ? volumeIds.find(id => id.includes(displaySetInstanceUID)) || undefined - : undefined; - } - - // For other viewports, no specific dataId is needed for now - return undefined; - } - /** * Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`. * This method prepares the colorbar state that will be used by the ViewportColorbarsContainer component. @@ -92,7 +74,7 @@ export default class ColorbarService extends PubSubService { return; } - const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID); + const dataId = getDataIdForViewport(viewport, displaySetInstanceUID); const properties = dataId ? viewport.getProperties(dataId) : viewport.getProperties(); const colormap = properties?.colormap; @@ -246,7 +228,7 @@ export default class ColorbarService extends PubSubService { } // Get the appropriate dataId for this viewport/displaySet combination - const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID); + const dataId = getDataIdForViewport(viewport, displaySetInstanceUID); // Set properties with or without dataId based on what the viewport supports viewport.setProperties({ colormap }, dataId); diff --git a/extensions/cornerstone/src/utils/getDataIdForViewport.ts b/extensions/cornerstone/src/utils/getDataIdForViewport.ts new file mode 100644 index 000000000..3f9111c14 --- /dev/null +++ b/extensions/cornerstone/src/utils/getDataIdForViewport.ts @@ -0,0 +1,22 @@ +/** + * Resolves the data ID (e.g. volumeId) for a viewport and display set. + * For viewports with multiple volumes/actors, returns the id that matches the display set; otherwise undefined. + * Use this to call viewport.getProperties(dataId) in a viewport-type-agnostic way. + * + * @param viewport - Viewport instance (stack, volume, or future types with optional getAllVolumeIds) + * @param displaySetInstanceUID - Display set instance UID to match + * @returns volumeId (or equivalent) for multi-actor viewports, undefined for single-actor + */ +export function getDataIdForViewport( + viewport: unknown, + displaySetInstanceUID: string +): string | undefined { + const vp = viewport as { getAllVolumeIds?: () => string[] }; + if (typeof vp.getAllVolumeIds !== 'function') { + return undefined; + } + const volumeIds = vp.getAllVolumeIds() || []; + return volumeIds.length > 0 + ? volumeIds.find(id => id.includes(displaySetInstanceUID)) ?? undefined + : undefined; +} diff --git a/tests/MeasurementPanel.spec.ts b/tests/MeasurementPanel.spec.ts index 6b686fa7c..86de99424 100644 --- a/tests/MeasurementPanel.spec.ts +++ b/tests/MeasurementPanel.spec.ts @@ -117,7 +117,7 @@ test('checks if image would jump when clicked on a measurement item', async ({ rightPanelPageObject, viewportPageObject, }) => { - const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight; + const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight.instanceNumber; // Image loads on slice 1, confirm on slice 1 then add measurement await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 }); diff --git a/tests/SegmentationPanel.spec.ts b/tests/SegmentationPanel.spec.ts index 98ede3f90..046f46bb2 100644 --- a/tests/SegmentationPanel.spec.ts +++ b/tests/SegmentationPanel.spec.ts @@ -44,7 +44,7 @@ test('checks saved segmentations loads and jumps to slices', async ({ rightPanelPageObject, viewportPageObject, }) => { - const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight; + const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight.instanceNumber; // Image loads on slice 1, confirm on slice 1 await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 }); diff --git a/tests/WindowLevelOverlayText.spec.ts b/tests/WindowLevelOverlayText.spec.ts new file mode 100644 index 000000000..2a7145d6d --- /dev/null +++ b/tests/WindowLevelOverlayText.spec.ts @@ -0,0 +1,40 @@ +import { expect, test, visitStudy } from './utils'; + +test('should display the window level text as an overlay in various viewport layouts', async ({ + page, + mainToolbarPageObject, + viewportPageObject, +}) => { + const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1'; + const mode = 'viewer'; + await visitStudy(page, studyInstanceUID, mode, 2000); + + await expect(viewportPageObject.getNth(0).overlayText.bottomLeft.windowLevel).toBeVisible(); + + await mainToolbarPageObject.layoutSelection.click(); + await page.getByTestId('Layout-3-0').click(); + + for (let i = 0; i < 3; i++) { + await expect(viewportPageObject.getNth(i).overlayText.bottomLeft.windowLevel).toBeVisible(); + } + + await mainToolbarPageObject.layoutSelection.click(); + await page.getByTestId('Layout-1-1').click(); + + for (let i = 0; i < 3; i++) { + await expect(viewportPageObject.getNth(i).overlayText.bottomLeft.windowLevel).toBeVisible(); + } +}); + +test('should display the window level text as an overlay in the various TMTV viewports', async ({ + page, + viewportPageObject, +}) => { + const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339'; + const mode = 'tmtv'; + await visitStudy(page, studyInstanceUID, mode, 2000); + + for (let i = 0; i < 9; i++) { + await expect(viewportPageObject.getNth(i).overlayText.bottomLeft.windowLevel).toBeVisible(); + } +}); diff --git a/tests/pages/ViewportPageObject.ts b/tests/pages/ViewportPageObject.ts index 138ac6cf8..690b7eea6 100644 --- a/tests/pages/ViewportPageObject.ts +++ b/tests/pages/ViewportPageObject.ts @@ -17,6 +17,23 @@ type NormalizedDragParams = { config?: { button?: 'left' | 'right' | 'middle'; delay?: number; steps?: number }; }; +export interface IOverlayText { + get windowLevel(): Locator; + get instanceNumber(): Locator; +} + +function overlayTextFactory(viewport: Locator, id: string): IOverlayText { + const locator = viewport.getByTestId(id); + return { + get windowLevel() { + return locator.getByTitle('Window Level'); + }, + get instanceNumber() { + return locator.getByTitle('Instance Number'); + }, + }; +} + export interface IViewportPageObject { nthAnnotation(nth: number): { locator: Locator; @@ -46,10 +63,10 @@ export interface IViewportPageObject { bottomMid: Locator; }; overlayText: { - topLeft: Locator; - topRight: Locator; - bottomLeft: Locator; - bottomRight: Locator; + topLeft: IOverlayText; + topRight: IOverlayText; + bottomLeft: IOverlayText; + bottomRight: IOverlayText; }; overlayMenu: { dataOverlay: DataOverlayPageObject; @@ -122,10 +139,10 @@ export class ViewportPageObject { private getOverlayText(viewport: Locator) { return { - topLeft: viewport.getByTestId('viewport-overlay-top-left'), - topRight: viewport.getByTestId('viewport-overlay-top-right'), - bottomLeft: viewport.getByTestId('viewport-overlay-bottom-left'), - bottomRight: viewport.getByTestId('viewport-overlay-bottom-right'), + topLeft: overlayTextFactory(viewport, 'viewport-overlay-top-left'), + topRight: overlayTextFactory(viewport, 'viewport-overlay-top-right'), + bottomLeft: overlayTextFactory(viewport, 'viewport-overlay-bottom-left'), + bottomRight: overlayTextFactory(viewport, 'viewport-overlay-bottom-right'), }; }