课程链接:www.bilibili.com/cheese/play…
课程目标
- 使用form表单控制URDF模型
1-产品需求
1.用slider和input 控制关节变换。
1.使用slider和input 变换机器人关节。
2.用复选框控制辅助图形的可见性。
2-代码架构思路
使用vue中的ref 对象将URDF图形与表单相关联,实现URDF图形与form表单的相互影响。
2-1-URDF图形
需要交互控制的URDF图形有2种:
- 机器人 joint
- 辅助对象:坐标系、碰撞体、质心、惯性矩
现在我们还没有创建辅助对象,但这并不妨碍我们先架构代码,
2-2-ref 对象
对应URDF图形,创建相应ref 对象,其数据类型如下:
// Joint 集合的类型
type JointMapType={
// 集合名称
name:string
// 过滤条件
filter:string
// 关节元素集合
eles:{
// 关节名称
name:string
// 关节类型
type:JointType
// 关节当前值
value:number
// 关节下限
lower:number
// 关节上限
upper:number
}[]
}
// 辅助对象集合的类型
type HelperMapType={
// 集合名称
name:string
// 集合的可见性
visible:boolean
// 过滤条件
filter:string
// 辅助元素集合
eles:{
// 元素名称
name:string
// 元素的可见性
visible:boolean
}[]
}
2-3-form 表单
form 表单可以使用element-plus 中组件。
3-创建URDFFormControl 类
URDFFormControl 可以URDF图形、ref 和form 进行统一管理。
- src/robot/URDFFormControl.ts
import { computed, ref } from "vue";
import { Object3D } from 'three';
import { type JointType, URDFRobot } from './URDFClasses';
// Joint 集合的类型
type JointMapType={
// 集合名称
name:string
// 过滤条件
filter:string
// 关节元素集合
eles:{
// 关节名称
name:string
// 关节类型
type:JointType
// 关节当前值
value:number
// 关节下限
lower:number
// 关节上限
upper:number
}[]
}
// 辅助对象集合的类型
type HelperMapType={
// 集合名称
name:string
// 集合的可见性
visible:boolean
// 过滤条件
filter:string
// 辅助元素集合
eles:{
// 元素名称
name:string
// 元素的可见性
visible:boolean
}[]
}
// 所有的集合类型
type AllMapsType={
jointMap:JointMapType
jointAxisMap:HelperMapType
collisionMap:HelperMapType
massMap:HelperMapType
inertiaMap:HelperMapType
}
// 所有的集合类型的key 类型
export type AllKeyType= keyof AllMapsType
// 解析辅助元素
function parseHelperEle(ele:Map<string, Object3D>){
return Array.from(ele.values()).map((item) => ({
name: item.parent?.name||'',
visible: false
}));
}
// URDF辅助对象控制类
class URDFFormControl{
// 辅助目标
robot: URDFRobot|undefined
// 所有的辅助对象集合
helperMaps=ref<AllMapsType>({
jointMap: {
name: 'joint',
filter: '',
eles:[]
},
jointAxisMap: {
name: 'joint axis',
visible: false,
filter: '',
eles: []
},
collisionMap: {
name: 'collision',
visible: false,
filter: '',
eles: []
},
massMap: {
name: 'mass',
visible: false,
filter: '',
eles: []
},
inertiaMap:{
name:'inertia',
visible:false,
filter:'',
eles:[]
}
})
// 当前的辅助对象的类型
currentHelperKey=ref<AllKeyType>('jointMap')
//当前类型的辅助对象集合,会根据filter 过滤
currentHelperEles=computed(() => {
const {helperMaps, currentHelperKey} = this;
let helperData=helperMaps.value[currentHelperKey.value] as JointMapType;
let {filter,eles} = helperData;
if (filter) {
filter = filter.toLowerCase();
eles = eles.filter(ele => {
return ele.name.toLowerCase().indexOf(filter) > -1;
});
}
return eles
})
constructor(robot?:URDFRobot){
robot&&this.setRobot(robot)
}
// 设置辅助目标
setRobot(robot:URDFRobot){
this.robot = robot;
this.init();
}
// 初始化所有的辅助对象的内容
init(robot=this.robot) {
if(!robot){return}
const {helperMaps:{value:helperMaps}}=this
const {userData} = robot;
for(let joint of userData.jointMap.values()){
const {name, userData:{type,value, limit} } = joint;
if(type=='fixed'){
continue
}
helperMaps.jointMap.eles.push({
name,
type,
value,
lower: Number(limit.lower.toFixed(4)),
upper: Number(limit.upper.toFixed(4))
})
}
helperMaps.jointAxisMap.eles = parseHelperEle(userData.jointAxisMap);
helperMaps.collisionMap.eles = parseHelperEle(userData.collisionMap);
helperMaps.massMap.eles = parseHelperEle(userData.massMap);
helperMaps.inertiaMap.eles = parseHelperEle(userData.inertiaMap);
}
// 设置某一类helper 的可见性
setHelpersVisible(bool: boolean){
const {robot}=this
if(!robot){return}
const {currentHelperKey,currentHelperEles}=this
for (let obj of robot.userData[currentHelperKey.value].values()) {
obj.visible = bool;
}
for(let ele of currentHelperEles.value){
if('visible' in ele){
ele.visible=bool
}
}
}
// 设置某一个helper 的可见性
setHelperVisible(bool: boolean,name:string){
const {robot}=this
if(!robot){return}
const {currentHelperKey,helperMaps}=this
if(!bool){
const helper=helperMaps.value[currentHelperKey.value];
('visible' in helper)&&(helper.visible=false)
}
const currentHelperEle=robot.userData[currentHelperKey.value].get(name);
(currentHelperEle)&&(currentHelperEle.visible=bool)
}
// 设置关节的value
setJointValue(value:number,name:string){
for(let ele of this.currentHelperEles.value){
if(ele.name==name){
ele.value=value
break
}
}
}
}
export { URDFFormControl };
4.创建form 元素
1.先安装element-plus。
npm i element-plus
2.在main.ts 中引入element-plus
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
3.在App.vue 页面中,用form 控制URDF的joint 变换量和辅助对象的可见性。
- src/App.vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { RobotVisual } from "./robot/RobotVisual";
import {type HelperKeyType, URDFFormControl } from "./robot/URDFFormControl";
import { URDFRobot } from "./robot/URDFClasses";
import { Search } from "@element-plus/icons-vue";
/* canvas 画布的Ref对象 */
const canvasWrapperRef = ref<HTMLDivElement>();
/* 机器人可视化 */
const hdrURL = "/texture/venice_sunset_1k.hdr";
const urdfURL = "./models/PR2/urdf/PR2.urdf";
let robotVisual = new RobotVisual(hdrURL);
// 辅助控制
const formControl = new URDFFormControl();
const { AllMaps, currentMapKey, currentMapEles } = formControl;
// 机器人
let robot: URDFRobot;
// 加载URDF模型
const urdfLoader= robotVisual.loadURDF(urdfURL,(model:URDFRobot)=>{
robot = model;
formControl.setRobot(model)
});
// 重写PR2 资源路径解析方法
urdfLoader.resolveSubPath=(filename: string)=>{
return filename.replace(
"package://urdf_tutorial",
'./models/PR2'
);
}
const radToDeg=(rad:number)=>{
return (180*rad/Math.PI).toFixed(2)+' °'
}
const sliderFormatTooltip=(rad:number,type:string)=>{
return type=='revolute'?radToDeg(rad):null
}
// 连续渲染
robotVisual.continuousRender();
/* 自适应窗口尺寸 */
window.addEventListener("resize", onResize);
function onResize() {
const canvasWrapper = canvasWrapperRef.value;
canvasWrapper&&robotVisual.resize(canvasWrapper.clientWidth, canvasWrapper.clientHeight);
}
onMounted(() => {
onResize();
const canvasWrapper = canvasWrapperRef.value;
canvasWrapper && canvasWrapper.append(robotVisual.renderer.domElement);
});
onUnmounted(() => {
window.removeEventListener("resize", onResize);
robotVisual.dispose();
});
</script>
<template>
<div id="robotVisual">
<el-menu
:default-active="currentMapKey"
class="el-menu-demo"
mode="horizontal"
:ellipsis="false"
@select="(k: HelperKeyType)=>{currentMapKey=k}"
>
<el-menu-item
v-for="(helper, index) in AllMaps"
:key="index"
:index="index"
>
{{ helper.name }}
</el-menu-item>
</el-menu>
<div id="cont">
<div id="controlPlane" ref="controlPlaneRef">
<div id="helperElesFilter">
<el-input
v-model="AllMaps[currentMapKey].filter"
placeholder="Please input"
:prefix-icon="Search"
>
</el-input>
</div>
<div id="helperEles">
<div v-if="currentMapKey!='jointMap'" class="helper-ele">
<el-checkbox
v-model="AllMaps[currentMapKey].visible"
@change="(bool:boolean)=>{formControl.setHelpersVisible(bool)}"
/>
all
</div>
<div
class="helper-ele"
v-for="item in currentMapEles"
:key="item.name"
>
<div v-if="currentMapKey=='jointMap'" class="joint-ele-row">
<div class="joint-name">{{ item.name }}</div>
<div class="joint-value">
<el-slider
v-if="item.type=='revolute'||item.type=='prismatic'"
v-model="item.value"
show-input
size="small"
:min="item.lower"
:max="item.upper"
:step="0.0001"
@input="robot&&robot.setJointValue(item.name,item.value)"
:format-tooltip ="(val:number)=>sliderFormatTooltip(val,item.type)"
/>
<el-input-number
v-else
v-model="item.value"
:step="0.1"
:precision="4"
size="small"
style="width:100%"
@input="robot&&robot.setJointValue(item.name,item.value)"
>
<template #suffix>
<span>{{ radToDeg(item.value)}}</span>
</template>
</el-input-number>
</div>
</div>
<div v-else class="helper-ele-row">
</div>
</div>
</div>
</div>
<div id="canvasWrapper" ref="canvasWrapperRef">
</div>
</div>
</div>
</template>
<style scoped>
#robotVisual {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
#cont {
display: flex;
flex: 1;
font-size: 14px;
color: #303133;
overflow: hidden;
}
#controlPlane {
width: 300px;
height: 100%;
overflow: hidden;
}
#helperElesFilter {
padding: 12px 18px 6px 12px;
}
#helperEles {
box-sizing: border-box;
height: calc(100vh - 109px);
padding: 9px 0 15px 0;
overflow-y: scroll;
}
.helper-ele {
padding: 3px 12px;
}
.joint-ele-row {
padding-bottom: 12px;
}
.joint-name{
padding-bottom: 6px;
}
.helper-ele-row {
display: flex;
align-items: center;
height: 32px;
}
.helper-ele-row .el-checkbox {
margin-right: 6px !important;
}
.helper-ele-row label {
margin-right: 6px !important;
}
#robotTip {
position: absolute;
background-color: rgba(0, 0, 0, 0.65);
color: #fff;
padding: 6px 9px;
transform: translate(18px, -100%);
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.4) 0 3px 3px;
}
#robotTip p {
margin: 0;
font-size: 13px;
line-height: 24px;
}
#canvasWrapper {
flex: 1;
position: relative;
height: 100%;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: #ddd;
}
</style>
<style>
.el-slider {
--el-slider-button-size: 15px!important;
--el-slider-height: 4px!important;
--el-slider-button-wrapper-offset: -16px!important;
}
.el-slider__runway.show-input {
margin-right: 15px!important;
}
.el-slider__input {
width: 108px!important;
}
</style>
效果如下:
左侧的关节面板可以控制关节的变换。
当前可以使用form 进行变换控制的表单有以下3种:
- continuous:连续关节,可无限旋转
- revolute:旋转关节,可在一定范围内旋转
- prismatic:推拉关节,可在一定范围内移动
总结
这一章,我们说了如何用Form旋转joint,并控制辅助图形的可见性。
下一章,我们会为joint 添加拖拽变换功能。