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;