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

+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;