Skip to content

Commit 2f7eb25

Browse files
authored
Merge pull request #705 from PaulHax/seg-sliders
Batch edit segment groups
2 parents 6df6e6e + bda9efe commit 2f7eb25

File tree

8 files changed

+247
-114
lines changed

8 files changed

+247
-114
lines changed

src/components/MeasurementsToolList.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,6 @@ const toggleSelectAll = (shouldSelectAll: boolean) => {
8888
}
8989
};
9090
91-
const forEachSelectedTool = (
92-
callback: (tool: (typeof tools.value)[number]) => void
93-
) =>
94-
tools.value
95-
.filter((tool) => selectionStore.isSelected(tool.id))
96-
.forEach(callback);
97-
9891
function removeAll() {
9992
selectionStore.selection.forEach((sel) => {
10093
const store = useAnnotationToolStore(sel.type);
@@ -111,6 +104,13 @@ const allHidden = computed(() => {
111104
.every((tool) => tool.toolData.hidden);
112105
});
113106
107+
const forEachSelectedTool = (
108+
callback: (tool: (typeof tools.value)[number]) => void
109+
) =>
110+
tools.value
111+
.filter((tool) => selectionStore.isSelected(tool.id))
112+
.forEach(callback);
113+
114114
function toggleGlobalHidden() {
115115
const hidden = !allHidden.value;
116116
forEachSelectedTool((tool) => {

src/components/SegmentGroupControls.vue

Lines changed: 155 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useGlobalLayerColorConfig } from '@/src/composables/useGlobalLayerColor
1515
import { usePaintToolStore } from '@/src/store/tools/paint';
1616
import { Maybe } from '@/src/types';
1717
import { reactive, ref, computed, watch, toRaw } from 'vue';
18+
import { useMultiSelection } from '@/src/composables/useMultiSelection';
1819
1920
const UNNAMED_GROUP_NAME = 'Unnamed Segment Group';
2021
@@ -160,6 +161,62 @@ function openSaveDialog(id: string) {
160161
saveId.value = id;
161162
saveDialog.value = true;
162163
}
164+
165+
const segGroupIds = computed(() =>
166+
currentSegmentGroups.value.map((group) => group.id)
167+
);
168+
169+
const { selected, selectedAll, selectedSome } = useMultiSelection(segGroupIds);
170+
171+
// ensure currentSegmentGroupID is always in selected
172+
watch(
173+
// includes currentImageID to reselect when switching images because currentSegmentGroupID is not updated on image change
174+
[currentSegmentGroupID, currentImageID],
175+
() => {
176+
const groupId = currentSegmentGroupID.value;
177+
if (!groupId) return;
178+
selected.value = [groupId];
179+
},
180+
{ immediate: true }
181+
);
182+
183+
function toggleSelectAll() {
184+
if (selectedAll.value && currentSegmentGroupID.value) {
185+
selected.value = [currentSegmentGroupID.value];
186+
} else if (selectedAll.value) {
187+
selected.value = [];
188+
} else {
189+
selected.value = segGroupIds.value;
190+
}
191+
}
192+
193+
const allHidden = computed(() => {
194+
return selected.value
195+
.map((id) => currentSegmentGroups.value.find((group) => id === group.id))
196+
.filter((group): group is NonNullable<typeof group> => group != null)
197+
.every((group) => !group.visibility);
198+
});
199+
200+
function toggleGlobalVisibility() {
201+
const shouldShow = allHidden.value;
202+
selected.value.forEach((id) => {
203+
const group = currentSegmentGroups.value.find((g) => g.id === id);
204+
if (group) {
205+
const { sampledConfig, updateConfig } = useGlobalLayerColorConfig(id);
206+
const currentBlend = sampledConfig.value!.config!.blendConfig;
207+
updateConfig({
208+
blendConfig: {
209+
...currentBlend,
210+
visibility: shouldShow,
211+
},
212+
});
213+
}
214+
});
215+
}
216+
217+
function deleteSelected() {
218+
selected.value.forEach((id) => deleteGroup(id));
219+
}
163220
</script>
164221

165222
<template>
@@ -203,63 +260,107 @@ function openSaveDialog(id: string) {
203260
</v-list>
204261
</v-menu>
205262
</div>
206-
<v-divider class="my-4" />
207263

208264
<segment-group-opacity
209265
v-if="currentSegmentGroupID"
210266
:group-id="currentSegmentGroupID"
267+
:selected="selected"
211268
/>
212-
<v-radio-group
213-
v-model="currentSegmentGroupID"
214-
hide-details
215-
density="comfortable"
216-
class="my-1 segment-group-list"
217-
>
218-
<v-radio
269+
270+
<div class="d-flex align-center" v-if="currentSegmentGroups.length > 0">
271+
<v-checkbox
272+
class="ml-3"
273+
:indeterminate="selectedSome && !selectedAll"
274+
label="Select All"
275+
:model-value="selectedAll"
276+
@update:model-value="toggleSelectAll"
277+
density="compact"
278+
hide-details
279+
/>
280+
<v-btn
281+
icon
282+
variant="text"
283+
:disabled="selected.length === 0"
284+
@click.stop="toggleGlobalVisibility"
285+
>
286+
<v-icon v-if="allHidden">mdi-eye-off</v-icon>
287+
<v-icon v-else>mdi-eye</v-icon>
288+
<v-tooltip location="top" activator="parent">
289+
{{ allHidden ? 'Show' : 'Hide' }} selected
290+
</v-tooltip>
291+
</v-btn>
292+
<v-btn
293+
icon
294+
variant="text"
295+
:disabled="selected.length === 0"
296+
@click.stop="deleteSelected"
297+
>
298+
<v-icon>mdi-delete</v-icon>
299+
<v-tooltip location="top" activator="parent">
300+
Delete selected
301+
</v-tooltip>
302+
</v-btn>
303+
</div>
304+
<v-list density="comfortable" class="my-1 segment-group-list">
305+
<v-list-item
219306
v-for="group in currentSegmentGroups"
220307
:key="group.id"
221-
:value="group.id"
308+
:active="currentSegmentGroupID === group.id"
309+
@click="currentSegmentGroupID = group.id"
222310
>
223-
<template #label>
224-
<div class="d-flex flex-row align-center w-100" :title="group.name">
225-
<span class="group-name">{{ group.name }}</span>
226-
<v-spacer />
227-
<v-btn
228-
icon
229-
variant="flat"
230-
size="small"
231-
@click.stop="group.toggleVisibility"
311+
<div class="d-flex flex-row align-center w-100" :title="group.name">
312+
<v-checkbox
313+
class="no-grow mr-4"
314+
density="compact"
315+
hide-details
316+
@click.stop
317+
:value="group.id"
318+
v-model="selected"
319+
:disabled="group.id === currentSegmentGroupID"
320+
/>
321+
<span class="group-name">{{ group.name }}</span>
322+
<v-spacer />
323+
<v-btn
324+
icon
325+
variant="text"
326+
size="small"
327+
@click.stop="group.toggleVisibility"
328+
>
329+
<v-icon v-if="group.visibility" style="pointer-events: none"
330+
>mdi-eye</v-icon
232331
>
233-
<v-icon v-if="group.visibility" style="pointer-events: none"
234-
>mdi-eye</v-icon
235-
>
236-
<v-icon v-else style="pointer-events: none">mdi-eye-off</v-icon>
237-
<v-tooltip location="left" activator="parent">{{
238-
group.visibility ? 'Hide' : 'Show'
239-
}}</v-tooltip>
240-
</v-btn>
241-
<v-btn
242-
icon="mdi-content-save"
243-
size="small"
244-
variant="flat"
245-
@click.stop="openSaveDialog(group.id)"
246-
></v-btn>
247-
<v-btn
248-
icon="mdi-pencil"
249-
size="small"
250-
variant="flat"
251-
@click.stop="startEditing(group.id)"
252-
></v-btn>
253-
<v-btn
254-
icon="mdi-delete"
255-
size="small"
256-
variant="flat"
257-
@click.stop="deleteGroup(group.id)"
258-
></v-btn>
259-
</div>
260-
</template>
261-
</v-radio>
262-
</v-radio-group>
332+
<v-icon v-else style="pointer-events: none">mdi-eye-off</v-icon>
333+
<v-tooltip location="left" activator="parent">
334+
{{ group.visibility ? 'Hide' : 'Show' }}
335+
</v-tooltip>
336+
</v-btn>
337+
<v-btn
338+
icon="mdi-content-save"
339+
size="small"
340+
variant="text"
341+
@click.stop="openSaveDialog(group.id)"
342+
/>
343+
<v-btn
344+
icon="mdi-pencil"
345+
size="small"
346+
variant="text"
347+
@click.stop="startEditing(group.id)"
348+
/>
349+
<v-btn
350+
icon="mdi-delete"
351+
size="small"
352+
variant="text"
353+
@click.stop="deleteGroup(group.id)"
354+
/>
355+
</div>
356+
</v-list-item>
357+
<v-list-item v-if="currentSegmentGroups.length === 0">
358+
<div class="text-center text-grey-darken-1 py-4 w-100">
359+
Create a segment group with the above buttons or click the paint tool
360+
</div>
361+
</v-list-item>
362+
</v-list>
363+
263364
<v-divider class="my-4" />
264365
</div>
265366
<div v-else class="text-center text-caption">No selected image</div>
@@ -306,5 +407,11 @@ function openSaveDialog(id: string) {
306407
white-space: nowrap;
307408
overflow: hidden;
308409
text-overflow: ellipsis;
410+
padding-right: 10px;
411+
text-align: left;
412+
}
413+
414+
.no-grow {
415+
flex: 0 0 auto;
309416
}
310417
</style>

src/components/SegmentGroupOpacity.vue

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,59 @@ import { useGlobalSegmentGroupConfig } from '@/src/store/view-configs/segmentGro
55
66
const props = defineProps<{
77
groupId: string;
8+
selected: string[];
89
}>();
910
10-
const { groupId } = toRefs(props);
11+
const { groupId, selected } = toRefs(props);
1112
12-
const { sampledConfig, updateConfig } = useGlobalLayerColorConfig(groupId);
13+
const { sampledConfig } = useGlobalLayerColorConfig(groupId);
1314
1415
const blendConfig = computed(() => sampledConfig.value!.config!.blendConfig);
1516
17+
const layerUpdateFunctions = computed(() =>
18+
selected.value.map((id) => {
19+
return useGlobalLayerColorConfig(id).updateConfig;
20+
})
21+
);
22+
1623
const setOpacity = (opacity: number) => {
17-
updateConfig({
18-
blendConfig: {
19-
...blendConfig.value,
20-
// 1.0 puts us in Opaque render pass which changes stack order.
21-
opacity: Math.min(opacity, 0.9999),
22-
},
24+
layerUpdateFunctions.value.forEach((updateFn) => {
25+
updateFn({
26+
blendConfig: {
27+
...blendConfig.value,
28+
// 1.0 puts us in Opaque render pass which changes stack order.
29+
opacity: Math.min(opacity, 0.9999),
30+
},
31+
});
2332
});
2433
};
2534
26-
const { config, updateConfig: updateSegmentGroupConfig } =
27-
useGlobalSegmentGroupConfig(groupId);
35+
const { config } = useGlobalSegmentGroupConfig(groupId);
36+
37+
const groupUpdateFunctions = computed(() =>
38+
selected.value.map((id) => {
39+
return useGlobalSegmentGroupConfig(id).updateConfig;
40+
})
41+
);
2842
2943
const outlineOpacity = computed({
3044
get: () => config.value!.config!.outlineOpacity,
3145
set: (opacity: number) => {
32-
updateSegmentGroupConfig({
33-
outlineOpacity: opacity,
46+
groupUpdateFunctions.value.forEach((updateFn) => {
47+
updateFn({
48+
outlineOpacity: opacity,
49+
});
3450
});
3551
},
3652
});
3753
3854
const outlineThickness = computed({
3955
get: () => config.value!.config!.outlineThickness,
4056
set: (thickness: number) => {
41-
updateSegmentGroupConfig({
42-
outlineThickness: thickness,
57+
groupUpdateFunctions.value.forEach((updateFn) => {
58+
updateFn({
59+
outlineThickness: thickness,
60+
});
4361
});
4462
},
4563
});
@@ -48,7 +66,7 @@ const outlineThickness = computed({
4866
<template>
4967
<v-slider
5068
class="mx-4"
51-
label="Segment Group Fill Opacity"
69+
label="Fill Opacity"
5270
min="0"
5371
max="1"
5472
step="0.01"
@@ -60,7 +78,7 @@ const outlineThickness = computed({
6078
/>
6179
<v-slider
6280
class="mx-4"
63-
label="Segment Group Outline Opacity"
81+
label="Outline Opacity"
6482
min="0"
6583
max="1"
6684
step="0.01"
@@ -71,7 +89,7 @@ const outlineThickness = computed({
7189
/>
7290
<v-slider
7391
class="mx-4"
74-
label="Segment Group Outline Thickness"
92+
label="Outline Thickness"
7593
min="0"
7694
max="10"
7795
step="1"

src/components/SliceViewer.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ const selectionPoints = computed(() => {
251251
.filter(
252252
({ tool }) =>
253253
tool.slice === currentSlice.value &&
254+
!tool.hidden &&
254255
doesToolFrameMatchViewAxis(viewAxis, tool, currentImageMetadata)
255256
)
256257
.flatMap(({ store, tool }) => store.getPoints(tool.id));

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,4 @@ export const CATEGORICAL_COLORS = [
505505
[228, 114, 126],
506506
[89, 38, 119],
507507
[105, 47, 61],
508-
];
508+
] as const;

0 commit comments

Comments
 (0)