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 <wayfarer3130@gmail.com>
This commit is contained in:
parent
54d9ea39bf
commit
fe1ecfe0cc
@ -67,10 +67,9 @@ function CustomizableViewportOverlay({
|
|||||||
}) {
|
}) {
|
||||||
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
|
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
|
||||||
servicesManager.services;
|
servicesManager.services;
|
||||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
const [annotationState, setAnnotationState] = useState(0);
|
const [annotationState, setAnnotationState] = useState(0);
|
||||||
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
|
const { isViewportBackgroundLight: isLight, windowLevel: voi } = useViewportRendering(viewportId);
|
||||||
const { imageIndex } = imageSliceData;
|
const { imageIndex } = imageSliceData;
|
||||||
|
|
||||||
// Historical usage defined the overlays as separate items due to lack of
|
// Historical usage defined the overlays as separate items due to lack of
|
||||||
@ -110,30 +109,6 @@ function CustomizableViewportOverlay({
|
|||||||
};
|
};
|
||||||
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
}, [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 => {
|
const annotationModified = useCallback(evt => {
|
||||||
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
|
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
|
||||||
// Update the annotation state to trigger a re-render
|
// Update the annotation state to trigger a re-render
|
||||||
@ -226,6 +201,7 @@ function CustomizableViewportOverlay({
|
|||||||
scale,
|
scale,
|
||||||
instanceNumber,
|
instanceNumber,
|
||||||
annotationState,
|
annotationState,
|
||||||
|
isLight,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -402,6 +378,7 @@ function OverlayItem(props) {
|
|||||||
*/
|
*/
|
||||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||||
const { windowWidth, windowCenter } = voi;
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
const { title } = customization;
|
||||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -410,6 +387,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
|||||||
<div
|
<div
|
||||||
className="overlay-item flex flex-row"
|
className="overlay-item flex flex-row"
|
||||||
style={{ color: customization?.color }}
|
style={{ color: customization?.color }}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
|
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
|
||||||
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
|
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||||
@ -443,11 +421,13 @@ function InstanceNumberOverlayItem({
|
|||||||
customization,
|
customization,
|
||||||
}: OverlayItemProps) {
|
}: OverlayItemProps) {
|
||||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||||
|
const { title } = customization;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="overlay-item flex flex-row"
|
className="overlay-item flex flex-row"
|
||||||
style={{ color: (customization && customization.color) || undefined }}
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{instanceNumber !== undefined && instanceNumber !== null ? (
|
{instanceNumber !== undefined && instanceNumber !== null ? (
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'WindowLevel',
|
id: 'WindowLevel',
|
||||||
inheritsFrom: 'ohif.overlayItem.windowLevel',
|
inheritsFrom: 'ohif.overlayItem.windowLevel',
|
||||||
|
title: 'Window Level',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ZoomLevel',
|
id: 'ZoomLevel',
|
||||||
@ -39,6 +40,7 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'InstanceNumber',
|
id: 'InstanceNumber',
|
||||||
inheritsFrom: 'ohif.overlayItem.instanceNumber',
|
inheritsFrom: 'ohif.overlayItem.instanceNumber',
|
||||||
|
title: 'Instance Number',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
BaseVolumeViewport,
|
BaseVolumeViewport,
|
||||||
cache,
|
cache,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
import { getDataIdForViewport } from '../utils/getDataIdForViewport';
|
||||||
import { WindowLevelPreset } from '../types/WindowLevel';
|
import { WindowLevelPreset } from '../types/WindowLevel';
|
||||||
import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar';
|
import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar';
|
||||||
import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig';
|
import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig';
|
||||||
@ -104,11 +105,6 @@ const opacityToLinear = (opacityValue: number): number => {
|
|||||||
return Math.pow(opacityValue, 1.0 / GAMMA);
|
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
|
* Hook to access window level functionality for a specific viewport
|
||||||
*
|
*
|
||||||
@ -124,15 +120,17 @@ export function useViewportRendering(
|
|||||||
const { cornerstoneViewportService, colorbarService, customizationService } =
|
const { cornerstoneViewportService, colorbarService, customizationService } =
|
||||||
servicesManager.services;
|
servicesManager.services;
|
||||||
|
|
||||||
const [is3DVolume, setIs3DVolume] = useState(
|
|
||||||
is3DViewport({ viewportId, cornerstoneViewportService })
|
|
||||||
);
|
|
||||||
const [hasColorbar, setHasColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
const [hasColorbar, setHasColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
||||||
const [colorbarPosition, setColorbarPosition] = useState<ColorbarPositionType>(
|
const [colorbarPosition, setColorbarPosition] = useState<ColorbarPositionType>(
|
||||||
options?.location ? getPosition(options.location) : 'bottom'
|
options?.location ? getPosition(options.location) : 'bottom'
|
||||||
);
|
);
|
||||||
const [voiRange, setVoiRange] = useState<{ lower: number; upper: number } | undefined>();
|
const [voiRange, setVoiRange] = useState<{ lower: number; upper: number } | undefined>();
|
||||||
const voiRangeRef = React.useRef<{ 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<Types.IViewport | null>(() =>
|
||||||
|
viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null
|
||||||
|
);
|
||||||
|
const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D);
|
||||||
const [opacity, setOpacityState] = useState<number | undefined>();
|
const [opacity, setOpacityState] = useState<number | undefined>();
|
||||||
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
|
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
|
||||||
const [threshold, setThresholdState] = useState<number | undefined>();
|
const [threshold, setThresholdState] = useState<number | undefined>();
|
||||||
@ -178,6 +176,34 @@ export function useViewportRendering(
|
|||||||
);
|
);
|
||||||
}, [viewportDisplaySets, presets]);
|
}, [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
|
// Calculate pixel value range for the active display set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeDisplaySetInstanceUID) {
|
if (!activeDisplaySetInstanceUID) {
|
||||||
@ -189,17 +215,15 @@ export function useViewportRendering(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
if (!viewport) {
|
||||||
|
|
||||||
if (!csViewport) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(csViewport instanceof BaseVolumeViewport)) {
|
if (!(viewport instanceof BaseVolumeViewport)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const volumeIds = csViewport.getAllVolumeIds();
|
const volumeIds = viewport.getAllVolumeIds();
|
||||||
const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID));
|
const volumeId = volumeIds.find(id => id.includes(activeDisplaySetInstanceUID));
|
||||||
|
|
||||||
if (!volumeId) {
|
if (!volumeId) {
|
||||||
@ -207,7 +231,7 @@ export function useViewportRendering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only handle volume viewports for now
|
// only handle volume viewports for now
|
||||||
const imageData = csViewport.getImageData(volumeId);
|
const imageData = viewport.getImageData(volumeId);
|
||||||
|
|
||||||
if (!imageData) {
|
if (!imageData) {
|
||||||
return;
|
return;
|
||||||
@ -220,7 +244,7 @@ export function useViewportRendering(
|
|||||||
const range = voxelManager.getRange();
|
const range = voxelManager.getRange();
|
||||||
|
|
||||||
setPixelValueRange({ min: range[0], max: range[1] });
|
setPixelValueRange({ min: range[0], max: range[1] });
|
||||||
}, [activeDisplaySetInstanceUID, displaySetService, cornerstoneViewportService, viewportId]);
|
}, [activeDisplaySetInstanceUID, displaySetService, viewport]);
|
||||||
|
|
||||||
// Get the presets specifically for the active display set
|
// Get the presets specifically for the active display set
|
||||||
const activeDisplaySetPresets = useMemo(() => {
|
const activeDisplaySetPresets = useMemo(() => {
|
||||||
@ -240,55 +264,47 @@ export function useViewportRendering(
|
|||||||
}, [allWindowLevelPresets, activeDisplaySetInstanceUID]);
|
}, [allWindowLevelPresets, activeDisplaySetInstanceUID]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIs3DVolume(is3DViewport({ viewportId, cornerstoneViewportService }));
|
setIs3DVolume(viewport instanceof VolumeViewport3D);
|
||||||
|
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
if (!viewport || !activeDisplaySetInstanceUID) {
|
||||||
|
return;
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!viewportId) {
|
if (!viewportId) {
|
||||||
@ -313,19 +329,11 @@ export function useViewportRendering(
|
|||||||
}, [colorbarService, viewportId]);
|
}, [colorbarService, viewportId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!viewportId || !activeDisplaySetInstanceUID) {
|
if (!activeDisplaySetInstanceUID || !viewport?.element) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
if (!viewport) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = viewport.element;
|
const element = viewport.element;
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateVOI = eventDetail => {
|
const updateVOI = eventDetail => {
|
||||||
const { range } = eventDetail.detail;
|
const { range } = eventDetail.detail;
|
||||||
@ -369,7 +377,7 @@ export function useViewportRendering(
|
|||||||
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||||
element.removeEventListener(Enums.Events.COLORMAP_MODIFIED, updateColormap);
|
element.removeEventListener(Enums.Events.COLORMAP_MODIFIED, updateColormap);
|
||||||
};
|
};
|
||||||
}, [viewportId, activeDisplaySetInstanceUID, cornerstoneViewportService, opacityToLinear]);
|
}, [activeDisplaySetInstanceUID, viewport, opacityToLinear]);
|
||||||
|
|
||||||
const validateActiveDisplaySet = useCallback(() => {
|
const validateActiveDisplaySet = useCallback(() => {
|
||||||
if (!activeDisplaySetInstanceUID) {
|
if (!activeDisplaySetInstanceUID) {
|
||||||
@ -560,16 +568,11 @@ export function useViewportRendering(
|
|||||||
context: 'CORNERSTONE',
|
context: 'CORNERSTONE',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[commandsManager, viewportId, validateActiveDisplaySet]
|
[commandsManager, validateActiveDisplaySet, viewport, viewportId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setOpacity = useCallback(
|
const setOpacity = useCallback(
|
||||||
(opacityValue: number) => {
|
(opacityValue: number) => {
|
||||||
if (!viewportId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
|
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -607,7 +610,7 @@ export function useViewportRendering(
|
|||||||
|
|
||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
[cornerstoneViewportService, viewportId, validateActiveDisplaySet, opacityToLinear]
|
[validateActiveDisplaySet, opacityToLinear, viewport]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setOpacityLinear = useCallback(
|
const setOpacityLinear = useCallback(
|
||||||
@ -621,11 +624,6 @@ export function useViewportRendering(
|
|||||||
|
|
||||||
const setThreshold = useCallback(
|
const setThreshold = useCallback(
|
||||||
(thresholdValue: number) => {
|
(thresholdValue: number) => {
|
||||||
if (!viewportId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
|
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -653,18 +651,16 @@ export function useViewportRendering(
|
|||||||
|
|
||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
[cornerstoneViewportService, viewportId, validateActiveDisplaySet]
|
[validateActiveDisplaySet, viewport]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the current colormap for the active display set
|
// Get the current colormap for the active display set
|
||||||
const colormap = useMemo(() => {
|
const colormap = useMemo(() => {
|
||||||
if (!viewportId || !activeDisplaySetInstanceUID || !viewportDisplaySets?.length) {
|
if (!activeDisplaySetInstanceUID || !viewportDisplaySets?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
||||||
|
|
||||||
if (!viewport) {
|
if (!viewport) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -711,13 +707,7 @@ export function useViewportRendering(
|
|||||||
colorbarProperties?.colormaps?.[0]
|
colorbarProperties?.colormaps?.[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [activeDisplaySetInstanceUID, viewportDisplaySets, colorbarProperties?.colormaps, viewport]);
|
||||||
cornerstoneViewportService,
|
|
||||||
viewportId,
|
|
||||||
activeDisplaySetInstanceUID,
|
|
||||||
viewportDisplaySets,
|
|
||||||
colorbarProperties?.colormaps,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3D volume rendering functions
|
// 3D volume rendering functions
|
||||||
const setVolumeRenderingPreset = useCallback(
|
const setVolumeRenderingPreset = useCallback(
|
||||||
@ -801,7 +791,9 @@ export function useViewportRendering(
|
|||||||
setWindowLevel,
|
setWindowLevel,
|
||||||
setVOIRange,
|
setVOIRange,
|
||||||
voiRange,
|
voiRange,
|
||||||
windowLevel: utilities.windowLevel.toWindowLevel(voiRange?.lower, voiRange?.upper),
|
windowLevel: voiRange
|
||||||
|
? utilities.windowLevel.toWindowLevel(voiRange?.lower, voiRange?.upper)
|
||||||
|
: { windowCenter: null, windowWidth: null },
|
||||||
|
|
||||||
// Colorbar functions
|
// Colorbar functions
|
||||||
hasColorbar,
|
hasColorbar,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { PubSubService, Types as OhifTypes } from '@ohif/core';
|
import { PubSubService, Types as OhifTypes } from '@ohif/core';
|
||||||
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
|
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
|
||||||
import { getRenderingEngine } from '@cornerstonejs/core';
|
import { getRenderingEngine } from '@cornerstonejs/core';
|
||||||
|
import { getDataIdForViewport } from '../../utils/getDataIdForViewport';
|
||||||
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
|
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
|
||||||
|
|
||||||
export default class ColorbarService extends PubSubService {
|
export default class ColorbarService extends PubSubService {
|
||||||
@ -38,25 +39,6 @@ export default class ColorbarService extends PubSubService {
|
|||||||
this.servicesManager = servicesManager;
|
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`.
|
* 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.
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataId = this.getDataIdForViewport(viewport, displaySetInstanceUID);
|
const dataId = getDataIdForViewport(viewport, displaySetInstanceUID);
|
||||||
const properties = dataId ? viewport.getProperties(dataId) : viewport.getProperties();
|
const properties = dataId ? viewport.getProperties(dataId) : viewport.getProperties();
|
||||||
const colormap = properties?.colormap;
|
const colormap = properties?.colormap;
|
||||||
|
|
||||||
@ -246,7 +228,7 @@ export default class ColorbarService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the appropriate dataId for this viewport/displaySet combination
|
// 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
|
// Set properties with or without dataId based on what the viewport supports
|
||||||
viewport.setProperties({ colormap }, dataId);
|
viewport.setProperties({ colormap }, dataId);
|
||||||
|
|||||||
22
extensions/cornerstone/src/utils/getDataIdForViewport.ts
Normal file
22
extensions/cornerstone/src/utils/getDataIdForViewport.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -117,7 +117,7 @@ test('checks if image would jump when clicked on a measurement item', async ({
|
|||||||
rightPanelPageObject,
|
rightPanelPageObject,
|
||||||
viewportPageObject,
|
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
|
// Image loads on slice 1, confirm on slice 1 then add measurement
|
||||||
await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 });
|
await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 });
|
||||||
|
|||||||
@ -44,7 +44,7 @@ test('checks saved segmentations loads and jumps to slices', async ({
|
|||||||
rightPanelPageObject,
|
rightPanelPageObject,
|
||||||
viewportPageObject,
|
viewportPageObject,
|
||||||
}) => {
|
}) => {
|
||||||
const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight;
|
const viewportInfoBottomRight = viewportPageObject.active.overlayText.bottomRight.instanceNumber;
|
||||||
// Image loads on slice 1, confirm on slice 1
|
// Image loads on slice 1, confirm on slice 1
|
||||||
await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 });
|
await expect(viewportInfoBottomRight).toContainText('1/', { timeout: 10000 });
|
||||||
|
|
||||||
|
|||||||
40
tests/WindowLevelOverlayText.spec.ts
Normal file
40
tests/WindowLevelOverlayText.spec.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -17,6 +17,23 @@ type NormalizedDragParams = {
|
|||||||
config?: { button?: 'left' | 'right' | 'middle'; delay?: number; steps?: number };
|
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 {
|
export interface IViewportPageObject {
|
||||||
nthAnnotation(nth: number): {
|
nthAnnotation(nth: number): {
|
||||||
locator: Locator;
|
locator: Locator;
|
||||||
@ -46,10 +63,10 @@ export interface IViewportPageObject {
|
|||||||
bottomMid: Locator;
|
bottomMid: Locator;
|
||||||
};
|
};
|
||||||
overlayText: {
|
overlayText: {
|
||||||
topLeft: Locator;
|
topLeft: IOverlayText;
|
||||||
topRight: Locator;
|
topRight: IOverlayText;
|
||||||
bottomLeft: Locator;
|
bottomLeft: IOverlayText;
|
||||||
bottomRight: Locator;
|
bottomRight: IOverlayText;
|
||||||
};
|
};
|
||||||
overlayMenu: {
|
overlayMenu: {
|
||||||
dataOverlay: DataOverlayPageObject;
|
dataOverlay: DataOverlayPageObject;
|
||||||
@ -122,10 +139,10 @@ export class ViewportPageObject {
|
|||||||
|
|
||||||
private getOverlayText(viewport: Locator) {
|
private getOverlayText(viewport: Locator) {
|
||||||
return {
|
return {
|
||||||
topLeft: viewport.getByTestId('viewport-overlay-top-left'),
|
topLeft: overlayTextFactory(viewport, 'viewport-overlay-top-left'),
|
||||||
topRight: viewport.getByTestId('viewport-overlay-top-right'),
|
topRight: overlayTextFactory(viewport, 'viewport-overlay-top-right'),
|
||||||
bottomLeft: viewport.getByTestId('viewport-overlay-bottom-left'),
|
bottomLeft: overlayTextFactory(viewport, 'viewport-overlay-bottom-left'),
|
||||||
bottomRight: viewport.getByTestId('viewport-overlay-bottom-right'),
|
bottomRight: overlayTextFactory(viewport, 'viewport-overlay-bottom-right'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user