fix(HistoryMemo): Segmentation delete wasn't remembered (#5775)

Will address a few more comments in a separate PR released after this one into 3.13

* fix(HistoryMemo): Segmentation delete wasn't remembered

* Temporary add of package.json linking

* fix: Use newer dcmjs version and delete segmentation with history cs3d
This commit is contained in:
Bill Wallace authored and GitHub committed 2026-02-05 11:49:49 -05:00
1 parent 3b9d3babf9
commit 48f2f6fb13
22 files changed
+363 -275

No files matched your search

@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.15.28",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/adapters": "4.15.29",
"@cornerstonejs/core": "4.15.29",
"@kitware/vtk.js": "34.15.1",
"react-color": "2.19.3"
}
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.15.28",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/adapters": "4.15.29",
"@cornerstonejs/core": "4.15.29",
"@kitware/vtk.js": "34.15.1",
"react-color": "2.19.3"
}
+4 -4
View File
@@ -38,7 +38,7 @@
"@ohif/extension-cornerstone": "3.12.0-beta.133",
"@ohif/extension-measurement-tracking": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.15.28",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/tools": "4.15.28",
"@cornerstonejs/adapters": "4.15.29",
"@cornerstonejs/core": "4.15.29",
"@cornerstonejs/tools": "4.15.29",
"classnames": "2.5.1"
}
}
@@ -34,7 +34,7 @@
"@ohif/extension-default": "3.12.0-beta.133",
"@ohif/i18n": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/tools": "4.15.28",
"@cornerstonejs/core": "4.15.29",
"@cornerstonejs/tools": "4.15.29",
"classnames": "2.5.1"
}
}
+8 -8
View File
@@ -40,10 +40,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.15.28",
"@cornerstonejs/dicom-image-loader": "4.15.29",
"@ohif/core": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
@@ -53,12 +53,12 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.15.28",
"@cornerstonejs/ai": "4.15.28",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/labelmap-interpolation": "4.15.28",
"@cornerstonejs/polymorphic-segmentation": "4.15.28",
"@cornerstonejs/tools": "4.15.28",
"@cornerstonejs/adapters": "4.15.29",
"@cornerstonejs/ai": "4.15.29",
"@cornerstonejs/core": "4.15.29",
"@cornerstonejs/labelmap-interpolation": "4.15.29",
"@cornerstonejs/polymorphic-segmentation": "4.15.29",
"@cornerstonejs/tools": "4.15.29",
"@icr/polyseg-wasm": "0.4.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "34.15.1",
@@ -2117,7 +2117,9 @@ describe('SegmentationService', () => {
service.removeSegment(segmentationId, segmentIndex);
expect(cstSegmentation.removeSegment).toHaveBeenCalledTimes(1);
expect(cstSegmentation.removeSegment).toHaveBeenCalledWith(segmentationId, segmentIndex);
expect(cstSegmentation.removeSegment).toHaveBeenCalledWith(segmentationId, segmentIndex, {
recordHistory: true,
});
});
});
@@ -27,6 +27,8 @@ import { SegmentationPresentation, SegmentationPresentationItem } from '../../ty
import { EasingFunctionEnum, EasingFunctionMap } from '../../utils/transitions';
import { ViewReference } from '@cornerstonejs/core/types';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const {
Labelmap: LABELMAP,
Contour: CONTOUR,
@@ -951,11 +953,75 @@ class SegmentationService extends PubSubService {
});
}
/**
* Creates a memo that records the current state of a segment (segmentationId/segmentIndex)
* so that undo can restore it via addSegment and redo can call removeSegment again
* without recording history.
*
* @param segmentationId - The ID of the segmentation.
* @param segmentIndex - The index of the segment (must still exist when called).
* @param _options - Reserved (e.g. deleting) for future use.
* @returns A Memo with restoreMemo(undo): undo => addSegment, redo => removeSegment (skipRecordingHistory).
*/
public createSegmentIndexMemo(
segmentationId: string,
segmentIndex: number,
_options?: { deleting?: boolean }
): csTypes.Memo | null {
const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
const segment = csSegmentation?.segments?.[segmentIndex];
if (!segment) {
return null;
}
let color: csTypes.Color | undefined;
let visibility: boolean | undefined;
const viewportIds = this.getViewportIdsWithSegmentation(segmentationId);
if (viewportIds.length > 0) {
const firstViewportId = viewportIds[0];
const representations = this.getSegmentationRepresentations(firstViewportId, {
segmentationId,
});
const repType = representations[0]?.type ?? LABELMAP;
color = this.getSegmentColor(firstViewportId, segmentationId, segmentIndex);
visibility = cstSegmentation.config.visibility.getSegmentIndexVisibility(
firstViewportId,
{ segmentationId, type: repType },
segmentIndex
);
}
const segmentState = {
segmentIndex,
label: segment.label,
isLocked: segment.locked,
active: segment.active,
color,
visibility,
};
const service = this;
const memo: csTypes.Memo = {
id: csUtils.uuidv4(),
operationType: 'segmentIndex',
restoreMemo(undo?: boolean) {
if (undo === true) {
service.addSegment(segmentationId, segmentState);
} else {
// Redo: remove the segment via cornerstone without recording history
cstSegmentation.removeSegment(segmentationId, segmentIndex, { recordHistory: false });
}
},
};
return memo;
}
/**
* Removes a segment from a segmentation and updates the active segment index if necessary.
*
* @param segmentationId - The ID of the segmentation containing the segment to remove.
* @param segmentIndex - The index of the segment to remove.
* @param options - Optional. skipRecordingHistory: if true, do not push undo memo (used when redoing).
*
* @remarks
* This method performs the following actions:
@@ -964,8 +1030,21 @@ class SegmentationService extends PubSubService {
* 3. If the removed segment was the active segment, it updates the active segment index.
*
*/
public removeSegment(segmentationId: string, segmentIndex: number): void {
cstSegmentation.removeSegment(segmentationId, segmentIndex);
public removeSegment(
segmentationId: string,
segmentIndex: number,
options?: { skipRecordingHistory?: boolean }
): void {
let memo;
if (!options?.skipRecordingHistory) {
memo = this.createSegmentIndexMemo(segmentationId, segmentIndex, { deleting: true });
DefaultHistoryMemo.startGroupRecording();
cstSegmentation.removeSegment(segmentationId, segmentIndex, { recordHistory: true });
DefaultHistoryMemo.push(memo);
DefaultHistoryMemo.endGroupRecording();
} else {
cstSegmentation.removeSegment(segmentationId, segmentIndex, { recordHistory: false });
}
}
public setSegmentVisibility(
@@ -59,6 +59,12 @@ export async function updateSegmentationStats({
Object.entries(stats).forEach(([segmentIndex, segmentStats]) => {
const index = parseInt(segmentIndex);
if (!updatedSegmentation.segments[index]) {
// This happens when a segment is being restored
console.warn('Segment not found to update cached stats:', index);
return;
}
if (!updatedSegmentation.segments[index].cachedStats) {
updatedSegmentation.segments[index].cachedStats = {};
hasUpdates = true;
+1 -1
View File
@@ -36,7 +36,7 @@
"peerDependencies": {
"@ohif/core": "3.12.0-beta.133",
"@ohif/i18n": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicomweb-client": "0.10.4",
"prop-types": "15.8.1",
"react": "18.3.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
+3 -3
View File
@@ -32,14 +32,14 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/tools": "4.15.28",
"@cornerstonejs/core": "4.15.29",
"@cornerstonejs/tools": "4.15.29",
"@ohif/core": "3.12.0-beta.133",
"@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.133",
"@ohif/extension-default": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"classnames": "2.5.1",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"lodash.debounce": "4.0.8",
"prop-types": "15.8.1",
"react": "18.3.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.12.0-beta.133",
"@ohif/ui": "3.12.0-beta.133",
"dcmjs": "0.48.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
+2 -2
View File
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.15.28",
"@cornerstonejs/tools": "4.15.28",
"@cornerstonejs/core": "4.15.29",
"@cornerstonejs/tools": "4.15.29",
"@ohif/core": "3.12.0-beta.133",
"@ohif/extension-cornerstone": "3.12.0-beta.133",
"@ohif/extension-default": "3.12.0-beta.133",