Skip to content

fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order#5933

Merged
wayfarer3130 merged 2 commits into
OHIF:masterfrom
pedrokohler:fix/deterministic-viewport-order
Apr 6, 2026
Merged

fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order#5933
wayfarer3130 merged 2 commits into
OHIF:masterfrom
pedrokohler:fix/deterministic-viewport-order

Conversation

@pedrokohler
Copy link
Copy Markdown
Collaborator

@pedrokohler pedrokohler commented Apr 1, 2026

Context

defaultRouteInit.applyHangingProtocol sorts display sets by modality priority and series number into sortedDisplaySets, but the original unsorted displaySets array was passed to hangingProtocolService.run(). Since displaySetService.getActiveDisplaySets() returns display sets in creation order -- which depends on asynchronous network response timing -- viewport ordering was non-deterministic across page loads.

This is a significant source of screenshot-based E2E test flakiness: when viewport positions shuffle between runs, full-page screenshot comparisons fail even though the clinical content is correct. Affected tests include multi-viewport layouts like RTDataOverlayForUnreferencedDisplaySetNoHydration, SEGDataOverlayForUnreferencedDisplaySetNoHydration, ContourSegmentToggleVisibility, ContourSegNavigation, Scoord3dProbe, and other hydration/MPR tests that compare 2x2 grids. These appear intermittently in CI (e.g. runs from Mar 25-27 with 5-7 failures each), pass on retry, and are caused by viewport shuffling rather than rendering bugs.

Beyond testing, deterministic order improves the end-user experience: reopening the same study always produces the same layout.

Changes & Results

  • One-word change in platform/app/src/routes/Mode/defaultRouteInit.ts: pass sortedDisplaySets instead of displaySets to hangingProtocolService.run().
  • Viewport ordering is now deterministic (sorted by modality priority, then series number) regardless of network timing.

Testing

  • Unit tests: 846/846 pass.
  • Playwright E2E: full suite ran with --update-snapshots -- regenerated screenshots are byte-identical to existing baselines, confirming the sort order matches what the committed baselines already expect.

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: macOS 15.4
  • Node version: 20.9.0
  • Browser: Chromium (Playwright headless)

Made with Cursor

Greptile Summary

This PR fixes a one-line bug in defaultRouteInit.applyHangingProtocol where the unsorted displaySets array was being passed to hangingProtocolService.run() even though a sorted copy (sortedDisplaySets) was already computed and used for getStudies(). Since displaySetService.getActiveDisplaySets() returns display sets in network-response order (non-deterministic), viewport layout could shuffle between page loads.

Key changes:

  • platform/app/src/routes/Mode/defaultRouteInit.ts: Replace displaySets with sortedDisplaySets in the hangingProtocolService.run() call (line 52), making the argument consistent with the already-sorted input to getStudies() on line 45.

Impact:

  • Viewport ordering is now fully deterministic (modality priority first, then series number), regardless of network timing.
  • Resolves intermittent E2E screenshot test failures in multi-viewport layouts caused by viewport shuffling rather than rendering bugs.
  • Improves end-user experience: reopening the same study always produces the same layout.

Assessment: The fix is minimal, correct, and self-contained. sortedDisplaySets is already constructed inside applyHangingProtocol via [...displaySets].sort(sortCriteria) — a non-mutating spread-then-sort — so no side effects are introduced. Both the hangingProtocolService's internal this.displaySets state and the protocolEngine.run() path now receive the sorted array consistently.

Confidence Score: 5/5

Safe to merge — single-line fix with no side effects, consistent with existing sort logic already applied to getStudies().

The change is a trivially correct one-liner: sortedDisplaySets was already being computed and used in the same function, the fix simply passes it consistently to hangingProtocolService.run(). No new logic, no new dependencies, no mutation of shared state. All 846 unit tests pass and E2E snapshots are byte-identical to baselines.

No files require special attention.

Important Files Changed

Filename Overview
platform/app/src/routes/Mode/defaultRouteInit.ts One-line fix: passes the already-computed sortedDisplaySets (sorted by modality priority then series number) to hangingProtocolService.run() instead of the original unsorted displaySets, making viewport ordering deterministic regardless of network response timing.

Sequence Diagram

sequenceDiagram
    participant DS as displaySetService
    participant DRI as applyHangingProtocol()
    participant HP as hangingProtocolService

    DS->>DRI: getActiveDisplaySets() → displaySets (network order)
    Note over DRI: [...displaySets].sort(seriesInfoSortingCriteria)<br/>→ sortedDisplaySets (modality priority + series number)
    DRI->>DRI: getStudies(studyInstanceUIDs, sortedDisplaySets)
    Note over DRI: Before fix: passed unsorted displaySets<br/>After fix: passes sortedDisplaySets
    DRI->>HP: run({ studies, activeStudy, displaySets: sortedDisplaySets }, hangingProtocolId, { stageIndex })
    HP->>HP: this.displaySets = sortedDisplaySets
    HP->>HP: _setProtocol → _updateViewports (deterministic layout)
Loading

Reviews (1): Last reviewed commit: "fix: pass sorted display sets to hanging..." | Re-trigger Greptile

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

…iewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 1, 2026

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 6ed7379
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69ce914a55025d0009338b75
😎 Deploy Preview https://deploy-preview-5933--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@pedrokohler pedrokohler requested review from jbocce and sedghi April 1, 2026 10:47
Copy link
Copy Markdown
Collaborator

@jbocce jbocce left a comment

Choose a reason for hiding this comment

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

Looks good to me. It kinda bothers me why we were not already doing this. @wayfarer3130 any thoughts?


// run the hanging protocol matching on the displaySets with the predefined
// hanging protocol in the mode configuration
hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you - this has gotten tweaked several times, with the original doing a sort on the underlying series themselves as stored in the display set service.

Copy link
Copy Markdown
Contributor

@wayfarer3130 wayfarer3130 left a comment

Choose a reason for hiding this comment

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

I'd really love to have a unit test on this - not sure if that is realistic or not, but it has been broken so many times that I'd like to have some verification if possible.

@wayfarer3130 wayfarer3130 merged commit 68b10d3 into OHIF:master Apr 6, 2026
8 checks passed
wayfarer3130 pushed a commit that referenced this pull request Apr 10, 2026
…or deterministic viewport order (#5933)

fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor
wayfarer3130 pushed a commit that referenced this pull request Apr 10, 2026
…or deterministic viewport order (#5933)

fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor
wayfarer3130 added a commit that referenced this pull request Apr 15, 2026
* fix: Use newer ONNX version and load without errors

* Only changes to enable SAM again

* fix(seg hydration): auto-hydrate RT struct on second load with disableConfirmationPrompts (#5875)

* chore(version): Update package versions to 3.13.0-beta.34 [skip ci]

* fix(Threshold tool): Threshold tool no longer becomes deselected when the Dynamic option is selected (#5884)

fix(Threshold tool): Added 'ThresholdCircularBrushDynamic' to the toolNames array so the evaluator correctly recognizes it as an active state for the Threshold button when Dynamic mode is selected.

* chore(version): Update package versions to 3.13.0-beta.35 [skip ci]

* fix: Modalities in study list should select starts with as primary (#5886)

* chore(version): Update package versions to 3.13.0-beta.36 [skip ci]

* fix(security): Bump tar version to address CVE-2026-31802. (#5893)

* chore(version): Update package versions to 3.13.0-beta.37 [skip ci]

* fix(segmentation): Display "No description S:{series number} {modality}" for segmentations with no label. (#5874)

* Bump CS3D dependency to get the fallbackLabel field additions.

* chore(version): Update package versions to 3.13.0-beta.38 [skip ci]

* fix(window level): The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. (#5865)

* fix(window level): Set up listener for viewport availability such that the initial window level can be read and displayed.

* PR feedback.

* PR feedback.

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.39 [skip ci]

* fix(security): Bump flattened version to address CVE-2026-32141. (#5897)

* chore(version): Update package versions to 3.13.0-beta.40 [skip ci]

* fix(sr-hydration): enable hydration and arrow navigation for 3D SR measurements (#5887)

Joe is away, so approving based on the code having the requested change, and otherwise looking good/passing tests.

* fix(sr-hydration): enable hydration and arrows navigation for 3D SR measurements

* test: add automated test for SR measurement navigation with arrows after hydration

* add cross-study warning in the 3D branch

* test: address reviewer feedback for the test

* fix: support 3D and 2D annotations for SR hydration

* test: improve navigation to first image

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.41 [skip ci]

* feat: Add combined build (#5895)

* Add combined build

* Link script location update

* Security and validation fixes

* Allow specifying target path in PR description

* fix: Version match

* Fix build detection issue

* fix: Playwright deploy

* Separate out the branch merge guard

* Update docs and link info

* test: Update the layout change to wait for network idle

* Move audit late so the rest of the build can be worked on

* Add text with network check to ensure we see this change is updated

* Attempt to fix the mpr loading on ohif-downstream

* PR review comments

* Update docs

* Update to CS3D 4.20.0

* PR comments

* Add log on ohif-integration builds

* Update build test

* Removed unused space to kickoff build

* chore(version): Update package versions to 3.13.0-beta.42 [skip ci]

* fix(SR): Added support for spline and live wire SR items. (#5870)

* fix(SR): Added support for spline and live wire SR items.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add a script to checkout a worktree for test builds

* fix: Allow download for testing sr validator

* Remove script that wasn't intended to be included

* Bump CS3D version.

* PR comments - simplify code and use single codepath for download

* Allow both download and save buttons for SEG and RTSTRUCT

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.43 [skip ci]

* chore(tests): contour segment interactions e2e tests - rename and togglevisibility (#5891)

* chore(version): Update package versions to 3.13.0-beta.44 [skip ci]

* chore(refactor): use public appConfig getter instead of private _appConfig field (#5923)

* chore(version): Update package versions to 3.13.0-beta.45 [skip ci]

* refactor(tests): update viewport page object usage to async and update all effected tests (#5927)

* chore(version): Update package versions to 3.13.0-beta.46 [skip ci]

* fix: prevent black viewport when navigating series with client-created segmentation (#5919)

* chore(version): Update package versions to 3.13.0-beta.47 [skip ci]

* fix(measurement): Restore viewport interactivity when deleting in-progress Spline or Livewire measurement (#5905)

* chore(version): Update package versions to 3.13.0-beta.48 [skip ci]

* fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set  (#5900)

- Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering
- Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch
- Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
- fix segmentation overlay order reversal on viewport re-render

* chore(version): Update package versions to 3.13.0-beta.49 [skip ci]

* fix(security): update dependencies to fix security vulnerabilities (#5936)

* chore(version): Update package versions to 3.13.0-beta.50 [skip ci]

* fix(security): Update yarn.lock that was missed in PR #5936. (#5940)

* chore(version): Update package versions to 3.13.0-beta.51 [skip ci]

* feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 (#5924)

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>

* fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order (#5933)

fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor

* chore(version): Update package versions to 3.13.0-beta.52 [skip ci]

* revert: rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID (#5943)

* chore(version): Update package versions to 3.13.0-beta.53 [skip ci]

* fix(cornerstone): read FrameOfReferenceUID from display set in viewport service (#5950)

* chore(version): Update package versions to 3.13.0-beta.54 [skip ci]

* fix: ignore auth in git (#5955)

* chore(version): Update package versions to 3.13.0-beta.55 [skip ci]

* ONNX latest version

* chore(version): Update package versions to 3.13.0-beta.56 [skip ci]

* bun lock

* fix high sev mathjs issue

* Revert onnx changes

* Update to recent CS3D version

* Undo unneeded change

* Add null check

* Undo unneeded change

---------

Co-authored-by: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: Pedro Köhler <pedrokohlerbh@gmail.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
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.

3 participants