fix(viewport): Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings (#4037)
This commit is contained in:
1 parent
a6c6fff43a
commit
f99a0bfb31
80 files changed
+1129
-801
No files matched your search
@@ -114,10 +114,21 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
initialImageIndex,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
isHangingProtocolLayout,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
if (!viewportId) {
|
||||
throw new Error('Viewport ID is required');
|
||||
}
|
||||
|
||||
// Since we only have support for dynamic data in volume viewports, we should
|
||||
// handle this case here and set the viewportType to volume if any of the
|
||||
// displaySets are dynamic volumes
|
||||
@@ -194,7 +205,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
const synchronizersStore = stateSyncService.getState().synchronizersStore;
|
||||
|
||||
if (synchronizersStore?.[viewportId]?.length) {
|
||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||
// If the viewport used to have a synchronizer, re apply it again
|
||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ function commandsModule({
|
||||
function _getActiveViewportEnabledElement() {
|
||||
return getActiveViewportEnabledElement(viewportGridService);
|
||||
}
|
||||
|
||||
function _getActiveViewportToolGroupId() {
|
||||
const viewport = _getActiveViewportEnabledElement();
|
||||
return toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Generates the selector props for the context menu, specific to
|
||||
@@ -300,42 +306,45 @@ function commandsModule({
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
});
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
},
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active;
|
||||
const toolIsActive = [
|
||||
Enums.ToolModes.Active,
|
||||
Enums.ToolModes.Enabled,
|
||||
Enums.ToolModes.Passive,
|
||||
].includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
if (prevToolName !== toolName) {
|
||||
actions.setToolActive({ toolName: prevToolName, toolGroupId });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
@@ -441,11 +450,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
},
|
||||
flipViewportVertical: () => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@@ -456,11 +463,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
},
|
||||
invertViewport: ({ element }) => {
|
||||
let enabledElement;
|
||||
@@ -819,7 +824,7 @@ function commandsModule({
|
||||
}
|
||||
|
||||
crosshairInstances.forEach(ins => {
|
||||
ins.resetCrosshairs();
|
||||
ins?.resetCrosshairs();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
return isPlaying
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
|
||||
: cineService.stopClip(enabledVPElement);
|
||||
};
|
||||
|
||||
|
||||
@@ -108,32 +108,27 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle',
|
||||
evaluate: ({ viewportId, button, disabledText }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
|
||||
toolGroup.getToolOptions(toolName).mode
|
||||
);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
},
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.synchronizer',
|
||||
@@ -146,7 +141,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
}
|
||||
|
||||
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
|
||||
const isArray = Array.isArray(button.commands);
|
||||
|
||||
const synchronizerType = isArray
|
||||
? button.commands?.[0].commandOptions.type
|
||||
: button.commands?.commandOptions.type;
|
||||
|
||||
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
|
||||
|
||||
@@ -268,18 +267,38 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
];
|
||||
}
|
||||
|
||||
function _evaluateToggle({ viewportId, button, disabledText, offModes, toolGroupService }) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: this is duplicate, we should move it to a shared location
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||
const firstCommand = commandsArray[0];
|
||||
if (typeof firstCommand === 'string') {
|
||||
// likely not a cornerstone tool
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('commandOptions' in firstCommand) {
|
||||
if (firstCommand?.commandOptions) {
|
||||
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeExit: (): void => {
|
||||
onModeExit: ({ servicesManager }): void => {
|
||||
const { cineService } = servicesManager.services;
|
||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@@ -69,6 +70,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
cineService.setIsCineEnabled(false);
|
||||
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import { CornerstoneServices } from './types';
|
||||
import initViewTiming from './utils/initViewTiming';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const { registerColormap } = csUtilities.colormap;
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
@@ -291,8 +293,21 @@ export default async function init({
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
|
||||
colormaps.forEach(registerColormap);
|
||||
|
||||
// Create a debounced function that shows the notification
|
||||
const debouncedShowNotification = debounce(detail => {
|
||||
uiNotificationService.show({
|
||||
title: detail.type,
|
||||
message: detail.message,
|
||||
type: 'error',
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// Event listener
|
||||
eventTarget.addEventListener(EVENTS.ERROR_EVENT, ({ detail }) => {
|
||||
debouncedShowNotification(detail);
|
||||
});
|
||||
}
|
||||
|
||||
function CPUModal() {
|
||||
|
||||
@@ -59,8 +59,8 @@ function initCineService(servicesManager) {
|
||||
return utilities.cine.playClip(element, playClipOptions);
|
||||
};
|
||||
|
||||
const stopClip = element => {
|
||||
return utilities.cine.stopClip(element);
|
||||
const stopClip = (element, stopClipOptions) => {
|
||||
return utilities.cine.stopClip(element, stopClipOptions);
|
||||
};
|
||||
|
||||
cineService.setServiceImplementation({
|
||||
|
||||
+5
-1
@@ -45,7 +45,11 @@ class CornerstoneCacheService {
|
||||
// as a reference volume, if so, we should hang a volume viewport
|
||||
// instead of a stack viewport
|
||||
if (this._shouldRenderSegmentation(displaySets)) {
|
||||
viewportType = 'volume';
|
||||
// if the viewport type is volume 3D, we should let it be as it is
|
||||
// Todo: in future here we should kick start the conversion of the
|
||||
// segmentation to closed surface
|
||||
viewportType =
|
||||
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
|
||||
|
||||
// update viewportOptions to reflect the new viewport type
|
||||
viewportOptions.viewportType = viewportType;
|
||||
|
||||
@@ -183,7 +183,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const { lutPresentation, positionPresentation } = presentations;
|
||||
if (lutPresentation) {
|
||||
const { presentation } = lutPresentation;
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
if (viewport instanceof BaseVolumeViewport) {
|
||||
if (presentation instanceof Map) {
|
||||
presentation.forEach((properties, volumeId) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
|
||||
@@ -27,7 +27,11 @@ const CornerstoneViewportDownloadForm = ({
|
||||
const activeViewportElement = enabledElement?.element;
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
|
||||
const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement;
|
||||
const {
|
||||
viewportId: activeViewportId,
|
||||
renderingEngineId,
|
||||
viewport: activeViewport,
|
||||
} = activeViewportEnabledElement;
|
||||
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
|
||||
|
||||
@@ -93,7 +97,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
renderingEngine.resize();
|
||||
|
||||
// Trigger the render on the viewport to update the on screen
|
||||
downloadViewport.resetCamera();
|
||||
// downloadViewport.resetCamera();
|
||||
downloadViewport.render();
|
||||
|
||||
downloadViewportElement.addEventListener(
|
||||
@@ -120,6 +124,12 @@ const CornerstoneViewportDownloadForm = ({
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport);
|
||||
|
||||
// for some reason we need a reset camera here, and I don't know why
|
||||
downloadViewport.resetCamera();
|
||||
const presentation = activeViewport.getViewPresentation();
|
||||
downloadViewport.setView(activeViewport.getViewReference(), presentation);
|
||||
downloadViewport.render();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -161,7 +171,6 @@ const CornerstoneViewportDownloadForm = ({
|
||||
downloadViewport.addActor(actor);
|
||||
});
|
||||
|
||||
downloadViewport.setCamera(viewport.getCamera());
|
||||
downloadViewport.render();
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
|
||||
@@ -51,20 +51,27 @@ export default function interleaveCenterLoader({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||
const uniqueViewportVolumeDisplaySetUIDs = new Set();
|
||||
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
|
||||
volumeInputArray.forEach(volumeInput => {
|
||||
const { volumeId } = volumeInput;
|
||||
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueMatchedDisplaySetUIDs = new Set();
|
||||
|
||||
matchDetails.forEach(matchDetail => {
|
||||
const { displaySetsInfo } = matchDetail;
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
|
||||
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
|
||||
});
|
||||
});
|
||||
|
||||
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all the matched volumes are loaded
|
||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||
const { SeriesInstanceUID } = details;
|
||||
|
||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||
// get volumes from cache
|
||||
const volumes = volumeIds.map(volumeId => {
|
||||
|
||||
@@ -75,23 +75,27 @@ export default function interleaveTopToBottom({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||
const uniqueViewportVolumeDisplaySetUIDs = new Set();
|
||||
viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => {
|
||||
volumeInputArray.forEach(volumeInput => {
|
||||
const { volumeId } = volumeInput;
|
||||
uniqueViewportVolumeDisplaySetUIDs.add(volumeId);
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueMatchedDisplaySetUIDs = new Set();
|
||||
|
||||
matchDetails.forEach(matchDetail => {
|
||||
const { displaySetsInfo } = matchDetail;
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID }) => {
|
||||
uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID);
|
||||
});
|
||||
});
|
||||
|
||||
if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all the matched volumes are loaded
|
||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||
const { SeriesInstanceUID, displaySetInstanceUID } = details;
|
||||
|
||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||
if (
|
||||
displaySetsToLoad.has(displaySetInstanceUID) &&
|
||||
!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||
// get volumes from cache
|
||||
const volumes = volumeIds.map(volumeId => {
|
||||
|
||||
Reference in new issue
Block a user