mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
[PhotonClient] Vite and Typescript complete refactor (#884)
This commit is contained in:
226
photon-client/src/components/app/photon-3d-visualizer.vue
Normal file
226
photon-client/src/components/app/photon-3d-visualizer.vue
Normal 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>
|
||||
50
photon-client/src/components/app/photon-camera-stream.vue
Normal file
50
photon-client/src/components/app/photon-camera-stream.vue
Normal 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>
|
||||
16
photon-client/src/components/app/photon-error-snackbar.vue
Normal file
16
photon-client/src/components/app/photon-error-snackbar.vue
Normal 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>
|
||||
143
photon-client/src/components/app/photon-log-view.vue
Normal file
143
photon-client/src/components/app/photon-log-view.vue
Normal 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>
|
||||
182
photon-client/src/components/app/photon-sidebar.vue
Normal file
182
photon-client/src/components/app/photon-sidebar.vue
Normal 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>
|
||||
550
photon-client/src/components/cameras/CameraCalibrationCard.vue
Normal file
550
photon-client/src/components/cameras/CameraCalibrationCard.vue
Normal file
@@ -0,0 +1,550 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { CalibrationBoardTypes, type VideoFormat, type Resolution } from "@/types/SettingTypes";
|
||||
import JsPDF from "jspdf";
|
||||
import { font as PromptRegular } from "@/assets/fonts/PromptRegular";
|
||||
import MonoLogo from "@/assets/images/logoMono.png";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvNumberInput from "@/components/common/cv-number-input.vue";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
|
||||
const settingsValid = ref(true);
|
||||
|
||||
const getCalibrationCoeffs = (resolution: Resolution) => {
|
||||
return useCameraSettingsStore().currentCameraSettings.completeCalibrations.find(cal => cal.resolution.width === resolution.width && cal.resolution.height === resolution.height);
|
||||
};
|
||||
const getUniqueVideoResolutions = (): VideoFormat[] => {
|
||||
const uniqueResolutions: VideoFormat[] = [];
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format, index) => {
|
||||
if(!uniqueResolutions.some(v => v.resolution.width === format.resolution.width && v.resolution.height === format.resolution.height)) {
|
||||
format.index = index;
|
||||
|
||||
const calib = getCalibrationCoeffs(format.resolution);
|
||||
if(calib !== undefined) {
|
||||
format.standardDeviation = calib.standardDeviation;
|
||||
format.mean = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
|
||||
format.horizontalFOV = 2 * Math.atan2(format.resolution.width/2, calib.intrinsics[0]) * (180/Math.PI);
|
||||
format.verticalFOV = 2 * Math.atan2(format.resolution.height/2, calib.intrinsics[4]) * (180/Math.PI);
|
||||
format.diagonalFOV = 2 * Math.atan2(Math.sqrt(format.resolution.width**2 + (format.resolution.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2, calib.intrinsics[0]) * (180/Math.PI);
|
||||
}
|
||||
uniqueResolutions.push(format);
|
||||
}
|
||||
});
|
||||
uniqueResolutions.sort((a, b) => (b.resolution.width + b.resolution.height) - (a.resolution.width + a.resolution.height));
|
||||
return uniqueResolutions;
|
||||
};
|
||||
const getUniqueVideoResolutionStrings = () => getUniqueVideoResolutions().map<{name: string, value: number}>(f => ({
|
||||
name: `${f.resolution.width} X ${f.resolution.height}`,
|
||||
// Index won't ever be undefined
|
||||
value: f.index || 0
|
||||
}));
|
||||
const calibrationDivisors = computed(() => [1, 2, 4].filter(v => {
|
||||
const currentRes = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return ((currentRes.width / v) >= 300 && (currentRes.height / v) >= 220) || (v === 1);
|
||||
}));
|
||||
|
||||
const squareSizeIn = ref(1);
|
||||
const patternWidth = ref(8);
|
||||
const patternHeight = ref(8);
|
||||
const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Chessboard);
|
||||
|
||||
const downloadCalibBoard = () => {
|
||||
const doc = new JsPDF({ unit: "in", format: "letter" });
|
||||
|
||||
doc.addFileToVFS("Prompt-Regular.tff", PromptRegular);
|
||||
doc.addFont("Prompt-Regular.tff", "Prompt-Regular", "normal");
|
||||
doc.setFont("Prompt-Regular");
|
||||
doc.setFontSize(12);
|
||||
|
||||
const paperWidth = 8.5;
|
||||
const paperHeight = 11.0;
|
||||
|
||||
switch (boardType.value) {
|
||||
case CalibrationBoardTypes.Chessboard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
|
||||
const xPos = chessboardStartX + squareX * squareSizeIn.value;
|
||||
const yPos = chessboardStartY + squareY * squareSizeIn.value;
|
||||
|
||||
// Only draw the odd squares to create the chessboard pattern
|
||||
if ((xPos + yPos + 0.25) % 2 === 0) {
|
||||
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CalibrationBoardTypes.DotBoard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const dotgridStartX = (paperWidth - (2 * (patternWidth.value - 1) + ((patternHeight.value - 1) % 2)) * squareSizeIn.value) / 2.0;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const dotgridStartY = (paperHeight - (patternHeight.value - squareSizeIn.value)) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
|
||||
const xPos = dotgridStartX + (2 * squareX + (squareY % 2)) * squareSizeIn.value;
|
||||
const yPos = dotgridStartY + squareY * squareSizeIn.value;
|
||||
|
||||
doc.circle(xPos, yPos, squareSizeIn.value / 4, "F");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw ruler pattern
|
||||
const lineStartX = 1.0;
|
||||
const lineEndX = paperWidth - lineStartX;
|
||||
const lineY = paperHeight - 1.0;
|
||||
|
||||
doc.setLineWidth(0.01);
|
||||
doc.line(lineStartX, lineY, lineEndX, lineY);
|
||||
|
||||
for (let tickX = lineStartX; tickX <= lineEndX; tickX++) {
|
||||
doc.line(tickX, lineY, tickX, lineY + 0.25);
|
||||
doc.text(`${tickX - 1}${tickX - 1 === 0 ? " in" : ""}`, tickX + 0.1, lineY + 0.25);
|
||||
}
|
||||
|
||||
// Add branding
|
||||
const logoImage = new Image();
|
||||
logoImage.src = MonoLogo;
|
||||
doc.addImage(logoImage, "PNG", 1.0, 0.75, 1.4, 0.5);
|
||||
|
||||
doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0,
|
||||
{
|
||||
maxWidth: (paperWidth - 2.0) / 2,
|
||||
align: "right"
|
||||
}
|
||||
);
|
||||
|
||||
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
|
||||
};
|
||||
|
||||
const importCalibrationFromCalibDB = ref();
|
||||
const openCalibUploadPrompt = () => {
|
||||
importCalibrationFromCalibDB.value.click();
|
||||
};
|
||||
const readImportedCalibration = ({ files } : { files: FileList}) => {
|
||||
files[0].text().then(text => {
|
||||
useCameraSettingsStore().importCalibDB({ payload: text, filename: files[0].name })
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: response.status === 200 ? "success" : "error"
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while uploading calibration file!",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isCalibrating = ref(false);
|
||||
const startCalibration = () => {
|
||||
useCameraSettingsStore().startPnPCalibration({
|
||||
squareSizeIn: squareSizeIn.value,
|
||||
patternHeight: patternHeight.value,
|
||||
patternWidth: patternWidth.value,
|
||||
boardType: boardType.value
|
||||
});
|
||||
// The Start PnP method already handles updating the backend so only a store update is required
|
||||
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
|
||||
isCalibrating.value = true;
|
||||
calibCanceled.value = false;
|
||||
};
|
||||
const showCalibEndDialog = ref(false);
|
||||
const calibCanceled = ref(false);
|
||||
const calibSuccess = ref<boolean | undefined>(undefined);
|
||||
const endCalibration = () => {
|
||||
if(!useStateStore().calibrationData.hasEnoughImages) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
|
||||
showCalibEndDialog.value = true;
|
||||
// Check if calibration finished cleanly or was canceled
|
||||
useCameraSettingsStore().endPnPCalibration()
|
||||
.then(() => {
|
||||
calibSuccess.value = true;
|
||||
})
|
||||
.catch(() => {
|
||||
calibSuccess.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
isCalibrating.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card
|
||||
class="pr-6 pb-3"
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>Camera Calibration</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="settingsValid"
|
||||
>
|
||||
<cv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
:select-cols="7"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Decimation"
|
||||
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||
:items="calibrationDivisors"
|
||||
:select-cols="7"
|
||||
@input="v => useCameraSettingsStore().changeCurrentPipelineSetting({streamingFrameDivisor: v}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="7"
|
||||
:items="['Chessboard', 'Dotboard']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<cv-number-input
|
||||
v-model="squareSizeIn"
|
||||
label="Pattern Spacing (in)"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v > 0) || 'Size must be positive']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
<cv-number-input
|
||||
v-model="patternWidth"
|
||||
label="Board Width (in)"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v >= 4) || 'Width must be at least 4']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
<cv-number-input
|
||||
v-model="patternHeight"
|
||||
label="Board Height (in)"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v >= 4) || 'Height must be at least 4']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-row
|
||||
align="start"
|
||||
class="pb-4 pt-2"
|
||||
>
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
dense
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Resolution
|
||||
</th>
|
||||
<th>
|
||||
Mean Error
|
||||
</th>
|
||||
<th>
|
||||
Standard Deviation
|
||||
</th>
|
||||
<th>
|
||||
Horizontal FOV
|
||||
</th>
|
||||
<th>
|
||||
Vertical FOV
|
||||
</th>
|
||||
<th>
|
||||
Diagonal FOV
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in getUniqueVideoResolutions()"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ value.resolution.width }} X {{ value.resolution.height }}</td>
|
||||
<td>{{ value.mean !== undefined ? value.mean.toFixed(2) + "px" : "-" }}</td>
|
||||
<td>{{ value.standardDeviation !== undefined ? value.standardDeviation.toFixed(2) + "px" : "-" }}</td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-row justify="center">
|
||||
<v-chip
|
||||
v-show="isCalibrating"
|
||||
label
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
|
||||
>
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least {{ useStateStore().calibrationData.minimumImageCount }}
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isCalibrating">
|
||||
<v-col
|
||||
cols="12"
|
||||
class="pt-0"
|
||||
>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="8"
|
||||
:step="0.1"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="8"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:label-cols="4"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraRedGain: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBlueGain: args}, false)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="secondary"
|
||||
style="width: 100%;"
|
||||
:disabled="!settingsValid"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot(true) : startCalibration()"
|
||||
>
|
||||
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
style="width: 100%;"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
color="accent"
|
||||
small
|
||||
outlined
|
||||
style="width: 100%;"
|
||||
:disabled="!settingsValid"
|
||||
@click="downloadCalibBoard"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Generate Board
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="isCalibrating"
|
||||
small
|
||||
style="width: 100%;"
|
||||
@click="openCalibUploadPrompt"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-upload
|
||||
</v-icon>
|
||||
Import From CalibDB
|
||||
</v-btn>
|
||||
<input
|
||||
ref="importCalibrationFromCalibDB"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none;"
|
||||
@change="readImportedCalibration"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-dialog
|
||||
v-model="showCalibEndDialog"
|
||||
width="500px"
|
||||
:persistent="true"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title class="pb-8">
|
||||
Camera Calibration
|
||||
</v-card-title>
|
||||
<div class="ml-3">
|
||||
<v-col style="text-align: center">
|
||||
<template v-if="calibCanceled">
|
||||
<v-icon
|
||||
color="blue"
|
||||
size="70"
|
||||
>
|
||||
mdi-cancel
|
||||
</v-icon>
|
||||
<v-card-text>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration process.</v-card-text>
|
||||
</template>
|
||||
<template v-else-if="isCalibrating">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
:size="70"
|
||||
:width="8"
|
||||
color="accent"
|
||||
/>
|
||||
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
||||
</template>
|
||||
<template v-else-if="calibSuccess">
|
||||
<v-icon
|
||||
color="green"
|
||||
size="70"
|
||||
>
|
||||
mdi-check-bold
|
||||
</v-icon>
|
||||
<v-card-text>
|
||||
Camera has been successfully calibrated for {{ getUniqueVideoResolutionStrings().find(v => v.value === useStateStore().calibrationData.videoFormatIndex).name }}!
|
||||
</v-card-text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon
|
||||
color="red"
|
||||
size="70"
|
||||
>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
<v-card-text>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align with the corners of the chessboard, and try again. More information is available in the program logs.</v-card-text>
|
||||
</template>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!isCalibrating"
|
||||
color="white"
|
||||
text
|
||||
@click="showCalibEndDialog = false"
|
||||
>
|
||||
OK
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
text-align: center;
|
||||
|
||||
th, td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
photon-client/src/components/cameras/CameraSettingsCard.vue
Normal file
82
photon-client/src/components/cameras/CameraSettingsCard.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvNumberInput from "@/components/common/cv-number-input.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ref } from "vue";
|
||||
|
||||
const currentFov = ref(useCameraSettingsStore().currentCameraSettings.fov.value);
|
||||
|
||||
const saveCameraSettings = () => {
|
||||
useCameraSettingsStore().updateCameraSettings({ fov: currentFov.value }, true)
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "success",
|
||||
message: response.data.text || response.data
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if(error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if(error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
class="mb-3 pr-6 pb-3"
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>Camera Settings</v-card-title>
|
||||
<div class="ml-5">
|
||||
<cv-select
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
:select-cols="8"
|
||||
@input="args => {
|
||||
currentFov = useCameraSettingsStore().cameras[args].fov.value;
|
||||
useCameraSettingsStore().setCurrentCameraIndex(args);
|
||||
}"
|
||||
/>
|
||||
<cv-number-input
|
||||
v-model="currentFov"
|
||||
:tooltip="!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
|
||||
label="Maximum Diagonal FOV"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
style="margin-top:10px"
|
||||
small
|
||||
color="secondary"
|
||||
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
|
||||
@click="saveCameraSettings"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-content-save
|
||||
</v-icon>
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
172
photon-client/src/components/cameras/CamerasView.vue
Normal file
172
photon-client/src/components/cameras/CamerasView.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
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 driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
|
||||
});
|
||||
|
||||
const fpsTooLow = computed<boolean>(() => {
|
||||
const currFPS = useStateStore().pipelineResults?.fps || 0;
|
||||
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
|
||||
const driverMode = useCameraSettingsStore().isDriverMode;
|
||||
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
|
||||
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
|
||||
|
||||
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
class="mb-3 pr-6 pb-3 pa-4"
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title
|
||||
class="pb-0 mb-2 pl-4 pt-1"
|
||||
style="min-height: 50px; justify-content: space-between; align-content: center"
|
||||
>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<div>
|
||||
<span
|
||||
class="mr-4"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
Cameras
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<v-chip
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0;"
|
||||
>
|
||||
<span class="pr-1">
|
||||
{{ Math.round(useStateStore().pipelineResults?.fps || 0) }} FPS – {{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto;"
|
||||
color="accent"
|
||||
class="pt-2"
|
||||
/>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<div
|
||||
class="stream-container pb-4"
|
||||
>
|
||||
<div class="stream">
|
||||
<photon-camera-stream
|
||||
v-show="value.includes(0)"
|
||||
stream-type="Raw"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="stream">
|
||||
<photon-camera-stream
|
||||
v-show="value.includes(1)"
|
||||
stream-type="Processed"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="pt-4">
|
||||
<p style="color: white;">
|
||||
Stream Display
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="localValue"
|
||||
:multiple="true"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon>mdi-import</v-icon>
|
||||
<span>Raw</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon>mdi-export</v-icon>
|
||||
<span>Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stream-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stream {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 512px) and (max-width: 960px) {
|
||||
.stream-container {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.stream {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
iconName: string,
|
||||
color?: string,
|
||||
tooltip?: string,
|
||||
right?: boolean,
|
||||
hover?: boolean
|
||||
}>(), {
|
||||
right: false,
|
||||
hover: false
|
||||
});
|
||||
|
||||
const hoverClass = props.hover ? "hover" : "";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip
|
||||
:right="right"
|
||||
:bottom="!right"
|
||||
nudge-right="10"
|
||||
:disabled="tooltip === undefined"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:color="color"
|
||||
@click="handleClick"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ text }}
|
||||
{{ iconName }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
@@ -20,33 +37,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icon',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['color', 'tooltip', 'text', 'right', 'hover'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hoverClass: {
|
||||
get() {
|
||||
if (this.hover !== undefined) {
|
||||
return "hover";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click');
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover:hover {
|
||||
color: white !important;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<img
|
||||
:id="id"
|
||||
crossOrigin="anonymous"
|
||||
:style="styleObject"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
@click="clickHandler"
|
||||
@error="loadErrHandler"
|
||||
>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CvImage",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected', 'alt'],
|
||||
data() {
|
||||
return {
|
||||
seed: 1.0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
let ret = {
|
||||
"border-radius": "3px",
|
||||
"display": "block",
|
||||
"object-fit": "contain",
|
||||
"background-size:": "contain",
|
||||
"object-position": "50% 50%",
|
||||
"max-width": "100%",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"max-height": this.maxHeight,
|
||||
height: `${this.scale}%`,
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/icons/eyedropper.svg")}),` : "pointer") + "default",
|
||||
};
|
||||
|
||||
if (this.$vuetify.breakpoint.xl) {
|
||||
ret["max-height"] = this.maxHeightXl;
|
||||
} else if (this.$vuetify.breakpoint.lg) {
|
||||
ret["max-height"] = this.maxHeightLg;
|
||||
} else if (this.$vuetify.breakpoint.md) {
|
||||
ret["max-height"] = this.maxHeightMd;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
src: {
|
||||
get() {
|
||||
const port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
//Invalid port, keep it spiny
|
||||
return require("../../../public/loading.svg");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
return this.getSrcURLFromPort(port);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.reload(); // Force reload image on creation
|
||||
},
|
||||
methods: {
|
||||
getCurPort(){
|
||||
let port = -1;
|
||||
if(this.disconnected){
|
||||
//Disconnected, port is unknown.
|
||||
port = -1;
|
||||
} else {
|
||||
//Connected - get the port
|
||||
if(this.id === 'raw-stream'){
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort
|
||||
} else {
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort
|
||||
}
|
||||
}
|
||||
return port;
|
||||
},
|
||||
getSrcURLFromPort(port){
|
||||
return "http://" + location.hostname + ":" + port + "/stream.mjpg" + "?" + this.seed;
|
||||
},
|
||||
loadErrHandler(event) {
|
||||
console.log(event);
|
||||
console.log("Error loading image, attempting to do it again...");
|
||||
this.reload();
|
||||
},
|
||||
clickHandler(event) {
|
||||
if(this.colorPicking){
|
||||
this.$emit('click', event);
|
||||
} else {
|
||||
const port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
console.log("No valid port, ignoring click.");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
window.open(this.getSrcURLFromPort(port), '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
reload() {
|
||||
this.seed = new Date().getTime();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,64 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
|
||||
|
||||
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,
|
||||
labelCols?: number,
|
||||
inputCols?: number,
|
||||
rules?: ((v: string) => boolean | string)[]
|
||||
}>(), {
|
||||
disabled: false,
|
||||
inputCols: 8
|
||||
});
|
||||
|
||||
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":
|
||||
if(!(props.rules || []).some(v => v(localValue.value) === false || typeof v(localValue.value) === "string")) {
|
||||
emit("onEnter", localValue.value);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
emit("onEscape");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="labelCols || (12 - (inputCols || 8))">
|
||||
<v-col :cols="labelCols || (12 - inputCols)">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="inputCols || 8">
|
||||
|
||||
<v-col :cols="inputCols">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
class="mt-1 pt-2"
|
||||
@keydown="handleKeyboard"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
components: {
|
||||
TooltippedLabel
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled', 'errorMessage', 'inputCols', 'labelCols', 'rules', 'tooltip'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyboard(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.$emit("Enter");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/cv-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,
|
||||
disabled?: boolean,
|
||||
labelCols?: number,
|
||||
rules?: ((v: number) => boolean | string)[],
|
||||
step?: number
|
||||
}>(), {
|
||||
disabled: false,
|
||||
labelCols: 2,
|
||||
step: 1
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: v => emit("input", parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="labelCols || 2">
|
||||
<v-col :cols="labelCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
@@ -28,30 +57,3 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'NumberInput',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip', 'disabled'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', parseFloat(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
|
||||
|
||||
const props = 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[]
|
||||
}>(), {
|
||||
disabled: false,
|
||||
inputCols: 8
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: v => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (inputCols || 8)">
|
||||
<v-col :cols="12 - inputCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="inputCols || 8">
|
||||
<v-col :cols="inputCols">
|
||||
<v-radio-group
|
||||
v-model="localValue"
|
||||
row
|
||||
@@ -18,7 +45,7 @@
|
||||
:mandatory="true"
|
||||
>
|
||||
<v-radio
|
||||
v-for="(radioName,index) in list"
|
||||
v-for="(radioName, index) in list"
|
||||
:key="index"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
@@ -30,33 +57,3 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'Radio',
|
||||
components: {
|
||||
TooltippedLabel
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'list', 'disabled', 'inputCols', 'tooltip'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
|
||||
|
||||
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,
|
||||
sliderCols?: number,
|
||||
disabled?: boolean,
|
||||
inverted?: boolean,
|
||||
}>(), {
|
||||
step: 1,
|
||||
disabled: false,
|
||||
inverted: false,
|
||||
sliderCols: 10
|
||||
});
|
||||
|
||||
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];
|
||||
},
|
||||
set: v => emit("input", v)
|
||||
});
|
||||
|
||||
const changeFromSlot = (v: number, i: number) => {
|
||||
// localValue.value must be replaced for a reactive change to take place
|
||||
const temp = localValue.value;
|
||||
temp[i] = v;
|
||||
localValue.value = temp;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col cols="2">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="10">
|
||||
<v-col :cols="sliderCols">
|
||||
<v-range-slider
|
||||
:value="localValue"
|
||||
v-model="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
@@ -23,44 +65,37 @@
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
:step="step"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
color="accent"
|
||||
:value="localValue[0]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="prependFocused = true"
|
||||
@blur="prependFocused = false"
|
||||
@input="v => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
color="accent"
|
||||
:value="localValue[1]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="appendFocused = true"
|
||||
@blur="appendFocused = false"
|
||||
@input="v => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
@@ -68,67 +103,3 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: "RangeSlider",
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["name", "min", "max", "value", "step", "tooltip", "disabled", "inverted"],
|
||||
data() {
|
||||
return {
|
||||
prependFocused: false,
|
||||
appendFocused: false,
|
||||
currentTempVal: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return Object.values(this.value || [0, 0]);
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
async handleChange(val) {
|
||||
this.currentTempVal = val;
|
||||
|
||||
await this.delay(200).then(() => {
|
||||
let i = 0;
|
||||
if (!this.prependFocused && this.appendFocused) {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
// will get empty string if entry is not a number
|
||||
if (this.currentTempVal !== val || val === "") return;
|
||||
|
||||
let parsed = parseFloat(val);
|
||||
let tmp = this.localValue;
|
||||
tmp[i] = Math.max(this.min, Math.min(parsed, this.max));
|
||||
this.localValue = tmp;
|
||||
|
||||
this.$emit("rollback", this.localValue);
|
||||
});
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.prependFocused || !this.appendFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,66 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
|
||||
|
||||
interface SelectItem {
|
||||
name: string | number,
|
||||
value: string | number,
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
label?: string,
|
||||
tooltip?: string,
|
||||
selectCols?: number,
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number,
|
||||
disabled?: boolean,
|
||||
items: string[] | number[] | SelectItem[]
|
||||
}>(), {
|
||||
selectCols: 9,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: v => emit("input", v)
|
||||
});
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
// Check if the prop exists on the object to infer object type
|
||||
if((props.items[0] as SelectItem).name) {
|
||||
return props.items as SelectItem[];
|
||||
}
|
||||
return props.items.map((v, i) => ({ name: v, value: i }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (selectCols || 9)">
|
||||
<v-col :cols="12 - selectCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="selectCols || 9">
|
||||
<v-col :cols="selectCols">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="indexList"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-value="index"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'Select',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['list', 'name', 'value', 'disabled', 'filteredIndices', 'selectCols', 'rules', 'tooltip'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
indexList() {
|
||||
let list = [];
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
if (this.filteredIndices instanceof Set && this.filteredIndices.has(i)) continue;
|
||||
list.push({
|
||||
name: this.list[i],
|
||||
index: i
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
label?: string,
|
||||
tooltip?: string,
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
step?: number
|
||||
disabled?: boolean,
|
||||
sliderCols?: number,
|
||||
}>(), {
|
||||
step: 1,
|
||||
disabled: false,
|
||||
sliderCols: 8
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: v => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (sliderCols || 8)">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols || 8">
|
||||
<v-col :cols="sliderCols">
|
||||
<v-slider
|
||||
:value="localValue"
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
@@ -21,29 +51,21 @@
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
@start="isClicked = true"
|
||||
@end="isClicked = false"
|
||||
@change="handleClick"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
:value="localValue"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
</template>
|
||||
</v-slider>
|
||||
@@ -51,58 +73,3 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: "Slider",
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled", "tooltip"],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
isClicked: false,
|
||||
currentBoxVal: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
this.currentBoxVal = val;
|
||||
setTimeout(() => {
|
||||
if (this.currentBoxVal !== val) return;
|
||||
// if (this.isFocused) {
|
||||
this.localValue = parseFloat(val);
|
||||
this.$emit("rollback", this.localValue);
|
||||
// }
|
||||
}, 200);
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.isFocused && this.isClicked) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
handleClick(val) {
|
||||
if (!this.isFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/cv-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: boolean,
|
||||
disabled?: boolean,
|
||||
labelCols?: number,
|
||||
switchCols?: number
|
||||
}>(), {
|
||||
disabled: false,
|
||||
labelCols: 2
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: boolean): void
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: v => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="textCols || 2">
|
||||
<v-col :cols="(12 - switchCols) || labelCols">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
:label="label"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="12 - (textCols || 2)">
|
||||
<v-col :cols="switchCols || (12 - labelCols)">
|
||||
<v-switch
|
||||
v-model="localValue"
|
||||
dark
|
||||
:disabled="disabled"
|
||||
color="#ffd843"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'CVSwitch',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled', 'textCols', 'tooltip'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string,
|
||||
tooltip?: string
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip
|
||||
@@ -5,23 +12,15 @@
|
||||
right
|
||||
open-delay="300"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<span
|
||||
style="cursor: text !important;"
|
||||
class="white--text"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>{{ text }}</span>
|
||||
>{{ label }}</span>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TooltippedLabel',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['text', 'tooltip'],
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
import { computed, ref } from "vue";
|
||||
import CvIcon from "@/components/common/cv-icon.vue";
|
||||
import CvInput from "@/components/common/cv-input.vue";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
|
||||
const changeCurrentCameraIndex = (index: number) => {
|
||||
useCameraSettingsStore().setCurrentCameraIndex(index, true);
|
||||
|
||||
switch (useCameraSettingsStore().cameras[index].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;
|
||||
}
|
||||
};
|
||||
|
||||
// Common RegEx used for naming both pipelines and cameras
|
||||
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
|
||||
|
||||
// Camera Name Edit
|
||||
const isCameraNameEdit = ref(false);
|
||||
const currentCameraName = ref(useCameraSettingsStore().currentCameraSettings.nickname);
|
||||
const startCameraNameEdit = () => {
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
isCameraNameEdit.value = true;
|
||||
};
|
||||
const checkCameraName = (name: string): string | boolean => {
|
||||
if(!nameChangeRegex.test(name)) return "A camera name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
|
||||
if(useCameraSettingsStore().cameraNames.some(cameraName => cameraName === name)) return "This camera name has already been used";
|
||||
|
||||
return true;
|
||||
};
|
||||
const saveCameraNameEdit = (newName: string) => {
|
||||
useCameraSettingsStore().changeCameraNickname(newName, false)
|
||||
.then(response => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "success",
|
||||
message: response.data.text || response.data
|
||||
});
|
||||
useCameraSettingsStore().currentCameraSettings.nickname = newName;
|
||||
})
|
||||
.catch(error => {
|
||||
if(error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if(error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
})
|
||||
.finally(() => isCameraNameEdit.value = false);
|
||||
};
|
||||
const cancelCameraNameEdit = () => {
|
||||
isCameraNameEdit.value = false;
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
};
|
||||
|
||||
// Pipeline Name Edit
|
||||
const pipelineNamesWrapper = computed<{name: string, value: number}[]>(() => {
|
||||
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
|
||||
|
||||
if(useCameraSettingsStore().isDriverMode) {
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if(useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
|
||||
return pipelineNames;
|
||||
});
|
||||
const isPipelineNameEdit = ref(false);
|
||||
const currentPipelineName = ref(useCameraSettingsStore().currentPipelineSettings.pipelineNickname);
|
||||
const startPipelineNameEdit = () => {
|
||||
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
|
||||
isPipelineNameEdit.value = true;
|
||||
};
|
||||
const checkPipelineName = (name: string): string | boolean => {
|
||||
if(!nameChangeRegex.test(name)) return "A pipeline name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
|
||||
if(useCameraSettingsStore().pipelineNames.some(pipelineName => pipelineName === name)) return "This pipeline name has already been used";
|
||||
|
||||
return true;
|
||||
};
|
||||
const savePipelineNameEdit = (name: string) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineNickname(name);
|
||||
isPipelineNameEdit.value = false;
|
||||
};
|
||||
const cancelPipelineNameEdit = () => {
|
||||
isPipelineNameEdit.value = false;
|
||||
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
|
||||
};
|
||||
|
||||
// Pipeline Creation
|
||||
const showPipelineCreationDialog = ref(false);
|
||||
const newPipelineName = ref("");
|
||||
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
||||
const showCreatePipelineDialog = () => {
|
||||
newPipelineName.value = "";
|
||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
showPipelineCreationDialog.value = true;
|
||||
};
|
||||
const createNewPipeline = () => {
|
||||
const type = newPipelineType.value;
|
||||
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
|
||||
useCameraSettingsStore().createNewPipeline(newPipelineName.value, type);
|
||||
showPipelineCreationDialog.value = false;
|
||||
};
|
||||
const cancelPipelineCreation = () => {
|
||||
showPipelineCreationDialog.value = false;
|
||||
newPipelineName.value = "";
|
||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
};
|
||||
|
||||
// Pipeline Creation
|
||||
const showPipelineDeletionConfirmationDialog = ref(false);
|
||||
const confirmDeleteCurrentPipeline = () => {
|
||||
useCameraSettingsStore().deleteCurrentPipeline();
|
||||
showPipelineDeletionConfirmationDialog.value = false;
|
||||
};
|
||||
|
||||
// Pipeline Type Change
|
||||
const showPipelineTypeChangeDialog = ref(false);
|
||||
const pipelineTypesWrapper = computed<{name: string, value: number}[]>(() => {
|
||||
const pipelineTypes =[
|
||||
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
|
||||
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag }
|
||||
// { name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||
];
|
||||
|
||||
if(useCameraSettingsStore().isDriverMode) {
|
||||
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if(useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
|
||||
return pipelineTypes;
|
||||
});
|
||||
const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
||||
const currentPipelineType = computed<WebsocketPipelineType>({
|
||||
get: () => {
|
||||
if(useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
|
||||
if(useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
|
||||
return pipelineType.value;
|
||||
},
|
||||
set: v => {
|
||||
pipelineType.value = v;
|
||||
}
|
||||
});
|
||||
const confirmChangePipelineType = () => {
|
||||
const type = currentPipelineType.value;
|
||||
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
|
||||
useCameraSettingsStore().changeCurrentPipelineType(type);
|
||||
showPipelineTypeChangeDialog.value = false;
|
||||
};
|
||||
const cancelChangePipelineType = () => {
|
||||
pipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
showPipelineTypeChangeDialog.value = false;
|
||||
};
|
||||
|
||||
// Change Props whenever the pipeline settings are changed
|
||||
useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
const currentCameraSettings = state.cameras[useStateStore().currentCameraIndex];
|
||||
|
||||
switch (currentCameraSettings.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;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
color="primary"
|
||||
>
|
||||
<v-row style="padding: 12px 12px 0 24px">
|
||||
<v-col
|
||||
cols="10"
|
||||
class="pa-0"
|
||||
>
|
||||
<cv-select
|
||||
v-if="!isCameraNameEdit"
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
@input="changeCurrentCameraIndex"
|
||||
/>
|
||||
<cv-input
|
||||
v-else
|
||||
v-model="currentCameraName"
|
||||
class="pt-2"
|
||||
:input-cols="12-3"
|
||||
:rules="[v => checkCameraName(v)]"
|
||||
label="Camera"
|
||||
@onEnter="saveCameraNameEdit"
|
||||
@onEscape="cancelCameraNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<cv-icon
|
||||
color="#c5c5c5"
|
||||
icon-name="mdi-pencil"
|
||||
tooltip="Edit Camera Name"
|
||||
@click="startCameraNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 0 24px;">
|
||||
<v-col
|
||||
cols="10"
|
||||
class="pa-0"
|
||||
>
|
||||
<cv-select
|
||||
v-if="!isPipelineNameEdit"
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="useCameraSettingsStore().isDriverMode
|
||||
|| useCameraSettingsStore().isCalibrationMode"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
<cv-input
|
||||
v-else
|
||||
v-model="currentPipelineName"
|
||||
:input-cols="12-3"
|
||||
:rules="[v => checkPipelineName(v)]"
|
||||
label="Pipeline"
|
||||
@onEnter="v => savePipelineNameEdit(v)"
|
||||
@onEscape="cancelPipelineNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
class="pa-0"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<v-menu
|
||||
v-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>
|
||||
</template>
|
||||
<v-list
|
||||
dark
|
||||
dense
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item @click="startPipelineNameEdit">
|
||||
<v-list-item-title>
|
||||
<cv-icon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
icon-name="mdi-pencil"
|
||||
tooltip="Edit pipeline name"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showCreatePipelineDialog">
|
||||
<v-list-item-title>
|
||||
<cv-icon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
icon-name="mdi-plus"
|
||||
tooltip="Add new pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
|
||||
<v-list-item-title>
|
||||
<cv-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="useCameraSettingsStore().duplicatePipeline(useCameraSettingsStore().currentCameraSettings.currentPipelineIndex)">
|
||||
<v-list-item-title>
|
||||
<cv-icon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
icon-name="mdi-content-copy"
|
||||
tooltip="Duplicate pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 12px 24px;">
|
||||
<v-col
|
||||
cols="10"
|
||||
class="pa-0"
|
||||
>
|
||||
<cv-select
|
||||
v-model="currentPipelineType"
|
||||
label="Type"
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:disabled="useCameraSettingsStore().isDriverMode
|
||||
|| useCameraSettingsStore().isCalibrationMode"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog
|
||||
v-model="showPipelineCreationDialog"
|
||||
dark
|
||||
persistent
|
||||
width="500"
|
||||
>
|
||||
<v-card
|
||||
dark
|
||||
color="primary"
|
||||
>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
style="font-family: 'Prompt', sans-serif !important;"
|
||||
primary-title
|
||||
>
|
||||
Create New Pipeline
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<cv-input
|
||||
v-model="newPipelineName"
|
||||
placeholder="Pipeline Name"
|
||||
:label-cols="3"
|
||||
:input-cols="12-3"
|
||||
label="Pipeline Name"
|
||||
:rules="[v => checkPipelineName(v)]"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="newPipelineType"
|
||||
:select-cols="12-3"
|
||||
label="Tracking Type"
|
||||
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:items="[
|
||||
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
|
||||
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag }
|
||||
// { name: 'Aruco', value: WebsocketPipelineType.Aruco }
|
||||
]"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
:disabled="checkPipelineName(newPipelineName) !== true"
|
||||
@click="createNewPipeline"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@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-card-title
|
||||
class="headline"
|
||||
style="font-family: 'Prompt', sans-serif !important;"
|
||||
primary-title
|
||||
>
|
||||
Pipeline Deletion Confirmation
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete this pipeline? This cannot be undone.
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="confirmDeleteCurrentPipeline"
|
||||
>
|
||||
Yes, I'm sure
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
@click="showPipelineDeletionConfirmationDialog = false"
|
||||
>
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="showPipelineTypeChangeDialog"
|
||||
persistent
|
||||
width="600"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>Change Pipeline Type</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export settings.
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="confirmChangePipelineType"
|
||||
>
|
||||
Yes, I'm sure
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
@click="cancelChangePipelineType"
|
||||
>
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
100
photon-client/src/components/dashboard/CamerasCard.vue
Normal file
100
photon-client/src/components/dashboard/CamerasCard.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
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 driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
|
||||
});
|
||||
|
||||
const fpsTooLow = computed<boolean>(() => {
|
||||
const currFPS = useStateStore().pipelineResults?.fps || 0;
|
||||
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
|
||||
const driverMode = useCameraSettingsStore().isDriverMode;
|
||||
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
|
||||
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
|
||||
|
||||
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
style="display: flex; flex-direction: column"
|
||||
dark
|
||||
>
|
||||
<v-card-title
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
style="min-height: 50px; justify-content: space-between; align-content: center"
|
||||
>
|
||||
<div class="pt-2">
|
||||
<span class="mr-4">Cameras</span>
|
||||
<v-chip
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<span class="pr-1">
|
||||
Processing @ {{ Math.round(useStateStore().pipelineResults?.fps || 0) }} FPS –
|
||||
</span>
|
||||
<span v-if="fpsTooLow && !useCameraSettingsStore().currentPipelineSettings.inputShouldShow && useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective">
|
||||
HSV thresholds are too broad; narrow them for better performance
|
||||
</span>
|
||||
<span v-else-if="fpsTooLow && useCameraSettingsStore().currentPipelineSettings.inputShouldShow">
|
||||
stop viewing the raw stream for better performance
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<div>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto;"
|
||||
color="accent"
|
||||
class="pt-2"
|
||||
/>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider style="border-color: white" />
|
||||
<v-row
|
||||
class="pl-3 pr-3 pt-3 pb-3"
|
||||
style="flex-wrap: nowrap; justify-content: center"
|
||||
>
|
||||
<v-col
|
||||
v-show="value.includes(0)"
|
||||
style="max-width: 500px; display: flex; align-items: center"
|
||||
>
|
||||
<photon-camera-stream
|
||||
id="input-camera-stream"
|
||||
stream-type="Raw"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-show="value.includes(1)"
|
||||
style="max-width: 500px; display: flex; align-items: center"
|
||||
>
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
162
photon-client/src/components/dashboard/ConfigOptions.vue
Normal file
162
photon-client/src/components/dashboard/ConfigOptions.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
|
||||
import ThresholdTab from "@/components/dashboard/tabs/ThresholdTab.vue";
|
||||
import ContoursTab from "@/components/dashboard/tabs/ContoursTab.vue";
|
||||
import AprilTagTab from "@/components/dashboard/tabs/AprilTagTab.vue";
|
||||
import ArucoTab from "@/components/dashboard/tabs/ArucoTab.vue";
|
||||
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 { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
interface ConfigOption {
|
||||
tabName: string,
|
||||
component: Component
|
||||
}
|
||||
|
||||
const allTabs = Object.freeze({
|
||||
inputTab: {
|
||||
tabName: "Input",
|
||||
component: InputTab
|
||||
},
|
||||
thresholdTab: {
|
||||
tabName: "Threshold",
|
||||
component: ThresholdTab
|
||||
},
|
||||
contoursTab: {
|
||||
tabName: "Contours",
|
||||
component: ContoursTab
|
||||
},
|
||||
apriltagTab: {
|
||||
tabName: "AprilTag",
|
||||
component: AprilTagTab
|
||||
},
|
||||
arucoTab: {
|
||||
tabName: "Aruco",
|
||||
component: ArucoTab
|
||||
},
|
||||
outputTab: {
|
||||
tabName: "Output",
|
||||
component: OutputTab
|
||||
},
|
||||
targetsTab: {
|
||||
tabName: "Targets",
|
||||
component: TargetsTab
|
||||
},
|
||||
pnpTab: {
|
||||
tabName: "PnP",
|
||||
component: PnPTab
|
||||
},
|
||||
map3dTab: {
|
||||
tabName: "3D",
|
||||
component: Map3DTab
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
if(smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
|
||||
return [Object.values(allTabs)];
|
||||
} else if(mdAndDown || !useStateStore().sidebarFolded) {
|
||||
return [
|
||||
[allTabs.inputTab, allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if(lgAndDown) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if(xl) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab],
|
||||
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
// Just return the input tab because we know that is always the case in driver mode
|
||||
if(useCameraSettingsStore().isDriverMode) return [[allTabs.inputTab]];
|
||||
|
||||
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
|
||||
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
|
||||
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
|
||||
|
||||
return getTabGroups().map(tabGroup => tabGroup.filter(tabConfig =>
|
||||
!(!allow3d && tabConfig.tabName === "3D") //Filter out 3D tab any time 3D isn't calibrated
|
||||
&& !((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
|
||||
&& !((isAprilTag || isAruco) && (tabConfig.tabName === "Threshold")) //Filter out threshold tab if we're doing AprilTags
|
||||
&& !((isAprilTag || isAruco) && (tabConfig.tabName === "Contours")) //Filter out contours if we're doing AprilTags
|
||||
&& !(!isAprilTag && tabConfig.tabName === "AprilTag") //Filter out apriltag unless we actually are doing AprilTags
|
||||
&& !(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
|
||||
));
|
||||
});
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
// Force the current tab to the input tab on driver mode change
|
||||
if(useCameraSettingsStore().isDriverMode) {
|
||||
selectedTabs.value[0] = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row
|
||||
no-gutters
|
||||
class="tabGroups"
|
||||
>
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<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-tab
|
||||
v-for="(tabConfig, index) in tabGroupData"
|
||||
:key="index"
|
||||
>
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-4 pb-2">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-slide-group__next--disabled, .v-slide-group__prev--disabled {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
118
photon-client/src/components/dashboard/StreamConfigCard.vue
Normal file
118
photon-client/src/components/dashboard/StreamConfigCard.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
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 processingMode = computed<number>({
|
||||
get: () => useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1: 0,
|
||||
set: v => {
|
||||
if(useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: v === 1 }, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
style="height: 100%; display: flex; flex-direction: column"
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
class="pa-3 pb-0"
|
||||
>
|
||||
<v-col>
|
||||
<p style="color: white;">
|
||||
Processing Mode
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="processingMode"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
>
|
||||
<v-icon>mdi-square-outline</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated"
|
||||
>
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
align="center"
|
||||
class="pa-3 pt-0"
|
||||
>
|
||||
<v-col>
|
||||
<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>mdi-import</v-icon>
|
||||
<span>Raw</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-export</v-icon>
|
||||
<span>Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
85
photon-client/src/components/dashboard/tabs/AprilTagTab.vue
Normal file
85
photon-client/src/components/dashboard/tabs/AprilTagTab.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// 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 = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.AprilTag">
|
||||
<cv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({tagFamily: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Blur"
|
||||
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({blur: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Threads"
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({threads: value}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
class="pt-2"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({refineEdges: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decision Margin Cutoff"
|
||||
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)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Pose Estimation Iterations"
|
||||
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)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
50
photon-client/src/components/dashboard/tabs/ArucoTab.vue
Normal file
50
photon-client/src/components/dashboard/tabs/ArucoTab.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// 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 = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.Aruco">
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Corner Iterations"
|
||||
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
|
||||
:min="30"
|
||||
:max="1000"
|
||||
:step="5"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.cornerAccuracy"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Corner Accuracy"
|
||||
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
|
||||
:min="0.01"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerAccuracy: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
213
photon-client/src/components/dashboard/tabs/ContoursTab.vue
Normal file
213
photon-client/src/components/dashboard/tabs/ContoursTab.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvRangeSlider from "@/components/common/cv-range-slider.vue";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// 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 = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
// TODO fix cv-range-slider so that store access doesn't need to be deferred
|
||||
const contourArea = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourArea = v
|
||||
});
|
||||
const contourRatio = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourRatio = v
|
||||
});
|
||||
const contourFullness = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourFullness) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourFullness = v
|
||||
});
|
||||
const contourPerimeter = computed<[number, number]>({
|
||||
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourPerimeter) as [number, number] : [0, 0] as [number, number],
|
||||
set: v => {
|
||||
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourPerimeter = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
const contourRadius = computed<[number, number]>({
|
||||
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourRadius) as [number, number] : [0, 0] as [number, number],
|
||||
set: v => {
|
||||
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourRadius = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-range-slider
|
||||
v-model="contourArea"
|
||||
label="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourArea: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
|
||||
v-model="contourRatio"
|
||||
label="Ratio (W/H)"
|
||||
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRatio: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
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']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourFullness"
|
||||
label="Fullness"
|
||||
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFullness: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourPerimeter"
|
||||
label="Perimeter"
|
||||
tooltip="Min and max perimeter of the shape, in pixels"
|
||||
min="0"
|
||||
max="4000"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourPerimeter: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
|
||||
label="Speckle Rejection"
|
||||
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSpecklePercentage: value}, false)"
|
||||
/>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeX"
|
||||
label="X Filter Tightness"
|
||||
tooltip="Rejects contours whose center X is further than X standard deviations left/right of the mean X location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeX: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeY"
|
||||
label="Y Filter Tightness"
|
||||
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeY: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
|
||||
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']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourGroupingMode: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
|
||||
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']"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourIntersection: value}, false)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
<v-divider class="mt-3" />
|
||||
<cv-select
|
||||
v-model="currentPipelineSettings.contourShape"
|
||||
label="Target Shape"
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourShape: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.accuracyPercentage"
|
||||
:disabled="currentPipelineSettings.contourShape < 1"
|
||||
label="Shape Simplification"
|
||||
tooltip="How much we should simply the input contour before checking how many sides it has"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({accuracyPercentage: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.circleDetectThreshold"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle match distance"
|
||||
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleDetectThreshold: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-model="contourRadius"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Radius"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRadius: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.maxCannyThresh"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Max Canny Threshold"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({maxCannyThresh: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.circleAccuracy"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle Accuracy"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleAccuracy: value}, false)"
|
||||
/>
|
||||
<v-divider class="mt-3" />
|
||||
</template>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
|
||||
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']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSortMode: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
132
photon-client/src/components/dashboard/tabs/InputTab.vue
Normal file
132
photon-client/src/components/dashboard/tabs/InputTab.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// 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(() => ["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({ name: v, value: i, disabled: useSettingsStore().gpuAccelerationEnabled ? [1, 3].includes(i) : false })));
|
||||
|
||||
const streamDivisors = [1, 2, 4, 6];
|
||||
const getFilteredStreamDivisors = (): number[] => {
|
||||
const currentResolutionWidth = useCameraSettingsStore().currentVideoFormat.resolution.width;
|
||||
return streamDivisors.filter(x =>
|
||||
useCameraSettingsStore().isDriverMode
|
||||
|| !useSettingsStore().gpuAccelerationEnabled
|
||||
|| currentResolutionWidth / x < 400);
|
||||
};
|
||||
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
|
||||
|
||||
const cameraResolutions = computed(() => useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(f => `${f.resolution.width} X ${f.resolution.height} at ${f.fps} FPS, ${f.pixelFormat}`));
|
||||
const handleResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
|
||||
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: getNumberOfSkippedDivisors() }, false);
|
||||
useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor = 0;
|
||||
|
||||
if(!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
|
||||
}
|
||||
};
|
||||
|
||||
const streamResolutions = computed(() => {
|
||||
const streamDivisors = getFilteredStreamDivisors();
|
||||
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return streamDivisors
|
||||
.map(x => `${Math.floor(currentResolution.width / x)} X ${Math.floor(currentResolution.height / x)}`);
|
||||
});
|
||||
const handleStreamResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: value + getNumberOfSkippedDivisors() }, false);
|
||||
};
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
|
||||
/>
|
||||
<cv-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)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
: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)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
: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)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
tooltip="Rotates the camera stream"
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({inputImageRotationMode: args}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
label="Resolution"
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => handleResolutionChange(args)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Stream Resolution"
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
25
photon-client/src/components/dashboard/tabs/Map3DTab.vue
Normal file
25
photon-client/src/components/dashboard/tabs/Map3DTab.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import Photon3dVisualizer from "@/components/app/photon-3d-visualizer.vue";
|
||||
|
||||
const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().pipelineResults?.targets || []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="width: 100%">
|
||||
<v-col style="display: flex; align-items: center; justify-content: center">
|
||||
<photon3d-visualizer
|
||||
:targets="trackedTargets"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
178
photon-client/src/components/dashboard/tabs/OutputTab.vue
Normal file
178
photon-client/src/components/dashboard/tabs/OutputTab.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { RobotOffsetType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const isTagPipeline = computed(() => useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco);
|
||||
|
||||
interface MetricItem {
|
||||
header: string,
|
||||
value?: string
|
||||
}
|
||||
|
||||
const offsetPoints = computed<MetricItem[]>(() => {
|
||||
switch (useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode) {
|
||||
case RobotOffsetPointMode.Single:
|
||||
const value = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetSinglePoint);
|
||||
return [{ header: "Offset Point", value: `(${value[0].toFixed(2)}°, ${value[1].toFixed(2)}°)` }];
|
||||
case RobotOffsetPointMode.Dual:
|
||||
const firstPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointA);
|
||||
const firstPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointAArea;
|
||||
const secondPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointB);
|
||||
const secondPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointBArea;
|
||||
return [{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
|
||||
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
|
||||
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
|
||||
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
|
||||
];
|
||||
default:
|
||||
case RobotOffsetPointMode.None:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-select
|
||||
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']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOffsetPointEdge: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-if="!isTagPipeline"
|
||||
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']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
|
||||
label="Show Multiple Targets"
|
||||
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
:switch-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({outputShowMultipleTargets: value}, false)"
|
||||
/>
|
||||
<v-divider />
|
||||
<table
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
class="metrics-table mt-3 mb-3"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in offsetPoints"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in offsetPoints"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
>
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<cv-select
|
||||
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']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({offsetRobotOffsetMode: value}, false)"
|
||||
/>
|
||||
<v-row
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-row v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single">
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual">
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.metrics-table{
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 5px;
|
||||
border: 1px solid white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid;
|
||||
font-weight: normal;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
41
photon-client/src/components/dashboard/tabs/PnPTab.vue
Normal file
41
photon-client/src/components/dashboard/tabs/PnPTab.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { TargetModel } from "@/types/PipelineTypes";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.targetModel"
|
||||
label="Target Model"
|
||||
:items="[
|
||||
{name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter},
|
||||
{name: '2020 High Goal Inner', value: TargetModel.InfiniteRechargeHighGoalInner},
|
||||
{name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget},
|
||||
{name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in},
|
||||
{name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall},
|
||||
{name: '2016 High Goal', value: TargetModel.StrongholdHighGoal},
|
||||
{name: '200mm AprilTag', value: TargetModel.Apriltag_200mm},
|
||||
{name: '6in (16h5) Aruco', value: TargetModel.Aruco6in_16h5},
|
||||
{name: '6in (16h5) AprilTag', value: TargetModel.Apriltag6in_16h5}
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({targetModel: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Contour simplification Percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerDetectionAccuracyPercentage: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
132
photon-client/src/components/dashboard/tabs/TargetsTab.vue
Normal file
132
photon-client/src/components/dashboard/tabs/TargetsTab.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="start"
|
||||
class="pb-4"
|
||||
style="height: 300px;"
|
||||
>
|
||||
<!-- 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
|
||||
>
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem;">
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target Count
|
||||
</th>
|
||||
<th
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco"
|
||||
class="text-center"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Pitch θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Skew θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area %
|
||||
</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th class="text-center">
|
||||
X meters
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Y meters
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Z Angle θ°
|
||||
</th>
|
||||
</template>
|
||||
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Ambiguity %
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(target, index) in useStateStore().pipelineResults?.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco">
|
||||
{{ target.fiducialId }}
|
||||
</td>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.pitch.toFixed(2) }}°</td>
|
||||
<td>{{ target.yaw.toFixed(2) }}°</td>
|
||||
<td>{{ target.skew.toFixed(2) }}°</td>
|
||||
<td>{{ target.area.toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-else-if="useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.pose?.x.toFixed(2) }} m</td>
|
||||
<td>{{ target.pose?.y.toFixed(2) }} m</td>
|
||||
<td>{{ (target.pose?.angle_z * 180.0 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.ambiguity?.toFixed(2) }}%</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
th, td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
photon-client/src/components/dashboard/tabs/ThresholdTab.vue
Normal file
255
photon-client/src/components/dashboard/tabs/ThresholdTab.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
||||
import CvRangeSlider from "@/components/common/cv-range-slider.vue";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
|
||||
|
||||
const averageHue = computed<number>(() => {
|
||||
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
|
||||
let val = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue).reduce((a, b) => a + b, 0);
|
||||
|
||||
if(isHueInverted) val += 180;
|
||||
if (val > 360) val -= 360;
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
// TODO fix cv-range-slider so that store access doesn't need to be deferred
|
||||
const hsvHue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvHue = v
|
||||
});
|
||||
const hsvSaturation = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v
|
||||
});
|
||||
const hsvValue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvValue = v
|
||||
});
|
||||
|
||||
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
|
||||
const handleStreamClick = (event: MouseEvent) => {
|
||||
if(!useStateStore().colorPickingMode || selectedEventMode === 0) return;
|
||||
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = cameraStream.clientWidth;
|
||||
canvas.height = cameraStream.clientHeight;
|
||||
|
||||
// Get the (x, y) position of the click with (0, 0) in the top left corner
|
||||
const rect = cameraStream.getBoundingClientRect();
|
||||
const x = Math.round((event.clientX - rect.left) / rect.width * cameraStream.clientWidth);
|
||||
const y = Math.round((event.clientY - rect.top) / rect.height * cameraStream.clientHeight);
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if(context === null) return;
|
||||
|
||||
context.drawImage(cameraStream as CanvasImageSource, 0, 0, cameraStream.clientWidth, cameraStream.clientHeight);
|
||||
const colorPicker = new ColorPicker(context.getImageData(x, y, 1, 1).data);
|
||||
|
||||
// Calculate HSV values based on the mode
|
||||
let selectedHSVData: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
|
||||
if(selectedEventMode === 1) {
|
||||
selectedHSVData = colorPicker.selectedColorRange();
|
||||
} else {
|
||||
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
|
||||
const currentSaturation = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation);
|
||||
const currentValue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue);
|
||||
|
||||
const currentData: [HSV, HSV] = [
|
||||
[currentHue[0], currentSaturation[0], currentValue[0]],
|
||||
[currentHue[1], currentSaturation[1], currentValue[1]]
|
||||
];
|
||||
|
||||
if(selectedEventMode === 2) {
|
||||
selectedHSVData = colorPicker.expandColorRange(currentData);
|
||||
} else if(selectedEventMode === 3) {
|
||||
selectedHSVData = colorPicker.shrinkColorRange(currentData);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store and backend with the new HSV values
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({
|
||||
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
|
||||
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
|
||||
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
|
||||
}, true);
|
||||
|
||||
disableColorPicking();
|
||||
};
|
||||
|
||||
// Put some default values in case color picking was enabled before the enableColorPicking method is called
|
||||
let inputShowing = true;
|
||||
let outputShowing = false;
|
||||
const enableColorPicking = (mode: 1 | 2 | 3) => {
|
||||
useStateStore().colorPickingMode = true;
|
||||
inputShowing = useCameraSettingsStore().currentPipelineSettings.inputShouldShow;
|
||||
outputShowing = useCameraSettingsStore().currentPipelineSettings.outputShouldShow;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false }, true);
|
||||
selectedEventMode = mode;
|
||||
};
|
||||
const disableColorPicking = () => {
|
||||
useStateStore().colorPickingMode = false;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing }, true);
|
||||
selectedEventMode = 0;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
cameraStream.addEventListener("click", handleStreamClick);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="threshold-modifiers"
|
||||
:style="{'--averageHue': averageHue}"
|
||||
>
|
||||
<cv-range-slider
|
||||
id="hue-slider"
|
||||
v-model="hsvHue"
|
||||
:class="useCameraSettingsStore().currentPipelineSettings.hueInverted ? 'inverted-slider' : 'normal-slider'"
|
||||
label="Hue"
|
||||
tooltip="Describes color"
|
||||
:min="0"
|
||||
:max="180"
|
||||
:slider-cols="interactiveCols"
|
||||
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvHue: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
id="sat-slider"
|
||||
v-model="hsvSaturation"
|
||||
class="normal-slider"
|
||||
label="Saturation"
|
||||
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvSaturation: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
id="value-slider"
|
||||
v-model="hsvValue"
|
||||
class="normal-slider"
|
||||
label="Value"
|
||||
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvValue: value}, false)"
|
||||
/>
|
||||
<cv-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)"
|
||||
/>
|
||||
<v-divider
|
||||
class="mt-3"
|
||||
/>
|
||||
<div>
|
||||
<div class="pt-3 white--text">
|
||||
Color Picker
|
||||
</div>
|
||||
<v-row
|
||||
justify="center"
|
||||
class="mt-3 mb-3"
|
||||
>
|
||||
<template v-if="!useStateStore().colorPickingMode">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-minus
|
||||
</v-icon>
|
||||
Shrink Range
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(1)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus-minus
|
||||
</v-icon>
|
||||
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3: 2)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Expand Range
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
style="width: 30%;"
|
||||
small
|
||||
@click="disableColorPicking"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.threshold-modifiers {
|
||||
--averageHue: 0;
|
||||
}
|
||||
#hue-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#sat-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#value-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
>>> .v-slider__thumb {
|
||||
outline: black solid thin;
|
||||
}
|
||||
.normal-slider >>> .v-slider__track-fill {
|
||||
outline: black solid thin;
|
||||
}
|
||||
|
||||
.inverted-slider >>> .v-slider__track-background {
|
||||
outline: black solid thin;
|
||||
}
|
||||
</style>
|
||||
@@ -1,268 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
id="MapContainer"
|
||||
style="flex-grow:1"
|
||||
>
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
align-self="stretch"
|
||||
>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
style="width:100%;height:100%"
|
||||
/>
|
||||
</v-col>
|
||||
<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>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {
|
||||
ArrowHelper,
|
||||
BoxGeometry,
|
||||
ConeGeometry,
|
||||
Mesh,
|
||||
MeshNormalMaterial,
|
||||
PerspectiveCamera,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Vector3,
|
||||
Color,
|
||||
WebGLRenderer
|
||||
} from "three";
|
||||
|
||||
// This import conflicts with Three.js docs but is required for the build to succeed
|
||||
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls"
|
||||
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
targets: Array,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scene: undefined,
|
||||
cubes: [],
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targets: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.drawTargets();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const scene = new Scene();
|
||||
this.scene = scene;
|
||||
const camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
this.camera = camera;
|
||||
|
||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
||||
this.canvas = canvas;
|
||||
const renderer = new WebGLRenderer({"canvas": canvas});
|
||||
this.renderer = renderer;
|
||||
scene.background = new Color(0xa9a9a9)
|
||||
|
||||
//Set up resize handlers
|
||||
this.onWindowResize();
|
||||
window.addEventListener( 'resize', this.onWindowResize, false );
|
||||
|
||||
//Add the reference frame cues
|
||||
this.refFrameCues = []
|
||||
// coordinate system
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0xff0000,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x00ff00,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x0000ff,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
|
||||
//something that looks vaguely like a camera
|
||||
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);
|
||||
this.refFrameCues.push(camBody)
|
||||
this.refFrameCues.push(camLens)
|
||||
|
||||
const 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;
|
||||
controls.keys = [65, 83, 68];
|
||||
this.controls = controls;
|
||||
|
||||
this.scene.add(...this.refFrameCues)
|
||||
this.resetCamFirstPerson();
|
||||
|
||||
controls.update();
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
|
||||
//camera.updateMatrixWorld();
|
||||
//console.log("================")
|
||||
//console.log(camera.position);
|
||||
//console.log(camera.rotation);
|
||||
//console.log(camera.up);
|
||||
|
||||
}
|
||||
|
||||
this.drawTargets()
|
||||
|
||||
animate();
|
||||
},
|
||||
methods: {
|
||||
drawTargets() {
|
||||
this.scene.remove(...this.cubes)
|
||||
this.cubes = []
|
||||
|
||||
for (const target of this.targets) {
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
let quat = (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(quat);
|
||||
this.cubes.push(cube)
|
||||
|
||||
let arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0xff0000,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.rotation.setFromQuaternion(quat)
|
||||
arrow.rotateZ(-Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
|
||||
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x00ff00,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.rotation.setFromQuaternion(quat)
|
||||
// arrow.rotateX(Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x0000ff,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.setRotationFromQuaternion(quat)
|
||||
arrow.rotateX(Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
}
|
||||
if(this.cubes.length > 0)
|
||||
this.scene.add(...this.cubes);
|
||||
},
|
||||
|
||||
onWindowResize() {
|
||||
const container = document.getElementById("MapContainer");
|
||||
if(container){
|
||||
this.canvas.width = container.clientWidth * 0.95;
|
||||
this.canvas.height = container.clientWidth * 0.85;
|
||||
this.camera.aspect = this.canvas.width / this.canvas.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize( this.canvas.width, this.canvas.height );
|
||||
}
|
||||
},
|
||||
resetCamThirdPerson(){
|
||||
//Sets camera to third person position
|
||||
this.controls.reset();
|
||||
this.camera.position.set(-1.39,-1.09,1.17);
|
||||
this.camera.up.set(0,0,1);
|
||||
this.controls.target.set(4.0,0.0,0.0);
|
||||
this.controls.update();
|
||||
this.scene.add(...this.refFrameCues)
|
||||
},
|
||||
resetCamFirstPerson(){
|
||||
//Sets camera to first person position
|
||||
this.controls.reset();
|
||||
this.camera.position.set(-0.1,0,0);
|
||||
this.camera.up.set(0,0,1);
|
||||
this.controls.target.set(0.0,0.0,0.0);
|
||||
this.controls.update();
|
||||
this.scene.remove(...this.refFrameCues)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,440 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
:timeout="2000"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
<v-row
|
||||
align="center"
|
||||
style="padding: 12px 12px 12px 24px"
|
||||
>
|
||||
<v-col
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
no-gutters
|
||||
class="pa-0"
|
||||
>
|
||||
<CVselect
|
||||
v-if="isCameraNameEdit === false"
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
/>
|
||||
<CVinput
|
||||
v-else
|
||||
v-model="newCameraName"
|
||||
name="Camera"
|
||||
input-cols="9"
|
||||
:error-message="checkCameraName"
|
||||
@Enter="saveCameraNameChange"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
class="pl-5"
|
||||
>
|
||||
<CVicon
|
||||
v-if="isCameraNameEdit === false"
|
||||
color="#c5c5c5"
|
||||
:hover="true"
|
||||
text="mdi-pencil"
|
||||
tooltip="Edit camera name"
|
||||
@click="changeCameraName"
|
||||
/>
|
||||
<div v-else>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="mdi-content-save"
|
||||
tooltip="Save Camera Name"
|
||||
@click="saveCameraNameChange"
|
||||
/>
|
||||
<CVicon
|
||||
color="error"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="mdi-close"
|
||||
tooltip="Discard Changes"
|
||||
@click="discardCameraNameChange"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
no-gutters
|
||||
class="pa-0"
|
||||
>
|
||||
<CVselect
|
||||
v-model="currentPipelineIndex"
|
||||
name="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="$store.getters.isDriverMode"
|
||||
:list="($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)"
|
||||
@input="handleInputWithIndex('currentPipeline', currentPipelineIndex)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
class="pl-5"
|
||||
>
|
||||
<v-menu
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
offset-y
|
||||
auto
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
color="#c5c5c5"
|
||||
v-on="on"
|
||||
>
|
||||
mdi-menu
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list
|
||||
dark
|
||||
dense
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item @click="toPipelineNameChange">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="mdi-pencil"
|
||||
tooltip="Edit pipeline name"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="toCreatePipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="mdi-plus"
|
||||
tooltip="Add new pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteCurrentPipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="red darken-2"
|
||||
:right="true"
|
||||
text="mdi-delete"
|
||||
tooltip="Delete pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="duplicatePipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="mdi-content-copy"
|
||||
tooltip="Duplicate pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="_currentPipelineType >= 0"
|
||||
cols="10"
|
||||
md="11"
|
||||
lg="10"
|
||||
no-gutters
|
||||
class="pa-0"
|
||||
>
|
||||
<CVselect
|
||||
v-model="_currentPipelineType"
|
||||
name="Type"
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:list="['Reflective Tape', 'Colored Shape', 'AprilTag']"
|
||||
@input="e => showTypeDialog(e)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!--pipeline naming dialog-->
|
||||
<v-dialog
|
||||
v-model="namingDialog"
|
||||
dark
|
||||
persistent
|
||||
width="500"
|
||||
height="357"
|
||||
>
|
||||
<v-card
|
||||
dark
|
||||
color="primary"
|
||||
>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
primary-title
|
||||
>
|
||||
{{ isPipelineNameEdit ? "Edit Pipeline Name" : "Create Pipeline" }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<CVinput
|
||||
v-model="newPipelineName"
|
||||
name="Name"
|
||||
:error-message="checkPipelineName"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
:disabled="checkPipelineName !==''"
|
||||
@click="savePipelineNameChange"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="discardPipelineNameChange"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="showPipeTypeDialog"
|
||||
width="600"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>Change Pipeline Type</v-card-title>
|
||||
<v-card-text>
|
||||
Changing the type of this pipeline will erase the current pipeline's settings and replace it with a new {{ ['Reflective', 'Shape'][proposedPipelineType] }} pipeline. <b class="red--text format_bold">You will lose all settings for the pipeline
|
||||
"{{ ($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)[currentPipelineIndex] }}."</b> Are you sure you want to do this?
|
||||
<v-row
|
||||
class="mt-6"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
align="center"
|
||||
>
|
||||
<v-btn
|
||||
class="mr-3"
|
||||
color="red"
|
||||
width="250"
|
||||
@click="e => changePipeType(true)"
|
||||
>
|
||||
Yes, replace this pipeline
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-10"
|
||||
color="secondary"
|
||||
width="250"
|
||||
@click="e => changePipeType(false)"
|
||||
>
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVicon from '../common/cv-icon'
|
||||
import CVselect from '../common/cv-select'
|
||||
import CVinput from '../common/cv-input'
|
||||
|
||||
export default {
|
||||
name: "CameraAndPipelineSelect",
|
||||
components: {
|
||||
CVicon,
|
||||
CVselect,
|
||||
CVinput
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
re: RegExp("^[A-Za-z0-9_ \\-)(]*[A-Za-z0-9][A-Za-z0-9_ \\-)(.]*$"),
|
||||
isCameraNameEdit: false,
|
||||
newCameraName: "",
|
||||
cameraNameError: "",
|
||||
isPipelineNameEdit: false,
|
||||
namingDialog: false,
|
||||
newPipelineName: "",
|
||||
duplicateDialog: false,
|
||||
showPipeTypeDialog: false,
|
||||
proposedPipelineType : 0,
|
||||
pipeIndexToDuplicate: undefined,
|
||||
snack: false,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: "",
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkCameraName() {
|
||||
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
|
||||
if (this.re.test(this.newCameraName)) {
|
||||
for (let cam in this.cameraList) {
|
||||
if (this.cameraList.hasOwnProperty(cam)) {
|
||||
if (this.newCameraName === this.cameraList[cam]) {
|
||||
return "A camera by that name already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "A camera name can only contain letters, numbers, and spaces"
|
||||
}
|
||||
}
|
||||
return "";
|
||||
},
|
||||
checkPipelineName() {
|
||||
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || !this.isPipelineNameEdit) {
|
||||
if (this.re.test(this.newPipelineName)) {
|
||||
for (let pipe in this.$store.getters.pipelineList) {
|
||||
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
|
||||
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
|
||||
return "A pipeline with this name already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "A pipeline name can only contain letters, numbers, and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
currentCameraIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraIndex;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentCameraIndex', value);
|
||||
}
|
||||
},
|
||||
currentPipelineIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
|
||||
}
|
||||
},
|
||||
_currentPipelineType: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.pipelineType - 2;
|
||||
},
|
||||
set(value) {
|
||||
value; // nop, since we have the dialog for this
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTypeDialog(idx) {
|
||||
// Only show the dialog if it's a new type
|
||||
this.showPipeTypeDialog = idx !== this._currentPipelineType;
|
||||
this.proposedPipelineType = idx;
|
||||
},
|
||||
changePipeType(actuallyChange) {
|
||||
const newIdx = actuallyChange ? this.proposedPipelineType : this._currentPipelineType
|
||||
this.handleInputWithIndex('pipelineType', newIdx);
|
||||
this.showPipeTypeDialog = false;
|
||||
},
|
||||
changeCameraName() {
|
||||
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
|
||||
this.isCameraNameEdit = true;
|
||||
},
|
||||
saveCameraNameChange() {
|
||||
if (this.checkCameraName === "") {
|
||||
this.axios.post('http://' + this.$address + '/api/settings/camera/setNickname',
|
||||
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
|
||||
.then(response => {
|
||||
this.$emit('camera-name-changed')
|
||||
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: response.data.text || response.data
|
||||
}
|
||||
this.snack = true;
|
||||
})
|
||||
.catch(error => {
|
||||
this.$emit('camera-name-changed')
|
||||
|
||||
if(error.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: error.response.data.text || error.response.data
|
||||
}
|
||||
} else if(error.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while trying to process the request! The backend didn't respond.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "An error occurred while trying to process the request.",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
})
|
||||
this.discardCameraNameChange();
|
||||
}
|
||||
},
|
||||
discardCameraNameChange() {
|
||||
this.isCameraNameEdit = false;
|
||||
this.newCameraName = "";
|
||||
},
|
||||
toPipelineNameChange() {
|
||||
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
|
||||
this.isPipelineNameEdit = true;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
toCreatePipeline() {
|
||||
this.newPipelineName = "New Pipeline";
|
||||
this.isPipelineNameEdit = false;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
deleteCurrentPipeline() {
|
||||
if (this.$store.getters.pipelineList.length > 1) {
|
||||
this.handleInputWithIndex('deleteCurrentPipeline', {});
|
||||
} else {
|
||||
this.snackbar = true;
|
||||
}
|
||||
},
|
||||
savePipelineNameChange() {
|
||||
if (this.checkPipelineName === "") {
|
||||
if (this.isPipelineNameEdit) {
|
||||
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
|
||||
} else {
|
||||
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this._currentPipelineType]); // 0 for reflective, 1 for colored shape
|
||||
}
|
||||
this.discardPipelineNameChange();
|
||||
}
|
||||
},
|
||||
duplicatePipeline() {
|
||||
this.handleInputWithIndex("duplicatePipeline", this.currentPipelineIndex);
|
||||
},
|
||||
discardPipelineNameChange() {
|
||||
this.namingDialog = false;
|
||||
this.isPipelineNameEdit = false;
|
||||
this.newPipelineName = "";
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="takePointA"
|
||||
>
|
||||
Take Point A
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="takePointB"
|
||||
>
|
||||
Take Point B
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="clearPoints"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DualCalibration",
|
||||
|
||||
methods: {
|
||||
clearPoints() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePointA() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 2, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePointB() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 3, this.$store.state.currentCameraIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
class="black--text"
|
||||
style="width: 100%;"
|
||||
@click="takePoint"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="clearPoint"
|
||||
>
|
||||
Clear Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SingleCalibration",
|
||||
methods: {
|
||||
clearPoint() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePoint() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 1, this.$store.state.currentCameraIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
393
photon-client/src/components/settings/DeviceControlCard.vue
Normal file
393
photon-client/src/components/settings/DeviceControlCard.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import axios from "axios";
|
||||
|
||||
const restartProgram = () => {
|
||||
axios.post("/utils/restartProgram")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully sent program restart request",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
// This endpoint always return 204 regardless of outcome
|
||||
if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const restartDevice = () => {
|
||||
axios.post("/utils/restartDevice")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to restart the device.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
};
|
||||
const handleOfflineUpdate = ({ files } : { files: FileList}) => {
|
||||
useStateStore().showSnackbarMessage({ message: "New Software Upload in Progress...", color: "secondary", timeout: -1 });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("jarData", files[0]);
|
||||
|
||||
axios.post("/utils/offlineUpdate", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = ((progress || 0) * 100.0);
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
AllSettings,
|
||||
HardwareConfig,
|
||||
HardwareSettings,
|
||||
NetworkConfig
|
||||
}
|
||||
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | number>(-1);
|
||||
const importFile = ref(null);
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === -1 || importFile.value === null) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
|
||||
let settingsEndpoint;
|
||||
switch (importType.value) {
|
||||
case ImportType.AllSettings:
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
case ImportType.HardwareConfig:
|
||||
settingsEndpoint = "/hardwareConfig";
|
||||
break;
|
||||
case ImportType.HardwareSettings:
|
||||
settingsEndpoint = "/hardwareSettings";
|
||||
break;
|
||||
case ImportType.NetworkConfig:
|
||||
settingsEndpoint = "/networkConfig";
|
||||
break;
|
||||
}
|
||||
|
||||
axios.post(`/settings${settingsEndpoint}`, formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
})
|
||||
.then(response => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = -1;
|
||||
importFile.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card-title>Device Control</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="4"
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="restartProgram"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon>
|
||||
Restart PhotonVision
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="4"
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="restartDevice"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart-alert
|
||||
</v-icon>
|
||||
Restart Device
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="4"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="openOfflineUpdatePrompt"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-upload
|
||||
</v-icon>
|
||||
Offline Update
|
||||
</v-btn>
|
||||
<input
|
||||
ref="offlineUpdate"
|
||||
type="file"
|
||||
accept=".jar"
|
||||
style="display: none;"
|
||||
@change="handleOfflineUpdate"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider style="margin: 12px 0;" />
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="() => showImportDialog = true"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-import
|
||||
</v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@input="() => {
|
||||
importType = -1;
|
||||
importFile = null;
|
||||
}"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>Import Settings</v-card-title>
|
||||
<v-card-text>
|
||||
Upload and apply previously saved or exported PhotonVision settings to this device
|
||||
<v-row
|
||||
class="mt-6 ml-4"
|
||||
>
|
||||
<cv-select
|
||||
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']"
|
||||
:select-cols="10"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row
|
||||
class="mt-6 ml-4 mr-8"
|
||||
>
|
||||
<v-file-input
|
||||
:disabled="importType === -1"
|
||||
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
|
||||
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
|
||||
@change="(file) => importFile = file"
|
||||
/>
|
||||
</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-btn
|
||||
color="secondary"
|
||||
:disabled="importFile === null"
|
||||
@click="handleSettingsImport"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-import
|
||||
</v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="openExportSettingsPrompt"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-export
|
||||
</v-icon>
|
||||
Export Settings
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="openExportLogsPrompt"
|
||||
>
|
||||
<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://' + address + '/api/utils/logs/photonvision-journalctl.txt'"
|
||||
download="photonvision-journalctl.txt"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="useStateStore().showLogModal = true"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-eye
|
||||
</v-icon>
|
||||
Show log viewer
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
25
photon-client/src/components/settings/LEDControlCard.vue
Normal file
25
photon-client/src/components/settings/LEDControlCard.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card-title>LED Control</v-card-title>
|
||||
<div class="ml-5">
|
||||
<cv-slider
|
||||
v-model="useSettingsStore().lighting.brightness"
|
||||
label="Brightness"
|
||||
class="pt-2"
|
||||
:slider-cols="12"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="args => useSettingsStore().changeLEDBrightness(args)"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
232
photon-client/src/components/settings/MetricsCard.vue
Normal file
232
photon-client/src/components/settings/MetricsCard.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { computed, onBeforeMount, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import CvIcon from "@/components/common/cv-icon.vue";
|
||||
|
||||
interface MetricItem {
|
||||
header: string,
|
||||
value?: string
|
||||
}
|
||||
|
||||
const generalMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
header: "Version",
|
||||
value: useSettingsStore().general.version || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Hardware Model",
|
||||
value: useSettingsStore().general.hardwareModel || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Platform",
|
||||
value: useSettingsStore().general.hardwarePlatform || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "GPU Acceleration",
|
||||
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
||||
}]);
|
||||
const platformMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
header: "CPU Temp",
|
||||
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
|
||||
},
|
||||
{
|
||||
header: "CPU Usage",
|
||||
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" :`${useSettingsStore().metrics.cpuUtil}%`
|
||||
},
|
||||
{
|
||||
header: "CPU Memory Usage",
|
||||
value: useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "GPU Memory Usage",
|
||||
value: useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "CPU Throttling",
|
||||
value: useSettingsStore().metrics.cpuThr || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "CPU Uptime",
|
||||
value: useSettingsStore().metrics.cpuUptime || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Disk Usage",
|
||||
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
|
||||
}
|
||||
]);
|
||||
|
||||
const metricsLastFetched = ref("Never");
|
||||
const fetchMetrics = () => {
|
||||
useSettingsStore()
|
||||
.requestMetricsUpdate()
|
||||
.catch(error => {
|
||||
if(error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Unable to fetch Metrics! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to fetch Metrics."
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
const pad = (num: number): string => {
|
||||
return String(num).padStart(2, "0");
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
fetchMetrics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card-title style="display: flex; justify-content: space-between">
|
||||
<span>Stats</span>
|
||||
<cv-icon
|
||||
icon-name="mdi-reload"
|
||||
color="white"
|
||||
tooltip="Reload Metrics"
|
||||
hover
|
||||
@click="fetchMetrics"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-row class="pt-2 pa-4 ma-0 ml-5 pb-1">
|
||||
<v-card-subtitle
|
||||
class="ma-0 pa-0 pb-2"
|
||||
style="font-size: 16px"
|
||||
>
|
||||
General Metrics
|
||||
</v-card-subtitle>
|
||||
<v-simple-table class="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in generalMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in generalMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
>
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-row class="pa-4 ma-0 ml-5">
|
||||
<v-card-subtitle
|
||||
class="ma-0 pa-0 pb-2"
|
||||
style="font-size: 16px"
|
||||
>
|
||||
Hardware Metrics
|
||||
</v-card-subtitle>
|
||||
<v-simple-table class="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in platformMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in platformMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
>
|
||||
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
|
||||
<span v-else>---</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<div style="text-align: right">
|
||||
<span>Last Fetched: {{ metricsLastFetched }}</span>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metrics-table{
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
font-size: 16px !important;
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid;
|
||||
font-weight: normal;
|
||||
color: white !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px !important;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
thead, tbody {
|
||||
background-color: #006492;
|
||||
}
|
||||
|
||||
:hover {
|
||||
tbody > tr {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
164
photon-client/src/components/settings/NetworkingCard.vue
Normal file
164
photon-client/src/components/settings/NetworkingCard.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { ref } from "vue";
|
||||
import CvInput from "@/components/common/cv-input.vue";
|
||||
import CvRadio from "@/components/common/cv-radio.vue";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { NetworkConnectionType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const settingsValid = ref(true);
|
||||
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
|
||||
// Check if it is a valid team number between 1-9999
|
||||
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
|
||||
// Check if it is a team number longer than 5 digits
|
||||
const badTeamNumberRegex = /^[0-9]{5,}$/;
|
||||
|
||||
if(v === undefined) return false;
|
||||
if (teamNumberRegex.test(v)) return true;
|
||||
if (isValidIPv4(v)) return true;
|
||||
// need to check these before the hostname. "0" and "99999" are valid hostnames, but we don't want to allow then
|
||||
if (v === "0") return false;
|
||||
if (badTeamNumberRegex.test(v)) return false;
|
||||
return isValidHostname(v);
|
||||
};
|
||||
const isValidIPv4 = (v: string | undefined) => {
|
||||
// https://stackoverflow.com/a/17871737
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
|
||||
|
||||
if(v === undefined) return false;
|
||||
return ipv4Regex.test(v);
|
||||
};
|
||||
const isValidHostname = (v: string | undefined) => {
|
||||
// https://stackoverflow.com/a/18494710
|
||||
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
|
||||
|
||||
if(v === undefined) return false;
|
||||
return hostnameRegex.test(v);
|
||||
};
|
||||
|
||||
const saveGeneralSettings = () => {
|
||||
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
|
||||
|
||||
useSettingsStore().saveGeneralSettings()
|
||||
.then(response => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if(error.response) {
|
||||
if (error.status === 504 || changingStaticIp) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${useSettingsStore().network.hostname}:5800?`
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
}
|
||||
} else if(error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card-title>Networking</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="settingsValid"
|
||||
>
|
||||
<cv-input
|
||||
v-model="useSettingsStore().network.ntServerAddress"
|
||||
label="Team Number/NetworkTables Server Address"
|
||||
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
|
||||
:label-cols="3"
|
||||
:disabled="useSettingsStore().network.runNTServer"
|
||||
:rules="[v => isValidNetworkTablesIP(v) || 'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname']"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) && !useSettingsStore().network.runNTServer"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
style="margin: 10px 0"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
|
||||
</v-banner>
|
||||
<cv-radio
|
||||
v-show="useSettingsStore().network.shouldMange"
|
||||
v-model="useSettingsStore().network.connectionType"
|
||||
label="IP Assignment Mode"
|
||||
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
|
||||
:input-cols="12-3"
|
||||
:list="['DHCP','Static']"
|
||||
/>
|
||||
<cv-input
|
||||
v-if="useSettingsStore().network.connectionType === NetworkConnectionType.Static"
|
||||
v-model="useSettingsStore().network.staticIp"
|
||||
:input-cols="12-3"
|
||||
label="Static IP"
|
||||
:rules="[v => isValidIPv4(v) || 'Invalid IPv4 address']"
|
||||
/>
|
||||
<cv-input
|
||||
v-show="useSettingsStore().network.shouldMange"
|
||||
v-model="useSettingsStore().network.hostname"
|
||||
label="Hostname"
|
||||
:input-cols="12-3"
|
||||
:rules="[v => isValidHostname(v) || 'Invalid hostname']"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useSettingsStore().network.runNTServer"
|
||||
label="Run NetworkTables Server (Debugging Only)"
|
||||
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
|
||||
class="mt-3 mb-3"
|
||||
:label-cols="3"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="useSettingsStore().network.runNTServer"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
|
||||
</v-banner>
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="accent"
|
||||
:class="useSettingsStore().network.runNTServer ? 'mt-3' : ''"
|
||||
style="color: black; width: 100%;"
|
||||
:disabled="!settingsValid && !useSettingsStore().network.runNTServer"
|
||||
@click="saveGeneralSettings"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-banner__wrapper {
|
||||
padding: 6px !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user