diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts index 5517d29a8..fd0e068dc 100644 --- a/client/dive-common/apispec.ts +++ b/client/dive-common/apispec.ts @@ -245,9 +245,11 @@ interface DatasetConfigMutable { cameraTransformTypes?: CameraTransformTypes; /** Producer provenance of the camera registration (see RegistrationSource). */ cameraRegistrationSource?: RegistrationSource | null; + /** Frame label mode label set defined by the dataset, in hotkey order. */ + frameLabels?: string[]; error?: string; } -const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource']; +const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource', 'frameLabels']; /** * Mutable keys the multicam/stereo viewer loads from the parent dataset. * Camera-targeted imports sync only these onto the parent — not per-camera diff --git a/client/dive-common/components/FrameLabelPanel.vue b/client/dive-common/components/FrameLabelPanel.vue new file mode 100644 index 000000000..67fe3d833 --- /dev/null +++ b/client/dive-common/components/FrameLabelPanel.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/client/dive-common/components/Viewer.vue b/client/dive-common/components/Viewer.vue index 9e18ebd9c..e0c829605 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,47 @@ export default defineComponent({ alignedView.setSuspended(picking); }, { immediate: true }); + // A dataset may define its own frame label set (e.g. presets applied at + // import); it takes precedence over the user's global label list. + const datasetFrameLabels = ref([] as string[]); + const frameLabels = computed(() => (datasetFrameLabels.value.length + ? datasetFrameLabels.value + : clientSettings.frameLabelSettings.labels)); + const frameLabelMode = useFrameLabelMode({ + cameraStore, + selectedCamera, + labels: frameLabels, + 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 = frameLabels.value.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 @@ -1404,6 +1448,7 @@ export default defineComponent({ resetMulticamAlignment(); } /* Otherwise, complete loading of the dataset */ + datasetFrameLabels.value = meta.frameLabels ?? []; trackStyleManager.populateTypeStyles(meta.customTypeStyling); groupStyleManager.populateTypeStyles(meta.customGroupStyling); if (meta.customTypeStyling) { @@ -1978,6 +2023,11 @@ export default defineComponent({ editingMode, editingDetails, eventChartData, + frameLabelEnabled: frameLabelMode.enabled, + frameLabelMousetrap, + frameLabelActive, + frameLabels, + datasetFrameLabels, groupChartData, imageData, lineChartData, @@ -2310,6 +2360,15 @@ export default defineComponent({ @track-seek="seekToFrame($event)" >