Skip to content

fix(segmentation): scope SegmentLabelTool hover to the current slice for contours - #2770

Merged
jbocce merged 15 commits into
cornerstonejs:mainfrom
jbocce:fix/OHIF-2641-segment-name-on-hover
Jul 1, 2026
Merged

fix(segmentation): scope SegmentLabelTool hover to the current slice for contours#2770
jbocce merged 15 commits into
cornerstonejs:mainfrom
jbocce:fix/OHIF-2641-segment-name-on-hover

Conversation

@jbocce

@jbocce jbocce commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Context

See OHIF/Viewers#6080

Changes & Results

Skip contours that are not on the currently displayed slice. The distance between the contour plane and the camera focal point, measured along the view plane normal, must be within half the slice spacing.

Testing

  1. Run the segment label example.
  2. There are two slices with segments - slice A and B. Note the position of each segment - SA and and SB.
  3. On slice A hover over SA, the segment label for slice A should display.
  4. On slice A hover over SB, no segment label should display.
  5. Do similar steps for slice B.

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

System:
OS: Windows 11 10.0.26200
CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700H
Memory: 8.83 GB / 31.68 GB
Binaries:
Node: 24.15.0 - C:\Users\joebo\AppData\Local\fnm_multishells\50148_1782198425052\node.EXE
Yarn: 1.22.22 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 11.12.1 - C:\Users\joebo\AppData\Local\fnm_multishells\50148_1782198425052\npm.CMD
pnpm: 11.5.2 - C:\Users\joebo\AppData\Local\pnpm\bin\pnpm.CMD
bun: 1.2.23 - C:\Users\joebo.bun\bin\bun.EXE
Browsers:
Chrome: 149.0.7827.115
Edge: Chromium (149.0.4022.80)
Internet Explorer: 11.0.26100.8115

Summary by CodeRabbit

  • New Features

    • Enhanced the segment label example so hover labels stay correctly scoped to the segments shown on the currently visible slice.
  • Bug Fixes

    • Fixed hover behavior so hovering empty/off-slice areas no longer displays incorrect segment labels.
    • Improved contour hit detection to align with what’s rendered for the current viewport slice.
  • Tests

    • Added Playwright coverage for slice-specific segment label hover behavior (including a reliable scroll-and-recheck flow).
    • Updated example loading timing for stack annotation tests and expanded mobile coverage for the hover spec.

@jbocce
jbocce requested review from sedghi and wayfarer3130 June 25, 2026 08:18
@coderabbitai

coderabbitai Bot commented Jun 25, 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: dd09f3a0-249d-4ded-b8f5-80a8c6b5fdb2

📥 Commits

Reviewing files that changed from the base of the PR and between e572ab6 and 71c9141.

📒 Files selected for processing (1)
  • tests/segmentLabelHover.spec.ts
💤 Files with no reviewable changes (1)
  • tests/segmentLabelHover.spec.ts

📝 Walkthrough

Walkthrough

The PR tightens segment-label hover behavior to the displayed slice, updates the example seeding flow, adds regression coverage, and makes two small viewport-related code changes in type formatting and VOI selection.

Changes

Segment label hover scope

Layer / File(s) Summary
Demo labels and seeded slices
packages/tools/examples/segmentLabel/index.ts
Adds SEGMENT_SLICE_SEPARATION, updates the example text, labels both contour segments, seeds them on separate slices, and restores the original camera state before rendering.
Slice-scoped hover filtering
packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts
Filters contour annotations to the ones displayed for the current viewport before hit-testing, replacing the prior view-plane-normal check.
Hover regression coverage
tests/segmentLabelHover.spec.ts, playwright.config.ts, tests/stackAnnotation.spec.ts, tests/stackAnnotationTiled.spec.ts
Adds a hover regression spec, excludes it from two mobile Playwright projects, and adjusts the stack annotation example visit timing.

Viewport cleanup

Layer / File(s) Summary
Viewport type formatting
packages/core/src/types/IGenericViewport.ts
Rewraps the getResolvedView() return type without changing its type.
Livewire VOI fallback
packages/tools/src/tools/annotation/LivewireContourTool.ts
Rewrites the voiRange fallback expression for display-set-capable viewports.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: wayfarer3130, sedghi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 matches the main change and uses the repo's semantic-release style.
Description check ✅ Passed The description follows the template with Context, Changes & Results, Testing, and a completed checklist, so it is sufficiently complete.
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

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

🧹 Nitpick comments (1)
utils/ExampleRunner/example-runner-cli.js (1)

296-309: 🩺 Stability & Availability | 🔵 Trivial

Quote the webpackConfigPath argument to prevent shell splitting on spaces.

When shell: true is enabled, Node concatenates the command and arguments into a single string and executes it via the system shell. The rspackBin is correctly quoted to handle spaces, but webpackConfigPath (derived from path.join(__dirname, ...)) is passed unquoted. If the installation directory contains spaces (e.g., Program Files), the shell will split the argument at the space, causing rspack to fail to locate the configuration file.

🔧 Quote the config path argument
     const rspackArgs = [
       'serve',
       '--host',
       '0.0.0.0',
       ...(options.https ? ['--https'] : []),
       '--config',
-      webpackConfigPath,
+      `"${webpackConfigPath}"`,
     ];
🤖 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/ExampleRunner/example-runner-cli.js` around lines 296 - 309, The rspack
launch in example-runner-cli.js is not quoting the webpackConfigPath argument
while shell: true is used, so paths with spaces can be split by the shell.
Update the spawnSync call near the rspackArgs construction so the config path is
passed in a shell-safe quoted form, matching the existing handling of rspackBin.
Keep the fix localized to the rspack serve invocation and ensure
webpackConfigPath is treated as a single argument even when the install path
contains spaces.
🤖 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 `@tests/segmentLabelHover.spec.ts`:
- Around line 40-52: In getHoverPositions, add a null guard for
viewport.boundingBox() before reading width/height so hidden elements don’t
cause a dereference failure, and then validate the computed segment1 and
segment2Projection coordinates against the returned bounding box dimensions
before returning them. Update the logic in getHoverPositions to either clamp or
reject out-of-bounds hover points so the hover test only uses coordinates that
are inside the viewport.

---

Nitpick comments:
In `@utils/ExampleRunner/example-runner-cli.js`:
- Around line 296-309: The rspack launch in example-runner-cli.js is not quoting
the webpackConfigPath argument while shell: true is used, so paths with spaces
can be split by the shell. Update the spawnSync call near the rspackArgs
construction so the config path is passed in a shell-safe quoted form, matching
the existing handling of rspackBin. Keep the fix localized to the rspack serve
invocation and ensure webpackConfigPath is treated as a single argument even
when the install path contains spaces.
🪄 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: 5ee1bcce-b425-4bb3-933b-715928a13a70

📥 Commits

Reviewing files that changed from the base of the PR and between fcab92d and 76be037.

📒 Files selected for processing (5)
  • packages/tools/examples/segmentLabel/index.ts
  • packages/tools/src/utilities/segmentation/getSegmentIndexAtWorldPoint.ts
  • playwright.config.ts
  • tests/segmentLabelHover.spec.ts
  • utils/ExampleRunner/example-runner-cli.js

Comment thread tests/segmentLabelHover.spec.ts
@wayfarer3130

Copy link
Copy Markdown
Collaborator

The technically correct version of this is to show an annotation if the source annotation slice thickness as originally created is within the destination slice range. That is, if the annotation was created on slice S of thickness S_T and normal S_N with a point on S_P, then it should be displayed on destination plane D with the same sub-variables if:
(S_P-D_P) dot S_T < (S_T + D_T)/2
If you look at the TMTV examples, you will see some slices that you want to treat that way. It works for annotations as well as segments.
The less than is very specifically NOT less than or equals, because for consecutive slices, this works out to:
(S_P - SNext_P) dot S_T = S_T (by definition), and the next slice is exactly on the edge of what should not be displayed.
@sedghi - do we want to be this strict on the definition?

@sedghi

sedghi commented Jun 26, 2026

Copy link
Copy Markdown
Member

Can't you use the isPlaneViewable or isReferenceViewable instead? this is the same logic we use for rendering, i'm not sure about this custom logic

@jbocce

jbocce commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Can't you use the isPlaneViewable or isReferenceViewable instead? this is the same logic we use for rendering, i'm not sure about this custom logic

Yes I can. Thanks so much.

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

🤖 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/tools/src/tools/annotation/LivewireContourTool.ts`:
- Around line 235-243: The `voiRange` fallback in `LivewireContourTool` is still
being forced to `Types.VOIRange` even when both
`getDisplaySetPresentation(dataId)?.voiRange` and `getDefaultVOIRange(dataId)`
return `undefined`. Remove the trailing no-op `?? undefined`/unsafe cast, keep
`voiRange` as optional, and add a guard before calling
`LivewireScissors.createInstanceFromRawPixelData` so the scissors instance is
only created when a real VOI range is available.
- Around line 233-243: The VOI lookup in LivewireContourTool’s source-viewport
branch can still resolve to undefined, but
LivewireScissors.createInstanceFromRawPixelData expects voiRange to be present.
Update the logic around csUtils.viewportSupportsDisplaySetPresentation,
getDisplaySetPresentation, and getDefaultVOIRange to either guard against a
missing VOI range before creating the scissors or provide an explicit safe
default, and remove the final cast that masks the undefined case.
🪄 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: d1b2fe92-aa7e-4a7a-a877-202c2dcca37c

📥 Commits

Reviewing files that changed from the base of the PR and between 394a3ec and f504f32.

📒 Files selected for processing (2)
  • packages/core/src/types/IGenericViewport.ts
  • packages/tools/src/tools/annotation/LivewireContourTool.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/types/IGenericViewport.ts

Comment thread packages/tools/src/tools/annotation/LivewireContourTool.ts
Comment on lines +235 to 243
voiRange = ((dataId
? (
sourceViewport.getDisplaySetPresentation(dataId) as
| { voiRange?: Types.VOIRange }
| undefined
)?.voiRange
: undefined) ?? sourceViewport.getDefaultVOIRange(dataId)) ??
: undefined) ??
sourceViewport.getDefaultVOIRange(dataId) ??
undefined) as Types.VOIRange;

@coderabbitai coderabbitai Bot Jul 1, 2026

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

Final as Types.VOIRange cast can mask an actually-undefined value.

Both getDisplaySetPresentation(dataId)?.voiRange and getDefaultVOIRange(dataId) can legitimately resolve to undefined (per the PlanarDataPresentation/getDefaultVOIRange signatures). The trailing ?? undefined is a no-op, and the outer as Types.VOIRange cast then forces the type to non-optional even when the value is genuinely undefined. This voiRange is passed straight into LivewireScissors.createInstanceFromRawPixelData(scalarData, width, height, voiRange) without a guard, so if both sources are absent, downstream code silently receives undefined typed as VOIRange.

Suggested simplification
-      voiRange = ((dataId
-        ? (
-            sourceViewport.getDisplaySetPresentation(dataId) as
-              | { voiRange?: Types.VOIRange }
-              | undefined
-          )?.voiRange
-        : undefined) ??
-        sourceViewport.getDefaultVOIRange(dataId) ??
-        undefined) as Types.VOIRange;
+      voiRange =
+        (dataId
+          ? sourceViewport.getDisplaySetPresentation(dataId)?.voiRange
+          : undefined) ?? sourceViewport.getDefaultVOIRange(dataId);

Consider also handling the case where voiRange remains undefined before constructing the scissors instance.

📝 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
voiRange = ((dataId
? (
sourceViewport.getDisplaySetPresentation(dataId) as
| { voiRange?: Types.VOIRange }
| undefined
)?.voiRange
: undefined) ?? sourceViewport.getDefaultVOIRange(dataId)) ??
: undefined) ??
sourceViewport.getDefaultVOIRange(dataId) ??
undefined) as Types.VOIRange;
voiRange =
(dataId
? sourceViewport.getDisplaySetPresentation(dataId)?.voiRange
: undefined) ?? sourceViewport.getDefaultVOIRange(dataId);
🤖 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/src/tools/annotation/LivewireContourTool.ts` around lines 235
- 243, The `voiRange` fallback in `LivewireContourTool` is still being forced to
`Types.VOIRange` even when both `getDisplaySetPresentation(dataId)?.voiRange`
and `getDefaultVOIRange(dataId)` return `undefined`. Remove the trailing no-op
`?? undefined`/unsafe cast, keep `voiRange` as optional, and add a guard before
calling `LivewireScissors.createInstanceFromRawPixelData` so the scissors
instance is only created when a real VOI range is available.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I didn't actually change this file so leaving it as is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@jbocce, understood — since this reshuffle is just a formatting/rebase artifact and the underlying voiRange fallback behavior predates this PR, I won't push for a fix here. Let me know if you'd like a follow-up issue opened to track the potential undefined VOI range masking separately; otherwise I'll leave it as is.

jbocce and others added 3 commits July 1, 2026 09:36
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@jbocce
jbocce merged commit 3dd8fa7 into cornerstonejs:main Jul 1, 2026
14 checks passed
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