From e060ea8cb55f70d711933c9c5d31e56e6d579313 Mon Sep 17 00:00:00 2001 From: Talal Emara Date: Thu, 18 Jun 2026 15:44:49 +0300 Subject: [PATCH 1/3] feat: add changeMeasurementColor command to update annotation colors via dialog and extend MeasurementService to support color persistence --- extensions/cornerstone/src/commandsModule.ts | 50 ++++++++++++++++++- .../MeasurementService/MeasurementService.ts | 1 + 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 98e3d495b99..290a615dea9 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -638,7 +638,9 @@ function commandsModule({ */ _handleMeasurementLabelDialog: async uid => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const renderContent = customizationService.getCustomization('ui.labellingComponent'); + const renderContent = customizationService.getCustomization( + 'ui.labellingComponent' + ) as React.ComponentType; const measurement = measurementService.getMeasurement(uid); if (!measurement) { @@ -681,6 +683,49 @@ function commandsModule({ renameMeasurement: async ({ uid }) => { await actions._handleMeasurementLabelDialog(uid); }, + + changeMeasurementColor: ({uid}) => { + const measurement = measurementService.getMeasurement(uid); + + if (!measurement) { + console.debug('No measurement found for color editing'); + return; + } + + const color = measurement.color; + + const rgbaColor = { + r: color[0], + g: color[1], + b: color[2], + a: (color[3] !== undefined ? color[3] : 255) / 255.0, + }; + + uiDialogService.show({ + id: 'measurement-color-dialog', + content: colorPickerDialog, + title: i18n.t('Tools:Measurement Color'), + contentProps: { + value: rgbaColor, + onSave: newRgbaColor => { + const colorArray = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; + const colorStr = `rgb(${newRgbaColor.r}, ${newRgbaColor.g}, ${newRgbaColor.b})`; + + // Update OHIF measurement service + measurementService.updateColorMeasurement(uid, colorArray); + + // Update cornerstone tools rendering + annotation.config.style.setAnnotationStyles(uid, { + color: colorStr, + }); + + // Force re-render + const renderingEngine = cornerstoneViewportService.getRenderingEngine(); + renderingEngine.render(); + }, + }, + }); + }, /** * * @param props - containing the updates to apply @@ -2543,6 +2588,9 @@ function commandsModule({ renameMeasurement: { commandFn: actions.renameMeasurement, }, + changeMeasurementColor: { + commandFn: actions.changeMeasurementColor, + }, updateMeasurement: { commandFn: actions.updateMeasurement, }, diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index d91a61b990d..53ee4cec184 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -543,6 +543,7 @@ class MeasurementService extends PubSubService { const newMeasurement = { ...oldMeasurement, ...measurement, + color: measurement.color || oldMeasurement?.color || [255, 255, 0, 255], modifiedTimestamp: Math.floor(Date.now() / 1000), uid: internalUID, }; From 98c5f6ea294ea1483e589f337eb682cb8e4a18c8 Mon Sep 17 00:00:00 2001 From: Talal Emara Date: Thu, 18 Jun 2026 17:18:02 +0300 Subject: [PATCH 2/3] fix(measurement): persist annotation colors and resolve rendering bugs Problem: 1. Unguarded access on `measurement.color` caused a silent crash (TypeError) for older measurements that lacked a color field. 2. The alpha channel was silently dropped during CSS string construction, breaking transparency. 3. Custom colors were lost when viewports were rebuilt (e.g., navigating away and back) because `setAnnotationStyles` was not re-called during annotation reload, causing them to render with the default color despite the custom color remaining in state. Solution: - Added a fallback default array (`?? [255, 255, 0, 255]`) to prevent crashes on measurements without existing color data. - Updated the CSS color string generator to use `rgba()` to properly apply the alpha channel to the canvas. - Synced Cornerstone's style manager with OHIF's `MeasurementService` during all lifecycle events: - `MEASUREMENT_ADDED`: Re-applies saved colors via `setAnnotationStyles` when viewports rebuild. - `RAW_MEASUREMENT_ADDED`: Restores colors when annotations are initially hydrated. - `MEASUREMENT_UPDATED`: Restored the style update block to gracefully sync RGBA colors back to the Cornerstone renderer on changes. --- extensions/cornerstone/src/commandsModule.ts | 9 ++-- .../cornerstone/src/initMeasurementService.ts | 41 ++++++++++++++++--- .../MeasurementService/MeasurementService.ts | 6 +++ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 290a615dea9..6507b526139 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -638,9 +638,7 @@ function commandsModule({ */ _handleMeasurementLabelDialog: async uid => { const labelConfig = customizationService.getCustomization('measurementLabels'); - const renderContent = customizationService.getCustomization( - 'ui.labellingComponent' - ) as React.ComponentType; + const renderContent = customizationService.getCustomization('ui.labellingComponent'); const measurement = measurementService.getMeasurement(uid); if (!measurement) { @@ -691,9 +689,8 @@ function commandsModule({ console.debug('No measurement found for color editing'); return; } - - const color = measurement.color; + const color = measurement.color ?? [255, 255, 0, 255]; const rgbaColor = { r: color[0], g: color[1], @@ -709,7 +706,7 @@ function commandsModule({ value: rgbaColor, onSave: newRgbaColor => { const colorArray = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; - const colorStr = `rgb(${newRgbaColor.r}, ${newRgbaColor.g}, ${newRgbaColor.b})`; + const colorStr = `rgba(${newRgbaColor.r}, ${newRgbaColor.g}, ${newRgbaColor.b}, ${newRgbaColor.a})`; // Update OHIF measurement service measurementService.updateColorMeasurement(uid, colorArray); diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index 7837aa8d4e2..51a96716239 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -353,7 +353,7 @@ const connectMeasurementServiceToTools = ({ }) => { const { measurementService, cornerstoneViewportService, viewportGridService } = servicesManager.services; - const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } = + const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED, MEASUREMENT_ADDED } = measurementService.EVENTS; measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements, trackingContext }) => { @@ -421,7 +421,7 @@ const connectMeasurementServiceToTools = ({ return; } - const { uid, label, isLocked, isVisible } = measurement; + const { uid, label, isLocked, isVisible, color } = measurement; const sourceAnnotation = annotation.state.getAnnotation(uid); const { data, metadata } = sourceAnnotation; @@ -440,9 +440,13 @@ const connectMeasurementServiceToTools = ({ // update the isVisible state annotation.visibility.setAnnotationVisibility(uid, isVisible); - // annotation.config.style.setAnnotationStyles(uid, { - // color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`, - // }); + if (color) { + annotation.config.style.setAnnotationStyles(uid, { + color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${ + (color[3] !== undefined ? color[3] : 255) / 255.0 + })`, + }); + } // I don't like this but will fix later const renderingEngine = @@ -454,6 +458,24 @@ const connectMeasurementServiceToTools = ({ } ); + measurementService.subscribe( + MEASUREMENT_ADDED, + ({ source, measurement }) => { + if (!source || source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { + return; + } + + const { uid, color } = measurement; + if (color) { + annotation.config.style.setAnnotationStyles(uid, { + color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${ + (color[3] !== undefined ? color[3] : 255) / 255.0 + })`, + }); + } + } + ); + measurementService.subscribe( RAW_MEASUREMENT_ADDED, ({ source, measurement, data, dataSource }) => { @@ -515,6 +537,15 @@ const connectMeasurementServiceToTools = ({ }, }; annotationManager.addAnnotation(newAnnotation); + + if (measurement.color) { + annotation.config.style.setAnnotationStyles(measurement.uid, { + color: `rgba(${measurement.color[0]}, ${measurement.color[1]}, ${measurement.color[2]}, ${ + (measurement.color[3] !== undefined ? measurement.color[3] : 255) / 255.0 + })`, + }); + } + commandsManager.run('triggerCreateAnnotationMemo', { annotation: newAnnotation, FrameOfReferenceUID: newAnnotation.metadata.FrameOfReferenceUID, diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 53ee4cec184..720f9603fb8 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -901,6 +901,12 @@ class MeasurementService extends PubSubService { return measurementUIDs.forEach(uid => this.toggleVisibilityMeasurement(uid, visibility)); } + /** + * Updates the color of a specific measurement and broadcasts the update event. + * + * @param {string} measurementUID The unique identifier of the measurement + * @param {number[]} color The new color as an RGBA array [r, g, b, a] + */ public updateColorMeasurement(measurementUID: string, color: number[]): void { const measurement = this.measurements.get(measurementUID); From 9ea3f755afa92b3c4d095a9e96a3caacf070cd8f Mon Sep 17 00:00:00 2001 From: Talal Emara Date: Thu, 18 Jun 2026 17:32:58 +0300 Subject: [PATCH 3/3] test: update MeasurementService mock objects to include color property --- .../src/services/MeasurementService/MeasurementService.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform/core/src/services/MeasurementService/MeasurementService.test.js b/platform/core/src/services/MeasurementService/MeasurementService.test.js index 51ec9b66d23..fd73b543971 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.test.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.test.js @@ -42,6 +42,8 @@ describe('MeasurementService.js', () => { { x: 1, y: 2 }, ], source: source, + color: [255, 255, 0, 255], + isDirty: undefined, }; // A measurement with various metadata missing (e.g. referenced SOPInstanceUID) that // would not typically get mapped my the MeasurementService possibly because it was