Skip to content

fix(segmentation): undoable overlap labelmap strokes, faster eager stroke resolution, brush hover guard#2785

Merged
wayfarer3130 merged 7 commits into
mainfrom
cornerstoneFixBrushUndoOverlay
Jul 10, 2026
Merged

fix(segmentation): undoable overlap labelmap strokes, faster eager stroke resolution, brush hover guard#2785
wayfarer3130 merged 7 commits into
mainfrom
cornerstoneFixBrushUndoOverlay

Conversation

@sedghi

@sedghi sedghi commented Jul 6, 2026

Copy link
Copy Markdown
Member

What

Three fixes for overlapping-labelmap editing (segmentation.overwriteMode), found while wiring overlap authoring into a viewer.

1. Undo/redo loses layer bookkeeping for overlap strokes

A stroke that forces a segment move to a private labelmap layer changes three things outside its history memo: the bulk voxel move, the layer registration, and the segment rebinding. Cross-layer overwrite erases were also unrecorded. Undo restored only the stroke's own voxels — the moved voxels stayed stranded on a layer that should not exist at that point in history, and further undos became no-ops.

Labelmap memos now carry ordered restore steps (priorSteps/postSteps, type LabelmapRestoreStep):

  • moveSegmentToPrivateLabelmap emits a moveStep (via moveStepCallback) that reverses/replays the whole move — voxels back to the source layer, binding restored, private layer unregistered on undo; fully replayed on redo.
  • Cross-layer erases are recorded per layer through crossLayerEraseCallback on EraseLabelmapEditTransactionOptions.
  • A mid-stroke voxel-manager swap rides the committed earlier memo along (memoAsStep).
  • Undo walks steps in reverse-chronological order, redo chronologically, so each stroke restores as one unit.

2. Eager strokes (eraser) were ~20x slower than they should be

Every eager (non-lazy) drag event re-ran isReferenceViewable over ALL labelmap imageIds via updateLabelmapSegmentationImageReferences, each call doing full metadata + slice-basis resolution. On a two-layer, 120-image labelmap: ~51ms per drag event. The resolver now memoizes completed scans per segmentation, keyed by the viewport's current image plus a fingerprint of the labelmap imageId set (layer changes invalidate naturally). Same scenario: ~2ms per event.

3. BrushTool crash on mousedown

preMouseDownCallback dereferenced this._hoverData.viewport without guarding createHoverData returning undefined (every other call site guards it). Now bails out cleanly, undoing the draw activation.

Testing

  • New createLabelmapMemo.spec.ts (6 tests): plain stroke round-trips, no-op commit rejection, steps-only commit, undo/redo step ordering, memoAsStep.
  • New privateLabelmap.spec.ts (6 tests): segment move emits a reversible step (voxels, binding, layer registration), default-mover integration via beginLabelmapEditTransaction, cross-layer erase recording, and a full two-stroke timeline (move + private write + cross-layer erase) that round-trips undo x2 / redo x2 to exact states.
  • All existing segmentation suites pass (74 tests across 11 suites); tsc --noEmit clean.
  • Validated in OHIF against a 4-viewport layout: migration undo removes the private layer and rebinds, redo replays; eraser drag events went from ~51ms to ~2ms.

Summary by CodeRabbit

  • New Features
    • Enhanced segmentation undo/redo with step-aware voxel history, including coordinated replay for segment moves and cross-layer erases.
    • Added optional cross-layer erase recording and move-step capture to improve deterministic restore behavior.
  • Bug Fixes
    • Improved Brush Tool mouse-down flow by clearing edit state when hover data is unavailable.
    • Ensured cross-layer erase updates notify segmentation state changes.
  • Tests
    • Added Jest coverage for move/erase undo-redo timelines, memo step-only scenarios, and cached labelmap reference resolution.

sedghi added 3 commits July 6, 2026 11:37
A stroke that forces a segment move to a private labelmap layer changed
three things outside its history memo: the bulk voxel move, the layer
registration, and the segment rebinding. Cross-layer overwrite erases were
also unrecorded. Undo then restored only the stroke's own voxels, leaving
the moved voxels stranded on a layer that should not exist at that point
in history.

Labelmap memos now carry ordered restore steps (priorSteps/postSteps):
moveSegmentToPrivateLabelmap emits a step that reverses/replays the whole
move, cross-layer erases are recorded per layer through
crossLayerEraseCallback, and a mid-stroke voxel-manager swap rides the
committed earlier memo along. Undo walks steps in reverse-chronological
order, redo chronologically, so each stroke restores as one unit.
Every eager (non-lazy) brush or eraser drag event re-ran
isReferenceViewable over ALL labelmap imageIds via
updateLabelmapSegmentationImageReferences, each call doing full metadata
and slice-basis resolution. On a two-layer, 120-image labelmap this cost
about 51ms per drag event (about 19fps while erasing).

The resolver now remembers completed scans per segmentation, keyed by the
viewport's current image plus a fingerprint of the labelmap imageId set,
and replays the stored mapping instead of rescanning. Layer additions or
removals change the fingerprint and force a fresh scan; removeSegmentation
and reset clear the memo. Same scenario now costs about 2ms per event.
…eated

preMouseDownCallback dereferenced this._hoverData.viewport without
guarding the case where createHoverData returns undefined (no active
segment index yet, or the viewport camera is not resolvable), crashing
with 'Cannot read properties of undefined (reading viewport)'. Every
other createHoverData call site already guards this; do the same here and
undo the draw activation before bailing.
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 6, 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: 170ba129-1ef6-4214-8620-e025eca7230c

📥 Commits

Reviewing files that changed from the base of the PR and between cbdea49 and ef20cf3.

📒 Files selected for processing (1)
  • packages/tools/src/tools/segmentation/BrushTool.ts

📝 Walkthrough

Walkthrough

This PR adds coordinated undo/redo handling for labelmap moves and cross-layer erases, extends memo replay ordering, caches labelmap reference scans, and adds an early BrushTool guard.

Changes

Undo/Redo Step Recording for Segment Moves and Cross-Layer Erase

Layer / File(s) Summary
LabelmapMemo step types and restoreMemo coordination
packages/tools/src/utilities/segmentation/createLabelmapMemo.ts, packages/tools/src/utilities/segmentation/createLabelmapMemo.spec.ts
Adds LabelmapRestoreStep, prior/post step tracking, coordinated restore ordering, memo wrapping, updated commit retention, and tests.
moveSegmentToPrivateLabelmap move step callback
packages/tools/src/stateManagement/segmentation/labelmapModel/privateLabelmap.ts
Tracks changed voxels and replays voxel, binding, and private-labelmap lifecycle changes through undo/redo handlers.
labelmapEditTransaction move step and cross-layer erase records
packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.ts
Adds move and erase callback contracts, captures transaction move steps, and emits per-layer erased voxel records.
BrushStrategy and crossLayerErase memo wiring
packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts, packages/tools/src/tools/segmentation/strategies/utils/crossLayerErase.ts
Records memo swaps, segment moves, and cross-layer erase operations in stroke history with segmentation update events.
privateLabelmap integration tests
packages/tools/src/stateManagement/segmentation/labelmapModel/privateLabelmap.spec.ts
Tests private-layer moves, transaction callbacks, cross-layer erases, and combined multi-stroke undo/redo behavior.

Reference Resolver Memoization and Brush Tool Guard

Layer / File(s) Summary
Scan-key memoization for image reference resolution
packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.ts, packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.spec.ts
Adds completed-scan caching, lifecycle cleanup, cached current/all scan handling, and repeated-viewport scan coverage.
BrushTool hover-data guard
packages/tools/src/tools/segmentation/BrushTool.ts
Clears edit data and returns early when hover data is unavailable.

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

Sequence Diagram(s)

sequenceDiagram
  participant BrushStrategy
  participant LabelmapEditTransaction
  participant LabelmapMemo
  participant PrivateLabelmap
  participant SegmentationEvent

  BrushStrategy->>LabelmapEditTransaction: begin labelmap edit transaction
  LabelmapEditTransaction->>PrivateLabelmap: move segment and capture move step
  BrushStrategy->>LabelmapMemo: record move and erase steps
  LabelmapMemo->>PrivateLabelmap: restore voxel and binding state
  LabelmapMemo->>SegmentationEvent: trigger segmentation data modified
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it misses required template sections and leaves the checklist incomplete. Add the missing Context and Checklist sections, and rename or expand 'What' to match the required 'Changes & Results' heading.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, accurate, and follows the semantic-release fix(scope): ... format.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cornerstoneFixBrushUndoOverlay

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/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.ts`:
- Around line 160-199: The memoized path in labelmapImageReferenceResolver’s
current-image lookup is reusing stackLabelmapImageIdReferenceMap entries that
are shared across viewports, so a scanKey can pass while still returning a value
computed for another viewport. Update the caching in the same resolver flow that
uses generateScanKey, hasCompletedScan, and markCompletedScan so the resolved
labelmap image id is stored per scanKey (or otherwise scoped by viewport.id)
instead of only by segmentationId/currentImageId, and read back that same keyed
value on cache hits.
🪄 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: 4436fc02-1c91-4d1f-ac16-d6d7396187d5

📥 Commits

Reviewing files that changed from the base of the PR and between b4c094e and 3489cbf.

📒 Files selected for processing (9)
  • packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.ts
  • packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.ts
  • packages/tools/src/stateManagement/segmentation/labelmapModel/privateLabelmap.spec.ts
  • packages/tools/src/stateManagement/segmentation/labelmapModel/privateLabelmap.ts
  • packages/tools/src/tools/segmentation/BrushTool.ts
  • packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts
  • packages/tools/src/tools/segmentation/strategies/utils/crossLayerErase.ts
  • packages/tools/src/utilities/segmentation/createLabelmapMemo.spec.ts
  • packages/tools/src/utilities/segmentation/createLabelmapMemo.ts

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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

🧹 Nitpick comments (1)
packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.spec.ts (1)

218-251: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test doesn't assert viewport2.isReferenceViewable call count.

The test validates viewport1.isReferenceViewable invocation count but not viewport2's, leaving the vp-2 scan path (and its interaction with the vp-1 cache entry) partially unverified.

♻️ Suggested addition
       expect(
         resolver.updateLabelmapSegmentationImageReferences('vp-1', 'seg-1')
       ).toBe('lm-a-0');
       expect(viewport1.isReferenceViewable).toHaveBeenCalledTimes(2);
+      expect(viewport2.isReferenceViewable).toHaveBeenCalledTimes(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/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.spec.ts`
around lines 218 - 251, The cache replay test for
updateLabelmapSegmentationImageReferences only verifies viewport1’s scan path;
add an assertion for viewport2.isReferenceViewable call count so the vp-2 lookup
and its interaction with the vp-1 cache entry are fully covered. Use the
existing viewport1 and viewport2 mock setup in
labelmapImageReferenceResolver.spec.ts and assert viewport2’s invocation count
alongside the current expectations.
🤖 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.

Nitpick comments:
In
`@packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.spec.ts`:
- Around line 218-251: The cache replay test for
updateLabelmapSegmentationImageReferences only verifies viewport1’s scan path;
add an assertion for viewport2.isReferenceViewable call count so the vp-2 lookup
and its interaction with the vp-1 cache entry are fully covered. Use the
existing viewport1 and viewport2 mock setup in
labelmapImageReferenceResolver.spec.ts and assert viewport2’s invocation count
alongside the current expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 29387e0e-5219-4cd4-94e3-51ef8520581d

📥 Commits

Reviewing files that changed from the base of the PR and between 3489cbf and 7f82dd8.

📒 Files selected for processing (2)
  • packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.spec.ts
  • packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tools/src/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.ts

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@wayfarer3130 wayfarer3130 merged commit 7d7dfb8 into main Jul 10, 2026
17 checks passed
@wayfarer3130 wayfarer3130 deleted the cornerstoneFixBrushUndoOverlay branch July 10, 2026 19:21
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.

2 participants