From 0c1ca6b04a6c1aef478c7a7d9f360f9b9c50f188 Mon Sep 17 00:00:00 2001 From: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com> Date: Tue, 26 May 2026 12:22:46 -0400 Subject: [PATCH] fix(segmentation): restore navigation for duplicated contour segments (#6038) --------- Co-authored-by: diattamo --- extensions/cornerstone/src/commandsModule.ts | 11 ++ .../SegmentationService.ts | 1 + tests/ContourSegmentDuplicate.spec.ts | 155 +++++++++++++++--- 3 files changed, 140 insertions(+), 27 deletions(-) diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index c55c52c41..0e6ea7687 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -2403,14 +2403,25 @@ function commandsModule({ targetSegmentInfo?: SegmentInfo; }) => { if (!targetSegmentInfo) { + const sourceSegmentation = segmentationService.getSegmentation( + sourceSegmentInfo.segmentationId + ); + const sourceCachedStats = + sourceSegmentation?.segments?.[sourceSegmentInfo.segmentIndex]?.cachedStats; + targetSegmentInfo = { segmentationId: sourceSegmentInfo.segmentationId, segmentIndex: segmentationService.getNextAvailableSegmentIndex( sourceSegmentInfo.segmentationId ), }; + + // Copy source cachedStats so jump-to-segment navigation works on the duplicate segment. segmentationService.addSegment(targetSegmentInfo.segmentationId, { segmentIndex: targetSegmentInfo.segmentIndex, + ...(sourceCachedStats && { + cachedStats: csUtils.deepClone(sourceCachedStats) as Record, + }), }); } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 4bea58b8f..ca37bf4d2 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -896,6 +896,7 @@ class SegmentationService extends PubSubService { active?: boolean; color?: csTypes.Color; // Add color type visibility?: boolean; // Add visibility option + cachedStats?: Record; } = {} ): void { if (config?.segmentIndex === 0) { diff --git a/tests/ContourSegmentDuplicate.spec.ts b/tests/ContourSegmentDuplicate.spec.ts index 27bd80e84..31930f22d 100644 --- a/tests/ContourSegmentDuplicate.spec.ts +++ b/tests/ContourSegmentDuplicate.spec.ts @@ -1,20 +1,10 @@ -import { - expect, - test, - visitStudy, - waitForViewportsRendered, -} from './utils'; -import { getSvgAttribute } from './utils/getSvgAttribute'; +import { expect, test, visitStudy, waitForViewportsRendered, getSvgAttribute } from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const defaultSegment0Name = 'Threshold'; const defaultSegment1Name = 'Big Sphere'; -test.beforeEach(async ({ - page, - leftPanelPageObject, - DOMOverlayPageObject, -}) => { +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => { const mode = 'segmentation'; await visitStudy(page, studyInstanceUID, mode, 2000); @@ -26,7 +16,6 @@ test.beforeEach(async ({ }); test('should duplicate a contour segment and add a new row to the panel', async ({ - page, rightPanelPageObject, }) => { const panel = rightPanelPageObject.contourSegmentationPanel.panel; @@ -44,32 +33,43 @@ test('should duplicate a contour segment and add a new row to the panel', async //New segment's default name is formatted as "Segment {segmentCount}" const newSegmentLocator = panel.nthSegment(initialCount).title; - await expect(newSegmentLocator, 'Expected correct title for duplicated segment').toHaveText(`Segment 5`); + await expect(newSegmentLocator, 'Expected correct title for duplicated segment').toHaveText( + `Segment 5` + ); // Original segment titles should be unchanged await expect(panel.nthSegment(0).title).toHaveText(defaultSegment0Name); await expect(panel.nthSegment(1).title).toHaveText(defaultSegment1Name); }); -test('should duplicate the same segment multiple times', async ({ - page, - rightPanelPageObject, -}) => { +test('should duplicate the same segment multiple times', async ({ rightPanelPageObject }) => { const panel = rightPanelPageObject.contourSegmentationPanel.panel; const segment0 = panel.nthSegment(0); await segment0.actions.duplicate(); - expect(await panel.getSegmentCount(), 'Expected one additional segment row after duplicating').toBe(5); + expect( + await panel.getSegmentCount(), + 'Expected one additional segment row after duplicating' + ).toBe(5); const firstDuplicateTitleLocator = panel.nthSegment(4).title; - await expect(firstDuplicateTitleLocator, 'Expected correct title for first duplicated segment').toHaveText(`Segment 5`); + await expect( + firstDuplicateTitleLocator, + 'Expected correct title for first duplicated segment' + ).toHaveText(`Segment 5`); await segment0.actions.duplicate(); - expect(await panel.getSegmentCount(), 'Expected another segment row after duplicating the same segment again').toBe(6); + expect( + await panel.getSegmentCount(), + 'Expected another segment row after duplicating the same segment again' + ).toBe(6); const secondDuplicateTitleLocator = panel.nthSegment(5).title; - await expect(secondDuplicateTitleLocator, 'Expected correct title for second duplicated segment').toHaveText(`Segment 6`); + await expect( + secondDuplicateTitleLocator, + 'Expected correct title for second duplicated segment' + ).toHaveText(`Segment 6`); }); test('should render the duplicated contour on the viewport', async ({ @@ -84,10 +84,16 @@ test('should render the duplicated contour on the viewport', async ({ await segment0.toggleVisibility(); await segment0.click(); - const sourceSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'}); + const sourceSvgPath = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); expect(sourceSvgPath, 'Expected a visible SVG path for the source segment').not.toBeNull(); const sourceSvgPaths = (await viewportPageObject.getById('default')).svg('path'); - expect(sourceSvgPaths, 'Expected only one SVG path element for the original segment').toHaveCount(1); + expect(sourceSvgPaths, 'Expected only one SVG path element for the original segment').toHaveCount( + 1 + ); // New segment is at index 4 await segment0.actions.duplicate(); @@ -97,13 +103,108 @@ test('should render the duplicated contour on the viewport', async ({ await segment0.toggleVisibility(); await duplicatedSegment.click(); - const duplicatedSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'}); - expect(duplicatedSvgPath, 'Expected a visible SVG path for the duplicated segment').not.toBeNull(); + const duplicatedSvgPath = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + expect( + duplicatedSvgPath, + 'Expected a visible SVG path for the duplicated segment' + ).not.toBeNull(); const duplicatedSvgPaths = (await viewportPageObject.getById('default')).svg('path'); - expect(duplicatedSvgPaths, 'Expected only one SVG path element for the duplicated segment').toHaveCount(1); + expect( + duplicatedSvgPaths, + 'Expected only one SVG path element for the duplicated segment' + ).toHaveCount(1); expect( duplicatedSvgPath, 'Expected the duplicated segment to have the same SVG path as the source' ).toBe(sourceSvgPath); }); + +test('should navigate to the correct instance number when a duplicated contour segment is selected', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + + // get instance overlay of the contour segment at index 0 + const originalSegment = panel.nthSegment(0); + await originalSegment.click(); + const originalSegmentInstanceInfo = (await viewportPageObject.getById('default')).overlayText + .bottomRight.instanceNumber; + expect( + originalSegmentInstanceInfo, + 'Expected instance information to be displayed in the viewport overlay' + ).toBeVisible(); + await expect( + originalSegmentInstanceInfo, + 'Expected instance information to be slice 46 for the Threshold segment' + ).toHaveText('I:46 (46/47)'); + + // Duplicate segment so new segment is at index 4 + await originalSegment.actions.duplicate(); + + //click another segment to ensure instance number changes accordingly + await panel.nthSegment(2).click(); + const anotherSegmentInstanceInfo = (await viewportPageObject.getById('default')).overlayText + .bottomRight.instanceNumber; + expect( + anotherSegmentInstanceInfo, + 'Expected instance information to be displayed in the viewport overlay' + ).toBeVisible(); + await expect( + anotherSegmentInstanceInfo, + 'Expected instance information to be different from original contour' + ).not.toHaveText('I:46 (46/47)'); + + //click duplicated segment to ensure instance number is consistent with original segment + const duplicatedSegment = panel.nthSegment(4); + await duplicatedSegment.click(); + const duplicatedSegmentInstanceInfoAfter = (await viewportPageObject.getById('default')) + .overlayText.bottomRight.instanceNumber; + expect( + duplicatedSegmentInstanceInfoAfter, + 'Expected instance information to be displayed in the viewport overlay' + ).toBeVisible(); + await expect( + duplicatedSegmentInstanceInfoAfter, + 'Expected instance information to be same as original contour after clicking duplicated segment' + ).toHaveText('I:46 (46/47)'); + + //verify the svg paths are the same for the original and duplicated segments + await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click(); + await originalSegment.toggleVisibility(); + await originalSegment.click(); + const originalSegmentSvgPath = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + expect( + originalSegmentSvgPath, + 'Expected a visible SVG path for the original segment' + ).not.toBeNull(); + + // hide original segment to show duplicate only + await originalSegment.toggleVisibility(); + + await duplicatedSegment.toggleVisibility(); + await duplicatedSegment.click(); + const duplicatedSegmentSvgPath = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + expect( + duplicatedSegmentSvgPath, + 'Expected a visible SVG path for the duplicated segment' + ).not.toBeNull(); + + expect( + duplicatedSegmentSvgPath, + 'Expected the duplicated segment to have the same SVG path as the original segment' + ).toBe(originalSegmentSvgPath); +});