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

@@ -3,7 +3,7 @@ import { computed, inject, ref, onBeforeUnmount } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import loadingImage from "@/assets/images/loading-transparent.svg";
import type { StyleValue } from "vue/types/jsx";
import type { StyleValue } from "vue";
import PvIcon from "@/components/common/pv-icon.vue";
import type { UiCameraConfiguration } from "@/types/SettingTypes";

View File

@@ -5,7 +5,7 @@ import { useStateStore } from "@/stores/StateStore";
<template>
<v-snackbar
v-model="useStateStore().snackbarData.show"
top
location="top"
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>

View File

@@ -7,13 +7,13 @@ const props = defineProps<{ source: LogMessage }>();
const logColorClass = computed<string>(() => {
switch (props.source.level) {
case LogLevel.ERROR:
return "red--text";
return "text-red";
case LogLevel.WARN:
return "yellow--text";
return "text-yellow";
case LogLevel.INFO:
return "light-blue--text";
return "text-light-blue";
case LogLevel.DEBUG:
return "white--text";
return "text-white";
}
return "";
});

View File

@@ -3,7 +3,7 @@ import { computed, inject, ref, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
import VirtualList from "vue-virtual-scroll-list";
import VirtualList from "vue3-virtual-scroll-list";
const backendHost = inject<string>("backendHost");
@@ -74,15 +74,15 @@ document.addEventListener("keydown", (e) => {
<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card class="dialog-container pa-6" color="primary" flat>
<!-- Logs header -->
<v-row class="no-gutters pb-3">
<v-row class="pb-3">
<v-col cols="4">
<v-card-title class="pa-0">Program Logs</v-card-title>
</v-col>
<v-col class="align-self-center pl-3" style="text-align: right">
<v-btn text color="white" @click="handleLogExport">
<v-icon left class="menu-icon"> mdi-download </v-icon>
<v-btn variant="text" color="white" @click="handleLogExport">
<v-icon start class="menu-icon"> mdi-download </v-icon>
<span class="menu-label">Download</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -94,12 +94,12 @@ document.addEventListener("keydown", (e) => {
target="_blank"
/>
</v-btn>
<v-btn text color="white" @click="handleLogClear">
<v-icon left class="menu-icon"> mdi-trash-can-outline </v-icon>
<v-btn variant="text" color="white" @click="handleLogClear">
<v-icon start class="menu-icon"> mdi-trash-can-outline </v-icon>
<span class="menu-label">Clear Client Logs</span>
</v-btn>
<v-btn text color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon left class="menu-icon"> mdi-close </v-icon>
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon start class="menu-icon"> mdi-close </v-icon>
<span class="menu-label">Close</span>
</v-btn>
</v-col>
@@ -109,38 +109,28 @@ document.addEventListener("keydown", (e) => {
<div class="dialog-data">
<!-- Log view options -->
<v-row class="pt-4 pt-md-0 no-gutters">
<v-col cols="12" md="5" class="align-self-center">
<v-row no-gutters class="pt-4 pt-md-0" style="display: flex; justify-content: space-between">
<v-col cols="12" md="7" style="display: flex; align-items: center">
<v-text-field
v-model="searchQuery"
dark
dense
density="compact"
clearable
hide-details="auto"
prepend-icon="mdi-magnify"
color="accent"
label="Search"
variant="underlined"
/>
</v-col>
<v-col cols="12" md="2" style="display: flex; align-items: center">
<input v-model="timeInput" type="time" step="1" class="white--text pl-0 pl-md-8" />
<v-btn icon class="ml-3" @click="timeInput = undefined">
<input v-model="timeInput" type="time" step="1" class="text-white pl-3" />
<v-btn icon variant="flat" @click="timeInput = undefined">
<v-icon>mdi-close-circle-outline</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="5" class="pr-3">
<v-row class="no-gutters">
<v-col v-for="level in [0, 1, 2, 3]" :key="level">
<v-row dense align="center">
<v-col cols="6" md="8" style="text-align: right">
{{ getLogLevelFromIndex(level) }}
</v-col>
<v-col cols="6" md="4">
<v-switch v-model="selectedLogLevels[level]" dark color="#ffd843" />
</v-col>
</v-row>
</v-col>
</v-row>
<v-col v-for="level in [0, 1, 2, 3]" :key="level" class="pr-3">
<div class="pb-0 pt-0" style="display: flex; align-items: center; flex: min-content">
{{ getLogLevelFromIndex(level)
}}<v-switch v-model="selectedLogLevels[level]" class="pl-2" hide-details color="#ffd843"></v-switch>
</div>
</v-col>
</v-row>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
import { useRoute } from "vue2-helpers/vue-router";
import { useRoute } from "vue-router";
import { useDisplay } from "vuetify";
const compact = computed<boolean>({
get: () => {
@@ -14,9 +15,9 @@ const compact = computed<boolean>({
useStateStore().setSidebarFolded(val);
}
});
const { mdAndUp } = useDisplay();
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
const needsCamerasConfigured = computed<boolean>(() => {
return (
@@ -27,115 +28,99 @@ const needsCamerasConfigured = computed<boolean>(() => {
</script>
<template>
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
<v-list>
<v-navigation-drawer permanent :rail="renderCompact" color="primary">
<v-list nav>
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<v-list-item-icon class="mr-0">
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<template #prepend>
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
</v-list-item-icon>
</template>
</v-list-item>
<v-list-item link to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/dashboard" prepend-icon="mdi-view-dashboard">
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item>
<v-list-item link to="/settings">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/settings" prepend-icon="mdi-cog">
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-list-item ref="camerasTabOpener" link to="/cameras">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Camera</v-list-item-title>
</v-list-item-content>
<v-list-item ref="camerasTabOpener" link to="/cameras" prepend-icon="mdi-camera">
<v-list-item-title>Camera</v-list-item-title>
</v-list-item>
<v-list-item
link
to="/cameraConfigs"
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
>
<v-list-item-icon>
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
</v-list-item-content>
<template #prepend>
<v-icon :class="{ 'text-red': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
</template>
<v-list-item-title :class="{ 'text-red': needsCamerasConfigured }">Camera Matching</v-list-item-title>
</v-list-item>
<v-list-item link to="/docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/docs" prepend-icon="mdi-bookshelf">
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item>
<div style="position: absolute; bottom: 0; left: 0">
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
<v-list-item-icon>
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
<v-icon v-else> mdi-chevron-left </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
NetworkTables server running for
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
class="text-wrap"
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="accent--text">
{{ useStateStore().ntConnectionStatus.address }}
</span>
</v-list-item-title>
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</v-list>
<template #append>
<v-list nav>
<v-list-item
v-if="mdAndUp"
link
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
@click="() => (compact = !compact)"
>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item>
<v-list-item
:prepend-icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" v-show="!renderCompact" class="text-wrap">
NetworkTables server running for
<span class="text-accent">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
v-show="!renderCompact"
class="text-wrap"
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="text-accent">
{{ useStateStore().ntConnectionStatus.address }}
</span>
</v-list-item-title>
<v-list-item-title
v-else
v-show="!renderCompact"
class="text-wrap"
style="flex-direction: column; display: flex"
>
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item>
<v-list-item :prepend-icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'">
<v-list-item-title v-show="!renderCompact" class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
</v-list-item-title>
</v-list-item>
</v-list>
</template>
</v-navigation-drawer>
</template>
<style scoped>
.v-list-item-title {
font-size: 1rem !important;
line-height: 1.2rem !important;
}
.logo {
width: 100%;
height: 70px;

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%;

View File

@@ -24,7 +24,7 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<template>
<div>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
<td>Device Number:</td>
@@ -66,6 +66,6 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</div>
</template>

View File

@@ -29,7 +29,7 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
<template>
<div>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr>
<th></th>
@@ -112,7 +112,7 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
<td>{{ cameraInfoFor(current).otherPaths }}</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const props = withDefaults(
withDefaults(
defineProps<{
iconName: string;
disabled?: boolean;
@@ -18,20 +18,17 @@ const props = withDefaults(
defineEmits<{
(e: "click"): void;
}>();
const hoverClass = props.hover ? "hover" : "";
</script>
<template>
<div>
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
<template #activator="{ on, attrs }">
<v-tooltip :right="right" :location="!right ? 'bottom' : undefined" offset="10" :disabled="tooltip === undefined">
<template #activator="{ props }">
<v-icon
:class="hoverClass"
:class="hover ? 'hover' : ''"
:color="color"
v-bind="attrs"
v-bind="props"
:disabled="disabled"
v-on="on"
@click="$emit('click')"
>
{{ iconName }}

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<string>({ required: true });
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: string;
disabled?: boolean;
errorMessage?: string;
placeholder?: string;
@@ -22,23 +21,17 @@ const props = withDefaults(
);
const emit = defineEmits<{
(e: "input", value: string): void;
(e: "onEnter", value: string): void;
(e: "onEscape"): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const handleKeydown = ({ key }) => {
switch (key) {
case "Enter":
// Explicitly check that all rule props return true
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
if (!props.rules?.every((rule) => rule(value.value) === true)) return;
emit("onEnter", localValue.value);
emit("onEnter", value.value);
break;
case "Escape":
emit("onEscape");
@@ -55,9 +48,8 @@ const handleKeydown = ({ key }) => {
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-text-field
v-model="localValue"
dark
dense
v-model="value"
density="compact"
color="accent"
:placeholder="placeholder"
:disabled="disabled"
@@ -65,6 +57,7 @@ const handleKeydown = ({ key }) => {
:rules="rules"
hide-details="auto"
class="light-error"
variant="underlined"
@keydown="handleKeydown"
/>
</v-col>
@@ -75,9 +68,3 @@ const handleKeydown = ({ key }) => {
margin-top: 0px;
}
</style>
<style>
.light-error .error--text {
color: red !important;
caret-color: red !important;
}
</style>

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
const value = defineModel<number>({
required: true
});
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
labelCols?: number;
rules?: ((v: number) => boolean | string)[];
@@ -20,13 +20,9 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", parseFloat(v as unknown as string))
get: () => value.value,
set: (v) => (value.value = parseFloat(v as unknown as string))
});
</script>
@@ -38,12 +34,13 @@ const localValue = computed({
<v-col class="pr-0">
<v-text-field
v-model="localValue"
dark
class="mt-0 pt-0"
density="compact"
hide-details
single-line
color="accent"
type="number"
variant="underlined"
style="width: 70px"
:step="step"
:disabled="disabled"

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<number>({
required: true
});
const props = withDefaults(
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
inputCols?: number;
list: string[];
@@ -17,15 +17,6 @@ const props = withDefaults(
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
@@ -34,13 +25,14 @@ const localValue = computed({
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-radio-group v-model="localValue" row dark :mandatory="true" hide-details="auto">
<v-radio-group v-model="value" row:mandatory="true" hide-details="auto">
<v-radio
v-for="(radioName, index) in list"
:key="index"
:value="index"
color="#ffd843"
:label="radioName"
:value="index"
:model-value="index"
:disabled="disabled"
/>
</v-radio-group>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
const value = defineModel<[number, number] | WebsocketNumberPair>({
required: true
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
value: [number, number];
min: number;
max: number;
step?: number;
@@ -24,19 +25,15 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: [number, number]): void;
}>();
const localValue = computed<[number, number]>({
get: (): [number, number] => {
return Object.values(props.value) as [number, number];
return Object.values(value.value) as [number, number];
},
set: (v) => {
for (let i = 0; i < v.length; i++) {
v[i] = parseFloat(v[i] as unknown as string);
}
emit("input", v);
value.value = v;
}
});
@@ -69,8 +66,7 @@ const checkNumberRange = (v: string): boolean => {
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
class="align-center ml-0 mr-0"
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
@@ -78,36 +74,38 @@ const checkNumberRange = (v: string): boolean => {
>
<template #prepend>
<v-text-field
:value="localValue[0]"
dark
:model-value="localValue[0]"
color="accent"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
variant="underlined"
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
@update:modelValue="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
<v-text-field
:value="localValue[1]"
dark
:model-value="localValue[1]"
color="accent"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
variant="underlined"
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"
@update:modelValue="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>

View File

@@ -7,14 +7,13 @@ export interface SelectItem {
value: string | number;
disabled?: boolean;
}
const value = defineModel<string | number | undefined>({ required: true });
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
selectCols?: number;
// TODO fully update v-model usage in custom components on Vue3 update
value: any;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
}>(),
@@ -24,15 +23,6 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: string): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
// Trivial case for empty list; we have no data
@@ -55,16 +45,15 @@ const items = computed<SelectItem[]>(() => {
</v-col>
<v-col :cols="selectCols" class="d-flex align-center pr-0">
<v-select
v-model="localValue"
v-model="value"
:items="items"
item-text="name"
item-title="name"
item-value="value"
item-disabled="disabled"
dark
color="accent"
item-color="secondary"
item-props.disabled="disabled"
:disabled="disabled"
hide-details="auto"
variant="underlined"
density="compact"
/>
</v-col>
</div>

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
modelValue: number;
min: number;
max: number;
step?: number;
@@ -20,9 +19,8 @@ const props = withDefaults(
sliderCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
(e: "update:modelValue", value: number): void;
}>();
// Debounce function
@@ -35,11 +33,11 @@ function debounce(func: (...args: any[]) => void, wait: number) {
}
const debouncedEmit = debounce((v: number) => {
emit("input", v);
emit("update:modelValue", v);
}, 20);
const localValue = computed({
get: () => props.value,
get: () => props.modelValue,
set: (v) => debouncedEmit(parseFloat(v as unknown as string))
});
</script>
@@ -49,10 +47,9 @@ const localValue = computed({
<v-col :cols="12 - sliderCols" class="pl-0 d-flex align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols - 1">
<v-col :cols="sliderCols - 1" class="pl-0">
<v-slider
v-model="localValue"
dark
class="align-center"
:max="max"
:min="min"
@@ -68,16 +65,17 @@ const localValue = computed({
</v-col>
<v-col :cols="1" class="pr-0">
<v-text-field
:value="localValue"
dark
:model-value="localValue"
color="accent"
:max="max"
:min="min"
:disabled="disabled"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
type="number"
variant="underlined"
style="width: 100%"
:step="step"
:hide-spin-buttons="true"

View File

@@ -1,43 +1,30 @@
<script setup lang="ts">
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
const value = defineModel<boolean>();
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: boolean;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
dense?: boolean;
}>(),
{
disabled: false,
labelCols: 2,
switchCols: 8,
dense: false
switchCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: boolean): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
<div class="d-flex">
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0">
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0 pt-2 pb-2">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0 pt-2 pb-2">
<v-switch v-model="value" :disabled="disabled" color="#ffd843" hide-details density="compact" />
</v-col>
</div>
</template>

View File

@@ -7,9 +7,9 @@ defineProps<{
<template>
<div>
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
<template #activator="{ on, attrs }">
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
<v-tooltip :disabled="tooltip === undefined" location="right" open-delay="300">
<template #activator="{ props }">
<span style="cursor: text !important" class="text-white" v-bind="props">{{ label }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>

View File

@@ -249,7 +249,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
:items="wrappedCameras"
@input="changeCurrentCameraUniqueName"
@update:modelValue="changeCurrentCameraUniqueName"
/>
<pv-input
v-else
@@ -270,7 +270,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:disabled="checkCameraName(currentCameraName) !== true"
@click="() => saveCameraNameEdit(currentCameraName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelCameraNameEdit" />
</div>
<pv-icon
v-else
@@ -285,7 +285,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isPipelineNameEdit"
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
:model-value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
label="Pipeline"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="
@@ -294,7 +294,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
!useCameraSettingsStore().hasConnected
"
:items="pipelineNamesWrapper"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
/>
<pv-input
v-else
@@ -314,13 +314,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:disabled="checkPipelineName(currentPipelineName) !== true"
@click="() => savePipelineNameEdit(currentPipelineName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelPipelineNameEdit" />
</div>
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
<template #activator="{ on }">
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset="7">
<template #activator="{ props }">
<v-icon color="#c5c5c5" v-bind="props" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
</template>
<v-list dark dense color="primary">
<v-list density="compact" color="primary">
<v-list-item @click="startPipelineNameEdit">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
@@ -333,7 +333,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<pv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
<pv-icon color="red-darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
@@ -365,12 +365,12 @@ const wrappedCameras = computed<SelectItem[]>(() =>
!useCameraSettingsStore().hasConnected
"
:items="pipelineTypesWrapper"
@input="showPipelineTypeChangeDialog = true"
@update:modelValue="showPipelineTypeChangeDialog = true"
/>
</v-col>
</v-row>
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
<v-card dark color="primary">
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
<v-card color="primary">
<v-card-title> Create New Pipeline </v-card-title>
<v-card-text>
<pv-input
@@ -394,18 +394,18 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-spacer />
<v-btn
color="#ffd843"
class="black--text"
:disabled="checkPipelineName(newPipelineName) !== true"
variant="flat"
@click="createNewPipeline"
>
Save
</v-btn>
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
<v-btn color="error" variant="elevated" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
<v-card dark color="primary">
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
<v-card color="primary">
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
<v-card-text>
Are you sure you want to delete the pipeline
@@ -417,8 +417,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
<v-btn variant="flat" color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn
variant="flat"
color="#ffd843"
class="text-black"
@click="showPipelineDeletionConfirmationDialog = false"
>
No, take me back
</v-btn>
</v-card-actions>
@@ -435,8 +440,10 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
<v-btn color="error" variant="elevated" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" variant="elevated" class="text-black" @click="cancelChangePipelineType">
No, take me back
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -6,10 +6,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const value = defineModel<number[]>();
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
@@ -43,20 +40,21 @@ const performanceRecommendation = computed<string>(() => {
<template>
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
<v-card-title class="justify-space-between align-center pt-3 pb-3">
<v-card-title class="justify-space-between align-center pt-1 pb-1 d-flex">
<span>Cameras</span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
:color="fpsTooLow ? 'error' : ''"
style="font-size: 1rem; padding: 0; margin: 0"
:variant="fpsTooLow ? 'tonal' : 'text'"
:style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</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 variant="text" color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1"> Camera not connected </span>
</v-chip>
<v-switch
@@ -69,7 +67,7 @@ const performanceRecommendation = computed<string>(() => {
</v-card-title>
<v-divider class="ml-3 mr-3" />
<v-row class="stream-viewer-container pa-3 align-center">
<v-col v-if="value.includes(0)" class="stream-view">
<v-col v-if="value?.includes(0)" class="stream-view">
<photon-camera-stream
id="input-camera-stream"
:camera-settings="useCameraSettingsStore().currentCameraSettings"
@@ -77,7 +75,7 @@ const performanceRecommendation = computed<string>(() => {
style="width: 100%; height: auto"
/>
</v-col>
<v-col v-if="value.includes(1)" class="stream-view">
<v-col v-if="value?.includes(1)" class="stream-view">
<photon-camera-stream
id="output-camera-stream"
:camera-settings="useCameraSettingsStore().currentCameraSettings"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Component } from "vue";
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
import { computed, ref } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
@@ -14,6 +14,7 @@ import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
interface ConfigOption {
tabName: string;
@@ -64,15 +65,12 @@ const allTabs = Object.freeze({
});
const selectedTabs = ref([0, 0, 0, 0]);
const getTabGroups = (): ConfigOption[][] => {
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
const { smAndDown, mdAndDown, lgAndDown, xl } = useDisplay();
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
const getTabGroups = (): ConfigOption[][] => {
if (smAndDown.value || useCameraSettingsStore().isDriverMode) {
return [Object.values(allTabs)];
} else if (mdAndDown || !useStateStore().sidebarFolded) {
} else if (mdAndDown.value || !useStateStore().sidebarFolded) {
return [
[
allTabs.inputTab,
@@ -85,7 +83,7 @@ const getTabGroups = (): ConfigOption[][] => {
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (lgAndDown) {
} else if (lgAndDown.value) {
return [
[allTabs.inputTab],
[
@@ -98,7 +96,7 @@ const getTabGroups = (): ConfigOption[][] => {
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (xl) {
} else if (xl.value) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
@@ -135,12 +133,12 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.filter((it) => it.length); // Remove empty tab groups
});
onBeforeUpdate(() => {
const onBeforeTabUpdate = () => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
selectedTabs.value[0] = 0;
}
});
};
</script>
<template>
@@ -148,7 +146,7 @@ onBeforeUpdate(() => {
<template v-if="!useCameraSettingsStore().hasConnected">
<v-col cols="12">
<v-card color="error">
<v-card-title class="white--text">
<v-card-title class="text-white">
Camera has not connected. Please check your connection and try again.
</v-card-title>
</v-card>
@@ -158,17 +156,12 @@ onBeforeUpdate(() => {
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>
<v-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs
v-model="selectedTabs[tabGroupIndex]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
>
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="primary" height="48" slider-color="accent">
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>

View File

@@ -3,19 +3,7 @@ import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
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[]>();
const processingMode = computed<number>({
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
@@ -32,14 +20,18 @@ const processingMode = computed<number>({
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
class="mt-3"
color="primary"
style="height: 100%; display: flex; flex-direction: column"
style="flex-grow: 1; display: flex; flex-direction: column"
>
<v-row class="pa-3 pb-0 align-center">
<v-col class="pa-4">
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
<v-icon left>mdi-square-outline</v-icon>
<v-btn-toggle v-model="processingMode" mandatory base-color="surface-variant" class="fill w-100">
<v-btn
color="secondary"
:disabled="!useCameraSettingsStore().hasConnected"
class="w-50"
prepend-icon="mdi-square-outline"
>
<span>2D</span>
</v-btn>
<v-btn
@@ -47,8 +39,9 @@ const processingMode = computed<number>({
:disabled="
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
"
class="w-50"
prepend-icon="mdi-cube-outline"
>
<v-icon left>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
</v-btn-toggle>
@@ -57,13 +50,13 @@ const processingMode = computed<number>({
<v-row class="pa-3 pt-0 align-center">
<v-col class="pa-4 pt-0">
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<v-btn-toggle v-model="value" :multiple="true" mandatory base-color="surface-variant" class="fill w-100">
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
@@ -73,14 +66,8 @@ const processingMode = computed<number>({
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
.v-btn--disabled {
background-color: #191919 !important;
}
th {

View File

@@ -3,22 +3,20 @@ import { PipelineType } from "@/types/PipelineTypes";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
label="Target family"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decimate"
@@ -38,7 +36,7 @@ const interactiveCols = computed(() =>
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.blur"
@@ -48,7 +46,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="5"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.threads"
@@ -57,7 +55,7 @@ const interactiveCols = computed(() =>
tooltip="Number of threads spawned by the AprilTag detector"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decisionMargin"
@@ -66,7 +64,9 @@ const interactiveCols = computed(() =>
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
:min="0"
:max="250"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.numIterations"
@@ -75,14 +75,18 @@ const interactiveCols = computed(() =>
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
:min="0"
:max="500"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.refineEdges"
:switch-cols="interactiveCols"
label="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)
"
/>
</div>
</template>

View File

@@ -5,20 +5,18 @@ import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-range-slider
v-model="currentPipelineSettings.threshWinSizes"
@@ -39,7 +37,9 @@ const interactiveCols = computed(() =>
:max="255"
:slider-cols="interactiveCols"
:step="2"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.threshStepSize"
@@ -49,7 +49,9 @@ const interactiveCols = computed(() =>
:min="2"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.threshConstant"
@@ -59,21 +61,27 @@ const interactiveCols = computed(() =>
:min="0"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.useCornerRefinement"
label="Refine Corners"
tooltip="Further refine the initial corners with subpixel accuracy."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.debugThreshold"
label="Debug Threshold"
tooltip="Display the first threshold step to the color stream."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)
"
/>
</div>
</template>

View File

@@ -4,8 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -48,12 +49,9 @@ const contourRadius = computed<[number, number]>({
}
}
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -65,7 +63,7 @@ const interactiveCols = computed(() =>
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
@@ -75,7 +73,9 @@ const interactiveCols = computed(() =>
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
<pv-range-slider
v-model="contourArea"
@@ -84,7 +84,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
@@ -95,7 +97,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
@@ -105,7 +109,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)
"
/>
<pv-range-slider
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
@@ -115,7 +121,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="4000"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
@@ -124,7 +132,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
@@ -137,7 +145,9 @@ const interactiveCols = computed(() =>
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.contourFilterRangeY"
@@ -147,7 +157,9 @@ const interactiveCols = computed(() =>
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
@@ -155,7 +167,9 @@ const interactiveCols = computed(() =>
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="interactiveCols"
:items="['Single', 'Dual', 'Two or More']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
@@ -164,7 +178,9 @@ const interactiveCols = computed(() =>
:select-cols="interactiveCols"
:items="['None', 'Up', 'Down', 'Left', 'Right']"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
@@ -174,7 +190,9 @@ const interactiveCols = computed(() =>
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape >= 1"
@@ -185,7 +203,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -196,7 +216,7 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
@@ -208,7 +228,9 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -218,7 +240,9 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)
"
/>
<pv-range-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -228,7 +252,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)
"
/>
</template>
</div>

View File

@@ -3,10 +3,11 @@ import PvSlider from "@/components/common/pv-slider.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { getResolutionString } from "@/lib/PhotonUtils";
import { useDisplay } from "vuetify";
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
const cameraRotations = computed(() =>
@@ -62,12 +63,10 @@ const handleStreamResolutionChange = (value: number) => {
false
);
};
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -75,11 +74,12 @@ const interactiveCols = computed(() =>
<div>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:switch-cols="interactiveCols"
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"
@@ -90,7 +90,9 @@ const interactiveCols = computed(() =>
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="interactiveCols"
:step="1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -98,7 +100,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -108,7 +112,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -118,7 +122,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
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"
@@ -128,14 +134,18 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
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)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
label="Auto White Balance"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraWhiteBalanceTemp"
@@ -144,7 +154,9 @@ const interactiveCols = computed(() =>
:min="useCameraSettingsStore().minWhiteBalanceTemp"
:max="useCameraSettingsStore().maxWhiteBalanceTemp"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
@@ -152,7 +164,9 @@ const interactiveCols = computed(() =>
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
:items="cameraRotations"
:select-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
@@ -160,7 +174,7 @@ const interactiveCols = computed(() =>
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
@input="(args) => handleResolutionChange(args)"
@update:modelValue="(args) => handleResolutionChange(args)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
@@ -168,7 +182,7 @@ const interactiveCols = computed(() =>
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@input="(args) => handleStreamResolutionChange(args)"
@update:modelValue="(args) => handleStreamResolutionChange(args)"
/>
<pv-switch
v-if="useCameraSettingsStore().isDriverMode"
@@ -176,7 +190,7 @@ const interactiveCols = computed(() =>
label="Crosshair"
:switch-cols="interactiveCols"
tooltip="Enables or disables a crosshair overlay on the camera stream"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
/>
</div>
</template>

View File

@@ -11,7 +11,7 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
<div>
<v-row style="width: 100%">
<v-col>
<span class="white--text">Target Visualization</span>
<span class="text-white">Target Visualization</span>
</v-col>
</v-row>
<v-row style="width: 100%">

View File

@@ -2,9 +2,12 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -22,11 +25,10 @@ const contourRatio = computed<[number, number]>({
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
// Filters out models that are not supported by the current backend, and returns a flattened list.
@@ -36,9 +38,12 @@ const supportedModels = computed(() => {
});
const selectedModel = computed({
get: () => supportedModels.value.indexOf(currentPipelineSettings.value.model),
get: () => {
const index = supportedModels.value.indexOf(currentPipelineSettings.value.model);
return index === -1 ? undefined : index;
},
set: (v) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false);
v && useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false);
}
});
</script>
@@ -61,7 +66,9 @@ const selectedModel = computed({
:min="0"
:max="1"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)
"
/>
<pv-range-slider
v-model="contourArea"
@@ -70,7 +77,9 @@ const selectedModel = computed({
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
"
/>
<pv-range-slider
v-model="contourRatio"
@@ -80,7 +89,9 @@ const selectedModel = computed({
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
@@ -88,7 +99,7 @@ const selectedModel = computed({
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
@@ -98,7 +109,9 @@ const selectedModel = computed({
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
</div>
</template>

View File

@@ -3,9 +3,10 @@ import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
const isTagPipeline = computed(
() =>
@@ -45,12 +46,10 @@ const offsetPoints = computed<MetricItem[]>(() => {
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -62,7 +61,7 @@ const interactiveCols = computed(() =>
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
:disabled="isTagPipeline"
:switch-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
"
/>
@@ -78,7 +77,9 @@ const interactiveCols = computed(() =>
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)
"
/>
<pv-switch
v-if="
@@ -92,7 +93,9 @@ const interactiveCols = computed(() =>
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
@@ -100,7 +103,7 @@ const interactiveCols = computed(() =>
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
"
/>
@@ -111,7 +114,7 @@ const interactiveCols = computed(() =>
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
@@ -121,7 +124,9 @@ const interactiveCols = computed(() =>
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="['None', 'Single Point', 'Dual Point']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
"
/>
<table
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
@@ -148,10 +153,10 @@ const interactiveCols = computed(() =>
>
<v-col cols="6" class="pl-0">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
class="text-black"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -159,9 +164,9 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="6" class="pr-0">
<v-btn
small
size="small"
block
color="yellow darken-3"
color="yellow-darken-3"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -174,10 +179,10 @@ const interactiveCols = computed(() =>
>
<v-col cols="6" lg="4" class="pl-0 pr-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
class="text-black"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -185,10 +190,10 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="6" lg="4" class="pl-2 pr-0 pr-lg-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
class="text-black"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -196,9 +201,9 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="12" lg="4" class="pl-0 pl-lg-2 pr-0">
<v-btn
small
size="small"
block
color="yellow darken-3"
color="yellow-darken-3"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points

View File

@@ -3,14 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { TargetModel } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
</script>
@@ -30,7 +29,9 @@ const interactiveCols = computed(() =>
{ name: '2025 Algae (16.25in)', value: TargetModel.ReefscapeAlgae }
]"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
@@ -39,7 +40,7 @@ const interactiveCols = computed(() =>
label="Contour simplification Percentage"
:min="0"
:max="100"
@input="
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
"

View File

@@ -34,8 +34,8 @@ const resetCurrentBuffer = () => {
<template>
<div>
<v-row align="start" class="pb-4">
<v-simple-table dense class="pt-2 pb-12">
<v-row class="pb-4">
<v-table density="compact" class="pt-2 pb-12 pl-3 pr-3">
<template #default>
<thead>
<tr>
@@ -44,24 +44,24 @@ const resetCurrentBuffer = () => {
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco
"
class="text-center white--text"
class="text-center text-white"
>
Fiducial ID
</th>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<th class="text-center white--text">Class</th>
<th class="text-center white--text">Confidence</th>
<th class="text-center text-white">Class</th>
<th class="text-center text-white">Confidence</th>
</template>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center white--text">Pitch &theta;&deg;</th>
<th class="text-center white--text">Yaw &theta;&deg;</th>
<th class="text-center white--text">Skew &theta;&deg;</th>
<th class="text-center white--text">Area %</th>
<th class="text-center text-white">Pitch &theta;&deg;</th>
<th class="text-center text-white">Yaw &theta;&deg;</th>
<th class="text-center text-white">Skew &theta;&deg;</th>
<th class="text-center text-white">Area %</th>
</template>
<template v-else>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
</template>
<template
v-if="
@@ -70,7 +70,7 @@ const resetCurrentBuffer = () => {
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<th class="text-center white--text">Ambiguity Ratio</th>
<th class="text-center text-white">Ambiguity Ratio</th>
</template>
</tr>
</thead>
@@ -78,7 +78,7 @@ const resetCurrentBuffer = () => {
<tr
v-for="(target, index) in useStateStore().currentPipelineResults?.targets"
:key="index"
class="white--text"
class="text-white"
>
<td
v-if="
@@ -91,13 +91,13 @@ const resetCurrentBuffer = () => {
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
class="text-center text-white"
>
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
class="text-center text-white"
>
{{ target.confidence.toFixed(2) }}
</td>
@@ -126,7 +126,7 @@ const resetCurrentBuffer = () => {
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
<v-container
v-if="
@@ -136,122 +136,123 @@ const resetCurrentBuffer = () => {
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
class="pl-3 pr-3"
>
<v-row class="pb-4 white--text">
<v-row class="pb-4 text-white">
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px"
>Multi-tag pose, field-to-camera</v-card-subtitle
>
<v-simple-table dense>
<v-table density="compact">
<template #default>
<thead>
<tr class="white--text">
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center white--text">Tags</th>
<tr class="text-white">
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z meters</th>
<th class="text-center text-white">X Angle &theta;&deg;</th>
<th class="text-center text-white">Y Angle &theta;&deg;</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
<th class="text-center text-white">Tags</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.z.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_x || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_y || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
<v-row class="pb-4 white--text" style="display: flex; flex-direction: column">
<v-row class="pb-4 text-white" style="display: flex; flex-direction: column">
<v-card-subtitle class="ma-0 pa-0 pb-4 pr-4" style="font-size: 16px"
>Multi-tag pose standard deviation over the last
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
</v-card-subtitle>
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" depressed @click="resetCurrentBuffer"
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" variant="flat" @click="resetCurrentBuffer"
>Reset Samples</v-btn
>
<v-simple-table dense>
<v-table density="compact">
<template #default>
<thead>
<tr>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z meters</th>
<th class="text-center text-white">X Angle &theta;&deg;</th>
<th class="text-center text-white">Y Angle &theta;&deg;</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.x) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.y) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.z) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_x)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_y)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_z)) || []
@@ -261,14 +262,14 @@ const resetCurrentBuffer = () => {
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
</v-container>
</div>
</template>
<style scoped lang="scss">
.v-data-table {
.v-table {
background-color: #006492 !important;
width: 100%;
font-size: 1rem !important;

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
import { computed, onBeforeUnmount, onMounted } from "vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { useStateStore } from "@/stores/StateStore";
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
import { useDisplay } from "vuetify";
const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
@@ -123,12 +124,10 @@ onBeforeUnmount(() => {
cameraStream.removeEventListener("click", handleStreamClick);
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
</script>
@@ -144,7 +143,7 @@ const interactiveCols = computed(() =>
:max="180"
:slider-cols="interactiveCols"
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
/>
<pv-range-slider
id="sat-slider"
@@ -155,7 +154,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)
"
/>
<pv-range-slider
id="value-slider"
@@ -166,53 +167,55 @@ const interactiveCols = computed(() =>
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
label="Invert Hue"
:switch-cols="interactiveCols"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)
"
/>
<div>
<div class="white--text pt-3">Color Picker</div>
<div class="text-white pt-3">Color Picker</div>
<div class="d-flex pt-3">
<template v-if="!useStateStore().colorPickingMode">
<v-col cols="4" class="pl-0 pr-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
class="text-black"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left> mdi-minus </v-icon>
<v-icon start> mdi-minus </v-icon>
Shrink Range
</v-btn>
</v-col>
<v-col cols="4" class="pl-0 pr-0">
<v-btn color="accent" class="black--text" small block @click="enableColorPicking(1)">
<v-icon left> mdi-plus-minus </v-icon>
<v-btn color="accent" class="text-black" size="small" block @click="enableColorPicking(1)">
<v-icon start> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
</v-col>
<v-col cols="4" class="pl-2 pr-0">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
class="text-black"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left> mdi-plus </v-icon>
<v-icon start> mdi-plus </v-icon>
Expand Range
</v-btn>
</v-col>
</template>
<template v-else>
<v-card-text class="pa-0 pt-3 pb-3">
<v-btn block color="accent" class="black--text" small @click="disableColorPicking"> Cancel </v-btn>
<v-btn block color="accent" class="text-black" size="small" @click="disableColorPicking"> Cancel </v-btn>
</v-card-text>
</template>
</div>
@@ -224,32 +227,32 @@ const interactiveCols = computed(() =>
.threshold-modifiers {
--averageHue: 0;
}
#hue-slider >>> .v-slider {
#hue-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#sat-slider >>> .v-slider {
#sat-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#value-slider >>> .v-slider {
#value-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {
:deep(.v-slider__thumb) {
outline: black solid thin;
}
.normal-slider >>> .v-slider__track-fill {
.normal-slider:deep(.v-slider__track-fill) {
outline: black solid thin;
}
.inverted-slider >>> .v-slider__track-background {
.inverted-slider:deep(.v-slider__track-background) {
outline: black solid thin;
}
</style>

View File

@@ -17,14 +17,14 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
</script>
<template>
<v-card dark style="background-color: #006492">
<v-card style="background-color: #006492">
<v-card-title class="pa-6">AprilTag Field Layout</v-card-title>
<v-card-text class="pa-6 pt-0">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-simple-table fixed-header height="100%" dense dark>
<v-table fixed-header height="100%" density="compact" dark>
<template #default>
<thead style="font-size: 1.25rem">
<tr>
@@ -47,13 +47,13 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-card-text>
</v-card>
</template>
<style scoped lang="scss">
.v-data-table {
.v-table {
width: 100%;
height: 100%;
text-align: center;

View File

@@ -2,6 +2,7 @@
import { inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvSelect from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import axios from "axios";
const restartProgram = () => {
@@ -140,10 +141,10 @@ enum ImportType {
ApriltagFieldLayout
}
const showImportDialog = ref(false);
const importType = ref<ImportType | number>(-1);
const importType = ref<ImportType | undefined>(undefined);
const importFile = ref<File | null>(null);
const handleSettingsImport = () => {
if (importType.value === -1 || importFile.value === null) return;
if (importType.value === undefined || importFile.value === null) return;
const formData = new FormData();
formData.append("data", importFile.value);
@@ -198,7 +199,7 @@ const handleSettingsImport = () => {
});
showImportDialog.value = false;
importType.value = -1;
importType.value = undefined;
importFile.value = null;
};
@@ -237,25 +238,25 @@ const nukePhotonConfigDirectory = () => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Device Control</v-card-title>
<div class="pa-6 pt-0">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartProgram">
<v-icon left class="open-icon"> mdi-restart </v-icon>
<v-icon start class="open-icon"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartDevice">
<v-icon left class="open-icon"> mdi-restart-alert </v-icon>
<v-icon start class="open-icon"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon left class="open-icon"> mdi-upload </v-icon>
<v-icon start class="open-icon"> mdi-upload </v-icon>
<span class="open-label">Offline Update</span>
</v-btn>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
@@ -265,15 +266,15 @@ const nukePhotonConfigDirectory = () => {
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@input="
@update:modelValue="
() => {
importType = -1;
importType = undefined;
importFile = null;
}
"
@@ -301,18 +302,14 @@ const nukePhotonConfigDirectory = () => {
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
v-model="importFile"
:disabled="importType === -1"
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
:disabled="importType === undefined"
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
/>
</v-row>
<v-row
class="mt-12 ml-8 mr-8 mb-1"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
</v-row>
@@ -322,7 +319,7 @@ const nukePhotonConfigDirectory = () => {
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @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">Export Settings</span>
</v-btn>
<a
@@ -335,7 +332,7 @@ const nukePhotonConfigDirectory = () => {
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left class="open-icon"> mdi-download </v-icon>
<v-icon start class="open-icon"> mdi-download </v-icon>
<span class="open-label">Download logs</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -350,7 +347,7 @@ const nukePhotonConfigDirectory = () => {
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left class="open-icon"> mdi-eye </v-icon>
<v-icon start class="open-icon"> mdi-eye </v-icon>
<span class="open-label">View program logs</span>
</v-btn>
</v-col>
@@ -359,10 +356,10 @@ const nukePhotonConfigDirectory = () => {
<v-row>
<v-col cols="12">
<v-btn color="error" @click="() => (showFactoryReset = true)">
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<v-icon start class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.breakpoint.mdAndUp
$vuetify.display.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING"
: "Factory Reset PhotonVision"
}}
@@ -373,22 +370,22 @@ const nukePhotonConfigDirectory = () => {
</div>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card dark color="primary" class="pa-3" flat>
<v-card color="primary" class="pa-3" flat>
<v-card-title style="justify-content: center" class="pb-6">
<span class="open-label">
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
Factory Reset PhotonVision
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
</span>
</v-card-title>
<v-card-text class="pt-3">
<v-row class="align-center white--text">
<v-row class="align-center text-white">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @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"
@@ -415,11 +412,9 @@ const nukePhotonConfigDirectory = () => {
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<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">
{{
$vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything"
}}
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>
</v-btn>
</v-card-text>

View File

@@ -4,7 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<pv-slider
@@ -14,7 +14,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
:slider-cols="12"
:min="0"
:max="100"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
@update:modelValue="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card>

View File

@@ -120,17 +120,17 @@ onBeforeMount(() => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pl-6" style="display: flex; justify-content: space-between">
<span class="pt-2 pb-2">Stats</span>
<v-btn text @click="fetchMetrics">
<v-icon left class="open-icon">mdi-reload</v-icon>
<v-btn variant="text" @click="fetchMetrics">
<v-icon start class="open-icon">mdi-reload</v-icon>
Last Fetched: {{ metricsLastFetched }}
</v-btn>
</v-card-title>
<v-card-text class="pa-6 pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<v-table class="metrics-table mt-3">
<thead>
<tr>
<th
@@ -163,11 +163,11 @@ onBeforeMount(() => {
</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-text class="pa-6 pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<v-table class="metrics-table mt-3">
<thead>
<tr>
<th
@@ -201,7 +201,7 @@ onBeforeMount(() => {
</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
</v-card>
</template>
@@ -263,7 +263,7 @@ onBeforeMount(() => {
text-decoration-color: #ffd843;
}
.v-data-table {
.v-table {
thead,
tbody {
background-color: #006492;

View File

@@ -124,9 +124,14 @@ const saveGeneralSettings = () => {
});
};
const currentNetworkInterfaceIndex = computed<number>({
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
const currentNetworkInterfaceIndex = computed<number | undefined>({
get: () => {
const index = useSettingsStore().networkInterfaceNames.indexOf(
useSettingsStore().network.networkManagerIface || ""
);
return index === -1 ? undefined : index;
},
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
});
watchEffect(() => {
@@ -136,7 +141,7 @@ watchEffect(() => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Global Settings</v-card-title>
<div class="pa-6 pt-0">
<v-divider class="pb-3" />
@@ -157,7 +162,7 @@ watchEffect(() => {
<v-banner
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
color="error"
bg-color="error"
text-color="white"
style="margin: 10px 0"
icon="mdi-alert-circle-outline"
@@ -233,7 +238,7 @@ watchEffect(() => {
!useSettingsStore().network.networkingDisabled
"
rounded
color="error"
bg-color="error"
text-color="white"
icon="mdi-information-outline"
>
@@ -248,7 +253,7 @@ watchEffect(() => {
<v-banner
v-if="tempSettingsStruct.runNTServer"
rounded
color="error"
bg-color="error"
text-color="white"
icon="mdi-information-outline"
>
@@ -265,7 +270,7 @@ watchEffect(() => {
<v-banner
v-if="tempSettingsStruct.shouldPublishProto"
rounded
color="error"
bg-color="error"
text-color="white"
icon="mdi-information-outline"
>
@@ -276,6 +281,7 @@ watchEffect(() => {
</v-form>
<v-btn
color="accent"
:variant="!settingsValid || !settingsHaveChanged() ? 'tonal' : 'elevated'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { ref, computed, inject } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
@@ -8,6 +8,28 @@ const showImportDialog = ref(false);
const importRKNNFile = ref<File | null>(null);
const importLabelsFile = ref<File | null>(null);
const host = inject<string>("backendHost");
const areValidFileNames = (weights: string | null, labels: string | null) => {
const weightsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)\.rknn$/;
const labelsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)-labels\.txt$/;
if (weights && labels) {
const weightsMatch = weights.match(weightsRegex);
const labelsMatch = labels.match(labelsRegex);
if (weightsMatch && labelsMatch) {
return (
weightsMatch[1] === labelsMatch[1] &&
weightsMatch[2] === labelsMatch[2] &&
weightsMatch[3] === labelsMatch[3] &&
weightsMatch[4] === labelsMatch[4]
);
}
}
return false;
};
// TODO gray out the button when model is uploading
const handleImport = async () => {
if (importRKNNFile.value === null || importLabelsFile.value === null) return;
@@ -65,19 +87,19 @@ const supportedModels = computed(() => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Object Detection</v-card-title>
<div class="pa-6 pt-0">
<v-row>
<v-col cols="12 ">
<v-btn color="secondary" class="justify-center" @click="() => (showImportDialog = true)">
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import New Model</span>
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@input="
@update:modelValue="
() => {
importRKNNFile = null;
importLabelsFile = null;
@@ -87,30 +109,28 @@ const supportedModels = computed(() => {
<v-card color="primary" dark>
<v-card-title>Import New Object Detection Model</v-card-title>
<v-card-text>
Upload a new object detection model to this device that can be used in a pipeline. Naming convention
should be <code>name-verticalResolution-horizontalResolution-yolovXXX</code>. The
<code>name</code> should only include alphanumeric characters, periods, and underscores. Additionally,
the labels file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the
end. For example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5, YOLOv8, and
YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
640x640 YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are
currently supported! See [the documentation]({{
host
}}/docs/objectDetection/about-object-detection.html) for more details.
<v-row class="mt-6 ml-4 mr-8">
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
</v-row>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input v-model="importLabelsFile" label="Labels File" accept=".txt" />
</v-row>
<v-row
class="mt-12 ml-8 mr-8 mb-1"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
<v-btn
color="secondary"
:disabled="importRKNNFile === null || importLabelsFile === null"
:disabled="
importRKNNFile === null ||
importLabelsFile === null ||
!areValidFileNames(importRKNNFile.name, importLabelsFile.name)
"
@click="handleImport"
>
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Object Detection Model</span>
</v-btn>
</v-row>
@@ -121,7 +141,7 @@ const supportedModels = computed(() => {
</v-row>
<v-row>
<v-col cols="12">
<v-simple-table fixed-header height="100%" dense dark>
<v-table fixed-header height="100%" density="compact" dark>
<thead style="font-size: 1.25rem">
<tr>
<th class="text-left">Available Models</th>
@@ -132,7 +152,7 @@ const supportedModels = computed(() => {
<td>{{ model }}</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-col>
</v-row>
</div>
@@ -151,7 +171,7 @@ const supportedModels = computed(() => {
display: none;
}
}
.v-data-table {
.v-table {
width: 100%;
height: 100%;
text-align: center;