Skip to content

fix(tools): only reject a preview the tool actually has - #2891

Open
TFRadicalImaging wants to merge 1 commit into
cornerstonejs:mainfrom
TFRadicalImaging:fix/labelmap-reject-preview-without-preview
Open

fix(tools): only reject a preview the tool actually has#2891
TFRadicalImaging wants to merge 1 commit into
cornerstonejs:mainfrom
TFRadicalImaging:fix/labelmap-reject-preview-without-preview

Conversation

@TFRadicalImaging

@TFRadicalImaging TFRadicalImaging commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Context

LabelmapBaseTool.previewData is a single static object shared by every labelmap tool instance:

public static previewData?: PreviewData = { preview: null, element: null, ... };
protected get _previewData() { return LabelmapBaseTool.previewData; }

So as soon as any brush has painted, previewData.element is set for all of them — the circular brush, the sphere brush, the erasers, the threshold brushes. rejectPreview took that element as its only condition and ran the tool's RejectPreview strategy regardless of whether that tool had a preview (previewData.preview is null in this state).

For the slice-wise tools that is wasted work: the reject handler undoes a history memo only when the memo carries preview voxels.

For the 3D variants it throws. ensureSegmentationVolumeFor3DManipulation raises Volume is not reconstructable for sphere manipulation whenever isValidVolume(viewport.getImageIds()) is false, and BrushTool.onSetToolPassivedisableCursor()rejectPreview(). So deactivating a sphere brush on a series that cannot form a volume (a single-frame DX/CR image, a cine, an unevenly spaced stack) throws — as long as some brush painted first.

The throw escapes ToolGroup.setToolPassive(), which is called from setToolActive() before the new tool is armed. The result on a real viewer: the tool group ends with no active primary tool, the toolbar shows nothing selected, the brush's options disappear and the brush cannot be selected again — clicking it re-arms the sphere, which throws on the next deactivation, and so on.

Measured live (OHIF-based viewer, @cornerstonejs/tools 5.6.8, CR chest, one image per series, isValidVolume: false), instrumenting ToolGroup.setToolActive/setToolPassive and applyActiveStrategyCallback:

setToolPassive default SphereBrush
  STRATEGY_THROW  tool=SphereBrush strategy=FILL_INSIDE_SPHERE cb=rejectPreview
                  "Volume is not reconstructable for sphere manipulation"
      at BrushStrategy.ensureSegmentationVolumeFor3DManipulation
      at getStrategyDataForStackViewport / getStrategyData
→ toolGroup.getActivePrimaryMouseButtonTool() === undefined

with previewData.preview === null and previewData.element set by the earlier circular-brush stroke.

Changes & Results

Run the RejectPreview strategy only when the tool has a preview to reject, in both BrushTool.rejectPreview (which overrides it) and LabelmapBaseTool.rejectPreview. The edit memo is still closed and the shared preview state still fully reset on every call, so undo/redo grouping is unchanged.

Before: deactivating a sphere brush after any brush stroke, on a viewport that cannot form a volume, throws and leaves the tool group with no active tool.
After: the deactivation is silent and the replacement tool is armed as usual.

The sibling acceptPreview has the same unconditional shape, but it is only reached from explicit accept paths (_endCallback for a non-drag interaction, the Enter action), so it is left alone here — happy to include it if you would rather have both symmetric.

Testing

New packages/tools/src/tools/segmentation/__tests__/rejectPreview.spec.ts, run against both classes:

  • the strategy is not applied when another tool set the shared element and there is no preview (the crash path), while doneEditMemo still runs;
  • the strategy is applied when the tool does have a preview, and the preview is cleared;
  • nothing happens at all when no tool has drawn.

pnpm jest --selectProjects tools: 50 suites / 746 tests pass (2 pre-existing skips). Negative check: restoring the unconditional call fails exactly the two "leaves the strategy alone" cases.

Manual: on a single-image CR/DX series, create a labelmap segmentation, arm the brush, paint one stroke, then switch the brush shape between Circle and Sphere. Before the change the switch throws and the brush ends up unarmed with its Shape/Radius options gone; after it, the brush stays armed.

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"
  • "Node version: 24.3.0"
  • "Browser: Chrome 151"

@wayfarer3130 assigning this one to you, as with #2880 — it is the upstream half of a sphere-brush defect we are closing downstream.

Summary by CodeRabbit

  • Bug Fixes
    • Improved segmentation preview rejection when no preview is active.
    • Prevented errors when switching tools on viewports that cannot form a reconstructable volume.
    • Ensured edit sessions close correctly while preserving active tool state.
    • Added coverage for preview rejection and cleanup behavior across brush and labelmap tools.

`LabelmapBaseTool.previewData` is a single static object shared by every
labelmap tool instance, so once any brush has painted, `previewData.element`
is set for all of them. `rejectPreview` took that element as its only
condition and ran the tool's RejectPreview strategy regardless of whether
that tool had a preview.

For the slice-wise tools this is wasted work: the reject handler undoes a
memo only when it carries preview voxels. For the 3D variants it throws.
`ensureSegmentationVolumeFor3DManipulation` raises "Volume is not
reconstructable for sphere manipulation" whenever the viewport cannot form a
volume, and `onSetToolPassive` rejects the preview — so simply deactivating a
sphere brush on a single-frame series (a DX/CR image, a cine, an unevenly
spaced stack) throws, as long as some brush has painted before. The throw
escapes `ToolGroup.setToolPassive` from inside `setToolActive`, so the tool
being activated in its place never is and the tool group is left with no
active primary tool: the toolbar shows no tool armed and a brush cannot be
selected again.

Run the strategy only when there is a preview to reject. The edit memo is
still closed and the shared preview state still reset on every call.
@coderabbitai

coderabbitai Bot commented Sep 1, 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: Team

Run ID: 87b39a90-9410-4fd8-99d9-9dd99907673e

📥 Commits

Reviewing files that changed from the base of the PR and between 1de8b51 and 7be1640.

📒 Files selected for processing (3)
  • packages/tools/src/tools/segmentation/BrushTool.ts
  • packages/tools/src/tools/segmentation/LabelmapBaseTool.ts
  • packages/tools/src/tools/segmentation/__tests__/rejectPreview.spec.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

rejectPreview now invokes the rejection strategy only when preview data exists. Tests cover BrushTool and LabelmapBaseTool behavior with and without previews.

Changes

Preview rejection handling

Layer / File(s) Summary
Conditional rejection strategy guards
packages/tools/src/tools/segmentation/BrushTool.ts, packages/tools/src/tools/segmentation/LabelmapBaseTool.ts
Both tools guard RejectPreview strategy callbacks with the preview state. Preview reset behavior remains unchanged.
Rejection behavior tests
packages/tools/src/tools/segmentation/__tests__/rejectPreview.spec.ts
Tests verify callback suppression without a preview, callback execution with a preview, preview clearing, and edit memo closure.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 7be16

The change prevents brush deactivation from throwing when no preview exists, so users can switch brush tools normally while preserving rejection of real previews. No actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: sedghi, wayfarer3130

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the shared preview-state defect, the code changes, expected behavior, test coverage, manual verification, and tested environment. All required template sections are pr…
Title check ✅ Passed The title uses the semantic-release format and accurately summarizes the main change: reject a preview only when the current tool owns one.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
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.
Full details: Description check

Explanation

The description clearly explains the shared preview-state defect, the code changes, expected behavior, test coverage, manual verification, and tested environment. All required template sections are present and substantially complete.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3 files.

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

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