Skip to content

fix(planar): anchor imageId-addressed slices at their exact IJK world center#2800

Merged
sedghi merged 3 commits into
mainfrom
fix/planar-volume-reference-slice-order
Jul 11, 2026
Merged

fix(planar): anchor imageId-addressed slices at their exact IJK world center#2800
sedghi merged 3 commits into
mainfrom
fix/planar-volume-reference-slice-order

Conversation

@sedghi

@sedghi sedghi commented Jul 10, 2026

Copy link
Copy Markdown
Member

Context

Follow-up to #2799 — that fix delivered the payload's initialImageIdIndex in the volume's imageId ordering, but two write paths still interpret imageId-list indices in the slice basis's CAMERA (scroll) ordering, which for the acquisition orientation runs along the NEGATED scan axis. An imageId-list index fed into the camera-order walk (min + index * spacing along the viewPlaneNormal) therefore navigates to the mirrored slice, and on oblique volumes additionally drifts off slice centers (the walk's corner projections span the voxel bounding box, not slice centers).

Observed in OHIF (next viewports): opening a SEG jumps to firstSegmentedSliceImageId via setViewReference({ referencedImageId }) — the viewport landed on slice 18/30 instead of 13/30 (mirror indices: 12 + 17 = 29). Reproducible live with one line: viewport.setViewReference({ referencedImageId: imageIds[12] }) landed on the plane of imageIds[17].

Changes & Results

  • New shared getVolumeImageIdIndexWorldPoint(imageVolume, k): the exact world center of IJK slice k via indexToWorld — the ordering-safe anchor for an index expressed in the volume's imageId list (volumes are constructed so imageIds[k] IS IJK slice k).
  • PlanarViewReferenceController: referencedImageId references (and plane points snapped via getClosestImageId) now resolve to that exact world center instead of routing an imageId-list index through the camera-order getVolumeSliceWorldPointForImageIdIndex. Numeric sliceIndex references keep the camera-order domain and the existing opposite-normal flip — scroll/getSliceIndex semantics are untouched.
  • PlanarViewport.createInitialVolumeSliceState: an explicitly carried initialImageIdIndex (addresses payload.imageIds) is anchored the same way; the undefined-index centering path is unchanged.
  • Test harness: the volume metaData mock placed imageIds[k] at world z = numSlices - 1 - k while indexToWorld mapped k -> z = k — modeling a volume whose imageId list runs against its own k axis, which createAndCacheVolume never produces. That coincidence made the camera-order walk line up with the imageId ordering in tests and masked the mirror. The mock now follows the real invariant; two read-side expectations that relied on the coincidence now assert the truthful pairing (camera-order sliceIndex alongside the actually displayed imageId), and new tests pin the round-trip (setViewReference(imageId) → that image is current) and the helper's geometry (including a flipped-Z volume and clamping).

Testing

  • npx jest (packages/core): 31 suites, 534 passed.
  • Live in OHIF against a 30-slice oblique MR + SEG study (next viewports): opening the SEG now lands on the first segmented slice (13/30), matching the legacy path; setViewReference({ referencedImageId }) round-trips to the exact requested image.

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.)

Summary by CodeRabbit

  • New Features

    • Improved volume slice navigation using exact slice positions in world space.
    • Referenced images now navigate directly to their corresponding slice centers.
    • Initial volume slices honor the specified image index more precisely.
  • Bug Fixes

    • Improved handling of reversed slice directions and out-of-range slice indexes.
    • Added fallback behavior when exact slice positioning is unavailable.
  • Tests

    • Added coverage for slice-center calculations, flipped volumes, index clamping, and missing image data.

… center

The planar volume slice basis positions a slice by walking
min + index * spacing along the viewPlaneNormal, which counts slices in
CAMERA (scroll) order. For the acquisition orientation the camera normal
is the negated scan axis, so an index expressed in the volume's imageId
list ordering - which runs along the scan axis - lands on the mirrored
slice when fed into that walk (and drifts off slice centers on oblique
volumes, whose corner projections span the voxel bounding box).

Two write paths fed imageId-list indices into the camera-order walk:

- PlanarViewReferenceController resolved referencedImageId (and plane
  points snapped via getClosestImageId) to an index in the volume's
  imageId list, then navigated through
  getVolumeSliceWorldPointForImageIdIndex. setViewReference with a
  referencedImageId therefore navigated to the mirror of the requested
  slice on volume-backed planar viewports.
- PlanarViewport.createInitialVolumeSliceState fed the payload's
  initialImageIdIndex (which addresses payload.imageIds) into the basis,
  so an explicitly carried initial slice opened mirrored.

Both now anchor at the slice's exact IJK center via a shared
getVolumeImageIdIndexWorldPoint helper (volumes are constructed so
imageIds[k] IS IJK slice k). Numeric sliceIndex references keep the
camera-order domain and the existing opposite-normal flip.

The test harness previously placed volume imageIds at mirrored world
positions (z = numSlices - 1 - index) while indexToWorld mapped k -> z=k,
modeling a volume whose imageId list runs against its own k axis -
something createAndCacheVolume never produces - which made the
camera-order walk coincide with the imageId ordering and masked the
mirror. The mock now follows the real invariant, and the two read-side
expectations that relied on the coincidence now assert the truthful
pairing (camera-order sliceIndex alongside the actually displayed
imageId).
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • tests/screenshots/chromium/MPRReformat.spec.ts/compat-mpr-reformat-after-interaction.png is excluded by !**/*.png
  • tests/screenshots/chromium/MPRReformat.spec.ts/compat-mpr-reformat-after.png is excluded by !**/*.png
  • tests/screenshots/chromium/MPRReformat.spec.ts/compat-mpr-reformat-before.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae8116d7-0c9e-42af-b573-4e9955de3e9e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Volume slice references now resolve image-list indices to exact world-space slice centers when possible. Explicit initial slice indices use the same mapping, with fallback to existing index-based behavior. Tests cover flipped volumes, clamping, missing image data, and controller navigation.

Changes

Volume slice targeting

Layer / File(s) Summary
Slice-center resolution and initialization
packages/core/src/RenderingEngine/GenericViewport/Planar/planarSliceBasis.ts, packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts, packages/core/test/planarSliceBasis.jest.js
Adds world-point resolution for clamped volume image indices and uses it for explicit initial slice indices, with fallback behavior when image data is unavailable.
Reference target resolution
packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewReferenceController.ts
Resolves camera-order indices, referenced images, and focal points into slice targets containing either exact world points or image indices.
Navigation and geometry validation
packages/core/test/planarViewReference.jest.js
Updates volume geometry expectations and verifies exact-center navigation for referenced images without using mirrored camera-order lookup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlanarViewReferenceController
  participant resolveVolumeReferenceSliceTarget
  participant getVolumeImageIdIndexWorldPoint
  participant PlanarViewport
  PlanarViewReferenceController->>resolveVolumeReferenceSliceTarget: resolve volume slice reference
  resolveVolumeReferenceSliceTarget->>getVolumeImageIdIndexWorldPoint: calculate image-index world center
  getVolumeImageIdIndexWorldPoint-->>resolveVolumeReferenceSliceTarget: return world point or undefined
  resolveVolumeReferenceSliceTarget-->>PlanarViewReferenceController: return slice target
  PlanarViewReferenceController->>PlanarViewport: apply world point or image index
Loading

Possibly related PRs

Suggested reviewers: wayfarer3130

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, follows semantic-release style, and accurately summarizes the main fix.
Description check ✅ Passed The description includes the required Context, Changes & Results, Testing, and Checklist sections with concrete details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/planar-volume-reference-slice-order

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: 1

🤖 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/src/RenderingEngine/GenericViewport/Planar/PlanarViewReferenceController.ts`:
- Around line 474-481: Update the sliceWorldPoint resolution in the relevant
controller method to prioritize the resolved sliceTarget anchor
(sliceWorldPoint, then imageIdIndex-derived point) before falling back to
normalizedViewRef.cameraFocalPoint. This ensures
resolveVolumeReferenceSliceTarget results determine the exact slice center for
combined references.
🪄 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: 6058541c-35a3-4500-b927-b37b00d6dc3e

📥 Commits

Reviewing files that changed from the base of the PR and between 642314b and 18d015f.

📒 Files selected for processing (5)
  • packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewReferenceController.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/PlanarViewport.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/planarSliceBasis.ts
  • packages/core/test/planarSliceBasis.jest.js
  • packages/core/test/planarViewReference.jest.js

Comment on lines 474 to 481
const sliceWorldPoint =
normalizedViewRef.cameraFocalPoint ??
(typeof nextImageIdIndex === 'number'
? this.host.getVolumeSliceWorldPointForImageIdIndex(nextImageIdIndex)
sliceTarget?.sliceWorldPoint ??
(typeof sliceTarget?.imageIdIndex === 'number'
? this.host.getVolumeSliceWorldPointForImageIdIndex(
sliceTarget.imageIdIndex
)
: 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 | 🟠 Major | ⚡ Quick win

Prioritize the resolved slice target over the raw focal point.

Line 475 currently wins whenever a reference carries cameraFocalPoint, so imageId/plane-point targets computed by resolveVolumeReferenceSliceTarget are discarded rather than snapping to the exact IJK slice center. This leaves combined references on an arbitrary focal point instead of the intended slice anchor.

Proposed fix
 const sliceWorldPoint =
-  normalizedViewRef.cameraFocalPoint ??
   sliceTarget?.sliceWorldPoint ??
   (typeof sliceTarget?.imageIdIndex === 'number'
     ? this.host.getVolumeSliceWorldPointForImageIdIndex(
         sliceTarget.imageIdIndex
       )
-    : undefined);
+    : undefined) ??
+  normalizedViewRef.cameraFocalPoint;
📝 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 sliceWorldPoint =
normalizedViewRef.cameraFocalPoint ??
(typeof nextImageIdIndex === 'number'
? this.host.getVolumeSliceWorldPointForImageIdIndex(nextImageIdIndex)
sliceTarget?.sliceWorldPoint ??
(typeof sliceTarget?.imageIdIndex === 'number'
? this.host.getVolumeSliceWorldPointForImageIdIndex(
sliceTarget.imageIdIndex
)
: undefined);
const sliceWorldPoint =
sliceTarget?.sliceWorldPoint ??
(typeof sliceTarget?.imageIdIndex === 'number'
? this.host.getVolumeSliceWorldPointForImageIdIndex(
sliceTarget.imageIdIndex
)
: undefined) ??
normalizedViewRef.cameraFocalPoint;
🤖 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/PlanarViewReferenceController.ts`
around lines 474 - 481, Update the sliceWorldPoint resolution in the relevant
controller method to prioritize the resolved sliceTarget anchor
(sliceWorldPoint, then imageIdIndex-derived point) before falling back to
normalizedViewRef.cameraFocalPoint. This ensures
resolveVolumeReferenceSliceTarget results determine the exact slice center for
combined references.

sedghi added 2 commits July 10, 2026 19:18
…itial slice

The compat lane's initial volume slice was one slice off the legacy
center: getInitialVolumeImageIdIndex returns floor(count / 2) to match
legacy resetCamera's getVolumeCenterIJK snap, but the camera-order basis
walk landed that index on its mirror - IJK floor((count - 1) / 2) for
even slice counts. The 30-slice MPR reformat volume therefore opened on
IJK 14 instead of the legacy IJK 15, and the old baselines encode that
off-center slice. With imageId-list indices now anchored at their exact
IJK centers the compat mount opens on IJK 15, matching legacy
resetCamera; baselines are promoted from the CI runner's actuals.
Same legacy-centered initial slice as the sibling baselines; this
snapshot was previously unreached because the earlier assertion in the
test failed first. Promoted from the CI runner's actual.
@sedghi sedghi merged commit 1a63832 into main Jul 11, 2026
17 checks passed
@sedghi sedghi deleted the fix/planar-volume-reference-slice-order branch July 11, 2026 02:40
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