Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
e460845
fix(commands): correct spelling of 'setVolumeRenderingQuality' in com…
mbellehumeur Feb 9, 2026
37cf727
feat(rendering): add volume rendering performance controls and comman…
mbellehumeur Feb 10, 2026
250e62e
feat(decimation): implement decimated volume loader and auto-decimati…
mbellehumeur Feb 10, 2026
c28e8ca
refactor(volume-rendering): replace volume decimation cache logic wit…
mbellehumeur Feb 10, 2026
76ca64c
feat(volume-rendering):load sample distance multiplier
mbellehumeur Feb 10, 2026
9c8d76e
feat(decimation): enhance auto-decimation logic by integrating app co…
mbellehumeur Feb 10, 2026
260643a
feat(thumbnail): add support for original instance count in thumbnail…
mbellehumeur Feb 11, 2026
a0693a9
refactor(volume-rendering): update VolumeRenderingPerformance compone…
mbellehumeur Feb 11, 2026
3a2139f
refactor(volume-rendering): remove sample distance multiplier functio…
mbellehumeur Feb 12, 2026
e80bc11
feat(commands): update volume loading logic to set actual image count…
mbellehumeur Feb 12, 2026
c491996
feat(volume-options): rename VolumeOptions
mbellehumeur Feb 12, 2026
492091a
feat(init): integrate displaySetService and add utility function to r…
mbellehumeur Feb 12, 2026
b12b15c
fix(commands): enhance baseVolumeId extraction logic for decimatedVol…
mbellehumeur Feb 12, 2026
911ad12
fix(thumbnail): streamline thumbnail rendering logic by removing unne…
mbellehumeur Feb 12, 2026
6a8efa6
refactor(cornerstone): simplify volume loader registration and enhanc…
mbellehumeur Feb 12, 2026
508e4fd
refactor(cornerstone): clean up CornerstoneCacheService by removing c…
mbellehumeur Feb 12, 2026
67a9aa7
Merge remote-tracking branch 'origin/master' into feat/decimatedVolum…
mbellehumeur Feb 12, 2026
0db6564
refactor(volume-options): update VolumeDecimationInfo type definition…
mbellehumeur Feb 16, 2026
4df9a6e
refactor(volume-rendering): enhance VolumeRenderingOptions and Volume…
mbellehumeur Feb 16, 2026
b980c73
feat(overlays): add AutoDecimationOverlay component to display auto-d…
mbellehumeur Feb 24, 2026
777b594
refactor(cornerstone): update CornerstoneCacheService to utilize cust…
mbellehumeur Feb 24, 2026
4ee5756
feat(volume-options): add VolumeOptionsMenu component to toolbar, ena…
mbellehumeur Feb 24, 2026
6e689e7
Merge remote-tracking branch 'origin/master' into feat/decimatedVolum…
mbellehumeur Feb 24, 2026
98a132a
feat(viewport): add setViewport3DViewDirection command and Viewport3D…
mbellehumeur Feb 24, 2026
aa91164
feat(viewport): implement rotateViewport3DBy command and enhance View…
mbellehumeur Feb 24, 2026
3dcfa0a
feat(viewport): enhance Viewport3DCompassMenu with radial gradient fo…
mbellehumeur Feb 25, 2026
e4d8e01
fix(viewport): correct rotation angle calculation in Viewport3DCompas…
mbellehumeur Feb 25, 2026
cf272d6
feat(volume-options): add VolumeCropping component and integrate it i…
mbellehumeur Feb 25, 2026
2bf13a8
chore(dependencies): update @cornerstonejs packages to version 4.18.5…
mbellehumeur Mar 14, 2026
52467f0
feat(orientation-controller): add OrientationControllerTool to corner…
mbellehumeur Mar 14, 2026
9b25201
feat(volume-loading): implement volumeLoadedHandler to update display…
mbellehumeur Mar 14, 2026
2bb981f
feat(volume-cropping): enhance VolumeCropping component to manage vis…
mbellehumeur Mar 14, 2026
b970eab
feat(volume-options): Document Volume Options menu with new cropping …
mbellehumeur Mar 15, 2026
d64a2ae
feat(cornerstone): add toggle3Dhandles command for VolumeCropping too…
mbellehumeur Mar 16, 2026
dd16c01
feat(commands): add togglePlaneRotation command to manage plane rotat…
mbellehumeur Mar 16, 2026
291f93d
fix(cornerstone): improve handling of OrientationController during Vo…
mbellehumeur Mar 17, 2026
9c5347b
fix(volume-cropping): correct terminology and improve tooltip instruc…
mbellehumeur Mar 17, 2026
4ae3180
chore(dependencies): update package versions in bun.lock, package.jso…
mbellehumeur Mar 17, 2026
61ae3d8
Merge branch 'master' of https://github.com/mbellehumeur/Viewers into…
mbellehumeur Mar 17, 2026
7e996ff
chore(dependencies): update package versions to 3.13.0-beta.42 and ad…
mbellehumeur Mar 17, 2026
82b2b0f
refactor(init): rename and update getDisplaySetFromVolumeId to getDis…
mbellehumeur Mar 17, 2026
1da2a55
feat(viewport): enhance viewport options to reflect manual decimation…
mbellehumeur Mar 18, 2026
281d3cb
chore(dependencies): update @ohif/ui to version 3.13.0-beta.43
mbellehumeur Mar 18, 2026
7f5c6e3
feat(init): add Cypress support for GPU performance testing by seedin…
mbellehumeur Mar 18, 2026
4670a08
fix(init): update GPU test handling to respect CPU rendering option a…
mbellehumeur Mar 18, 2026
b568417
fix(init): enhance GPU test handling for Cypress and CPU rendering sc…
mbellehumeur Mar 18, 2026
5d75e7e
Merge branch 'master' into feat/decimatedVolumeLoading
mbellehumeur Mar 18, 2026
0f96344
refactor(config): migrate customizationService settings from default.…
mbellehumeur Mar 18, 2026
a74b484
Merge branch 'feat/decimatedVolumeLoading' of https://github.com/mbel…
mbellehumeur Mar 18, 2026
2648ec1
feat(config): add useNorm16Texture option to netlify.js for enhanced …
mbellehumeur Mar 20, 2026
cb20091
feat(viewport): swpin should continue after changing focus to another…
mbellehumeur Mar 21, 2026
8bb999b
Move spin to the viewport instead of corner menu.
mbellehumeur Mar 21, 2026
edda830
Save and restore camera in commandsModule reloadVolumewithDecimation
mbellehumeur May 14, 2026
8026b4b
Remove OrientationController workaround now that VolumeCropping actor…
mbellehumeur May 14, 2026
bf00639
Update extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.css
mbellehumeur May 14, 2026
f661f74
Update extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverla…
mbellehumeur May 14, 2026
5d0337e
Update extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverla…
mbellehumeur May 14, 2026
59448bf
refactor(AutoDecimationOverlay): simplify viewport type check by remo…
mbellehumeur May 22, 2026
9392bec
feat(gpuPerformanceTest): add 'failed' state to GPU performance resul…
mbellehumeur May 22, 2026
487f7d2
feat(CornerstoneOverlays): integrate viewport slice progress scrollba…
mbellehumeur May 22, 2026
dcedf30
Add files to merge CornerstoneOverlays.
mbellehumeur May 22, 2026
8b878dd
refactor(getSopClassHandlerModule): streamline attribute setting for …
mbellehumeur Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,109 changes: 3,749 additions & 3,360 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';

import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { Volume3dSpinDriver } from '../components/ViewportOrientationMenu/Volume3dSpinDriver';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
Expand Down Expand Up @@ -280,7 +281,12 @@ const OHIFCornerstoneViewport = React.memo(
initialImageIndex
);

const presentations = getViewportPresentations(viewportId, viewportOptions);
const optionsToUse =
'viewportOptions' in viewportData && viewportData.viewportOptions
? viewportData.viewportOptions
: viewportOptions;

const presentations = getViewportPresentations(viewportId, optionsToUse);

// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
Expand All @@ -295,7 +301,7 @@ const OHIFCornerstoneViewport = React.memo(
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
optionsToUse,
displaySetOptions,
presentations
);
Expand Down Expand Up @@ -353,6 +359,7 @@ const OHIFCornerstoneViewport = React.memo(
/>
)}
</div>
<Volume3dSpinDriver viewportId={viewportId} />
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { useViewportRendering } from '../../hooks';

/**
* Renders the auto-decimation message in the viewport top-right, same style as
* demographic overlay items (Study Date, Series Description).
*/
function AutoDecimationOverlay({
viewportId,
servicesManager,
}: {
viewportId: string;
servicesManager: AppTypes.ServicesManager;
}) {
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const options = servicesManager?.services?.cornerstoneViewportService
?.getViewportInfo(viewportId)
?.getViewportOptions?.();
const info = options?.autoDecimationInfo;
const viewportType = options?.viewportType;
const isVolume =
viewportType === 'orthographic' ||
viewportType === 'volume3d';

if (!info?.message || !isVolume) {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return null;
}
Comment on lines +15 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect useViewportRendering to confirm what it subscribes to / re-renders on.
fd -t f 'useViewportRendering' extensions/cornerstone/src/hooks
fd -t f 'useViewportRendering' extensions/cornerstone/src/hooks --exec sed -n '1,200p' {}

# Check how reloadVolumeWithDecimation updates viewport options / broadcasts.
rg -nP 'autoDecimationInfo|VIEWPORT_DATA_CHANGED|reloadVolumeWithDecimation' extensions/cornerstone/src -C2

Repository: OHIF/Viewers

Length of output: 18977


Ensure AutoDecimationOverlay re-renders when autoDecimationInfo updates

autoDecimationInfo is read imperatively but the component depends on useViewportRendering for updates. This hook subscribes to VIEWPORT_DATA_CHANGED only until the viewport reference is found, then unsubscribes. Since reloadVolumeWithDecimation broadcasts this event to update options, the overlay will not re-render to show the message without an unrelated trigger.

Fix by subscribing directly to cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED within the component or ensuring the data source is reactive.

Affected code
  const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
  const options = servicesManager?.services?.cornerstoneViewportService
    ?.getViewportInfo(viewportId)
    ?.getViewportOptions?.();
  const info = options?.autoDecimationInfo;
  const viewportType = options?.viewportType;
  const isVolume =
    viewportType === 'orthographic' ||
    viewportType === 'volume3d';

  if (!info?.message || !isVolume) {
    return null;
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/cornerstone/src/Viewport/Overlays/AutoDecimationOverlay.tsx`
around lines 15 - 27, `AutoDecimationOverlay` is reading `autoDecimationInfo`
imperatively, so it won’t re-render when `reloadVolumeWithDecimation` updates
viewport options. Update the component to subscribe to
`cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED` (or make the options
source reactive) so changes to
`getViewportInfo(viewportId).getViewportOptions()` trigger a render. Keep the
existing `useViewportRendering(viewportId)` usage, but ensure the overlay’s
message visibility is driven by `autoDecimationInfo`, `viewportType`, and the
new event subscription.


const colorClass = 'text-yellow-400';
const shadowClass = isLight ? 'shadow-light' : 'shadow-dark';

return (
<div
className={`absolute viewport-overlay auto-decimation-overlay pointer-events-none ${colorClass} ${shadowClass} text-base leading-5 text-right`}
style={{ top: '0.5rem', right: '3.5rem', maxWidth: 'calc(100% - 4rem)' }}
title="Volume auto-decimated"
data-cy="auto-decimation-overlay"
>
<div className="overlay-item">
<span className="break-words whitespace-pre-line">{info.message}</span>
</div>
</div>
);
}

export default AutoDecimationOverlay;
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useEffect, useState } from 'react';

import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportSliceProgressScrollbar from './ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar';
import CustomizableViewportOverlay from './CustomizableViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
import AutoDecimationOverlay from './AutoDecimationOverlay';

function CornerstoneOverlays(props: withAppTypes) {
const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services;
const { cornerstoneViewportService, customizationService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0,
numberOfSlices: 0,
Expand All @@ -29,7 +31,7 @@ function CornerstoneOverlays(props: withAppTypes) {
return () => {
unsubscribe();
};
}, [viewportId]);
}, [viewportId, cornerstoneViewportService]);

if (!element) {
return null;
Expand All @@ -39,21 +41,41 @@ function CornerstoneOverlays(props: withAppTypes) {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);

if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
return null;
return (
<div className="noselect">
<AutoDecimationOverlay viewportId={viewportId} servicesManager={servicesManager} />
</div>
);
}
}

const viewportScrollbarVariant = customizationService.getCustomization(
'viewportScrollbar.variant'
);
const useProgressScrollbar = viewportScrollbarVariant !== 'legacy';

return (
<div className="noselect">
<ViewportImageScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
{useProgressScrollbar ? (
<ViewportSliceProgressScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
servicesManager={servicesManager}
/>
) : (
<ViewportImageScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}

<CustomizableViewportOverlay
imageSliceData={imageSliceData}
Expand All @@ -63,6 +85,8 @@ function CornerstoneOverlays(props: withAppTypes) {
element={element}
/>

<AutoDecimationOverlay viewportId={viewportId} servicesManager={servicesManager} />

<ViewportImageSliceLoadingIndicator
viewportData={viewportData}
element={element}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function CustomizableViewportOverlay({
instanceNumber,
viewportId,
toolGroupService,
servicesManager,
isLight,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import {
SmartScrollbar,
SmartScrollbarTrack,
SmartScrollbarFill,
SmartScrollbarIndicator,
SmartScrollbarEndpoints,
} from '@ohif/ui-next';
import { getViewportImageIds } from './helpers';
import {
useLoadedSliceBytes,
useProgressScrollbarMode,
useViewedSliceBytes,
useViewportSliceSync,
} from './hooks';
import { ViewportSliceProgressScrollbarProps } from './types';

function ViewportSliceProgressScrollbar({
viewportData,
viewportId,
element,
imageSliceData,
setImageSliceData,
servicesManager,
}: ViewportSliceProgressScrollbarProps) {
const { cineService, cornerstoneViewportService, customizationService, viewedDataService } =
servicesManager.services;

const showLoadedEndpoints =
customizationService.getCustomization('viewportScrollbar.showLoadedEndpoints') !== false;
const showLoadedFill =
customizationService.getCustomization('viewportScrollbar.showLoadedFill') !== false;
const showViewedFill =
customizationService.getCustomization('viewportScrollbar.showViewedFill') !== false;
const showLoadingPattern =
customizationService.getCustomization('viewportScrollbar.showLoadingPattern') !== false;
const viewedDwellMsRaw = customizationService.getCustomization('viewportScrollbar.viewedDwellMs');
const loadedBatchIntervalMsRaw = customizationService.getCustomization(
'viewportScrollbar.loadedBatchIntervalMs'
);
const viewedDwellMs =
typeof viewedDwellMsRaw === 'number' && viewedDwellMsRaw >= 0 ? viewedDwellMsRaw : 0;
const loadedBatchIntervalMs =
typeof loadedBatchIntervalMsRaw === 'number' && loadedBatchIntervalMsRaw >= 0
? loadedBatchIntervalMsRaw
: 200;

const { numberOfSlices, imageIndex } = imageSliceData;

const imageIds = useMemo(() => getViewportImageIds(viewportData), [viewportData]);
const imageIdToIndex = useMemo(() => {
const map = new Map<string, number>();
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (imageId) {
map.set(imageId, i);
}
}
return map;
}, [imageIds]);

const isFullMode = useProgressScrollbarMode({
viewportData,
viewportId,
element,
cornerstoneViewportService,
});

useViewportSliceSync({
viewportData,
viewportId,
element,
cornerstoneViewportService,
setImageSliceData,
});

const {
bytes: loadedBytes,
version: loadedVersion,
isFull: isFullyLoaded,
} = useLoadedSliceBytes({
isFullMode,
numberOfSlices,
viewportData,
imageIds,
imageIdToIndex,
loadedBatchIntervalMs,
});

const { bytes: viewedBytes, version: viewedVersion } = useViewedSliceBytes({
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
imageIdToIndex,
viewedDwellMs,
viewedDataService,
});

const onScrollbarValueChange = targetImageIndex => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);

if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}

const { isCineEnabled } = cineService.getState();

if (isCineEnabled) {
cineService.stopClip(element, { viewportId });
cineService.setCine({ id: viewportId, frameRate: undefined, isPlaying: false });
}

csUtils.jumpToSlice(viewport.element, {
imageIndex: targetImageIndex,
debounceLoading: true,
});
};

const isLoading = isFullMode && showLoadingPattern ? !isFullyLoaded : false;

if (!numberOfSlices || numberOfSlices <= 1) {
return null;
}

return (
<div
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
padding: '8px 5px',
zIndex: 10,
}}
>
<div
style={{
position: 'relative',
height: '100%',
width: '11px',
}}
>
<SmartScrollbar
className="absolute inset-0"
value={imageIndex || 0}
total={numberOfSlices}
onValueChange={onScrollbarValueChange}
isLoading={isLoading}
enableKeyboardNavigation={false}
aria-label="Image navigation scrollbar"
indicator={
customizationService.getCustomization('viewportScrollbar.indicator') as
| Record<string, unknown>
| undefined
}
>
<SmartScrollbarTrack>
{isFullMode && showLoadedFill && (
<SmartScrollbarFill
marked={loadedBytes}
version={loadedVersion}
className="bg-neutral/25"
loadingClassName="bg-neutral/50"
/>
)}
{isFullMode && showViewedFill && (
<SmartScrollbarFill
marked={viewedBytes}
version={viewedVersion}
className="bg-primary/35"
loadingClassName="bg-primary/35"
/>
)}
</SmartScrollbarTrack>
<SmartScrollbarIndicator />
{isFullMode && showLoadedEndpoints && (
<SmartScrollbarEndpoints
marked={loadedBytes}
version={loadedVersion}
/>
)}
</SmartScrollbar>
</div>
</div>
);
}

ViewportSliceProgressScrollbar.propTypes = {
viewportData: PropTypes.object,
viewportId: PropTypes.string.isRequired,
element: PropTypes.instanceOf(Element),
imageSliceData: PropTypes.object.isRequired,
setImageSliceData: PropTypes.func.isRequired,
servicesManager: PropTypes.object.isRequired,
};

export default ViewportSliceProgressScrollbar;
Loading