From 7f6f3a861890c5c4fc807c503c06e028cdf36712 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Sun, 17 May 2026 17:22:24 -0400 Subject: [PATCH 01/12] feat(cine): add multi-frame DICOM playback foundation --- package-lock.json | 8 + package.json | 1 + src/components/PatientStudyVolumeBrowser.vue | 10 +- src/components/PlayControls.vue | 141 ++++++++ src/components/SliceViewer.vue | 24 +- src/components/SliceViewerOverlay.vue | 19 +- .../tools/polygon/PolygonWidget2D.vue | 4 +- .../tools/rectangle/RectangleWidget2D.vue | 4 +- src/components/tools/ruler/RulerWidget2D.vue | 4 +- .../vtk/VtkBaseSliceRepresentation.vue | 151 ++++++++- .../vtk/VtkSliceViewSlicingKeyManipulator.vue | 12 +- .../vtk/VtkSliceViewSlicingManipulator.vue | 12 +- src/core/cine/DicomCineImage.ts | 289 +++++++++++++++++ .../cine/__tests__/parseCineDicom.spec.ts | 253 +++++++++++++++ src/core/cine/frameCache.ts | 184 +++++++++++ src/core/cine/getRenderSlice.ts | 17 + src/core/cine/isCineImage.ts | 14 + src/core/cine/parseCineDicom.ts | 307 ++++++++++++++++++ src/core/dicomTags.ts | 27 +- src/core/progressiveImage.ts | 8 + src/core/streaming/chunkImage.ts | 5 - src/core/streaming/dicomChunkImage.ts | 8 +- .../vtk/useMouseRangeManipulatorListener.ts | 8 +- src/store/datasets-dicom.ts | 97 +++++- src/store/image-cache.ts | 5 + src/store/image-stats.ts | 5 + src/store/segmentGroups.ts | 7 +- src/store/tools/useAnnotationTool.ts | 5 +- src/store/view-configs/slicing.ts | 14 + tests/specs/cine-rendering.e2e.ts | 38 +++ tests/specs/configTestUtils.ts | 7 + tests/specs/reveal-slice.e2e.ts | 169 ++++++++++ wdio.shared.conf.ts | 4 + 33 files changed, 1806 insertions(+), 55 deletions(-) create mode 100644 src/components/PlayControls.vue create mode 100644 src/core/cine/DicomCineImage.ts create mode 100644 src/core/cine/__tests__/parseCineDicom.spec.ts create mode 100644 src/core/cine/frameCache.ts create mode 100644 src/core/cine/getRenderSlice.ts create mode 100644 src/core/cine/isCineImage.ts create mode 100644 src/core/cine/parseCineDicom.ts create mode 100644 tests/specs/cine-rendering.e2e.ts create mode 100644 tests/specs/reveal-slice.e2e.ts diff --git a/package-lock.json b/package-lock.json index 9573fada4..448150066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "cors": "^2.8.5", "cross-env": "^10.1.0", "deep-equal": "^2.2.3", + "dicom-parser": "^1.8.21", "dicomweb-client-typed": "^0.8.6", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", @@ -11938,6 +11939,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dicom-parser": { + "version": "1.8.21", + "resolved": "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.21.tgz", + "integrity": "sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w==", + "dev": true, + "license": "MIT" + }, "node_modules/dicomweb-client-typed": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/dicomweb-client-typed/-/dicomweb-client-typed-0.8.6.tgz", diff --git a/package.json b/package.json index 6822ffe4e..0715c9995 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "cors": "^2.8.5", "cross-env": "^10.1.0", "deep-equal": "^2.2.3", + "dicom-parser": "^1.8.21", "dicomweb-client-typed": "^0.8.6", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", diff --git a/src/components/PatientStudyVolumeBrowser.vue b/src/components/PatientStudyVolumeBrowser.vue index 2a640a3cb..519457eae 100644 --- a/src/components/PatientStudyVolumeBrowser.vue +++ b/src/components/PatientStudyVolumeBrowser.vue @@ -3,9 +3,7 @@ import { computed, defineComponent, reactive, toRefs, watch } from 'vue'; import type { PropType } from 'vue'; import GroupableItem from '@/src/components/GroupableItem.vue'; import { DataSelection, isDicomImage } from '@/src/utils/dataSelection'; -import { ThumbnailStrategy } from '@/src/core/streaming/chunkImage'; import { useImageCacheStore } from '@/src/store/image-cache'; -import DicomChunkImage from '@/src/core/streaming/dicomChunkImage'; import { getDisplayName, useDICOMStore } from '@/src/store/datasets-dicom'; import { useDatasetStore } from '@/src/store/datasets'; import { useMultiSelection } from '@/src/composables/useMultiSelection'; @@ -100,13 +98,11 @@ export default defineComponent({ } const image = imageCacheStore.imageById[key]; - if (!image || !(image instanceof DicomChunkImage)) return; + if (!image) return; try { - const thumb = await image.getThumbnail( - ThumbnailStrategy.MiddleSlice - ); - if (thumb !== null) { + const thumb = await image.getThumbnail(); + if (thumb) { thumbnailCache[cacheKey] = { kind: 'image', value: thumb }; } else { thumbnailCache[cacheKey] = { diff --git a/src/components/PlayControls.vue b/src/components/PlayControls.vue new file mode 100644 index 000000000..0ccfeb53e --- /dev/null +++ b/src/components/PlayControls.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/components/SliceViewer.vue b/src/components/SliceViewer.vue index d2a07dbc2..5d704177e 100644 --- a/src/components/SliceViewer.vue +++ b/src/components/SliceViewer.vue @@ -146,6 +146,7 @@