mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-29 02:21:41 +00:00
TypeCheck Frontend (#2394)
We recently had an error that would've been caught by type checking in the frontend (see #2393). This PR implements type checking so that future errors will be caught. Additionally, this PR contains miscellaneous frontend cleanup that's tangentially related to type-checking.
This commit is contained in:
@@ -3,7 +3,7 @@ import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
|
||||
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
|
||||
import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
|
||||
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
|
||||
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
|
||||
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
|
||||
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
|
||||
const {
|
||||
ArrowHelper,
|
||||
@@ -20,7 +20,7 @@ const {
|
||||
Scene,
|
||||
WebGLRenderer
|
||||
} = await import("three");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
|
||||
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
|
||||
@@ -213,14 +213,14 @@ onMounted(async () => {
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
drawTargets(props.targets);
|
||||
await drawTargets(props.targets);
|
||||
animate();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
});
|
||||
watchEffect(() => {
|
||||
drawTargets(props.targets);
|
||||
void drawTargets(props.targets);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue";
|
||||
import type {
|
||||
Scene as SceneType,
|
||||
PerspectiveCamera as PerspectiveCameraType,
|
||||
WebGLRenderer as WebGLRendererType,
|
||||
Group as GroupType,
|
||||
Object3D
|
||||
} from "three";
|
||||
import type { TrackballControls as TrackballControlsType } from "three/examples/jsm/controls/TrackballControls.js";
|
||||
const {
|
||||
AmbientLight,
|
||||
AxesHelper,
|
||||
@@ -16,7 +24,7 @@ const {
|
||||
SphereGeometry,
|
||||
WebGLRenderer
|
||||
} = await import("three");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
|
||||
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
@@ -31,12 +39,12 @@ const props = defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
let scene: Scene | undefined;
|
||||
let camera: PerspectiveCamera | undefined;
|
||||
let renderer: WebGLRenderer | undefined;
|
||||
let controls: TrackballControls | undefined;
|
||||
let scene: SceneType | undefined;
|
||||
let camera: PerspectiveCameraType | undefined;
|
||||
let renderer: WebGLRendererType | undefined;
|
||||
let controls: TrackballControlsType | undefined;
|
||||
|
||||
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
|
||||
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => {
|
||||
const group = new Group();
|
||||
|
||||
if (obs.locationInImageSpace.length === 0) return group;
|
||||
@@ -194,9 +202,6 @@ const resetCamThirdPerson = () => {
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
// Grab data first off
|
||||
fetchCalibrationData();
|
||||
|
||||
scene = new Scene();
|
||||
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
|
||||
@@ -256,6 +261,10 @@ onMounted(async () => {
|
||||
|
||||
controls.update();
|
||||
|
||||
// Fetch calibration only after the scene is ready so the initial draw
|
||||
// can happen immediately when the data arrives.
|
||||
await fetchCalibrationData();
|
||||
|
||||
const animate = () => {
|
||||
if (!scene || !camera || !renderer || !controls) {
|
||||
return;
|
||||
@@ -318,7 +327,7 @@ if (import.meta.hot) {
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
drawCalibration(calibrationData.value);
|
||||
void drawCalibration(calibrationData.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -328,9 +337,9 @@ watch(
|
||||
props.resolution.height,
|
||||
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
|
||||
],
|
||||
() => {
|
||||
async () => {
|
||||
console.log("Camera or resolution changed, refetching calibration");
|
||||
fetchCalibrationData();
|
||||
await fetchCalibrationData();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { computed, inject, onBeforeUnmount, useTemplateRef } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import type { StyleValue } from "vue";
|
||||
@@ -13,6 +13,7 @@ const props = defineProps<{
|
||||
cameraSettings: UiCameraConfiguration;
|
||||
}>();
|
||||
|
||||
const backendHostname = inject<string>("backendHostname");
|
||||
const emptyStreamSrc = "//:0";
|
||||
const streamSrc = computed<string>(() => {
|
||||
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
@@ -21,7 +22,7 @@ const streamSrc = computed<string>(() => {
|
||||
return emptyStreamSrc;
|
||||
}
|
||||
|
||||
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
return `http://${backendHostname}:${port}/stream.mjpg`;
|
||||
});
|
||||
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
|
||||
const streamStyle = computed<StyleValue>(() => {
|
||||
@@ -67,26 +68,26 @@ const handleCaptureClick = () => {
|
||||
const handlePopoutClick = () => {
|
||||
window.open(streamSrc.value);
|
||||
};
|
||||
const handleFullscreenRequest = () => {
|
||||
const handleFullscreenRequest = async () => {
|
||||
const stream = document.getElementById(props.id);
|
||||
if (!stream) return;
|
||||
stream.requestFullscreen();
|
||||
await stream.requestFullscreen();
|
||||
};
|
||||
|
||||
const mjpgStream: any = ref(null);
|
||||
const mjpgStream = useTemplateRef("mjpgStream");
|
||||
|
||||
const handleStreamError = () => {
|
||||
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
|
||||
console.error("Error loading stream:", streamSrc.value, " Trying again.");
|
||||
setTimeout(() => {
|
||||
mjpgStream.value.src = streamSrc.value;
|
||||
mjpgStream.value!.src = streamSrc.value;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!mjpgStream.value) return;
|
||||
mjpgStream.value["src"] = emptyStreamSrc;
|
||||
mjpgStream.value.src = emptyStreamSrc;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { computed, inject, ref, useTemplateRef, watch } from "vue";
|
||||
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import LogEntry from "@/components/app/photon-log-entry.vue";
|
||||
@@ -10,10 +10,10 @@ const backendHost = inject<string>("backendHost");
|
||||
const searchQuery = ref("");
|
||||
const timeInput = ref<string>();
|
||||
const autoScroll = ref(true);
|
||||
const logList = ref();
|
||||
const logList = useTemplateRef<InstanceType<typeof VirtualList>>("logList"); // this needs to be typed in the definition since vue has trouble inferring it
|
||||
const logKeeps = ref(40);
|
||||
const exportLogFile = ref();
|
||||
const selectedLogLevels = ref({
|
||||
const exportLogFile = useTemplateRef("exportLogFile");
|
||||
const selectedLogLevels = ref<Record<number, boolean>>({
|
||||
[LogLevel.ERROR]: true,
|
||||
[LogLevel.WARN]: true,
|
||||
[LogLevel.INFO]: true,
|
||||
@@ -48,7 +48,7 @@ watch(logs, () => {
|
||||
);
|
||||
autoScroll.value = bottomOffset < 50;
|
||||
|
||||
if (autoScroll.value) logList.value.scrollToBottom();
|
||||
if (autoScroll.value) logList.value?.scrollToBottom();
|
||||
});
|
||||
|
||||
const getLogLevelFromIndex = (index: number): string => {
|
||||
@@ -56,7 +56,7 @@ const getLogLevelFromIndex = (index: number): string => {
|
||||
};
|
||||
|
||||
const handleLogExport = () => {
|
||||
exportLogFile.value.click();
|
||||
exportLogFile.value?.click();
|
||||
};
|
||||
|
||||
const handleLogClear = () => {
|
||||
|
||||
@@ -89,7 +89,7 @@ const calibrationDivisors = computed(() =>
|
||||
})
|
||||
);
|
||||
|
||||
const uniqueVideoResolutionString = ref("");
|
||||
const uniqueVideoResolutionIndex = ref(getUniqueVideoResolutionStrings()?.[0]?.value);
|
||||
|
||||
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
|
||||
// This avoids trying to index into an array that may be empty during page reload.
|
||||
@@ -106,7 +106,7 @@ watchEffect(() => {
|
||||
? currentFormatIndex
|
||||
: names.length - 1;
|
||||
useStateStore().calibrationData.videoFormatIndex = currentIndex;
|
||||
uniqueVideoResolutionString.value = names[currentIndex] ?? "";
|
||||
uniqueVideoResolutionIndex.value = currentIndex;
|
||||
});
|
||||
const squareSizeIn = ref(1);
|
||||
const markerSizeIn = ref(0.75);
|
||||
@@ -191,7 +191,7 @@ const downloadCalibBoard = async () => {
|
||||
};
|
||||
|
||||
const isCalibrating = computed(
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf()
|
||||
);
|
||||
|
||||
const startCalibration = () => {
|
||||
@@ -310,23 +310,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
>
|
||||
<v-form v-model="settingsValid">
|
||||
<pv-select
|
||||
v-model="uniqueVideoResolutionString"
|
||||
v-model="uniqueVideoResolutionIndex"
|
||||
label="Resolution"
|
||||
:select-cols="8"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
@update:model-value="
|
||||
useStateStore().calibrationData.videoFormatIndex =
|
||||
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
|
||||
"
|
||||
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="8"
|
||||
:items="['Chessboard', 'ChArUco']"
|
||||
:items="[
|
||||
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
|
||||
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
|
||||
]"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<v-alert
|
||||
@@ -356,7 +356,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
label="Tag Family"
|
||||
tooltip="Dictionary of ArUco markers on the ChArUco board"
|
||||
:select-cols="8"
|
||||
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
|
||||
:items="[
|
||||
{ value: CalibrationTagFamilies.Dict_4X4_1000, name: 'Dict_4X4_1000' },
|
||||
{ value: CalibrationTagFamilies.Dict_5X5_1000, name: 'Dict_5X5_1000' },
|
||||
{ value: CalibrationTagFamilies.Dict_6X6_1000, name: 'Dict_6X6_1000' },
|
||||
{ value: CalibrationTagFamilies.Dict_7X7_1000, name: 'Dict_7X7_1000' }
|
||||
]"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-number-input
|
||||
|
||||
@@ -3,7 +3,7 @@ import PhotonCalibrationVisualizer from "@/components/app/photon-calibration-vis
|
||||
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { computed, inject, ref, useTemplateRef } from "vue";
|
||||
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
|
||||
import { useTheme } from "vuetify";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
@@ -13,28 +13,28 @@ const props = defineProps<{
|
||||
videoFormat: VideoFormat;
|
||||
}>();
|
||||
|
||||
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
|
||||
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat });
|
||||
|
||||
const removeCalibration = (vf: VideoFormat) => {
|
||||
axiosPost("/calibration/remove", "delete a camera calibration", {
|
||||
const removeCalibration = async (vf: VideoFormat) => {
|
||||
await axiosPost("/calibration/remove", "delete a camera calibration", {
|
||||
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
|
||||
width: vf.resolution.width,
|
||||
height: vf.resolution.height
|
||||
});
|
||||
};
|
||||
|
||||
const exportCalibration = ref();
|
||||
const exportCalibration = useTemplateRef("exportCalibration");
|
||||
const openExportCalibrationPrompt = () => {
|
||||
exportCalibration.value.click();
|
||||
exportCalibration.value?.click();
|
||||
};
|
||||
|
||||
const importCalibrationFromPhotonJson = ref();
|
||||
const importCalibrationFromPhotonJson = useTemplateRef("importCalibrationFromPhotonJson");
|
||||
const openUploadPhotonCalibJsonPrompt = () => {
|
||||
importCalibrationFromPhotonJson.value.click();
|
||||
importCalibrationFromPhotonJson.value?.click();
|
||||
};
|
||||
const importCalibration = async () => {
|
||||
const files = importCalibrationFromPhotonJson.value.files;
|
||||
if (files.length === 0) return;
|
||||
const files = importCalibrationFromPhotonJson.value?.files;
|
||||
if (!files?.length) return;
|
||||
const uploadedJson = files[0];
|
||||
|
||||
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);
|
||||
|
||||
@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
|
||||
.get("/utils/getImageSnapshots")
|
||||
.then((response) => {
|
||||
imgData.value = response.data.map(
|
||||
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
|
||||
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index: number) => {
|
||||
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
|
||||
|
||||
return {
|
||||
@@ -99,7 +99,7 @@ const expanded = ref([]);
|
||||
<v-card-text class="pt-0">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="fetchSnapshots"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { computed, ref, watchEffect } from "vue";
|
||||
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost } from "@/lib/PhotonUtils";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -20,7 +21,7 @@ const focusMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isFocusMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
@@ -65,8 +66,8 @@ const settingsHaveChanged = (): boolean => {
|
||||
const a = tempSettingsStruct.value;
|
||||
const b = useCameraSettingsStore().currentCameraSettings;
|
||||
|
||||
for (const q in ValidQuirks) {
|
||||
if (a.quirksToChange[q] !== b.cameraQuirks.quirks[q]) return true;
|
||||
for (const quirk of Object.values(ValidQuirks)) {
|
||||
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true;
|
||||
}
|
||||
|
||||
return a.fov !== b.fov.value;
|
||||
@@ -120,12 +121,12 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
const showDeleteCamera = ref(false);
|
||||
const deleteThisCamera = () => {
|
||||
axiosPost("/utils/nukeOneCamera", "delete this camera", {
|
||||
const deleteThisCamera = async () => {
|
||||
await axiosPost("/utils/nukeOneCamera", "delete this camera", {
|
||||
cameraUniqueName: useStateStore().currentCameraUniqueName
|
||||
});
|
||||
};
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
const wrappedCameras = computed<SelectItem<string>[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
@@ -159,7 +160,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
v-model="arducamSelectWrapper"
|
||||
label="Arducam Model"
|
||||
:items="[
|
||||
{ name: 'None', value: 0, disabled: true },
|
||||
{ name: 'None', value: 0 },
|
||||
{ name: 'OV9281', value: 1 },
|
||||
{ name: 'OV2311', value: 2 },
|
||||
{ name: 'OV9782', value: 3 }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useTheme } from "vuetify";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -15,7 +16,7 @@ const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
v ? WebsocketPipelineType.DriverMode : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import { cameraInfoFor } from "@/lib/PhotonUtils";
|
||||
|
||||
const { camera } = defineProps({
|
||||
camera: {
|
||||
@@ -7,19 +8,6 @@ const { camera } = defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import { cameraInfoFor } from "@/lib/PhotonUtils";
|
||||
|
||||
function isEqual<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
@@ -25,19 +26,6 @@ const { saved, current } = defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -25,7 +25,7 @@ const emit = defineEmits<{
|
||||
(e: "onEscape"): void;
|
||||
}>();
|
||||
|
||||
const handleKeydown = ({ key }) => {
|
||||
const handleKeydown = ({ key }: KeyboardEvent) => {
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
// Explicitly check that all rule props return true
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends string | number">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
export interface SelectItem {
|
||||
export interface SelectItem<TValue extends string | number> {
|
||||
name: string | number;
|
||||
value: string | number;
|
||||
value: TValue;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const value = defineModel<string | number | undefined>({ required: true });
|
||||
|
||||
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
|
||||
const value = defineModel<T>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -15,7 +17,7 @@ const props = withDefaults(
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
items: SelectItems;
|
||||
}>(),
|
||||
{
|
||||
selectCols: 9,
|
||||
@@ -23,18 +25,20 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const areSelectItems = (items: SelectItems): items is SelectItem<T>[] => typeof items[0] === "object";
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
const items = computed<SelectItem<T>[]>(() => {
|
||||
// Trivial case for empty list; we have no data
|
||||
if (!props.items.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the prop exists on the object to infer object type
|
||||
if ((props.items[0] as SelectItem).name) {
|
||||
return props.items as SelectItem[];
|
||||
if (areSelectItems(props.items)) {
|
||||
return props.items;
|
||||
}
|
||||
return props.items.map((v, i) => ({ name: v, value: i }));
|
||||
|
||||
return props.items.map((item) => ({ name: item, value: item }));
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -49,7 +53,7 @@ const items = computed<SelectItem[]>(() => {
|
||||
:items="items"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
item-props.disabled="disabled"
|
||||
item-props
|
||||
:disabled="disabled"
|
||||
hide-details="auto"
|
||||
variant="underlined"
|
||||
|
||||
@@ -18,11 +18,11 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
|
||||
|
||||
// Debounce function
|
||||
function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
function debounce(func: (...args: number[]) => void, wait: number) {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return function (...args: any[]) {
|
||||
return function (...args: number[]) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,28 +13,6 @@ import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
|
||||
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
|
||||
|
||||
switch (useCameraSettingsStore().cameras[cameraUniqueName].pipelineSettings.pipelineType) {
|
||||
case PipelineType.Reflective:
|
||||
pipelineType.value = WebsocketPipelineType.Reflective;
|
||||
break;
|
||||
case PipelineType.ColoredShape:
|
||||
pipelineType.value = WebsocketPipelineType.ColoredShape;
|
||||
break;
|
||||
case PipelineType.AprilTag:
|
||||
pipelineType.value = WebsocketPipelineType.AprilTag;
|
||||
break;
|
||||
case PipelineType.Aruco:
|
||||
pipelineType.value = WebsocketPipelineType.Aruco;
|
||||
break;
|
||||
case PipelineType.ObjectDetection:
|
||||
pipelineType.value = WebsocketPipelineType.ObjectDetection;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Common RegEx used for naming both pipelines and cameras
|
||||
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
|
||||
|
||||
@@ -87,17 +65,17 @@ const cancelCameraNameEdit = () => {
|
||||
};
|
||||
|
||||
// Pipeline Name Edit
|
||||
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
|
||||
const pipelineNamesWrapper = computed(() => {
|
||||
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() });
|
||||
}
|
||||
if (useCameraSettingsStore().isFocusMode) {
|
||||
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
|
||||
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() });
|
||||
}
|
||||
|
||||
return pipelineNames;
|
||||
@@ -240,7 +218,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
const wrappedCameras = computed<SelectItem<string>[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
@@ -257,7 +235,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="wrappedCameras"
|
||||
@update:modelValue="changeCurrentCameraUniqueName"
|
||||
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
|
||||
@@ -13,9 +13,9 @@ import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
|
||||
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 { PipelineType } from "@/types/PipelineTypes";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
import { useDisplay } from "vuetify/lib/composables/display";
|
||||
import { useTheme } from "vuetify";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -106,6 +106,17 @@ const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
});
|
||||
|
||||
// This boolean is used to satisfy type-checking requirements.
|
||||
const shouldUseWideSecondTabGroup = computed(() => {
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
return (
|
||||
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
|
||||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
|
||||
currentPipelineSettings.doMultiTarget
|
||||
);
|
||||
});
|
||||
|
||||
const onBeforeTabUpdate = () => {
|
||||
// Force the current tab to the input tab on driver mode change
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
@@ -129,7 +140,7 @@ const onBeforeTabUpdate = () => {
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:cols="tabGroupIndex === 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
|
||||
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
@vue:before-update="onBeforeTabUpdate"
|
||||
>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { PipelineType, type AprilTagPipelineSettings, AprilTagFamily } 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 } 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 currentPipelineSettings = computed<AprilTagPipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -25,7 +24,10 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
|
||||
:items="[
|
||||
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
|
||||
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType, type ActivePipelineSettings } from "@/types/PipelineTypes";
|
||||
import { PipelineType, type ArucoPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes";
|
||||
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";
|
||||
@@ -11,8 +11,8 @@ 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 currentPipelineSettings = computed<ArucoPipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings as ArucoPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -25,7 +25,10 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
|
||||
:items="[
|
||||
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
|
||||
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ActivePipelineSettings,
|
||||
PipelineType,
|
||||
ContourSortMode,
|
||||
ContourTargetOrientation,
|
||||
ContourGroupingMode,
|
||||
ContourIntersection,
|
||||
ContourShape
|
||||
} 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";
|
||||
@@ -61,7 +69,10 @@ const interactiveCols = computed(() =>
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
@@ -72,7 +83,15 @@ const interactiveCols = computed(() =>
|
||||
label="Target Sort"
|
||||
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']"
|
||||
:items="[
|
||||
{ value: ContourSortMode.Largest, name: 'Largest' },
|
||||
{ value: ContourSortMode.Smallest, name: 'Smallest' },
|
||||
{ value: ContourSortMode.Highest, name: 'Highest' },
|
||||
{ value: ContourSortMode.Lowest, name: 'Lowest' },
|
||||
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
|
||||
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
|
||||
{ value: ContourSortMode.Centermost, name: 'Centermost' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
@@ -166,7 +185,11 @@ const interactiveCols = computed(() =>
|
||||
label="Target Grouping"
|
||||
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']"
|
||||
:items="[
|
||||
{ value: ContourGroupingMode.Single, name: 'Single' },
|
||||
{ value: ContourGroupingMode.Dual, name: 'Dual' },
|
||||
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
|
||||
"
|
||||
@@ -176,7 +199,13 @@ const interactiveCols = computed(() =>
|
||||
label="Target Intersection"
|
||||
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['None', 'Up', 'Down', 'Left', 'Right']"
|
||||
:items="[
|
||||
{ value: ContourIntersection.None, name: 'None' },
|
||||
{ value: ContourIntersection.Up, name: 'Up' },
|
||||
{ value: ContourIntersection.Down, name: 'Down' },
|
||||
{ value: ContourIntersection.Left, name: 'Left' },
|
||||
{ value: ContourIntersection.Right, name: 'Right' }
|
||||
]"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
|
||||
@@ -189,7 +218,12 @@ const interactiveCols = computed(() =>
|
||||
label="Target Shape"
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
:items="[
|
||||
{ value: ContourShape.Circle, name: 'Circle' },
|
||||
{ value: ContourShape.Polygon, name: 'Polygon' },
|
||||
{ value: ContourShape.Triangle, name: 'Triangle' },
|
||||
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
|
||||
"
|
||||
|
||||
@@ -30,11 +30,11 @@ const getFilteredStreamDivisors = (): number[] => {
|
||||
};
|
||||
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
|
||||
|
||||
const cameraResolutions = computed(() =>
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
|
||||
(f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
|
||||
)
|
||||
);
|
||||
const cameraResolutions = (): { name: string; value: number }[] =>
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map<{ name: string; value: number }>((f) => ({
|
||||
name: `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`,
|
||||
value: f.index || 0 // Index won't ever be undefined
|
||||
}));
|
||||
const handleResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
|
||||
|
||||
@@ -49,20 +49,24 @@ const handleResolutionChange = (value: number) => {
|
||||
const streamResolutions = computed(() => {
|
||||
const streamDivisors = getFilteredStreamDivisors();
|
||||
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return streamDivisors.map(
|
||||
(x) =>
|
||||
`${getResolutionString({
|
||||
width: Math.floor(currentResolution.width / x),
|
||||
height: Math.floor(currentResolution.height / x)
|
||||
})}`
|
||||
);
|
||||
return streamDivisors.map((x, i) => ({
|
||||
name: `${Math.floor(currentResolution.width / x)}x${Math.floor(currentResolution.height / x)}`,
|
||||
value: i
|
||||
}));
|
||||
});
|
||||
const currentStreamResolutionIndex = computed<number>({
|
||||
get: () => {
|
||||
const stored = useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor;
|
||||
const skipped = getNumberOfSkippedDivisors();
|
||||
return stored - skipped;
|
||||
},
|
||||
set: (index) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ streamingFrameDivisor: index + getNumberOfSkippedDivisors() },
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
const handleStreamResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
|
||||
false
|
||||
);
|
||||
};
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -182,17 +186,16 @@ const interactiveCols = computed(() =>
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
label="Resolution"
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:items="cameraResolutions()"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="(args) => handleResolutionChange(args)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
v-model="currentStreamResolutionIndex"
|
||||
label="Stream Resolution"
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="(args) => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="useCameraSettingsStore().isDriverMode"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ObjectDetectionPipelineSettings,
|
||||
PipelineType,
|
||||
ContourSortMode,
|
||||
ContourTargetOrientation
|
||||
} from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
@@ -44,19 +49,19 @@ const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
|
||||
return availableModels.filter(isSupported);
|
||||
});
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => {
|
||||
const currentModel = currentPipelineSettings.value.model;
|
||||
if (!currentModel) return undefined;
|
||||
const modelWrapper = computed<SelectItem<string>[]>(() =>
|
||||
supportedModels.value.map((model) => ({
|
||||
name: model.nickname,
|
||||
value: model.modelPath
|
||||
}))
|
||||
);
|
||||
|
||||
const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
|
||||
return index === -1 ? undefined : index;
|
||||
},
|
||||
|
||||
set: (v) => {
|
||||
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
|
||||
const newModel = supportedModels.value[v];
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
|
||||
const selectedModel = computed<string>({
|
||||
get: () => currentPipelineSettings.value.model?.modelPath ?? "",
|
||||
set: (value) => {
|
||||
const model = supportedModels.value.find((supportedModel) => supportedModel.modelPath === value);
|
||||
if (model) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ model }, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -69,7 +74,7 @@ const selectedModel = computed({
|
||||
label="Model"
|
||||
tooltip="The model used to detect objects in the camera feed"
|
||||
:select-cols="interactiveCols"
|
||||
:items="supportedModels.map((model) => model.nickname)"
|
||||
:items="modelWrapper"
|
||||
/>
|
||||
|
||||
<pv-slider
|
||||
@@ -123,14 +128,13 @@ const selectedModel = computed({
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
@@ -138,13 +142,17 @@ const selectedModel = computed({
|
||||
label="Target Sort"
|
||||
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']"
|
||||
:items="[
|
||||
{ value: ContourSortMode.Largest, name: 'Largest' },
|
||||
{ value: ContourSortMode.Smallest, name: 'Smallest' },
|
||||
{ value: ContourSortMode.Highest, name: 'Highest' },
|
||||
{ value: ContourSortMode.Lowest, name: 'Lowest' },
|
||||
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
|
||||
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
|
||||
{ value: ContourSortMode.Centermost, name: 'Centermost' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ActivePipelineSettings,
|
||||
PipelineType,
|
||||
RobotOffsetPointMode,
|
||||
ContourTargetOrientation,
|
||||
ContourTargetOffsetPointEdge
|
||||
} from "@/types/PipelineTypes";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed } from "vue";
|
||||
@@ -108,7 +114,13 @@ const interactiveCols = computed(() =>
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
|
||||
label="Target Offset Point"
|
||||
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
|
||||
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
|
||||
:items="[
|
||||
{ value: ContourTargetOffsetPointEdge.Center, name: 'Center' },
|
||||
{ value: ContourTargetOffsetPointEdge.Top, name: 'Top' },
|
||||
{ value: ContourTargetOffsetPointEdge.Bottom, name: 'Bottom' },
|
||||
{ value: ContourTargetOffsetPointEdge.Left, name: 'Left' },
|
||||
{ value: ContourTargetOffsetPointEdge.Right, name: 'Right' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
|
||||
@@ -119,7 +131,10 @@ const interactiveCols = computed(() =>
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
@@ -129,7 +144,11 @@ const interactiveCols = computed(() =>
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
|
||||
label="Robot Offset Mode"
|
||||
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
|
||||
:items="['None', 'Single Point', 'Dual Point']"
|
||||
:items="[
|
||||
{ value: RobotOffsetPointMode.None, name: 'None' },
|
||||
{ value: RobotOffsetPointMode.Single, name: 'Single Point' },
|
||||
{ value: RobotOffsetPointMode.Dual, name: 'Dual Point' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@ -0,0 +1,565 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, computed, ref, watch } from "vue";
|
||||
import { inject, computed, ref, watch, useTemplateRef } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
@@ -15,20 +14,20 @@ const theme = useTheme();
|
||||
|
||||
const restartProgram = async () => {
|
||||
if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
const restartDevice = async () => {
|
||||
if (await axiosPost("/utils/restartDevice", "restart the device")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const offlineUpdate = useTemplateRef("offlineUpdate");
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
offlineUpdate.value?.click();
|
||||
};
|
||||
|
||||
const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar");
|
||||
@@ -37,8 +36,8 @@ const majorVersionRegex = new RegExp("(?:dev-)?(\\d+)\\.\\d+\\.\\d+");
|
||||
const offlineUpdateDialog = ref({ show: false, confirmString: "" });
|
||||
|
||||
const handleOfflineUpdateRequest = async () => {
|
||||
const files = offlineUpdate.value.files;
|
||||
if (files.length === 0) return;
|
||||
const files = offlineUpdate.value?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
const match = files[0].name.match(offlineUpdateRegex);
|
||||
if (!match) {
|
||||
@@ -68,7 +67,7 @@ const handleOfflineUpdateRequest = async () => {
|
||||
});
|
||||
return;
|
||||
} else if (versionMatch && !dev) {
|
||||
handleOfflineUpdate(files[0]);
|
||||
await handleOfflineUpdate(files[0]);
|
||||
} else if (!versionMatch && !dev) {
|
||||
offlineUpdateDialog.value = {
|
||||
show: true,
|
||||
@@ -99,7 +98,7 @@ const handleOfflineUpdate = async (file: File) => {
|
||||
if (
|
||||
await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -118,18 +117,18 @@ const handleOfflineUpdate = async (file: File) => {
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const exportLogFile = useTemplateRef("exportLogFile");
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
exportLogFile.value?.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const exportSettings = useTemplateRef("exportSettings");
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
exportSettings.value?.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
@@ -141,10 +140,10 @@ enum ImportType {
|
||||
}
|
||||
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importType = ref<ImportType>(ImportType.AllSettings);
|
||||
const importFile = ref<File | null>(null);
|
||||
|
||||
const handleSettingsImport = () => {
|
||||
const handleSettingsImport = async () => {
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
@@ -167,18 +166,18 @@ const handleSettingsImport = () => {
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
}
|
||||
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
await axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
showImportDialog.value = false;
|
||||
importType.value = undefined;
|
||||
importType.value = ImportType.AllSettings;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
const showFactoryReset = ref(false);
|
||||
const nukePhotonConfigDirectory = async () => {
|
||||
if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -503,7 +502,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
width="600"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = undefined;
|
||||
importType = ImportType.AllSettings;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
@@ -517,7 +516,13 @@ watch(metricsHistorySnapshot, () => {
|
||||
v-model="importType"
|
||||
label="Type"
|
||||
tooltip="Select the type of settings file you are trying to upload"
|
||||
:items="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config', 'Apriltag Layout']"
|
||||
:items="[
|
||||
{ value: ImportType.AllSettings, name: 'All Settings' },
|
||||
{ value: ImportType.HardwareConfig, name: 'Hardware Config' },
|
||||
{ value: ImportType.HardwareSettings, name: 'Hardware Settings' },
|
||||
{ value: ImportType.NetworkConfig, name: 'Network Config' },
|
||||
{ value: ImportType.ApriltagFieldLayout, name: 'AprilTag Field Layout' }
|
||||
]"
|
||||
:select-cols="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -558,7 +563,9 @@ watch(metricsHistorySnapshot, () => {
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="
|
||||
offlineUpdateDialog.show = false;
|
||||
handleOfflineUpdate(offlineUpdate.files[0]);
|
||||
if (offlineUpdate?.files?.length) {
|
||||
handleOfflineUpdate(offlineUpdate.files[0]);
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
|
||||
|
||||
@@ -106,6 +106,7 @@ const saveGeneralSettings = async () => {
|
||||
|
||||
// Update the local settings cause the backend checked their validity. Assign is to deref value
|
||||
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
resetTempSettingsStruct();
|
||||
if (error.response) {
|
||||
@@ -150,14 +151,11 @@ const saveGeneralSettings = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
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])
|
||||
const currentNetworkInterface = computed<string>({
|
||||
get: () => useSettingsStore().network.networkManagerIface || "",
|
||||
set: (v) => {
|
||||
tempSettingsStruct.value.networkManagerIface = v;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -256,7 +254,7 @@ watchEffect(() => {
|
||||
/>
|
||||
<pv-select
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="currentNetworkInterfaceIndex"
|
||||
v-model="currentNetworkInterface"
|
||||
label="NetworkManager interface"
|
||||
:disabled="
|
||||
!tempSettingsStruct.shouldManage ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onBeforeUnmount, watch } from "vue";
|
||||
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Color - original (adjusted)
|
||||
@@ -8,14 +8,14 @@ import { useTheme } from "vuetify";
|
||||
// green - 65, 181, 127 (r: 75, g: 209, b: 147)
|
||||
// red - 238, 102, 102 (r: 238, g: 102, b: 102)
|
||||
const colors = {
|
||||
"blue-LightTheme": { r: 255, g: 216, b: 67 },
|
||||
"blue-DarkTheme": { r: 92, g: 154, b: 255 },
|
||||
"purple-LightTheme": { r: 255, g: 216, b: 67 },
|
||||
"purple-DarkTheme": { r: 167, g: 104, b: 196 },
|
||||
"red-LightTheme": { r: 255, g: 216, b: 67 },
|
||||
"red-DarkTheme": { r: 238, g: 102, b: 102 },
|
||||
"green-LightTheme": { r: 255, g: 216, b: 67 },
|
||||
"green-DarkTheme": { r: 75, g: 209, b: 147 }
|
||||
"blue-light": { r: 255, g: 216, b: 67 },
|
||||
"blue-dark": { r: 92, g: 154, b: 255 },
|
||||
"purple-light": { r: 255, g: 216, b: 67 },
|
||||
"purple-dark": { r: 167, g: 104, b: 196 },
|
||||
"red-light": { r: 255, g: 216, b: 67 },
|
||||
"red-dark": { r: 238, g: 102, b: 102 },
|
||||
"green-light": { r: 255, g: 216, b: 67 },
|
||||
"green-dark": { r: 75, g: 209, b: 147 }
|
||||
};
|
||||
const DEFAULT_COLOR = "blue";
|
||||
|
||||
@@ -26,9 +26,13 @@ const typeLabels = {
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const chartRef = ref(null);
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
interface TooltipSeriesParam {
|
||||
value: [number, number];
|
||||
}
|
||||
|
||||
const getOptions = (data: ChartData[] = []) => {
|
||||
const now = Date.now();
|
||||
return {
|
||||
@@ -37,7 +41,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: any) => {
|
||||
formatter: (params: TooltipSeriesParam[]) => {
|
||||
const p = params[0];
|
||||
const append = typeLabels[props.type];
|
||||
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
|
||||
@@ -80,12 +84,12 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
min: now - 55 * 1000,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
|
||||
color: theme.global.current.value.dark ? "#777" : "#aaa"
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
align: "left",
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
|
||||
color: theme.global.current.value.dark ? "#ddd" : "#fff",
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleTimeString([], {
|
||||
@@ -102,12 +106,12 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
position: "right",
|
||||
min:
|
||||
props.min ??
|
||||
function (value) {
|
||||
function (value: { min: number; max: number }) {
|
||||
return Math.max(0, (value.min - 10) | 0);
|
||||
},
|
||||
max:
|
||||
props.max ??
|
||||
function (value) {
|
||||
function (value: { min: number; max: number }) {
|
||||
return (value.max + 10) | 0;
|
||||
},
|
||||
splitNumber: 2,
|
||||
@@ -118,7 +122,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
|
||||
color: theme.global.current.value.dark ? "#ddd" : "#fff"
|
||||
}
|
||||
},
|
||||
series: getSeries(data),
|
||||
@@ -127,7 +131,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
};
|
||||
|
||||
const getSeries = (data: ChartData[] = []) => {
|
||||
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
|
||||
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`];
|
||||
return [
|
||||
{
|
||||
type: "line",
|
||||
@@ -188,10 +192,10 @@ interface ChartData {
|
||||
// Type options: "percentage", "temperature", "mb"
|
||||
const props = defineProps<{
|
||||
data: ChartData[];
|
||||
type: string;
|
||||
type: keyof typeof typeLabels;
|
||||
min?: number;
|
||||
max?: number;
|
||||
color?: string;
|
||||
color?: "red" | "green" | "blue" | "purple";
|
||||
}>();
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { ref, computed, inject, useTemplateRef } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
|
||||
@@ -46,7 +46,7 @@ const handleImport = async () => {
|
||||
if (
|
||||
await axiosPost("/objectdetection/import", "import an object detection model", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -74,20 +74,20 @@ const handleImport = async () => {
|
||||
importVersion.value = null;
|
||||
};
|
||||
|
||||
const deleteModel = (model: ObjectDetectionModelProperties) => {
|
||||
axiosPost("/objectdetection/delete", "delete an object detection model", {
|
||||
const deleteModel = async (model: ObjectDetectionModelProperties) => {
|
||||
await axiosPost("/objectdetection/delete", "delete an object detection model", {
|
||||
modelPath: model.modelPath
|
||||
});
|
||||
};
|
||||
|
||||
const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
|
||||
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Renaming Object Detection Model...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
await axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
modelPath: model.modelPath,
|
||||
newName: newName
|
||||
});
|
||||
@@ -97,7 +97,7 @@ const renameModel = (model: ObjectDetectionModelProperties, newName: string) =>
|
||||
// Filters out models that are not supported by the current backend, and returns a flattened list.
|
||||
const supportedModels = computed(() => {
|
||||
const { availableModels, supportedBackends } = useSettingsStore().general;
|
||||
const isSupported = (model: any) => {
|
||||
const isSupported = (model: ObjectDetectionModelProperties) => {
|
||||
// Check if model's family is in the list of supported backends
|
||||
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
|
||||
};
|
||||
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
|
||||
return availableModels.filter(isSupported);
|
||||
});
|
||||
|
||||
const exportModels = ref();
|
||||
const exportModels = useTemplateRef("exportModels");
|
||||
const openExportPrompt = () => {
|
||||
exportModels.value.click();
|
||||
exportModels.value?.click();
|
||||
};
|
||||
|
||||
const exportIndividualModel = ref();
|
||||
const exportIndividualModel = useTemplateRef("exportIndividualModel");
|
||||
const openExportIndividualModelPrompt = () => {
|
||||
exportIndividualModel.value.click();
|
||||
exportIndividualModel.value?.click();
|
||||
};
|
||||
|
||||
const showNukeDialog = ref(false);
|
||||
const nukeModels = () => {
|
||||
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
|
||||
const nukeModels = async () => {
|
||||
await axiosPost("/objectdetection/nuke", "clear and reset object detection models");
|
||||
};
|
||||
|
||||
const showBulkImportDialog = ref(false);
|
||||
@@ -132,7 +132,7 @@ const handleBulkImport = async () => {
|
||||
if (
|
||||
await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
|
||||
Reference in New Issue
Block a user