Skip to content

feat(metadata): framework-agnostic display-set splitting + DWI mixed-b-value W/L fix#2777

Closed
sedghi wants to merge 25 commits into
mainfrom
fix/dwiwrongwlsplit
Closed

feat(metadata): framework-agnostic display-set splitting + DWI mixed-b-value W/L fix#2777
sedghi wants to merge 25 commits into
mainfrom
fix/dwiwrongwlsplit

Conversation

@sedghi

@sedghi sedghi commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Moves the display-set concept out of OHIF into @cornerstonejs/metadata as a framework-agnostic, data-shaped module, and fixes the original bug: a diffusion MR (DWI) series that mixes 4D b-value frames with trailing no-b-value frames was rendered as one volume, applying the wrong window/level to the non-4D portion.

What's here

  • Display-set split pipeline (splitImageIdsBySplitRulescreateDisplaySetFromGroup): resolves a series' imageIds into instance groups via ordered split rules (first match wins), then builds an IDisplaySet per group.
  • DWI fix (mixedDimensionalityBValue rule): splits the undefined-b-value frames into their own display set so 4D rendering is not applied to the 3D portion.
  • Split-rule API: a rule is matches (per-instance predicate) + groupBy (bucket recipe), with an optional series hook for facts derived from the whole series.

Split-rule shape

Most rules need only matches + groupBy. A rule reaches for series only when it needs a value computed from the whole series and reused by matches/groupBy. series is pure — it returns that rule's derived facts and does not mutate shared state:

{
  series: ({ instances }) => ({
    mixedBValue:
      instances[0]?.Modality === 'MR' &&
      instances.some((i) => i.DiffusionBValue !== undefined) &&
      instances.some((i) => i.DiffusionBValue === undefined),
  }),
  matches: (_instance, { series }) => series.mixedBValue,
  groupBy: ['SeriesInstanceUID', (i) => i.DiffusionBValue === undefined],
}

Testing

  • packages/metadata jest suite: 63 passing (covers the DWI mixed/non-mixed split, rule namespacing, deterministic group order, custom attributes, classifiers).
  • Docs: end-to-end display-set workflow page under concepts/cornerstone-metadata/display-sets.

Summary by CodeRabbit

  • New Features

    • Added display-set–driven mounting via new setDisplaySets support across core viewports (including WSI, video, ECG, stack, volumes).
    • Introduced a display-set split/creation workflow with deterministic grouping and 4D-friendly helpers.
    • Added the “Display Sets — one viewport per display set” demo.
  • Bug Fixes

    • Improved consistency for video/ECG/WSI/stack/volume rendering by aligning display-set registration and mounting.
  • Documentation

    • Expanded display-set documentation and updated migration/API examples to use the display-set metadata provider.
  • Tests

    • Added/updated tests covering display-set construction, split rules, and rendering setup.

wayfarer3130 and others added 22 commits June 8, 2026 15:20
…ack and volume viewports (#2717)

* fix: closed freehand stat value mismatch

* fix: review comments

* chore: renamed function

* docs: fix MDX rendering issue in snapIndexBounds JSDoc

* Fix view issue on stack so that the issue is testable

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
- isVideoInstance: reuse shared videoUIDs (no MPEG2 misclassification)
- buildSeriesInfo + default split rules: guard empty instance lists
- numImageFrames: coerce NumberOfFrames with Number()
- MetadataModuleType.displaySetModule: type as full IDisplaySet
- createDisplaySetFromGroup: denylist reserved data keys
- Viewport: extract shared mountDisplaySets template for the 5 overrides
- re-export isWsiInstance; add JSDoc to classifiers/viewport-type helpers
- demo toBaseImageId: normalize ?frame=N as well as &frame=N
- tests for the above
Add the split -> create -> consume pipeline, driving a viewport via
setDisplaySets, caching display sets in the metadata layer, the split-rule
model (with the DWI mixedDimensionalityBValue worked example), and the
reusable instance classifiers.
- GenericViewport: add getDisplaySets() to ViewportController + base impl
  (derived from live bindings), giving parity with the legacy viewport.
- createDisplaySetFromGroup: fold splitNumber into the default
  displaySetInstanceUID so a split series (DWI) yields unique ids; the
  viewport displaySetId == the display set instance UID.
- demo splitter passes splitNumber; displaySets example keys the registry
  on displaySetInstanceUID to exercise the convention.
- legacy mountDisplaySets records only the mounted source (honest
  getDisplaySets); documents overlays as a non-breaking future addition.
- rename genericViewportDataSet* -> genericViewportDisplaySet* across the
  registry/access layer, types, and call sites (beta, no alias).
Collapse the displaySetId / displaySetInstanceUID duality onto a single
name, displaySetId:
- metadata: rename IDisplaySet.displaySetInstanceUID -> displaySetId (and
  CreateDisplaySetFromGroup option, reserved keys, tests, docs).
- generic viewport: rename the DataId type -> DisplaySetId; align the
  registry/access param names (dataId -> displaySetId).
- keep dataId in the public, persisted ViewReference / ViewportDataReference
  (a generic data-ref pointer) and in loadECGWaveform (an imageId), per the
  identifier-surface-only scope.
- group buckets are now namespaced by rule and JSON-encoded, so different
  rules can't collide on a shared key and separator/undefined aliasing is gone
  (issues 1, 6).
- groups are returned in deterministic (split-key sorted) order, making
  split identity / displaySetId suffixes stable across input ordering (3).
- groupInstancesBySplitRules/splitImageIdsBySplitRules accept onUnmatched(d)
  so instances matching no rule are observable, not silently dropped (5).
- updateSeriesInfo typed () => void (return was always ignored) (4).
- default rules: singleImageModality split key is now series-scoped (7);
  comments document the instances[0] homogeneity assumption (2), the
  multiFrame SliceLocation guard (8); removed the redundant viewportTypes-only
  customAttributes now that rule-level viewportTypes is applied (9).
- tests for rule namespacing, deterministic order, and onUnmatched.
The metadata page had grown to be mostly display-set content. Move it to a
dedicated concepts/cornerstone-metadata/display-sets page (pipeline, driving a
viewport, caching, split-rule model + DWI example, IDisplaySet model), leave a
short pointer on the metadata index, and add it to the sidebar. Also folds in
the split-engine guarantees (rule-namespaced keys, deterministic order,
onUnmatched, updateSeriesInfo mutation).
Complete the in-progress rename of the SplitRule fields so the engine and
the rules line up again:

- ruleSelector -> matches (per-instance predicate)
- splitKey (rule field) -> groupBy (bucket recipe)
- keep updateSeriesInfo; revert the stray augmentSeriesInfo so the
  series-level hook actually fires

The rename had been applied to types.ts and groupInstancesBySplitRules.ts
but not to defaultDisplaySetSplitRules.ts/tests, so every rule fell back to
a catch-all grouped by SeriesInstanceUID and the series flags were never set
- silently disabling the DWI mixed-b-value split. Updates the rules, tests,
and docs to the new names.
…ies hook

Series-level facts used to be contributed by a rule's `updateSeriesInfo`
hook, which mutated a shared `seriesInfo` object that `matches`/`groupBy`
then read. Replace that with a pure, rule-scoped `series` hook:

- `series?: ({ instances }) => SeriesFacts` runs once per rule per split and
  *returns* that rule's derived facts (no shared mutation).
- `matches`/`groupBy` receive a `RuleContext` ({ series }) carrying only that
  rule's own facts.
- `buildSeriesInfo` is now a standalone series-statistics helper, decoupled
  from rules; `SeriesInfo` no longer carries rule flags.

Most rules need only matches + groupBy; series is the opt-in for whole-series
facts (multi-frame, volumetric, mixed-b-value DWI). Adds tests covering the
DWI mixed/non-mixed split and updates the display-sets docs.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c672b395-8edb-4162-9ff0-860b676ae08e

📥 Commits

Reviewing files that changed from the base of the PR and between c0aa67a and 8128046.

📒 Files selected for processing (1)
  • packages/core/src/RenderingEngine/BaseVolumeViewport.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/RenderingEngine/BaseVolumeViewport.ts

📝 Walkthrough

Walkthrough

Adds display-set metadata types and split helpers, migrates generic viewport plumbing to display-set metadata, adds setDisplaySets to viewports, and updates demos, tests, docs, and utilities to use the new display-set flow.

Changes

Display set subsystem and provider migration

Layer / File(s) Summary
Display-set contracts and module types
packages/metadata/src/displayset/IDisplaySet.ts, packages/metadata/src/displayset/types.ts, packages/metadata/src/enums/MetadataModules.ts, packages/metadata/src/types/MetadataModuleTypes.ts, packages/core/src/RenderingEngine/GenericViewport/ViewportArchitectureTypes.ts, packages/core/src/RenderingEngine/GenericViewport/index.ts
Adds IDisplaySet, split-rule types, MetadataModules.DISPLAY_SET, MetadataModuleType.displaySetModule, and DisplaySetId-based viewport architecture typing.
Split rules and display-set construction
packages/metadata/src/displayset/is*.ts, packages/metadata/src/displayset/buildSeriesInfo.ts, packages/metadata/src/displayset/resolveInstances.ts, packages/metadata/src/displayset/viewportTypes.ts, packages/metadata/src/displayset/groupInstancesBySplitRules.ts, packages/metadata/src/displayset/splitImageIdsBySplitRules.ts, packages/metadata/src/displayset/BaseDisplaySet.ts, packages/metadata/src/displayset/ImageStackDisplaySet.ts, packages/metadata/src/displayset/createDisplaySetFromGroup.ts, packages/metadata/src/displayset/defaultDisplaySetSplitRules.ts, packages/metadata/src/displayset/displayset.test.ts
Adds instance classifiers, grouping and splitting helpers, display-set constructors, default split rules, and tests covering the split and grouping behavior.
Display-set metadata provider and exports
packages/core/src/utilities/genericViewportDisplaySetMetadataProvider.ts, packages/core/src/utilities/index.ts, packages/metadata/src/displayset/displaySetProvider.ts, packages/metadata/src/displayset/registerDisplaySetMetadata.ts, packages/metadata/src/registerDefaultProviders.ts, packages/metadata/src/displayset/index.ts, packages/metadata/src/index.ts, packages/metadata/src/utilities/index.ts, packages/metadata/jest.config.js
Adds the display-set metadata provider, registration helper, default-provider wiring, and package re-exports.
Display-set access helpers and legacy adapter updates
packages/core/src/RenderingEngine/GenericViewport/genericViewportDisplaySetAccess.ts, packages/core/src/RenderingEngine/GenericViewport/Planar/..., packages/core/src/RenderingEngine/GenericViewport/Video/..., packages/core/src/RenderingEngine/GenericViewport/Volume3D/..., packages/core/src/RenderingEngine/GenericViewport/WSI/..., packages/core/src/RenderingEngine/GenericViewport/ECG/...
Replaces data-set access helpers with display-set access helpers and updates the dependent viewport adapters and providers to use the new metadata source.
Viewport display-set lifecycle and typing
packages/core/src/RenderingEngine/Viewport.ts, packages/core/src/RenderingEngine/GenericViewport/GenericViewport.ts
Adds base display-set tracking and mounting helpers, and updates GenericViewport signatures to DisplaySetId.
Concrete viewport setDisplaySets implementations
packages/core/src/RenderingEngine/StackViewport.ts, packages/core/src/RenderingEngine/BaseVolumeViewport.ts, packages/core/src/RenderingEngine/VideoViewport.ts, packages/core/src/RenderingEngine/WSIViewport.ts, packages/core/src/RenderingEngine/ECGViewport.ts
Adds setDisplaySets to each viewport class and resets mounted display-set state in the native setters.
Demo helpers for display-set splitting and 4D image IDs
utils/demo/helpers/splitDisplaySetsFromImageIds.ts, utils/demo/helpers/index.js
Adds helpers for creating display sets from image IDs, deriving viewport types, and selecting 4D image groups.
Examples, tests, and CLI updates
packages/core/examples/displaySets/..., packages/core/examples/video/..., packages/tools/examples/video*/..., packages/tools/examples/generic*/..., packages/tools/examples/dynamicCINETool/..., packages/tools/examples/generateImageFromTimeData/..., packages/tools/src/tools/displayTools/Labelmap/..., tests/vitest-browser/..., utils/ExampleRunner/example-runner-cli.js
Updates examples, browser tests, labelmap tooling, and the example runner to use the new display-set provider and mounting flow.
Documentation and navigation updates
packages/docs/docs/concepts/cornerstone-metadata/display-sets.md, packages/docs/docs/concepts/cornerstone-metadata/index.md, packages/docs/docs/concepts/cornerstone-core/generic-viewport/api.md, packages/docs/docs/concepts/cornerstone-core/generic-viewport/data-bindings-and-loading.md, packages/docs/docs/concepts/cornerstone-core/generic-viewport/migration.md, packages/docs/docs/migration-guides/5x/2-generic-viewport.md, packages/docs/sidebars.js
Adds the display-sets concept page, updates generic-viewport examples, and adds the new page to the sidebar.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • wayfarer3130

Poem

🐇 I hopped through stacks and volumes bright,
Display sets danced in morning light.
ECG and WSI found their place,
With setDisplaySets in the race.
A carrot codepath, neat and new,
Now splits and mounts the rabbit view.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is descriptive, scoped, and accurately reflects the main metadata splitting and DWI fix work.
Description check ✅ Passed The description covers the summary, key changes, split-rule shape, and testing, but it omits the template's Context and Checklist sections.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dwiwrongwlsplit

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.

…tadataProvider

Three core jest suites still imported/mocked the pre-rename
`genericViewportDataSetMetadataProvider` module and its `VIEWPORT_V2_DATA_SET`
constant, so they failed to load after the DataSet->DisplaySet rename. Update
the module path and constant to the current `genericViewportDisplaySetMetadataProvider`
/ `VIEWPORT_V2_DISPLAY_SET` names.

@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: 17

Caution

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

⚠️ Outside diff range comments (4)
packages/core/src/RenderingEngine/WSIViewport.ts (1)

547-554: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve miniNavigationOverlay when replaying WSI display-set metadata.

getGenericViewportWSIDisplaySet normalizes options.miniNavigationOverlay, and WSIViewportLegacyAdapter.setDataIds stores that option before delegating here. Both of these paths forward only webClient, so a registered WSI display set with miniNavigationOverlay: false silently falls back to the default overlay behavior after this migration.

Suggested fix
   public setDataList(
     entries: Array<{ dataId: string; options?: Record<string, unknown> }>
   ) {
     if (!entries.length) {
       return;
     }
     const { dataId } = entries[0];
-    const dataSet = metaData.get('genericViewportDisplaySet', dataId) as
-      | { imageIds?: string[]; options?: { webClient?: WSIClientLike } }
-      | undefined;
+    const dataSet = getGenericViewportWSIDisplaySet(dataId);

     if (dataSet?.imageIds) {
-      return this.setDataIds(dataSet.imageIds, {
-        webClient: dataSet.options?.webClient,
-      } as Parameters<typeof this.setDataIds>[1]);
+      return this.setDataIds(dataSet.imageIds, dataSet.options);
     }

     return this.setDataIds([dataId]);
   }
@@
   public async setDisplaySets(
     ...entries: Array<{ displaySetId: string; options?: unknown }>
   ): Promise<void> {
     await this.mountDisplaySets(entries, async (entry) => {
       const dataSet = getGenericViewportWSIDisplaySet(entry.displaySetId);
       if (!dataSet?.imageIds?.length || !dataSet.options?.webClient) {
         throw new Error(
           `[WSIViewport] No registered WSI dataset (imageIds + webClient) for display set ${entry.displaySetId}`
         );
       }

-      await this.setWSI(dataSet.imageIds, dataSet.options.webClient);
+      await this.setDataIds(dataSet.imageIds, dataSet.options);
     });
   }

Also applies to: 643-655

🤖 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 `@packages/core/src/RenderingEngine/WSIViewport.ts` around lines 547 - 554,
`getGenericViewportWSIDisplaySet` is dropping the stored `miniNavigationOverlay`
when it replays metadata through `WSIViewport.setDataIds`, so the legacy
adapter’s saved overlay setting is lost. Update the metadata shape and
forwarding logic in `getGenericViewportWSIDisplaySet` and
`WSIViewportLegacyAdapter.setDataIds` to include `options.miniNavigationOverlay`
alongside `webClient`, and pass it through unchanged when calling `setDataIds`
so registered display sets preserve the original overlay preference.
packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts (1)

508-517: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clean up viewport-owned overlay display sets on teardown.

addImages now registers image/imageData in the global display-set provider, but the teardown here still only removes renderImageObjectDataId. Overlays created through addImages survive removeData() and viewport destruction, which keeps large buffers reachable and leaves stale metadata behind for future displaySetId reuse.

Suggested fix
+  private readonly ownedDisplaySetIds = new Set<string>();
+
   async addImages(stackInputs: IStackInput[]): Promise<void> {
     ...
       const dataId = this.resolveOverlayDataId(stackInput, image, reference);
       genericViewportDisplaySetMetadataProvider.add(dataId, {
         image,
         imageData: stackInput.imageData,
         imageIds: [stackInput.imageId],
         initialImageIdIndex: 0,
         kind: 'planar',
         reference,
         useWorldCoordinateImageData:
           stackInput.useWorldCoordinateImageData === true,
       });
+      this.ownedDisplaySetIds.add(dataId);

       await this.addDisplaySet(dataId, {
         role: 'overlay',
       });
     ...
   }

   removeData(dataId: string): void {
     this.clearResolvedViewCache();
     super.removeData(dataId);
     this.mountedData.handleRemovedData(dataId);
+    if (this.ownedDisplaySetIds.delete(dataId)) {
+      genericViewportDisplaySetMetadataProvider.remove(dataId);
+    }

     if (!this.isDestroyed && this.getCurrentBinding()) {
       this.updateBindingsCameraState();
     }
   }

   protected override onDestroy(): void {
     this.clearResolvedViewCache();
     if (this.renderImageObjectDataId) {
       genericViewportDisplaySetMetadataProvider.remove(
         this.renderImageObjectDataId
       );
       this.renderImageObjectDataId = undefined;
     }
+    this.ownedDisplaySetIds.forEach((dataId) => {
+      genericViewportDisplaySetMetadataProvider.remove(dataId);
+    });
+    this.ownedDisplaySetIds.clear();
     ...
   }

Also applies to: 1247-1254

🤖 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 `@packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts`
around lines 508 - 517, The teardown for PlanarViewport is only removing the
renderImageObjectDataId entry, so viewport-owned overlay display sets added by
addImages remain in genericViewportDisplaySetMetadataProvider and keep
buffers/metadata alive. Update the cleanup path in PlanarViewport’s
removeData/destroy logic to also unregister every display set created from
addImages, using the same dataId or displaySetId bookkeeping used when calling
genericViewportDisplaySetMetadataProvider.add. Make sure the fix covers both
teardown locations referenced by the review so stale overlay metadata cannot
survive viewport destruction or be reused later.
packages/tools/examples/videoSegmentation/index.ts (1)

193-223: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Select the video display set explicitly here.

This sample uses the same mixed series as packages/core/examples/video/index.ts, so const [displaySet] = displaySets can grab the still-image split first. That would drive getViewportTypeForDisplaySet(displaySet) and videoId from the wrong display set, breaking the example.

Suggested fix
   const displaySets = await createDisplaySets({
     StudyInstanceUID: '2.25.96975534054447904995905761963464388233',
     SeriesInstanceUID: '2.25.15054212212536476297201250326674987992',
     wadoRsRoot:
       getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
   });
 
-  const [displaySet] = displaySets;
+  const displaySet = displaySets.find(
+    (ds) => getViewportTypeForDisplaySet(ds) === Enums.ViewportType.VIDEO
+  );
   if (!displaySet) {
-    throw new Error('No display set found in series');
+    throw new Error('No video display set found in series');
   }
   const videoId = displaySet.instances[0].imageId;
🤖 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 `@packages/tools/examples/videoSegmentation/index.ts` around lines 193 - 223,
The video segmentation example is picking the first display set from
createDisplaySets, which can be the still-image split instead of the actual
video display set. Update the selection logic in the createDisplaySets /
displaySet handling block to explicitly choose the video display set before
using getViewportTypeForDisplaySet(displaySet), displaySet.instances[0].imageId,
and viewport.setDisplaySets, so the viewport is configured from the correct
video series.
packages/tools/examples/videoTools/index.ts (1)

172-228: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t assume the first split result is the video display set.

This example points at the same mixed series handled in packages/core/examples/video/index.ts. Picking displaySets[0] can select the still-image display set, which then makes both the computed viewport type and videoId wrong.

Suggested fix
   const displaySets = await createDisplaySets({
     StudyInstanceUID: '2.25.96975534054447904995905761963464388233',
     SeriesInstanceUID: '2.25.15054212212536476297201250326674987992',
     wadoRsRoot:
       getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
   });
 
-  const [displaySet] = displaySets;
+  const displaySet = displaySets.find(
+    (ds) => getViewportTypeForDisplaySet(ds) === Enums.ViewportType.VIDEO
+  );
   if (!displaySet) {
-    throw new Error('No display set found in series');
+    throw new Error('No video display set found in series');
   }
   const videoId = displaySet.instances[0].imageId;
🤖 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 `@packages/tools/examples/videoTools/index.ts` around lines 172 - 228, The
example currently assumes `displaySets[0]` is the video series, which can pick
the still-image set and break both `getViewportTypeForDisplaySet(displaySet)`
and `videoId`. Update the selection logic in the
`createDisplaySets`/`displaySet` block to explicitly find the video display set
using its identifying metadata or type, then use that chosen set for the
viewport setup and `viewport.setDisplaySets` instead of relying on the first
array item.
🧹 Nitpick comments (1)
packages/core/src/RenderingEngine/BaseVolumeViewport.ts (1)

1536-1541: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Attach a rejection handler to the fire-and-forget volume.load().

volume.load() is intentionally not awaited (streaming), but the returned promise has no .catch. A load failure surfaces as an unhandled promise rejection rather than a viewport-scoped error. Consider catching and routing it (e.g. log / event) so failures are observable.

🛡️ Suggested handling
       if (!cache.getVolume(volumeId)) {
         const volume = await createAndCacheVolume(volumeId, {
           imageIds: dataSet.imageIds,
         });
-        volume.load();
+        volume.load().catch((error) => {
+          console.error(
+            `[VolumeViewport] Failed to load volume ${volumeId} for display set ${entry.displaySetId}`,
+            error
+          );
+        });
       }
🤖 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 `@packages/core/src/RenderingEngine/BaseVolumeViewport.ts` around lines 1536 -
1541, Attach a rejection handler to the fire-and-forget load path in
BaseVolumeViewport by handling the promise returned from volume.load() after
createAndCacheVolume. Keep the non-awaited streaming behavior, but add a .catch
on volume.load() so failures are routed through viewport-scoped logging or
events instead of becoming unhandled promise rejections. Use the unique symbols
createAndCacheVolume, cache.getVolume, and volume.load() to locate the change.
🤖 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 `@packages/core/examples/displaySets/index.ts`:
- Around line 181-185: The metadata rendering in the displaySets example is
using row.detailsElement.innerHTML and line.innerHTML, which can inject
untrusted DICOM values into the page. Update the logic in the row/details
rendering path to build each line with DOM nodes instead of HTML, using
textContent for the metadata label and value. Keep the fix localized around the
loop that processes lines and appends to row.detailsElement.

In `@packages/docs/docs/concepts/cornerstone-metadata/display-sets.md`:
- Around line 46-53: The getNaturalizedInstance example references metaData
without showing its source, so update the snippet to either add the missing
metaData import from `@cornerstonejs/metadata` or explicitly note in the
surrounding comment that metaData is the default export. Keep the example
consistent with the rest of the page by making the dependency visible near
getNaturalizedInstance and metaData.get('instance', imageId).

In `@packages/metadata/src/displayset/BaseDisplaySet.ts`:
- Around line 27-34: Clone the incoming viewportTypes and instances in the
BaseDisplaySet constructor instead of storing the caller’s arrays by reference.
Update the BaseDisplaySet initialization so viewportTypes is copied before
calling getPreferredViewportType, and instances is also assigned from a new
array copy, keeping the display set immutable after construction just like the
imageIds and underlyingImageIds fields.

In `@packages/metadata/src/displayset/buildSeriesInfo.ts`:
- Around line 14-31: Deduplicate the resolved instances before computing series
aggregates in buildSeriesInfo, because imageIds can expand to the same
naturalized instance multiple times and inflate the series facts. Update the
logic around NumberOfSeriesRelatedInstances, numberOfFrames,
numberOfNonImageObjects, and numberOfSOPInstanceUIDsPerSeries to operate on a
unique set keyed by the underlying instance identity (for example SOPInstanceUID
or another stable instance key) before summing.

In `@packages/metadata/src/displayset/defaultDisplaySetSplitRules.ts`:
- Around line 95-96: The mixedDimensionalityBValue split rule is now matching
too broadly and making both subgroups stack-preferred, which regresses
defined-b-value multi-slice DWI behavior. Update the rule in
defaultDisplaySetSplitRules so it only applies to the intended subgroup(s) and
does not override the existing volume/volume3d default for the defined-b-value
split; verify the ordering and matching logic around mixedDimensionalityBValue
and the surrounding split rules.

In `@packages/metadata/src/displayset/groupInstancesBySplitRules.ts`:
- Around line 17-27: The split bucket key is currently using only the rule id
when present, which can merge results from different rules that reuse the same
id. Update the grouping logic in buildSplitKey and its caller so the bucket
namespace always includes ruleIndex alongside any splitRule.id, preserving
separation between rules even when ids collide. Make sure the change is applied
wherever the discriminator is constructed so identical groupBy values from
different rules cannot collapse into one InstanceGroup or inherit the wrong
matchedRule/custom attributes.

In `@packages/metadata/src/displayset/ImageStackDisplaySet.ts`:
- Around line 95-101: The fromImageIds path in ImageStackDisplaySet should
preserve the caller-supplied frame-level imageIds instead of rebuilding them
from NaturalizedInstance.imageId. Keep using getNaturalizedInstance to resolve
metadata, but update fromImageIds/fromInstances so the resulting
displaySet.imageIds stays aligned with the original imageIds array for
multi-frame or 4D callers, rather than collapsing to underlying instance ids.

In `@packages/metadata/src/displayset/isVideoInstance.ts`:
- Around line 15-22: The transfer syntax helper is only checking the first
advertised value, so `getTransferSyntaxUid` can miss a video syntax stored later
in the array. Update the logic in `getTransferSyntaxUid` so it examines all
values from `AvailableTransferSyntaxUID`, `TransferSyntaxUID`, or `00083002` and
returns a matching video transfer syntax if any entry qualifies, instead of
blindly using `tsuid[0]`. Make sure the downstream `createDisplaySetFromGroup`
classification uses the corrected result so multi-valued metadata is handled
properly.

In `@packages/metadata/src/displayset/registerDisplaySetMetadata.ts`:
- Around line 13-18: The registration set is being seeded from arbitrary
imageIds in registerDisplaySetMetadata, which can cause split display sets to
overwrite sibling frames under MetadataModules.DISPLAY_SET. Update
registerDisplaySetMetadata to derive the primary keys from the provided
displaySet itself, and only add any extra aliases explicitly through validation
that they belong to the same group. Make sure the logic in
registerDisplaySetMetadata and any related handling around the displaySet
metadata write path only registers identifiers that are valid for that specific
display set.

In `@packages/metadata/src/types/MetadataModuleTypes.ts`:
- Around line 124-131: The `displaySetModule` type in `MetadataModuleTypes` does
not match the object stored by `registerDisplaySetMetadata`, which wraps the
cached `IDisplaySet` inside a `displaySet` property. Update the
`displaySetModule` declaration and any related type usage so
`getTyped(MetadataModules.DISPLAY_SET, imageId)` is typed as the wrapper shape
rather than a bare `IDisplaySet`, keeping the type aligned with the payload
created in `registerDisplaySetMetadata`.

In `@packages/tools/examples/videoColor/index.ts`:
- Around line 192-196: The viewport binding in the videoColor demo is using the
first instance imageId instead of the display set’s own logical id, which breaks
the contract from createDisplaySets/setDisplaySets. Update the logic around
displaySet handling so the value passed to the viewport binding comes from
displaySet itself (the DisplaySetId returned by createDisplaySets) rather than
displaySet.instances[0].imageId, and apply the same fix anywhere else in this
flow uses the instance imageId for binding.

In `@packages/tools/examples/videoContourSegmentation/index.ts`:
- Around line 253-264: The video demo is currently assuming the first result
from createDisplaySets() is the video display set, which can break when the
series is split in a different order. Update the logic around displaySets so it
explicitly selects the display set whose preferredViewportType is 'video' before
reading instances[0].imageId and before using that set for the mounted viewport
content. Keep the No display set found check, but make it validate the
video-specific display set instead of displaySets[0].

In `@packages/tools/examples/videoGroup/index.ts`:
- Around line 327-339: The demo is currently assuming the first result from
createDisplaySets is the video set, which can break when multiple display sets
are returned. Update the selection logic in the videoGroup example to explicitly
find the display set whose preferredViewportType is 'video' before using
instances[0].imageId, and apply the same change in the other affected block so
the mounted viewport and content always come from the video display set.

In `@packages/tools/examples/videoNavigation/index.ts`:
- Around line 172-182: The demo is currently assuming the first result from
createDisplaySets() is the video series, which can break when the series is
split into multiple display sets. Update the videoNavigation flow to explicitly
choose the display set whose preferredViewportType is video before using its
instances[0].imageId, and apply the same selection logic wherever the series is
mounted or rendered in the later videoNavigation code path so the viewport and
content always use the correct video display set.

In `@packages/tools/examples/videoRange/index.ts`:
- Around line 303-314: The demo currently assumes the first entry from
createDisplaySets() is the video set, which can pick the wrong display set when
multiple are returned. Update the videoRange example to explicitly select the
display set whose preferredViewportType is video before accessing
instances[0].imageId, and apply the same selection logic anywhere else in the
file that mounts or references the video display set. Keep the existing error
handling if no matching video display set is found.

In `@packages/tools/examples/videoSplineROITools/index.ts`:
- Around line 279-298: Select the video display set explicitly instead of
relying on the first result from createDisplaySets(). In
videoSplineROITools/index.ts, update the logic around the displaySets
destructuring so it finds the set whose preferredViewportType is 'video' before
using displaySet.instances[0].imageId and
getViewportTypeForDisplaySet(displaySet). Keep the existing no-display-set
guard, but make sure the chosen display set is the video one so the viewport and
mounted content match the demo intent.

In `@utils/demo/helpers/splitDisplaySetsFromImageIds.ts`:
- Around line 100-116: The frame-level image IDs are being collapsed too early
in getInstanceLevelImageIds, which prevents splitImageIdsBySplitRules from
seeing per-frame tags. Update this helper so it preserves distinct
frame-specific candidates instead of keeping only the first imageId per
SOPInstanceUID, and let the later split logic handle grouping; make sure
collectFrameImageIdsForGroup can still operate on the full candidate set for
multiframe MR mixed-b-value cases.

---

Outside diff comments:
In `@packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts`:
- Around line 508-517: The teardown for PlanarViewport is only removing the
renderImageObjectDataId entry, so viewport-owned overlay display sets added by
addImages remain in genericViewportDisplaySetMetadataProvider and keep
buffers/metadata alive. Update the cleanup path in PlanarViewport’s
removeData/destroy logic to also unregister every display set created from
addImages, using the same dataId or displaySetId bookkeeping used when calling
genericViewportDisplaySetMetadataProvider.add. Make sure the fix covers both
teardown locations referenced by the review so stale overlay metadata cannot
survive viewport destruction or be reused later.

In `@packages/core/src/RenderingEngine/WSIViewport.ts`:
- Around line 547-554: `getGenericViewportWSIDisplaySet` is dropping the stored
`miniNavigationOverlay` when it replays metadata through
`WSIViewport.setDataIds`, so the legacy adapter’s saved overlay setting is lost.
Update the metadata shape and forwarding logic in
`getGenericViewportWSIDisplaySet` and `WSIViewportLegacyAdapter.setDataIds` to
include `options.miniNavigationOverlay` alongside `webClient`, and pass it
through unchanged when calling `setDataIds` so registered display sets preserve
the original overlay preference.

In `@packages/tools/examples/videoSegmentation/index.ts`:
- Around line 193-223: The video segmentation example is picking the first
display set from createDisplaySets, which can be the still-image split instead
of the actual video display set. Update the selection logic in the
createDisplaySets / displaySet handling block to explicitly choose the video
display set before using getViewportTypeForDisplaySet(displaySet),
displaySet.instances[0].imageId, and viewport.setDisplaySets, so the viewport is
configured from the correct video series.

In `@packages/tools/examples/videoTools/index.ts`:
- Around line 172-228: The example currently assumes `displaySets[0]` is the
video series, which can pick the still-image set and break both
`getViewportTypeForDisplaySet(displaySet)` and `videoId`. Update the selection
logic in the `createDisplaySets`/`displaySet` block to explicitly find the video
display set using its identifying metadata or type, then use that chosen set for
the viewport setup and `viewport.setDisplaySets` instead of relying on the first
array item.

---

Nitpick comments:
In `@packages/core/src/RenderingEngine/BaseVolumeViewport.ts`:
- Around line 1536-1541: Attach a rejection handler to the fire-and-forget load
path in BaseVolumeViewport by handling the promise returned from volume.load()
after createAndCacheVolume. Keep the non-awaited streaming behavior, but add a
.catch on volume.load() so failures are routed through viewport-scoped logging
or events instead of becoming unhandled promise rejections. Use the unique
symbols createAndCacheVolume, cache.getVolume, and volume.load() to locate the
change.
🪄 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: 253763be-49f0-479d-af99-6418c1586430

📥 Commits

Reviewing files that changed from the base of the PR and between 902d3ae and ffa1af0.

📒 Files selected for processing (97)
  • packages/core/examples/displaySets/index.ts
  • packages/core/examples/dynamicVolume/index.ts
  • packages/core/examples/genericEcg/index.ts
  • packages/core/examples/genericMultiVolumeAPI/index.ts
  • packages/core/examples/genericStackAPI/index.ts
  • packages/core/examples/genericStackPosition/index.ts
  • packages/core/examples/genericVideo/index.ts
  • packages/core/examples/genericViewportScale/index.ts
  • packages/core/examples/genericWsi/index.ts
  • packages/core/examples/video/index.ts
  • packages/core/examples/viewportProjection/index.ts
  • packages/core/examples/wsi/index.ts
  • packages/core/src/RenderingEngine/BaseVolumeViewport.ts
  • packages/core/src/RenderingEngine/ECGViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/ECG/DefaultECGDataProvider.ts
  • packages/core/src/RenderingEngine/GenericViewport/GenericViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/DefaultPlanarDataProvider.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarLegacyCompatibilityController.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewportLegacyAdapter.ts
  • packages/core/src/RenderingEngine/GenericViewport/Video/DefaultVideoDataProvider.ts
  • packages/core/src/RenderingEngine/GenericViewport/Video/VideoViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/ViewportArchitectureTypes.ts
  • packages/core/src/RenderingEngine/GenericViewport/Volume3D/DefaultVolume3DDataProvider.ts
  • packages/core/src/RenderingEngine/GenericViewport/Volume3D/VolumeViewport3DLegacyAdapter.ts
  • packages/core/src/RenderingEngine/GenericViewport/Volume3D/viewport3D.ts
  • packages/core/src/RenderingEngine/GenericViewport/WSI/DefaultWSIDataProvider.ts
  • packages/core/src/RenderingEngine/GenericViewport/WSI/WSIViewportLegacyAdapter.ts
  • packages/core/src/RenderingEngine/GenericViewport/genericViewportDisplaySetAccess.ts
  • packages/core/src/RenderingEngine/GenericViewport/index.ts
  • packages/core/src/RenderingEngine/StackViewport.ts
  • packages/core/src/RenderingEngine/VideoViewport.ts
  • packages/core/src/RenderingEngine/Viewport.ts
  • packages/core/src/RenderingEngine/WSIViewport.ts
  • packages/core/src/utilities/genericViewportDataSetMetadataProvider.ts
  • packages/core/src/utilities/genericViewportDisplaySetMetadataProvider.ts
  • packages/core/src/utilities/index.ts
  • packages/docs/docs/concepts/cornerstone-core/generic-viewport/api.md
  • packages/docs/docs/concepts/cornerstone-core/generic-viewport/data-bindings-and-loading.md
  • packages/docs/docs/concepts/cornerstone-core/generic-viewport/migration.md
  • packages/docs/docs/concepts/cornerstone-metadata/display-sets.md
  • packages/docs/docs/concepts/cornerstone-metadata/index.md
  • packages/docs/docs/migration-guides/5x/2-generic-viewport.md
  • packages/docs/sidebars.js
  • packages/metadata/jest.config.js
  • packages/metadata/src/displayset/BaseDisplaySet.ts
  • packages/metadata/src/displayset/IDisplaySet.ts
  • packages/metadata/src/displayset/ImageStackDisplaySet.ts
  • packages/metadata/src/displayset/buildSeriesInfo.ts
  • packages/metadata/src/displayset/createDisplaySetFromGroup.ts
  • packages/metadata/src/displayset/defaultDisplaySetSplitRules.ts
  • packages/metadata/src/displayset/displaySetProvider.ts
  • packages/metadata/src/displayset/displayset.test.ts
  • packages/metadata/src/displayset/groupInstancesBySplitRules.ts
  • packages/metadata/src/displayset/index.ts
  • packages/metadata/src/displayset/isEcgInstance.ts
  • packages/metadata/src/displayset/isImageInstance.ts
  • packages/metadata/src/displayset/isVideoInstance.ts
  • packages/metadata/src/displayset/isWsiInstance.ts
  • packages/metadata/src/displayset/registerDisplaySetMetadata.ts
  • packages/metadata/src/displayset/resolveInstances.ts
  • packages/metadata/src/displayset/splitImageIdsBySplitRules.ts
  • packages/metadata/src/displayset/types.ts
  • packages/metadata/src/displayset/viewportTypes.ts
  • packages/metadata/src/enums/MetadataModules.ts
  • packages/metadata/src/index.ts
  • packages/metadata/src/registerDefaultProviders.ts
  • packages/metadata/src/types/MetadataModuleTypes.ts
  • packages/metadata/src/utilities/index.ts
  • packages/tools/examples/dynamicCINETool/index.ts
  • packages/tools/examples/generateImageFromTimeData/index.ts
  • packages/tools/examples/genericLabelmapOverlapPlayground/index.ts
  • packages/tools/examples/genericLabelmapRendering/index.ts
  • packages/tools/examples/genericLabelmapSegmentationTools/index.ts
  • packages/tools/examples/genericLabelmapSliceRendering/index.ts
  • packages/tools/examples/genericLabelmapSliceRenderingTools/index.ts
  • packages/tools/examples/genericStackLabelmapSegmentation/index.ts
  • packages/tools/examples/genericStackManipulationTools/index.ts
  • packages/tools/examples/genericVolumeAnnotationTools/index.ts
  • packages/tools/examples/localAdvanced/index.ts
  • packages/tools/examples/videoColor/index.ts
  • packages/tools/examples/videoContourSegmentation/index.ts
  • packages/tools/examples/videoGroup/index.ts
  • packages/tools/examples/videoNavigation/index.ts
  • packages/tools/examples/videoRange/index.ts
  • packages/tools/examples/videoSegmentation/index.ts
  • packages/tools/examples/videoSplineROITools/index.ts
  • packages/tools/examples/videoTools/index.ts
  • packages/tools/examples/viewportProjectionSynchronizer/index.ts
  • packages/tools/examples/wsiAnnotationTools/index.ts
  • packages/tools/src/tools/displayTools/Labelmap/labelmapRenderPlan.spec.ts
  • packages/tools/src/tools/displayTools/Labelmap/labelmapRenderPlan/planarGenericVolumeLabelmap.ts
  • packages/tools/src/tools/displayTools/Labelmap/removeLabelmapRepresentationData.ts
  • tests/vitest-browser/genericStackApi.browser.test.ts
  • utils/ExampleRunner/example-runner-cli.js
  • utils/demo/helpers/index.js
  • utils/demo/helpers/splitDisplaySetsFromImageIds.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/utilities/genericViewportDataSetMetadataProvider.ts

Comment thread packages/core/examples/displaySets/index.ts Outdated
Comment thread packages/docs/docs/concepts/cornerstone-metadata/display-sets.md
Comment thread packages/metadata/src/displayset/BaseDisplaySet.ts
Comment on lines +14 to +31
const NumberOfSeriesRelatedInstances = instances.length;
let numberOfFrames = 0;
let numberOfNonImageObjects = 0;
let numberOfSOPInstanceUIDsPerSeries = 0;

for (const instance of instances) {
if (instance.NumberOfFrames) {
numberOfFrames += Number(instance.NumberOfFrames);
} else if (instance.Rows) {
numberOfFrames += 1;
} else {
numberOfNonImageObjects += 1;
}

if (instance.SOPInstanceUID) {
numberOfSOPInstanceUIDsPerSeries += 1;
}
}

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

Deduplicate repeated underlying instances before aggregating series facts.

This pipeline starts from imageIds, so frame-expanded inputs can resolve the same naturalized instance multiple times. Counting raw array entries here inflates NumberOfSeriesRelatedInstances, numberOfFrames, and numberOfSOPInstanceUIDsPerSeries, which can skew any split rule reading seriesInfo.

Suggested fix
 export function buildSeriesInfo(instances: NaturalizedInstance[]): SeriesInfo {
-  const NumberOfSeriesRelatedInstances = instances.length;
+  const uniqueInstances = [
+    ...new Map(
+      instances.map((instance, index) => [
+        instance.SOPInstanceUID ?? instance.imageId ?? `__instance_${index}`,
+        instance,
+      ])
+    ).values(),
+  ];
+
+  const NumberOfSeriesRelatedInstances = uniqueInstances.length;
   let numberOfFrames = 0;
   let numberOfNonImageObjects = 0;
   let numberOfSOPInstanceUIDsPerSeries = 0;
 
-  for (const instance of instances) {
+  for (const instance of uniqueInstances) {
     if (instance.NumberOfFrames) {
       numberOfFrames += Number(instance.NumberOfFrames);
     } else if (instance.Rows) {
       numberOfFrames += 1;
     } else {
📝 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 NumberOfSeriesRelatedInstances = instances.length;
let numberOfFrames = 0;
let numberOfNonImageObjects = 0;
let numberOfSOPInstanceUIDsPerSeries = 0;
for (const instance of instances) {
if (instance.NumberOfFrames) {
numberOfFrames += Number(instance.NumberOfFrames);
} else if (instance.Rows) {
numberOfFrames += 1;
} else {
numberOfNonImageObjects += 1;
}
if (instance.SOPInstanceUID) {
numberOfSOPInstanceUIDsPerSeries += 1;
}
}
const uniqueInstances = [
...new Map(
instances.map((instance, index) => [
instance.SOPInstanceUID ?? instance.imageId ?? `__instance_${index}`,
instance,
])
).values(),
];
const NumberOfSeriesRelatedInstances = uniqueInstances.length;
let numberOfFrames = 0;
let numberOfNonImageObjects = 0;
let numberOfSOPInstanceUIDsPerSeries = 0;
for (const instance of uniqueInstances) {
if (instance.NumberOfFrames) {
numberOfFrames += Number(instance.NumberOfFrames);
} else if (instance.Rows) {
numberOfFrames += 1;
} else {
numberOfNonImageObjects += 1;
}
if (instance.SOPInstanceUID) {
numberOfSOPInstanceUIDsPerSeries += 1;
}
}
🤖 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 `@packages/metadata/src/displayset/buildSeriesInfo.ts` around lines 14 - 31,
Deduplicate the resolved instances before computing series aggregates in
buildSeriesInfo, because imageIds can expand to the same naturalized instance
multiple times and inflate the series facts. Update the logic around
NumberOfSeriesRelatedInstances, numberOfFrames, numberOfNonImageObjects, and
numberOfSOPInstanceUIDsPerSeries to operate on a unique set keyed by the
underlying instance identity (for example SOPInstanceUID or another stable
instance key) before summing.

Comment thread packages/metadata/src/displayset/defaultDisplaySetSplitRules.ts Outdated
Comment thread packages/tools/examples/videoGroup/index.ts
Comment thread packages/tools/examples/videoNavigation/index.ts
Comment thread packages/tools/examples/videoRange/index.ts
Comment thread packages/tools/examples/videoSplineROITools/index.ts
Comment on lines +100 to +116
function getInstanceLevelImageIds(imageIds: string[]): string[] {
const bySop = new Map<string, string>();

for (const imageId of imageIds) {
const instance = getNaturalizedInstanceForDisplaySetSplit(imageId);
if (!instance) {
continue;
}

const sopUid = instance.SOPInstanceUID as string | undefined;
const key = sopUid ?? imageId;
if (!bySop.has(key)) {
bySop.set(key, instance.imageId ?? toBaseImageId(imageId));
}
}

return [...bySop.values()];

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 collapse frame-level candidates to one imageId per SOP before splitting.

This keeps only the first imageId for each SOPInstanceUID, so frame-specific tags never reach splitImageIdsBySplitRules. For multiframe MR, collectFrameImageIdsForGroup then pulls the whole SOP back into one display set, which defeats the new mixed-b-value split.

🤖 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 `@utils/demo/helpers/splitDisplaySetsFromImageIds.ts` around lines 100 - 116,
The frame-level image IDs are being collapsed too early in
getInstanceLevelImageIds, which prevents splitImageIdsBySplitRules from seeing
per-frame tags. Update this helper so it preserves distinct frame-specific
candidates instead of keeping only the first imageId per SOPInstanceUID, and let
the later split logic handle grouping; make sure collectFrameImageIdsForGroup
can still operate on the full candidate set for multiframe MR mixed-b-value
cases.

sedghi added 2 commits June 29, 2026 18:28
Library:
- defaultDisplaySetSplitRules: mixed-b-value DWI rule now prefers volume
  (MPR) over stack, matching normal MR series; it runs before volume3d so
  stack-first was regressing the defined-b-value subgroup.
- isVideoInstance: check all advertised transfer syntaxes, not just the
  first array entry, so a video syntax in a later slot is not missed.
- BaseDisplaySet: clone viewportTypes and instances so a caller mutating
  them after construction cannot drift the display set / preferredViewportType.
- ImageStackDisplaySet.fromImageIds: preserve caller frame-level imageIds
  instead of collapsing them to underlying-instance ids.
- BaseVolumeViewport: attach a rejection handler to the fire-and-forget
  streaming volume.load().

Examples/docs:
- video examples: select the video display set by preferredViewportType
  instead of assuming displaySets[0], which is fragile once a series splits.
- displaySets example: build metadata rows with DOM nodes/textContent
  instead of innerHTML (avoids injecting DICOM-derived markup).
- docs: import metaData in the getNaturalizedInstance snippet; align the
  mixed-b-value rule example with the volume-first viewport order.
The CodeRabbit nitpick assumed volume.load() returns a promise, but it
returns void, so .catch was a type error that broke the ESM build
(TS2339). Restore the original fire-and-forget volume.load() call.
@sedghi

sedghi commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

Closing — opened in error. These commits were pushed to the existing PR #2738 (branch fix/dwi-wrong-wl-split) instead.

@sedghi sedghi closed this Jun 30, 2026
@sedghi sedghi deleted the fix/dwiwrongwlsplit branch June 30, 2026 00:14
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