Skip to content

Commit

Permalink
feat: add initial segment editor
Browse files Browse the repository at this point in the history
  • Loading branch information
floryst committed Oct 13, 2023
1 parent ddeb75c commit 3b68ef7
Show file tree
Hide file tree
Showing 15 changed files with 730 additions and 193 deletions.
185 changes: 185 additions & 0 deletions src/components/LabelSegmentList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<script setup lang="ts">
import SegmentEditor from '@/src/components/SegmentEditor.vue';
import IsolatedDialog from '@/src/components/IsolatedDialog.vue';
import {
useLabelmapStore,
makeDefaultSegmentName,
} from '@/src/store/datasets-labelmaps';
import { Maybe } from '@/src/types';
import { hexaToRGBA, rgbaToHexa } from '@/src/utils/color';
import { reactive, ref, toRefs, computed, watch } from 'vue';
import { LabelMapSegment } from '@/src/types/labelmap';
import { usePaintToolStore } from '@/src/store/tools/paint';
const props = defineProps({
labelmapId: {
required: true,
type: String,
},
});
const { labelmapId } = toRefs(props);
const labelmapStore = useLabelmapStore();
const paintStore = usePaintToolStore();
const segments = computed<Maybe<LabelMapSegment[]>>(() => {
return labelmapStore.segmentsByLabelmapID[labelmapId.value];
});
function addNewSegment() {
labelmapStore.addSegment(labelmapId.value);
}
// --- selection --- //
const selectedSegment = ref<Maybe<number>>(null);
// reset selection when necessary
watch(
segments,
(segments_) => {
let reset = true;
if (segments_ && selectedSegment.value) {
reset = !segments_.find((seg) => seg.value === selectedSegment.value);
}
if (reset) {
selectedSegment.value = segments_?.length ? segments_[0].value : null;
}
},
{ immediate: true }
);
// TODO disable the paint tool when no segments?
// sync selection to paint brush value
watch(
selectedSegment,
(value) => {
if (value) paintStore.setBrushValue(value);
// else disable paint tool
},
{ immediate: true }
);
// --- editing state --- //
const editingSegmentValue = ref<Maybe<number>>(null);
const editState = reactive({
name: '',
color: '',
});
const editDialog = ref(false);
const editingSegment = computed(() => {
if (editingSegmentValue.value == null) return null;
return labelmapStore.getSegment(labelmapId.value, editingSegmentValue.value);
});
function startEditing(value: number) {
editDialog.value = true;
editingSegmentValue.value = value;
if (!editingSegment.value) return;
editState.name = editingSegment.value.name;
editState.color = rgbaToHexa(editingSegment.value.color);
}
function stopEditing(commit: boolean) {
if (editingSegmentValue.value && commit)
labelmapStore.updateSegment(labelmapId.value, editingSegmentValue.value, {
name: editState.name ?? makeDefaultSegmentName(editingSegmentValue.value),
color: hexaToRGBA(editState.color),
});
editingSegmentValue.value = null;
editDialog.value = false;
}
function deleteSegment(value: number) {
labelmapStore.deleteSegment(labelmapId.value, value);
}
function deleteEditingSegment() {
if (editingSegmentValue.value) deleteSegment(editingSegmentValue.value);
stopEditing(false);
}
</script>

<template>
<v-item-group mandatory selected-class="selected" v-model="selectedSegment">
<v-item
v-for="segment in segments"
:key="segment.value"
:value="segment.value"
v-slot="{ selectedClass, toggle }"
>
<v-list-item
:class="[selectedClass, 'my-1', 'segment']"
@click.stop="toggle"
>
{{ segment.name }}
<template #prepend>
<div
class="dot"
:style="{
backgroundColor: rgbaToHexa(segment.color),
}"
></div>
</template>
<template #append>
<v-btn
icon
size="x-small"
variant="plain"
@click.stop="startEditing(segment.value)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
icon
size="x-small"
variant="plain"
@click.stop="deleteSegment(segment.value)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-list-item>
</v-item>
<v-list-item class="text-center" @click.stop="addNewSegment">
<v-icon>mdi-plus</v-icon>
New segment
</v-list-item>
</v-item-group>

<isolated-dialog v-model="editDialog" @keydown.stop max-width="800px">
<segment-editor
v-if="!!editingSegment"
v-model:name="editState.name"
v-model:color="editState.color"
@delete="deleteEditingSegment"
@cancel="stopEditing(false)"
@done="stopEditing(true)"
/>
</isolated-dialog>
</template>

<style scoped>
.selected {
background: rgba(var(--v-theme-selection-bg-color));
}
.dot {
width: 16px;
height: 16px;
border-radius: 8px;
background: yellow;
margin-right: 8px;
border: 1px solid #111;
}
.segment {
overflow: hidden;
text-overflow: clip;
}
</style>
136 changes: 125 additions & 11 deletions src/components/LabelmapList.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<script setup lang="ts">
import LabelSegmentList from '@/src/components/LabelSegmentList.vue';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { useLabelmapStore } from '@/src/store/datasets-labelmaps';
import {
useLabelmapStore,
DEFAULT_LABELMAP_NAME,
} from '@/src/store/datasets-labelmaps';
import { usePaintToolStore } from '@/src/store/tools/paint';
import { computed } from 'vue';
import { Maybe } from '@/src/types';
import { reactive, ref, computed, watch } from 'vue';
const labelmapStore = useLabelmapStore();
const { currentImageID } = useCurrentImage();
Expand All @@ -19,19 +24,128 @@ const currentLabelmaps = computed(() => {
});
const paintStore = usePaintToolStore();
const targetPaintLabelmap = computed({
const selectedLabelmapID = computed({
get: () => paintStore.activeLabelmapID,
set: (id) => paintStore.setActiveLabelmap(id),
});
// clear selection if we delete the labelmaps
watch(currentLabelmaps, () => {
const selection = selectedLabelmapID.value;
if (selection && !(selection in labelmapStore.dataIndex)) {
selectedLabelmapID.value = null;
}
});
function createLabelmap() {
if (!currentImageID.value)
throw new Error('Cannot create a labelmap without a base image');
const id = labelmapStore.newLabelmapFromImage(currentImageID.value);
if (!id) throw new Error('Could not create a new labelmap');
selectedLabelmapID.value = id;
}
function deleteLabelmap(id: string) {
labelmapStore.removeLabelmap(id);
}
// --- editing state --- //
const editingLabelmapID = ref<Maybe<string>>(null);
const editState = reactive({ name: '' });
const editDialog = ref(false);
const editingMetadata = computed(() => {
if (!editingLabelmapID.value) return null;
return labelmapStore.labelmapMetadata[editingLabelmapID.value];
});
function startEditing(id: string) {
editDialog.value = true;
editingLabelmapID.value = id;
if (editingMetadata.value) {
editState.name = editingMetadata.value.name;
}
}
function stopEditing(commit: boolean) {
editDialog.value = false;
if (editingLabelmapID.value && commit)
labelmapStore.updateMetadata(editingLabelmapID.value, {
name: editState.name || DEFAULT_LABELMAP_NAME,
});
editingLabelmapID.value = null;
}
</script>

<template>
<v-radio-group v-model="targetPaintLabelmap">
<v-radio
v-for="labelmap in currentLabelmaps"
:key="labelmap.id"
:label="labelmap.name"
:value="labelmap.id"
/>
</v-radio-group>
<v-select
v-if="currentImageID"
v-model="selectedLabelmapID"
:items="currentLabelmaps"
item-title="name"
item-value="id"
placeholder="Select a labelmap"
variant="outlined"
density="compact"
>
<template #append>
<v-btn icon size="x-small" variant="flat" style="top: -4px">
<v-icon>mdi-dots-vertical</v-icon>
<v-menu activator="parent">
<v-list>
<v-list-item v-if="currentImageID" @click="createLabelmap">
Create a new labelmap
</v-list-item>
<v-list-item
v-if="selectedLabelmapID"
@click="startEditing(selectedLabelmapID)"
>
Edit labelmap
</v-list-item>
<v-list-item v-if="selectedLabelmapID">
Convert to image
</v-list-item>
<v-list-item
v-if="selectedLabelmapID"
@click="deleteLabelmap(selectedLabelmapID)"
>
Delete
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</template>
</v-select>
<div v-else class="text-center text-caption">No selected image</div>
<label-segment-list
v-if="selectedLabelmapID"
:labelmap-id="selectedLabelmapID"
/>

<v-dialog v-model="editDialog" max-width="400px">
<v-card>
<v-card-text>
<v-text-field
v-model="editState.name"
:placeholder="DEFAULT_LABELMAP_NAME"
hide-details
@keydown.stop.enter="stopEditing(true)"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="stopEditing(false)">Cancel</v-btn>
<v-btn @click="stopEditing(true)">Done</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<style>
.labelmap-radio .v-label {
justify-content: space-between;
}
</style>
Loading

0 comments on commit 3b68ef7

Please sign in to comment.