Vue 3 Upgrade (#1900)

## Description

Upgrades to Vue 3 and necessary associated dependencies. Also fixes some
issues with the layout and adds validation for object detection models.

Closes #885, closes #1943, closes #1449.
## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Matt M <matthew.morley.ca@gmail.com>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
Co-authored-by: samfreund <techguy763@gmail.com>
This commit is contained in:
Graham
2025-05-06 18:21:41 -04:00
committed by GitHub
parent 29f76bc1c3
commit bec8092660
54 changed files with 1661 additions and 1843 deletions

View File

@@ -218,8 +218,8 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
<v-card-text v-show="!isCalibrating">
<v-card-subtitle class="pt-3 pl-2 pb-4 white--text">Current Calibration</v-card-subtitle>
<v-simple-table fixed-header height="100%" dense>
<v-card-subtitle class="pt-3 pl-2 pb-4 text-white">Current Calibration</v-card-subtitle>
<v-table fixed-header height="100%" density="compact">
<thead>
<tr>
<th>Resolution</th>
@@ -239,20 +239,20 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
<v-icon small class="mr-2">mdi-information</v-icon>
<v-tooltip location="bottom">
<template #activator="{ props }">
<td v-bind="props" @click="setSelectedVideoFormat(value)">
<v-icon size="small" class="mr-2">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
</v-tooltip>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 white--text"
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
@@ -272,7 +272,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)"
@update:modelValue="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)
"
/>
<pv-select
v-model="boardType"
@@ -353,14 +355,18 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
@@ -371,7 +377,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="7"
:step="1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -379,7 +387,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -389,7 +399,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -399,7 +411,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:max="100"
:slider-cols="7"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -409,7 +423,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:max="100"
:slider-cols="7"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
<v-banner
v-if="tooManyPoints"
@@ -423,7 +439,11 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</v-banner>
</v-card-text>
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
<v-chip label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
<v-chip
variant="flat"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'grey-darken-2'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
@@ -431,26 +451,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
small
size="small"
block
color="secondary"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<v-icon start class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
small
size="small"
block
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon left class="calib-btn-icon">
<v-icon start class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
@@ -460,8 +479,15 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</v-col>
</v-card-text>
<v-card-text class="pa-6 pt-0">
<v-btn color="accent" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
<v-btn
color="accent"
size="small"
block
variant="outlined"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</v-card-text>
@@ -507,7 +533,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</div>
<v-card-actions>
<v-spacer />
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
<v-btn v-if="!isCalibrating" color="white" variant="text" @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -518,7 +544,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</template>
<style scoped lang="scss">
.v-data-table {
.v-table {
text-align: center;
width: 100%;

View File

@@ -97,7 +97,7 @@ const calibrationImageURL = (index: number) =>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
<v-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
<v-icon left> mdi-import</v-icon>
<v-icon start> mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -115,7 +115,7 @@ const calibrationImageURL = (index: number) =>
style="width: 100%"
@click="openExportCalibrationPrompt"
>
<v-icon left>mdi-export</v-icon>
<v-icon start>mdi-export</v-icon>
<span>Export</span>
</v-btn>
<a
@@ -126,16 +126,24 @@ const calibrationImageURL = (index: number) =>
/>
</v-col>
</div>
<v-card-title class="pt-0 pb-3"
<v-card-title class="pl-6 pt-0 pb-0"
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
>
<v-card-text v-if="!currentCalibrationCoeffs">
<v-banner rounded color="secondary" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
<v-banner
rounded
bg-color="secondary"
color="secondary"
text-color="white"
class="pt-3 pb-3 mt-3"
density="compact"
icon="mdi-alert-circle-outline"
>
The selected video format has not been calibrated.
</v-banner>
</v-card-text>
<v-card-text>
<v-simple-table dense style="width: 100%">
<v-card-text class="pt-0">
<v-table density="compact" style="width: 100%">
<template #default>
<thead>
<tr>
@@ -238,25 +246,36 @@ const calibrationImageURL = (index: number) =>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0">Individual Observations</v-card-title>
<v-card-text v-if="currentCalibrationCoeffs">
<v-data-table
dense
density="compact"
style="width: 100%"
:headers="[
{ text: 'Observation Id', value: 'index' },
{ text: 'Mean Reprojection Error', value: 'mean' },
{ text: '', value: 'data-table-expand' }
{ title: 'Observation Id', key: 'index' },
{ title: 'Mean Reprojection Error', key: 'mean' },
{ title: '', key: 'data-table-expand' }
]"
:items="getObservationDetails()"
item-key="index"
item-value="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<template #item.data-table-expand="{ internalItem, toggleExpand }">
<v-btn
icon="mdi-eye"
class="text-none"
color="medium-emphasis"
size="small"
variant="text"
slim
@click="toggleExpand(internalItem)"
></v-btn>
</template>
<template #expanded-row="{ columns, item }">
<td :colspan="columns.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div>

View File

@@ -91,16 +91,16 @@ const expanded = ref([]);
</script>
<template>
<v-card dark style="background-color: #006492">
<v-card style="background-color: #006492">
<v-card-title>Camera Control</v-card-title>
<v-card-text>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon left class="open-icon"> mdi-folder </v-icon>
<v-icon start class="open-icon"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-card-text>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>
<v-divider />
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
@@ -110,22 +110,33 @@ const expanded = ref([]);
<v-data-table
v-model:expanded="expanded"
:headers="[
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
{ text: 'Camera Nickname', value: 'cameraNickname' },
{ text: 'Stream Type', value: 'streamType' },
{ text: 'Time Created', value: 'timeCreated' },
{ text: 'Actions', value: 'actions', sortable: false }
{ title: 'Snapshot Name', key: 'snapshotShortName', sortable: false },
{ title: 'Camera Unique Name', key: 'cameraUniqueName' },
{ title: 'Camera Nickname', key: 'cameraNickname' },
{ title: 'Stream Type', key: 'streamType' },
{ title: 'Time Created', key: 'timeCreated' },
{ title: 'Actions', key: 'actions', sortable: false }
]"
:items="imgData"
group-by="cameraUniqueName"
:group-by="[{ key: 'cameraUniqueName' }]"
class="elevation-0"
item-key="index"
item-value="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<template #item.data-table-expand="{ internalItem, toggleExpand }">
<v-btn
icon="mdi-eye"
class="text-none"
color="medium-emphasis"
size="small"
variant="text"
slim
@click="toggleExpand(internalItem)"
></v-btn>
</template>
<template #expanded-row="{ item, columns }">
<td :colspan="columns.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
</div>
@@ -135,7 +146,7 @@ const expanded = ref([]);
<template #item.actions="{ item }">
<div style="display: flex; justify-content: center">
<a :download="item.snapshotName" :href="item.snapshotSrc">
<v-icon small> mdi-download </v-icon>
<v-icon size="small"> mdi-download </v-icon>
</a>
</div>
</template>
@@ -157,7 +168,7 @@ const expanded = ref([]);
.v-btn {
width: 100%;
}
.v-data-table {
.v-table {
text-align: center;
background-color: #006492 !important;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
@@ -203,30 +204,30 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn block small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon left> mdi-content-save </v-icon>
<v-btn block size="small" color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon start> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn block small color="error" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-trash-can-outline </v-icon>
<v-btn block size="small" color="error" @click="() => (showDeleteCamera = true)">
<v-icon start> mdi-trash-can-outline </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" dark width="800">
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
<v-dialog v-model="showDeleteCamera" width="800">
<v-card class="dialog-container pa-3 pb-2" color="primary" flat>
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
<v-card-text>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
<span class="text-white"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<v-icon start class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
@@ -257,7 +258,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:loading="deletingCamera"
@click="deleteThisCamera"
>
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
</v-btn>
</v-card-text>

View File

@@ -6,19 +6,7 @@ import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const value = defineModel<number[]>({ required: true });
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
@@ -52,15 +40,14 @@ const fpsTooLow = computed<boolean>(() => {
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1">
<span class="pr-1" :style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }">
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<v-chip v-else label color="red" variant="text" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1">Camera not connected</span>
</v-chip>
</div>
@@ -98,13 +85,20 @@ const fpsTooLow = computed<boolean>(() => {
</div>
</v-card-text>
<v-card-text class="pt-0">
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
<v-btn-toggle
v-model="value"
:multiple="true"
mandatory
class="fill"
style="width: 100%"
base-color="surface-variant"
>
<v-btn
color="secondary"
class="fill"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn
@@ -112,7 +106,7 @@ const fpsTooLow = computed<boolean>(() => {
class="fill"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
@@ -123,7 +117,6 @@ const fpsTooLow = computed<boolean>(() => {
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;