# 建图预览功能移植方案 ## 📋 项目概述 将 `robot_map_editor` 的 VSLAM 建图预览功能移植到 `pns-web` 项目中。 **源项目**: robot_map_editor (React + Redux) **目标项目**: pns-web (Vue 2 + Vuex) --- ## 🎯 核心功能需求 ### 1. 实时 3D 点云渲染 - ✅ 使用 Three.js + Potree 渲染关键帧点云 - ✅ 支持百万级点云数据显示 - ✅ 点云高度颜色映射(蓝→绿→黄→红) - ✅ 视锥剔除优化(只渲染可见区域) - ✅ 点云稀释算法(最多显示100帧) ### 2. 实时机器人位姿显示 - ✅ MQTT 订阅位姿信息 - ✅ 3D 机器人模型显示(VRML/GLTF/OBJ) - ✅ 5 种视角模式:俯视、第三人称、第一人称、当前视角、自由视角 ### 3. 交互功能 - ✅ 引导模式(点击地面发送目标点) - ✅ 可视化对象检测显示(3D 边界框) - ✅ 规划轨迹实时显示 - ✅ 建图过程回放 - ✅ 手动控制(摇杆) ### 4. 地图优化 - ✅ 闭环检测与自动地图修正 - ✅ 路径轴线显示 - ✅ 动态地面网格扩展 --- ## 📂 目录结构规划 ``` pns-web/ ├── src/ │ ├── views/map/vslam/ │ │ ├── index.vue # 主页面(对应 VisualSlam/index.js) │ │ ├── components/ │ │ │ ├── VSlamView.vue # 核心 3D 渲染组件 │ │ │ ├── VSlamToolbar.vue # 顶部工具栏 │ │ │ ├── VSlamControlPanel.vue # 右侧控制面板 │ │ │ ├── ManualControl.vue # 摇杆控制 │ │ │ └── MarkList.vue # 标记列表 │ │ └── utils/ │ │ ├── CreateMesh.js # 3D 对象创建工具 │ │ ├── Utils.js # 点云生成和颜色映射 │ │ └── IntersectPointsMesh.js # 视锥内点云管理 │ │ │ ├── api/map/ │ │ └── vslam.js # VSLAM API 接口封装 │ │ │ ├── store/modules/ │ │ └── vslam.js # Vuex 状态管理模块 │ │ │ └── workers/ │ ├── StatisticsWorker.js # 统计信息 Worker │ ├── KeyframeWorker.js # 点云数据 Worker │ └── KeyframeTransWorker.js # 变换矩阵 Worker │ ├── public/ │ └── static_assets/ │ ├── proto/ # Protobuf 定义文件 │ │ ├── pointcloud_pb.js │ │ └── transform_pb.js │ └── models/ # 机器人 3D 模型 │ ├── Robot.wrl │ ├── JQG.glb │ └── drone_costum.obj ``` --- ## 🔧 技术栈映射 | 功能模块 | robot_map_editor (源) | pns-web (目标) | 处理方式 | |---------|---------------------|---------------|---------| | UI 框架 | React + Material-UI | Vue 2 + Element UI | 重写组件 | | 状态管理 | Redux | Vuex | 改写 Actions/Mutations | | 3D 渲染 | Three.js + Potree | Three.js + Potree | 直接迁移 | | HTTP 请求 | Axios | Axios | 适配 API 前缀 | | MQTT 通信 | MQTT.js | 已有 MQTT 组件 | 复用组件 | | 数据格式 | Protobuf | Protobuf | 安装 google-protobuf | | Web Workers | 原生 Worker | 原生 Worker | 直接迁移 | --- ## 📦 依赖安装 ### 需要新增的 npm 包: ```bash npm install three@0.132.2 --save npm install google-protobuf@3.21.4 --save npm install vue-joystick-component@6.2.1 --save # 摇杆控制 ``` ### 已有依赖(无需安装): - ✅ axios (0.28.1) - ✅ mqtt (4.2.1) - ✅ ol (6.15.1) - 用于 2D 地图 - ✅ Potree 库(已在 public/static_assets/libs/potree) --- ## 🔄 核心组件映射关系 ### 1. 主页面组件 | robot_map_editor | pns-web | 说明 | |-----------------|---------|-----| | `pages/VisualSlam/index.js` | `views/map/vslam/index.vue` | 主容器页面 | | `components/VSlamHeaderToolBar` | `views/map/vslam/components/VSlamToolbar.vue` | 顶部工具栏 | | `components/Views/VSlamView` | `views/map/vslam/components/VSlamView.vue` | 核心渲染组件 | | `components/VSlamClosureWindow` | `views/map/vslam/components/VSlamControlPanel.vue` | 控制面板 | ### 2. Redux → Vuex 映射 **Redux State (vslamReducer.js)** ```javascript { mapName: '', currentView: 5, bootModeIsCheck: false, runningState: false, robotPosition: {}, robotVisiable: false, replayState: 0, fullScreen: {}, uiConfig: {} } ``` **Vuex Store (vslam.js)** ```javascript { state: { ... }, // 同上 mutations: { // 对应 Redux Reducers SET_MAP_NAME, SET_CURRENT_VIEW, SET_BOOT_MODE, // ... }, actions: { // 对应 Redux Actions updateMapName, changeView, toggleBootMode, // ... } } ``` ### 3. API 接口映射 **HTTP API 端点(需要在后端实现):** ```javascript // 统计信息 GET /v1/vslam/statistics?map={mapName} 返回: { keyframes: 数量, closures: 闭环数, running: true/false } // 关键帧点云数据 GET /v1/vslam/keyframe/cloud?map={map}&idx={index} 格式: Protobuf (PointcloudType) // 变换矩阵 GET /v1/vslam/keyframe/trans?map={map}&idx={index} 格式: Protobuf (Transform) ``` **MQTT 主题订阅:** ```javascript // 机器人位姿 Topic: /exploration/localization/pose 数据: { pose: { xyz: [x,y,z], rpy: [r,p,y] } } // 可视化对象 Topic: /visualization/object 数据: [ { id, name, type, points, scale, color } ] // 规划轨迹 Topic: /exploration/planning/trajectory 数据: [ [x1,y1,z1], [x2,y2,z2], ... ] ``` --- ## 🚀 实施步骤详解 ### Phase 1: 基础架构搭建(第1-2天) #### 任务 1: 创建主页面组件 ```vue ``` #### 任务 2: 创建 Vuex Store 模块 ```javascript // src/store/modules/vslam.js const state = { mapName: '', currentView: 5, // 1:俯视 2:第三人称 3:第一人称 4:当前视角 5:自由视角 bootModeIsCheck: false, runningState: false, robotPosition: { x: 0, y: 0, z: 0 }, robotVisiable: false, replayState: 0, fullScreen: { name: 'webPage', state: false }, uiConfig: { robotVisiable: true, axesHelper: false, modelName: 'robot', offset: [0, 0, 0] } } const mutations = { SET_MAP_NAME(state, mapName) { state.mapName = mapName }, SET_CURRENT_VIEW(state, view) { state.currentView = view }, SET_BOOT_MODE(state, mode) { state.bootModeIsCheck = mode if (mode) state.currentView = 1 // 引导模式强制俯视图 }, SET_RUNNING_STATE(state, running) { state.runningState = running }, SET_ROBOT_POSITION(state, position) { state.robotPosition = position }, SET_ROBOT_VISIABLE(state, visible) { state.robotVisiable = visible }, // ... 其他 mutations } const actions = { updateMapName({ commit }, mapName) { commit('SET_MAP_NAME', mapName) }, setCurrentView({ commit }, view) { commit('SET_CURRENT_VIEW', view) }, setBootMode({ commit }, mode) { commit('SET_BOOT_MODE', mode) }, // ... 其他 actions } export default { namespaced: true, state, mutations, actions } ``` 注册到主 store: ```javascript // src/store/index.js import vslam from './modules/vslam' export default new Vuex.Store({ modules: { // ... 其他模块 vslam } }) ``` --- ### Phase 2: 核心 3D 渲染组件(第3-5天) #### 任务 3: 创建 VSlamView 核心组件 ```vue ``` --- ### Phase 3: Web Workers 实现(第6天) #### 任务 4: 创建 3 个 Web Workers **1. StatisticsWorker.js** ```javascript // public/workers/StatisticsWorker.js let pollingIntervalId = null self.onmessage = function(event) { const action = event.data.action if (action === 'startPolling') { const { url, interval = 1000 } = event.data if (pollingIntervalId) { clearInterval(pollingIntervalId) } pollingIntervalId = setInterval(() => { fetch(url) .then(response => response.json()) .then(data => { self.postMessage(data) }) .catch(error => { console.error('Statistics fetch error:', error) }) }, interval) } else if (action === 'stopPolling') { if (pollingIntervalId) { clearInterval(pollingIntervalId) pollingIntervalId = null } } } ``` **2. KeyframeWorker.js** ```javascript // public/workers/KeyframeWorker.js self.onmessage = function(event) { const { url } = event.data fetch(url, { method: 'GET', responseType: 'arraybuffer' }) .then(response => response.arrayBuffer()) .then(rsp => { const arry = new Uint8Array(rsp) self.postMessage(arry) }) .catch(error => { console.error('Keyframe fetch error:', error) }) } ``` **3. KeyframeTransWorker.js** ```javascript // public/workers/KeyframeTransWorker.js self.onmessage = function(event) { const { url } = event.data fetch(url, { method: 'GET', responseType: 'arraybuffer' }) .then(response => response.arrayBuffer()) .then(rsp => { const arry = new Uint8Array(rsp) self.postMessage(arry) }) .catch(error => { console.error('Transform fetch error:', error) }) } ``` --- ### Phase 4: 工具类实现(第7天) #### 任务 5 & 6: 创建 CreateMesh.js 和 Utils.js **CreateMesh.js** - 完整复制 robot_map_editor 的 CreateMesh.js **Utils.js** - 完整复制 robot_map_editor 的 Utils.js **IntersectPointsMesh.js** - 完整复制 createIntersectPointsMesh.js 这些文件是纯 JavaScript,无框架依赖,可直接复制。 --- ### Phase 5: Protobuf 配置(第8天) #### 任务 7: 配置 Protobuf **1. 安装依赖** ```bash npm install google-protobuf@3.21.4 --save ``` **2. 复制 proto 文件** ```bash # 复制 .proto 源文件 cp robot_map_editor/proto/*.proto pns-web/proto/ # 复制已生成的 *_pb.js 文件 cp robot_map_editor/src/datastruct/proto/*.js pns-web/src/datastruct/proto/ ``` **3. 在 Vue 组件中使用** ```javascript // src/api/map/vslam.js import kfcloud from '@/datastruct/proto/pointcloud_pb' import kftrans from '@/datastruct/proto/transform_pb' // 解析点云数据 export function parsePointcloudData(arrayBuffer) { const arry = new Uint8Array(arrayBuffer) return kfcloud.PointcloudType.deserializeBinary(arry).toObject() } // 解析变换矩阵 export function parseTransformData(arrayBuffer) { const arry = new Uint8Array(arrayBuffer) return kftrans.Transform.deserializeBinary(arry).toObject() } ``` --- ### Phase 6: API 接口封装(第9天) #### 任务 8: 创建 VSlamAPI.js ```javascript // src/api/map/vslam.js import request from '@/utils/request' import kfcloud from '@/datastruct/proto/pointcloud_pb' import kftrans from '@/datastruct/proto/transform_pb' // 获取 VSLAM 统计信息 export function getVSlamStatistics(mapName) { return request({ url: '/v1/vslam/statistics', baseURL: '/pns', method: 'get', params: { map: mapName } }) } // 获取关键帧点云数据(返回解析后的对象) export function getKeyframePointcloud(mapName, idx) { return request({ url: '/v1/vslam/keyframe/cloud', baseURL: '/pns', method: 'get', params: { map: mapName, idx }, responseType: 'arraybuffer', headers: { 'Content-Type': 'application/x-protobuf', 'Accept': 'application/x-protobuf' } }).then(response => { const arry = new Uint8Array(response) return kfcloud.PointcloudType.deserializeBinary(arry).toObject() }) } // 获取关键帧变换矩阵(返回解析后的对象) export function getKeyframeTrans(mapName, idx) { return request({ url: '/v1/vslam/keyframe/trans', baseURL: '/pns', method: 'get', params: { map: mapName, idx }, responseType: 'arraybuffer', headers: { 'Content-Type': 'application/x-protobuf', 'Accept': 'application/x-protobuf' } }).then(response => { const arry = new Uint8Array(response) return kftrans.Transform.deserializeBinary(arry).toObject() }) } // 获取闭环详情 export function getClosureDetails(mapName, idx) { return request({ url: '/v1/vslam/closure/details', baseURL: '/pns', method: 'get', params: { map: mapName, idx } }) } // URL 生成器(用于 Workers) export function urlVSlamStatistics(mapName) { return `/pns/v1/vslam/statistics?map=${mapName}` } export function urlKeyframePointcloud(mapName, idx) { return `/pns/v1/vslam/keyframe/cloud?map=${mapName}&idx=${idx}` } export function urlKeyframeTrans(mapName, idx) { return `/pns/v1/vslam/keyframe/trans?map=${mapName}&idx=${idx}` } ``` --- ### Phase 7: UI 组件实现(第10-11天) #### 任务 9 & 10: 创建控制面板和工具栏 **VSlamControlPanel.vue** ```vue 播放建图过程 机器人位置 X: {{ robotPosition.x || 0 }} m Y: {{ robotPosition.y || 0 }} m Z: {{ robotPosition.z || 0 }} m ``` **VSlamToolbar.vue** ```vue 首页 {{ mapName }} X: {{ robotPosition.x || 0 }} Y: {{ robotPosition.y || 0 }} Z: {{ robotPosition.z || 0 }} 创建子地图 刷新 ``` --- ### Phase 8: MQTT 集成(第12天) #### 任务 11: 集成 MQTT 通信 在主页面中订阅 MQTT 主题: ```vue ``` --- ### Phase 9: 路由配置(第13天) #### 任务 12: 添加路由 ```javascript // src/router/index.js { path: '/map', component: Layout, children: [ // ... 其他路由 { path: 'vslam/:mapName', name: 'VSlamPreview', component: () => import('@/views/map/vslam/index'), meta: { title: 'VSLAM 建图预览', icon: 'el-icon-view', noCache: true } } ] } ``` 使用方式: ```javascript // 从地图列表页跳转 this.$router.push({ name: 'VSlamPreview', params: { mapName: 'my_map_001' } }) ``` --- ## 🧪 测试验证(第14天) #### 任务 13: 测试和调试 **测试清单:** - [ ] **基础功能** - [ ] 页面正常加载,无报错 - [ ] Potree Viewer 正常初始化 - [ ] 3D 场景渲染正常 - [ ] **数据获取** - [ ] 统计信息轮询正常 - [ ] 点云数据获取正常 - [ ] 变换矩阵获取正常 - [ ] Protobuf 解析正确 - [ ] **点云渲染** - [ ] 点云正确显示 - [ ] 颜色映射正确 - [ ] 点云稀释算法生效 - [ ] 视锥剔除生效 - [ ] **机器人显示** - [ ] 模型加载成功 - [ ] 位姿更新正确 - [ ] MQTT 通信正常 - [ ] **视角控制** - [ ] 5种视角模式切换正常 - [ ] 相机跟随正确 - [ ] 引导模式点击正常 - [ ] **性能测试** - [ ] 大规模点云(>1000帧)流畅度 - [ ] 内存占用正常 - [ ] CPU 占用合理 --- ## 📝 注意事项 ### 1. Potree 库引入 确保在 `public/index.html` 中引入 Potree: ```html ``` ### 2. Three.js 版本 必须使用 Three.js 0.132.2,与 Potree 兼容。 ### 3. Web Workers 路径 Workers 需要放在 `public/workers/` 目录下,通过绝对路径引用。 ### 4. Protobuf 编译 如果修改了 .proto 文件,需要重新编译: ```bash cd proto protoc --js_out=import_style=commonjs,binary:../src/datastruct/proto pointcloud.proto protoc --js_out=import_style=commonjs,binary:../src/datastruct/proto transform.proto ``` ### 5. MQTT 主题前缀 确保使用正确的 MQTT 主题前缀(通过 `this.$mqttPrefix` 访问)。 ### 6. 后端 API 支持 确保后端实现了以下 API: - `/v1/vslam/statistics` - `/v1/vslam/keyframe/cloud` - `/v1/vslam/keyframe/trans` - `/v1/vslam/closure/details` --- ## 🎨 样式规范 使用 Element UI 主题色: - 主色:`#409EFF` - 成功:`#67C23A` - 警告:`#E6A23C` - 危险:`#F56C6C` --- ## 📚 参考文档 - **Potree 文档**: http://potree.org/ - **Three.js 文档**: https://threejs.org/docs/ - **Protocol Buffers**: https://developers.google.com/protocol-buffers - **Element UI**: https://element.eleme.cn/ --- ## 🔗 相关链接 - 源项目:robot_map_editor - 目标项目:pns-web - 移植分支:feature/vslam-preview --- ## 📞 技术支持 如有问题,请联系项目负责人或提交 Issue。 **移植完成预计时间:14天** --- *最后更新:2025-11-06*