UI patches (#905)

- Show 0 clients when NT server props are undefined
- Add Prettier 

---------

Co-authored-by: Matthew Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Sriman Achanta
2023-08-31 16:56:58 -04:00
committed by GitHub
parent de394418f6
commit 08892b9e68
55 changed files with 3323 additions and 2808 deletions

View File

@@ -18,7 +18,7 @@ import { TrackballControls } from "three/examples/jsm/controls/TrackballControls
import { type Object3D } from "three";
const props = defineProps<{
targets: PhotonTarget[]
targets: PhotonTarget[];
}>();
let scene: Scene | undefined;
@@ -29,25 +29,20 @@ 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) {
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
scene.remove(...previousTargets);
previousTargets = [];
targets.forEach(target => {
if(target.pose === undefined) return;
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 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);
@@ -72,7 +67,7 @@ const drawTargets = (targets: PhotonTarget[]) => {
previousTargets.push(arrow);
});
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
@@ -80,7 +75,7 @@ const onWindowResize = () => {
const container = document.getElementById("container");
const canvas = document.getElementById("view");
if(container === null || canvas === null || camera === undefined || renderer === undefined) {
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
return;
}
@@ -91,7 +86,7 @@ const onWindowResize = () => {
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
};
const resetCamFirstPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
@@ -100,12 +95,12 @@ const resetCamFirstPerson = () => {
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const resetCamThirdPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
@@ -114,7 +109,7 @@ const resetCamThirdPerson = () => {
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
@@ -124,7 +119,7 @@ onMounted(() => {
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
const canvas = document.getElementById("view");
if(canvas === null) return;
if (canvas === null) return;
renderer = new WebGLRenderer({ canvas: canvas });
scene.background = new Color(0xa9a9a9);
@@ -133,14 +128,20 @@ onMounted(() => {
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));
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 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);
@@ -150,10 +151,7 @@ onMounted(() => {
referenceFrameCues.push(camBody);
referenceFrameCues.push(camLens);
controls = new TrackballControls(
camera,
renderer.domElement
);
controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
@@ -168,7 +166,7 @@ onMounted(() => {
controls.update();
const animate = () => {
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
@@ -185,41 +183,23 @@ onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
drawTargets(props.targets);
});
</script>
<template>
<div
id="container"
style="width: 100%"
>
<div id="container" style="width: 100%">
<v-row>
<v-col
align-self="stretch"
style="display: flex; justify-content: center"
>
<canvas
id="view"
/>
<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-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-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
</v-col>
</v-row>
</div>

View File

@@ -6,14 +6,15 @@ import loadingImage from "@/assets/images/loading.svg";
import type { StyleValue } from "vue/types/jsx";
const props = defineProps<{
streamType: "Raw" | "Processed",
id?: string
streamType: "Raw" | "Processed";
id?: string;
}>();
const src = computed<string>(() => {
const port = useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
const port =
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
if(!useStateStore().backendConnected || port === 0) {
if (!useStateStore().backendConnected || port === 0) {
return loadingImage;
}
@@ -22,9 +23,9 @@ const src = computed<string>(() => {
const alt = computed<string>(() => `${props.streamType} Stream View`);
const style = computed<StyleValue>(() => {
if(useStateStore().colorPickingMode) {
if (useStateStore().colorPickingMode) {
return { cursor: "crosshair" };
} else if(src.value !== loadingImage) {
} else if (src.value !== loadingImage) {
return { cursor: "pointer" };
}
@@ -32,19 +33,12 @@ const style = computed<StyleValue>(() => {
});
const handleClick = () => {
if(!useStateStore().colorPickingMode && src.value !== loadingImage) {
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"
>
<img :id="id" crossorigin="anonymous" :src="src" :alt="alt" :style="style" @click="handleClick" />
</template>

View File

@@ -5,7 +5,9 @@ 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 logs = computed<LogMessage[]>(() =>
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
);
const backendHost = inject<string>("backendHost");
@@ -33,38 +35,22 @@ const handleLogExport = () => {
exportLogFile.value.click();
};
document.addEventListener("keydown", e => {
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "`":
useStateStore().$patch(state => state.showLogModal = !state.showLogModal);
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-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>
<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 -->
@@ -79,34 +65,16 @@ document.addEventListener("keydown", e => {
</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"
>
<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"
>
<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}">
<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>
@@ -118,13 +86,7 @@ document.addEventListener("keydown", e => {
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="() => useStateStore().showLogModal = false"
>
Close
</v-btn>
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -4,8 +4,12 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const compact = computed<boolean>({
get: () => { return useStateStore().sidebarFolded; },
set: (val) => { useStateStore().setSidebarFolded(val); }
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
@@ -13,39 +17,17 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
</script>
<template>
<v-navigation-drawer
dark
app
permanent
:mini-variant="compact || !mdAndUp"
color="primary"
>
<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 :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"
>
<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 link to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
@@ -53,11 +35,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<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 ref="camerasTabOpener" link to="/cameras">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
@@ -65,10 +43,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<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 link to="/settings">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
@@ -76,10 +51,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<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 link to="/docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
@@ -87,46 +59,27 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<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 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-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;">
<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-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-if="useSettingsStore().network.runNTServer" class="text-wrap">
NetworkTables server running for
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
@@ -134,17 +87,11 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span
class="accent--text"
>
<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"
>
<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>
@@ -152,15 +99,8 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<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-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">

View File

@@ -12,41 +12,60 @@ 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);
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)) {
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) {
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);
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));
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 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);
@@ -85,7 +104,8 @@ const downloadCalibBoard = () => {
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;
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;
@@ -118,12 +138,10 @@ const downloadCalibBoard = () => {
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.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`);
};
@@ -132,28 +150,29 @@ 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 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"
});
}
});
});
};
@@ -174,13 +193,14 @@ const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const endCalibration = () => {
if(!useStateStore().calibrationData.hasEnoughImages) {
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
}
showCalibEndDialog.value = true;
// Check if calibration finished cleanly or was canceled
useCameraSettingsStore().endPnPCalibration()
useCameraSettingsStore()
.endPnPCalibration()
.then(() => {
calibSuccess.value = true;
})
@@ -195,22 +215,12 @@ const endCalibration = () => {
<template>
<div>
<v-card
class="pr-6 pb-3"
color="primary"
dark
>
<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"
>
<v-col cols="12" md="6">
<v-form ref="form" v-model="settingsValid">
<cv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
@@ -225,7 +235,9 @@ const endCalibration = () => {
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)"
@input="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)
"
/>
<cv-select
v-model="boardType"
@@ -240,7 +252,7 @@ const endCalibration = () => {
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="5"
/>
<cv-number-input
@@ -248,7 +260,7 @@ const endCalibration = () => {
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']"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="5"
/>
<cv-number-input
@@ -256,54 +268,31 @@ const endCalibration = () => {
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']"
: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
>
<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>
<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"
>
<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.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>
@@ -317,16 +306,14 @@ const endCalibration = () => {
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least {{ useStateStore().calibrationData.minimumImageCount }}
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"
>
<v-col cols="12" class="pt-0">
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
@@ -336,7 +323,7 @@ const endCalibration = () => {
:max="100"
:slider-cols="8"
:step="0.1"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -344,7 +331,9 @@ const endCalibration = () => {
:min="0"
:max="100"
:slider-cols="8"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
@@ -352,7 +341,9 @@ const endCalibration = () => {
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -361,7 +352,7 @@ const endCalibration = () => {
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -370,7 +361,7 @@ const endCalibration = () => {
: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)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -379,7 +370,7 @@ const endCalibration = () => {
: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)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
</v-col>
</v-row>
@@ -388,7 +379,7 @@ const endCalibration = () => {
<v-btn
small
color="secondary"
style="width: 100%;"
style="width: 100%"
:disabled="!settingsValid"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot(true) : startCalibration()"
>
@@ -400,7 +391,7 @@ const endCalibration = () => {
small
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
style="width: 100%;"
style="width: 100%"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
@@ -414,104 +405,70 @@ const endCalibration = () => {
color="accent"
small
outlined
style="width: 100%;"
style="width: 100%"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon left>
mdi-download
</v-icon>
<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>
<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;"
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>
<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"
<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
>
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-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-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 }}!
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"
<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
>
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-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -522,7 +479,8 @@ const endCalibration = () => {
.v-data-table {
text-align: center;
th, td {
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}

View File

@@ -8,43 +8,40 @@ 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;
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 class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Settings</v-card-title>
<div class="ml-5">
<cv-select
@@ -52,29 +49,33 @@ const saveCameraSettings = () => {
label="Camera"
:items="useCameraSettingsStore().cameraNames"
:select-cols="8"
@input="args => {
currentFov = useCameraSettingsStore().cameras[args].fov.value;
useCameraSettingsStore().setCurrentCameraIndex(args);
}"
@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'"
: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>
<br />
<v-btn
style="margin-top:10px"
style="margin-top: 10px"
small
color="secondary"
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
@click="saveCameraSettings"
>
<v-icon left>
mdi-content-save
</v-icon>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</div>

View File

@@ -8,22 +8,25 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
@@ -33,38 +36,30 @@ const fpsTooLow = computed<boolean>(() => {
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
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 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>
<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;"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1">
{{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash; {{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
{{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
</div>
@@ -75,43 +70,24 @@ const fpsTooLow = computed<boolean>(() => {
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode"
label="Driver Mode"
style="margin-left: auto;"
style="margin-left: auto"
color="accent"
class="pt-2"
/>
</div>
</v-card-title>
<div
class="stream-container pb-4"
>
<div class="stream-container pb-4">
<div class="stream">
<photon-camera-stream
v-show="value.includes(0)"
stream-type="Raw"
style="max-width: 100%"
/>
<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%"
/>
<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%"
>
<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"

View File

@@ -1,34 +1,30 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
iconName: string,
color?: string,
tooltip?: string,
right?: boolean,
hover?: boolean
}>(), {
right: false,
hover: false
});
const props = withDefaults(
defineProps<{
iconName: string;
color?: string;
tooltip?: string;
right?: boolean;
hover?: boolean;
}>(),
{
right: false,
hover: false
}
);
defineEmits<{
(e: "click"): void;
}>();
const hoverClass = props.hover ? "hover" : "";
</script>
<template>
<div>
<v-tooltip
:right="right"
:bottom="!right"
nudge-right="10"
:disabled="tooltip === undefined"
>
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
<template #activator="{ on, attrs }">
<v-icon
:class="hoverClass"
:color="color"
v-bind="attrs"
v-on="on"
@click="$emit('click')"
>
<v-icon :class="hoverClass" :color="color" v-bind="attrs" v-on="on" @click="$emit('click')">
{{ iconName }}
</v-icon>
</template>
@@ -38,7 +34,7 @@ const hoverClass = props.hover ? "hover" : "";
</template>
<style scoped>
.hover:hover {
color: white !important;
}
.hover:hover {
color: white !important;
}
</style>

View File

@@ -2,38 +2,40 @@
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 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
(e: "input", value: string): void;
(e: "onEnter", value: string): void;
(e: "onEscape"): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
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")) {
if (!(props.rules || []).some((v) => v(localValue.value) === false || typeof v(localValue.value) === "string")) {
emit("onEnter", localValue.value);
}
break;
@@ -42,20 +44,13 @@ const handleKeydown = ({ key }) => {
break;
}
};
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="labelCols || (12 - inputCols)">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<v-row dense align="center">
<v-col :cols="labelCols || 12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">

View File

@@ -2,42 +2,39 @@
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 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
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", parseFloat(v as unknown as string))
set: (v) => emit("input", parseFloat(v as unknown as string))
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="labelCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col>
<v-text-field

View File

@@ -2,48 +2,40 @@
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 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
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - inputCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-radio-group
v-model="localValue"
row
dark
:mandatory="true"
>
<v-radio-group v-model="localValue" row dark :mandatory="true">
<v-radio
v-for="(radioName, index) in list"
:key="index"

View File

@@ -2,34 +2,37 @@
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 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
(e: "input", value: [number, number]): void;
}>();
const localValue = computed<[number, number]>({
get: ():[number, number] => {
get: (): [number, number] => {
return Object.values(props.value) as [number, number];
},
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const changeFromSlot = (v: number, i: number) => {
@@ -42,15 +45,9 @@ const changeFromSlot = (v: number, i: number) => {
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-range-slider
@@ -79,7 +76,7 @@ const changeFromSlot = (v: number, i: number) => {
:step="step"
type="number"
style="width: 60px"
@input="v => changeFromSlot(v, 0)"
@input="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
@@ -95,7 +92,7 @@ const changeFromSlot = (v: number, i: number) => {
:step="step"
type="number"
style="width: 60px"
@input="v => changeFromSlot(v, 1)"
@input="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>

View File

@@ -3,37 +3,40 @@ import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
interface SelectItem {
name: string | number,
value: string | number,
disabled?: boolean
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 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
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
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) {
if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
}
return props.items.map((v, i) => ({ name: v, value: i }));
@@ -42,15 +45,9 @@ const items = computed<SelectItem[]>(() => {
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - selectCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols">
<v-select

View File

@@ -2,43 +2,40 @@
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 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
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-slider

View File

@@ -2,48 +2,40 @@
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 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
(e: "input", value: boolean): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="(12 - switchCols) || labelCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<v-row dense align="center">
<v-col :cols="12 - switchCols || labelCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || (12 - labelCols)">
<v-switch
v-model="localValue"
dark
:disabled="disabled"
color="#ffd843"
/>
<v-col :cols="switchCols || 12 - labelCols">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" />
</v-col>
</v-row>
</div>

View File

@@ -1,24 +1,15 @@
<script setup lang="ts">
defineProps<{
label?: string,
tooltip?: string
label?: string;
tooltip?: string;
}>();
</script>
<template>
<div>
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="300"
>
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
<template #activator="{ on, attrs }">
<span
style="cursor: text !important;"
class="white--text"
v-bind="attrs"
v-on="on"
>{{ label }}</span>
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>

View File

@@ -38,40 +38,43 @@ const startCameraNameEdit = () => {
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";
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 => {
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: "success",
message: response.data.text || response.data
color: "error",
message: error.response.data.text || error.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);
} 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;
@@ -79,13 +82,13 @@ const cancelCameraNameEdit = () => {
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineNamesWrapper = computed<{ name: string; value: number }[]>(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if(useCameraSettingsStore().isDriverMode) {
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if(useCameraSettingsStore().isCalibrationMode) {
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -98,8 +101,10 @@ const startPipelineNameEdit = () => {
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";
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;
};
@@ -123,7 +128,7 @@ const showCreatePipelineDialog = () => {
};
const createNewPipeline = () => {
const type = newPipelineType.value;
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().createNewPipeline(newPipelineName.value, type);
showPipelineCreationDialog.value = false;
};
@@ -142,18 +147,18 @@ const confirmDeleteCurrentPipeline = () => {
// Pipeline Type Change
const showPipelineTypeChangeDialog = ref(false);
const pipelineTypesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineTypes =[
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) {
if (useCameraSettingsStore().isDriverMode) {
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if(useCameraSettingsStore().isCalibrationMode) {
if (useCameraSettingsStore().isCalibrationMode) {
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -162,17 +167,17 @@ const pipelineTypesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const currentPipelineType = computed<WebsocketPipelineType>({
get: () => {
if(useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if(useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
return pipelineType.value;
},
set: v => {
set: (v) => {
pipelineType.value = v;
}
});
const confirmChangePipelineType = () => {
const type = currentPipelineType.value;
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().changeCurrentPipelineType(type);
showPipelineTypeChangeDialog.value = false;
};
@@ -203,14 +208,9 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
</script>
<template>
<v-card
color="primary"
>
<v-card color="primary">
<v-row style="padding: 12px 12px 0 24px">
<v-col
cols="10"
class="pa-0"
>
<v-col cols="10" class="pa-0">
<cv-select
v-if="!isCameraNameEdit"
v-model="useStateStore().currentCameraIndex"
@@ -222,164 +222,101 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
v-else
v-model="currentCameraName"
class="pt-2"
:input-cols="12-3"
:rules="[v => checkCameraName(v)]"
:input-cols="12 - 3"
:rules="[(v) => checkCameraName(v)]"
label="Camera"
@onEnter="saveCameraNameEdit"
@onEscape="cancelCameraNameEdit"
@on-enter="saveCameraNameEdit"
@on-escape="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 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"
>
<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"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:items="pipelineNamesWrapper"
@input="args => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
/>
<cv-input
v-else
v-model="currentPipelineName"
:input-cols="12-3"
:rules="[v => checkPipelineName(v)]"
:input-cols="12 - 3"
:rules="[(v) => checkPipelineName(v)]"
label="Pipeline"
@onEnter="v => savePipelineNameEdit(v)"
@onEscape="cancelPipelineNameEdit"
@on-enter="(v) => savePipelineNameEdit(v)"
@on-escape="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
>
<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>
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
</template>
<v-list
dark
dense
color="primary"
>
<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"
/>
<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"
/>
<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"
/>
<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
@click="
useCameraSettingsStore().duplicatePipeline(
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex
)
"
>
<v-list-item-title>
<cv-icon
color="#c5c5c5"
:right="true"
icon-name="mdi-content-copy"
tooltip="Duplicate pipeline"
/>
<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"
>
<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"
: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-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
<v-card dark color="primary">
<v-card-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"
:input-cols="12 - 3"
label="Pipeline Name"
:rules="[v => checkPipelineName(v)]"
:rules="[(v) => checkPipelineName(v)]"
/>
<cv-select
v-model="newPipelineType"
:select-cols="12-3"
:select-cols="12 - 3"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="[
@@ -393,87 +330,38 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
Save
</v-btn>
<v-btn
color="error"
@click="cancelPipelineCreation"
>
Cancel
</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-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
<v-card dark color="primary">
<v-card-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-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-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.
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-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>

View File

@@ -8,12 +8,16 @@ import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
@@ -23,17 +27,12 @@ const fpsTooLow = computed<boolean>(() => {
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
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 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"
@@ -49,7 +48,13 @@ const fpsTooLow = computed<boolean>(() => {
<span class="pr-1">
Processing @ {{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
</span>
<span v-if="fpsTooLow && !useCameraSettingsStore().currentPipelineSettings.inputShouldShow && useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective">
<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">
@@ -61,39 +66,16 @@ const fpsTooLow = computed<boolean>(() => {
</v-chip>
</div>
<div>
<v-switch
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
class="pt-2"
/>
<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-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-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>

View File

@@ -15,8 +15,8 @@ import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
interface ConfigOption {
tabName: string,
component: Component
tabName: string;
component: Component;
}
const allTabs = Object.freeze({
@@ -65,20 +65,27 @@ const getTabGroups = (): ConfigOption[][] => {
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
if(smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
return [Object.values(allTabs)];
} else if(mdAndDown || !useStateStore().sidebarFolded) {
} else if (mdAndDown || !useStateStore().sidebarFolded) {
return [
[allTabs.inputTab, allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
[
allTabs.inputTab,
allTabs.thresholdTab,
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if(lgAndDown) {
} else if (lgAndDown) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
[allTabs.inputTab],
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if(xl) {
} else if (xl) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
@@ -91,45 +98,41 @@ const getTabGroups = (): ConfigOption[][] => {
};
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]];
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
));
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) {
if (useCameraSettingsStore().isDriverMode) {
selectedTabs.value[0] = 0;
}
});
</script>
<template>
<v-row
no-gutters
class="tabGroups"
>
<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-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs
v-model="selectedTabs[tabGroupIndex]"
grow
@@ -138,10 +141,7 @@ onBeforeUpdate(() => {
height="48"
slider-color="accent"
>
<v-tab
v-for="(tabConfig, index) in tabGroupData"
:key="index"
>
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
@@ -156,7 +156,8 @@ onBeforeUpdate(() => {
</template>
<style>
.v-slide-group__next--disabled, .v-slide-group__prev--disabled {
.v-slide-group__next--disabled,
.v-slide-group__prev--disabled {
display: none !important;
}
</style>

View File

@@ -5,23 +5,22 @@ import { useStateStore } from "@/stores/StateStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const processingMode = computed<number>({
get: () => useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1: 0,
set: v => {
if(useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
set: (v) => {
if (useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: v === 1 }, true);
}
}
@@ -35,62 +34,30 @@ const processingMode = computed<number>({
color="primary"
style="height: 100%; display: flex; flex-direction: column"
>
<v-row
align="center"
class="pa-3 pb-0"
>
<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"
>
<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-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-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"
>
<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-btn color="secondary" class="fill">
<v-icon>mdi-export</v-icon>
<span>Processed</span>
</v-btn>

View File

@@ -11,7 +11,13 @@ import { useStateStore } from "@/stores/StateStore";
// 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;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -21,7 +27,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({tagFamily: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.decimate"
@@ -31,7 +37,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.blur"
@@ -42,7 +48,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="5"
:step="0.1"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({blur: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.threads"
@@ -52,14 +58,14 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Number of threads spawned by the AprilTag detector"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({threads: value}, false)"
@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)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.decisionMargin"
@@ -69,7 +75,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.numIterations"
@@ -79,7 +85,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
</div>
</template>

View File

@@ -9,7 +9,13 @@ import { useStateStore } from "@/stores/StateStore";
// 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;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -22,7 +28,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.numIterations"
@@ -33,7 +39,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="30"
:max="1000"
:step="5"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.cornerAccuracy"
@@ -44,7 +50,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0.01"
:max="100"
:step="0.01"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerAccuracy: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ cornerAccuracy: value }, false)"
/>
</div>
</template>

View File

@@ -14,34 +14,46 @@ 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
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
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
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) {
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) {
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;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -53,7 +65,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourArea: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<cv-range-slider
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
@@ -64,7 +76,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRatio: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
@@ -72,7 +84,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<cv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
@@ -82,7 +96,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFullness: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
/>
<cv-range-slider
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
@@ -92,7 +106,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
min="0"
max="4000"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourPerimeter: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
@@ -101,7 +115,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSpecklePercentage: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
<cv-slider
@@ -112,7 +128,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeX: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.contourFilterRangeY"
@@ -122,24 +138,24 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeY: value}, false)"
@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)"
: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']"
:items="['None', 'Up', 'Down', 'Left', 'Right']"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourIntersection: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
@@ -150,7 +166,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourShape: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.accuracyPercentage"
@@ -160,7 +176,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({accuracyPercentage: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.circleDetectThreshold"
@@ -170,7 +186,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleDetectThreshold: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
<cv-range-slider
v-model="contourRadius"
@@ -179,7 +197,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRadius: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.maxCannyThresh"
@@ -188,7 +206,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({maxCannyThresh: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.circleAccuracy"
@@ -197,7 +215,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleAccuracy: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
/>
<v-divider class="mt-3" />
</template>
@@ -206,8 +224,8 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

View File

@@ -8,26 +8,38 @@ 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 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);
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 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) {
if (!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
}
};
@@ -35,14 +47,24 @@ const handleResolutionChange = (value: number) => {
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)}`);
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);
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -56,7 +78,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -64,7 +86,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
@@ -72,7 +94,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Auto Exposure"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -82,7 +104,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -92,7 +114,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
: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)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -102,7 +124,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
: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)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
@@ -110,7 +132,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Rotates the camera stream"
:items="cameraRotations"
:select-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({inputImageRotationMode: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
@@ -118,7 +140,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
@input="args => handleResolutionChange(args)"
@input="(args) => handleResolutionChange(args)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
@@ -126,7 +148,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@input="args => handleStreamResolutionChange(args)"
@input="(args) => handleStreamResolutionChange(args)"
/>
</div>
</template>

View File

@@ -16,9 +16,7 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().pipelineRe
</v-row>
<v-row style="width: 100%">
<v-col style="display: flex; align-items: center; justify-content: center">
<photon3d-visualizer
:targets="trackedTargets"
/>
<photon3d-visualizer :targets="trackedTargets" />
</v-col>
</v-row>
</div>

View File

@@ -7,11 +7,15 @@ 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);
const isTagPipeline = computed(
() =>
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
);
interface MetricItem {
header: string,
value?: string
header: string;
value?: string;
}
const offsetPoints = computed<MetricItem[]>(() => {
@@ -24,10 +28,11 @@ const offsetPoints = computed<MetricItem[]>(() => {
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)}%` }
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:
@@ -35,7 +40,13 @@ const offsetPoints = computed<MetricItem[]>(() => {
}
});
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -44,9 +55,11 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
label="Target Offset Point"
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="['Center','Top','Bottom','Left','Right']"
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOffsetPointEdge: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
"
/>
<cv-select
v-if="!isTagPipeline"
@@ -55,7 +68,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
@@ -63,7 +78,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
"
/>
<v-divider />
<table
@@ -71,20 +88,12 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
class="metrics-table mt-3 mb-3"
>
<tr>
<th
v-for="(item, itemIndex) in offsetPoints"
:key="itemIndex"
class="metric-item metric-item-title"
>
<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"
>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
@@ -93,21 +102,23 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
label="Robot Offset Mode"
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="['None','Single Point','Dual Point']"
:items="['None', 'Single Point', 'Dual Point']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({offsetRobotOffsetMode: value}, false)"
@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-row
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
@@ -115,12 +126,14 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
</v-btn>
</v-col>
</v-row>
<v-row v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual">
<v-row
v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
@@ -131,7 +144,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
@@ -143,7 +156,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
style="width: 100%"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -154,7 +167,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
</template>
<style scoped>
.metrics-table{
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;

View File

@@ -6,7 +6,13 @@ 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;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -15,18 +21,18 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
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}
{ 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)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
@@ -35,7 +41,10 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Contour simplification Percentage"
:min="0"
:max="100"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerDetectionAccuracyPercentage: value}, false)"
@input="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
"
/>
</div>
</template>

View File

@@ -6,69 +6,52 @@ import { useStateStore } from "@/stores/StateStore";
<template>
<div>
<v-row
align="start"
class="pb-4"
style="height: 300px;"
>
<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
>
<v-simple-table fixed-header height="100%" dense dark>
<template #default>
<thead style="font-size: 1.25rem;">
<thead style="font-size: 1.25rem">
<tr>
<th class="text-center">
Target Count
</th>
<th class="text-center">Target Count</th>
<th
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco"
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 &theta;&deg;
</th>
<th class="text-center">
Yaw &theta;&deg;
</th>
<th class="text-center">
Skew &theta;&deg;
</th>
<th class="text-center">
Area %
</th>
<th class="text-center">Pitch &theta;&deg;</th>
<th class="text-center">Yaw &theta;&deg;</th>
<th class="text-center">Skew &theta;&deg;</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 &theta;&deg;
</th>
<th class="text-center">X meters</th>
<th class="text-center">Y meters</th>
<th class="text-center">Z Angle &theta;&deg;</th>
</template>
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Ambiguity %
</th>
<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"
>
<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">
<td
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
"
>
{{ target.fiducialId }}
</td>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
@@ -80,9 +63,14 @@ import { useStateStore } from "@/stores/StateStore";
<template v-else-if="useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<td>{{ target.pose?.x.toFixed(2) }}&nbsp;m</td>
<td>{{ target.pose?.y.toFixed(2) }}&nbsp;m</td>
<td>{{ (target.pose?.angle_z * 180.0 / Math.PI).toFixed(2) }}&deg;</td>
<td>{{ ((target.pose?.angle_z * 180.0) / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<template
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<td>{{ target.ambiguity?.toFixed(2) }}%</td>
</template>
</tr>
@@ -100,7 +88,8 @@ import { useStateStore } from "@/stores/StateStore";
text-align: center;
background-color: #006492 !important;
th, td {
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}

View File

@@ -10,7 +10,7 @@ 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 (isHueInverted) val += 180;
if (val > 360) val -= 360;
return val;
@@ -19,23 +19,23 @@ const averageHue = computed<number>(() => {
// 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
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
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
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvValue = v)
});
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
const handleStreamClick = (event: MouseEvent) => {
if(!useStateStore().colorPickingMode || selectedEventMode === 0) return;
if (!useStateStore().colorPickingMode || selectedEventMode === 0) return;
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
const canvas = document.createElement("canvas");
canvas.width = cameraStream.clientWidth;
@@ -43,18 +43,21 @@ const handleStreamClick = (event: MouseEvent) => {
// 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 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;
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) {
let selectedHSVData: [HSV, HSV] = [
[0, 0, 0],
[0, 0, 0]
];
if (selectedEventMode === 1) {
selectedHSVData = colorPicker.selectedColorRange();
} else {
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
@@ -66,19 +69,22 @@ const handleStreamClick = (event: MouseEvent) => {
[currentHue[1], currentSaturation[1], currentValue[1]]
];
if(selectedEventMode === 2) {
if (selectedEventMode === 2) {
selectedHSVData = colorPicker.expandColorRange(currentData);
} else if(selectedEventMode === 3) {
} 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);
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();
};
@@ -90,36 +96,45 @@ 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);
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);
useCameraSettingsStore().changeCurrentPipelineSetting(
{ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing },
true
);
selectedEventMode = 0;
};
onMounted(() => {
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
cameraStream.addEventListener("click", handleStreamClick);
});
onBeforeUnmount(() => {
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
cameraStream.removeEventListener("click", handleStreamClick);
});
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
<div
class="threshold-modifiers"
:style="{'--averageHue': averageHue}"
>
<div class="threshold-modifiers" :style="{ '--averageHue': averageHue }">
<cv-range-slider
id="hue-slider"
v-model="hsvHue"
@@ -130,7 +145,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="180"
:slider-cols="interactiveCols"
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvHue: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
/>
<cv-range-slider
id="sat-slider"
@@ -141,7 +156,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvSaturation: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
/>
<cv-range-slider
id="value-slider"
@@ -152,26 +167,19 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvValue: value}, false)"
@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"
@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"
>
<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"
@@ -179,42 +187,25 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left>
mdi-minus
</v-icon>
<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>
<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)"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left>
mdi-plus
</v-icon>
<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"
>
<v-btn color="accent" class="ma-2 black--text" style="width: 30%" small @click="disableColorPicking">
Cancel
</v-btn>
</template>
@@ -228,18 +219,21 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
--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% );
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#sat-slider >>> .v-slider {
background: linear-gradient( to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100% );
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#value-slider >>> .v-slider {
background: linear-gradient( to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100% );
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {

View File

@@ -5,54 +5,56 @@ 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"
});
}
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"
});
}
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");
@@ -61,55 +63,60 @@ 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 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) {
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({
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."
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
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();
@@ -154,33 +161,34 @@ const handleSettingsImport = () => {
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."
});
}
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;
@@ -189,99 +197,52 @@ const handleSettingsImport = () => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<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>
<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>
<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>
<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"
>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider style="margin: 12px 0;" />
<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>
<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;
}"
@input="
() => {
importType = -1;
importFile = null;
}
"
>
<v-card
color="primary"
dark
>
<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"
>
<v-row class="mt-6 ml-4">
<cv-select
v-model="importType"
label="Type"
@@ -290,14 +251,12 @@ const handleSettingsImport = () => {
:select-cols="10"
/>
</v-row>
<v-row
class="mt-6 ml-4 mr-8"
>
<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"
@change="(file) => (importFile = file)"
/>
</v-row>
<v-row
@@ -305,14 +264,8 @@ const handleSettingsImport = () => {
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>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left> mdi-import </v-icon>
Import Settings
</v-btn>
</v-row>
@@ -320,17 +273,9 @@ const handleSettingsImport = () => {
</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>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left> mdi-export </v-icon>
Export Settings
</v-btn>
<a
@@ -341,17 +286,9 @@ const handleSettingsImport = () => {
target="_blank"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="openExportLogsPrompt"
>
<v-icon left>
mdi-download
</v-icon>
<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 -->
@@ -364,17 +301,9 @@ const handleSettingsImport = () => {
/>
</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>
<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>

View File

@@ -4,11 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card 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
@@ -18,7 +14,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
:slider-cols="12"
:min="0"
:max="100"
@input="args => useSettingsStore().changeLEDBrightness(args)"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card>

View File

@@ -5,27 +5,28 @@ import { useStateStore } from "@/stores/StateStore";
import CvIcon from "@/components/common/cv-icon.vue";
interface MetricItem {
header: string,
value?: string
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"
}]);
{
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",
@@ -33,15 +34,21 @@ const platformMetrics = computed<MetricItem[]>(() => [
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" :`${useSettingsStore().metrics.cpuUtil}%`
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`
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`
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
@@ -60,28 +67,28 @@ const platformMetrics = computed<MetricItem[]>(() => [
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");
};
.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())}`;
});
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
};
onBeforeMount(() => {
@@ -90,47 +97,24 @@ onBeforeMount(() => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<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"
/>
<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-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"
>
<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"
>
<td v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
@@ -138,31 +122,18 @@ onBeforeMount(() => {
</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-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"
>
<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"
>
<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>
@@ -177,7 +148,7 @@ onBeforeMount(() => {
</template>
<style scoped lang="scss">
.metrics-table{
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
@@ -203,7 +174,8 @@ onBeforeMount(() => {
}
.v-data-table {
thead, tbody {
thead,
tbody {
background-color: #006492;
}

View File

@@ -14,7 +14,7 @@ const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a team number longer than 5 digits
const badTeamNumberRegex = /^[0-9]{5,}$/;
if(v === undefined) return false;
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
@@ -26,77 +26,80 @@ 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;
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;
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) {
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: "Error while trying to process the request! The backend didn't respond."
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
useSettingsStore().network.hostname
}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
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 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"
>
<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']"
: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"
v-show="
!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) &&
!useSettingsStore().network.runNTServer
"
rounded
color="red"
text-color="white"
@@ -110,22 +113,22 @@ const saveGeneralSettings = () => {
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']"
: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"
:input-cols="12 - 3"
label="Static IP"
:rules="[v => isValidIPv4(v) || 'Invalid IPv4 address']"
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
/>
<cv-input
v-show="useSettingsStore().network.shouldManage"
v-model="useSettingsStore().network.hostname"
label="Hostname"
:input-cols="12-3"
:rules="[v => isValidHostname(v) || 'Invalid hostname']"
:input-cols="12 - 3"
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
/>
<cv-switch
v-model="useSettingsStore().network.runNTServer"
@@ -147,7 +150,7 @@ const saveGeneralSettings = () => {
<v-btn
color="accent"
:class="useSettingsStore().network.runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
style="color: black; width: 100%"
:disabled="!settingsValid && !useSettingsStore().network.runNTServer"
@click="saveGeneralSettings"
>