[PhotonClient] Vite and Typescript complete refactor (#884)

This commit is contained in:
Sriman Achanta
2023-08-21 01:51:35 -04:00
committed by GitHub
parent 8397b43bef
commit f623e4a1cc
119 changed files with 11821 additions and 19318 deletions

View File

@@ -0,0 +1,226 @@
<script setup lang="ts">
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
import {
ArrowHelper,
BoxGeometry,
ConeGeometry,
Mesh,
MeshNormalMaterial,
PerspectiveCamera,
Quaternion,
Scene,
Vector3,
Color,
WebGLRenderer
} from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { type Object3D } from "three";
const props = defineProps<{
targets: PhotonTarget[]
}>();
let scene: Scene | undefined;
let camera: PerspectiveCamera | undefined;
let renderer: WebGLRenderer | undefined;
let controls: TrackballControls | undefined;
let previousTargets: Object3D[] = [];
const drawTargets = (targets: PhotonTarget[]) => {
// Check here, since if we check in watchEffect this never gets called
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
scene.remove(...previousTargets);
previousTargets = [];
targets.forEach(target => {
if(target.pose === undefined) return;
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
const material = new MeshNormalMaterial();
const quaternion = new Quaternion(
target.pose.qx,
target.pose.qy,
target.pose.qz,
target.pose.qw
);
const cube = new Mesh(geometry, material);
cube.position.set(target.pose.x, target.pose.y, target.pose.z);
cube.rotation.setFromQuaternion(quaternion);
previousTargets.push(cube);
let arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1);
arrow.rotation.setFromQuaternion(quaternion);
arrow.rotateZ(-Math.PI / 2);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1);
arrow.rotation.setFromQuaternion(quaternion);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1);
arrow.setRotationFromQuaternion(quaternion);
arrow.rotateX(Math.PI / 2);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
});
if(previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const onWindowResize = () => {
const container = document.getElementById("container");
const canvas = document.getElementById("view");
if(container === null || canvas === null || camera === undefined || renderer === undefined) {
return;
}
canvas.style.width = container.clientWidth * 0.75 + "px";
canvas.style.height = container.clientWidth * 0.35 + "px";
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
};
const resetCamFirstPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
return;
}
controls.reset();
camera.position.set(0.2, 0, 0);
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const resetCamThirdPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
return;
}
controls.reset();
camera.position.set(-1.39, -1.09, 1.17);
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
onMounted(() => {
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
const canvas = document.getElementById("view");
if(canvas === null) return;
renderer = new WebGLRenderer({ canvas: canvas });
scene.background = new Color(0xa9a9a9);
onWindowResize();
window.addEventListener("resize", onWindowResize);
const referenceFrameCues: Object3D[] = [];
referenceFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1));
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1));
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1));
// Draw the Camera Body
const camSize = 0.2;
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
const camLensGeometry = new ConeGeometry(camSize*0.4, camSize*0.8, 30);
const camMaterial = new MeshNormalMaterial();
const camBody = new Mesh(camBodyGeometry, camMaterial);
const camLens = new Mesh(camLensGeometry, camMaterial);
camBody.position.set(0, 0, 0);
camLens.rotateZ(Math.PI / 2);
camLens.position.set(camSize * 0.8, 0, 0);
referenceFrameCues.push(camBody);
referenceFrameCues.push(camLens);
controls = new TrackballControls(
camera,
renderer.domElement
);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
scene.add(...referenceFrameCues);
resetCamThirdPerson();
controls.update();
const animate = () => {
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
drawTargets(props.targets);
animate();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
});
</script>
<template>
<div
id="container"
style="width: 100%"
>
<v-row>
<v-col
align-self="stretch"
style="display: flex; justify-content: center"
>
<canvas
id="view"
/>
</v-col>
</v-row>
<v-row style="margin-bottom: 24px">
<v-col style="display: flex; justify-content: center">
<v-btn
color="secondary"
@click="resetCamFirstPerson"
>
First Person
</v-btn>
</v-col>
<v-col style="display: flex; justify-content: center">
<v-btn
color="secondary"
@click="resetCamThirdPerson"
>
Third Person
</v-btn>
</v-col>
</v-row>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { computed, inject } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import loadingImage from "@/assets/images/loading.svg";
import type { StyleValue } from "vue/types/jsx";
const props = defineProps<{
streamType: "Raw" | "Processed",
id?: string
}>();
const src = computed<string>(() => {
const port = useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
if(!useStateStore().backendConnected || port === 0) {
return loadingImage;
}
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
});
const alt = computed<string>(() => `${props.streamType} Stream View`);
const style = computed<StyleValue>(() => {
if(useStateStore().colorPickingMode) {
return { cursor: "crosshair" };
} else if(src.value !== loadingImage) {
return { cursor: "pointer" };
}
return {};
});
const handleClick = () => {
if(!useStateStore().colorPickingMode && src.value !== loadingImage) {
window.open(src.value);
}
};
</script>
<template>
<img
:id="id"
crossorigin="anonymous"
:src="src"
:alt="alt"
:style="style"
@click="handleClick"
>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { useStateStore } from "@/stores/StateStore";
</script>
<template>
<v-snackbar
v-model="useStateStore().snackbarData.show"
top
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>
<p style="padding: 0; margin: 0; text-align: center">
{{ useStateStore().snackbarData.message }}
</p>
</v-snackbar>
</template>

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import { computed, ref, inject } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
const logs = computed<LogMessage[]>(() => useStateStore().logMessages.filter(message => selectedLogLevels.value.includes(message.level)));
const backendHost = inject<string>("backendHost");
const getLogColor = (level: LogLevel): string => {
switch (level) {
case LogLevel.ERROR:
return "red";
case LogLevel.WARN:
return "yellow";
case LogLevel.INFO:
return "green";
case LogLevel.DEBUG:
return "white";
}
return "";
};
const getLogLevelFromIndex = (index: number): string => {
return LogLevel[index];
};
const exportLogFile = ref();
const handleLogExport = () => {
exportLogFile.value.click();
};
document.addEventListener("keydown", e => {
switch (e.key) {
case "`":
useStateStore().$patch(state => state.showLogModal = !state.showLogModal);
break;
}
});
</script>
<template>
<v-dialog
v-model="useStateStore().showLogModal"
width="1500"
dark
>
<v-card
dark
class="pt-3"
color="primary"
flat
>
<v-card-title>
View Program Logs
<v-btn
color="secondary"
style="margin-left: auto;"
depressed
@click="handleLogExport"
>
<v-icon left>
mdi-download
</v-icon>
Download Current Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="`http://${backendHost}/api/utils/photonvision-journalctl.txt`"
download="photonvision-journalctl.txt"
target="_blank"
/>
</v-btn>
</v-card-title>
<div class="pr-6 pl-6">
<v-btn-toggle
v-model="selectedLogLevels"
dark
multiple
class="fill mb-4"
>
<v-btn
v-for="(level) in [0, 1, 2, 3]"
:key="level"
color="secondary"
class="fill"
>
{{ getLogLevelFromIndex(level) }}
</v-btn>
</v-btn-toggle>
<v-card-text
v-if="logs.length === 0"
style="font-size: 18px; font-weight: 600"
>
There are no Logs to show
</v-card-text>
<v-virtual-scroll
v-else
:items="logs"
item-height="50"
height="600"
>
<template #default="{item}">
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
{{ item.message }}
</div>
</template>
</v-virtual-scroll>
</div>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="() => useStateStore().showLogModal = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 25%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup lang="ts">
import { computed, getCurrentInstance } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const compact = computed<boolean>({
get: () => { return useStateStore().sidebarFolded; },
set: (val) => { useStateStore().setSidebarFolded(val); }
});
// 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);
</script>
<template>
<v-navigation-drawer
dark
app
permanent
:mini-variant="compact || !mdAndUp"
color="primary"
>
<v-list>
<!-- 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"
>
<img
v-else
class="logo"
src="@/assets/images/logoSmall.svg"
alt="small logo"
>
</v-list-item-icon>
</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>
<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>Cameras</v-list-item-title>
</v-list-item-content>
</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>
<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>
<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>
<div style="position: absolute; bottom: 0; left: 0;">
<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 }}</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>
</v-navigation-drawer>
</template>
<style scoped>
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
</style>