mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-03 03:01:40 +00:00
UI Redesign (#22)
* Rework UI into a new, responsive layout * Send two streams (only one is currently downscaled)
This commit is contained in:
committed by
GitHub
parent
aed92e7132
commit
8b46ad1cab
@@ -1,128 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<camera-and-pipeline-select />
|
||||
<v-row no-gutters>
|
||||
<!-- vision tabs -->
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="7"
|
||||
xl="6"
|
||||
class="colsClass pr-8"
|
||||
<v-container
|
||||
class="pa-3"
|
||||
fluid
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="selectedTab"
|
||||
fixed-tabs
|
||||
background-color="#232c37"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
<v-col
|
||||
cols="12"
|
||||
:class="['pb-3 ', $store.getters.isDriverMode ? '' : 'pr-lg-3']"
|
||||
:lg="$store.getters.isDriverMode ? 12 : 8"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-tab>Input</v-tab>
|
||||
<v-tab>Threshold</v-tab>
|
||||
<v-tab>Contours</v-tab>
|
||||
<v-tab>Output</v-tab>
|
||||
<v-tab>3D</v-tab>
|
||||
</v-tabs>
|
||||
<div
|
||||
v-else
|
||||
style="height: 48px"
|
||||
/>
|
||||
<div style="padding-left:30px">
|
||||
<keep-alive>
|
||||
<!-- vision component -->
|
||||
<component
|
||||
:is="selectedComponent"
|
||||
ref="component"
|
||||
v-model="$store.getters.pipeline"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="5"
|
||||
xl="6"
|
||||
class="colsClass"
|
||||
>
|
||||
<div>
|
||||
<!-- camera image tabs -->
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="outputShowThresholded"
|
||||
background-color="#232c37"
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
centered
|
||||
style="padding-bottom:10px"
|
||||
@change="handleTruthyPipelineData('outputShowThresholded')"
|
||||
>
|
||||
<v-tab>Normal</v-tab>
|
||||
<v-tab>Threshold</v-tab>
|
||||
</v-tabs>
|
||||
<div
|
||||
v-else
|
||||
style="height: 58px"
|
||||
/>
|
||||
<!-- camera image stream -->
|
||||
<div class="videoClass">
|
||||
<v-row align="center">
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress"
|
||||
:scale="75"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
<span style=" position: absolute; top: 0.2%; left: 13%;">FPS: {{ parseFloat(fps).toFixed(2) }}</span>
|
||||
<span style=" position: absolute; top: 0.2%; right: 13%;">Latency: {{ parseFloat(latency).toFixed(2) }}ms</span>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-simple-table
|
||||
style="text-align: center;background-color: transparent; display: block;margin: auto"
|
||||
dense
|
||||
dark
|
||||
<v-card-title
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
style="height: 10%;"
|
||||
>
|
||||
Cameras
|
||||
</v-card-title>
|
||||
<v-row
|
||||
align="center"
|
||||
style="height: 90%;"
|
||||
>
|
||||
<v-col
|
||||
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
|
||||
:key="idx"
|
||||
cols="12"
|
||||
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Pitch
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.getters.currentPipelineResults.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td>{{ parseFloat(value['pitch']).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value['yaw']).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value['area']).toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress[idx]"
|
||||
scale="100"
|
||||
max-height="300px"
|
||||
max-height-md="320px"
|
||||
max-height-xl="450px"
|
||||
:alt="'Stream' + idx"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
<span style="position: absolute; top: 2%; left: 2%; font-size: 28px; -webkit-text-stroke: 1px black;">{{ parseFloat(fps).toFixed(2) }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="pb-3"
|
||||
:lg="$store.getters.isDriverMode ? 12 : 4"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
>
|
||||
<camera-and-pipeline-select />
|
||||
</v-card>
|
||||
<v-card
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
class="pl-3 pr-3"
|
||||
>
|
||||
<v-col lg="12">
|
||||
<p style="color: white;">
|
||||
Processing mode:
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="processingMode"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn color="secondary">
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary">
|
||||
<v-icon>mdi-crop-square</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-col lg="12">
|
||||
<p style="color: white;">
|
||||
Stream display:
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="selectedOutputs"
|
||||
:multiple="$vuetify.breakpoint.mdAndUp"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-palette</v-icon>
|
||||
<span>Normal</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-compare</v-icon>
|
||||
<span>Threshold</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row no-gutters>
|
||||
<v-col
|
||||
v-for="(tabs, idx) in tabGroups"
|
||||
:key="idx"
|
||||
:cols="Math.floor(12 / tabGroups.length)"
|
||||
:class="idx != tabGroups.length - 1 ? 'pr-3' : ''"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
class="pr-4 pl-4"
|
||||
>
|
||||
<v-tabs
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
v-model="selectedTabs[idx]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab
|
||||
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || is3D)"
|
||||
:key="i"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-2">
|
||||
<keep-alive>
|
||||
<!-- vision component -->
|
||||
<component
|
||||
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
||||
ref="component"
|
||||
v-model="$store.getters.pipeline"
|
||||
:is3d="is3D"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<!-- snack bar -->
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
@@ -144,12 +189,13 @@
|
||||
|
||||
<script>
|
||||
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
|
||||
import cvImage from '../components/common/cv-image'
|
||||
import InputTab from './PipelineViews/InputTab'
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab'
|
||||
import ContoursTab from './PipelineViews/ContoursTab'
|
||||
import OutputTab from './PipelineViews/OutputTab'
|
||||
import pnpTab from './PipelineViews/3D'
|
||||
import cvImage from '../components/common/cv-image';
|
||||
import InputTab from './PipelineViews/InputTab';
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab';
|
||||
import ContoursTab from './PipelineViews/ContoursTab';
|
||||
import OutputTab from './PipelineViews/OutputTab';
|
||||
import TargetsTab from "./PipelineViews/TargetsTab";
|
||||
import PnPTab from './PipelineViews/PnPTab';
|
||||
|
||||
export default {
|
||||
name: 'CameraTab',
|
||||
@@ -160,26 +206,107 @@
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
pnpTab,
|
||||
TargetsTab,
|
||||
PnPTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
selectedTabs: [0, 0, 0, 0],
|
||||
snackbar: false,
|
||||
is3D: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
outputShowThresholded: {
|
||||
tabGroups: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.outputShowThresholded ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('mutatePipeline', {'outputShowThresholded': !!value});
|
||||
let tabs = {
|
||||
input: {
|
||||
name: "Input",
|
||||
component: "InputTab",
|
||||
},
|
||||
threshold: {
|
||||
name: "Threshold",
|
||||
component: "ThresholdTab",
|
||||
},
|
||||
contours: {
|
||||
name: "Contours",
|
||||
component: "ContoursTab",
|
||||
},
|
||||
output: {
|
||||
name: "Output",
|
||||
component: "OutputTab",
|
||||
},
|
||||
targets: {
|
||||
name: "Target Info",
|
||||
component: "TargetsTab",
|
||||
},
|
||||
pnp: {
|
||||
name: "3D",
|
||||
component: "PnPTab",
|
||||
}
|
||||
};
|
||||
|
||||
// 2D array of tab names and component names; each sub-array is a separate tab group
|
||||
let ret = [];
|
||||
if (this.$vuetify.breakpoint.smAndDown || !this.$store.state.compactMode || this.$store.getters.isDriverMode) {
|
||||
// One big tab group with all the tabs
|
||||
ret[0] = Object.values(tabs);
|
||||
} else if (this.$vuetify.breakpoint.mdAndDown) {
|
||||
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[1] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.lgAndDown) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[2] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.xl) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold];
|
||||
ret[2] = [tabs.contours, tabs.output]
|
||||
ret[3] = [tabs.targets, tabs.pnp];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
selectedComponent: {
|
||||
processingMode: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineIndex + 1) === 0 ? "InputTab" : ["InputTab", "ThresholdTab", "ContoursTab", "OutputTab", "pnpTab"][this.selectedTab];
|
||||
return this.is3D ? 0 : 1;
|
||||
},
|
||||
set(value) {
|
||||
this.is3D = value === 0;
|
||||
}
|
||||
},
|
||||
selectedOutputs: {
|
||||
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
||||
get() {
|
||||
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
||||
let ret = 0;
|
||||
if (!this.$store.getters.isDriverMode) {
|
||||
ret = this.$store.state.selectedOutputs || [0];
|
||||
}
|
||||
|
||||
if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
return ret;
|
||||
} else {
|
||||
return ret[0] || 0;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
let valToCommit = [0];
|
||||
if (value instanceof Array) {
|
||||
// Value is already an array, we don't need to do anything
|
||||
value.sort(); // Sort for visual consistency
|
||||
valToCommit = value;
|
||||
} else if (value) {
|
||||
// Value is assumed to be a number, so we wrap it into an array
|
||||
valToCommit = [value];
|
||||
}
|
||||
this.$store.commit("selectedOutputs", valToCommit);
|
||||
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
|
||||
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
|
||||
}
|
||||
},
|
||||
fps: {
|
||||
@@ -204,6 +331,16 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.colsClass {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -216,5 +353,4 @@
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user