diff --git a/client/dive-common/components/AnnotationVisibilityMenu.vue b/client/dive-common/components/AnnotationVisibilityMenu.vue
index f4e1a4857..10fe3e24a 100644
--- a/client/dive-common/components/AnnotationVisibilityMenu.vue
+++ b/client/dive-common/components/AnnotationVisibilityMenu.vue
@@ -29,8 +29,12 @@ export default defineComponent({
type: Object as PropType<{ before: number; after: number }>,
required: true,
},
+ showUserCreatedIcon: {
+ type: Boolean,
+ default: true,
+ },
},
- emits: ['set-annotation-state', 'update:tail-settings'],
+ emits: ['set-annotation-state', 'update:tail-settings', 'update:show-user-created-icon'],
setup(props, { emit }) {
const STORAGE_KEY = 'annotationVisibilityMenu.expanded';
@@ -116,6 +120,10 @@ export default defineComponent({
emit('update:tail-settings', settings);
};
+ const toggleShowUserCreatedIcon = () => {
+ emit('update:show-user-created-icon', !props.showUserCreatedIcon);
+ };
+
return {
isExpanded,
viewButtons,
@@ -123,6 +131,7 @@ export default defineComponent({
toggleVisible,
toggleExpanded,
updateTailSettings,
+ toggleShowUserCreatedIcon,
};
},
});
@@ -166,6 +175,7 @@ export default defineComponent({
:color="button.active ? 'grey darken-2' : ''"
class="mx-1 mode-button"
small
+ @click.stop
@click="button.click"
>
{{ button.icon }}
@@ -173,6 +183,16 @@ export default defineComponent({
{{ button.description }}
+
@@ -246,16 +266,53 @@ export default defineComponent({
mdi-chevron-left
-
- {{ button.icon }}
-
+
+
+
+ {{ button.icon }}
+
+
+
+
+
+
+
+ {{ button.icon }}
+
+
-
+
{{ pair[0] }}
@@ -96,9 +100,28 @@ export default defineComponent({
>
{{ pair[1].toFixed(4) }}
+
+
+
+
+ mdi-pencil
+
+
+ This annotation has been modified by a user
+
+
,
default: () => ({ before: 20, after: 10 }),
},
+ showUserCreatedIcon: {
+ type: Boolean,
+ default: true,
+ },
},
- emits: ['set-annotation-state', 'update:tail-settings'],
+ emits: ['set-annotation-state', 'update:tail-settings', 'update:show-user-created-icon'],
setup(props, { emit }) {
const toolTimeTimeout = ref(null);
const STORAGE_KEY = 'editorMenu.editButtonsExpanded';
@@ -325,8 +329,10 @@ export default defineComponent({
diff --git a/client/dive-common/components/TrackDetailsPanel.vue b/client/dive-common/components/TrackDetailsPanel.vue
index 43dabda37..6ed235e5c 100644
--- a/client/dive-common/components/TrackDetailsPanel.vue
+++ b/client/dive-common/components/TrackDetailsPanel.vue
@@ -23,6 +23,7 @@ import {
useSelectedCamera,
} from 'vue-media-annotator/provides';
import { Attribute } from 'vue-media-annotator/use/AttributeTypes';
+import type Track from 'src/track';
import TrackItem from 'vue-media-annotator/components/TrackItem.vue';
import TooltipBtn from 'vue-media-annotator/components/TooltipButton.vue';
import TypePicker from 'vue-media-annotator/components/TypePicker.vue';
@@ -110,14 +111,24 @@ export default defineComponent({
if (multiSelectList.value.length > 0) {
return multiSelectList.value.map(
(trackId) => cameraStore.getAnyPossibleTrack(trackId),
- ).filter((t) => t !== undefined);
+ ).filter((t): t is Track => t !== undefined);
}
if (selectedTrackIdRef.value !== null) {
- return [cameraStore.getAnyTrack(selectedTrackIdRef.value)];
+ const track = cameraStore.getAnyTrack(selectedTrackIdRef.value);
+ return track ? [track] : [];
}
return [];
});
+ const isUserModified = computed(() => {
+ if (selectedTrackList.value.length === 1) {
+ const track = selectedTrackList.value[0];
+ const [feature] = track.getFeature(frameRef.value);
+ return feature?.attributes?.userModified === true;
+ }
+ return false;
+ });
+
function setEditIndividual(attribute: Attribute | null) {
editIndividual.value = attribute;
}
@@ -231,6 +242,14 @@ export default defineComponent({
});
}
+ function setTrackType(type: string) {
+ const track = selectedTrackList.value[0];
+ // Find the confidence value for this type in the track's confidence pairs
+ const existingPair = track.confidencePairs.find(([t]) => t === type);
+ const confidenceVal = existingPair ? existingPair[1] : 1;
+ track.setType(type, confidenceVal);
+ }
+
return {
selectedTrackIdRef,
editingGroupIdRef,
@@ -249,6 +268,7 @@ export default defineComponent({
frameRef,
/* Selected */
selectedTrackList,
+ isUserModified,
multiSelectList,
multiSelectInProgress,
editingMultiTrack,
@@ -270,6 +290,7 @@ export default defineComponent({
toggleMerge,
unstageFromMerge,
updateSelectedTracksType,
+ setTrackType,
};
},
});
@@ -574,7 +595,8 @@ export default defineComponent({
flatten(selectedTrackList.map((t) => t.confidencePairs)).sort((a, b) => b[1] - a[1])
"
:disabled="selectedTrackList.length > 1"
- @set-type="selectedTrackList[0].setType($event)"
+ :user-modified="isUserModified"
+ @set-type="setTrackType($event)"
/>
diff --git a/client/dive-common/store/settings.ts b/client/dive-common/store/settings.ts
index adab2e24f..930bc5379 100644
--- a/client/dive-common/store/settings.ts
+++ b/client/dive-common/store/settings.ts
@@ -97,6 +97,7 @@ const defaultSettings: AnnotationSettings = {
multiBounds: false,
transition: false,
},
+ showUserCreatedIcon: false,
},
timelineCountSettings: {
totalCount: true,
diff --git a/client/dive-common/use/useModeManager.ts b/client/dive-common/use/useModeManager.ts
index aaeb0db6a..fb1333d8b 100644
--- a/client/dive-common/use/useModeManager.ts
+++ b/client/dive-common/use/useModeManager.ts
@@ -400,7 +400,10 @@ export default function useModeManager({
const track = cameraStore.getPossibleTrack(selectedTrackId.value, selectedCamera.value);
if (track) {
// Determines if we are creating a new Detection
- const { interpolate } = track.canInterpolate(frameNum);
+ const { interpolate, features } = track.canInterpolate(frameNum);
+ const [real] = features;
+ // If there's already a keyframe at this frame, we're editing an existing annotation
+ const isEditingExisting = real !== null && real.keyframe;
track.setFeature({
frame: frameNum,
@@ -409,6 +412,11 @@ export default function useModeManager({
keyframe: true,
interpolate: _shouldInterpolate(interpolate),
});
+ // Mark as user-modified if editing existing annotation (as detection attribute)
+ // Skip if track is userCreated (user-created tracks don't need userModified on every detection)
+ if (isEditingExisting && track.attributes?.userCreated !== true) {
+ track.setFeatureAttribute(frameNum, 'userModified', true);
+ }
newTrackSettingsAfterLogic(track);
}
}
@@ -507,6 +515,9 @@ export default function useModeManager({
}
// Update the state of the track in the trackstore.
if (somethingChanged) {
+ // If there's already a keyframe at this frame, we're editing an existing annotation
+ const isEditingExisting = real !== null && real.keyframe;
+
track.setFeature({
frame: frameNum,
flick: flickNum,
@@ -522,6 +533,12 @@ export default function useModeManager({
})),
));
+ // Mark as user-modified if editing existing annotation (as detection attribute)
+ // Skip if track is userCreated (user-created tracks don't need userModified on every detection)
+ if (isEditingExisting && track.attributes?.userCreated !== true) {
+ track.setFeatureAttribute(frameNum, 'userModified', true);
+ }
+
// Only perform "initialization" after the first shape.
// Treat this as a completed annotation if eventType is editing
// Or none of the recieps reported that they were unfinished.
diff --git a/client/src/TrackStore.ts b/client/src/TrackStore.ts
index 860afa252..0378efc2f 100644
--- a/client/src/TrackStore.ts
+++ b/client/src/TrackStore.ts
@@ -13,6 +13,7 @@ export default class TrackStore extends BaseAnnotationStore