From b069b68faa0dac061baaac1494042a0c9c5cea25 Mon Sep 17 00:00:00 2001 From: Forrest Date: Tue, 29 Aug 2023 20:34:07 -0400 Subject: [PATCH 1/2] feat(CurrentImageProvider): inject current image Decouples the current image from the current selection. --- src/components/CurrentImageProvider.vue | 19 ++++ src/components/VtkTwoView.vue | 2 +- src/composables/annotationTool.ts | 31 ++++-- src/composables/useCurrentImage.ts | 127 ++++++++++++++---------- src/store/datasets.ts | 1 + src/store/tools/useAnnotationTool.ts | 3 +- 6 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 src/components/CurrentImageProvider.vue diff --git a/src/components/CurrentImageProvider.vue b/src/components/CurrentImageProvider.vue new file mode 100644 index 000000000..a96840c10 --- /dev/null +++ b/src/components/CurrentImageProvider.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/VtkTwoView.vue b/src/components/VtkTwoView.vue index 67b227faa..3b8c921a6 100644 --- a/src/components/VtkTwoView.vue +++ b/src/components/VtkTwoView.vue @@ -863,7 +863,7 @@ export default defineComponent({ .filter( ({ tool }) => tool.slice === currentSlice.value && - doesToolFrameMatchViewAxis(viewAxis, tool) + doesToolFrameMatchViewAxis(viewAxis, tool, curImageMetadata) ) .flatMap(({ store, tool }) => store.getPoints(tool.id)); }); diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts index 6afa9487a..4a12a3c9b 100644 --- a/src/composables/annotationTool.ts +++ b/src/composables/annotationTool.ts @@ -1,4 +1,13 @@ -import { Ref, UnwrapRef, computed, readonly, ref, watch } from 'vue'; +import { + MaybeRef, + Ref, + UnwrapRef, + computed, + readonly, + ref, + unref, + watch, +} from 'vue'; import { Vector2 } from '@kitware/vtk.js/types'; import { useCurrentImage } from '@/src/composables/useCurrentImage'; import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference'; @@ -19,34 +28,35 @@ import { ContextMenuEvent, vtkAnnotationToolWidget, } from '@/src/vtk/ToolWidgetUtils/types'; +import { ImageMetadata } from '@/src/types/image'; const SHOW_OVERLAY_DELAY = 250; // milliseconds // does the tools's frame of reference match // the view's axis export const doesToolFrameMatchViewAxis = ( - viewAxis: Ref, - tool: Partial + viewAxis: MaybeRef, + tool: Partial, + imageMetadata: MaybeRef ) => { if (!tool.frameOfReference) return false; - const { currentImageMetadata } = useCurrentImage(); const toolAxis = frameOfReferenceToImageSliceAndAxis( tool.frameOfReference, - currentImageMetadata.value, + unref(imageMetadata), { allowOutOfBoundsSlice: true, } ); - return !!toolAxis && toolAxis.axis === viewAxis.value; + return !!toolAxis && toolAxis.axis === unref(viewAxis); }; export const useCurrentTools = ( toolStore: S, viewAxis: Ref -) => - computed(() => { - const { currentImageID } = useCurrentImage(); +) => { + const { currentImageID, currentImageMetadata } = useCurrentImage(); + return computed(() => { const curImageID = currentImageID.value; type ToolType = S['tools'][number]; @@ -55,11 +65,12 @@ export const useCurrentTools = ( // current view axis and not hidden return ( tool.imageID === curImageID && - doesToolFrameMatchViewAxis(viewAxis, tool) && + doesToolFrameMatchViewAxis(viewAxis, tool, currentImageMetadata) && !tool.hidden ); }); }); +}; // --- Context Menu --- // diff --git a/src/composables/useCurrentImage.ts b/src/composables/useCurrentImage.ts index eaf365470..71a82d3c0 100644 --- a/src/composables/useCurrentImage.ts +++ b/src/composables/useCurrentImage.ts @@ -1,12 +1,32 @@ -import { computed } from 'vue'; -import { useDatasetStore } from '../store/datasets'; -import { useDICOMStore } from '../store/datasets-dicom'; -import { defaultImageMetadata, useImageStore } from '../store/datasets-images'; -import { useLayersStore } from '../store/datasets-layers'; -import { createLPSBounds, getAxisBounds } from '../utils/lps'; +import { + InjectionKey, + MaybeRef, + Ref, + computed, + hasInjectionContext, + inject, + unref, +} from 'vue'; +import { Maybe } from '@/src/types'; +import { + defaultImageMetadata, + useImageStore, +} from '@/src/store/datasets-images'; +import { useLayersStore } from '@/src/store/datasets-layers'; +import { createLPSBounds, getAxisBounds } from '@/src/utils/lps'; +import { getImageID, useDatasetStore } from '@/src/store/datasets'; +import { storeToRefs } from 'pinia'; + +export interface CurrentImageContext { + imageID: Ref>; +} + +export const CurrentImageInjectionKey = Symbol( + 'CurrentImage' +) as InjectionKey; // Returns a spatially inflated image extent -export function getImageSpatialExtent(imageID: string | null) { +export function getImageSpatialExtent(imageID: Maybe) { const imageStore = useImageStore(); if (imageID && imageID in imageStore.metadata) { @@ -24,62 +44,59 @@ export function getImageSpatialExtent(imageID: string | null) { return createLPSBounds(); } -export function useCurrentImage() { - const dataStore = useDatasetStore(); - const dicomStore = useDICOMStore(); - const imageStore = useImageStore(); - const layersStore = useLayersStore(); - - const currentImageID = computed(() => { - const { primarySelection } = dataStore; - const { volumeToImageID } = dicomStore; +export function getImageMetadata(imageID: Maybe) { + const { metadata } = useImageStore(); + return imageID ? metadata[imageID] : defaultImageMetadata(); +} - if (primarySelection?.type === 'image') { - return primarySelection.dataID; - } - if (primarySelection?.type === 'dicom') { - return volumeToImageID[primarySelection.volumeKey] || null; - } - return null; - }); +export function getImageData(imageID: Maybe) { + const { dataIndex } = useImageStore(); + return imageID ? dataIndex[imageID] : null; +} - const currentImageMetadata = computed(() => { - const { metadata } = imageStore; - const imageID = currentImageID.value; +export function getIsImageLoading(imageID: Maybe) { + const dataStore = useDatasetStore(); + if (!dataStore.primarySelection) return false; - if (imageID) { - return metadata[imageID]; - } - return defaultImageMetadata(); - }); + const selectedImageID = getImageID(dataStore.primarySelection); + if (selectedImageID !== unref(imageID)) return false; - const currentImageData = computed(() => { - if (currentImageID.value) - // assumed to be only images for now - return imageStore.dataIndex[currentImageID.value]; - return undefined; - }); + return !!dataStore.primarySelection && !dataStore.primaryDataset; +} - const currentImageExtent = computed(() => - getImageSpatialExtent(currentImageID.value) - ); +export function getImageLayers(imageID: Maybe) { + const layersStore = useLayersStore(); + return layersStore + .getLayers(imageID ? { type: 'image', dataID: imageID } : null) + .filter(({ id }) => id in layersStore.layerImages); +} - const isImageLoading = computed(() => { - return !!dataStore.primarySelection && !dataStore.primaryDataset; - }); +export function useImage(imageID: MaybeRef>) { + return { + id: computed(() => unref(imageID)), + imageData: computed(() => getImageData(unref(imageID))), + metadata: computed(() => getImageMetadata(unref(imageID))), + extent: computed(() => getImageSpatialExtent(unref(imageID))), + isLoading: computed(() => getIsImageLoading(unref(imageID))), + layers: computed(() => getImageLayers(unref(imageID))), + }; +} - const currentLayers = computed(() => - layersStore - .getLayers(dataStore.primarySelection) - .filter(({ id }) => id in layersStore.layerImages) - ); +export function useCurrentImage() { + const { primaryImageID } = storeToRefs(useDatasetStore()); + const defaultContext = { imageID: primaryImageID }; + const { imageID } = hasInjectionContext() + ? inject(CurrentImageInjectionKey, defaultContext) + : defaultContext; + const { id, imageData, metadata, extent, isLoading, layers } = + useImage(imageID); return { - currentImageData, - currentImageID, - currentImageMetadata, - currentImageExtent, - isImageLoading, - currentLayers, + currentImageID: id, + currentImageMetadata: metadata, + currentImageData: imageData, + currentImageExtent: extent, + isImageLoading: isLoading, + currentLayers: layers, }; } diff --git a/src/store/datasets.ts b/src/store/datasets.ts index 103377fa2..4db700707 100644 --- a/src/store/datasets.ts +++ b/src/store/datasets.ts @@ -179,6 +179,7 @@ export const useDatasetStore = defineStore('dataset', () => { }); return { + primaryImageID, primarySelection, primaryDataset, allDataIDs, diff --git a/src/store/tools/useAnnotationTool.ts b/src/store/tools/useAnnotationTool.ts index 04f86e4df..1be76c6d1 100644 --- a/src/store/tools/useAnnotationTool.ts +++ b/src/store/tools/useAnnotationTool.ts @@ -136,9 +136,10 @@ export const useAnnotationTool = < }); }); + const { currentImageID, currentImageMetadata } = useCurrentImage(); + function jumpToTool(toolID: ToolID) { const tool = toolByID.value[toolID]; - const { currentImageID, currentImageMetadata } = useCurrentImage(); const imageID = currentImageID.value; if (!imageID || tool.imageID !== imageID) return; From c57abd74b611eb8710d3c9fceb1b9d99bb20aea4 Mon Sep 17 00:00:00 2001 From: Forrest Date: Thu, 31 Aug 2023 11:09:00 -0400 Subject: [PATCH 2/2] refactor: currentImageID is now Maybe --- src/components/VtkObliqueView.vue | 4 ++-- src/components/VtkTwoView.vue | 6 +++--- src/components/tools/SliceScrollTool.vue | 2 +- src/composables/usePersistCameraConfig.ts | 13 +++++++------ src/composables/useSceneBuilder.ts | 3 ++- src/store/tools/crop.ts | 3 ++- src/store/tools/paint.ts | 8 ++++---- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/VtkObliqueView.vue b/src/components/VtkObliqueView.vue index 4ae8f8e80..57fdd9f81 100644 --- a/src/components/VtkObliqueView.vue +++ b/src/components/VtkObliqueView.vue @@ -219,7 +219,7 @@ export default defineComponent({ get: () => windowingStore.getConfig(viewID.value, curImageID.value), set: (newValue) => { const imageID = curImageID.value; - if (imageID !== null && newValue != null) { + if (imageID != null && newValue != null) { windowingStore.updateConfig(viewID.value, imageID, newValue); } }, @@ -229,7 +229,7 @@ export default defineComponent({ const windowLevel = computed(() => wlConfig.value?.level); const dicomInfo = computed(() => { if ( - curImageID.value !== null && + curImageID.value != null && curImageID.value in dicomStore.imageIDToVolumeKey ) { const volumeKey = dicomStore.imageIDToVolumeKey[curImageID.value]; diff --git a/src/components/VtkTwoView.vue b/src/components/VtkTwoView.vue index 3b8c921a6..94ee61c43 100644 --- a/src/components/VtkTwoView.vue +++ b/src/components/VtkTwoView.vue @@ -333,7 +333,7 @@ export default defineComponent({ get: () => windowingStore.getConfig(viewID.value, curImageID.value), set: (newValue) => { const imageID = curImageID.value; - if (imageID !== null && newValue != null) { + if (imageID != null && newValue != null) { windowingStore.updateConfig(viewID.value, imageID, newValue); } }, @@ -346,7 +346,7 @@ export default defineComponent({ ); const dicomInfo = computed(() => { if ( - curImageID.value !== null && + curImageID.value != null && curImageID.value in dicomStore.imageIDToVolumeKey ) { const volumeKey = dicomStore.imageIDToVolumeKey[curImageID.value]; @@ -388,7 +388,7 @@ export default defineComponent({ // --- setters --- // const setSlice = (slice: number) => { - if (curImageID.value !== null) { + if (curImageID.value != null) { viewSliceStore.updateConfig(viewID.value, curImageID.value, { slice, }); diff --git a/src/components/tools/SliceScrollTool.vue b/src/components/tools/SliceScrollTool.vue index cd04de824..0feaf6c51 100644 --- a/src/components/tools/SliceScrollTool.vue +++ b/src/components/tools/SliceScrollTool.vue @@ -96,7 +96,7 @@ export default defineComponent({ range.step, () => scrollVal.value, (slice) => { - if (currentImageID.value !== null) { + if (currentImageID.value != null) { viewSliceStore.updateConfig(viewID.value, currentImageID.value, { slice, }); diff --git a/src/composables/usePersistCameraConfig.ts b/src/composables/usePersistCameraConfig.ts index 2fdfb5eea..5bddbe127 100644 --- a/src/composables/usePersistCameraConfig.ts +++ b/src/composables/usePersistCameraConfig.ts @@ -1,12 +1,13 @@ import { manageVTKSubscription } from '@/src/composables/manageVTKSubscription'; import { Ref } from 'vue'; +import { Maybe } from '@/src/types'; import { CameraConfig } from '../store/view-configs/types'; import { vtkLPSViewProxy } from '../types/vtk-types'; import useViewCameraStore from '../store/view-configs/camera'; export function usePersistCameraConfig( viewID: Ref, - dataID: Ref, + dataID: Ref>, viewProxy: Ref, ...toPersist: (keyof CameraConfig)[] ) { @@ -19,7 +20,7 @@ export function usePersistCameraConfig( if (toPersist.indexOf('position') > -1) { persist.push(() => { - if (dataID.value !== null && persistCameraConfig) { + if (dataID.value != null && persistCameraConfig) { viewCameraStore.updateConfig(viewID.value, dataID.value, { position: viewProxy.value.getCamera().getPosition(), }); @@ -28,7 +29,7 @@ export function usePersistCameraConfig( } if (toPersist.indexOf('viewUp') > -1) { persist.push(() => { - if (dataID.value !== null && persistCameraConfig) { + if (dataID.value != null && persistCameraConfig) { viewCameraStore.updateConfig(viewID.value, dataID.value, { viewUp: viewProxy.value.getCamera().getViewUp(), }); @@ -37,7 +38,7 @@ export function usePersistCameraConfig( } if (toPersist.indexOf('focalPoint') > -1) { persist.push(() => { - if (dataID.value !== null && persistCameraConfig) { + if (dataID.value != null && persistCameraConfig) { viewCameraStore.updateConfig(viewID.value, dataID.value, { focalPoint: viewProxy.value.getCamera().getFocalPoint(), }); @@ -46,7 +47,7 @@ export function usePersistCameraConfig( } if (toPersist.indexOf('directionOfProjection') > -1) { persist.push(() => { - if (dataID.value !== null && persistCameraConfig) { + if (dataID.value != null && persistCameraConfig) { viewCameraStore.updateConfig(viewID.value, dataID.value, { directionOfProjection: viewProxy.value .getCamera() @@ -57,7 +58,7 @@ export function usePersistCameraConfig( } if (toPersist.indexOf('parallelScale') > -1) { persist.push(() => { - if (dataID.value !== null && persistCameraConfig) { + if (dataID.value != null && persistCameraConfig) { viewCameraStore.updateConfig(viewID.value, dataID.value, { parallelScale: viewProxy.value.getCamera().getParallelScale(), }); diff --git a/src/composables/useSceneBuilder.ts b/src/composables/useSceneBuilder.ts index 71b1dd991..3b95c1906 100644 --- a/src/composables/useSceneBuilder.ts +++ b/src/composables/useSceneBuilder.ts @@ -1,11 +1,12 @@ import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy'; import { computed, Ref, watch } from 'vue'; +import { Maybe } from '@/src/types'; import { useViewStore } from '../store/views'; import { vtkLPSViewProxy } from '../types/vtk-types'; import { arrayEquals } from '../utils'; interface Scene { - baseImage?: Ref; + baseImage?: Ref>; labelmaps?: Ref; layers?: Ref; models?: Ref; diff --git a/src/store/tools/crop.ts b/src/store/tools/crop.ts index 9fe59a13c..73e100a18 100644 --- a/src/store/tools/crop.ts +++ b/src/store/tools/crop.ts @@ -9,6 +9,7 @@ import { MaybeRef } from '@vueuse/core'; import { vec3 } from 'gl-matrix'; import { defineStore } from 'pinia'; import { arrayEqualsWithComparator } from '@/src/utils'; +import { Maybe } from '@/src/types'; import { useImageStore } from '../datasets-images'; import { LPSCroppingPlanes } from '../../types/crop'; import { ImageMetadata } from '../../types/image'; @@ -89,7 +90,7 @@ export const useCropStore = defineStore('crop', () => { croppingByImageID: {} as Record, }); - const getComputedVTKPlanes = (imageID: MaybeRef) => + const getComputedVTKPlanes = (imageID: MaybeRef>) => computed(() => { const id = unref(imageID); if (id && id in state.croppingByImageID && id in imageStore.metadata) { diff --git a/src/store/tools/paint.ts b/src/store/tools/paint.ts index e7716e657..0a96dffcd 100644 --- a/src/store/tools/paint.ts +++ b/src/store/tools/paint.ts @@ -15,7 +15,7 @@ export const usePaintToolStore = defineStore('paint', () => { type _This = ReturnType; const activeMode = ref(PaintMode.CirclePaint); - const activeSegmentGroupID = ref(null); + const activeSegmentGroupID = ref>(null); const activeSegment = ref>(null); const brushSize = ref(DEFAULT_BRUSH_SIZE); const strokePoints = ref([]); @@ -48,7 +48,7 @@ export const usePaintToolStore = defineStore('paint', () => { /** * Sets the active labelmap. */ - function setActiveLabelmap(segmentGroupID: string | null) { + function setActiveLabelmap(segmentGroupID: Maybe) { activeSegmentGroupID.value = segmentGroupID; } @@ -57,7 +57,7 @@ export const usePaintToolStore = defineStore('paint', () => { * * If a labelmap exists, pick the first one. If no labelmap exists, create one. */ - function setActiveLabelmapFromImage(imageID: string | null) { + function setActiveLabelmapFromImage(imageID: Maybe) { if (!imageID) { setActiveLabelmap(null); return; @@ -178,7 +178,7 @@ export const usePaintToolStore = defineStore('paint', () => { function serialize(state: StateFile) { const { paint } = state.manifest.tools; - paint.activeSegmentGroupID = activeSegmentGroupID.value; + paint.activeSegmentGroupID = activeSegmentGroupID.value ?? null; paint.brushSize = brushSize.value; paint.activeSegment = activeSegment.value; paint.labelmapOpacity = labelmapOpacity.value;