From 121d58240ff06d9a7ef6b7261593cd58f3d13e46 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Wed, 5 Aug 2026 16:02:55 -0400 Subject: [PATCH 1/5] Frame label mode: rapid whole-frame event labeling Hotkeys 1-9 start a full-frame interval track at the current frame with forward-fill until the next event; 0 ends the open segment. Segments are ordinary tracks so timeline, export, and classifier training work unchanged. --- .../components/FrameLabelPanel.vue | 165 ++++++++++++++++++ client/dive-common/components/Viewer.vue | 50 ++++++ client/dive-common/store/settings.ts | 7 + client/src/use/index.ts | 2 + client/src/use/useFrameLabelMode.spec.ts | 132 ++++++++++++++ client/src/use/useFrameLabelMode.ts | 121 +++++++++++++ 6 files changed, 477 insertions(+) create mode 100644 client/dive-common/components/FrameLabelPanel.vue create mode 100644 client/src/use/useFrameLabelMode.spec.ts create mode 100644 client/src/use/useFrameLabelMode.ts diff --git a/client/dive-common/components/FrameLabelPanel.vue b/client/dive-common/components/FrameLabelPanel.vue new file mode 100644 index 000000000..4a8ce24b1 --- /dev/null +++ b/client/dive-common/components/FrameLabelPanel.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/client/dive-common/components/Viewer.vue b/client/dive-common/components/Viewer.vue index 9e18ebd9c..a4fd4fbf7 100644 --- a/client/dive-common/components/Viewer.vue +++ b/client/dive-common/components/Viewer.vue @@ -10,6 +10,7 @@ import { cloneDeep, debounce } from 'lodash'; /* VUE MEDIA ANNOTATOR */ import { useAttributes, + useFrameLabelMode, useImageEnhancements, useLineChart, useTimeObserver, @@ -93,6 +94,7 @@ import MultiCamToolbar from './MultiCamToolbar.vue'; import AlignedViewToggle from './AlignedViewToggle.vue'; import PrimaryAttributeTrackFilter from './PrimaryAttributeTrackFilter.vue'; import UserSettingsDialog from './UserSettingsDialog.vue'; +import FrameLabelPanel from './FrameLabelPanel.vue'; export interface ImageDataItem { url: string; @@ -127,6 +129,7 @@ export default defineComponent({ ConfidenceSubsection, AttributeSubsection, AttributeEditor, + FrameLabelPanel, }, // TODO: remove this in vue 3 @@ -624,6 +627,41 @@ export default defineComponent({ alignedView.setSuspended(picking); }, { immediate: true }); + const frameLabelMode = useFrameLabelMode({ + cameraStore, + selectedCamera, + labels: computed(() => clientSettings.frameLabelSettings.labels), + getMaxFrame: () => aggregateController.value.maxFrame.value, + getFullFrameBounds: () => { + const bounds = aggregateController.value + .getController(selectedCamera.value).originalBounds.value; + return [bounds.left, bounds.top, bounds.right, bounds.bottom]; + }, + }); + const frameLabelMousetrap = computed(() => { + if (!frameLabelMode.enabled.value || readonlyState.value) { + return []; + } + const binds = clientSettings.frameLabelSettings.labels.slice(0, 9) + .map((label, index) => ({ + bind: `${index + 1}`, + handler: () => frameLabelMode.labelFrame(label, time.frame.value), + })); + binds.push({ bind: '0', handler: () => frameLabelMode.endLabel(time.frame.value) }); + return binds; + }); + const frameLabelActive = computed(() => { + if (!frameLabelMode.enabled.value) { + return null; + } + // pendingSaveCount registers annotation edits as a dependency; the + // track stores themselves are not deeply reactive + if (pendingSaveCount.value < 0) { + return null; + } + return frameLabelMode.labelAtFrame(time.frame.value); + }); + /** * Every camera pane calls updateTime() from its own seek/play/pause, but * useTime()'s frame/flick is a single shared value consumed app-wide as @@ -1978,6 +2016,9 @@ export default defineComponent({ editingMode, editingDetails, eventChartData, + frameLabelEnabled: frameLabelMode.enabled, + frameLabelMousetrap, + frameLabelActive, groupChartData, imageData, lineChartData, @@ -2310,6 +2351,13 @@ export default defineComponent({ @track-seek="seekToFrame($event)" >