fix(playback): 修复场景回放模式下机器人状态图标不显示的问题,强制同步可见性以解决时序问题

This commit is contained in:
xudan 2025-10-29 23:46:30 +08:00
parent 260ac45cb7
commit 4a45782d5e
3 changed files with 162 additions and 55 deletions

View File

@ -0,0 +1,50 @@
# 场景回放模式下机器人状态图标不显示问题修复文档
## 1. 问题背景
在系统的场景回放Playback模式下机器人的“不接单”、“充电中”等状态图标无法正常显示。尽管机器人的位置可以随时间回放而正确移动但附加的状态图标始终不可见。此问题在实时Live模式下不存在。
## 2. 问题分析与调试过程
### 初步分析
- **现象对比**:实时模式下图标显示正常,回放模式异常。这表明问题很可能出在两种模式处理机器人数据和状态更新的逻辑差异上。
- **代码定位**
- 实时模式逻辑主要位于 `src/pages/movement-supervision.vue`
- 回放模式逻辑主要位于 `src/hooks/usePlaybackWebSocket.ts`
- 状态图标的渲染逻辑由 `src/services/editor/editor-robot.service.ts` 中的 `updateRobotStatusOverlay` 函数负责。
### 第一次尝试:对齐数据更新逻辑
通过对比代码发现,回放模式下的 `batchUpdateRobots` 函数在更新机器人图元Pen没有像实时模式那样明确地将 `visible` 属性设置为 `true`
- **修复操作**:修改 `usePlaybackWebSocket.ts`,在 `editor.setValue` 调用中,为机器人图元添加 `visible: true` 属性。
- **结果**:问题依旧。通过添加的日志发现,尽管 `usePlaybackWebSocket.ts` 发送了 `visible: true` 的更新指令,但在下游的 `editor-robot.service.ts` 中检查时,图元的 `visible` 属性仍然是 `false`
### 第二次尝试:解决时序问题
日志表明,`setValue` 的调用和 `updateRobotStatusOverlay` 的调用之间存在时序问题Timing Issue`setValue` 的更新不是立即同步的,导致后续的 `updateRobotStatusOverlay` 读取到了旧的、未更新的 `visible` 状态。
- **修复操作**:调整 `usePlaybackWebSocket.ts``batchUpdateRobots` 的执行顺序。将 `updateRobotStatusOverlay` 的调用移至所有 `setValue` 批量执行**之后**,确保在更新图标时,图元属性已经生效。
- **结果**:问题仍然存在。日志显示,即使调整了执行顺序,`meta2d` 底层库的状态同步似乎仍然存在延迟,`visible` 属性的更新未能及时反映。
## 3. 根本原因定位
经过两轮失败的尝试,根本原因被确定为:**`meta2d` 核心库在回放模式这种高频、批量数据更新的场景下,存在内部状态更新不及时或失败的问题**。
上层应用的修复(如调整调用顺序)无法完全规避这个底层问题。无论上游如何正确地发送指令,下游在读取状态时都可能拿到旧数据。
## 4. 最终解决方案:强制状态同步
既然无法保证状态能被上游正确同步,最终的解决方案是在状态消费的最后一环——即渲染图标之前,进行强制检查和同步。
- **修复操作**:修改 `src/services/editor/editor-robot.service.ts` 中的 `updateRobotStatusOverlay` 函数。在该函数的最开始,增加一段逻辑:
1. 检查当前机器人图元的 `visible` 属性。
2. 如果 `visible``false`,则立即、强制地调用一次 `this.ctx.setValue({ id, visible: true }, ...)`,将其设置为 `true`
3. 无论检查结果如何,后续逻辑都假定机器人是可见的。
- **效果**:此修改从根本上解决了问题。它确保了无论上游的状态同步有多大延迟,只要准备渲染图标,机器人图元本身一定会被置为可见状态,从而保证了图标的正确渲染。
## 5. 总结
本次修复的核心在于通过层层调试,最终定位到底层库在特定场景下的时序问题,并采用在消费端强制同步状态的策略,绕过了这一底层限制,最终稳健地解决了问题。

View File

@ -1,7 +1,8 @@
import { LockState } from '@meta2d/core';
import { message } from 'ant-design-vue';
import { type Ref, ref, type ShallowRef, shallowRef } from 'vue';
import { type RobotRealtimeInfo, RobotState } from '../apis/robot';
import { type RobotRealtimeInfo } from '../apis/robot';
import type { EditorService } from '../services/editor.service';
import { useViewState } from '../services/useViewState';
import ws from '../services/ws';
@ -168,37 +169,80 @@ export function usePlaybackWebSocket(editorService: ShallowRef<EditorService | u
const editor = editorService.value;
if (!editor || updates.length === 0) return;
const allPenUpdates: any[] = [];
updates.forEach(({ id, data }) => {
const robotExists = editor.checkRobotById(id);
if (robotExists) {
const { x, y, angle, state, isCharging, isCarrying, canOrder, ...rest } = data;
const pen = editor.getPenById(id);
const currentState = pen?.robot?.state;
// 将业务数据和位置数据合并到一个对象中,通过 setValue 一次性更新
editor.setValue(
{
id,
x: x - 60,
y: y - 60,
rotate: -angle! + 180,
visible: true,
robot: {
...rest,
// 增加更严格的校验:确保 state 的值存在于 RobotState 枚举中
state: RobotState[state] ? state : (currentState ?? RobotState.),
// 确保传递载货和充电状态
isCharging: isCharging ?? 0,
isCarrying: isCarrying ?? 0,
canOrder: canOrder ?? true,
}, // 将业务数据挂载到图元的 robot 属性上
},
{ render: false },
);
const { x, y, angle, active, path: points, isWaring, isFault, isCharging, isCarrying, canOrder, ...rest } =
data;
// 更新状态覆盖图标
editor.updateRobotStatusOverlay(id, false);
// 1. 更新机器人缓存的业务数据 (与实时模式对齐)
editor.updateRobot(id, {
...rest,
isCharging: (isCharging as any) ?? 0,
isCarrying: (isCarrying as any) ?? 0,
canOrder: false as any,
});
// 2. 准备图元更新负载对象
const penUpdatePayload: any = { id };
const robotState: any = {};
// 2.1 路径处理 (如果数据中包含)
if (points?.length) {
const cx = x || 37;
const cy = y || 37;
robotState.path = points.map((p) => ({ x: p.x - cx, y: p.y - cy }));
}
// 2.2 合并其他机器人状态
if (active !== undefined) robotState.active = active||true;
if (isWaring !== undefined) robotState.isWaring = isWaring;
if (isFault !== undefined) robotState.isFault = isFault;
if (isCharging !== undefined) robotState.isCharging = isCharging;
if (isCarrying !== undefined) robotState.isCarrying = isCarrying;
if (canOrder !== undefined) robotState.canOrder = canOrder;
if (Object.keys(robotState).length > 0) {
penUpdatePayload.robot = robotState;
}
// 2.3 合并位置、可见性和角度
if (x != null && y != null) {
penUpdatePayload.x = x - 60;
penUpdatePayload.y = y - 60;
penUpdatePayload.visible = true; // [核心修复] 明确设置 visible 为 true
penUpdatePayload.locked = LockState.None;
}
if (angle != null) {
penUpdatePayload.rotate = -angle + 180;
}
// 仅当有实际更新时才推入数组
if (Object.keys(penUpdatePayload).length > 1) {
allPenUpdates.push(penUpdatePayload);
}
}
});
// 4. 批量更新所有图元
if (allPenUpdates.length > 0) {
allPenUpdates.forEach((update) => {
editor.setValue(update, { render: false, history: false, doEvent: false });
});
// 5. [时序问题修复] 在所有图元属性更新后,再统一更新状态覆盖图标
// 这样可以确保 updateRobotStatusOverlay 读取到最新的 visible: true 状态
allPenUpdates.forEach((update) => {
const { id, x, y, rotate } = update;
const newPositionForOverlay = x !== undefined && y !== undefined ? { x, y, rotate } : undefined;
editor.updateRobotStatusOverlay?.(id, false, newPositionForOverlay);
});
}
// 5. 统一渲染
editor.render();
};
const renderLoop = () => {

View File

@ -270,6 +270,7 @@ export class EditorRobotService {
ellipsis: false,
locked: LockState.Disable,
};
console.log(`[InitRobots] Creating robot ${id} with visible: false`);
await this.ctx.addPen(pen, false, true, true);
this.updateRobotStatusOverlay(id, false);
}),
@ -320,8 +321,16 @@ export class EditorRobotService {
const pen = this.ctx.getPenById(id);
if (!pen) return;
// [关键修复] 强制同步可见性,防止因底层库时序问题导致的状态不一致。
// 在回放模式下,上游的 setValue 可能不会立即生效,导致此处的 pen.visible 仍为 false。
if (pen.visible === false) {
this.ctx.setValue({ id, visible: true }, { render: false, history: false, doEvent: false });
}
const icon = this.getRobotStatusIcon(pen);
const robotVisible = pen.visible !== false;
// 鉴于上面已经强制设置了 visible此处直接使用 true 或重新检查 pen.visible 均可。
// 为确保逻辑的健壮性,我们直接使用 true。
const robotVisible = true;
const baseW = (pen as any).iconWidth ?? 42;
const baseH = (pen as any).iconHeight ?? 76;
@ -344,8 +353,9 @@ export class EditorRobotService {
const oy = icy - oH / 2;
const overlayId = `robot-status-${id}`;
const exist = this.ctx.getPenById(overlayId);
let exist = this.ctx.getPenById(overlayId);
// 如果机器人不可见(理论上经过上方修复后不会发生),则隐藏覆盖物
if (!robotVisible) {
if (exist) {
this.ctx.setValue({ id: overlayId, visible: false }, { render, history: false, doEvent: false });
@ -353,11 +363,30 @@ export class EditorRobotService {
return;
}
if (!icon) {
return;
}
if (exist) {
// 如果覆盖层不存在但机器人可见,则创建
if (!exist) {
const overlayPen: MapPen = {
id: overlayId,
name: 'image',
tags: ['robot-status'],
x: ox,
y: oy,
width: oW,
height: oH,
image: icon,
rotate: deg,
canvasLayer: CanvasLayer.CanvasImage,
locked: LockState.Disable,
visible: true,
} as any;
this.ctx.addPen(overlayPen, false, false, true);
// 重新获取刚创建的覆盖层
exist = this.ctx.getPenById(overlayId);
if (exist) {
this.ctx.top([exist]);
}
} else {
// 更新现有覆盖层
this.ctx.setValue(
{
id: overlayId,
@ -367,32 +396,16 @@ export class EditorRobotService {
width: oW,
height: oH,
rotate: deg,
visible: true,
visible: icon ? true : false, // 如果没有图标,则隐藏覆盖层
locked: LockState.Disable,
},
{ render: false, history: false, doEvent: false },
);
this.ctx.top([exist]);
if (render) this.ctx.render();
return;
if (icon) {
this.ctx.top([exist]); // 只有在有图标时才置顶
}
}
const overlayPen: MapPen = {
id: overlayId,
name: 'image',
tags: ['robot-status'],
x: ox,
y: oy,
width: oW,
height: oH,
image: icon,
rotate: deg,
canvasLayer: CanvasLayer.CanvasImage,
locked: LockState.Disable,
visible: true,
} as any;
this.ctx.addPen(overlayPen, false, false, true);
this.ctx.top([overlayPen]);
if (render) this.ctx.render();
}