Skip to content

[WIP] feat/decimated loading#6034

Open
mbellehumeur wants to merge 62 commits into
OHIF:masterfrom
mbellehumeur:test-merge-decimateLoading
Open

[WIP] feat/decimated loading#6034
mbellehumeur wants to merge 62 commits into
OHIF:masterfrom
mbellehumeur:test-merge-decimateLoading

Conversation

@mbellehumeur

@mbellehumeur mbellehumeur commented May 22, 2026

Copy link
Copy Markdown
Contributor

Context

Changes & Results

Testing

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • [] My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] OS:
  • [] Node version:
  • [] Browser:

Greptile Summary

This WIP PR introduces decimated volume loading for 3D rendering in OHIF: volumes exceeding a configurable voxel threshold are automatically downsampled via a new decimatedVolumeLoader, a GPU benchmark runs at startup to calibrate sample-distance defaults, and a new SmartScrollbar tracks per-slice load/viewed state with a visual progress fill.

  • Auto-decimation pipeline: applyAutoDecimationIfNecessary estimates voxel count from display-set metadata and selects an IJK decimation tuple; CornerstoneCacheService constructs a decimatedVolumeLoader:…:I_J_K volume ID and passes it to Cornerstone's volume cache; an AutoDecimationOverlay badge notifies the user when a volume has been reduced.
  • GPU benchmark (gpuPerformanceTest): measures triangle throughput, texture upload, and buffer operations via a temporary off-screen WebGL canvas; stores a generalPerformanceScore in localStorage to set rotateSampleDistanceFactor and sampleDistanceMultiplier once per browser session; Cypress/CPU-rendering paths skip the live test.
  • SmartScrollbar + ViewportSliceProgressScrollbar: replaces the legacy scrollbar by default; renders loaded-bytes and viewed-bytes fills via useByteArray batched updates; introduces ViewedDataService to persist per-imageId viewed state across viewports.

Confidence Score: 3/5

Not ready to merge — the PR is explicitly marked [WIP] and several unresolved issues from prior review rounds remain in the code.

Multiple issues flagged in earlier review rounds are still present: numImageFrames is overwritten for decimated volumes in volumeLoadedHandler, decimated volume cache entries are not purged during display-set invalidation, and the GPU result assigns level 'failed' outside the declared union type. New findings in this round are lower-impact but add to the overall debt.

init.tsx (volumeLoadedHandler still corrupts numImageFrames), CornerstoneCacheService.ts (stale decimated volumes survive invalidation, no-op delete calls), gpuPerformanceTest.ts (type-union mismatch on level, debug window assignment).

Important Files Changed

Filename Overview
extensions/cornerstone/src/utils/decimation/applyAutoDecimationIfNecessary.ts New utility for auto-decimation of large volumes; fallback loop produces anisotropic K-only decimation for very large datasets; otherwise logic is sound for typical cases.
extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts Volume loading extended with decimated-loader support; volumeImageIds.delete calls inside the invalidation loop use volume IDs instead of display set UIDs (no-ops); decimated volumes still not purged on invalidation (flagged in prior review).
extensions/cornerstone/src/utils/gpuPerformanceTest.ts New GPU benchmark utility; stores full result on window.gpuPerformanceResult in both paths (debug leak); 'failed' level not in the GPUPerformanceResult.level union (flagged in prior review).
extensions/cornerstone/src/init.tsx Registers decimated volume loader, runs GPU benchmark on init, adds volumeLoadedHandler; handler still corrupts numImageFrames for decimated volumes (flagged in prior review).
platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx New compound scrollbar component with ResizeObserver, drag, keyboard nav, and expandable indicator; well-structured with clear context separation and runtime child validation.
extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts New hooks for load/viewed progress tracking; useViewedSliceBytes correctly guards viewedDataService as nullable at runtime despite non-optional type annotation; event cleanup is properly handled.
extensions/cornerstone/src/utils/decimation/constants.ts Exports decimation constants; AUTO_DECIMATION_VIEWPORT_TYPES includes 'volume' which is not a valid Cornerstone3D ViewportType and will never match.
extensions/cornerstone/src/commandsModule.ts Adds volume cropping, decimation reload, and quality commands; fixes setVolumeRenderingQulaity typo; reloadVolumeWithDecimation handles camera/preset restoration and concurrent viewport updates.

Comments Outside Diff (1)

  1. extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts, line 110-145 (link)

    P1 Decimated volumes are never purged from cache during invalidation

    invalidateVolume iterates candidateVolumeIds (baseVolumeId, …:volume3d, …:orthographic) but decimated volumes are stored under the key decimatedVolumeLoader:${baseVolumeIdWithSuffix}:${i}_${j}_${k}. When a display set is invalidated (e.g. after a series reload), the stale decimated volume remains in cs3DCache and will be served on the next viewport render instead of the freshly-loaded data. A scan of cs3DCache._volumeCache for keys that start with decimatedVolumeLoader: and contain the display set UID needs to be added to the purge loop.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
    Line: 110-145
    
    Comment:
    **Decimated volumes are never purged from cache during invalidation**
    
    `invalidateVolume` iterates `candidateVolumeIds` (`baseVolumeId`, `…:volume3d`, `…:orthographic`) but decimated volumes are stored under the key `decimatedVolumeLoader:${baseVolumeIdWithSuffix}:${i}_${j}_${k}`. When a display set is invalidated (e.g. after a series reload), the stale decimated volume remains in `cs3DCache` and will be served on the next viewport render instead of the freshly-loaded data. A scan of `cs3DCache._volumeCache` for keys that start with `decimatedVolumeLoader:` and contain the display set UID needs to be added to the purge loop.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
extensions/cornerstone/src/utils/decimation/applyAutoDecimationIfNecessary.ts:229-246
**Fallback decimation loop only increments K axis**

When no pre-built candidate achieves the `requiredFactor` (i.e., the volume exceeds `8 × threshold`), the fallback loop keeps I and J fixed at `2` and increments K indefinitely. For a 900×512×512 volume at a 10M-voxel threshold the required factor is ≈ 47 → `[2, 2, 12]`, yielding a volume 6× more coarse in the slice direction than in-plane, making 3D renders look severely anisotropic. The fix would be to balance increments across all three axes in the fallback loop (e.g., cycle through incrementing i, j, and k in turn).

### Issue 2 of 4
extensions/cornerstone/src/utils/decimation/constants.ts:3
`'volume'` is not a valid Cornerstone3D `ViewportType` value — the enum only defines `'volume3d'`, `'orthographic'`, `'stack'`, etc. This entry will never be matched by `resolveViewportType` and is dead code that creates a false impression that plain `'volume'`-typed viewports are auto-decimated.

```suggestion
export const AUTO_DECIMATION_VIEWPORT_TYPES = new Set(['volume3d']);
```

### Issue 3 of 4
extensions/cornerstone/src/utils/gpuPerformanceTest.ts:603-606
**Debug result leaked onto `window` in production**

`window.gpuPerformanceResult` is set in both the success and failure paths of `gpuPerformanceTest`. The full `GPUPerformanceResult` object (including renderer info, texture sizes, and heap metrics) is permanently accessible from the browser console or any injected script. If this is a development-only aid, it should be guarded by a dev/debug flag or removed from production builds.

### Issue 4 of 4
extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts:115-125
**`volumeImageIds.delete` calls inside the loop are no-ops**

`this.volumeImageIds` is keyed by `displaySet.displaySetInstanceUID` (see `this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds)` in `_getVolumeViewportData`). The loop deletes by `candidateVolumeId` (a fully-qualified volume ID like `cornerstoneStreamingImageVolume:DSUID:volume3d`), which will never match a display-set-UID key. Only the final `this.volumeImageIds.delete(invalidatedDisplaySetInstanceUID)` after the loop is effective. The per-candidate deletes inside the loop should be removed to avoid confusion.

Reviews (4): Last reviewed commit: "Add files to merge CornerstoneOverlays." | Re-trigger Greptile

Summary by CodeRabbit

  • New Features

    • Added a new volume options menu for 3D viewports, including cropping controls, sampling settings, and downsampling controls.
    • Added a 3D orientation compass with direct view-direction controls and spin adjustment.
    • Added a new viewport progress scrollbar with loaded/viewed indicators and slice navigation.
  • Bug Fixes

    • Improved volume loading and refresh behavior when decimation changes.
    • Updated instance counts to show original vs. current values where applicable.
  • Documentation

    • Documented new volume decimation and volume options settings.

…ds for sample distance multiplier and volume reload with decimation
…on logic for improved performance and memory management
…h parsing from volumeId for improved clarity and maintainability
…nfig threshold and refactoring related functions
…nt to use AllInOneMenu for improved layout and organization
…nality and update related components for improved clarity
… in display sets and enhance event broadcasting
…umeLoader format and update IJK decimation parsing to handle baseVolumeId with colons
…e volume ID generation logic for improved clarity and performance
…ommented code and unnecessary whitespace for improved readability. Add doc.
… and remove unused import for improved clarity
…RenderingPresets components by integrating props instead of hooks for improved flexibility and clarity
…ecimation messages in the viewport, enhancing user feedback on volume rendering changes
…omizationService for volume decimation settings, improving configuration flexibility and clarity
…bling volume-specific options and enhancing user interaction with volume rendering settings
…CompassMenu component for enhanced 3D viewport orientation control
…port3DCompassMenu with spin controls for improved 3D viewport interaction
…r center sphere and adjust layout for spin controls
…nto VolumeOptionsMenu for enhanced volume manipulation features
… and integrate GPU performance testing utility for improved volume rendering settings
…stone tools and integrate it into volume3D tool group with configuration options
mbellehumeur and others added 18 commits March 17, 2026 15:26
…playSetFromVolume for improved clarity and functionality
… settings and broadcast updated viewport data
…js to netlify.js for improved configuration management
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
…ys.tsx

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
…ys.tsx

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@netlify

netlify Bot commented May 22, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev failed. Why did it fail? →

Name Link
🔨 Latest commit 8b878dd
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a3d4938a294b400085c4ae7

Comment thread extensions/cornerstone/src/utils/gpuPerformanceTest.ts Outdated
Comment thread platform/core/src/utils/volumeOptionsStorage.ts
Comment thread extensions/cornerstone/src/init.tsx
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds GPU-adaptive volume decimation (automatic IJK downsampling based on WebGL benchmarks and voxel-count thresholds), a new SmartScrollbar component with per-slice loaded/viewed fill tracking via a new ViewedDataService, a 3D viewport compass menu with spin animation and view-direction commands, volume cropping and sampling UI (VolumeOptionsMenu), and a reloadVolumeWithDecimation command — all wired through updated services, toolbar registrations, and configuration.

Changes

3D Volume Rendering: Decimation, SmartScrollbar & Viewport Controls

Layer / File(s) Summary
Shared types, storage, and ViewedDataService
platform/core/src/types/AppTypes.ts, platform/core/src/types/ViewportGridType.ts, extensions/cornerstone/src/services/ViewportService/Viewport.ts, extensions/cornerstone/src/types/CornerstoneCacheService.ts, extensions/cornerstone/src/types/ViewportPresets.ts, platform/core/src/utils/volumeOptionsStorage.ts, platform/core/src/utils/index.ts, platform/core/src/index.ts, extensions/cornerstone/src/services/ViewedDataService/..., extensions/cornerstone/src/types/AppTypes.ts, platform/core/src/services/ServicesManager.ts, extensions/cornerstone/src/index.tsx, extensions/cornerstone/src/constants/index.ts
Adds ijkDecimation/autoDecimationInfo to viewport option types; adds VolumeOptions localStorage persistence; adds ViewedDataService PubSubService tracking viewed slice IDs; registers the service in the extension lifecycle; passes appConfig through ServicesManager.registerService.
GPU performance test and auto-decimation logic
extensions/cornerstone/src/utils/gpuPerformanceTest.ts, extensions/cornerstone/src/utils/decimation/applyAutoDecimationIfNecessary.ts, extensions/cornerstone/src/utils/decimation/constants.ts
Adds GPUPerformanceTester running three WebGL micro-benchmarks to score the GPU and select a sampling tier; adds applyAutoDecimationIfNecessary estimating voxel counts across display sets and selecting an IJK decimation tuple with optional overlay message.
CornerstoneCacheService decimation pipeline and init wiring
extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts, extensions/cornerstone/src/init.tsx, extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Updates CornerstoneCacheService to enrich viewport options via applyAutoDecimationIfNecessary and build viewport-type-scoped decimatedVolumeLoader: cache keys; updates init.tsx to run GPU probing, register the decimated volume loader, and use decimation-aware display-set resolution; propagates enrichedViewportOptions through OHIFCornerstoneViewport.
SmartScrollbar component system and useByteArray
platform/ui-next/src/components/SmartScrollbar/...
Adds SmartScrollbar compound component (pointer/keyboard slider with contraction animation), SmartScrollbarTrack, SmartScrollbarFill, SmartScrollbarIndicator, SmartScrollbarEndpoints, the useByteArray hook for batched Uint8Array mutations, and pixel-space fill/indicator-offset utilities with Jest tests.
ViewportSliceProgressScrollbar overlays and AutoDecimationOverlay
extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/..., extensions/cornerstone/src/Viewport/Overlays/AutoDecimationOverlay.tsx, extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx, extensions/cornerstone/src/customizations/viewportScrollbarCustomization.tsx
Adds ViewportSliceProgressScrollbar with four hooks tracking loaded (from Cornerstone cache) and viewed (from ViewedDataService) slices; adds AutoDecimationOverlay displaying the decimation message; updates CornerstoneOverlays to render both and select progress vs. legacy scrollbar variant via customization.
3D viewport compass, spin driver, cropping, and volume options menu
extensions/cornerstone/src/components/ViewportOrientationMenu/..., extensions/cornerstone/src/components/VolumeOptionsMenu/...
Adds viewport3dSpinStore, Volume3dSpinDriver (RAF-based rotation loop), Viewport3DCompassMenu (SVG compass with S/P/R/L/A/I direction buttons), ViewportOrientationMenuWrapper branching to compass for volume3d; adds VolumeCropping UI (handle/clipping-plane/rotate switches) and VolumeOptions UI (sampling and IJK decimation steppers) inside VolumeOptionsMenuWrapper.
commandsModule: decimation reload, cropping, and 3D camera commands
extensions/cornerstone/src/commandsModule.ts, extensions/cornerstone/src/hooks/useViewportRendering.tsx, extensions/cornerstone/src/components/WindowLevelActionMenu/...
Adds reloadVolumeWithDecimation (snapshot/restore camera + preset, swap decimated volume, broadcast VIEWPORT_DATA_CHANGED); adds volume-cropping commands; adds setViewport3DViewDirection and rotateViewport3DBy; fixes setVolumeRenderingQuality typo; passes volumeRenderingPresets and volumeRenderingQualityRange as explicit props.
Tool registration, toolbar, display set counts, config, i18n, docs
extensions/cornerstone/src/initCornerstoneTools.js, extensions/cornerstone/src/getToolbarModule.tsx, modes/basic/src/..., extensions/default/src/getSopClassHandlerModule.js, platform/ui-next/src/components/Thumbnail/..., platform/app/public/config/netlify.js, platform/i18n/..., platform/docs/...
Registers new Cornerstone tools; wires volumeOptionsMenu and VolumeCropping toolbar entries; adds numImageFramesOriginal through display sets to thumbnail count display; adds netlify customizationService config; adds i18n "Volume Options" in 5 locales; adds docs for volumeAutoDecimationThreshold and Volume Options menu.

Sequence Diagram(s)

sequenceDiagram
  participant App as init.tsx
  participant GPU as GPUPerformanceTester
  participant Cache as CornerstoneCacheService
  participant DL as decimatedVolumeLoader
  participant Cmd as commandsManager
  participant VP as OHIFCornerstoneViewport

  App->>GPU: gpuPerformanceTest(appConfig)
  GPU-->>App: setVolumeOptions(rotateSampleDistanceFactor, sampleDistanceMultiplier)
  App->>DL: registerVolumeLoader('decimatedVolumeLoader')
  VP->>Cache: createViewportData(displaySets, viewportOptions)
  Cache->>Cache: applyAutoDecimationIfNecessary → enrichedViewportOptions + ijkDecimation
  Cache->>DL: createAndCacheVolume('decimatedVolumeLoader:...', {ijkDecimation})
  Cache-->>VP: VolumeViewportData {viewportOptions: enriched}
  VP->>VP: optionsToUse = viewportData.viewportOptions
  Note over VP: Decimation overlay message shown if threshold exceeded
  Cmd->>Cmd: reloadVolumeWithDecimation({ijkDecimation})
  Cmd->>Cache: remove old volume, create new decimated volume
  Cmd->>VP: restore camera + preset, render, broadcast VIEWPORT_DATA_CHANGED
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • wayfarer3130
  • sedghi

Poem

🐰 Hop hop, the volumes shrink and grow,
GPU tests tell me how fast pixels flow,
The scrollbar glows where slices have been seen,
A compass spins the 3D world so keen,
IJK decimation, small but bright —
This bunny ships volume rendering right! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is only the template and leaves all required sections and checkboxes empty. Fill in Context, Changes & Results, Testing, and Tested Environment, and mark the checklist items with [x].
Docstring Coverage ⚠️ Warning Docstring coverage is 30.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and directly reflects the main change: decimated volume loading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch test-merge-decimateLoading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts (1)

146-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve viewport options during invalidation reloads.

This path calls _getVolumeViewportData without viewportData.viewportOptions, so auto/manual ijkDecimation and autoDecimationInfo are dropped after metadata invalidation.

Proposed fix
     const newViewportData = await this._getVolumeViewportData(
       dataSource,
       displaySets,
-      viewportData.viewportType
+      viewportData.viewportType,
+      viewportData.viewportOptions
     );
🤖 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/services/CornerstoneCacheService/CornerstoneCacheService.ts`
around lines 146 - 150, The invalidation reload path in CornerstoneCacheService
drops viewport-specific options because _getVolumeViewportData is called without
viewportData.viewportOptions. Update the reload flow to pass the existing
viewportOptions through this call so auto/manual ijkDecimation and
autoDecimationInfo are preserved when rebuilding the viewport data.
extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx (1)

284-304: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include decimation options in the memo comparison.

optionsToUse now makes ijkDecimation/autoDecimationInfo affect viewport data, but areEqual below does not compare those fields. Prop-only decimation changes can be memo-skipped and never reach setViewportData.

Proposed fix
 function areEqual(prevProps, nextProps) {
+  if (
+    prevProps.viewportOptions.ijkDecimation?.join('_') !==
+    nextProps.viewportOptions.ijkDecimation?.join('_')
+  ) {
+    return false;
+  }
+
+  if (
+    prevProps.viewportOptions.autoDecimationInfo?.message !==
+    nextProps.viewportOptions.autoDecimationInfo?.message
+  ) {
+    return false;
+  }
+
   if (nextProps.needsRerendering) {
     return false;
   }
🤖 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/OHIFCornerstoneViewport.tsx` around lines
284 - 304, `optionsToUse` now depends on decimation settings, but the
memoization check in `areEqual` does not include `ijkDecimation` and
`autoDecimationInfo`, so prop-only decimation updates can be skipped. Update
`areEqual` in `OHIFCornerstoneViewport` to compare those fields alongside the
existing viewport props, ensuring changes force `setViewportData` to run when
decimation options change.
modes/basic/src/initToolGroups.ts (1)

296-338: 🎯 Functional Correctness | 🟠 Major

Resolve conflicting mouse bindings between VolumeCropping and TrackballRotateTool.

Both tools are configured as active in the same tool group using Enums.MouseBindings.Primary. In this configuration, the VolumeCropping tool (listed first) will capture all primary mouse interactions, rendering the TrackballRotateTool unresponsive to mouse dragging.

Remove the active Primary binding from TrackballRotateTool or change it to a modifier key if rotation is intended alongside cropping.

Confirmed Issue Context
  const tools = {
    active: [
      // VolumeCropping captures Primary bindings first
      {
        toolName: 'VolumeCropping',
        bindings: [
          { mouseButton: Enums.MouseBindings.Primary }, // Conflict point
          ...
        ],
      },
      // TrackballRotateTool's Primary binding is effectively ignored
      {
        toolName: toolNames.TrackballRotateTool,
        bindings: [{ mouseButton: Enums.MouseBindings.Primary }], // Never receives event
      },
      ...
    ],
  };
🤖 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 `@modes/basic/src/initToolGroups.ts` around lines 296 - 338, The active tool
bindings in initToolGroups.ts have a mouse conflict: VolumeCropping and
TrackballRotateTool both use Enums.MouseBindings.Primary, so the first tool wins
and rotation becomes unreachable. Update the tools.active configuration by
removing the plain Primary binding from toolNames.TrackballRotateTool or moving
it behind a modifier key, while keeping VolumeCropping on Primary, so both tools
can be used without shadowing each other.
🧹 Nitpick comments (5)
extensions/cornerstone/src/types/ViewportPresets.ts (1)

55-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider tightening dimensions to a 3-tuple for consistency.

dimensions is typed as number[] while ijkDecimation and originalDecimation are [number, number, number]. If dimensions always represents IJK extents, a Point3/[number, number, number] type would prevent malformed shapes and align with the decimation tuples in this same type.

🤖 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/types/ViewportPresets.ts` around lines 55 - 60,
The VolumeDecimationInfo type is using an overly broad dimensions array, which
should be narrowed for consistency with the other 3-value tuples in this type.
Update the dimensions property in VolumeDecimationInfo to use a
3-tuple/Point3-style type, matching ijkDecimation and
appliedDecimation.originalDecimation, so the shape is fixed and cannot accept
malformed lengths.
platform/core/src/utils/index.ts (1)

52-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent export surface: helpers added to named exports but not the utils object.

getVolumeOptions/setVolumeOptions are re-exported as named exports but omitted from the utils aggregate object (lines 60–112), unlike the surrounding utilities. Consumers using OHIF.utils.getVolumeOptions would get undefined. If only named-import access is intended this is fine; otherwise add them to utils for consistency.

Also applies to: 154-155

🤖 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 `@platform/core/src/utils/index.ts` around lines 52 - 55, The utils export
surface is inconsistent because getVolumeOptions and setVolumeOptions are only
re-exported as named exports from index.ts and are missing from the utils
aggregate object. Update the utils object in index.ts to include these
volumeOptionsStorage helpers alongside the other utility entries so
OHIF.utils.getVolumeOptions and OHIF.utils.setVolumeOptions resolve correctly;
use the existing getVolumeOptions/setVolumeOptions imports and keep the named
exports intact.
extensions/cornerstone/src/components/ViewportOrientationMenu/Viewport3DCompassMenu.tsx (1)

26-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Spin UI state can desync from the store on viewport switch.

spin is seeded once from getVolume3dSpin(viewportIdToUse) via the useState initializer, but it never re-reads when viewportIdToUse changes (e.g., active viewport switches) or when the store is mutated elsewhere. The popover would then display/animate a stale value for the newly-targeted viewport. Consider reading via useSyncExternalStore (as Volume3dSpinDriver does) so the UI reflects the authoritative store value.

🤖 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/components/ViewportOrientationMenu/Viewport3DCompassMenu.tsx`
around lines 26 - 29, The Compass menu’s local `spin` state is initialized once
in `Viewport3DCompassMenu` from `getVolume3dSpin(viewportIdToUse)` and can
become stale when `viewportIdToUse` changes or the store updates elsewhere.
Update this component to read the spin value from the authoritative store on
every relevant change, ideally by mirroring `Volume3dSpinDriver` with
`useSyncExternalStore` (or equivalent subscription-based state) keyed by the
current viewport id. Keep the UI bound to the current viewport’s store value
instead of the one-time `useState` initializer.
extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx (2)

21-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Destructure the service from servicesManager.services.

Access the service via destructuring to follow the established pattern instead of chained property access.

♻️ Proposed refactor
-  const { servicesManager } = useSystem();
-  const viewportInfo = servicesManager.services.cornerstoneViewportService.getViewportInfo(
-    viewportId
-  );
+  const { servicesManager } = useSystem();
+  const { cornerstoneViewportService } = servicesManager.services;
+  const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);

As per coding guidelines: "access via const { ServiceName } = servicesManager.services".

🤖 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/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx`
around lines 21 - 24, The `ViewportOrientationMenuWrapper` is accessing
`cornerstoneViewportService` through chained property lookup instead of the
standard destructuring pattern. Update the service lookup in this component by
destructuring `cornerstoneViewportService` from `servicesManager.services`
before calling `getViewportInfo`, matching the established pattern used
elsewhere and keeping the service reference easy to locate.

Source: Coding guidelines


26-28: 📐 Maintainability & Code Quality | 🔵 Trivial

Remove redundant string literal fallback

Enums.ViewportType.VOLUME_3D is defined as the string 'volume3d', making the explicit string comparison redundant.

(https://github.com/CodeRabbit-Inc/assistant/blob/main/docs/classification_tags.md#optional_refactors_recommended)

🤖 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/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx`
around lines 26 - 28, Remove the redundant fallback string check in
ViewportOrientationMenuWrapper by relying on Enums.ViewportType.VOLUME_3D alone
when computing isVolume3D. Update the condition in the viewport type comparison
so it only uses the enum constant, since it already resolves to the same value
and this keeps the logic centralized and easier to maintain.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@extensions/cornerstone/src/commandsModule.ts`:
- Around line 3174-3179: The `definitions` object in `commandsModule.ts`
contains duplicate entries for `setViewport3DViewDirection` and
`rotateViewport3DBy`, with this object-form block being overwritten by the later
raw-function definitions. Remove one of the two definitions for each
command—prefer keeping the intended single form in `definitions`—so there is
only one entry per command and the dead code/lint conflict is eliminated.
- Around line 1768-1806: The volume load and viewport restore flow is split
across separate error handling, so a rejection from
volumeLoader.createAndCacheVolume or later restoration can leave affected
viewports blank. Move the creation of newVolume, the load call, and the viewport
restoration loop into one unified try/catch around this whole sequence in
commandsModule.ts, using the existing cleanup/recovery path and
uiNotificationService to report failures. Keep the old-volume cleanup and cache
removal in the same recovery flow so the actors are re-added whenever loading or
restoration fails.

In `@extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx`:
- Around line 55-104: The volume state in VolumeOptions.tsx is only recalculated
on viewportId/service changes, so it misses reloads when the underlying volume
is swapped in place. Update the VolumeOptions component’s useEffect around
getCornerstoneViewport and cs3DCache.getVolume to also react to volume updates
by subscribing to cornerstoneViewportService’s VIEWPORT_DATA_CHANGED event, or
otherwise depend on the current volumeId if it changes on reload. Make sure the
effect reruns the existing dimension/decimation recalculation and cleanup is
handled when the subscription is removed.

In `@extensions/cornerstone/src/constants/index.ts`:
- Line 2: The default volume loader scheme is causing double-prefixed decimated
IDs and breaking loader resolution. Update the fallback in
CornerstoneCacheService and the decimated ID construction logic so the generated
baseVolumeId and the decimatedVolumeLoader ID are not prefixed twice; use a
standard scheme for the default case or make the prefixing conditional in the
code paths around VOLUME_LOADER_SCHEME and the decimated ID builder.

In `@extensions/cornerstone/src/init.tsx`:
- Around line 71-96: The startup guard in init.tsx is treating the synthetic
“skipped” gpuTestResults from the Cypress/CPU path as a completed probe, which
blocks later real benchmarking. Update the logic around getVolumeOptions and
setVolumeOptions so skipped results are distinguishable from actual GPU test
output, and only bypass gpuPerformanceTest when a genuine benchmark has already
been recorded; keep the existing gpuPerformanceTest(appConfig) path available
when rendering mode changes.

In `@extensions/cornerstone/src/initCornerstoneTools.js`:
- Around line 191-193: The OrientationControllerTool mapping in
initCornerstoneTools still unconditionally reads toolName, which can throw if
the tool is missing despite the earlier guard. Update the
OrientationControllerTool entry to use optional chaining (matching the defensive
check around OrientationControllerTool) so module initialization won’t crash
when the export is absent or incompatible.

In
`@extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts`:
- Around line 116-138: Invalidate decimated volume cache entries alongside the
existing base and suffixed volume IDs in CornerstoneCacheService’s invalidation
flow. Update the candidate ID collection in the volume invalidation logic to
also derive and delete the decimated cache keys used by decimatedVolumeLoader
for the same baseVolumeId variants, so stale decimated volumes are removed from
cs3DCache._volumeCache and any related image load objects are cleaned up
consistently.
- Around line 326-354: The volume caching logic in CornerstoneCacheService
should resolve the decimated volume ID before the cache check, because the
current `(!volumeImageIds || !volume)` guard can skip the decimated-loader path
when a base volume already exists and cause full-resolution reuse. Update the
flow around the volume lookup in the cache creation branch so the requested
`ijkDecimation` is computed first, then derive and check the appropriate
`volumeId` (including the decimated `decimatedVolumeLoader:` variant) before
deciding whether to reuse or create the volume. Focus on the logic in the volume
creation block that uses `volumeImageIds`, `volumeId`, and `useDecimatedLoader`.

In
`@extensions/cornerstone/src/utils/decimation/applyAutoDecimationIfNecessary.ts`:
- Around line 200-217: The auto-decimation check in
applyAutoDecimationIfNecessary is short-circuiting too early because it uses
displaySets.some(), so maxVoxelCount may not reflect the largest volume in the
viewport. Update the logic to iterate over all display sets in
applyAutoDecimationIfNecessary, still tracking the maximum voxel count from
estimateDisplaySetVoxelCount, and then decide whether decimation is needed after
the full scan rather than inside the loop.

In `@extensions/cornerstone/src/utils/decimation/constants.ts`:
- Line 3: Update AUTO_DECIMATION_VIEWPORT_TYPES in the decimation constants so
it matches the viewportType values returned by resolveViewportType; replace the
incorrect 'volume' entry with 'orthographic' so
AUTO_DECIMATION_VIEWPORT_TYPES.has(viewportType) works for orthographic
viewports and auto-decimation runs as intended.

In `@extensions/cornerstone/src/utils/gpuPerformanceTest.ts`:
- Around line 528-553: The GPU sampling tier logic is misaligned with the
benchmark thresholds, causing `getGpuTier()` and `getSamplingDefaults()` to
assign the wrong defaults for most fast GPUs. Update `getGpuTier()` in
`gpuPerformanceTest.ts` to use the same cutoff values as `getPerformanceLevel()`
(for example, the `80+` and `60+` ranges) so `high`, `mid`, and `low` match the
benchmark scoring, and keep the tier-to-default mapping in
`getSamplingDefaults()` consistent with that corrected tiering.

In `@extensions/cornerstone/src/Viewport/Overlays/AutoDecimationOverlay.tsx`:
- Around line 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.

In
`@extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts`:
- Around line 4-7: getImageIndexFromEvent currently destructures event.detail
directly, which can throw if detail is missing. Update this helper to
defensively read from event?.detail, matching the pattern used by
getImageIdFromCacheEvent, and return undefined when no detail payload is
present. Keep the existing fallback logic for imageIndex, newImageIdIndex, and
imageIdIndex inside the guarded detail access.

In `@extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx`:
- Around line 492-493: The StudyBrowser panel is using ds.numImageFrames
directly for numInstances, which can leave the value undefined when frame
metadata is missing. Update the PanelStudyBrowser assignment to match the
tracking panel’s fallback behavior by using ds.numImageFrames ??
ds.instances?.length, keeping the numInstances handling consistent and resilient
across PanelStudyBrowser and PanelStudyBrowserTracking.

In `@platform/core/src/utils/volumeOptionsStorage.ts`:
- Around line 42-46: `setVolumeOptions` currently writes to `localStorage`
without protection, so startup flows that call it can fail if storage is
unavailable or full. Update `setVolumeOptions` in `volumeOptionsStorage` to
mirror the defensive pattern used by `getVolumeOptions`: wrap the
merge-and-`localStorage.setItem` path in try/catch, and safely no-op or log on
failure so GPU probe startup from `init.tsx`/`gpuPerformanceTest.ts` cannot
throw an unhandled error. Keep the behavior localized to `setVolumeOptions` and
preserve the existing `getVolumeOptions` fallback semantics.

In `@platform/docs/docs/configuration/configurationFiles.md`:
- Line 136: The documentation entry for volumeAutoDecimationThreshold
incorrectly claims a default is defined in config/default.js. Update the text in
configurationFiles.md to remove that file reference or replace it with the
correct source of truth, and make sure the description matches the actual
behavior exposed by volumeAutoDecimationThreshold rather than attributing a
default value to default.js.

In `@platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json`:
- Line 10: The “Volume Options” localization key is missing from the ru and ar
locale sets, so add it to those locale JSON files to match the existing entries
in en-US, fr, nl, zh, and test-LNG. Use the same key name and add appropriate
translated values alongside the other WindowLevelActionMenu strings so the
locale coverage stays consistent across all supported languages.

In `@platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx`:
- Around line 178-189: The SmartScrollbar state logic keeps the track expanded
when the view never enters loading, because `wasEverLoading.current` stays false
and `hasSettled` never flips back. Update the `SmartScrollbar` component so
`hasSettled` starts as true by default, while still letting the `useEffect`
reset it to false during loading and settle it after loading in the existing
`isLoading` flow. This will let `isExpanded` and `effectiveWidth` contract
correctly for instantly loaded viewports.
- Around line 320-342: The SmartScrollbar slider is always focusable because
`tabIndex={0}` is unconditional, but `handleKeyDown` is only attached when
`enableKeyboardNavigation` is true, so `ViewportSliceProgressScrollbar` can
expose a non-operable keyboard target. Update the `SmartScrollbar` component so
the tab stop is gated by the same `enableKeyboardNavigation` flag used for
`onKeyDown`, keeping the slider focusable only when keyboard interaction is
actually supported.

---

Outside diff comments:
In
`@extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts`:
- Around line 146-150: The invalidation reload path in CornerstoneCacheService
drops viewport-specific options because _getVolumeViewportData is called without
viewportData.viewportOptions. Update the reload flow to pass the existing
viewportOptions through this call so auto/manual ijkDecimation and
autoDecimationInfo are preserved when rebuilding the viewport data.

In `@extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx`:
- Around line 284-304: `optionsToUse` now depends on decimation settings, but
the memoization check in `areEqual` does not include `ijkDecimation` and
`autoDecimationInfo`, so prop-only decimation updates can be skipped. Update
`areEqual` in `OHIFCornerstoneViewport` to compare those fields alongside the
existing viewport props, ensuring changes force `setViewportData` to run when
decimation options change.

In `@modes/basic/src/initToolGroups.ts`:
- Around line 296-338: The active tool bindings in initToolGroups.ts have a
mouse conflict: VolumeCropping and TrackballRotateTool both use
Enums.MouseBindings.Primary, so the first tool wins and rotation becomes
unreachable. Update the tools.active configuration by removing the plain Primary
binding from toolNames.TrackballRotateTool or moving it behind a modifier key,
while keeping VolumeCropping on Primary, so both tools can be used without
shadowing each other.

---

Nitpick comments:
In
`@extensions/cornerstone/src/components/ViewportOrientationMenu/Viewport3DCompassMenu.tsx`:
- Around line 26-29: The Compass menu’s local `spin` state is initialized once
in `Viewport3DCompassMenu` from `getVolume3dSpin(viewportIdToUse)` and can
become stale when `viewportIdToUse` changes or the store updates elsewhere.
Update this component to read the spin value from the authoritative store on
every relevant change, ideally by mirroring `Volume3dSpinDriver` with
`useSyncExternalStore` (or equivalent subscription-based state) keyed by the
current viewport id. Keep the UI bound to the current viewport’s store value
instead of the one-time `useState` initializer.

In
`@extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx`:
- Around line 21-24: The `ViewportOrientationMenuWrapper` is accessing
`cornerstoneViewportService` through chained property lookup instead of the
standard destructuring pattern. Update the service lookup in this component by
destructuring `cornerstoneViewportService` from `servicesManager.services`
before calling `getViewportInfo`, matching the established pattern used
elsewhere and keeping the service reference easy to locate.
- Around line 26-28: Remove the redundant fallback string check in
ViewportOrientationMenuWrapper by relying on Enums.ViewportType.VOLUME_3D alone
when computing isVolume3D. Update the condition in the viewport type comparison
so it only uses the enum constant, since it already resolves to the same value
and this keeps the logic centralized and easier to maintain.

In `@extensions/cornerstone/src/types/ViewportPresets.ts`:
- Around line 55-60: The VolumeDecimationInfo type is using an overly broad
dimensions array, which should be narrowed for consistency with the other
3-value tuples in this type. Update the dimensions property in
VolumeDecimationInfo to use a 3-tuple/Point3-style type, matching ijkDecimation
and appliedDecimation.originalDecimation, so the shape is fixed and cannot
accept malformed lengths.

In `@platform/core/src/utils/index.ts`:
- Around line 52-55: The utils export surface is inconsistent because
getVolumeOptions and setVolumeOptions are only re-exported as named exports from
index.ts and are missing from the utils aggregate object. Update the utils
object in index.ts to include these volumeOptionsStorage helpers alongside the
other utility entries so OHIF.utils.getVolumeOptions and
OHIF.utils.setVolumeOptions resolve correctly; use the existing
getVolumeOptions/setVolumeOptions imports and keep the named exports intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 90369be4-028a-441b-8857-a4c71aeaa28d

📥 Commits

Reviewing files that changed from the base of the PR and between becd208 and 8b878dd.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (75)
  • extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
  • extensions/cornerstone/src/Viewport/Overlays/AutoDecimationOverlay.tsx
  • extensions/cornerstone/src/Viewport/Overlays/CornerstoneOverlays.tsx
  • extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx
  • extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar.tsx
  • extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts
  • extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts
  • extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/index.ts
  • extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/types.ts
  • extensions/cornerstone/src/commandsModule.ts
  • extensions/cornerstone/src/components/ViewportOrientationMenu/Viewport3DCompassMenu.tsx
  • extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenuWrapper.tsx
  • extensions/cornerstone/src/components/ViewportOrientationMenu/Volume3dSpinDriver.tsx
  • extensions/cornerstone/src/components/ViewportOrientationMenu/viewport3dSpinStore.ts
  • extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeCropping.tsx
  • extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx
  • extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptionsMenuWrapper.tsx
  • extensions/cornerstone/src/components/VolumeOptionsMenu/index.ts
  • extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx
  • extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx
  • extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx
  • extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx
  • extensions/cornerstone/src/constants/index.ts
  • extensions/cornerstone/src/customizations/viewportScrollbarCustomization.tsx
  • extensions/cornerstone/src/getCustomizationModule.tsx
  • extensions/cornerstone/src/getToolbarModule.tsx
  • extensions/cornerstone/src/hooks/useViewportRendering.tsx
  • extensions/cornerstone/src/index.tsx
  • extensions/cornerstone/src/init.tsx
  • extensions/cornerstone/src/initCornerstoneTools.js
  • extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts
  • extensions/cornerstone/src/services/ViewedDataService/ViewedDataService.ts
  • extensions/cornerstone/src/services/ViewedDataService/index.ts
  • extensions/cornerstone/src/services/ViewportService/Viewport.ts
  • extensions/cornerstone/src/types/AppTypes.ts
  • extensions/cornerstone/src/types/CornerstoneCacheService.ts
  • extensions/cornerstone/src/types/ViewportPresets.ts
  • extensions/cornerstone/src/utils/decimation/applyAutoDecimationIfNecessary.ts
  • extensions/cornerstone/src/utils/decimation/constants.ts
  • extensions/cornerstone/src/utils/gpuPerformanceTest.ts
  • extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx
  • extensions/default/src/getSopClassHandlerModule.js
  • extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx
  • modes/basic/src/index.tsx
  • modes/basic/src/initToolGroups.ts
  • modes/basic/src/toolbarButtons.ts
  • package.json
  • platform/app/public/config/default.js
  • platform/app/public/config/netlify.js
  • platform/core/src/index.ts
  • platform/core/src/services/ServicesManager.ts
  • platform/core/src/types/AppTypes.ts
  • platform/core/src/types/ViewportGridType.ts
  • platform/core/src/utils/index.ts
  • platform/core/src/utils/volumeOptionsStorage.ts
  • platform/docs/docs/configuration/configurationFiles.md
  • platform/docs/docs/platform/services/ui/viewport-action-menu.md
  • platform/i18n/src/locales/en-US/WindowLevelActionMenu.json
  • platform/i18n/src/locales/fr/WindowLevelActionMenu.json
  • platform/i18n/src/locales/nl/WindowLevelActionMenu.json
  • platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json
  • platform/i18n/src/locales/zh/WindowLevelActionMenu.json
  • platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx
  • platform/ui-next/src/components/SmartScrollbar/SmartScrollbarEndpoints.tsx
  • platform/ui-next/src/components/SmartScrollbar/SmartScrollbarFill.tsx
  • platform/ui-next/src/components/SmartScrollbar/SmartScrollbarIndicator.tsx
  • platform/ui-next/src/components/SmartScrollbar/SmartScrollbarTrack.tsx
  • platform/ui-next/src/components/SmartScrollbar/defaultSmartScrollbarIndicatorConfig.tsx
  • platform/ui-next/src/components/SmartScrollbar/index.ts
  • platform/ui-next/src/components/SmartScrollbar/useByteArray.ts
  • platform/ui-next/src/components/SmartScrollbar/utils.test.ts
  • platform/ui-next/src/components/SmartScrollbar/utils.ts
  • platform/ui-next/src/components/Thumbnail/Thumbnail.tsx
  • platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx
  • platform/ui-next/src/components/index.ts
💤 Files with no reviewable changes (1)
  • platform/app/public/config/default.js

Comment on lines +1768 to +1806
try {
const oldVolume = cache.getVolume(currentVolumeId);
if (oldVolume) {
if (
typeof (oldVolume as { cancelLoading?: () => void })
.cancelLoading === 'function'
) {
(oldVolume as { cancelLoading: () => void }).cancelLoading();
}
if (
(oldVolume as { imageData?: { delete: () => void } }).imageData
) {
(oldVolume as { imageData: { delete: () => void } }).imageData.delete();
}
}
cache.removeVolumeLoadObject(currentVolumeId);
} catch (e) {
console.error('Failed to remove old volume from cache:', e);
}

const parts = currentVolumeId.split(':');
const baseVolumeId =
parts[0] === 'decimatedVolumeLoader' && parts.length >= 3
? parts.slice(1, -1).join(':')
: currentVolumeId;
const decimationSuffix = decimationValues.join('_');
const newVolumeId = `decimatedVolumeLoader:${baseVolumeId}:${decimationSuffix}`;

const newVolume = await volumeLoader.createAndCacheVolume(newVolumeId, {
imageIds: originalImageIds,
progressiveRendering: true,
ijkDecimation: decimationValues,
});
const volumeWithLoad = newVolume as unknown as {
load?: () => void | Promise<void>;
};
if (typeof volumeWithLoad.load === 'function') {
await Promise.resolve(volumeWithLoad.load());
}

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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@cornerstonejs/core 4.20.0 volumeLoader.createAndCacheVolume rejection behavior

💡 Result:

In Cornerstone.js (@cornerstonejs/core), the createAndCacheVolume function is an asynchronous operation that returns a Promise [1][2]. The behavior regarding rejection is as follows: 1. Promise Rejection: The promise returned by createAndCacheVolume will reject if an error occurs during the volume loading process [1][2]. This includes scenarios where the underlying volume loader fails to fetch or initialize the volume data (e.g., due to network issues, invalid image IDs, or processing errors) [1][2][3]. 2. Input Validation: The function explicitly checks if the provided volumeId is undefined at the beginning of its execution. If it is, it throws an Error synchronously: "createAndCacheVolume: parameter volumeId must not be undefined" [1][2]. 3. Caching Behavior: The function first checks the cache using cache.getVolumeLoadObject(volumeId). If a volume load object already exists for that ID, it returns the existing promise, which may already be resolved or rejected [1][2]. If not, it triggers the loading process via the registered volume loader, caches the resulting load object, and returns its promise [1][2]. Because this is a Promise-based API, you should handle rejections using standard JavaScript patterns, such as a.catch block or a try-catch statement within an async function [4][5]: try { const volume = await volumeLoader.createAndCacheVolume(volumeId, options); volume.load; } catch (error) { console.error("Failed to create or cache volume:", error); } Note: If you encounter specific errors during loading, such as "Array buffer allocation failed" or "attempt to read past end of buffer," these typically indicate issues with the data payload, browser memory limits, or incompatible DICOM transfer syntaxes, rather than the rejection logic of the createAndCacheVolume function itself [6][7].

Citations:


🏁 Script executed:

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const fileBuffer = fs.readFileSync('extensions/cornerstone/src/commandsModule.ts', 'utf-8');
const lines = fileBuffer.split('\n');
let startLine = 1756; // 1-based index from original comment
const endLine = 1810;

// We need to extract the surrounding logic to see if the try-catch covers the entire "remove old -> create new -> restore" sequence.
// The original comment mentioned:
// 1. Actors removed (Lines 1756-1766)
// 2. Old volume removed (Line 1768)
// 3. New volume created/loaded (Line 1800+)

// Let's print the specific block from 1750 to 1810 to see the control flow.
const snippet = lines.slice(startLine - 1, endLine).join('\n');
console.log(`Snippet from lines ${startLine}-${endLine}:\n`);
console.log(snippet);

Repository: OHIF/Viewers

Length of output: 245


🏁 Script executed:

node - <<'JS'
const fs = require('fs');
const path = require('path');

try {
  const fileBuffer = fs.readFileSync('extensions/cornerstone/src/commandsModule.ts', 'utf-8');
  const lines = fileBuffer.split('\n');
  
  // We need to check the control flow. 
  // The review comment suggested that actors are removed (1756-1766), 
  // then volume removed (1768+), then create is called (1800+).
  // We need to see if the create is inside a try block that wraps the whole operation.
  
  const startLine = 1750;
  const endLine = 1820;
  
  console.log(`Code around lines ${startLine}-${endLine}:\n`);
  for (let i = startLine - 1; i < Math.min(endLine, lines.length); i++) {
    const num = (i + 1).toString().padEnd(5, ' ');
    console.log(`${num} | ${lines[i]}`);
  }
} catch (e) {
  console.error("Error reading file:", e.message);
}
JS

Repository: OHIF/Viewers

Length of output: 3300


Wrap volume creation and viewport restoration in a unified exception handler.

The current code removes actors from all affected viewports (Lines 1756-1766) and cleans up the old volume (Lines 1768-1786), but the await volumeLoader.createAndCacheVolume call (Line 1796) and the subsequent viewport restoration loop (Lines 1811+) fall outside the try/catch block.

Since createAndCacheVolume returns a Promise that rejects on failure (e.g., network errors, invalid data), any rejection will skip the restoration loop entirely, leaving the viewports blank.

Modify the code to wrap both the volume loading logic and the viewport restoration loop in a single try/catch block to ensure actors are re-added or an error is reported via uiNotificationService if loading fails.

🤖 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/commandsModule.ts` around lines 1768 - 1806, The
volume load and viewport restore flow is split across separate error handling,
so a rejection from volumeLoader.createAndCacheVolume or later restoration can
leave affected viewports blank. Move the creation of newVolume, the load call,
and the viewport restoration loop into one unified try/catch around this whole
sequence in commandsModule.ts, using the existing cleanup/recovery path and
uiNotificationService to report failures. Keep the old-volume cleanup and cache
removal in the same recovery flow so the actors are re-added whenever loading or
restoration fails.

Comment on lines +3174 to +3179
setViewport3DViewDirection: {
commandFn: actions.setViewport3DViewDirection,
},
rotateViewport3DBy: {
commandFn: actions.rotateViewport3DBy,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate command definitions for setViewport3DViewDirection and rotateViewport3DBy.

Both keys are defined here (object { commandFn } form) and again at Lines 3318-3319 (raw-function form) inside the same definitions object. The later entries silently overwrite these, making this block dead code and tripping the lint error. Remove one set to avoid confusion.

Proposed fix (drop the duplicate object-form entries)
     setSampleDistanceMultiplier: {
       commandFn: actions.setSampleDistanceMultiplier,
     },
-    setViewport3DViewDirection: {
-      commandFn: actions.setViewport3DViewDirection,
-    },
-    rotateViewport3DBy: {
-      commandFn: actions.rotateViewport3DBy,
-    },
     setVolumeRenderingQuality: {
       commandFn: actions.setVolumeRenderingQuality,
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setViewport3DViewDirection: {
commandFn: actions.setViewport3DViewDirection,
},
rotateViewport3DBy: {
commandFn: actions.rotateViewport3DBy,
},
setSampleDistanceMultiplier: {
commandFn: actions.setSampleDistanceMultiplier,
},
setVolumeRenderingQuality: {
commandFn: actions.setVolumeRenderingQuality,
},
🧰 Tools
🪛 Biome (2.5.0)

[error] 3174-3176: This property is later overwritten by an object member with the same name.

(lint/suspicious/noDuplicateObjectKeys)


[error] 3177-3179: This property is later overwritten by an object member with the same name.

(lint/suspicious/noDuplicateObjectKeys)

🤖 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/commandsModule.ts` around lines 3174 - 3179, The
`definitions` object in `commandsModule.ts` contains duplicate entries for
`setViewport3DViewDirection` and `rotateViewport3DBy`, with this object-form
block being overwritten by the later raw-function definitions. Remove one of the
two definitions for each command—prefer keeping the intended single form in
`definitions`—so there is only one entry per command and the dead code/lint
conflict is eliminated.

Source: Linters/SAST tools

Comment on lines +55 to +104
useEffect(() => {
try {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) return;
const actors = viewport.getActors();
if (!actors?.length) return;

const volumeActor = actors.find(
(a: { actor?: { getClassName?: () => string }; referencedId?: string }) =>
a?.actor?.getClassName?.() === 'vtkVolume'
);
if (!volumeActor?.referencedId) return;

const volumeId = volumeActor.referencedId;
const volume = cs3DCache.getVolume(volumeId) as
| VolumeDecimationInfo
| undefined;
if (!volume?.dimensions || volume.dimensions.length < 3) return;

const dimensions = volume.dimensions;
const currentIjk = getIjkDecimationFromVolumeId(volumeId);
const meta = volume.originalMetadata;

const inPlane = Math.max(1, Math.min(currentIjk[0], MAX_IN_PLANE_DECIMATION));
const kAxis = Math.max(1, Math.min(currentIjk[2], MAX_K_AXIS_DECIMATION));
if (meta) {
const originalDims: [number, number, number] = [
meta.Columns ?? dimensions[0],
meta.Rows ?? dimensions[1],
Math.floor(dimensions[2] * currentIjk[2]),
];
setVolumeDimensions(originalDims);
setCurrentInPlaneDecimation(inPlane);
setCurrentKAxisDecimation(kAxis);
} else {
const originalDims: [number, number, number] = [
dimensions[0] * currentIjk[0],
dimensions[1] * currentIjk[1],
dimensions[2] * currentIjk[2],
];
setVolumeDimensions(originalDims);
setCurrentInPlaneDecimation(inPlane);
setCurrentKAxisDecimation(kAxis);
}
} catch {
setVolumeDimensions(null);
setCurrentInPlaneDecimation(1);
setCurrentKAxisDecimation(1);
}
}, [viewportId, cornerstoneViewportService]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find events emitted when a decimated volume is reloaded/swapped.
rg -nP "reloadVolumeWithDecimation|VIEWPORT_DATA_CHANGED" extensions/cornerstone/src/commandsModule.ts -A3 -B1

Repository: OHIF/Viewers

Length of output: 1291


🏁 Script executed:

#!/bin/bash
# Read the implementation of reloadVolumeWithDecimation to verify if VIEWPORT_DATA_CHANGED is emitted there.
sed -n '1625,1700p' extensions/cornerstone/src/commandsModule.ts

Repository: OHIF/Viewers

Length of output: 2649


🏁 Script executed:

#!/bin/bash
# Check if VolumeOptions.tsx subscribes to VIEWPORT_DATA_CHANGED or includes volumeId in deps.
rg -n "VIEWPORT_DATA_CHANGED\|SUBSCRIBE" extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx -A2 -B2

Repository: OHIF/Viewers

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Check if the `cornerstoneViewportService` exposes the event emission mechanism used elsewhere to ensure we understand how to subscribe.
rg -n "eventsListenerService\|services.*events" extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx | head -20

Repository: OHIF/Viewers

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Inspect imports and initialization in VolumeOptions.tsx to identify the available services and service manager
head -50 extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx

Repository: OHIF/Viewers

Length of output: 2329


🏁 Script executed:

#!/bin/bash
# Verify if the component subscribes to events, specifically looking for useEventListener or similar patterns.
# Also check the specific lines around the useEffect to see the full context.
sed -n '50,110p' extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx

Repository: OHIF/Viewers

Length of output: 2391


🏁 Script executed:

#!/bin/bash
# Check how other components interact with events (subscribe) to understand the pattern used in extension.
rg -n "subscribe\|VIEWPORT_DATA_CHANGED" extensions/cornerstone/src/components/ -A3 -B1 | head -60

Repository: OHIF/Viewers

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Search for service subscription patterns in the cornerstone extension to see how events are typically handled.
rg -n "servicesManager.services" extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptionsMenu.tsx extensions/cornerstone/src/components/VolumeOptionsMenu/VolumeOptions.tsx -A 10 -B 3

Repository: OHIF/Viewers

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Verify how events are subscribed to in other parts of the extension, specifically looking for the event service or similar.
rg -n "subscribe" extensions/cornerstone/src/ --type ts -A 2 -B 1 | head -40

Repository: OHIF/Viewers

Length of output: 3702


Effect does not update volume dimensions after a reload.

The useEffect dependency array [viewportId, cornerstoneViewportService] prevents the callback from re-running when the underlying volume is swapped or reloaded (e.g., via reloadVolumeWithDecimation). Since viewportId remains constant, the component continues to display the dimensions and decimation values calculated during the initial render, even though cs3DCache contains the updated volume data.

To fix this, subscribe to the VIEWPORT_DATA_CHANGED event from cornerstoneViewportService to trigger the calculation upon volume updates, or ensure the hook depends on the specific volumeId if it changes upon reload.

Current Effect Code
  useEffect(() => {
    try {
      const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
      if (!viewport) return;
      // ... logic to get volume and set state
    } catch {
      // ... error handling
    }
  }, [viewportId, cornerstoneViewportService]);
🤖 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/components/VolumeOptionsMenu/VolumeOptions.tsx`
around lines 55 - 104, The volume state in VolumeOptions.tsx is only
recalculated on viewportId/service changes, so it misses reloads when the
underlying volume is swapped in place. Update the VolumeOptions component’s
useEffect around getCornerstoneViewport and cs3DCache.getVolume to also react to
volume updates by subscribing to cornerstoneViewportService’s
VIEWPORT_DATA_CHANGED event, or otherwise depend on the current volumeId if it
changes on reload. Make sure the effect reruns the existing dimension/decimation
recalculation and cleanup is handled when the subscription is removed.

// Volume loader schemes
export const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
export const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; No newline at end of file
export const VOLUME_LOADER_SCHEME = 'decimatedVolumeLoader';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Confirm the loader is registered under this scheme
rg -nP "registerVolumeLoader\(\s*['\"]decimatedVolumeLoader" --type=ts -C2
# 2) Find all consumers of VOLUME_LOADER_SCHEME / DEFAULT_VOLUME_LOADER_SCHEME to assess fallback impact
rg -nP '\bVOLUME_LOADER_SCHEME\b' --type=ts -C2
rg -nP 'DEFAULT_VOLUME_LOADER_SCHEME\s*=' --type=ts -C1
# 3) Inspect where displaySet.volumeLoaderSchema is assigned (does it ever stay unset?)
rg -nP 'volumeLoaderSchema\s*[:=]' --type=ts --type=js -C1

Repository: OHIF/Viewers

Length of output: 150


Fix the double-prefixed volume ID causing decimation to fail or load incorrect data.

Changing VOLUME_LOADER_SCHEME to 'decimatedVolumeLoader' creates a logic error:

  • CornerstoneCacheService defaults to this scheme if volumeLoaderSchema is unset, constructing baseVolumeId as decimatedVolumeLoader:uid.
  • Logic generating the decimated ID blindly prepends decimatedVolumeLoader: again, resulting in decimatedVolumeLoader:decimatedVolumeLoader:uid:....
  • The decimatedVolumeLoader implementation expects a single prefix; the extra prefix corrupts the parsed volume ID, preventing the loader from resolving data.

Use a standard scheme (e.g., cornerstoneStreamingImageVolume) for the default fallback or update the ID construction logic to handle the schema conditionally.

🤖 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/constants/index.ts` at line 2, The default volume
loader scheme is causing double-prefixed decimated IDs and breaking loader
resolution. Update the fallback in CornerstoneCacheService and the decimated ID
construction logic so the generated baseVolumeId and the decimatedVolumeLoader
ID are not prefixed twice; use a standard scheme for the default case or make
the prefixing conditional in the code paths around VOLUME_LOADER_SCHEME and the
decimated ID builder.

Comment on lines +71 to +96
const volumeOptions = getVolumeOptions();
if (!volumeOptions.gpuTestResults) {
try {
const isCypress =
typeof window !== 'undefined' && Boolean((window as any).Cypress);

if (isCypress || Boolean(appConfig.useCPURendering)) {
// Avoid WebGL/GPU probing in Cypress/CI runs, or when CPU rendering is forced.
setVolumeOptions({
gpuTestResults: {
generalPerformanceScore: 100,
renderer: isCypress
? 'Cypress (GPU test skipped)'
: 'CPU rendering forced (GPU test skipped)',
maxTextureSize: 0,
memoryUsedMB: 0,
memoryLimitMB: 0,
triangleRendering: 0,
textureUpload: 0,
bufferOperations: 0,
},
rotateSampleDistanceFactor: 2,
sampleDistanceMultiplier: 1,
});
} else {
await gpuPerformanceTest(appConfig);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t let skipped GPU probes suppress future real probes.

The guard only checks gpuTestResults, but the Cypress/CPU path stores a fake “skipped” result. If rendering mode changes later, startup will keep skipping the real benchmark and retain the high-score sampling defaults.

Proposed fix direction
   const volumeOptions = getVolumeOptions();
-  if (!volumeOptions.gpuTestResults) {
+  const isCypress =
+    typeof window !== 'undefined' && Boolean((window as any).Cypress);
+  const shouldSkipGpuTest = isCypress || Boolean(appConfig.useCPURendering);
+  const hasUsableGpuTestResults =
+    volumeOptions.gpuTestResults &&
+    (shouldSkipGpuTest ||
+      !String(volumeOptions.gpuTestResults.renderer).includes('GPU test skipped'));
+
+  if (!hasUsableGpuTestResults) {
     try {
-      const isCypress =
-        typeof window !== 'undefined' && Boolean((window as any).Cypress);
-
-      if (isCypress || Boolean(appConfig.useCPURendering)) {
+      if (shouldSkipGpuTest) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const volumeOptions = getVolumeOptions();
if (!volumeOptions.gpuTestResults) {
try {
const isCypress =
typeof window !== 'undefined' && Boolean((window as any).Cypress);
if (isCypress || Boolean(appConfig.useCPURendering)) {
// Avoid WebGL/GPU probing in Cypress/CI runs, or when CPU rendering is forced.
setVolumeOptions({
gpuTestResults: {
generalPerformanceScore: 100,
renderer: isCypress
? 'Cypress (GPU test skipped)'
: 'CPU rendering forced (GPU test skipped)',
maxTextureSize: 0,
memoryUsedMB: 0,
memoryLimitMB: 0,
triangleRendering: 0,
textureUpload: 0,
bufferOperations: 0,
},
rotateSampleDistanceFactor: 2,
sampleDistanceMultiplier: 1,
});
} else {
await gpuPerformanceTest(appConfig);
const volumeOptions = getVolumeOptions();
const isCypress =
typeof window !== 'undefined' && Boolean((window as any).Cypress);
const shouldSkipGpuTest = isCypress || Boolean(appConfig.useCPURendering);
const hasUsableGpuTestResults =
volumeOptions.gpuTestResults &&
(shouldSkipGpuTest ||
!String(volumeOptions.gpuTestResults.renderer).includes('GPU test skipped'));
if (!hasUsableGpuTestResults) {
try {
if (shouldSkipGpuTest) {
// Avoid WebGL/GPU probing in Cypress/CI runs, or when CPU rendering is forced.
setVolumeOptions({
gpuTestResults: {
generalPerformanceScore: 100,
renderer: isCypress
? 'Cypress (GPU test skipped)'
: 'CPU rendering forced (GPU test skipped)',
maxTextureSize: 0,
memoryUsedMB: 0,
memoryLimitMB: 0,
triangleRendering: 0,
textureUpload: 0,
bufferOperations: 0,
},
rotateSampleDistanceFactor: 2,
sampleDistanceMultiplier: 1,
});
} else {
await gpuPerformanceTest(appConfig);
🤖 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/init.tsx` around lines 71 - 96, The startup guard
in init.tsx is treating the synthetic “skipped” gpuTestResults from the
Cypress/CPU path as a completed probe, which blocks later real benchmarking.
Update the logic around getVolumeOptions and setVolumeOptions so skipped results
are distinguishable from actual GPU test output, and only bypass
gpuPerformanceTest when a genuine benchmark has already been recorded; keep the
existing gpuPerformanceTest(appConfig) path available when rendering mode
changes.

Comment on lines +42 to +46
export function setVolumeOptions(partial: Partial<VolumeOptions>): void {
const current = getVolumeOptions();
const merged = { ...current, ...partial };
localStorage.setItem(VOLUME_OPTIONS_KEY, JSON.stringify(merged));
}

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 | ⚡ Quick win

Guard setVolumeOptions against localStorage failures.

getVolumeOptions is wrapped in try/catch, but setVolumeOptions is not. localStorage.setItem can throw (Safari private mode, disabled storage, quota exceeded). This setter is invoked during GPU-probe startup (init.tsx/gpuPerformanceTest.ts), so an exception here could surface as an unhandled startup error.

🛡️ Proposed fix
 export function setVolumeOptions(partial: Partial<VolumeOptions>): void {
-  const current = getVolumeOptions();
-  const merged = { ...current, ...partial };
-  localStorage.setItem(VOLUME_OPTIONS_KEY, JSON.stringify(merged));
+  try {
+    const current = getVolumeOptions();
+    const merged = { ...current, ...partial };
+    localStorage.setItem(VOLUME_OPTIONS_KEY, JSON.stringify(merged));
+  } catch {
+    // storage unavailable (private mode / quota); ignore
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function setVolumeOptions(partial: Partial<VolumeOptions>): void {
const current = getVolumeOptions();
const merged = { ...current, ...partial };
localStorage.setItem(VOLUME_OPTIONS_KEY, JSON.stringify(merged));
}
export function setVolumeOptions(partial: Partial<VolumeOptions>): void {
try {
const current = getVolumeOptions();
const merged = { ...current, ...partial };
localStorage.setItem(VOLUME_OPTIONS_KEY, JSON.stringify(merged));
} catch {
// storage unavailable (private mode / quota); ignore
}
}
🤖 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 `@platform/core/src/utils/volumeOptionsStorage.ts` around lines 42 - 46,
`setVolumeOptions` currently writes to `localStorage` without protection, so
startup flows that call it can fail if storage is unavailable or full. Update
`setVolumeOptions` in `volumeOptionsStorage` to mirror the defensive pattern
used by `getVolumeOptions`: wrap the merge-and-`localStorage.setItem` path in
try/catch, and safely no-op or log on failure so GPU probe startup from
`init.tsx`/`gpuPerformanceTest.ts` cannot throw an unhandled error. Keep the
behavior localized to `setVolumeOptions` and preserve the existing
`getVolumeOptions` fallback semantics.

- `omitQuotationForMultipartRequest`: Some servers (e.g., .NET) require the `multipart/related` request to be sent without quotation marks. Defaults to `false`. If your server doesn't require this, then setting this flag to `true` might improve performance (by removing the need for preflight requests). Also note that
if auth headers are used, a preflight request is required.
- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type.
- `volumeAutoDecimationThreshold`: (number, optional) If set, volumes with voxel count above this value are automatically decimated when loaded (e.g. for 3D viewports), reducing memory use. Default in `config/default.js` is `500_000_000` (500M voxels). When unset or undefined, auto-decimation is disabled. Users can still change decimation manually via the viewport Volume Downsizing menu.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether default.js defines volumeAutoDecimationThreshold and its value
fd -t f 'default.js' -p 'config' | xargs rg -nC3 'volumeAutoDecimationThreshold|customizationService' 2>/dev/null

Repository: OHIF/Viewers

Length of output: 50371


🏁 Script executed:

#!/bin/bash
# Find default.js in the typical config location
DEFAULT_JS=$(fd -t f 'default.js' 'platform/app/public/config' | head -n 1)

if [ -z "$DEFAULT_JS" ]; then
  echo "File default.js not found in platform/app/public/config"
  # Try broader search if needed, but usually it's here
  fd -t f 'default.js' 'platform' | head -n 5
else
  echo "Found file: $DEFAULT_JS"
  echo "--- Content around volumeAutoDecimationThreshold ---"
  rg -nC5 'volumeAutoDecimationThreshold' "$DEFAULT_JS"
fi

Repository: OHIF/Viewers

Length of output: 253


🏁 Script executed:

#!/bin/bash
DEFAULT_JS="platform/app/public/config/default.js"
# Check if the file exists
if [ -f "$DEFAULT_JS" ]; then
    echo "File exists. Lines count: $(wc -l < $DEFAULT_JS)"
    # Search for the specific key with context
    echo "--- Grep result for volumeAutoDecimationThreshold ---"
    rg -n 'volumeAutoDecimationThreshold' "$DEFAULT_JS"
    
    # If no result, search the file for just 'volume' or 'decimation' to see if it's named differently
    if [ $? -ne 0 ]; then
        echo "--- Search for related terms ---"
        rg -ni 'volume|decimation' "$DEFAULT_JS" | head -n 20
    fi
else
    echo "Path not found: $DEFAULT_JS"
    # List contents of the config directory
    ls -la platform/app/public/config/ || echo "Directory not found"
fi

Repository: OHIF/Viewers

Length of output: 311


Remove the incorrect reference to config/default.js regarding volumeAutoDecimationThreshold.

The file platform/app/public/config/default.js does not define volumeAutoDecimationThreshold. The documentation currently claims the default is 500_000_000 defined in that file, which is factually incorrect and misleading. Please either remove the reference to the specific file or update the documentation to reflect that this is not set by default in default.js (or specify where it is actually defaulted).

  • platform/docs/docs/configuration/configurationFiles.md line 136
Snippet ``` - `volumeAutoDecimationThreshold`: (number, optional) If set, volumes with voxel count above this value are automatically decimated when loaded (e.g. for 3D viewports), reducing memory use. Default in `config/default.js` is `500_000_000` (500M voxels). When unset or undefined, auto-decimation is disabled. Users can still change decimation manually via the viewport Volume Downsizing menu. ```
🤖 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 `@platform/docs/docs/configuration/configurationFiles.md` at line 136, The
documentation entry for volumeAutoDecimationThreshold incorrectly claims a
default is defined in config/default.js. Update the text in
configurationFiles.md to remove that file reference or replace it with the
correct source of truth, and make sure the description matches the actual
behavior exposed by volumeAutoDecimationThreshold rather than attributing a
default value to default.js.

"Preview in viewport": "Test Preview in viewport",
"Grayscale": "Test Grayscale",
"Rendering Options": "Test Rendering Options",
"Volume Options": "Test Volume Options",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check which WindowLevelActionMenu locale files contain the "Volume Options" key
fd 'WindowLevelActionMenu.json' platform/i18n/src/locales --exec sh -c 'printf "%s: " "$1"; grep -q "\"Volume Options\"" "$1" && echo present || echo MISSING' _ {}

Repository: OHIF/Viewers

Length of output: 614


Add "Volume Options" to ru and ar locale files.

The key is already present in en-US, fr, nl, zh, and test-LNG, but is missing in ru and ar. Adding it here ensures consistent translation coverage across all supported locales.

🤖 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 `@platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json` at line 10,
The “Volume Options” localization key is missing from the ru and ar locale sets,
so add it to those locale JSON files to match the existing entries in en-US, fr,
nl, zh, and test-LNG. Use the same key name and add appropriate translated
values alongside the other WindowLevelActionMenu strings so the locale coverage
stays consistent across all supported languages.

Comment on lines +178 to +189
useEffect(() => {
if (isLoading) {
wasEverLoading.current = true;
setHasSettled(false);
} else if (wasEverLoading.current) {
const timer = setTimeout(() => setHasSettled(true), SETTLE_DELAY);
return () => clearTimeout(timer);
}
}, [isLoading]);

const isExpanded = !hasSettled || isHovered || isDragging;
const effectiveWidth = isExpanded ? TRACK_WIDTH : RESTING_WIDTH;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -n 120 platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx

Repository: OHIF/Viewers

Length of output: 3748


🏁 Script executed:

sed -n '120,200p' platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx

Repository: OHIF/Viewers

Length of output: 3164


Scrollbar stays expanded for instantly loaded viewports.

If isLoading is false on the initial render (no previous loading state), wasEverLoading.current remains false and the useEffect fails to set hasSettled to true. This permanently locks isExpanded to true, forcing the track to stay at TRACK_WIDTH. Initialize hasSettled to true by default so the scrollbar contracts to RESTING_WIDTH when isLoading is never true.

🤖 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 `@platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx` around
lines 178 - 189, The SmartScrollbar state logic keeps the track expanded when
the view never enters loading, because `wasEverLoading.current` stays false and
`hasSettled` never flips back. Update the `SmartScrollbar` component so
`hasSettled` starts as true by default, while still letting the `useEffect`
reset it to false during loading and settle it after loading in the existing
`isLoading` flow. This will let `isExpanded` and `effectiveWidth` contract
correctly for instantly loaded viewports.

Comment on lines +320 to +342
role="slider"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={total - 1}
aria-orientation="vertical"
aria-label={ariaLabel}
tabIndex={0}
className={className}
style={{
width: TRACK_WIDTH + hitZoneSideExtension * 2,
height: '100%',
position: 'relative',
marginLeft: -hitZoneSideExtension,
cursor: isDragging ? 'grabbing' : 'grab',
touchAction: 'none',
}}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onKeyDown={enableKeyboardNavigation ? handleKeyDown : undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Focusable role="slider" without keyboard operability when enableKeyboardNavigation is false.

tabIndex={0} is unconditional, so the slider is keyboard-focusable, but onKeyDown is only wired when enableKeyboardNavigation is true. The progress overlay (ViewportSliceProgressScrollbar) passes enableKeyboardNavigation={false}, leaving a focusable widget that can't be operated by keyboard (WCAG 2.1.1). Gate the tab stop on the same flag.

Proposed fix
-          aria-label={ariaLabel}
-          tabIndex={0}
+          aria-label={ariaLabel}
+          tabIndex={enableKeyboardNavigation ? 0 : -1}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
role="slider"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={total - 1}
aria-orientation="vertical"
aria-label={ariaLabel}
tabIndex={0}
className={className}
style={{
width: TRACK_WIDTH + hitZoneSideExtension * 2,
height: '100%',
position: 'relative',
marginLeft: -hitZoneSideExtension,
cursor: isDragging ? 'grabbing' : 'grab',
touchAction: 'none',
}}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onKeyDown={enableKeyboardNavigation ? handleKeyDown : undefined}
aria-label={ariaLabel}
tabIndex={enableKeyboardNavigation ? 0 : -1}
className={className}
style={{
width: TRACK_WIDTH + hitZoneSideExtension * 2,
height: '100%',
position: 'relative',
marginLeft: -hitZoneSideExtension,
cursor: isDragging ? 'grabbing' : 'grab',
touchAction: 'none',
}}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onKeyDown={enableKeyboardNavigation ? handleKeyDown : undefined}
🤖 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 `@platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx` around
lines 320 - 342, The SmartScrollbar slider is always focusable because
`tabIndex={0}` is unconditional, but `handleKeyDown` is only attached when
`enableKeyboardNavigation` is true, so `ViewportSliceProgressScrollbar` can
expose a non-operable keyboard target. Update the `SmartScrollbar` component so
the tab stop is gated by the same `enableKeyboardNavigation` flag used for
`onKeyDown`, keeping the slider focusable only when keyboard interaction is
actually supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant