fix: Enhance multiframe instance handling and improve instance valida… (#5956)

fix: Enhance multiframe instance handling and improve instance validation

- Updated `getDisplaySetInfo` to ensure local file frame imageIds resolve correctly, falling back to the source multiframe instance when necessary.
- Improved instance filtering in `isDisplaySetReconstructable` to handle undefined instances more robustly.
- Modified `App` prop types to make `routerBasename` optional and added new props for loading indicators and data sources.
- Refactored `CinePlayer` component to replace Button with a div for better accessibility and interaction.
This commit is contained in:
Alireza 2026-04-13 09:31:28 -04:00 committed by GitHub
parent 3069d33c99
commit f5a66b9eea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 30 additions and 15 deletions

View File

@ -48,12 +48,20 @@ function getDisplaySetInfo(instances) {
let firstTimePointInstances; let firstTimePointInstances;
if (instances[0].NumberOfFrames > 1 && timePoints.length > 1) { if (instances[0].NumberOfFrames > 1 && timePoints.length > 1) {
// handle multiframe dynamic volume // Handle multiframe dynamic volumes. Local file frame imageIds do not
firstTimePointInstances = timePoints[0].map(imageId => metaData.get('instance', imageId)); // always resolve to a frame-level instance object, so keep resolved
// entries and fall back to the source multiframe instance when needed.
firstTimePointInstances = timePoints[0]
.map(imageId => metaData.get('instance', imageId))
.filter(Boolean);
if (!firstTimePointInstances.length) {
firstTimePointInstances = [instances[0]];
}
} else { } else {
// O(n) to convert it into a map and O(1) to find each instance // O(n) to convert it into a map and O(1) to find each instance
instances.forEach(instance => instancesMap.set(instance.imageId, instance)); instances.forEach(instance => instancesMap.set(instance.imageId, instance));
firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId)); firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId)).filter(Boolean);
} }
displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances, appConfig); displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances, appConfig);
} else { } else {

View File

@ -184,12 +184,16 @@ App.propTypes = {
config: PropTypes.oneOfType([ config: PropTypes.oneOfType([
PropTypes.func, PropTypes.func,
PropTypes.shape({ PropTypes.shape({
routerBasename: PropTypes.string.isRequired, routerBasename: PropTypes.string,
oidc: PropTypes.array, oidc: PropTypes.array,
whiteLabeling: PropTypes.object, whiteLabeling: PropTypes.object,
extensions: PropTypes.array, extensions: PropTypes.array,
showLoadingIndicator: PropTypes.bool,
showStudyList: PropTypes.bool,
modes: PropTypes.array,
dataSources: PropTypes.array,
}), }),
]).isRequired, ]),
/* Extensions that are "bundled" or "baked-in" to the application. /* Extensions that are "bundled" or "baked-in" to the application.
* These would be provided at build time as part of they entry point. */ * These would be provided at build time as part of they entry point. */
defaultExtensions: PropTypes.array, defaultExtensions: PropTypes.array,

View File

@ -11,12 +11,14 @@ const iopTolerance = 0.01;
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
*/ */
export default function isDisplaySetReconstructable(instances, appConfig) { export default function isDisplaySetReconstructable(instances, appConfig) {
if (!instances.length) { const definedInstances = instances?.filter(Boolean) || [];
if (!definedInstances.length) {
return { value: false }; return { value: false };
} }
const firstInstance = instances[0]; const firstInstance = definedInstances[0];
const isMultiframe = firstInstance.NumberOfFrames > 1; const isMultiframe = (firstInstance.NumberOfFrames || 0) > 1;
if (appConfig) { if (appConfig) {
const rows = toNumber(firstInstance.Rows); const rows = toNumber(firstInstance.Rows);
@ -30,16 +32,16 @@ export default function isDisplaySetReconstructable(instances, appConfig) {
// in favor of the calculation by metadata (orientation and positions) // in favor of the calculation by metadata (orientation and positions)
// Can't reconstruct if we only have one image. // Can't reconstruct if we only have one image.
if (!isMultiframe && instances.length === 1) { if (!isMultiframe && definedInstances.length === 1) {
return { value: false }; return { value: false };
} }
// Can't reconstruct if all instances don't have the ImagePositionPatient. // Can't reconstruct if all instances don't have the ImagePositionPatient.
if (!isMultiframe && !instances.every(instance => instance.ImagePositionPatient)) { if (!isMultiframe && !definedInstances.every(instance => instance.ImagePositionPatient)) {
return { value: false }; return { value: false };
} }
const sortedInstances = sortInstancesByPosition(instances); const sortedInstances = sortInstancesByPosition(definedInstances);
return isMultiframe ? processMultiframe(sortedInstances[0]) : processSingleframe(sortedInstances); return isMultiframe ? processMultiframe(sortedInstances[0]) : processSingleframe(sortedInstances);
} }

View File

@ -103,9 +103,10 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
onOpenChange={setPopoverOpen} onOpenChange={setPopoverOpen}
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <div
variant="ghost" role="button"
className="h-full border-none bg-transparent p-0 hover:bg-transparent" tabIndex={0}
className="h-full cursor-pointer border-none bg-transparent p-0"
> >
<Numeric.Container <Numeric.Container
mode="stepper" mode="stepper"
@ -129,7 +130,7 @@ const CinePlayer: React.FC<CinePlayerProps> = ({
</div> </div>
</Numeric.NumberStepper> </Numeric.NumberStepper>
</Numeric.Container> </Numeric.Container>
</Button> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
side="bottom" side="bottom"