From f8ef80cce7f181774aed9f0c04ed6f2cb9869c1f Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Mon, 13 Jul 2026 12:47:54 -0300 Subject: [PATCH 1/2] fix(cornerstone): share one color LUT across a segmentation's viewport representations A labelmap segmentation created by createLabelmapForDisplaySet had no dedicated color LUT, so every viewport that rendered it created its own copy of the default LUT. Editing a segment color only mutated the active viewport's LUT, so the change was lost as soon as a drawing tool was used in another viewport and the segment reverted to the default color. Create a dedicated color LUT when the segmentation is created and record its index, mirroring the SEG and RT paths, so all representations reuse the same LUT and color edits are retained everywhere. --- .../SegmentationService.test.ts | 21 +++++++++++++++++++ .../SegmentationService.ts | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts index 754c4aaf0a7..e5ca0e0b6da 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts @@ -1331,6 +1331,27 @@ describe('SegmentationService', () => { expect(retrievedSegmentationId).toEqual(segmentationId); }); + + it('should create a dedicated color LUT and remember its index so every viewport representation reuses it', async () => { + const displaySet = { + imageIds: ['imageId'], + isDynamicVolume: false, + SeriesNumber: 1, + SeriesDescription: 'Series Description', + } as unknown as AppTypes.DisplaySet; + + jest + .spyOn(imageLoader, 'createAndCacheDerivedLabelmapImages') + .mockReturnValue([{ imageId: 'imageId' }] as csTypes.IImage[]); + jest.spyOn(cstSegmentation.state, 'getSegmentations').mockReturnValue([]); + jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); + jest.mocked(cstSegmentation.state.addColorLUT).mockReturnValue(7); + + const segmentationId = await service.createLabelmapForDisplaySet(displaySet); + + expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledWith([[0, 0, 0, 0]]); + expect(service['_segmentationIdToColorLUTIndexMap'].get(segmentationId)).toBe(7); + }); }); describe('createSegmentationForSEGDisplaySet', () => { diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index a1dd3226449..a3ee55199e6 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -512,6 +512,14 @@ class SegmentationService extends PubSubService implements ISegmentationServiceI }, }; + // Create a dedicated color LUT up front and remember its index so that every + // representation of this segmentation (one per viewport) reuses the same LUT. + // Otherwise each viewport would get its own default LUT copy and editing a + // segment color on one viewport would not be reflected on the others (the + // segment color appears to revert to the default when interacting elsewhere). + const colorLUTIndex = addColorLUT([[0, 0, 0, 0]] as csTypes.ColorLUT); + this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); + this.addOrUpdateSegmentation(segmentationPublicInput); return segmentationId; } From 209edf4018f19711ae65f5a2e8222c1d4f22ae42 Mon Sep 17 00:00:00 2001 From: Thomas Forster Date: Mon, 13 Jul 2026 13:18:22 -0300 Subject: [PATCH 2/2] fix(cornerstone): keep the existing color LUT when a segmentation id is reused The caller may pass the id of an existing segmentation, which this method updates rather than replaces. Allocating a new LUT and overwriting the mapping in that case would strand the representations already rendering the segmentation on the previous LUT while later ones used the new index, reintroducing the colour divergence this is meant to fix. Only allocate a LUT when the segmentation has none. --- .../SegmentationService.test.ts | 24 +++++++++++++++++++ .../SegmentationService.ts | 9 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts index e5ca0e0b6da..d5530dc9654 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts @@ -1352,6 +1352,30 @@ describe('SegmentationService', () => { expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledWith([[0, 0, 0, 0]]); expect(service['_segmentationIdToColorLUTIndexMap'].get(segmentationId)).toBe(7); }); + + it('should keep the existing color LUT when called again with the same segmentation id', async () => { + const displaySet = { + imageIds: ['imageId'], + isDynamicVolume: false, + SeriesNumber: 1, + SeriesDescription: 'Series Description', + } as unknown as AppTypes.DisplaySet; + + jest + .spyOn(imageLoader, 'createAndCacheDerivedLabelmapImages') + .mockReturnValue([{ imageId: 'imageId' }] as csTypes.IImage[]); + jest.spyOn(cstSegmentation.state, 'getSegmentations').mockReturnValue([]); + jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); + jest.mocked(cstSegmentation.state.addColorLUT).mockReturnValueOnce(7).mockReturnValueOnce(8); + + const segmentationId = await service.createLabelmapForDisplaySet(displaySet); + await service.createLabelmapForDisplaySet(displaySet, { segmentationId }); + + // Updating an existing segmentation must not strand the representations + // already rendering it on the previous LUT. + expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledTimes(1); + expect(service['_segmentationIdToColorLUTIndexMap'].get(segmentationId)).toBe(7); + }); }); describe('createSegmentationForSEGDisplaySet', () => { diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index a3ee55199e6..e0a54fe37e7 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -517,8 +517,13 @@ class SegmentationService extends PubSubService implements ISegmentationServiceI // Otherwise each viewport would get its own default LUT copy and editing a // segment color on one viewport would not be reflected on the others (the // segment color appears to revert to the default when interacting elsewhere). - const colorLUTIndex = addColorLUT([[0, 0, 0, 0]] as csTypes.ColorLUT); - this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); + // The caller may pass the id of an existing segmentation, which this method + // updates rather than replaces; keep its LUT so representations already + // rendering it don't diverge from the ones created afterwards. + if (!this._segmentationIdToColorLUTIndexMap.has(segmentationId)) { + const colorLUTIndex = addColorLUT([[0, 0, 0, 0]] as csTypes.ColorLUT); + this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); + } this.addOrUpdateSegmentation(segmentationPublicInput); return segmentationId;