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:
Sam Freund
2026-05-05 10:24:19 -05:00
committed by GitHub
parent d587cd19bb
commit 2372e110f9
43 changed files with 578 additions and 388 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = () => {