用form控制URDF模型

课程链接:www.bilibili.com/cheese/play...

代码链接:github.com/buglas/robo...

课程目标

  • 使用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 对象,其数据类型如下:

typescript 复制代码
// 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
typescript 复制代码
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。

css 复制代码
npm i element-plus

2.在main.ts 中引入element-plus

javascript 复制代码
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
typescript 复制代码
<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 添加拖拽变换功能。

相关推荐
雾酩12 小时前
深拷贝与浅拷贝:一篇彻底讲明白的入门博客
开发语言·前端·javascript
李伟_Li慢慢12 小时前
joint 拖拽变换辅助路径
前端
倔强的石头_12 小时前
零代码复刻 OpenAI DeepResearch:我用 Dify × EdgeOne 打造全球科技热点深度起底神器
前端
李伟_Li慢慢12 小时前
初始项目的搭建
前端·机器人·three.js
李伟_Li慢慢12 小时前
joint的拖拽旋转
前端·机器人·three.js
李伟_Li慢慢12 小时前
joint的拖拽推拉
前端·机器人·three.js
李伟_Li慢慢12 小时前
《机器人Web前端可视化》课程简介
前端·机器人·three.js
Rain50912 小时前
架构解密:mini-cc 的核心设计思路
前端·架构·开源·node.js·ai编程
IMPYLH12 小时前
Linux 的 users 命令
linux·运维·服务器·前端·数据库·bash