diff --git a/src/components/ImageDataBrowser.vue b/src/components/ImageDataBrowser.vue index 14d5ce14..03f6b5a3 100644 --- a/src/components/ImageDataBrowser.vue +++ b/src/components/ImageDataBrowser.vue @@ -15,6 +15,9 @@ import { useDatasetStore } from '../store/datasets'; import { useMultiSelection } from '../composables/useMultiSelection'; import { useLayersStore } from '../store/datasets-layers'; +import { useViewSliceStore } from '../store/view-configs/slicing' +import { useViewCameraStore } from '../store/view-configs/camera' +import { useCameraAutoResetState } from '@/src/composables/useCameraAutoResetState' function imageCacheKey(dataID: string) { return `image-${dataID}`; @@ -31,6 +34,9 @@ export default defineComponent({ const dataStore = useDatasetStore(); const layersStore = useLayersStore(); const segmentGroupStore = useSegmentGroupStore(); + const viewSliceStore = useViewSliceStore(); + const viewCameraStore = useViewCameraStore(); + const cameraAutoResetState = useCameraAutoResetState(); const primarySelection = computed(() => dataStore.primarySelection); @@ -116,6 +122,31 @@ export default defineComponent({ { immediate: true, deep: true } ); + // --- sync --- // + + const sameSpaceImages = computed(() => { + return imageStore.checkAllImagesSameSpace(); + }); + + const isSync = computed(() => { + return viewSliceStore.isSync() && viewCameraStore.isSync(); + }); + + function toggleSyncImages() { + viewSliceStore.toggleSyncImages(); + viewCameraStore.toggleSyncCameras(); + cameraAutoResetState.cameraAutoResetState.value = true; + } + + watch( + isSync, + () => { + viewSliceStore.updateSyncConfigs(); + viewCameraStore.updateSyncConfigs(); + } + ); + + // --- selection --- // const { selected, selectedAll, selectedSome, toggleSelectAll } = @@ -151,6 +182,9 @@ export default defineComponent({ setPrimarySelection: (sel: DataSelection) => { dataStore.setPrimarySelection(sel); }, + sameSpaceImages, + toggleSyncImages, + isSync, }; }, }); @@ -173,6 +207,22 @@ export default defineComponent({ /> + + mdi-lock + mdi-lock-open-variant + + Sync Images + + ({ diff --git a/src/composables/useSliceConfig.ts b/src/composables/useSliceConfig.ts index be82b526..44d1aa63 100644 --- a/src/composables/useSliceConfig.ts +++ b/src/composables/useSliceConfig.ts @@ -19,6 +19,11 @@ export function useSliceConfig( const imageIdVal = unref(imageID); if (!imageIdVal || val == null) return; store.updateConfig(unref(viewID), imageIdVal, { slice: val }); + + // Update other synchronized views if any + if (config.value?.syncState) { + store.updateSyncConfigs(); + } }, }); const range = computed((): Vector2 => { diff --git a/src/io/import/processors/importSingleFile.ts b/src/io/import/processors/importSingleFile.ts index f64de81c..0a972e92 100644 --- a/src/io/import/processors/importSingleFile.ts +++ b/src/io/import/processors/importSingleFile.ts @@ -8,6 +8,10 @@ import { ImportHandler } from '@/src/io/import/common'; import { DataSourceWithFile } from '@/src/io/import/dataSource'; import { useDatasetStore } from '@/src/store/datasets'; import { useMessageStore } from '@/src/store/messages'; +import { useViewStore } from '@/src/store/views'; +import { useViewSliceStore } from '@/src/store/view-configs/slicing'; +import { getLPSAxisFromDir } from '@/src/utils/lps'; +import { InitViewSpecs } from '@/src/config'; /** * Reads and imports a file DataSource. @@ -36,6 +40,19 @@ const importSingleFile: ImportHandler = async (dataSource, { done }) => { ); fileStore.add(dataID, [dataSource as DataSourceWithFile]); + // Create a default view for each viewID + useViewStore().viewIDs.forEach((viewID) => { + const { lpsOrientation, dimensions } = useImageStore().metadata[dataID]; + const axisDir = InitViewSpecs[viewID].props.viewDirection + const lpsFromDir = getLPSAxisFromDir(axisDir); + const lpsOrient = lpsOrientation[lpsFromDir]; + + const dimMax = dimensions[lpsOrient]; + + useViewSliceStore().updateConfig(viewID, dataID, { axisDirection: axisDir, max: dimMax - 1 }); + useViewSliceStore().resetSlice(viewID, dataID); + }); + return done({ dataID, dataSource, diff --git a/src/io/state-file/schema.ts b/src/io/state-file/schema.ts index 65a8cbc9..753191b7 100644 --- a/src/io/state-file/schema.ts +++ b/src/io/state-file/schema.ts @@ -113,6 +113,7 @@ const SliceConfig = z.object({ min: z.number(), max: z.number(), axisDirection: LPSAxisDir, + syncState: z.boolean(), }) satisfies z.ZodType; const CameraConfig = z.object({ @@ -121,6 +122,7 @@ const CameraConfig = z.object({ focalPoint: Vector3.optional(), directionOfProjection: Vector3.optional(), viewUp: Vector3.optional(), + syncState: z.boolean().optional(), }) satisfies z.ZodType; const ColorBy = z.object({ diff --git a/src/store/datasets-images.ts b/src/store/datasets-images.ts index 817a7c06..460d8921 100644 --- a/src/store/datasets-images.ts +++ b/src/store/datasets-images.ts @@ -10,6 +10,7 @@ import { StateFile, DatasetType } from '../io/state-file/schema'; import { serializeData } from '../io/state-file/utils'; import { useFileStore } from './datasets-files'; import { ImageMetadata } from '../types/image'; +import { compareImageSpaces } from '../utils/imageSpace'; export const defaultImageMetadata = () => ({ name: '(none)', @@ -79,6 +80,17 @@ export const useImageStore = defineStore('images', { } }, + checkAllImagesSameSpace() { + if (this.idList.length < 2) return false; + + const dataFirst = this.dataIndex[this.idList[0]]; + const allEqual = this.idList.slice(1).every((id) => { + return compareImageSpaces(this.dataIndex[id], dataFirst); + }); + + return allEqual; + }, + async serialize(stateFile: StateFile) { const fileStore = useFileStore(); // We want to filter out volume images (which are generated and don't have diff --git a/src/store/view-configs/camera.ts b/src/store/view-configs/camera.ts index 81044e4d..35ea5c5c 100644 --- a/src/store/view-configs/camera.ts +++ b/src/store/view-configs/camera.ts @@ -7,11 +7,14 @@ import { } from '@/src/utils/doubleKeyRecord'; import { reactive } from 'vue'; import { Maybe } from '@/src/types'; +import { useCurrentImage } from '@/src/composables/useCurrentImage'; import { createViewConfigSerializer } from './common'; import { ViewConfig } from '../../io/state-file/schema'; import { CameraConfig } from './types'; +import { useImageStore } from '../datasets-images'; export const useViewCameraStore = defineStore('viewCamera', () => { + const imageStore = useImageStore(); const configs = reactive>({}); const getConfig = (viewID: Maybe, dataID: Maybe) => @@ -42,6 +45,40 @@ export const useViewCameraStore = defineStore('viewCamera', () => { } }; + const toggleSyncCameras = () => { + // Synchronize all cameras when toggled + Object.keys(configs).forEach((viewID) => { + imageStore.idList.forEach((imageID) => { + const { syncState } = { + ...getConfig(viewID, imageID) + }; + updateConfig(viewID, imageID, { syncState: !syncState }); + }); + }); + } + + const isSync = () => { + const allSync = Object.keys(configs).every((sc) => Object.keys(configs[sc]).every((c) => configs[sc][c].syncState)); + + return allSync; + } + + const updateSyncConfigs = () => { + Object.keys(configs).forEach((viewID) => { + const { currentImageID } = useCurrentImage(); + const config = getConfig(viewID, currentImageID.value); + imageStore.idList.forEach((imageID) => { + const { syncState } = { + ...getConfig(viewID, imageID) + }; + + if (syncState) { + updateConfig(viewID, imageID, { position: config?.position, focalPoint: config?.focalPoint, parallelScale: config?.parallelScale }); + } + }); + }); + } + const serialize = createViewConfigSerializer(configs, 'camera'); const deserialize = (viewID: string, config: Record) => { @@ -58,6 +95,9 @@ export const useViewCameraStore = defineStore('viewCamera', () => { updateConfig, removeView, removeData, + toggleSyncCameras, + updateSyncConfigs, + isSync, serialize, deserialize, }; diff --git a/src/store/view-configs/slicing.ts b/src/store/view-configs/slicing.ts index c715df7c..ac53ac07 100644 --- a/src/store/view-configs/slicing.ts +++ b/src/store/view-configs/slicing.ts @@ -8,18 +8,22 @@ import { patchDoubleKeyRecord, } from '@/src/utils/doubleKeyRecord'; import { Maybe } from '@/src/types'; +import { useCurrentImage } from '@/src/composables/useCurrentImage'; import { createViewConfigSerializer } from './common'; import { ViewConfig } from '../../io/state-file/schema'; import { SliceConfig } from './types'; +import { useImageStore } from '../datasets-images'; export const defaultSliceConfig = (): SliceConfig => ({ slice: 0, min: 0, max: 1, axisDirection: 'Inferior', + syncState: false, }); export const useViewSliceStore = defineStore('viewSlice', () => { + const imageStore = useImageStore(); const configs = reactive>({}); const getConfig = (viewID: Maybe, dataID: Maybe) => @@ -64,6 +68,42 @@ export const useViewSliceStore = defineStore('viewSlice', () => { } }; + const toggleSyncImages = () => { + // Synchronize all images when toggled + Object.keys(configs).forEach((viewID) => { + imageStore.idList.forEach((imageID) => { + const { syncState } = { + ...defaultSliceConfig(), + ...getConfig(viewID, imageID) + }; + updateConfig(viewID, imageID, { syncState: !syncState }); + }); + }); + } + + const isSync = () => { + const allSync = Object.keys(configs).every((sc) => Object.keys(configs[sc]).every((c) => configs[sc][c].syncState)); + + return allSync; + } + + const updateSyncConfigs = () => { + Object.keys(configs).forEach((viewID) => { + const { currentImageID } = useCurrentImage(); + const config = getConfig(viewID, currentImageID.value); + imageStore.idList.forEach((imageID) => { + const { syncState } = { + ...defaultSliceConfig(), + ...getConfig(viewID, imageID) + }; + + if (syncState) { + updateConfig(viewID, imageID, { slice: config?.slice }); + } + }); + }); + } + const serialize = createViewConfigSerializer(configs, 'slice'); const deserialize = (viewID: string, config: Record) => { @@ -81,6 +121,9 @@ export const useViewSliceStore = defineStore('viewSlice', () => { resetSlice, removeView, removeData, + toggleSyncImages, + updateSyncConfigs, + isSync, serialize, deserialize, }; diff --git a/src/store/view-configs/types.ts b/src/store/view-configs/types.ts index 7a54766d..781d6450 100644 --- a/src/store/view-configs/types.ts +++ b/src/store/view-configs/types.ts @@ -14,6 +14,7 @@ export interface CameraConfig { focalPoint?: Vector3; directionOfProjection?: Vector3; viewUp?: Vector3; + syncState?: boolean; } export interface SliceConfig { @@ -21,6 +22,7 @@ export interface SliceConfig { min: number; max: number; axisDirection: LPSAxisDir; + syncState: boolean; } export interface VolumeColorConfig {