* chore(tests): Update multiple screenshot test images for various specs * feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts * fix(DICOMSRDisplayTool): Improve actor presence check in viewport * chore(tests): Update multiple screenshot assets for various specs * chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability * chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs * test: update progressive loading screenshots * jest 30 test fixes for compatibility with pnpm cs3d * Use correct setDisplaySets instead of setDataId * fix: Naming change for LegacyVolumeViewport3D * Update to allow tolerance for contour tests * update * fix * refactor: Replace instanceof checks with utility functions for viewport type validation * fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully * bun lock * fix: Install cs3d with pnpm instead of bun * Update node version for playwright * Update to v5.0.0 of cs3d * fix: Build dependency * audit * Change to a web await retry assert * Fix timing related test failures * fix: Freehand close --------- Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
387 lines
13 KiB
TypeScript
387 lines
13 KiB
TypeScript
import OHIF, { errorHandler } from '@ohif/core';
|
|
import React from 'react';
|
|
|
|
import * as cornerstone from '@cornerstonejs/core';
|
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
|
import {
|
|
init as cs3DInit,
|
|
eventTarget,
|
|
EVENTS,
|
|
metaData,
|
|
volumeLoader,
|
|
imageLoadPoolManager,
|
|
getEnabledElement,
|
|
Settings,
|
|
utilities as csUtilities,
|
|
} from '@cornerstonejs/core';
|
|
import {
|
|
cornerstoneStreamingImageVolumeLoader,
|
|
cornerstoneStreamingDynamicImageVolumeLoader,
|
|
} from '@cornerstonejs/core/loaders';
|
|
|
|
import RequestTypes from '@cornerstonejs/core/enums/RequestType';
|
|
|
|
import initWADOImageLoader from './initWADOImageLoader';
|
|
import initCornerstoneTools from './initCornerstoneTools';
|
|
|
|
import { connectToolsToMeasurementService } from './initMeasurementService';
|
|
import initCineService from './initCineService';
|
|
import initStudyPrefetcherService from './initStudyPrefetcherService';
|
|
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
|
import nthLoader from './utils/nthLoader';
|
|
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
|
import initContextMenu from './initContextMenu';
|
|
import initDoubleClick from './initDoubleClick';
|
|
import initViewTiming from './utils/initViewTiming';
|
|
import { colormaps } from './utils/colormaps';
|
|
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
|
import { useLutPresentationStore } from './stores/useLutPresentationStore';
|
|
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
|
|
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
|
|
import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities';
|
|
import { initializeWebWorkerProgressHandler } from './utils/initWebWorkerProgressHandler';
|
|
|
|
const { registerColormap } = csUtilities.colormap;
|
|
|
|
// TODO: Cypress tests are currently grabbing this from the window?
|
|
(window as any).cornerstone = cornerstone;
|
|
(window as any).cornerstoneTools = cornerstoneTools;
|
|
/**
|
|
*
|
|
*/
|
|
export default async function init({
|
|
servicesManager,
|
|
commandsManager,
|
|
extensionManager,
|
|
appConfig,
|
|
}: withAppTypes): Promise<void> {
|
|
// Use a public library path of PUBLIC_URL plus the component name
|
|
// This safely separates components that are loaded as-is.
|
|
window.PUBLIC_LIB_URL ||= './${component}/';
|
|
|
|
// Note: this should run first before initializing the cornerstone
|
|
// DO NOT CHANGE THE ORDER
|
|
|
|
await cs3DInit({
|
|
peerImport: appConfig.peerImport,
|
|
});
|
|
|
|
// For debugging e2e tests that are failing on CI
|
|
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
|
|
|
|
cornerstone.setConfiguration({
|
|
...cornerstone.getConfiguration(),
|
|
rendering: {
|
|
...cornerstone.getConfiguration().rendering,
|
|
strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport,
|
|
// Opt-in: route legacy viewport types through the new GenericViewport render
|
|
// paths while keeping the legacy public API via compatibility adapters.
|
|
// No-op on cornerstone builds that predate the GenericViewport architecture.
|
|
useGenericViewport: Boolean(appConfig.useGenericViewport),
|
|
},
|
|
});
|
|
|
|
// For debugging large datasets, otherwise prefer the defaults
|
|
const { maxCacheSize } = appConfig;
|
|
if (maxCacheSize) {
|
|
cornerstone.cache.setMaxCacheSize(maxCacheSize);
|
|
}
|
|
|
|
initCornerstoneTools();
|
|
|
|
Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors));
|
|
|
|
const {
|
|
userAuthenticationService,
|
|
customizationService,
|
|
uiModalService,
|
|
uiNotificationService,
|
|
cornerstoneViewportService,
|
|
hangingProtocolService,
|
|
viewportGridService,
|
|
segmentationService,
|
|
measurementService,
|
|
colorbarService,
|
|
displaySetService,
|
|
toolbarService,
|
|
} = servicesManager.services;
|
|
|
|
toolbarService.registerEventForToolbarUpdate(colorbarService, [
|
|
colorbarService.EVENTS.STATE_CHANGED,
|
|
]);
|
|
|
|
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
|
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
|
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
|
segmentationService.EVENTS.SEGMENTATION_ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
|
|
]);
|
|
|
|
window.services = servicesManager.services;
|
|
window.extensionManager = extensionManager;
|
|
window.commandsManager = commandsManager;
|
|
|
|
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
|
|
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
|
}
|
|
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
|
|
|
|
const { getPresentationId: getSegmentationPresentationId } =
|
|
useSegmentationPresentationStore.getState();
|
|
|
|
const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
|
|
|
|
// register presentation id providers
|
|
viewportGridService.addPresentationIdProvider(
|
|
'positionPresentationId',
|
|
getPositionPresentationId
|
|
);
|
|
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
|
|
viewportGridService.addPresentationIdProvider(
|
|
'segmentationPresentationId',
|
|
getSegmentationPresentationId
|
|
);
|
|
|
|
segmentationService.setStyle(
|
|
{ type: SegmentationRepresentations.Contour },
|
|
{
|
|
// Declare these alpha values at the Contour type level so that they can be set/changed/inherited for all contour segmentations.
|
|
fillAlpha: 0.5,
|
|
fillAlphaInactive: 0.4,
|
|
|
|
// In general do not fill contours so that hydrated RTSTRUCTs are not filled in when active or inactive by default.
|
|
// However, hydrated RTSTRUCTs are filled in when active or inactive if the user chooses to fill ALL contours.
|
|
// Those Contours created in OHIF (i.e. using the Segmentation Panel) will override both fill properties upon creation.
|
|
renderFill: false,
|
|
renderFillInactive: false,
|
|
}
|
|
);
|
|
|
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
|
|
|
volumeLoader.registerVolumeLoader(
|
|
'cornerstoneStreamingImageVolume',
|
|
cornerstoneStreamingImageVolumeLoader
|
|
);
|
|
|
|
volumeLoader.registerVolumeLoader(
|
|
'cornerstoneStreamingDynamicImageVolume',
|
|
cornerstoneStreamingDynamicImageVolumeLoader
|
|
);
|
|
|
|
// Register strategies using the wrapper
|
|
const imageLoadStrategies = {
|
|
interleaveCenter: interleaveCenterLoader,
|
|
interleaveTopToBottom: interleaveTopToBottom,
|
|
nth: nthLoader,
|
|
};
|
|
|
|
Object.entries(imageLoadStrategies).forEach(([name, strategyFn]) => {
|
|
hangingProtocolService.registerImageLoadStrategy(
|
|
name,
|
|
createMetadataWrappedStrategy(strategyFn)
|
|
);
|
|
});
|
|
|
|
// These are set reasonably low to allow for interleaved retrieves and slower
|
|
// connections.
|
|
imageLoadPoolManager.maxNumRequests = {
|
|
[RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
|
|
[RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
|
|
[RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
|
|
[RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
|
|
};
|
|
|
|
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
|
|
|
|
// Add OHIF metadata providers after dicomImageLoader.init().
|
|
// The linked metadata branch clears providers during loader init.
|
|
metaData.addProvider(csUtilities.genericMetadataProvider.get, 9998);
|
|
metaData.addProvider(
|
|
csUtilities.calibratedPixelSpacingMetadataProvider.get.bind(
|
|
csUtilities.calibratedPixelSpacingMetadataProvider
|
|
)
|
|
); // this provider is required for Calibration tool
|
|
metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
|
|
|
/* Measurement Service */
|
|
this.measurementServiceSource = connectToolsToMeasurementService({
|
|
servicesManager,
|
|
commandsManager,
|
|
extensionManager,
|
|
});
|
|
|
|
initCineService(servicesManager);
|
|
initStudyPrefetcherService(servicesManager);
|
|
|
|
measurementService.subscribe(measurementService.EVENTS.JUMP_TO_MEASUREMENT, evt => {
|
|
const { measurement } = evt;
|
|
const { uid: annotationUID } = measurement;
|
|
commandsManager.runCommand('jumpToMeasurementViewport', { measurement, annotationUID, evt });
|
|
});
|
|
|
|
// When a custom image load is performed, update the relevant viewports
|
|
hangingProtocolService.subscribe(
|
|
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
|
volumeInputArrayMap => {
|
|
const { lutPresentationStore } = useLutPresentationStore.getState();
|
|
const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
|
|
const { positionPresentationStore } = usePositionPresentationStore.getState();
|
|
|
|
for (const entry of volumeInputArrayMap.entries()) {
|
|
const [viewportId, volumeInputArray] = entry;
|
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
|
|
|
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
|
|
|
const { presentationIds } = ohifViewport.getViewportOptions();
|
|
|
|
const presentations = {
|
|
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
|
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
|
segmentationPresentation:
|
|
segmentationPresentationStore[presentationIds?.segmentationPresentationId],
|
|
};
|
|
|
|
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
|
}
|
|
}
|
|
);
|
|
|
|
initContextMenu({
|
|
cornerstoneViewportService,
|
|
customizationService,
|
|
commandsManager,
|
|
});
|
|
|
|
initDoubleClick({
|
|
customizationService,
|
|
commandsManager,
|
|
});
|
|
|
|
/**
|
|
* Runs error handler for failed requests.
|
|
* @param event
|
|
*/
|
|
const imageLoadFailedHandler = ({ detail }) => {
|
|
const handler = errorHandler.getHTTPErrorHandler();
|
|
handler(detail.error);
|
|
};
|
|
|
|
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
|
|
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
|
|
|
|
const getDisplaySetFromVolumeId = (volumeId: string) => {
|
|
const allDisplaySets = displaySetService.getActiveDisplaySets();
|
|
const volume = cornerstone.cache.getVolume(volumeId);
|
|
const imageIds = volume.imageIds;
|
|
return allDisplaySets.find(ds => ds.imageIds?.some(id => imageIds.includes(id)));
|
|
};
|
|
|
|
function elementEnabledHandler(evt) {
|
|
const { element } = evt.detail;
|
|
const { viewport } = getEnabledElement(element);
|
|
initViewTiming({ element });
|
|
|
|
element.addEventListener(EVENTS.CAMERA_RESET, evt => {
|
|
const { element } = evt.detail;
|
|
const enabledElement = getEnabledElement(element);
|
|
if (!enabledElement) {
|
|
return;
|
|
}
|
|
const { viewportId } = enabledElement;
|
|
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
|
});
|
|
|
|
// limitation: currently supporting only volume viewports with fusion
|
|
if (viewport.type !== cornerstone.Enums.ViewportType.ORTHOGRAPHIC) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
|
|
|
colormaps.forEach(registerColormap);
|
|
|
|
// Event listener
|
|
eventTarget.addEventListenerDebounced(
|
|
EVENTS.ERROR_EVENT,
|
|
({ detail }) => {
|
|
// Create a stable ID for deduplication based on error type and message
|
|
const errorId = `cornerstone-error-${detail.type}-${detail.message.substring(0, 50)}`;
|
|
|
|
uiNotificationService.show({
|
|
title: detail.type,
|
|
message: detail.message,
|
|
type: 'error',
|
|
id: errorId,
|
|
allowDuplicates: false, // Prevent duplicate error notifications
|
|
deduplicationInterval: 30000, // 30 seconds deduplication window
|
|
});
|
|
},
|
|
100
|
|
);
|
|
|
|
// Subscribe to actor events to dynamically update colorbars
|
|
|
|
// Call this function when initializing
|
|
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
|
|
}
|
|
|
|
/**
|
|
* Creates a wrapped image load strategy with metadata handling
|
|
* @param strategyFn - The image loading strategy function to wrap
|
|
* @returns A wrapped strategy function that handles metadata configuration
|
|
*/
|
|
const createMetadataWrappedStrategy = (strategyFn: (args: any) => any) => {
|
|
return (args: any) => {
|
|
const clonedConfig = imageRetrieveMetadataProvider.clone();
|
|
imageRetrieveMetadataProvider.clear();
|
|
|
|
try {
|
|
const result = strategyFn(args);
|
|
return result;
|
|
} finally {
|
|
// Ensure metadata is always restored, even if there's an error
|
|
setTimeout(() => {
|
|
imageRetrieveMetadataProvider.restore(clonedConfig);
|
|
}, 10);
|
|
}
|
|
};
|
|
};
|
|
|
|
function CPUModal() {
|
|
return (
|
|
<div>
|
|
<p>
|
|
Your computer does not have enough GPU power to support the default GPU rendering mode. OHIF
|
|
has switched to CPU rendering mode. Please note that CPU rendering does not support all
|
|
features such as Volume Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function _showCPURenderingModal(uiModalService, hangingProtocolService) {
|
|
const callback = progress => {
|
|
if (progress === 100) {
|
|
uiModalService.show({
|
|
content: CPUModal,
|
|
title: 'OHIF Fell Back to CPU Rendering',
|
|
});
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
const { unsubscribe } = hangingProtocolService.subscribe(
|
|
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
|
() => {
|
|
const done = callback(100);
|
|
|
|
if (done) {
|
|
unsubscribe();
|
|
}
|
|
}
|
|
);
|
|
}
|