feat: add support to scoord3d (#5016)

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
Pedro KöhlerandBill Wallace authored and GitHub committed 2025-07-31 00:21:26 -04:00
1 parent af40f06524
commit 735405a855
73 files changed
+1032 -993

No files matched your search

+2 -2
View File
@@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone": "3.11.0-beta.114",
@@ -79,7 +79,7 @@
"classnames": "^2.3.2",
"core-js": "*",
"cornerstone-math": "^0.1.9",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9",
"dotenv-webpack": "^1.7.0",
+3 -35
View File
@@ -26,7 +26,7 @@ window.config = {
// filterQueryParam: false,
// Uses the ohif datasource as the default - this requires that KHEOPS be
// configured with an OHIF path to .../viewer/dicomwebproxy
defaultDataSourceName: 'dicomweb',
defaultDataSourceName: 'ohif',
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
// dangerouslyUseDynamicConfig: {
// enabled: true,
@@ -40,7 +40,7 @@ window.config = {
dataSources: [
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
sourceName: 'ohif',
configuration: {
friendlyName: 'AWS S3 Static wado server',
name: 'aws',
@@ -54,47 +54,15 @@ window.config = {
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
singlepart: 'video,pdf',
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
},
omitQuotationForMultipartRequest: true,
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'ohif2',
configuration: {
friendlyName: 'AWS S3 Static wado secondary server',
name: 'aws',
wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
},
omitQuotationForMultipartRequest: true,
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'ohif3',
+9 -140
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback, useRef } from 'react';
import { Types, MeasurementService } from '@ohif/core';
import { Types } from '@ohif/core';
import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import EmptyViewport from './EmptyViewport';
@@ -14,13 +14,8 @@ function ViewerViewportGrid(props: withAppTypes) {
const { numCols, numRows } = layout;
const layoutHash = useRef(null);
const {
displaySetService,
measurementService,
hangingProtocolService,
uiNotificationService,
customizationService,
} = servicesManager.services;
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
servicesManager.services;
const generateLayoutHash = () => `${numCols}-${numRows}`;
@@ -32,9 +27,9 @@ function ViewerViewportGrid(props: withAppTypes) {
*/
const updateDisplaySetsFromProtocol = (
protocol: Types.HangingProtocol.Protocol,
_protocol: Types.HangingProtocol.Protocol,
stage,
activeStudyUID,
_activeStudyUID,
viewportMatchDetails
) => {
const availableDisplaySets = displaySetService.getActiveDisplaySets();
@@ -154,100 +149,6 @@ function ViewerViewportGrid(props: withAppTypes) {
}
}, [viewportGridService, generateLayoutHash]);
useEffect(() => {
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
event => {
const { viewportId, measurement, isConsumed } = event;
if (isConsumed) {
return;
}
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
const { viewports } = viewportGridService.getState();
// Check if any viewport can display this measurement
let canAnyViewportDisplayMeasurement = false;
viewports.forEach((viewport, id) => {
const displaySetInstanceUIDs = viewport.displaySetInstanceUIDs || [];
const viewportHasDisplaySet = displaySetInstanceUIDs.includes(
referencedDisplaySetInstanceUID
);
// Extract metadata and prepare reference
const { FrameOfReferenceUID, ...metadataRest } = measurement.metadata;
const reference = {
...(viewportHasDisplaySet ? measurement.metadata : metadataRest),
displaySetInstanceUID: referencedDisplaySetInstanceUID,
};
// Check if viewport can display the reference
if (
viewport.isReferenceViewable?.({
viewportId: id,
reference,
})
) {
canAnyViewportDisplayMeasurement = true;
}
});
if (canAnyViewportDisplayMeasurement) {
// Let the viewports handle the jump
return;
}
// Need to change layouts since no viewport consumed the event
const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID);
if (!updatedViewports?.[0]) {
console.warn(
'ViewportGrid::Unable to navigate to viewport containing',
referencedDisplaySetInstanceUID
);
return;
}
// Find the viewport that can display the measurement
const viewport = updatedViewports.find(viewport => {
const gridViewport = viewportGridService.getViewportState(viewport.viewportId);
return gridViewport.isReferenceViewable?.({
viewportId: viewport.viewportId,
reference: {
...measurement.metadata,
displaySetInstanceUID: referencedDisplaySetInstanceUID,
},
viewportOptions: gridViewport.viewportOptions || {},
});
});
if (!viewport) {
console.warn('No suitable viewport found for displaying measurement');
return;
}
// Update stored position presentation
commandsManager.run('updateStoredPositionPresentation', {
viewportId: viewport.viewportId,
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
referencedImageId: measurement.referencedImageId,
options: {
...measurement.metadata,
},
});
event.consume();
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
}
);
return () => {
unsubscribe();
};
}, [viewports, _getUpdatedViewports]);
const onDropHandler = (viewportId, { displaySetInstanceUID }) => {
const { viewportGridService } = servicesManager.services;
const customOnDropHandler = customizationService.getCustomization('customOnDropHandler');
@@ -267,23 +168,6 @@ function ViewerViewportGrid(props: withAppTypes) {
viewportGridService.publishViewportOnDropHandled({ displaySetInstanceUID });
};
// Store previous isReferenceViewable values to avoid infinite loops
const prevReferenceViewableMap = useRef(new Map());
// Track viewports that need isReferenceViewable updates
const viewportsToUpdate = useRef(new Map());
// Apply isReferenceViewable updates in an effect, not during render
useEffect(() => {
const updates = viewportsToUpdate.current;
if (updates.size > 0) {
updates.forEach((isReferenceViewable, viewportId) => {
viewportGridService.setIsReferenceViewable(viewportId, isReferenceViewable);
prevReferenceViewableMap.current.set(viewportId, isReferenceViewable);
});
viewportsToUpdate.current.clear();
}
});
const getViewportPanes = useCallback(() => {
const viewportPanes = [];
@@ -315,27 +199,12 @@ function ViewerViewportGrid(props: withAppTypes) {
return !displaySet?.unsupported;
});
const { component: ViewportComponent, isReferenceViewable } = _getViewportComponent(
const { component: ViewportComponent } = _getViewportComponent(
displaySets,
viewportComponents,
uiNotificationService
);
// Only queue isReferenceViewable updates if it's changed to avoid render loops
// We need to handle both function and non-function values
if (viewportId) {
const prevValue = prevReferenceViewableMap.current.get(viewportId);
const isFunction = typeof isReferenceViewable === 'function';
const isSameFunction = isFunction && typeof prevValue === 'function';
// For non-functions, compare directly. For functions, we treat them as always different
// (this is conservative but safe)
if (!isSameFunction && prevValue !== isReferenceViewable) {
// Queue the update instead of doing it during render
viewportsToUpdate.current.set(viewportId, isReferenceViewable);
}
}
// look inside displaySets to see if they need reRendering
const displaySetsNeedsRerendering = displaySets.some(displaySet => {
return displaySet.needsRerendering;
@@ -462,8 +331,8 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
throw new Error('displaySetsToDisplay is null');
}
if (viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)) {
const { component, isReferenceViewable } = viewportComponents[i];
return { component, isReferenceViewable };
const { component } = viewportComponents[i];
return { component };
}
}
@@ -474,7 +343,7 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
type: 'error',
});
return { component: EmptyViewport, isReferenceViewable: () => false };
return { component: EmptyViewport };
}
export default ViewerViewportGrid;
+3 -3
View File
@@ -37,15 +37,15 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/dicom-image-loader": "^3.30.3",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/ui": "3.11.0-beta.114",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4",
"gl-matrix": "^3.4.3",
"immutability-helper": "^3.1.1",
@@ -74,10 +74,11 @@ const EVENTS = {
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
// Give the viewport a chance to jump to the measurement
JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport',
// Give the layout a chance to jump to the measurement
JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout',
/**
* Indicate some viewport should be jumped to. This will have to be implemented
* by a single handler that can look at all viewports to decide who should handle it.
*/
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
};
const VALUE_TYPES = {
@@ -431,11 +432,7 @@ class MeasurementService extends PubSubService {
return;
}
let internalUID = data.id;
if (!internalUID) {
internalUID = guid();
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
}
const internalUID = data.uid || guid();
const annotationData = data.annotation.data;
@@ -497,7 +494,7 @@ class MeasurementService extends PubSubService {
mapping => mapping.annotationType === annotationType
);
if (!sourceMapping) {
console.log('No source mapping', source);
console.log('No source mapping', source.uid, annotationType, source);
return;
}
const { toMeasurementSchema } = sourceMapping;
@@ -656,19 +653,7 @@ class MeasurementService extends PubSubService {
}
/**
* This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT
* and JUMP_TO_MEASUREMENT_LAYOUT. There are two events which are
* fired because there are two different items which might want to handle
* the event. First, there might already be a viewport which can handle
* the event. If so, then the layout doesn't need to necessarily change.
* This is communicated by the isConsumed value on the event itself.
* Otherwise, the layout itself may need to be navigated to in order
* to provide a viewport which can show the given measurement.
*
* When a viewport decides to apply the event, it should call the consume()
* method on the event, so that other listeners know they do not need to
* navigate. This does NOT affect whether the layout event is fired, and
* merely causes it to fire the event with the isConsumed set to true.
* This method calls the subscription for JUMP_TO_MEASUREMENT
*/
public jumpToMeasurement(viewportId: string, measurementUID: string): void {
@@ -678,16 +663,14 @@ class MeasurementService extends PubSubService {
log.warn(`No measurement uid, or unable to find by uid.`);
return;
}
const consumableEvent = this.createConsumableEvent({
const event = {
viewportId,
measurement,
});
};
// Important: we should broadcast the layout event first, since
// in the layout there might be a viewport that we can match and choose
// and jump in it before we decide on changing the orientation of different viewports
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent);
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent);
// A single handler will decide on which window to jump to, previously
// this was handled by a consumable event
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT, event);
}
_getSourceUID(name, version) {
@@ -45,10 +45,6 @@ class ViewportGridService extends PubSubService {
return this.presentationIdProviders.get(id);
}
public setIsReferenceViewable(viewportId: string, isReferenceViewable: boolean): void {
this.serviceImplementation._setIsReferenceViewable(viewportId, isReferenceViewable);
}
public getPresentationId(id: string, viewportId: string): string | null {
const state = this.getState();
const viewport = state.viewports.get(viewportId);
@@ -104,7 +100,6 @@ class ViewportGridService extends PubSubService {
set: setImplementation,
getNumViewportPanes: getNumViewportPanesImplementation,
setViewportIsReady: setViewportIsReadyImplementation,
setIsReferenceViewable: setIsReferenceViewableImplementation,
getViewportState: getViewportStateImplementation,
}): void {
if (getViewportStateImplementation) {
@@ -139,9 +134,6 @@ class ViewportGridService extends PubSubService {
if (setViewportIsReadyImplementation) {
this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation;
}
if (setIsReferenceViewableImplementation) {
this.serviceImplementation._setIsReferenceViewable = setIsReferenceViewableImplementation;
}
}
public publishViewportsReady() {
+3
View File
@@ -29,6 +29,9 @@ export type DisplaySet = {
/** A fetch method to get the thumbnail */
getThumbnailSrc?(imageId?: string): Promise<string>;
/** An opaque type of this viewport, used internally to specify which viewport to use */
viewportType;
/**
* A fetch URL to display the content. This is used for content such as
* pdf display.
@@ -45,10 +45,6 @@ export interface ViewportGridState {
activeViewportId: string | null;
layout: Layout;
isHangingProtocolLayout: boolean;
isReferenceViewable: (props: {
viewportId: string;
reference: Record<string, unknown>;
}) => boolean;
viewports: GridViewports;
}
@@ -9,6 +9,10 @@ summary: Migration guide for OHIF 3.11 additional changes
* **`connectToolsToMeasurementService` parameters:** The `connectToolsToMeasurementService` function from the `@ohif/cornerstone-extensions` now take different arguments.
* **`data-viewportId`** The `data-viewportId` naming was not compliant with react and was causing warnings. Rename references to `data-viewportid`.
* **`setIsReferenceViewable`** is no longer available or required by ViewportGridService. Instead, the cornerstone viewports
themselves provide the isReferenceViewable. This occurs because there were a lot more deciding issues to navigate to viewports than could be added to viewport grid service.
* **`JUMP_TO_MEASUREMENT_VIEWPORT` and `JUMP_TO_MEASUREMENT_LAYOUT`** are combined into `JUMP_TO_MEASUREMENT` with no
consume event. Only the single event is fired. This will need to be handled to redirect the changes to the appropriate viewport type as it was not possible to figure that out with generic information available in `ViewportGridService`
**Migration Steps:**
@@ -31,8 +31,7 @@ There are seven events that get publish in `MeasurementService`:
| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) |
| MEASUREMENT_REMOVED | Fires when a measurement is removed |
| MEASUREMENTS_CLEARED | Fires when all measurements are deleted |
| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. |
| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. |
| JUMP_TO_MEASUREMENT | Fires when a measurement is requested to be jumped to |
## API
@@ -246,11 +246,9 @@ const DefaultFallback = ({
const ErrorBoundary = ({
context = 'OHIF',
onReset = () => {},
onError = () => {},
onError = _error => {},
fallbackComponent: FallbackComponent = DefaultFallback,
children,
fallbackRoute = null,
isPage,
}: ErrorBoundaryProps) => {
const [error, setError] = useState<ErrorBoundaryError | null>(null);
@@ -276,8 +274,8 @@ const ErrorBoundary = ({
event.preventDefault();
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
setError(event.reason);
onErrorHandler(event.reason, null);
setError(event.reason || event);
onErrorHandler(event.reason || event, null);
}, 100);
};
@@ -291,7 +289,10 @@ const ErrorBoundary = ({
};
}, []);
const onErrorHandler = (error: ErrorBoundaryError, componentStack: string | null) => {
const onErrorHandler = (
error: ErrorBoundaryError | ErrorEvent,
componentStack: string | null
) => {
console.debug(`${context} Error Boundary`, error, componentStack, context);
onError(error, componentStack || '', context);
};
@@ -125,6 +125,7 @@ interface ViewportGridApi {
setViewportGridSizeChanged: (props: any) => void;
publishViewportsReady: () => void;
getDisplaySetsUIDsForViewport: (viewportId: string) => string[];
isReferenceViewable: (viewportId: string, viewRef, options?) => boolean;
}
// Update the context type
@@ -142,19 +143,6 @@ interface ViewportGridProviderProps {
export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) {
const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => {
switch (action.type) {
case 'SET_IS_REFERENCE_VIEWABLE': {
const { viewportId, isReferenceViewable } = action.payload;
const viewports = new Map(state.viewports);
const viewport = viewports.get(viewportId);
if (!viewport) {
return;
}
viewports.set(viewportId, {
...viewport,
isReferenceViewable,
});
return { ...state, viewports };
}
case 'SET_ACTIVE_VIEWPORT_ID': {
return { ...state, ...{ activeViewportId: action.payload } };
}
@@ -392,13 +380,6 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
[dispatch]
);
const setIsReferenceViewable = useCallback(
(viewportId, isReferenceViewable) => {
dispatch({ type: 'SET_IS_REFERENCE_VIEWABLE', payload: { viewportId, isReferenceViewable } });
},
[dispatch]
);
const setDisplaySetsForViewports = useCallback(
viewports =>
dispatch({
@@ -495,7 +476,7 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
getState,
setActiveViewportId,
setDisplaySetsForViewports,
setIsReferenceViewable,
isReferenceViewable: () => false,
setLayout,
reset,
onModeExit: reset,
@@ -511,7 +492,6 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
service,
setActiveViewportId,
setDisplaySetsForViewports,
setIsReferenceViewable,
setLayout,
reset,
set,
@@ -527,8 +507,8 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
setActiveViewportId: index => service.setActiveViewportId(index),
setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]),
setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props),
setIsReferenceViewable: (viewportId, isReferenceViewable) =>
service.setIsReferenceViewable(viewportId, isReferenceViewable),
isReferenceViewable: (viewportId, isReferenceViewable, options) =>
service.isReferenceViewable(viewportId, isReferenceViewable, options),
setLayout: layout => service.setLayout(layout),
getViewportState: viewportId => service.getViewportState(viewportId),
reset: () => service.reset(),