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:
1 parent
54d9ea39bf
commit
fe1ecfe0cc
9 files changed
+188
-153
No files matched your search
@@ -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) {
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: customization?.color }}
|
||||
title={title}
|
||||
>
|
||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
|
||||
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||
@@ -443,11 +421,13 @@ function InstanceNumberOverlayItem({
|
||||
customization,
|
||||
}: OverlayItemProps) {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
const { title } = customization;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
title={title}
|
||||
>
|
||||
<span>
|
||||
{instanceNumber !== undefined && instanceNumber !== null ? (
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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<ColorbarPositionType>(
|
||||
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<Types.IViewport | null>(() =>
|
||||
viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null
|
||||
);
|
||||
const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D);
|
||||
const [opacity, setOpacityState] = useState<number | undefined>();
|
||||
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
|
||||
const [threshold, setThresholdState] = useState<number | undefined>();
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user