Skip to content

Commit 46c4d92

Browse files
committed
feat(segmentGroups): add per view outline opacity config
1 parent e74f38b commit 46c4d92

File tree

8 files changed

+229
-17
lines changed

8 files changed

+229
-17
lines changed

src/components/SegmentGroupControls.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ function openSaveDialog(id: string) {
208208
<segment-group-opacity
209209
v-if="currentSegmentGroupID"
210210
:group-id="currentSegmentGroupID"
211-
class="my-1"
212211
/>
213212
<v-radio-group
214213
v-model="currentSegmentGroupID"

src/components/SegmentGroupOpacity.vue

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { computed, toRefs } from 'vue';
33
import { useGlobalLayerColorConfig } from '@/src/composables/useGlobalLayerColorConfig';
4+
import { useGlobalSegmentGroupConfig } from '@/src/store/view-configs/segmentGroups';
45
56
const props = defineProps<{
67
groupId: string;
@@ -21,12 +22,33 @@ const setOpacity = (opacity: number) => {
2122
},
2223
});
2324
};
25+
26+
const { config, updateConfig: updateSegmentGroupConfig } =
27+
useGlobalSegmentGroupConfig(groupId);
28+
29+
const outlineOpacity = computed({
30+
get: () => config.value!.config!.outlineOpacity,
31+
set: (opacity: number) => {
32+
updateSegmentGroupConfig({
33+
outlineOpacity: opacity,
34+
});
35+
},
36+
});
37+
38+
const outlineThickness = computed({
39+
get: () => config.value!.config!.outlineThickness,
40+
set: (thickness: number) => {
41+
updateSegmentGroupConfig({
42+
outlineThickness: thickness,
43+
});
44+
},
45+
});
2446
</script>
2547

2648
<template>
2749
<v-slider
2850
class="mx-4"
29-
label="Segment Group Opacity"
51+
label="Segment Group Fill Opacity"
3052
min="0"
3153
max="1"
3254
step="0.01"
@@ -36,4 +58,26 @@ const setOpacity = (opacity: number) => {
3658
:model-value="blendConfig.opacity"
3759
@update:model-value="setOpacity($event)"
3860
/>
61+
<v-slider
62+
class="mx-4"
63+
label="Segment Group Outline Opacity"
64+
min="0"
65+
max="1"
66+
step="0.01"
67+
density="compact"
68+
hide-details
69+
thumb-label
70+
v-model="outlineOpacity"
71+
/>
72+
<v-slider
73+
class="mx-4"
74+
label="Segment Group Outline Thickness"
75+
min="0"
76+
max="10"
77+
step="1"
78+
density="compact"
79+
hide-details
80+
thumb-label
81+
v-model="outlineThickness"
82+
/>
3983
</template>

src/components/vtk/VtkSegmentationSliceRepresentation.vue

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { toRefs, watchEffect, inject, computed } from 'vue';
2+
import { toRefs, watchEffect, inject, computed, unref } from 'vue';
33
import { useImage } from '@/src/composables/useCurrentImage';
44
import { useSliceRepresentation } from '@/src/core/vtk/useSliceRepresentation';
55
import { LPSAxis } from '@/src/types/lps';
@@ -17,6 +17,7 @@ import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef';
1717
import { syncRef } from '@vueuse/core';
1818
import { useSliceConfig } from '@/src/composables/useSliceConfig';
1919
import useLayerColoringStore from '@/src/store/view-configs/layers';
20+
import { useSegmentGroupConfigStore } from '@/src/store/view-configs/segmentGroups';
2021
import { useSegmentGroupConfigInitializer } from '@/src/composables/useSegmentGroupConfigInitializer';
2122
2223
interface Props {
@@ -135,24 +136,29 @@ const applySegmentColoring = () => {
135136
136137
watchEffect(applySegmentColoring);
137138
139+
const configStore = useSegmentGroupConfigStore();
140+
const config = computed(() =>
141+
configStore.getConfig(unref(viewId), unref(segmentationId))
142+
);
143+
144+
const outlineThickness = computed(() => config.value?.outlineThickness ?? 2);
138145
sliceRep.property.setUseLabelOutline(true);
139-
sliceRep.property.setUseLookupTableScalarRange(true); // For the labelmap is rendered correctly
146+
sliceRep.property.setUseLookupTableScalarRange(true);
140147
141-
// watchEffect(() => {
142-
// sliceRep.property.setLabelOutlineOpacity(opacity.value);
143-
// });
148+
watchEffect(() => {
149+
sliceRep.property.setLabelOutlineOpacity(config.value?.outlineOpacity ?? 1);
150+
});
144151
145-
const outlinePixelThickness = 2;
146152
watchEffect(() => {
147153
if (!metadata.value) return; // segment group just deleted
148154
155+
const thickness = outlineThickness.value;
149156
const { segments } = metadata.value;
150-
const max = Math.max(...segments.order);
157+
const largestValue = Math.max(...segments.order);
151158
152-
const segThicknesses = Array.from({ length: max }, (_, i) => {
153-
const value = i + 1;
154-
const segment = segments.byValue[value];
155-
return ((!segment || segment.visible) && outlinePixelThickness) || 0;
159+
const segThicknesses = Array.from({ length: largestValue }, (_, value) => {
160+
const segment = segments.byValue[value + 1];
161+
return ((!segment || segment.visible) && thickness) || 0;
156162
});
157163
sliceRep.property.setLabelOutlineThickness(segThicknesses);
158164
});

src/composables/useSegmentGroupConfigInitializer.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import useLayerColoringStore from '@/src/store/view-configs/layers';
21
import { watchImmediate } from '@vueuse/core';
32
import { MaybeRef, computed, unref } from 'vue';
3+
import useLayerColoringStore from '@/src/store/view-configs/layers';
4+
import { useSegmentGroupConfigStore } from '@/src/store/view-configs/segmentGroups';
45

5-
export function useSegmentGroupConfigInitializer(
6+
function useLayerConfigInitializerForSegmentGroups(
67
viewId: MaybeRef<string>,
78
layerId: MaybeRef<string>
89
) {
@@ -16,6 +17,25 @@ export function useSegmentGroupConfigInitializer(
1617

1718
const viewIdVal = unref(viewId);
1819
const layerIdVal = unref(layerId);
19-
coloringStore.initConfig(viewIdVal, layerIdVal);
20+
coloringStore.initConfig(viewIdVal, layerIdVal); // initConfig instead of resetColorPreset for layers
21+
});
22+
}
23+
24+
export function useSegmentGroupConfigInitializer(
25+
viewId: MaybeRef<string>,
26+
segmentGroupId: MaybeRef<string>
27+
) {
28+
useLayerConfigInitializerForSegmentGroups(viewId, segmentGroupId);
29+
30+
const configStore = useSegmentGroupConfigStore();
31+
const config = computed(() =>
32+
configStore.getConfig(unref(viewId), unref(segmentGroupId))
33+
);
34+
35+
watchImmediate(config, (config_) => {
36+
if (config_) return;
37+
const viewIdVal = unref(viewId);
38+
const layerIdVal = unref(segmentGroupId);
39+
configStore.initConfig(viewIdVal, layerIdVal);
2040
});
2141
}

src/io/state-file/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
SliceConfig,
2222
WindowLevelConfig,
2323
LayersConfig,
24+
SegmentGroupConfig,
2425
VolumeColorConfig,
2526
} from '../../store/view-configs/types';
2627
import type { LPSAxisDir, LPSAxis } from '../../types/lps';
@@ -215,10 +216,15 @@ const LayersConfig = z.object({
215216
blendConfig: BlendConfig,
216217
}) satisfies z.ZodType<LayersConfig>;
217218

219+
const SegmentGroupConfig = z.object({
220+
outlineOpacity: z.number(),
221+
}) satisfies z.ZodType<SegmentGroupConfig>;
222+
218223
const ViewConfig = z.object({
219224
window: WindowLevelConfig.optional(),
220225
slice: SliceConfig.optional(),
221226
layers: LayersConfig.optional(),
227+
segmentGroup: SegmentGroupConfig.optional(),
222228
camera: CameraConfig.optional(),
223229
volumeColorConfig: VolumeColorConfig.optional(),
224230
});

src/store/view-configs/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { StateFile, ViewConfig } from '../../io/state-file/schema';
33
import {
44
CameraConfig,
55
LayersConfig,
6+
SegmentGroupConfig,
67
SliceConfig,
78
VolumeColorConfig,
89
WindowLevelConfig,
@@ -14,7 +15,8 @@ type SubViewConfig =
1415
| SliceConfig
1516
| VolumeColorConfig
1617
| WindowLevelConfig
17-
| LayersConfig;
18+
| LayersConfig
19+
| SegmentGroupConfig;
1820

1921
type ViewConfigGetter = (
2022
viewID: string,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { reactive, computed, unref, MaybeRef } from 'vue';
2+
import { defineStore } from 'pinia';
3+
4+
import {
5+
DoubleKeyRecord,
6+
deleteSecondKey,
7+
getDoubleKeyRecord,
8+
patchDoubleKeyRecord,
9+
} from '@/src/utils/doubleKeyRecord';
10+
import { Maybe } from '@/src/types';
11+
12+
import { createViewConfigSerializer } from './common';
13+
import { ViewConfig } from '../../io/state-file/schema';
14+
import { SegmentGroupConfig } from './types';
15+
16+
type Config = SegmentGroupConfig;
17+
const CONFIG_NAME = 'segmentGroup';
18+
19+
export const defaultConfig = () => ({
20+
outlineOpacity: 1.0,
21+
outlineThickness: 2,
22+
});
23+
24+
export const useSegmentGroupConfigStore = defineStore(
25+
`${CONFIG_NAME}Config`,
26+
() => {
27+
const configs = reactive<DoubleKeyRecord<Config>>({});
28+
29+
const getConfig = (viewID: Maybe<string>, dataID: Maybe<string>) =>
30+
getDoubleKeyRecord(configs, viewID, dataID);
31+
32+
const updateConfig = (
33+
viewID: string,
34+
dataID: string,
35+
patch: Partial<Config>
36+
) => {
37+
const config = {
38+
...defaultConfig(),
39+
...getConfig(viewID, dataID),
40+
...patch,
41+
};
42+
43+
patchDoubleKeyRecord(configs, viewID, dataID, config);
44+
};
45+
46+
const initConfig = (viewID: string, dataID: string) =>
47+
updateConfig(viewID, dataID, defaultConfig());
48+
49+
const removeView = (viewID: string) => {
50+
delete configs[viewID];
51+
};
52+
53+
const removeData = (dataID: string, viewID?: string) => {
54+
if (viewID) {
55+
delete configs[viewID]?.[dataID];
56+
} else {
57+
deleteSecondKey(configs, dataID);
58+
}
59+
};
60+
61+
const serialize = createViewConfigSerializer(configs, CONFIG_NAME);
62+
63+
const deserialize = (
64+
viewID: string,
65+
config: Record<string, ViewConfig>
66+
) => {
67+
Object.entries(config).forEach(([dataID, viewConfig]) => {
68+
if (viewConfig.segmentGroup) {
69+
updateConfig(viewID, dataID, viewConfig.segmentGroup);
70+
}
71+
});
72+
};
73+
74+
// For updating all configs together //
75+
76+
const aConfig = computed(() => {
77+
const viewIDs = Object.keys(configs);
78+
if (viewIDs.length === 0) return null;
79+
const firstViewID = viewIDs[0];
80+
const dataIDs = Object.keys(configs[firstViewID]);
81+
if (dataIDs.length === 0) return null;
82+
const firstDataID = dataIDs[0];
83+
return configs[firstViewID][firstDataID];
84+
});
85+
86+
const updateAllConfigs = (dataID: string, patch: Partial<Config>) => {
87+
Object.keys(configs).forEach((viewID) => {
88+
updateConfig(viewID, dataID, patch);
89+
});
90+
};
91+
92+
return {
93+
configs,
94+
getConfig,
95+
initConfig,
96+
updateConfig,
97+
removeView,
98+
removeData,
99+
serialize,
100+
deserialize,
101+
aConfig,
102+
updateAllConfigs,
103+
};
104+
}
105+
);
106+
107+
export const useGlobalSegmentGroupConfig = (dataId: MaybeRef<string>) => {
108+
const store = useSegmentGroupConfigStore();
109+
110+
const views = computed(() => Object.keys(store.configs));
111+
112+
const configs = computed(() =>
113+
views.value.map((viewID) => ({
114+
config: store.getConfig(viewID, unref(dataId)),
115+
viewID,
116+
}))
117+
);
118+
119+
// get any one
120+
const config = computed(() => configs.value.find(({ config: c }) => c));
121+
122+
// update all configs
123+
const updateConfig = (patch: Partial<Config>) => {
124+
configs.value.forEach(({ viewID }) =>
125+
store.updateConfig(viewID, unref(dataId), patch)
126+
);
127+
};
128+
129+
return { config, updateConfig };
130+
};

src/store/view-configs/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@ export interface LayersConfig {
5757
opacityFunction: OpacityFunction;
5858
blendConfig: BlendConfig;
5959
}
60+
61+
export interface SegmentGroupConfig {
62+
outlineOpacity: number;
63+
outlineThickness: number;
64+
}

0 commit comments

Comments
 (0)