fix(segmentation): restore navigation for duplicated contour segments (#6038)

---------

Co-authored-by: diattamo <mmddiatta@gmail.com>
This commit is contained in:
Ghadeer Albattarni 2026-05-26 12:22:46 -04:00 committed by GitHub
parent 9a2d2c3d13
commit 0c1ca6b04a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 140 additions and 27 deletions

View File

@ -2403,14 +2403,25 @@ function commandsModule({
targetSegmentInfo?: SegmentInfo; targetSegmentInfo?: SegmentInfo;
}) => { }) => {
if (!targetSegmentInfo) { if (!targetSegmentInfo) {
const sourceSegmentation = segmentationService.getSegmentation(
sourceSegmentInfo.segmentationId
);
const sourceCachedStats =
sourceSegmentation?.segments?.[sourceSegmentInfo.segmentIndex]?.cachedStats;
targetSegmentInfo = { targetSegmentInfo = {
segmentationId: sourceSegmentInfo.segmentationId, segmentationId: sourceSegmentInfo.segmentationId,
segmentIndex: segmentationService.getNextAvailableSegmentIndex( segmentIndex: segmentationService.getNextAvailableSegmentIndex(
sourceSegmentInfo.segmentationId sourceSegmentInfo.segmentationId
), ),
}; };
// Copy source cachedStats so jump-to-segment navigation works on the duplicate segment.
segmentationService.addSegment(targetSegmentInfo.segmentationId, { segmentationService.addSegment(targetSegmentInfo.segmentationId, {
segmentIndex: targetSegmentInfo.segmentIndex, segmentIndex: targetSegmentInfo.segmentIndex,
...(sourceCachedStats && {
cachedStats: csUtils.deepClone(sourceCachedStats) as Record<string, unknown>,
}),
}); });
} }

View File

@ -896,6 +896,7 @@ class SegmentationService extends PubSubService {
active?: boolean; active?: boolean;
color?: csTypes.Color; // Add color type color?: csTypes.Color; // Add color type
visibility?: boolean; // Add visibility option visibility?: boolean; // Add visibility option
cachedStats?: Record<string, unknown>;
} = {} } = {}
): void { ): void {
if (config?.segmentIndex === 0) { if (config?.segmentIndex === 0) {

View File

@ -1,20 +1,10 @@
import { import { expect, test, visitStudy, waitForViewportsRendered, getSvgAttribute } from './utils';
expect,
test,
visitStudy,
waitForViewportsRendered,
} from './utils';
import { getSvgAttribute } from './utils/getSvgAttribute';
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
const defaultSegment0Name = 'Threshold'; const defaultSegment0Name = 'Threshold';
const defaultSegment1Name = 'Big Sphere'; const defaultSegment1Name = 'Big Sphere';
test.beforeEach(async ({ test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => {
page,
leftPanelPageObject,
DOMOverlayPageObject,
}) => {
const mode = 'segmentation'; const mode = 'segmentation';
await visitStudy(page, studyInstanceUID, mode, 2000); 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 ({ test('should duplicate a contour segment and add a new row to the panel', async ({
page,
rightPanelPageObject, rightPanelPageObject,
}) => { }) => {
const panel = rightPanelPageObject.contourSegmentationPanel.panel; 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}" //New segment's default name is formatted as "Segment {segmentCount}"
const newSegmentLocator = panel.nthSegment(initialCount).title; 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 // Original segment titles should be unchanged
await expect(panel.nthSegment(0).title).toHaveText(defaultSegment0Name); await expect(panel.nthSegment(0).title).toHaveText(defaultSegment0Name);
await expect(panel.nthSegment(1).title).toHaveText(defaultSegment1Name); await expect(panel.nthSegment(1).title).toHaveText(defaultSegment1Name);
}); });
test('should duplicate the same segment multiple times', async ({ test('should duplicate the same segment multiple times', async ({ rightPanelPageObject }) => {
page,
rightPanelPageObject,
}) => {
const panel = rightPanelPageObject.contourSegmentationPanel.panel; const panel = rightPanelPageObject.contourSegmentationPanel.panel;
const segment0 = panel.nthSegment(0); const segment0 = panel.nthSegment(0);
await segment0.actions.duplicate(); 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; 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(); 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; 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 ({ 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.toggleVisibility();
await segment0.click(); 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(); expect(sourceSvgPath, 'Expected a visible SVG path for the source segment').not.toBeNull();
const sourceSvgPaths = (await viewportPageObject.getById('default')).svg('path'); 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 // New segment is at index 4
await segment0.actions.duplicate(); await segment0.actions.duplicate();
@ -97,13 +103,108 @@ test('should render the duplicated contour on the viewport', async ({
await segment0.toggleVisibility(); await segment0.toggleVisibility();
await duplicatedSegment.click(); await duplicatedSegment.click();
const duplicatedSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'}); const duplicatedSvgPath = await getSvgAttribute({
expect(duplicatedSvgPath, 'Expected a visible SVG path for the duplicated segment').not.toBeNull(); 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'); 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( expect(
duplicatedSvgPath, duplicatedSvgPath,
'Expected the duplicated segment to have the same SVG path as the source' 'Expected the duplicated segment to have the same SVG path as the source'
).toBe(sourceSvgPath); ).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);
});