From 7929f0898f1bf882682a912b1c32d93d0944726f Mon Sep 17 00:00:00 2001 From: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:52:14 -0400 Subject: [PATCH] fix(measurement): Restore viewport interactivity when deleting in-progress Spline or Livewire measurement (#5905) --- .../cornerstone/src/initMeasurementService.ts | 8 ++- tests/Livewire.spec.ts | 70 +++++++++++++++++- tests/Spline.spec.ts | 72 ++++++++++++++++++- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index 9decf4cd4..bedf84ecd 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -1,5 +1,5 @@ import { eventTarget, Types } from '@cornerstonejs/core'; -import { Enums, annotation } from '@cornerstonejs/tools'; +import { Enums, annotation, cancelActiveManipulations } from '@cornerstonejs/tools'; import { DicomMetadataStore } from '@ohif/core'; import * as CSExtensionEnums from './enums'; @@ -504,6 +504,12 @@ const connectMeasurementServiceToTools = ({ if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { return; } + // Cancel any active tool manipulation (e.g., Spline/Livewire) to avoid leaving the tool + // in a drawing state after deleting a not completed measurement, which can block viewport interactivity. + const element = getActiveViewportEnabledElement(viewportGridService)?.viewport?.element; + if (element) { + cancelActiveManipulations(element); + } const removedAnnotation = annotation.state.getAnnotation(removedMeasurementId); removeAnnotation(removedMeasurementId); // Ensure `removedAnnotation` is available before triggering the memo, diff --git a/tests/Livewire.spec.ts b/tests/Livewire.spec.ts index c6f3d211c..cce917d00 100644 --- a/tests/Livewire.spec.ts +++ b/tests/Livewire.spec.ts @@ -1,4 +1,5 @@ -import { checkForScreenshot, screenShotPaths, test, visitStudy } from './utils'; +import { checkForScreenshot, screenShotPaths, test, visitStudy, expect } from './utils'; +import { press } from './utils/keyboardUtils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; @@ -25,3 +26,70 @@ test('should display the livewire tool', async ({ await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); await checkForScreenshot(page, page, screenShotPaths.livewire.livewireDisplayedCorrectly); }); + +test('should restore viewport interactivity after deleting an in-progress Livewire annotation via context menu', async ({ + DOMOverlayPageObject, + mainToolbarPageObject, + viewportPageObject, +}) => { + await mainToolbarPageObject.measurementTools.livewireContour.click(); + const activeViewport = await viewportPageObject.active; + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + ]); + + await activeViewport.clickAt([{ x: 550, y: 312 }], 'right'); + + const deleteButton = DOMOverlayPageObject.viewport.annotationContextMenu.delete; + await expect(deleteButton.locator).toBeVisible(); + await deleteButton.click(); + + await expect(activeViewport.nthAnnotation(0).locator).toBeHidden(); + + // Draw and complete a new Livewire annotation to verify interactivity is restored + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + { x: 581, y: 287 }, + { x: 482, y: 333 }, + { x: 383, y: 301 }, + ]); + await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); + await expect(activeViewport.nthAnnotation(0).locator).toBeVisible(); +}); + +test('should restore viewport interactivity after deleting an in-progress Livewire annotation via Backspace', async ({ + page, + DOMOverlayPageObject, + mainToolbarPageObject, + viewportPageObject, +}) => { + await mainToolbarPageObject.measurementTools.livewireContour.click(); + const activeViewport = await viewportPageObject.active; + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + ]); + + // Ensure the three points clicked above are rendered in the DOM before pressing Backspace + await expect(activeViewport.svg('circle')).toHaveCount(3); + await press({ page, key: 'Backspace' }); + + await expect(activeViewport.nthAnnotation(0).locator).toBeHidden(); + + // Draw and complete a new Livewire annotation to verify interactivity is restored + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + { x: 581, y: 287 }, + { x: 482, y: 333 }, + { x: 383, y: 301 }, + ]); + await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); + await expect(activeViewport.nthAnnotation(0).locator).toBeVisible(); +}); diff --git a/tests/Spline.spec.ts b/tests/Spline.spec.ts index 79a41ee60..70356d25f 100644 --- a/tests/Spline.spec.ts +++ b/tests/Spline.spec.ts @@ -1,4 +1,5 @@ -import { checkForScreenshot, screenShotPaths, test, visitStudy } from './utils'; +import { checkForScreenshot, screenShotPaths, test, visitStudy, expect } from './utils'; +import { press } from './utils/keyboardUtils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; @@ -25,3 +26,72 @@ test('should display the spline tool', async ({ await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); await checkForScreenshot(page, page, screenShotPaths.spline.splineDisplayedCorrectly); }); + +test('should restore viewport interactivity after deleting an in-progress Spline annotation via context menu', async ({ + DOMOverlayPageObject, + mainToolbarPageObject, + viewportPageObject, +}) => { + await mainToolbarPageObject.measurementTools.splineROI.click(); + + const activeViewport = await viewportPageObject.active; + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + ]); + + await activeViewport.clickAt([{ x: 550, y: 312 }], 'right'); + + const deleteButton = DOMOverlayPageObject.viewport.annotationContextMenu.delete; + await expect(deleteButton.locator).toBeVisible(); + await deleteButton.click(); + + await expect(activeViewport.nthAnnotation(0).locator).toBeHidden(); + + // Draw and complete a new Spline annotation to verify interactivity is restored + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + { x: 581, y: 287 }, + { x: 482, y: 333 }, + { x: 383, y: 301 }, + ]); + await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); + await expect(activeViewport.nthAnnotation(0).locator).toBeVisible(); +}); + +test('should restore viewport interactivity after deleting an in-progress Spline annotation via Backspace', async ({ + page, + DOMOverlayPageObject, + mainToolbarPageObject, + viewportPageObject, +}) => { + await mainToolbarPageObject.measurementTools.splineROI.click(); + + const activeViewport = await viewportPageObject.active; + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + ]); + + // Ensure the three points clicked above are rendered in the DOM before pressing Backspace + await expect(activeViewport.svg('circle')).toHaveCount(3); + await press({ page, key: 'Backspace' }); + + await expect(activeViewport.nthAnnotation(0).locator).toBeHidden(); + + // Draw and complete a new Spline annotation to verify interactivity is restored + await activeViewport.clickAt([ + { x: 380, y: 299 }, + { x: 420, y: 236 }, + { x: 523, y: 232 }, + { x: 581, y: 287 }, + { x: 482, y: 333 }, + { x: 383, y: 301 }, + ]); + await DOMOverlayPageObject.viewport.measurementTracking.confirm.click(); + await expect(activeViewport.nthAnnotation(0).locator).toBeVisible(); +});