From 01b22816a66c3b348d22bdd011fb58f07e292fc5 Mon Sep 17 00:00:00 2001 From: xudan Date: Wed, 18 Jun 2025 11:38:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E4=BA=BA=E8=BF=90=E5=8A=A8=E7=9B=91=E6=8E=A7=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E5=88=86=E6=9E=90=E6=96=87=E6=A1=A3=EF=BC=8C?= =?UTF-8?q?=E6=B6=B5=E7=9B=96=E7=BB=84=E4=BB=B6=E6=9E=B6=E6=9E=84=E3=80=81?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E3=80=81=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E6=9C=BA=E5=88=B6=E5=8F=8A=E5=A4=9A=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BD=8D=E7=BD=AE=E4=B8=80=E8=87=B4=E6=80=A7=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E7=9A=84=E8=A7=A3=E5=86=B3=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 机器人运动监控组件详细分析.md | 632 ++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 机器人运动监控组件详细分析.md diff --git a/机器人运动监控组件详细分析.md b/机器人运动监控组件详细分析.md new file mode 100644 index 0000000..0abccaa --- /dev/null +++ b/机器人运动监控组件详细分析.md @@ -0,0 +1,632 @@ +# 机器人运动监控组件详细分析 + +## 1. 组件架构概述 + +### 1.1 核心组件结构 + +`movement-supervision.vue` 是机器人运动监控的主要组件,负责实时显示机器人在场景中的位置和状态。 + +```typescript +// 组件核心属性 +type Props = { + sid: string; // 场景ID + id?: string; // 机器人组ID(可选) +}; +``` + +### 1.2 依赖服务架构 + +- **EditorService**: 基于Meta2D的场景编辑器服务 +- **WebSocket服务**: 提供实时数据通信 +- **场景API服务**: 处理场景数据的增删改查 + +## 2. 组件生命周期详解 + +### 2.1 组件初始化流程 + +```typescript +onMounted(async () => { + await readScene(); // 步骤1: 加载场景数据 + await editor.value?.initRobots(); // 步骤2: 初始化机器人 + await monitorScene(); // 步骤3: 建立WebSocket监控 +}); +``` + +#### 步骤1: readScene() - 场景数据加载 + +```typescript +const readScene = async () => { + const res = props.id ? await getSceneByGroupId(props.id, props.sid) : await getSceneById(props.sid); + title.value = res?.label ?? ''; + editor.value?.load(res?.json); +}; +``` + +**关键问题点**: 每个页面实例都独立调用API获取场景数据,可能导致: + +- 不同时间点获取的数据版本不一致 +- 网络延迟造成的数据获取时差 +- 场景数据在获取期间被其他页面修改 + +#### 步骤2: initRobots() - 机器人初始化 + +```typescript +public async initRobots(): Promise { + await Promise.all( + this.robots.map(async ({ id, label, type }) => { + const pen: MapPen = { + ...this.#mapRobotImage(type, true), + id, + name: 'robot', + tags: ['robot'], + x: 0, // 关键: 初始位置固定为(0,0) + y: 0, // 关键: 初始位置固定为(0,0) + width: 74, + height: 74, + lineWidth: 1, + robot: { type }, + visible: false, // 关键: 初始状态为不可见 + text: label, + textTop: -24, + whiteSpace: 'nowrap', + ellipsis: false, + locked: LockState.Disable, + }; + await this.addPen(pen, false, true, true); + }), + ); +} +``` + +**问题分析**: + +- 所有机器人初始位置都设为`(0,0)` +- 初始状态为`visible: false`,需要WebSocket数据才能显示 +- 如果WebSocket连接延迟,不同页面的机器人可能长时间处于不可见状态 + +#### 步骤3: monitorScene() - WebSocket监控建立 + +```typescript +const monitorScene = async () => { + client.value?.close(); // 关闭之前的连接 + const ws = await monitorSceneById(props.sid); // 创建新连接 + if (isNil(ws)) return; + + ws.onmessage = (e) => { + const { id, x, y, active, angle, path, ...rest } = JSON.parse(e.data || '{}'); + + if (!editor.value?.checkRobotById(id)) return; // 验证机器人存在 + + editor.value?.updateRobot(id, rest); // 更新机器人基本信息 + + if (isNil(x) || isNil(y)) { + // 关键逻辑: 无位置信息时隐藏机器人 + editor.value.updatePen(id, { visible: false }); + } else { + // 关键逻辑: 有位置信息时更新位置并显示 + editor.value.refreshRobot(id, { x, y, active, angle, path }); + } + }; + client.value = ws; +}; +``` + +## 3. 机器人实时移动机制深度分析 + +### 3.1 WebSocket消息处理流程 + +每当接收到WebSocket消息时,会执行以下处理逻辑: + +1. **消息解析**: 将JSON字符串解析为`RobotRealtimeInfo`对象 +2. **机器人验证**: 调用`checkRobotById(id)`验证机器人是否存在 +3. **基本信息更新**: 调用`updateRobot(id, rest)`更新电量、状态等信息 +4. **位置处理**: 根据坐标是否存在进行不同处理 + +### 3.2 位置更新核心逻辑: refreshRobot() + +```typescript +public refreshRobot(id: RobotInfo['id'], info: Partial): void { + const pen = this.getPenById(id); + const { rotate: or, robot } = pen ?? {}; + if (!robot?.type) return; + + // 获取当前机器人位置 + const { x: ox, y: oy } = this.getPenRect(pen!); + + // 解析新的位置信息(默认值为37,37是机器人中心点) + const { x: cx = 37, y: cy = 37, active, angle, path: points } = info; + + // 关键坐标转换: 从中心点坐标转换为左上角坐标 + const x = cx - 37; // 机器人宽度74,中心偏移37 + const y = cy - 37; // 机器人高度74,中心偏移37 + + const rotate = angle ?? or; // 角度更新 + + // 路径坐标转换 + const path = + points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ?? // 新路径相对于机器人中心 + robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y })); // 旧路径坐标调整 + + const o = { ...robot, ...omitBy({ active, path }, isNil) }; + + if (isNil(active)) { + // active为null时,只更新位置不改变图标 + this.setValue( + { id, x, y, rotate, robot: o, visible: true }, + { render: true, history: false, doEvent: false } + ); + } else { + // active有值时,同时更新图标状态(运行/停止状态图标不同) + this.setValue( + { id, ...this.#mapRobotImage(robot.type, active), x, y, rotate, robot: o, visible: true }, + { render: true, history: false, doEvent: false } + ); + } +} +``` + +### 3.3 机器人图标映射逻辑 + +```typescript +#mapRobotImage( + type: RobotType, + active?: boolean, +): Required> { + const theme = this.data().theme; + const image = import.meta.env.BASE_URL + + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`); + return { + image, + iconWidth: 34, + iconHeight: 54, + iconTop: -5 + }; +} +``` + +### 3.4 机器人绘制函数 + +```typescript +function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void { + const theme = sTheme.editor; + const { lineWidth: s = 1 } = pen.calculative ?? {}; + const { x = 0, y = 0, width: w = 0, height: h = 0, rotate: deg = 0 } = pen.calculative?.worldRect ?? {}; + const { active, path } = pen.robot ?? {}; + + if (!active) return; // 关键: 非活跃状态不绘制路径 + + const ox = x + w / 2; // 机器人中心X坐标 + const oy = y + h / 2; // 机器人中心Y坐标 + + ctx.save(); + // 绘制机器人本体(椭圆) + ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2); + ctx.fillStyle = get(theme, 'robot.fill') ?? ''; + ctx.fill(); + ctx.strokeStyle = get(theme, 'robot.stroke') ?? ''; + ctx.stroke(); + + // 绘制运动路径 + if (path?.length) { + ctx.strokeStyle = get(theme, 'robot.line') ?? ''; + ctx.lineCap = 'round'; + ctx.lineWidth = s * 4; + ctx.setLineDash([s * 5, s * 10]); // 虚线样式 + ctx.translate(ox, oy); + ctx.rotate((-deg * Math.PI) / 180); // 根据机器人角度旋转 + + // 绘制路径线条 + ctx.beginPath(); + ctx.moveTo(0, 0); + path.forEach((d) => ctx.lineTo(d.x * s, d.y * s)); + ctx.stroke(); + + // 绘制路径终点箭头 + const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {}; + const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {}; + const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI; + ctx.setLineDash([0]); + ctx.translate(ex1 * s, ey1 * s); + ctx.beginPath(); + ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10); + ctx.lineTo(0, 0); + ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10); + ctx.stroke(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + ctx.restore(); +} +``` + +## 4. 多页面位置不一致问题深度分析 + +### 4.1 根本原因:缺乏全局状态同步机制 + +每个页面实例都是完全独立的,具体表现为: + +1. **独立的EditorService实例** + + - 每个页面创建独立的`new EditorService(container.value!)` + - 各自维护独立的机器人状态映射`#robotMap` + - 无法共享机器人位置信息 + +2. **独立的WebSocket连接** + - 每个页面调用`monitorSceneById(props.sid)`创建独立连接 + - 服务器可能向不同连接推送不同时间点的数据 + - 网络延迟导致消息到达时间不同 + +### 4.2 具体问题场景分析 + +#### 场景1: 初始化时间差异 + +```typescript +// 页面A在时间T1执行 +onMounted(async () => { + await readScene(); // T1时刻的场景数据 + await initRobots(); // 创建机器人,位置(0,0),visible:false + await monitorScene(); // T1+100ms建立WebSocket +}); + +// 页面B在时间T2执行(T2 > T1) +onMounted(async () => { + await readScene(); // T2时刻的场景数据(可能已更新) + await initRobots(); // 创建机器人,位置(0,0),visible:false + await monitorScene(); // T2+80ms建立WebSocket +}); +``` + +**结果**: 两个页面获取的初始场景数据可能不同,机器人列表或配置存在差异。 + +#### 场景2: WebSocket消息时序差异 + +```typescript +// WebSocket消息处理逻辑 +ws.onmessage = (e) => { + const { id, x, y, active, angle, path, ...rest } = JSON.parse(e.data || '{}'); + + if (isNil(x) || isNil(y)) { + // 关键问题: 无坐标消息会隐藏机器人 + editor.value.updatePen(id, { visible: false }); + } else { + editor.value.refreshRobot(id, { x, y, active, angle, path }); + } +}; +``` + +**问题分析**: + +- 页面A先收到有坐标的消息,机器人显示在位置(100, 200) +- 页面B后收到无坐标的消息,机器人被隐藏 +- 页面C收到旧的坐标消息,机器人显示在位置(80, 150) + +#### 场景3: 坐标转换精度问题 + +```typescript +// refreshRobot中的坐标转换 +const { x: cx = 37, y: cy = 37, active, angle, path: points } = info; +const x = cx - 37; // 默认值37导致的问题 +const y = cy - 37; + +// 当服务器发送的坐标为null/undefined时 +// cx和cy都会使用默认值37,导致机器人位置为(0,0) +``` + +**问题**: 不同页面接收到的消息中坐标字段可能为`null`、`undefined`或有效数值,默认值处理导致位置计算不一致。 + +#### 场景4: 机器人状态检查差异 + +```typescript +if (!editor.value?.checkRobotById(id)) return; + +// checkRobotById实现 +public checkRobotById(id: RobotInfo['id']): boolean { + return this.#robotMap.has(id); +} +``` + +**问题**: 不同页面的`#robotMap`内容可能不同,导致某些页面忽略特定机器人的更新消息。 + +### 4.3 路径绘制不一致问题 + +```typescript +// 路径坐标转换逻辑 +const path = + points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ?? // 新路径处理 + robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y })); // 旧路径处理 +``` + +**问题分析**: + +1. 新路径使用`p.x - cx, p.y - cy`进行坐标转换 +2. 旧路径使用`p.x + ox! - x, p.y + oy! - y`进行坐标转换 +3. 两种转换方式在特定情况下可能产生不同结果 +4. 不同页面可能处于新旧路径的不同阶段 + +### 4.4 渲染状态不同步 + +```typescript +// setValue方法的渲染参数 +this.setValue( + { id, x, y, rotate, robot: o, visible: true }, + { render: true, history: false, doEvent: false }, // 立即渲染,不记录历史 +); +``` + +**问题**: + +- `render: true`表示立即重新渲染 +- 不同页面的渲染时机不同步 +- 可能出现某个页面正在渲染时收到新消息的情况 + +## 5. 解决方案详细设计 + +### 5.1 方案一: 全局状态管理器 + +```typescript +/** + * 全局机器人状态管理器 + * 单例模式,确保所有页面共享同一份状态 + */ +class GlobalRobotStateManager { + private static instance: GlobalRobotStateManager; + + // 存储所有机器人的最新状态 + private robotStates = new Map(); + + // 订阅者列表,用于通知状态变化 + private subscribers = new Set<(robotId: string, info: RobotRealtimeInfo) => void>(); + + // 连接管理,避免重复连接 + private connections = new Map(); + + static getInstance(): GlobalRobotStateManager { + if (!this.instance) { + this.instance = new GlobalRobotStateManager(); + } + return this.instance; + } + + /** + * 订阅机器人状态变化 + */ + subscribe(callback: (robotId: string, info: RobotRealtimeInfo) => void): () => void { + this.subscribers.add(callback); + + // 立即推送当前所有机器人状态 + this.robotStates.forEach((info, robotId) => { + callback(robotId, info); + }); + + // 返回取消订阅函数 + return () => this.subscribers.delete(callback); + } + + /** + * 更新机器人状态并通知所有订阅者 + */ + updateRobotState(robotId: string, info: RobotRealtimeInfo): void { + // 合并状态更新 + const currentState = this.robotStates.get(robotId) || ({} as RobotRealtimeInfo); + const newState = { ...currentState, ...info }; + + this.robotStates.set(robotId, newState); + + // 通知所有订阅者 + this.subscribers.forEach((callback) => { + try { + callback(robotId, newState); + } catch (error) { + console.error('机器人状态更新回调执行失败:', error); + } + }); + } + + /** + * 获取或创建WebSocket连接(复用连接) + */ + async getOrCreateConnection(sceneId: string): Promise { + // 检查现有连接 + const existingConnection = this.connections.get(sceneId); + if (existingConnection && existingConnection.readyState === WebSocket.OPEN) { + return existingConnection; + } + + try { + const ws = await monitorSceneById(sceneId); + if (!ws) return null; + + // 设置消息处理 + ws.onmessage = (e) => { + try { + const robotInfo = JSON.parse(e.data || '{}') as RobotRealtimeInfo; + this.updateRobotState(robotInfo.id, robotInfo); + } catch (error) { + console.error('WebSocket消息解析失败:', error); + } + }; + + // 设置连接关闭处理 + ws.onclose = () => { + this.connections.delete(sceneId); + }; + + // 存储连接 + this.connections.set(sceneId, ws); + return ws; + } catch (error) { + console.error('创建WebSocket连接失败:', error); + return null; + } + } +} +``` + +### 5.2 方案二: 改进的组件实现 + +```typescript +// 改进后的movement-supervision.vue核心逻辑 + +``` + +### 5.3 方案三: EditorService增强 + +```typescript +// 为EditorService添加状态缓存和同步机制 +export class EditorService extends Meta2d { + // 添加状态缓存 + private robotStateCache = new Map(); + + /** + * 改进的坐标转换方法 + */ + private normalizeCoordinates(info: Partial): { x: number; y: number } | null { + const { x: cx, y: cy } = info; + + // 严格的坐标验证 + if (typeof cx !== 'number' || typeof cy !== 'number' || isNaN(cx) || isNaN(cy) || cx < 0 || cy < 0) { + return null; // 返回null表示无效坐标 + } + + // 坐标转换:从中心点转换为左上角 + return { + x: cx - 37, // 机器人宽度74,中心偏移37 + y: cy - 37, // 机器人高度74,中心偏移37 + }; + } + + /** + * 改进的refreshRobot方法 + */ + public refreshRobot(id: RobotInfo['id'], info: Partial): void { + const pen = this.getPenById(id); + const { rotate: or, robot } = pen ?? {}; + if (!robot?.type) return; + + // 使用改进的坐标转换 + const coords = this.normalizeCoordinates(info); + + // 无效坐标处理 + if (!coords) { + this.setValue({ id, visible: false }, { render: true, history: false, doEvent: false }); + return; + } + + const { x, y } = coords; + const { active, angle, path: points } = info; + const rotate = angle ?? or; + + // 路径处理优化 + let path: Point[] | undefined; + if (points && Array.isArray(points)) { + // 新路径:相对于机器人中心的坐标 + path = points.map((p) => ({ + x: (p.x || 0) - (info.x || 37), + y: (p.y || 0) - (info.y || 37), + })); + } else if (robot.path) { + // 保持原有路径,但需要调整坐标 + const { x: ox, y: oy } = this.getPenRect(pen!); + path = robot.path.map((p) => ({ + x: p.x + ox - x, + y: p.y + oy - y, + })); + } + + const robotState = { ...robot, ...omitBy({ active, path }, isNil) }; + + // 根据active状态决定渲染方式 + if (typeof active === 'boolean') { + // 有明确的活跃状态,更新图标 + this.setValue( + { + id, + ...this.#mapRobotImage(robot.type, active), + x, + y, + rotate, + robot: robotState, + visible: true, + }, + { render: true, history: false, doEvent: false }, + ); + } else { + // 无活跃状态信息,只更新位置 + this.setValue( + { id, x, y, rotate, robot: robotState, visible: true }, + { render: true, history: false, doEvent: false }, + ); + } + } +} +``` + +## 6. 性能优化建议 + +### 6.1 渲染优化 + +- 使用`requestAnimationFrame`批量处理渲染更新 +- 实现视口裁剪,只渲染可见区域的机器人 +- 添加机器人状态变化的diff检测,避免无效渲染 + +### 6.2 内存管理 + +- 定期清理过期的机器人状态缓存 +- 使用WeakMap存储临时状态,避免内存泄漏 +- 在组件卸载时正确清理WebSocket连接和事件监听器 + +### 6.3 网络优化 + +- 实现WebSocket连接池,复用连接 +- 添加消息压缩,减少网络传输量 +- 使用心跳机制检测连接状态 + +## 7. 总结 + +机器人运动监控组件的多页面位置不一致问题主要源于: + +1. **架构设计缺陷**: 缺乏全局状态管理,每个页面独立维护状态 +2. **WebSocket连接独立性**: 多个连接可能接收到不同时间点的数据 +3. **初始化时序问题**: 不同页面的初始化时间不同,导致状态基线不一致 +4. **坐标转换逻辑**: 默认值处理和坐标转换在边界情况下存在问题 +5. **状态验证不足**: 缺乏对接收数据的有效性验证 + +通过实施全局状态管理、WebSocket连接复用、状态缓存机制和坐标转换优化等解决方案,可以有效解决这些问题,确保多页面间机器人位置的一致性。