From b255b0c408ff3f8df1936209969c0f1ca960715c Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 24 Oct 2024 18:44:36 -0400 Subject: [PATCH] feat(segmentGroups): add per view outline opacity config --- src/components/SegmentGroupControls.vue | 1 - src/components/SegmentGroupOpacity.vue | 46 ++++++- src/components/SegmentList.vue | 2 +- .../VtkSegmentationSliceRepresentation.vue | 28 ++-- .../useSegmentGroupConfigInitializer.ts | 26 +++- src/io/state-file/schema.ts | 6 + src/store/view-configs/common.ts | 4 +- src/store/view-configs/segmentGroups.ts | 130 ++++++++++++++++++ src/store/view-configs/types.ts | 5 + 9 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 src/store/view-configs/segmentGroups.ts diff --git a/src/components/SegmentGroupControls.vue b/src/components/SegmentGroupControls.vue index 1a26299d5..91e3149d9 100644 --- a/src/components/SegmentGroupControls.vue +++ b/src/components/SegmentGroupControls.vue @@ -208,7 +208,6 @@ function openSaveDialog(id: string) { import { computed, toRefs } from 'vue'; import { useGlobalLayerColorConfig } from '@/src/composables/useGlobalLayerColorConfig'; +import { useGlobalSegmentGroupConfig } from '@/src/store/view-configs/segmentGroups'; const props = defineProps<{ groupId: string; @@ -21,12 +22,33 @@ const setOpacity = (opacity: number) => { }, }); }; + +const { config, updateConfig: updateSegmentGroupConfig } = + useGlobalSegmentGroupConfig(groupId); + +const outlineOpacity = computed({ + get: () => config.value!.config!.outlineOpacity, + set: (opacity: number) => { + updateSegmentGroupConfig({ + outlineOpacity: opacity, + }); + }, +}); + +const outlineThickness = computed({ + get: () => config.value!.config!.outlineThickness, + set: (thickness: number) => { + updateSegmentGroupConfig({ + outlineThickness: thickness, + }); + }, +}); diff --git a/src/components/SegmentList.vue b/src/components/SegmentList.vue index 13e5cca30..9296f92b7 100644 --- a/src/components/SegmentList.vue +++ b/src/components/SegmentList.vue @@ -172,7 +172,7 @@ function deleteEditingSegment() { -import { toRefs, watchEffect, inject, computed } from 'vue'; +import { toRefs, watchEffect, inject, computed, unref } from 'vue'; import { useImage } from '@/src/composables/useCurrentImage'; import { useSliceRepresentation } from '@/src/core/vtk/useSliceRepresentation'; import { LPSAxis } from '@/src/types/lps'; @@ -17,6 +17,7 @@ import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; import { syncRef } from '@vueuse/core'; import { useSliceConfig } from '@/src/composables/useSliceConfig'; import useLayerColoringStore from '@/src/store/view-configs/layers'; +import { useSegmentGroupConfigStore } from '@/src/store/view-configs/segmentGroups'; import { useSegmentGroupConfigInitializer } from '@/src/composables/useSegmentGroupConfigInitializer'; interface Props { @@ -135,24 +136,29 @@ const applySegmentColoring = () => { watchEffect(applySegmentColoring); +const configStore = useSegmentGroupConfigStore(); +const config = computed(() => + configStore.getConfig(unref(viewId), unref(segmentationId)) +); + +const outlineThickness = computed(() => config.value?.outlineThickness ?? 2); sliceRep.property.setUseLabelOutline(true); -sliceRep.property.setUseLookupTableScalarRange(true); // For the labelmap is rendered correctly +sliceRep.property.setUseLookupTableScalarRange(true); -// watchEffect(() => { -// sliceRep.property.setLabelOutlineOpacity(opacity.value); -// }); +watchEffect(() => { + sliceRep.property.setLabelOutlineOpacity(config.value?.outlineOpacity ?? 1); +}); -const outlinePixelThickness = 2; watchEffect(() => { if (!metadata.value) return; // segment group just deleted + const thickness = outlineThickness.value; const { segments } = metadata.value; - const max = Math.max(...segments.order); + const largestValue = Math.max(...segments.order); - const segThicknesses = Array.from({ length: max }, (_, i) => { - const value = i + 1; - const segment = segments.byValue[value]; - return ((!segment || segment.visible) && outlinePixelThickness) || 0; + const segThicknesses = Array.from({ length: largestValue }, (_, value) => { + const segment = segments.byValue[value + 1]; + return ((!segment || segment.visible) && thickness) || 0; }); sliceRep.property.setLabelOutlineThickness(segThicknesses); }); diff --git a/src/composables/useSegmentGroupConfigInitializer.ts b/src/composables/useSegmentGroupConfigInitializer.ts index b2674f5a2..7e5145040 100644 --- a/src/composables/useSegmentGroupConfigInitializer.ts +++ b/src/composables/useSegmentGroupConfigInitializer.ts @@ -1,8 +1,9 @@ -import useLayerColoringStore from '@/src/store/view-configs/layers'; import { watchImmediate } from '@vueuse/core'; import { MaybeRef, computed, unref } from 'vue'; +import useLayerColoringStore from '@/src/store/view-configs/layers'; +import { useSegmentGroupConfigStore } from '@/src/store/view-configs/segmentGroups'; -export function useSegmentGroupConfigInitializer( +function useLayerConfigInitializerForSegmentGroups( viewId: MaybeRef, layerId: MaybeRef ) { @@ -16,6 +17,25 @@ export function useSegmentGroupConfigInitializer( const viewIdVal = unref(viewId); const layerIdVal = unref(layerId); - coloringStore.initConfig(viewIdVal, layerIdVal); + coloringStore.initConfig(viewIdVal, layerIdVal); // initConfig instead of resetColorPreset for layers + }); +} + +export function useSegmentGroupConfigInitializer( + viewId: MaybeRef, + segmentGroupId: MaybeRef +) { + useLayerConfigInitializerForSegmentGroups(viewId, segmentGroupId); + + const configStore = useSegmentGroupConfigStore(); + const config = computed(() => + configStore.getConfig(unref(viewId), unref(segmentGroupId)) + ); + + watchImmediate(config, (config_) => { + if (config_) return; + const viewIdVal = unref(viewId); + const layerIdVal = unref(segmentGroupId); + configStore.initConfig(viewIdVal, layerIdVal); }); } diff --git a/src/io/state-file/schema.ts b/src/io/state-file/schema.ts index e251f7ac7..536fb279f 100644 --- a/src/io/state-file/schema.ts +++ b/src/io/state-file/schema.ts @@ -21,6 +21,7 @@ import type { SliceConfig, WindowLevelConfig, LayersConfig, + SegmentGroupConfig, VolumeColorConfig, } from '../../store/view-configs/types'; import type { LPSAxisDir, LPSAxis } from '../../types/lps'; @@ -215,10 +216,15 @@ const LayersConfig = z.object({ blendConfig: BlendConfig, }) satisfies z.ZodType; +const SegmentGroupConfig = z.object({ + outlineOpacity: z.number(), +}) satisfies z.ZodType; + const ViewConfig = z.object({ window: WindowLevelConfig.optional(), slice: SliceConfig.optional(), layers: LayersConfig.optional(), + segmentGroup: SegmentGroupConfig.optional(), camera: CameraConfig.optional(), volumeColorConfig: VolumeColorConfig.optional(), }); diff --git a/src/store/view-configs/common.ts b/src/store/view-configs/common.ts index e02adadca..3142fbfb3 100644 --- a/src/store/view-configs/common.ts +++ b/src/store/view-configs/common.ts @@ -3,6 +3,7 @@ import { StateFile, ViewConfig } from '../../io/state-file/schema'; import { CameraConfig, LayersConfig, + SegmentGroupConfig, SliceConfig, VolumeColorConfig, WindowLevelConfig, @@ -14,7 +15,8 @@ type SubViewConfig = | SliceConfig | VolumeColorConfig | WindowLevelConfig - | LayersConfig; + | LayersConfig + | SegmentGroupConfig; type ViewConfigGetter = ( viewID: string, diff --git a/src/store/view-configs/segmentGroups.ts b/src/store/view-configs/segmentGroups.ts new file mode 100644 index 000000000..d20b82102 --- /dev/null +++ b/src/store/view-configs/segmentGroups.ts @@ -0,0 +1,130 @@ +import { reactive, computed, unref, MaybeRef } from 'vue'; +import { defineStore } from 'pinia'; + +import { + DoubleKeyRecord, + deleteSecondKey, + getDoubleKeyRecord, + patchDoubleKeyRecord, +} from '@/src/utils/doubleKeyRecord'; +import { Maybe } from '@/src/types'; + +import { createViewConfigSerializer } from './common'; +import { ViewConfig } from '../../io/state-file/schema'; +import { SegmentGroupConfig } from './types'; + +type Config = SegmentGroupConfig; +const CONFIG_NAME = 'segmentGroup'; + +export const defaultConfig = () => ({ + outlineOpacity: 1.0, + outlineThickness: 2, +}); + +export const useSegmentGroupConfigStore = defineStore( + `${CONFIG_NAME}Config`, + () => { + const configs = reactive>({}); + + const getConfig = (viewID: Maybe, dataID: Maybe) => + getDoubleKeyRecord(configs, viewID, dataID); + + const updateConfig = ( + viewID: string, + dataID: string, + patch: Partial + ) => { + const config = { + ...defaultConfig(), + ...getConfig(viewID, dataID), + ...patch, + }; + + patchDoubleKeyRecord(configs, viewID, dataID, config); + }; + + const initConfig = (viewID: string, dataID: string) => + updateConfig(viewID, dataID, defaultConfig()); + + const removeView = (viewID: string) => { + delete configs[viewID]; + }; + + const removeData = (dataID: string, viewID?: string) => { + if (viewID) { + delete configs[viewID]?.[dataID]; + } else { + deleteSecondKey(configs, dataID); + } + }; + + const serialize = createViewConfigSerializer(configs, CONFIG_NAME); + + const deserialize = ( + viewID: string, + config: Record + ) => { + Object.entries(config).forEach(([dataID, viewConfig]) => { + if (viewConfig.segmentGroup) { + updateConfig(viewID, dataID, viewConfig.segmentGroup); + } + }); + }; + + // For updating all configs together // + + const aConfig = computed(() => { + const viewIDs = Object.keys(configs); + if (viewIDs.length === 0) return null; + const firstViewID = viewIDs[0]; + const dataIDs = Object.keys(configs[firstViewID]); + if (dataIDs.length === 0) return null; + const firstDataID = dataIDs[0]; + return configs[firstViewID][firstDataID]; + }); + + const updateAllConfigs = (dataID: string, patch: Partial) => { + Object.keys(configs).forEach((viewID) => { + updateConfig(viewID, dataID, patch); + }); + }; + + return { + configs, + getConfig, + initConfig, + updateConfig, + removeView, + removeData, + serialize, + deserialize, + aConfig, + updateAllConfigs, + }; + } +); + +export const useGlobalSegmentGroupConfig = (dataId: MaybeRef) => { + const store = useSegmentGroupConfigStore(); + + const views = computed(() => Object.keys(store.configs)); + + const configs = computed(() => + views.value.map((viewID) => ({ + config: store.getConfig(viewID, unref(dataId)), + viewID, + })) + ); + + // get any one + const config = computed(() => configs.value.find(({ config: c }) => c)); + + // update all configs + const updateConfig = (patch: Partial) => { + configs.value.forEach(({ viewID }) => + store.updateConfig(viewID, unref(dataId), patch) + ); + }; + + return { config, updateConfig }; +}; diff --git a/src/store/view-configs/types.ts b/src/store/view-configs/types.ts index 781d64502..b672d44dc 100644 --- a/src/store/view-configs/types.ts +++ b/src/store/view-configs/types.ts @@ -57,3 +57,8 @@ export interface LayersConfig { opacityFunction: OpacityFunction; blendConfig: BlendConfig; } + +export interface SegmentGroupConfig { + outlineOpacity: number; + outlineThickness: number; +}